diff --git a/docs/release/v0.2-frontend-overhaul.md b/docs/release/v0.2-frontend-overhaul.md
index 45c25f0..0a2d211 100644
--- a/docs/release/v0.2-frontend-overhaul.md
+++ b/docs/release/v0.2-frontend-overhaul.md
@@ -199,8 +199,8 @@ Filled during execution. Each entry: phase #, date, what landed, what's next.
|---|---|---|
| 0 | ✅ complete | Doc written; tooling pins recorded |
| 1 | ✅ complete | Tooling baseline green: `npm run check` clean, `npm test` clean, `npm run test:e2e` 16/16, `npm run guard:no-skeleton` clean, 10 screenshot artefacts. Browser-preview OS detection fixed (see Regression diary). |
-| 2 | pending | UI/form deps |
-| 3 | pending | Additive tokens + KI-05 |
+| 2 | ✅ complete | bits-ui 2.18.1, formsnap 2.0.1, sveltekit-superforms 2.30.1, zod 4.4.3, @internationalized/date 3.12.1. `npm audit signatures` 273 verified + 93 attestations. |
+| 3 | ✅ complete | New tokens `--color-caution`, `--color-info`, `--color-accent-environment` (dark + light), `--color-warning` aliased to `var(--color-caution)`. KI-05 resolved: `theme` dropped from SettingsState type + defaults, all four route-layout migration `$effect`s deleted, two SettingsPage SegmentedButton bindings repointed to `preferences.theme` via Svelte 5 function bindings, one-shot legacy-theme migration on first mount strips the field after copying. Gate green (check 0/0, vitest 13/13, e2e 16/16). |
| 4 | pending | Wrapper alias layer |
| 5 | pending | New primitives + design-system-v2 preview |
| 6 | pending | Shell split |
diff --git a/src/app.css b/src/app.css
index 9d0e98c..582fe89 100644
--- a/src/app.css
+++ b/src/app.css
@@ -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;
diff --git a/src/design-system/colors_and_type.css b/src/design-system/colors_and_type.css
index 98dfa93..e5acd54 100644
--- a/src/design-system/colors_and_type.css
+++ b/src/design-system/colors_and_type.css
@@ -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);
diff --git a/src/lib/pages/SettingsPage.svelte b/src/lib/pages/SettingsPage.svelte
index d3d0310..c00c739 100644
--- a/src/lib/pages/SettingsPage.svelte
+++ b/src/lib/pages/SettingsPage.svelte
@@ -1269,7 +1269,14 @@
Theme
-
+
prefs.theme === "light" ? "Light" : "Dark",
+ (v) => updatePreferences({ theme: v === "Light" ? "light" : "dark" })
+ }
+ />
Font size
@@ -2603,7 +2610,13 @@
Theme
-
+
prefs.theme === "light" ? "Light" : prefs.theme === "system" ? "System" : "Dark",
+ (v) => updatePreferences({ theme: v === "Light" ? "light" : v === "System" ? "system" : "dark" })
+ }
+ />
diff --git a/src/lib/stores/page.svelte.ts b/src/lib/stores/page.svelte.ts
index 8c56a65..c31add7 100644
--- a/src/lib/stores/page.svelte.ts
+++ b/src/lib/stores/page.svelte.ts
@@ -69,7 +69,6 @@ const defaults: SettingsState = {
soundCues: false,
soundCueVolume: 0.15,
includeTimestamps: true,
- theme: "Dark",
fontSize: 14,
aiTier: "cleanup",
llmModelId: null,
diff --git a/src/lib/types/app.ts b/src/lib/types/app.ts
index 34409d5..a560657 100644
--- a/src/lib/types/app.ts
+++ b/src/lib/types/app.ts
@@ -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;
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 1f4afc9..15a0af0 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -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);
diff --git a/src/routes/float/+layout@.svelte b/src/routes/float/+layout@.svelte
index a6ad755..61f334f 100644
--- a/src/routes/float/+layout@.svelte
+++ b/src/routes/float/+layout@.svelte
@@ -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") {
diff --git a/src/routes/preview/+layout@.svelte b/src/routes/preview/+layout@.svelte
index d023117..73c5c33 100644
--- a/src/routes/preview/+layout@.svelte
+++ b/src/routes/preview/+layout@.svelte
@@ -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) => {
diff --git a/src/routes/viewer/+layout@.svelte b/src/routes/viewer/+layout@.svelte
index 2efddd4..eb6415f 100644
--- a/src/routes/viewer/+layout@.svelte
+++ b/src/routes/viewer/+layout@.svelte
@@ -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") {