v0.2 Phase 3: additive semantic tokens + KI-05 resolution

Additive token grammar (no renames, no replacements):

- --color-caution (dark #e8be4a, light #a08a1f) becomes the canonical
  name for tuned-amber notice surfaces in the new wrapper grammar
- --color-warning is kept as a CSS var() alias of --color-caution so
  every existing text-warning / bg-warning call site stays valid
- --color-info (dark #7a9ec0, light #3d6a8a) is the soft blue-grey
  signal for the new LumotiaNotice info variant
- --color-accent-environment (dark #8fae9a, light #4a7058) is an
  optional sage/moss support token for empty-state illustrations
  and environment-neutral status dots. NOT a brand swap — amber/
  copper --color-accent stays primary

Mirrored in src/design-system/colors_and_type.css (the buildless
preview pages bypass Tailwind so the duplication is intentional).

KI-05 resolved in the same commit, per the plan:

- src/lib/types/app.ts: drop `theme` from SettingsState
- src/lib/stores/page.svelte.ts: drop `theme: "Dark"` from defaults
- src/routes/+layout.svelte: drop the migration $effect, add a
  one-shot migrateLegacyTheme() on mount that copies any historical
  lumotia_settings.theme into preferences.theme and strips the
  legacy field. Idempotent — subsequent loads short-circuit
- src/routes/{float,viewer,preview}/+layout@.svelte: drop the same
  $effect; secondary windows inherit theme via PREFERENCES_CHANGED_EVENT
- src/lib/pages/SettingsPage.svelte: both SegmentedButton bindings
  (quick-settings row at :1272, Appearance group at :2606) now use
  Svelte 5 function bindings ({ get, set }) backed by prefs.theme
  and updatePreferences. No SegmentedButton API change

Phase 3 per-page gate green: npm run check (0/0/4129 files),
npm test (13/13), npm run test:e2e (16/16). No regressions in
the Phase 1 smoke baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 08:35:54 +01:00
parent 94e6a79515
commit 66e25aa778
10 changed files with 79 additions and 43 deletions

View File

@@ -100,7 +100,18 @@
(#1b1a17) verified mentally for each. */
--color-success: #5fc28a;
--color-danger: #e85f5f;
--color-warning: #e8be4a;
/* v0.2 coherence-pass aliases. `caution` is the canonical name in the
new wrapper grammar; `warning` is kept as a CSS var() alias so
existing `text-warning` / `bg-warning` call sites resolve to the
same value without a touch. */
--color-caution: #e8be4a;
--color-warning: var(--color-caution);
--color-info: #7a9ec0;
/* v0.2: optional support token for sage/moss surfaces — empty-state
illustrations, environmental neutral status dots. NOT a brand
swap; --color-accent (amber/copper) stays primary. */
--color-accent-environment: #8fae9a;
/* Overlays — used by modal scrims. Derived from --color-bg #0f0e0c at
0.7 alpha so the dim sits on the brand neutral, not pure black. */
@@ -190,7 +201,12 @@
AA on cream backgrounds. */
--color-success: #1f7344;
--color-danger: #b32626;
--color-warning: #a08a1f;
/* v0.2 coherence-pass: --color-warning inherits @theme's
`var(--color-caution)`, so light theme only needs to redefine the
source token. */
--color-caution: #a08a1f;
--color-info: #3d6a8a;
--color-accent-environment: #4a7058;
--color-sidebar: #f5f2ed;
--color-nav-active: #eae6e0;

View File

@@ -95,7 +95,13 @@
/* — Semantic — Phase 10b chroma bump for clearer signal. — */
--success: #5fc28a;
--danger: #e85f5f;
--warning: #e8be4a;
/* v0.2 coherence-pass aliases (mirrors src/app.css @theme). */
--caution: #e8be4a;
--warning: var(--caution);
--info: #7a9ec0;
/* v0.2 optional sage/moss support token (mirrors src/app.css). */
--accent-environment: #8fae9a;
/* — Overlays — modal scrim. Derived from --bg #0f0e0c at 0.7 alpha. — */
--overlay-dim: rgba(15, 14, 12, 0.7);

View File

@@ -1269,7 +1269,14 @@
<div class="grid grid-cols-3 gap-3 px-1 pb-4 border-b border-border-subtle mb-2">
<div>
<p class="text-[12px] font-medium text-text-secondary uppercase tracking-wider mb-2">Theme</p>
<SegmentedButton size="small" options={["Dark", "Light"]} bind:value={settings.theme} />
<SegmentedButton
size="small"
options={["Dark", "Light"]}
bind:value={
() => prefs.theme === "light" ? "Light" : "Dark",
(v) => updatePreferences({ theme: v === "Light" ? "light" : "dark" })
}
/>
</div>
<div>
<p class="text-[12px] font-medium text-text-secondary uppercase tracking-wider mb-2">Font size</p>
@@ -2603,7 +2610,13 @@
<div class="animate-fade-in">
<div class="mb-6">
<p class="text-[12px] font-medium text-text-secondary uppercase tracking-wider mb-2">Theme</p>
<SegmentedButton options={["Dark", "Light", "System"]} bind:value={settings.theme} />
<SegmentedButton
options={["Dark", "Light", "System"]}
bind:value={
() => prefs.theme === "light" ? "Light" : prefs.theme === "system" ? "System" : "Dark",
(v) => updatePreferences({ theme: v === "Light" ? "light" : v === "System" ? "system" : "dark" })
}
/>
</div>
<div class="mb-6">

View File

@@ -69,7 +69,6 @@ const defaults: SettingsState = {
soundCues: false,
soundCueVolume: 0.15,
includeTimestamps: true,
theme: "Dark",
fontSize: 14,
aiTier: "cleanup",
llmModelId: null,

View File

@@ -54,7 +54,9 @@ export interface SettingsState {
soundCues: boolean;
soundCueVolume: number;
includeTimestamps: boolean;
theme: "Dark" | "Light" | "System";
// theme removed in v0.2 Phase 3 — KI-05 resolution. The canonical
// theme field is `preferences.theme` (preferences.svelte.ts). The
// legacy field migrates on first load (see page.svelte.ts).
fontSize: number;
aiTier: AiTier;
llmModelId: LlmModelIdStr | null;

View File

@@ -56,16 +56,28 @@
$sveltePage.url.pathname.startsWith("/viewer")
);
// Theme — migrate from old class-based to new data-attribute system
// The preferences store handles DOM application via data-theme attribute
$effect(() => {
// Sync legacy settings.theme → preferences store
const legacyTheme = settings.theme;
const mapped = legacyTheme === "Light" ? "light" : legacyTheme === "Dark" ? "dark" : "system";
if (prefs.theme !== mapped) {
updatePreferences({ theme: mapped });
}
});
// KI-05 resolution (v0.2 Phase 3) — preferences.theme is canonical.
// The legacy settings.theme field is gone from SettingsState; a
// one-shot localStorage migration (migrateLegacyTheme below) runs
// once on mount to copy any historical "Dark"/"Light"/"System"
// value into preferences, then strips the field so subsequent
// loads short-circuit.
function migrateLegacyTheme() {
if (typeof localStorage === "undefined") return;
const raw = localStorage.getItem("lumotia_settings");
if (!raw) return;
let parsed;
try { parsed = JSON.parse(raw); } catch { return; }
const data = parsed?.data ?? parsed;
if (!data || typeof data !== "object" || !("theme" in data)) return;
const legacy = data.theme;
const mapped = legacy === "Light" ? "light" : legacy === "System" ? "system" : "dark";
if (mapped !== prefs.theme) updatePreferences({ theme: mapped });
delete data.theme;
if (parsed && typeof parsed === "object" && "data" in parsed) parsed.data = data;
else parsed = data;
try { localStorage.setItem("lumotia_settings", JSON.stringify(parsed)); } catch {}
}
// Global hotkey registration — dual backend
// Wayland: evdev via lumotia-hotkey crate (works without display server)
@@ -340,6 +352,12 @@
}
onMount(async () => {
// KI-05 v0.2 one-shot: copy any legacy settings.theme into
// preferences.theme, then strip it from localStorage. Runs first
// so the DOM picks up the right data-theme attribute before any
// theme-sensitive rendering.
migrateLegacyTheme();
// Auto-collapse if window is already narrow on first load
handleResize();
window.addEventListener("resize", handleResize);

View File

@@ -23,14 +23,8 @@
const prefs = getPreferences();
// Theme — sync legacy settings to preferences store (same as main layout)
$effect(() => {
const legacyTheme = settings.theme;
const mapped = legacyTheme === "Light" ? "light" : legacyTheme === "Dark" ? "dark" : "system";
if (prefs.theme !== mapped) {
updatePreferences({ theme: mapped });
}
});
// v0.2 Phase 3 KI-05: theme is now preferences.theme only. The
// float window inherits via PREFERENCES_CHANGED_EVENT below.
// Listen for settings changes from main window
if (typeof window !== "undefined") {

View File

@@ -17,14 +17,8 @@
const prefs = getPreferences();
// Keep transcript-editor theme sync trick: legacy settings → preferences
$effect(() => {
const legacyTheme = settings.theme;
const mapped = legacyTheme === "Light" ? "light" : legacyTheme === "Dark" ? "dark" : "system";
if (prefs.theme !== mapped) {
updatePreferences({ theme: mapped });
}
});
// v0.2 Phase 3 KI-05: theme is now preferences.theme only. The
// preview window inherits via PREFERENCES_CHANGED_EVENT below.
if (typeof window !== "undefined") {
window.addEventListener("storage", (event) => {

View File

@@ -20,14 +20,8 @@
const prefs = getPreferences();
// Theme — sync legacy settings to preferences store
$effect(() => {
const legacyTheme = settings.theme;
const mapped = legacyTheme === "Light" ? "light" : legacyTheme === "Dark" ? "dark" : "system";
if (prefs.theme !== mapped) {
updatePreferences({ theme: mapped });
}
});
// v0.2 Phase 3 KI-05: theme is now preferences.theme only. The
// viewer window inherits via PREFERENCES_CHANGED_EVENT below.
// Sync settings from main window
if (typeof window !== "undefined") {