From cfd1def4a93d50ea909d5df2c04413768c91cf8b Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 23 Feb 2026 13:59:01 +0000 Subject: [PATCH] feat(web): add theme definition system with 5 built-in themes (#493) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .../web/src/themes/__tests__/registry.test.ts | 170 ++++++++++++++++++ apps/web/src/themes/dark.ts | 41 +++++ apps/web/src/themes/dracula.ts | 45 +++++ apps/web/src/themes/index.ts | 18 ++ apps/web/src/themes/light.ts | 41 +++++ apps/web/src/themes/nord.ts | 45 +++++ apps/web/src/themes/registry.ts | 50 ++++++ apps/web/src/themes/solarized-dark.ts | 45 +++++ apps/web/src/themes/types.ts | 99 ++++++++++ docs/TASKS.md | 42 ++--- 10 files changed, 575 insertions(+), 21 deletions(-) create mode 100644 apps/web/src/themes/__tests__/registry.test.ts create mode 100644 apps/web/src/themes/dark.ts create mode 100644 apps/web/src/themes/dracula.ts create mode 100644 apps/web/src/themes/index.ts create mode 100644 apps/web/src/themes/light.ts create mode 100644 apps/web/src/themes/nord.ts create mode 100644 apps/web/src/themes/registry.ts create mode 100644 apps/web/src/themes/solarized-dark.ts create mode 100644 apps/web/src/themes/types.ts diff --git a/apps/web/src/themes/__tests__/registry.test.ts b/apps/web/src/themes/__tests__/registry.test.ts new file mode 100644 index 0000000..ec3a7ee --- /dev/null +++ b/apps/web/src/themes/__tests__/registry.test.ts @@ -0,0 +1,170 @@ +import { describe, expect, it } from "vitest"; + +import { darkTheme } from "../dark"; +import { draculaTheme } from "../dracula"; +import { lightTheme } from "../light"; +import { nordTheme } from "../nord"; +import { + DEFAULT_THEME_ID, + getAllThemes, + getDarkThemes, + getLightThemes, + getTheme, + getThemeOrDefault, + isValidThemeId, +} from "../registry"; +import { solarizedDarkTheme } from "../solarized-dark"; +import type { ThemeColors, ThemeDefinition } from "../types"; +import { themeToVariables } from "../types"; + +const ALL_THEMES = [darkTheme, lightTheme, nordTheme, draculaTheme, solarizedDarkTheme]; + +const REQUIRED_COLOR_KEYS: (keyof ThemeColors)[] = [ + "bg-950", + "bg-900", + "bg-850", + "surface-800", + "surface-750", + "border-700", + "text-100", + "text-300", + "text-500", + "blue-500", + "blue-400", + "red-500", + "red-400", + "purple-500", + "purple-400", + "teal-500", + "teal-400", + "amber-500", + "amber-400", + "pink-500", + "emerald-500", + "orange-500", + "cyan-500", + "indigo-500", +]; + +describe("Theme Registry", () => { + it("getAllThemes returns all 5 built-in themes", () => { + const themes = getAllThemes(); + expect(themes).toHaveLength(5); + expect(themes.map((t) => t.id)).toEqual(["dark", "light", "nord", "dracula", "solarized-dark"]); + }); + + it("getTheme returns correct theme by id", () => { + expect(getTheme("dark")).toBe(darkTheme); + expect(getTheme("light")).toBe(lightTheme); + expect(getTheme("nord")).toBe(nordTheme); + expect(getTheme("dracula")).toBe(draculaTheme); + expect(getTheme("solarized-dark")).toBe(solarizedDarkTheme); + }); + + it("getTheme returns undefined for unknown id", () => { + expect(getTheme("nonexistent")).toBeUndefined(); + }); + + it("getThemeOrDefault falls back to dark theme", () => { + expect(getThemeOrDefault("nonexistent")).toBe(darkTheme); + expect(getThemeOrDefault("dark")).toBe(darkTheme); + }); + + it("getDarkThemes returns only dark themes", () => { + const dark = getDarkThemes(); + expect(dark.every((t) => t.isDark)).toBe(true); + expect(dark).toHaveLength(4); + }); + + it("getLightThemes returns only light themes", () => { + const light = getLightThemes(); + expect(light.every((t) => !t.isDark)).toBe(true); + expect(light).toHaveLength(1); + expect(light[0]?.id).toBe("light"); + }); + + it("isValidThemeId validates correctly", () => { + expect(isValidThemeId("dark")).toBe(true); + expect(isValidThemeId("light")).toBe(true); + expect(isValidThemeId("nope")).toBe(false); + }); + + it("DEFAULT_THEME_ID is dark", () => { + expect(DEFAULT_THEME_ID).toBe("dark"); + }); + + it("getAllThemes returns a copy, not the internal array", () => { + const a = getAllThemes(); + const b = getAllThemes(); + expect(a).not.toBe(b); + expect(a).toEqual(b); + }); +}); + +describe("Theme Definitions", () => { + it.each(ALL_THEMES.map((t) => [t.id, t] as const))( + "%s has all required fields", + (_id, theme: ThemeDefinition) => { + expect(theme.id).toBeTruthy(); + expect(theme.name).toBeTruthy(); + expect(theme.description).toBeTruthy(); + expect(theme.author).toBeTruthy(); + expect(typeof theme.isDark).toBe("boolean"); + expect(theme.colorPreview).toHaveLength(5); + } + ); + + it.each(ALL_THEMES.map((t) => [t.id, t] as const))( + "%s has all required color tokens", + (_id, theme: ThemeDefinition) => { + for (const key of REQUIRED_COLOR_KEYS) { + expect(theme.colors[key], `missing color: ${key}`).toBeTruthy(); + expect(theme.colors[key]).toMatch(/^#[0-9a-f]{6}$/i); + } + } + ); + + it.each(ALL_THEMES.map((t) => [t.id, t] as const))( + "%s has valid shadow definitions", + (_id, theme: ThemeDefinition) => { + expect(theme.shadows.sm).toBeTruthy(); + expect(theme.shadows.md).toBeTruthy(); + expect(theme.shadows.lg).toBeTruthy(); + } + ); + + it.each(ALL_THEMES.map((t) => [t.id, t] as const))( + "%s colorPreview values are valid hex colors", + (_id, theme: ThemeDefinition) => { + for (const color of theme.colorPreview) { + expect(color).toMatch(/^#[0-9a-f]{6}$/i); + } + } + ); + + it("all theme IDs are unique", () => { + const ids = ALL_THEMES.map((t) => t.id); + expect(new Set(ids).size).toBe(ids.length); + }); +}); + +describe("themeToVariables", () => { + it("maps color tokens to --ms-* CSS variables", () => { + const vars = themeToVariables(darkTheme); + expect(vars["--ms-bg-900"]).toBe("#0f141d"); + expect(vars["--ms-blue-500"]).toBe("#2f80ff"); + expect(vars["--ms-text-100"]).toBe("#eef3ff"); + }); + + it("includes shadow variables", () => { + const vars = themeToVariables(darkTheme); + expect(vars["--shadow-sm"]).toBe(darkTheme.shadows.sm); + expect(vars["--shadow-md"]).toBe(darkTheme.shadows.md); + expect(vars["--shadow-lg"]).toBe(darkTheme.shadows.lg); + }); + + it("generates correct number of variables (24 colors + 3 shadows)", () => { + const vars = themeToVariables(darkTheme); + expect(Object.keys(vars)).toHaveLength(27); + }); +}); diff --git a/apps/web/src/themes/dark.ts b/apps/web/src/themes/dark.ts new file mode 100644 index 0000000..6e25b70 --- /dev/null +++ b/apps/web/src/themes/dark.ts @@ -0,0 +1,41 @@ +import type { ThemeDefinition } from "./types"; + +export const darkTheme: ThemeDefinition = { + id: "dark", + name: "Dark", + description: "Default dark theme — deep navy with vibrant accents", + author: "Mosaic Stack", + isDark: true, + colorPreview: ["#0f141d", "#1b2331", "#eef3ff", "#2f80ff", "#8b5cf6"], + colors: { + "bg-950": "#080b12", + "bg-900": "#0f141d", + "bg-850": "#151b26", + "surface-800": "#1b2331", + "surface-750": "#232d3f", + "border-700": "#2f3b52", + "text-100": "#eef3ff", + "text-300": "#c5d0e6", + "text-500": "#8f9db7", + "blue-500": "#2f80ff", + "blue-400": "#56a0ff", + "red-500": "#e5484d", + "red-400": "#f06a6f", + "purple-500": "#8b5cf6", + "purple-400": "#a78bfa", + "teal-500": "#14b8a6", + "teal-400": "#2dd4bf", + "amber-500": "#f59e0b", + "amber-400": "#fbbf24", + "pink-500": "#ec4899", + "emerald-500": "#10b981", + "orange-500": "#f97316", + "cyan-500": "#06b6d4", + "indigo-500": "#6366f1", + }, + shadows: { + sm: "0 1px 2px 0 rgb(0 0 0 / 0.3)", + md: "0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3)", + lg: "0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4)", + }, +}; diff --git a/apps/web/src/themes/dracula.ts b/apps/web/src/themes/dracula.ts new file mode 100644 index 0000000..a82d604 --- /dev/null +++ b/apps/web/src/themes/dracula.ts @@ -0,0 +1,45 @@ +import type { ThemeDefinition } from "./types"; + +/** + * Dracula theme — dark theme with vibrant neon accents. + * Based on https://draculatheme.com/ + */ +export const draculaTheme: ThemeDefinition = { + id: "dracula", + name: "Dracula", + description: "Dark theme with vibrant, neon-inspired accents", + author: "Zeno Rocha", + isDark: true, + colorPreview: ["#282a36", "#44475a", "#f8f8f2", "#7b93db", "#ff79c6"], + colors: { + "bg-950": "#1e1f29", + "bg-900": "#282a36", + "bg-850": "#2d303d", + "surface-800": "#343746", + "surface-750": "#44475a", + "border-700": "#555a78", + "text-100": "#f8f8f2", + "text-300": "#d4d4cd", + "text-500": "#6272a4", + "blue-500": "#7b93db", + "blue-400": "#99aee6", + "red-500": "#ff5555", + "red-400": "#ff7777", + "purple-500": "#bd93f9", + "purple-400": "#caa9fa", + "teal-500": "#50fa7b", + "teal-400": "#69ff93", + "amber-500": "#f1fa8c", + "amber-400": "#f5fca6", + "pink-500": "#ff79c6", + "emerald-500": "#50fa7b", + "orange-500": "#ffb86c", + "cyan-500": "#8be9fd", + "indigo-500": "#8b8fe8", + }, + shadows: { + sm: "0 1px 2px 0 rgb(0 0 0 / 0.3)", + md: "0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3)", + lg: "0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4)", + }, +}; diff --git a/apps/web/src/themes/index.ts b/apps/web/src/themes/index.ts new file mode 100644 index 0000000..14d2db8 --- /dev/null +++ b/apps/web/src/themes/index.ts @@ -0,0 +1,18 @@ +export type { ThemeColors, ThemeDefinition, ThemeShadows, ThemeColorKey } from "./types"; +export { themeToVariables } from "./types"; + +export { darkTheme } from "./dark"; +export { lightTheme } from "./light"; +export { nordTheme } from "./nord"; +export { draculaTheme } from "./dracula"; +export { solarizedDarkTheme } from "./solarized-dark"; + +export { + getAllThemes, + getTheme, + getThemeOrDefault, + getDarkThemes, + getLightThemes, + isValidThemeId, + DEFAULT_THEME_ID, +} from "./registry"; diff --git a/apps/web/src/themes/light.ts b/apps/web/src/themes/light.ts new file mode 100644 index 0000000..387f985 --- /dev/null +++ b/apps/web/src/themes/light.ts @@ -0,0 +1,41 @@ +import type { ThemeDefinition } from "./types"; + +export const lightTheme: ThemeDefinition = { + id: "light", + name: "Light", + description: "Clean light theme — soft blues with crisp contrast", + author: "Mosaic Stack", + isDark: false, + colorPreview: ["#f0f4fc", "#dde4f2", "#0f141d", "#2f80ff", "#8b5cf6"], + colors: { + "bg-950": "#f8faff", + "bg-900": "#f0f4fc", + "bg-850": "#e8edf8", + "surface-800": "#dde4f2", + "surface-750": "#d0d9ec", + "border-700": "#b8c4de", + "text-100": "#0f141d", + "text-300": "#2f3b52", + "text-500": "#5a6a87", + "blue-500": "#2f80ff", + "blue-400": "#56a0ff", + "red-500": "#e5484d", + "red-400": "#f06a6f", + "purple-500": "#8b5cf6", + "purple-400": "#a78bfa", + "teal-500": "#14b8a6", + "teal-400": "#2dd4bf", + "amber-500": "#f59e0b", + "amber-400": "#fbbf24", + "pink-500": "#ec4899", + "emerald-500": "#10b981", + "orange-500": "#f97316", + "cyan-500": "#06b6d4", + "indigo-500": "#6366f1", + }, + shadows: { + sm: "0 1px 2px 0 rgb(0 0 0 / 0.05), 0 1px 3px 0 rgb(0 0 0 / 0.05)", + md: "0 4px 6px -1px rgb(0 0 0 / 0.08), 0 2px 4px -2px rgb(0 0 0 / 0.06)", + lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.08)", + }, +}; diff --git a/apps/web/src/themes/nord.ts b/apps/web/src/themes/nord.ts new file mode 100644 index 0000000..4e8bc1e --- /dev/null +++ b/apps/web/src/themes/nord.ts @@ -0,0 +1,45 @@ +import type { ThemeDefinition } from "./types"; + +/** + * Nord theme — Arctic, north-bluish palette. + * Based on https://www.nordtheme.com/ + */ +export const nordTheme: ThemeDefinition = { + id: "nord", + name: "Nord", + description: "Arctic, north-bluish color palette inspired by the beauty of the arctic", + author: "Arctic Ice Studio", + isDark: true, + colorPreview: ["#2e3440", "#3b4252", "#eceff4", "#5e81ac", "#b48ead"], + colors: { + "bg-950": "#242933", + "bg-900": "#2e3440", + "bg-850": "#333a47", + "surface-800": "#3b4252", + "surface-750": "#434c5e", + "border-700": "#4c566a", + "text-100": "#eceff4", + "text-300": "#d8dee9", + "text-500": "#7b88a1", + "blue-500": "#5e81ac", + "blue-400": "#81a1c1", + "red-500": "#bf616a", + "red-400": "#d08787", + "purple-500": "#b48ead", + "purple-400": "#c4a5bf", + "teal-500": "#8fbcbb", + "teal-400": "#88c0d0", + "amber-500": "#ebcb8b", + "amber-400": "#f0d8a8", + "pink-500": "#c97fba", + "emerald-500": "#a3be8c", + "orange-500": "#d08770", + "cyan-500": "#88c0d0", + "indigo-500": "#7b88a1", + }, + shadows: { + sm: "0 1px 2px 0 rgb(0 0 0 / 0.25)", + md: "0 4px 6px -1px rgb(0 0 0 / 0.35), 0 2px 4px -2px rgb(0 0 0 / 0.25)", + lg: "0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.35)", + }, +}; diff --git a/apps/web/src/themes/registry.ts b/apps/web/src/themes/registry.ts new file mode 100644 index 0000000..de439ea --- /dev/null +++ b/apps/web/src/themes/registry.ts @@ -0,0 +1,50 @@ +import { darkTheme } from "./dark"; +import { draculaTheme } from "./dracula"; +import { lightTheme } from "./light"; +import { nordTheme } from "./nord"; +import { solarizedDarkTheme } from "./solarized-dark"; +import type { ThemeDefinition } from "./types"; + +/** All built-in themes, ordered for display */ +const builtInThemes: ThemeDefinition[] = [ + darkTheme, + lightTheme, + nordTheme, + draculaTheme, + solarizedDarkTheme, +]; + +const themeMap = new Map(builtInThemes.map((t) => [t.id, t])); + +/** Default theme when no preference is set */ +export const DEFAULT_THEME_ID = "dark"; + +/** Get all registered themes */ +export function getAllThemes(): ThemeDefinition[] { + return [...builtInThemes]; +} + +/** Get a theme by ID, or undefined if not found */ +export function getTheme(id: string): ThemeDefinition | undefined { + return themeMap.get(id); +} + +/** Get a theme by ID, falling back to the default dark theme */ +export function getThemeOrDefault(id: string): ThemeDefinition { + return themeMap.get(id) ?? darkTheme; +} + +/** Get only dark themes */ +export function getDarkThemes(): ThemeDefinition[] { + return builtInThemes.filter((t) => t.isDark); +} + +/** Get only light themes */ +export function getLightThemes(): ThemeDefinition[] { + return builtInThemes.filter((t) => !t.isDark); +} + +/** Check if a theme ID is valid */ +export function isValidThemeId(id: string): boolean { + return themeMap.has(id); +} diff --git a/apps/web/src/themes/solarized-dark.ts b/apps/web/src/themes/solarized-dark.ts new file mode 100644 index 0000000..5941584 --- /dev/null +++ b/apps/web/src/themes/solarized-dark.ts @@ -0,0 +1,45 @@ +import type { ThemeDefinition } from "./types"; + +/** + * Solarized Dark theme — precision colors for machines and people. + * Based on https://ethanschoonover.com/solarized/ + */ +export const solarizedDarkTheme: ThemeDefinition = { + id: "solarized-dark", + name: "Solarized Dark", + description: "Precision color palette with selective contrast relationships", + author: "Ethan Schoonover", + isDark: true, + colorPreview: ["#002b36", "#073642", "#fdf6e3", "#268bd2", "#6c71c4"], + colors: { + "bg-950": "#001e26", + "bg-900": "#002b36", + "bg-850": "#04313d", + "surface-800": "#073642", + "surface-750": "#174452", + "border-700": "#2a5565", + "text-100": "#fdf6e3", + "text-300": "#93a1a1", + "text-500": "#657b83", + "blue-500": "#268bd2", + "blue-400": "#4ba2de", + "red-500": "#dc322f", + "red-400": "#e35855", + "purple-500": "#6c71c4", + "purple-400": "#8b8fd3", + "teal-500": "#2aa198", + "teal-400": "#47b5ad", + "amber-500": "#b58900", + "amber-400": "#cba020", + "pink-500": "#d33682", + "emerald-500": "#859900", + "orange-500": "#cb4b16", + "cyan-500": "#36bcb3", + "indigo-500": "#4b66c4", + }, + shadows: { + sm: "0 1px 2px 0 rgb(0 0 0 / 0.35)", + md: "0 4px 6px -1px rgb(0 0 0 / 0.45), 0 2px 4px -2px rgb(0 0 0 / 0.35)", + lg: "0 10px 15px -3px rgb(0 0 0 / 0.55), 0 4px 6px -4px rgb(0 0 0 / 0.45)", + }, +}; diff --git a/apps/web/src/themes/types.ts b/apps/web/src/themes/types.ts new file mode 100644 index 0000000..0ef8c70 --- /dev/null +++ b/apps/web/src/themes/types.ts @@ -0,0 +1,99 @@ +/** + * Mosaic Theme System — Type Definitions + * + * Each theme provides a complete set of CSS variable overrides. + * The token names map to `--ms-{key}` CSS variables in globals.css. + */ + +export interface ThemeColors { + /** Deepest background (e.g. behind modals) */ + "bg-950": string; + /** Main page background */ + "bg-900": string; + /** Elevated background (sidebar, panels) */ + "bg-850": string; + /** Card/panel surface */ + "surface-800": string; + /** Hover/secondary surface */ + "surface-750": string; + /** Border color */ + "border-700": string; + + /** Primary text */ + "text-100": string; + /** Secondary text */ + "text-300": string; + /** Muted/tertiary text */ + "text-500": string; + + /** Primary accent */ + "blue-500": string; + /** Primary accent lighter */ + "blue-400": string; + /** Danger/error */ + "red-500": string; + /** Danger lighter */ + "red-400": string; + /** Purple accent */ + "purple-500": string; + /** Purple lighter */ + "purple-400": string; + /** Success/teal */ + "teal-500": string; + /** Success lighter */ + "teal-400": string; + /** Warning/amber */ + "amber-500": string; + /** Warning lighter */ + "amber-400": string; + /** Pink accent */ + "pink-500": string; + /** Emerald accent */ + "emerald-500": string; + /** Orange accent */ + "orange-500": string; + /** Cyan accent */ + "cyan-500": string; + /** Indigo accent */ + "indigo-500": string; +} + +export interface ThemeShadows { + sm: string; + md: string; + lg: string; +} + +export interface ThemeDefinition { + /** Unique identifier (used in localStorage + UserPreference) */ + id: string; + /** Display name */ + name: string; + /** Short description */ + description: string; + /** Theme author/credit */ + author: string; + /** Whether this is a dark-mode theme */ + isDark: boolean; + /** Five representative colors for preview swatches [bg, surface, text, primary, accent] */ + colorPreview: [string, string, string, string, string]; + /** Color token overrides (maps to --ms-{key} CSS variables) */ + colors: ThemeColors; + /** Shadow overrides */ + shadows: ThemeShadows; +} + +/** The color token keys that map to --ms-{key} CSS variables */ +export type ThemeColorKey = keyof ThemeColors; + +/** All CSS variable names a theme can set (--ms-{colorKey} + --shadow-{sm|md|lg}) */ +export function themeToVariables(theme: ThemeDefinition): Record { + const vars: Record = {}; + for (const [key, value] of Object.entries(theme.colors) as [string, string][]) { + vars[`--ms-${key}`] = value; + } + vars["--shadow-sm"] = theme.shadows.sm; + vars["--shadow-md"] = theme.shadows.md; + vars["--shadow-lg"] = theme.shadows.lg; + return vars; +} diff --git a/docs/TASKS.md b/docs/TASKS.md index da0efbb..3b61918 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -2,33 +2,33 @@ > Single-writer: orchestrator only. Workers read but never modify. -| id | status | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes | -| ----------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ---- | ------ | ------------------------------------------------------ | ------------------------------------------- | ------------ | ---------- | ------------ | -------- | ---- | ----- | -| TW-PLAN-001 | in-progress | Plan MS18 task breakdown, create milestone + issues, populate TASKS.md | — | — | — | | TW-THM-001,TW-WDG-001,TW-EDT-001,TW-KBN-001 | orchestrator | 2026-02-23 | — | 15K | — | | -| TW-THM-001 | not-started | Theme architecture — Create theme definition interface, theme registry, and 5 built-in themes (Dark, Light, Nord, Dracula, Solarized) as TS files | #487 | web | TBD | TW-PLAN-001 | TW-THM-002,TW-THM-003 | worker | — | — | 30K | — | | -| TW-THM-002 | not-started | ThemeProvider upgrade — Load themes dynamically from registry, apply CSS variables, support instant theme switching without page reload | #487 | web | TBD | TW-THM-001 | TW-THM-003,TW-VER-002 | worker | — | — | 25K | — | | -| TW-THM-003 | not-started | Theme selection UI — Settings page section with theme browser, live preview swatches, persist selection to UserPreference.theme via API | #487 | web | TBD | TW-THM-001,TW-THM-002 | TW-VER-002 | worker | — | — | 25K | — | | -| TW-WDG-001 | not-started | Widget definition seeding — Seed 7 existing widgets into widget_definitions table with correct sizing constraints and configSchema | #488 | api | TBD | TW-PLAN-001 | TW-WDG-002 | worker | — | — | 15K | — | | -| TW-WDG-002 | not-started | Dashboard → WidgetGrid migration — Replace hardcoded dashboard layout with WidgetGrid, load/save layout via UserLayout API, default layout on first visit | #488 | web | TBD | TW-WDG-001 | TW-WDG-003,TW-WDG-004,TW-WDG-005 | worker | — | — | 40K | — | | -| TW-WDG-003 | not-started | Widget picker UI — Drawer/dialog to browse available widgets from registry, preview size/description, add to dashboard | #488 | web | TBD | TW-WDG-002 | TW-VER-001 | worker | — | — | 25K | — | | -| TW-WDG-004 | not-started | Widget configuration UI — Per-widget settings dialog using configSchema, configure data source/filters/colors/title | #488 | web | TBD | TW-WDG-002 | TW-VER-001 | worker | — | — | 30K | — | | -| TW-WDG-005 | not-started | Layout management UI — Save/rename/switch/delete layouts, reset to default. UI controls in dashboard header area | #488 | web | TBD | TW-WDG-002 | TW-VER-001 | worker | — | — | 20K | — | | -| TW-EDT-001 | not-started | Tiptap integration — Install @tiptap/react + extensions, build KnowledgeEditor component with toolbar (headings, bold, italic, lists, code, links, tables) | #489 | web | TBD | TW-PLAN-001 | TW-EDT-002 | worker | — | — | 35K | — | | -| TW-EDT-002 | not-started | Markdown round-trip + File Manager integration — Import markdown to Tiptap, export to markdown + HTML. Replace textarea in knowledge create/edit | #489 | web | TBD | TW-EDT-001 | TW-VER-001 | worker | — | — | 30K | — | | -| TW-KBN-001 | not-started | Kanban filtering — Add filter bar (project, assignee, priority, search). Support project-level and user-level views. URL param persistence | #490 | web | TBD | TW-PLAN-001 | TW-VER-001 | worker | — | — | 30K | — | | -| TW-VER-001 | not-started | Tests — Unit tests for new components, update existing tests, fix any regressions | #491 | web | TBD | TW-WDG-003,TW-WDG-004,TW-WDG-005,TW-EDT-002,TW-KBN-001 | TW-VER-002,TW-DOC-001 | worker | — | — | 25K | — | | -| TW-VER-002 | not-started | Theme verification — Verify all 5 themes render correctly on all pages, no broken colors/contrast issues | #491 | web | TBD | TW-THM-003,TW-VER-001 | TW-DOC-001 | worker | — | — | 15K | — | | -| TW-DOC-001 | not-started | Documentation updates — TASKS.md, manifest, scratchpad, PRD status updates | #491 | — | — | TW-VER-001,TW-VER-002 | TW-VER-003 | orchestrator | — | — | 10K | — | | -| TW-VER-003 | not-started | Deploy to Coolify + smoke test — Deploy, verify themes/widgets/editor/kanban all functional, auth working, no console errors | #491 | — | — | TW-DOC-001 | | orchestrator | — | — | 15K | — | | +| id | status | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes | +| ----------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ---- | ---------------------------- | ------------------------------------------------------ | ------------------------------------------- | ------------ | ---------- | ------------ | -------- | ---- | ------------------------------------------ | +| TW-PLAN-001 | done | Plan MS18 task breakdown, create milestone + issues, populate TASKS.md | — | — | — | | TW-THM-001,TW-WDG-001,TW-EDT-001,TW-KBN-001 | orchestrator | 2026-02-23 | 2026-02-23 | 15K | ~12K | Planning complete, all artifacts committed | +| TW-THM-001 | in-progress | Theme architecture — Create theme definition interface, theme registry, and 5 built-in themes (Dark, Light, Nord, Dracula, Solarized) as TS files | #487 | web | feat/ms18-theme-architecture | TW-PLAN-001 | TW-THM-002,TW-THM-003 | worker | 2026-02-23 | — | 30K | — | | +| TW-THM-002 | not-started | ThemeProvider upgrade — Load themes dynamically from registry, apply CSS variables, support instant theme switching without page reload | #487 | web | TBD | TW-THM-001 | TW-THM-003,TW-VER-002 | worker | — | — | 25K | — | | +| TW-THM-003 | not-started | Theme selection UI — Settings page section with theme browser, live preview swatches, persist selection to UserPreference.theme via API | #487 | web | TBD | TW-THM-001,TW-THM-002 | TW-VER-002 | worker | — | — | 25K | — | | +| TW-WDG-001 | not-started | Widget definition seeding — Seed 7 existing widgets into widget_definitions table with correct sizing constraints and configSchema | #488 | api | TBD | TW-PLAN-001 | TW-WDG-002 | worker | — | — | 15K | — | | +| TW-WDG-002 | not-started | Dashboard → WidgetGrid migration — Replace hardcoded dashboard layout with WidgetGrid, load/save layout via UserLayout API, default layout on first visit | #488 | web | TBD | TW-WDG-001 | TW-WDG-003,TW-WDG-004,TW-WDG-005 | worker | — | — | 40K | — | | +| TW-WDG-003 | not-started | Widget picker UI — Drawer/dialog to browse available widgets from registry, preview size/description, add to dashboard | #488 | web | TBD | TW-WDG-002 | TW-VER-001 | worker | — | — | 25K | — | | +| TW-WDG-004 | not-started | Widget configuration UI — Per-widget settings dialog using configSchema, configure data source/filters/colors/title | #488 | web | TBD | TW-WDG-002 | TW-VER-001 | worker | — | — | 30K | — | | +| TW-WDG-005 | not-started | Layout management UI — Save/rename/switch/delete layouts, reset to default. UI controls in dashboard header area | #488 | web | TBD | TW-WDG-002 | TW-VER-001 | worker | — | — | 20K | — | | +| TW-EDT-001 | not-started | Tiptap integration — Install @tiptap/react + extensions, build KnowledgeEditor component with toolbar (headings, bold, italic, lists, code, links, tables) | #489 | web | TBD | TW-PLAN-001 | TW-EDT-002 | worker | — | — | 35K | — | | +| TW-EDT-002 | not-started | Markdown round-trip + File Manager integration — Import markdown to Tiptap, export to markdown + HTML. Replace textarea in knowledge create/edit | #489 | web | TBD | TW-EDT-001 | TW-VER-001 | worker | — | — | 30K | — | | +| TW-KBN-001 | not-started | Kanban filtering — Add filter bar (project, assignee, priority, search). Support project-level and user-level views. URL param persistence | #490 | web | TBD | TW-PLAN-001 | TW-VER-001 | worker | — | — | 30K | — | | +| TW-VER-001 | not-started | Tests — Unit tests for new components, update existing tests, fix any regressions | #491 | web | TBD | TW-WDG-003,TW-WDG-004,TW-WDG-005,TW-EDT-002,TW-KBN-001 | TW-VER-002,TW-DOC-001 | worker | — | — | 25K | — | | +| TW-VER-002 | not-started | Theme verification — Verify all 5 themes render correctly on all pages, no broken colors/contrast issues | #491 | web | TBD | TW-THM-003,TW-VER-001 | TW-DOC-001 | worker | — | — | 15K | — | | +| TW-DOC-001 | not-started | Documentation updates — TASKS.md, manifest, scratchpad, PRD status updates | #491 | — | — | TW-VER-001,TW-VER-002 | TW-VER-003 | orchestrator | — | — | 10K | — | | +| TW-VER-003 | not-started | Deploy to Coolify + smoke test — Deploy, verify themes/widgets/editor/kanban all functional, auth working, no console errors | #491 | — | — | TW-DOC-001 | | orchestrator | — | — | 15K | — | | ## Summary | Metric | Value | | ------------- | ----------------- | | Total tasks | 16 | -| Completed | 0 | -| In Progress | 1 (PLAN-001) | -| Remaining | 15 | +| Completed | 1 (PLAN-001) | +| In Progress | 1 (THM-001) | +| Remaining | 14 | | PRs merged | — | | Issues closed | — | | Milestone | MS18-ThemeWidgets |