Ariver
2026-06-20 0a64c8163445e27f5123fdcf083fb3a5449b5c49
Fix dark Space hover readability
5 files modified
1 files added
478 ■■■■■ changed files
C1.source/Resources/Aligner-Info.plist 4 ●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/QuickSwitchRootView.swift 50 ●●●● patch | view | raw | blame | history
C3.tools/round1-horizontal-waterfall-fixture-qa.sh 25 ●●●●● patch | view | raw | blame | history
C3.tools/round1-main-ui-qa.sh 4 ●●● patch | view | raw | blame | history
C3.tools/round1-space-lane-dark-hover-fixture-qa.sh 390 ●●●●● patch | view | raw | blame | history
CHANGELOG.md 5 ●●●●● patch | view | raw | blame | history
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>
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,
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"
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
C3.tools/round1-space-lane-dark-hover-fixture-qa.sh
New file
@@ -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
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 浅色 / 深色皮肤切换。