From 0a64c8163445e27f5123fdcf083fb3a5449b5c49 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 20 Jun 2026 03:43:08 +0800
Subject: [PATCH] Fix dark Space hover readability
---
C3.tools/round1-main-ui-qa.sh | 4
C3.tools/round1-horizontal-waterfall-fixture-qa.sh | 25 +++
C3.tools/round1-space-lane-dark-hover-fixture-qa.sh | 390 ++++++++++++++++++++++++++++++++++++++++++++++++
C1.source/Sources/Aligner/QuickSwitchRootView.swift | 50 ++++-
C1.source/Resources/Aligner-Info.plist | 4
CHANGELOG.md | 5
6 files changed, 462 insertions(+), 16 deletions(-)
diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index 0c386e9..f821f95 100644
--- a/C1.source/Resources/Aligner-Info.plist
+++ b/C1.source/Resources/Aligner-Info.plist
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>0.1.1</string>
+ <string>0.1.2</string>
<key>CFBundleVersion</key>
- <string>20260620.0136</string>
+ <string>20260620.0319</string>
<key>LSMinimumSystemVersion</key>
<string>26.0</string>
<key>NSHighResolutionCapable</key>
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index f137091..81e6310 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -4454,17 +4454,41 @@
)
: nil
- applySpaceLaneContentVisualState(segment, isLocked: isLocked)
+ applySpaceLaneContentVisualState(
+ segment,
+ isLocked: isLocked,
+ usesLightActiveFill: resolvedTheme == .dark && hasWindows && !isLocked && (isActive || isAssociated)
+ )
}
- private func applySpaceLaneContentVisualState(_ segment: SpaceLaneSegmentLayers, isLocked: Bool) {
- let primaryTextColor: NSColor = isLocked ? .white : themePalette.spaceLanePrimaryText
+ private func applySpaceLaneContentVisualState(
+ _ segment: SpaceLaneSegmentLayers,
+ isLocked: Bool,
+ usesLightActiveFill: Bool
+ ) {
+ let primaryTextColor: NSColor = isLocked
+ ? .white
+ : usesLightActiveFill
+ ? NSColor.black.withAlphaComponent(0.82)
+ : themePalette.spaceLanePrimaryText
let secondaryTextColor: NSColor = isLocked
? .white
- : themePalette.spaceLaneSecondaryText
+ : usesLightActiveFill
+ ? NSColor.black.withAlphaComponent(0.58)
+ : themePalette.spaceLaneSecondaryText
+ let markerTextColor: NSColor = isLocked
+ ? .white
+ : usesLightActiveFill
+ ? NSColor.black.withAlphaComponent(0.62)
+ : themePalette.spaceLaneSecondaryText.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.54)
+ let blockColor: NSColor = isLocked
+ ? .white
+ : usesLightActiveFill
+ ? NSColor.black.withAlphaComponent(0.20)
+ : spaceLaneOccupiedColor.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.58)
segment.labelLayer.foregroundColor = primaryTextColor.cgColor
- segment.countLayer.foregroundColor = (isLocked ? NSColor.white : themePalette.spaceLaneSecondaryText).cgColor
+ segment.countLayer.foregroundColor = secondaryTextColor.cgColor
segment.appLayer.foregroundColor = (segment.space.type == .fullscreen
? primaryTextColor
: secondaryTextColor
@@ -4474,16 +4498,10 @@
}
for block in segment.windowBlocks {
- block.backgroundColor = (isLocked
- ? NSColor.white
- : spaceLaneOccupiedColor.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.58)
- ).cgColor
+ block.backgroundColor = blockColor.cgColor
}
- (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor = (isLocked
- ? NSColor.white
- : themePalette.spaceLaneSecondaryText.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.54)
- ).cgColor
+ (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor = markerTextColor.cgColor
}
private func shouldScrollSpaceLane(for event: NSEvent) -> Bool {
@@ -7035,6 +7053,10 @@
}
let isAppAssociated = appHoverAssociatedSpaceIDs.contains(space.id)
let isWindowAssociated = space.id == windowHoverAssociatedSpaceID
+ let isLocked = space.id == currentViewModel?.lockedSpaceID
+ let isActive = space.id == activeSpaceFocusID || isLocked
+ let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive
+ let usesLightActiveFill = resolvedTheme == .dark && space.windowCount > 0 && !isLocked && (isActive || isAssociated)
let group = spaceLaneDisplayGroup(containingSpaceID: space.id)
let visibleFrame = segment.containerLayer.frame.offsetBy(
dx: (group?.containerLayer.frame.minX ?? 0) + spaceLaneContentLayer.frame.minX,
@@ -7053,6 +7075,8 @@
"label": space.label,
"type": spaceTypeName(space.type),
"visualStates": visualStates(for: space),
+ "usesLightActiveFill": usesLightActiveFill,
+ "foregroundContrastStyle": usesLightActiveFill ? "darkOnLight" : "themeDefault",
"fullscreenMarkerVisible": segment.fullscreenMarkerLayer != nil,
"fullscreenMarkerKind": fullscreenMarkerKind(
markerLayer: segment.fullscreenMarkerLayer,
diff --git a/C3.tools/round1-horizontal-waterfall-fixture-qa.sh b/C3.tools/round1-horizontal-waterfall-fixture-qa.sh
index 3f8ec00..c25b508 100755
--- a/C3.tools/round1-horizontal-waterfall-fixture-qa.sh
+++ b/C3.tools/round1-horizontal-waterfall-fixture-qa.sh
@@ -11,6 +11,8 @@
# shellcheck source=build-output-paths.sh
source "$SCRIPT_DIR/build-output-paths.sh"
APP="$BUILD_CURRENT_APP"
+DOMAIN="com.ar.Aligner"
+THEME_KEY="com.ar.Aligner.preferences.appearance.theme"
LAYOUT_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-layout-report.json"
KEYBOARD_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-keyboard-report.json"
APP_ENTER_REPORT="$BUILD_REPORT_ROOT/round01-horizontal-waterfall-app-enter-report.json"
@@ -34,6 +36,8 @@
WINDOW_SHORTCUT_TARGET_WINDOW_INDEX="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_WINDOW_SHORTCUT_TARGET_WINDOW_INDEX:-5}"
ARROW_WRAP_KEY_SEQUENCE="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_ARROW_WRAP_KEY_SEQUENCE:-app:1,down,up,left,down,enter}"
REPORT_WAIT="${ALIGNER_ROUND1_HORIZONTAL_WATERFALL_REPORT_WAIT:-6.0}"
+OLD_THEME_SET=0
+OLD_THEME=""
fail() {
echo "Round01.1 horizontal Waterfall fixture QA failed: $*" >&2
@@ -59,6 +63,23 @@
done
fail "current Aligner app did not exit before QA"
+}
+
+preserve_user_theme() {
+ if OLD_THEME="$(/usr/bin/defaults read "$DOMAIN" "$THEME_KEY" 2>/dev/null)"; then
+ OLD_THEME_SET=1
+ else
+ OLD_THEME_SET=0
+ OLD_THEME=""
+ fi
+}
+
+restore_user_theme() {
+ if [ "$OLD_THEME_SET" -eq 1 ]; then
+ /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$OLD_THEME"
+ else
+ /usr/bin/defaults delete "$DOMAIN" "$THEME_KEY" >/dev/null 2>&1 || true
+ fi
}
wait_for_loaded_report() {
@@ -688,8 +709,11 @@
wait "$APP_PID" 2>/dev/null || true
fi
stop_current_aligner
+ restore_user_theme
}
trap cleanup EXIT
+preserve_user_theme
+/usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "light"
run_fixture "$LAYOUT_REPORT" --round01-debug-mouse-sequence="hover-app:$HOVER_APP_INDEX"
wait_for_loaded_report "$LAYOUT_REPORT"
@@ -754,4 +778,5 @@
trap - EXIT
stop_current_aligner
+restore_user_theme
echo "Round01.1 horizontal Waterfall fixture QA passed"
diff --git a/C3.tools/round1-main-ui-qa.sh b/C3.tools/round1-main-ui-qa.sh
index 05fe694..99c1b6d 100755
--- a/C3.tools/round1-main-ui-qa.sh
+++ b/C3.tools/round1-main-ui-qa.sh
@@ -116,6 +116,7 @@
run_step app-column-alignment "$SCRIPT_DIR/round1-app-column-alignment-fixture-qa.sh"
run_step horizontal-waterfall "$SCRIPT_DIR/round1-horizontal-waterfall-fixture-qa.sh"
run_step theme-toggle "$SCRIPT_DIR/round1-theme-toggle-fixture-qa.sh"
+run_step space-lane-dark-hover "$SCRIPT_DIR/round1-space-lane-dark-hover-fixture-qa.sh"
run_step no-quick-switch-residual swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch
assert_no_current_aligner
@@ -128,5 +129,6 @@
Covered: trigger, overlay uniqueness, keyboard navigation, candidate filtering,
screenshots/fallbacks, minimized-window restore/activation, mouse activation,
multi-page precise activation/close targeting, App column alignment,
-Split View Space Lane rendering, theme toggle, and final no-residual check.
+Split View Space Lane rendering, theme toggle, Space Lane dark hover readability,
+and final no-residual check.
EOF
diff --git a/C3.tools/round1-space-lane-dark-hover-fixture-qa.sh b/C3.tools/round1-space-lane-dark-hover-fixture-qa.sh
new file mode 100755
index 0000000..e20a9dc
--- /dev/null
+++ b/C3.tools/round1-space-lane-dark-hover-fixture-qa.sh
@@ -0,0 +1,390 @@
+#!/bin/bash
+# Round01.1 Space Lane dark-theme hover readability QA.
+# Verifies that dark theme keeps the light hover surface but flips Space tile
+# foreground elements to dark colors only when the tile actually uses a light
+# occupied active/associated fill.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+# shellcheck source=build-output-paths.sh
+source "$SCRIPT_DIR/build-output-paths.sh"
+
+APP="$BUILD_CURRENT_APP"
+DOMAIN="com.ar.Aligner"
+THEME_KEY="com.ar.Aligner.preferences.appearance.theme"
+REPORT_WAIT="${ALIGNER_ROUND1_SPACE_DARK_HOVER_REPORT_WAIT:-6.0}"
+REPORT_DARK_BASE="$BUILD_REPORT_ROOT/round01-space-dark-hover-base-report.json"
+REPORT_DARK_HOVER="$BUILD_REPORT_ROOT/round01-space-dark-hover-report.json"
+REPORT_DARK_SPLIT_BASE="$BUILD_REPORT_ROOT/round01-space-dark-hover-split-base-report.json"
+REPORT_DARK_SPLIT_HOVER="$BUILD_REPORT_ROOT/round01-space-dark-hover-split-report.json"
+REPORT_DARK_EMPTY_BASE="$BUILD_REPORT_ROOT/round01-space-dark-hover-empty-base-report.json"
+REPORT_DARK_EMPTY_HOVER="$BUILD_REPORT_ROOT/round01-space-dark-hover-empty-report.json"
+REPORT_LIGHT_HOVER="$BUILD_REPORT_ROOT/round01-space-dark-hover-light-report.json"
+OLD_THEME_SET=0
+OLD_THEME=""
+APP_PID=""
+
+fail() {
+ echo "Round01.1 Space Lane dark hover QA failed: $*" >&2
+ exit 1
+}
+
+aligner_pids_for_current_app() {
+ ps -axo pid=,args= | while read -r pid command; do
+ case "$command" in
+ "$APP/Contents/MacOS/Aligner"*) echo "$pid" ;;
+ esac
+ done
+}
+
+stop_current_aligner() {
+ if [ -n "${APP_PID:-}" ]; then
+ kill "$APP_PID" 2>/dev/null || true
+ wait "$APP_PID" 2>/dev/null || true
+ APP_PID=""
+ fi
+
+ for pid in $(aligner_pids_for_current_app); do
+ kill "$pid" 2>/dev/null || true
+ done
+
+ for _ in {1..30}; do
+ [ -z "$(aligner_pids_for_current_app)" ] && return
+ sleep 0.1
+ done
+
+ fail "current Aligner app did not exit before QA"
+}
+
+preserve_user_theme() {
+ if OLD_THEME="$(/usr/bin/defaults read "$DOMAIN" "$THEME_KEY" 2>/dev/null)"; then
+ OLD_THEME_SET=1
+ else
+ OLD_THEME_SET=0
+ OLD_THEME=""
+ fi
+}
+
+restore_user_theme() {
+ if [ "$OLD_THEME_SET" -eq 1 ]; then
+ /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$OLD_THEME"
+ else
+ /usr/bin/defaults delete "$DOMAIN" "$THEME_KEY" >/dev/null 2>&1 || true
+ fi
+}
+
+wait_for_loaded_report() {
+ local report="$1"
+ local expected_theme="$2"
+ local expected_mouse_command="${3:-}"
+
+ /usr/bin/python3 - "$report" "$expected_theme" "$expected_mouse_command" "$REPORT_WAIT" <<'PY'
+import json
+import sys
+import time
+
+path = sys.argv[1]
+expected_theme = sys.argv[2]
+expected_mouse_command = sys.argv[3]
+timeout = float(sys.argv[4])
+deadline = time.monotonic() + timeout
+last_report = None
+
+while time.monotonic() < deadline:
+ try:
+ with open(path, "r", encoding="utf-8") as file:
+ report = json.load(file)
+ last_report = report
+ root = report.get("rootView", {})
+ if (
+ report.get("snapshotLoaded") is True
+ and report.get("quickSwitchVisible") is True
+ and report.get("resolvedQuickSwitchTheme") == expected_theme
+ and root.get("themeResolved") == expected_theme
+ and (not expected_mouse_command or root.get("lastMouseCommand") == expected_mouse_command)
+ ):
+ sys.exit(0)
+ except FileNotFoundError:
+ pass
+ except json.JSONDecodeError:
+ pass
+ time.sleep(0.2)
+
+if last_report is not None:
+ print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
+print(f"report did not reach {expected_theme} within {timeout:.1f}s", file=sys.stderr)
+sys.exit(1)
+PY
+}
+
+select_space_id() {
+ local report="$1"
+ local selector="$2"
+
+ /usr/bin/python3 - "$report" "$selector" <<'PY'
+import json
+import sys
+
+with open(sys.argv[1], "r", encoding="utf-8") as file:
+ report = json.load(file)
+
+selector = sys.argv[2]
+segments = report.get("rootView", {}).get("spaceLaneSegments", [])
+
+def unlocked(segment):
+ return "locked" not in segment.get("visualStates", [])
+
+for segment in segments:
+ if selector == "occupied" and segment.get("windowCount", 0) > 0 and unlocked(segment):
+ print(segment["spaceID"])
+ sys.exit(0)
+ if selector == "split" and len(segment.get("splitViewAppNames", [])) >= 2 and segment.get("windowCount", 0) > 0 and unlocked(segment):
+ print(segment["spaceID"])
+ sys.exit(0)
+ if selector == "empty" and segment.get("windowCount", 0) == 0 and unlocked(segment):
+ print(segment["spaceID"])
+ sys.exit(0)
+
+print("", end="")
+PY
+}
+
+run_fixture() {
+ local theme="$1"
+ local report="$2"
+ shift 2
+
+ stop_current_aligner
+ /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$theme"
+ rm -f "$report"
+
+ "$APP/Contents/MacOS/Aligner" \
+ --round0-skip-permissions \
+ --round01-open-quick-switch \
+ --round01-fixture-app-count=4 \
+ --round01-fixture-windows-per-app=2 \
+ --round01-disable-screenshot-refresh \
+ --round01-quick-switch-report="$report" \
+ --round01-quick-switch-quit-after=1.8 \
+ "$@" >/dev/null 2>&1 &
+
+ APP_PID=$!
+}
+
+run_split_fixture() {
+ local theme="$1"
+ local report="$2"
+ shift 2
+
+ stop_current_aligner
+ /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$theme"
+ rm -f "$report"
+
+ "$APP/Contents/MacOS/Aligner" \
+ --round0-skip-permissions \
+ --round01-open-quick-switch \
+ --round01-fixture-split-view \
+ --round01-disable-screenshot-refresh \
+ --round01-quick-switch-report="$report" \
+ --round01-quick-switch-quit-after=1.8 \
+ "$@" >/dev/null 2>&1 &
+
+ APP_PID=$!
+}
+
+run_space_filter_fixture() {
+ local theme="$1"
+ local report="$2"
+ shift 2
+
+ stop_current_aligner
+ /usr/bin/defaults write "$DOMAIN" "$THEME_KEY" -string "$theme"
+ rm -f "$report"
+
+ "$APP/Contents/MacOS/Aligner" \
+ --round0-skip-permissions \
+ --round01-open-quick-switch \
+ --round01-fixture-space-filter \
+ --round01-disable-screenshot-refresh \
+ --round01-quick-switch-report="$report" \
+ --round01-quick-switch-quit-after=1.8 \
+ "$@" >/dev/null 2>&1 &
+
+ APP_PID=$!
+}
+
+assert_dark_on_light_hover() {
+ local report="$1"
+ local space_id="$2"
+ local require_split="${3:-0}"
+
+ /usr/bin/python3 - "$report" "$space_id" "$require_split" <<'PY'
+import json
+import sys
+
+path = sys.argv[1]
+space_id = int(sys.argv[2])
+require_split = sys.argv[3] == "1"
+
+with open(path, "r", encoding="utf-8") as file:
+ report = json.load(file)
+
+root = report.get("rootView", {})
+segments = root.get("spaceLaneSegments", [])
+segment = next((item for item in segments if int(item.get("spaceID")) == space_id), None)
+
+def fail(message):
+ print(message, file=sys.stderr)
+ print(json.dumps(segment or report, indent=2, ensure_ascii=False), file=sys.stderr)
+ sys.exit(1)
+
+def avg(color):
+ return (color.get("red", 0) + color.get("green", 0) + color.get("blue", 0)) / 3
+
+def require_dark(color, name, max_avg=0.36, min_alpha=0.12):
+ if color.get("alpha", 0) < min_alpha or avg(color) > max_avg:
+ fail(f"{name} must be dark enough on light hover fill")
+
+def require_light(color, name, min_avg=0.44, min_alpha=0.50):
+ if color.get("alpha", 0) < min_alpha or avg(color) < min_avg:
+ fail(f"{name} must remain a light hover fill")
+
+if root.get("themeResolved") != "dark" or report.get("resolvedQuickSwitchTheme") != "dark":
+ fail("report must be in dark theme")
+if segment is None:
+ fail("target space segment not found")
+if segment.get("isHovered") is not True or "hover" not in segment.get("visualStates", []):
+ fail("target space must be hovered")
+if segment.get("usesLightActiveFill") is not True:
+ fail("hovered occupied dark tile must report usesLightActiveFill")
+if segment.get("foregroundContrastStyle") != "darkOnLight":
+ fail("hovered occupied dark tile must use darkOnLight foreground style")
+if segment.get("backgroundKind") != "solid":
+ fail("hovered occupied dark tile must use a solid light hover background")
+
+require_light(segment.get("backgroundColor", {}), "hover background")
+require_dark(segment.get("labelColor", {}), "space label")
+require_dark(segment.get("countColor", {}), "window count")
+require_dark(segment.get("appColor", {}), "app text")
+
+if require_split and len(segment.get("splitViewAppNames", [])) < 2:
+ fail("split fixture target must expose split view app names")
+for index, color in enumerate(segment.get("splitViewLabelColors", [])):
+ require_dark(color, f"split app label {index}")
+
+if segment.get("fullscreenMarkerVisible"):
+ require_dark(segment.get("fullscreenMarkerStrokeColor", {}), "fullscreen/split marker")
+
+for index, color in enumerate(segment.get("windowBlockColors", [])):
+ require_dark(color, f"window block {index}", max_avg=0.38, min_alpha=0.05)
+
+print(json.dumps({
+ "spaceID": space_id,
+ "label": segment.get("label"),
+ "foregroundContrastStyle": segment.get("foregroundContrastStyle"),
+ "backgroundKind": segment.get("backgroundKind"),
+ "labelColor": segment.get("labelColor"),
+ "countColor": segment.get("countColor"),
+ "appColor": segment.get("appColor")
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+assert_no_light_active_fill() {
+ local report="$1"
+ local space_id="$2"
+ local expected_theme="$3"
+
+ /usr/bin/python3 - "$report" "$space_id" "$expected_theme" <<'PY'
+import json
+import sys
+
+with open(sys.argv[1], "r", encoding="utf-8") as file:
+ report = json.load(file)
+
+space_id = int(sys.argv[2])
+expected_theme = sys.argv[3]
+root = report.get("rootView", {})
+segment = next((item for item in root.get("spaceLaneSegments", []) if int(item.get("spaceID")) == space_id), None)
+
+def fail(message):
+ print(message, file=sys.stderr)
+ print(json.dumps(segment or report, indent=2, ensure_ascii=False), file=sys.stderr)
+ sys.exit(1)
+
+if root.get("themeResolved") != expected_theme or report.get("resolvedQuickSwitchTheme") != expected_theme:
+ fail("theme mismatch")
+if segment is None:
+ fail("target space segment not found")
+if segment.get("isHovered") is not True:
+ fail("target space must be hovered")
+if segment.get("usesLightActiveFill") is not False:
+ fail("segment must not use the dark-theme light active fill path")
+if segment.get("foregroundContrastStyle") != "themeDefault":
+ fail("segment must keep theme default foreground style")
+
+print(json.dumps({
+ "theme": expected_theme,
+ "spaceID": space_id,
+ "label": segment.get("label"),
+ "foregroundContrastStyle": segment.get("foregroundContrastStyle"),
+ "backgroundKind": segment.get("backgroundKind"),
+ "windowCount": segment.get("windowCount")
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+cleanup() {
+ stop_current_aligner || true
+ restore_user_theme
+}
+
+preserve_user_theme
+trap cleanup EXIT
+stop_current_aligner
+
+run_fixture "dark" "$REPORT_DARK_BASE"
+wait_for_loaded_report "$REPORT_DARK_BASE" "dark"
+OCCUPIED_SPACE_ID="$(select_space_id "$REPORT_DARK_BASE" occupied)"
+[ -n "$OCCUPIED_SPACE_ID" ] || fail "no occupied Space found in layout fixture"
+
+run_fixture "dark" "$REPORT_DARK_HOVER" --round01-debug-mouse-sequence="hover-space:$OCCUPIED_SPACE_ID"
+wait_for_loaded_report "$REPORT_DARK_HOVER" "dark" "hover-space:$OCCUPIED_SPACE_ID"
+assert_dark_on_light_hover "$REPORT_DARK_HOVER" "$OCCUPIED_SPACE_ID"
+
+run_split_fixture "dark" "$REPORT_DARK_SPLIT_BASE"
+wait_for_loaded_report "$REPORT_DARK_SPLIT_BASE" "dark"
+SPLIT_SPACE_ID="$(select_space_id "$REPORT_DARK_SPLIT_BASE" split)"
+[ -n "$SPLIT_SPACE_ID" ] || fail "no Split View Space found in split fixture"
+
+run_split_fixture "dark" "$REPORT_DARK_SPLIT_HOVER" --round01-debug-mouse-sequence="hover-space:$SPLIT_SPACE_ID"
+wait_for_loaded_report "$REPORT_DARK_SPLIT_HOVER" "dark" "hover-space:$SPLIT_SPACE_ID"
+assert_dark_on_light_hover "$REPORT_DARK_SPLIT_HOVER" "$SPLIT_SPACE_ID" 1
+
+run_space_filter_fixture "dark" "$REPORT_DARK_EMPTY_BASE"
+wait_for_loaded_report "$REPORT_DARK_EMPTY_BASE" "dark"
+EMPTY_SPACE_ID="$(select_space_id "$REPORT_DARK_EMPTY_BASE" empty)"
+[ -n "$EMPTY_SPACE_ID" ] || fail "no empty Space found in space-filter fixture"
+
+run_space_filter_fixture "dark" "$REPORT_DARK_EMPTY_HOVER" --round01-debug-mouse-sequence="hover-space:$EMPTY_SPACE_ID"
+wait_for_loaded_report "$REPORT_DARK_EMPTY_HOVER" "dark" "hover-space:$EMPTY_SPACE_ID"
+assert_no_light_active_fill "$REPORT_DARK_EMPTY_HOVER" "$EMPTY_SPACE_ID" "dark"
+
+run_fixture "light" "$REPORT_LIGHT_HOVER" --round01-debug-mouse-sequence="hover-space:$OCCUPIED_SPACE_ID"
+wait_for_loaded_report "$REPORT_LIGHT_HOVER" "light" "hover-space:$OCCUPIED_SPACE_ID"
+assert_no_light_active_fill "$REPORT_LIGHT_HOVER" "$OCCUPIED_SPACE_ID" "light"
+
+stop_current_aligner
+restore_user_theme
+trap - EXIT
+
+cat <<EOF
+Round01.1 Space Lane dark hover QA passed
+Reports:
+- $REPORT_DARK_HOVER
+- $REPORT_DARK_SPLIT_HOVER
+- $REPORT_DARK_EMPTY_HOVER
+- $REPORT_LIGHT_HOVER
+EOF
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa68b51..87858d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## 0.1.2 / 20260620.0319
+
+- 修复深色模式下 Space tile hover 浅色高亮时,Space 编号、窗口数量、App 文本、Split View 标签、角标线和占用条仍为浅色导致不可读的问题。
+- 增加 Space Lane 深色 hover 可读性 fixture QA,并纳入主 UI 回归入口。
+
## 0.1.1 / 20260620.0136
- 增加 Quick Switch 浅色 / 深色皮肤切换。
--
Gitblit v1.9.3