feat(web): add theme definition system with 5 built-in themes (#493)
All checks were successful
ci/woodpecker/push/web Pipeline was successful
All checks were successful
ci/woodpecker/push/web Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #493.
This commit is contained in:
170
apps/web/src/themes/__tests__/registry.test.ts
Normal file
170
apps/web/src/themes/__tests__/registry.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
41
apps/web/src/themes/dark.ts
Normal file
41
apps/web/src/themes/dark.ts
Normal file
@@ -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)",
|
||||
},
|
||||
};
|
||||
45
apps/web/src/themes/dracula.ts
Normal file
45
apps/web/src/themes/dracula.ts
Normal file
@@ -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)",
|
||||
},
|
||||
};
|
||||
18
apps/web/src/themes/index.ts
Normal file
18
apps/web/src/themes/index.ts
Normal file
@@ -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";
|
||||
41
apps/web/src/themes/light.ts
Normal file
41
apps/web/src/themes/light.ts
Normal file
@@ -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)",
|
||||
},
|
||||
};
|
||||
45
apps/web/src/themes/nord.ts
Normal file
45
apps/web/src/themes/nord.ts
Normal file
@@ -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)",
|
||||
},
|
||||
};
|
||||
50
apps/web/src/themes/registry.ts
Normal file
50
apps/web/src/themes/registry.ts
Normal file
@@ -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<string, ThemeDefinition>(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);
|
||||
}
|
||||
45
apps/web/src/themes/solarized-dark.ts
Normal file
45
apps/web/src/themes/solarized-dark.ts
Normal file
@@ -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)",
|
||||
},
|
||||
};
|
||||
99
apps/web/src/themes/types.ts
Normal file
99
apps/web/src/themes/types.ts
Normal file
@@ -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<string, string> {
|
||||
const vars: Record<string, string> = {};
|
||||
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;
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user