From 2e043ec1c244bb6849ae89c39d1a77a1896610e5 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 14 Apr 2026 19:30:51 -0500 Subject: [PATCH] feat(theme): introduce semantic design tokens and integrate with Tailwind and Vuetify themes --- meshchatx/src/frontend/main.js | 32 +-- meshchatx/src/frontend/style.css | 137 +++++------- meshchatx/src/frontend/theme/designTokens.js | 215 +++++++++++++++++++ tailwind.config.js | 7 +- tests/frontend/designTokens.test.js | 82 +++++++ 5 files changed, 362 insertions(+), 111 deletions(-) create mode 100644 meshchatx/src/frontend/theme/designTokens.js create mode 100644 tests/frontend/designTokens.test.js diff --git a/meshchatx/src/frontend/main.js b/meshchatx/src/frontend/main.js index 4989050..54f86f1 100644 --- a/meshchatx/src/frontend/main.js +++ b/meshchatx/src/frontend/main.js @@ -4,6 +4,9 @@ import { createI18n } from "vue-i18n"; import vClickOutside from "click-outside-vue3"; import DOMPurify from "dompurify"; import "./style.css"; +import { injectMeshchatThemeVariables, vuetifyThemesFromTokens } from "./theme/designTokens.js"; + +injectMeshchatThemeVariables(); window.DOMPurify = DOMPurify; import "@mdi/font/css/materialdesignicons.css"; @@ -35,34 +38,7 @@ import { createVuetify } from "vuetify"; const vuetify = createVuetify({ theme: { defaultTheme: "light", - themes: { - light: { - dark: false, - colors: { - background: "#f8fafc", - surface: "#ffffff", - primary: "#2563eb", - secondary: "#475569", - error: "#dc2626", - info: "#0284c7", - success: "#16a34a", - warning: "#f97316", - }, - }, - dark: { - dark: true, - colors: { - background: "#09090b", - surface: "#18181b", - primary: "#60a5fa", - secondary: "#94a3b8", - error: "#f87171", - info: "#38bdf8", - success: "#34d399", - warning: "#fb923c", - }, - }, - }, + themes: vuetifyThemesFromTokens(), }, }); diff --git a/meshchatx/src/frontend/style.css b/meshchatx/src/frontend/style.css index df7ad7a..a31b622 100644 --- a/meshchatx/src/frontend/style.css +++ b/meshchatx/src/frontend/style.css @@ -4,7 +4,7 @@ * { scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; + scrollbar-color: var(--mc-scrollbar-thumb) var(--mc-scrollbar-track); } *::-webkit-scrollbar { @@ -13,150 +13,124 @@ } *::-webkit-scrollbar-track { - background: #e2e8f0; + background: var(--mc-scrollbar-track); } *::-webkit-scrollbar-thumb { - background-color: #94a3b8; + background-color: var(--mc-scrollbar-thumb); border-radius: 999px; - border: 2px solid #e2e8f0; -} - -.dark * { - scrollbar-color: #52525b #18181b; -} - -.dark *::-webkit-scrollbar-track { - background: #18181b; -} - -.dark *::-webkit-scrollbar-thumb { - background-color: #52525b; - border-color: #18181b; + border: 2px solid var(--mc-scrollbar-thumb-border); } .glass-card { - @apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-xl px-4 py-4; + @apply bg-sem-glass backdrop-blur border border-sem-border-card rounded-3xl shadow-xl px-4 py-4; } .input-field { - @apply bg-gray-50/90 dark:bg-zinc-900/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-2xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 block w-full p-2.5 text-gray-900 dark:text-gray-100 transition; + @apply bg-sem-surface-muted border border-sem-border text-sm rounded-2xl focus:ring-2 focus:ring-sem-focus focus:border-sem-focus-border block w-full p-2.5 text-sem-fg transition; } select.input-field option { - @apply bg-white text-gray-900 dark:bg-zinc-900 dark:text-gray-100; + @apply bg-sem-surface text-sem-fg; } .primary-chip { - @apply inline-flex items-center justify-center gap-x-2 rounded-full bg-blue-600 px-4 py-2 text-xs font-bold text-white shadow-sm transition-all !important; + @apply inline-flex items-center justify-center gap-x-2 rounded-full bg-sem-accent px-4 py-2 text-xs font-bold text-white shadow-sm transition-all !important; } .primary-chip:hover { - @apply bg-blue-500 -translate-y-0.5 !important; + @apply bg-sem-accent-hover -translate-y-0.5 !important; } .primary-chip:active { @apply scale-95 translate-y-0 !important; } .secondary-chip { - @apply inline-flex items-center justify-center gap-x-2 rounded-full border border-gray-300 px-4 py-2 text-xs font-bold shadow-sm transition-all duration-200 !important; - background-color: white !important; - color: #374151 !important; /* text-gray-700 */ + @apply inline-flex items-center justify-center gap-x-2 rounded-full px-4 py-2 text-xs font-bold shadow-sm transition-all duration-200 !important; + background-color: var(--mc-secondary-chip-bg) !important; + color: var(--mc-secondary-chip-text) !important; + border: 1px solid var(--mc-secondary-chip-border) !important; } .secondary-chip:hover { - background-color: #f9fafb !important; /* bg-gray-50 */ - color: #111827 !important; /* text-gray-900 */ - border-color: #60a5fa !important; /* border-blue-400 */ + background-color: var(--mc-secondary-chip-hover-bg) !important; + color: var(--mc-secondary-chip-hover-text) !important; + border-color: var(--mc-secondary-chip-hover-border) !important; @apply -translate-y-0.5 shadow-md !important; } .secondary-chip:active { @apply scale-95 translate-y-0 shadow-sm !important; } -.dark .secondary-chip { - background-color: #18181b !important; /* bg-zinc-900 */ - color: #f4f4f5 !important; /* text-zinc-100 */ - border-color: #3f3f46 !important; /* border-zinc-700 */ -} .dark .secondary-chip:hover { - background-color: #27272a !important; /* bg-zinc-800 */ - color: #ffffff !important; /* text-white */ - border-color: #3b82f6 !important; /* border-blue-500 */ - @apply shadow-lg shadow-blue-500/10 !important; + @apply shadow-lg dark:shadow-blue-500/10 !important; } .danger-chip { - @apply inline-flex items-center justify-center gap-x-2 rounded-full border border-red-200 px-4 py-2 text-xs font-bold transition-all duration-200 !important; - background-color: #fef2f2 !important; /* bg-red-50 */ - color: #b91c1c !important; /* text-red-700 */ + @apply inline-flex items-center justify-center gap-x-2 rounded-full px-4 py-2 text-xs font-bold transition-all duration-200 !important; + background-color: var(--mc-danger-chip-bg) !important; + color: var(--mc-danger-chip-text) !important; + border: 1px solid var(--mc-danger-chip-border) !important; } .danger-chip:hover { - background-color: #fee2e2 !important; /* bg-red-100 */ - color: #991b1b !important; /* text-red-800 */ + background-color: var(--mc-danger-chip-hover-bg) !important; + color: var(--mc-danger-chip-hover-text) !important; @apply -translate-y-0.5 shadow-md !important; } .danger-chip:active { @apply scale-95 translate-y-0 !important; } -.dark .danger-chip { - background-color: rgba(69, 10, 10, 0.2) !important; /* bg-red-950/20 */ - color: #f87171 !important; /* text-red-400 */ - border-color: rgba(127, 29, 29, 0.5) !important; /* border-red-900/50 */ -} .dark .danger-chip:hover { - background-color: rgba(127, 29, 29, 0.4) !important; /* bg-red-900/40 */ - color: #fca5a5 !important; /* text-red-300 */ - @apply shadow-lg shadow-red-500/10 !important; + @apply shadow-lg dark:shadow-red-500/10 !important; } .glass-label { - @apply mb-1 text-sm font-semibold text-gray-800 dark:text-gray-200; + @apply mb-1 text-sm font-semibold text-sem-fg-secondary dark:text-sem-fg-label; } .monospace-field { - @apply font-mono tracking-tight text-gray-900 dark:text-white; + @apply font-mono tracking-tight text-sem-fg dark:text-white; } .metric-row { - @apply grid gap-3 sm:grid-cols-2 text-gray-800 dark:text-gray-100; + @apply grid gap-3 sm:grid-cols-2 text-sem-fg-secondary dark:text-sem-fg; } .metric-value { - @apply text-lg font-semibold text-gray-900 dark:text-white; + @apply text-lg font-semibold text-sem-fg dark:text-white; } .address-card { - @apply relative border border-gray-200 dark:border-zinc-800 rounded-2xl bg-white/80 dark:bg-zinc-900/70 p-4 space-y-2; + @apply relative border border-sem-border-card rounded-2xl bg-sem-surface-raised p-4 space-y-2; } .address-card__label { - @apply text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400; + @apply text-xs uppercase tracking-wide text-sem-fg-muted; } .address-card__value { - @apply text-sm text-gray-900 dark:text-white break-words pr-16; + @apply text-sm text-sem-fg dark:text-white break-words pr-16; } .address-card__action { - @apply absolute top-3 right-3 inline-flex items-center gap-1 rounded-full border border-gray-200 px-3 py-1 text-[10px] font-bold text-gray-700 bg-white shadow-sm transition-all !important; + @apply absolute top-3 right-3 inline-flex items-center gap-1 rounded-full px-3 py-1 text-[10px] font-bold shadow-sm transition-all !important; + background-color: var(--mc-address-action-bg) !important; + color: var(--mc-address-action-text) !important; + border: 1px solid var(--mc-address-action-border) !important; } .address-card__action:hover { - @apply bg-gray-50 text-gray-900 border-blue-400 !important; -} -.dark .address-card__action { - @apply border-zinc-700 text-zinc-100 bg-zinc-900 !important; -} -.dark .address-card__action:hover { - @apply bg-zinc-800 text-white border-blue-500 !important; + background-color: var(--mc-address-action-hover-bg) !important; + color: var(--mc-address-action-hover-text) !important; + border-color: var(--mc-address-action-hover-border) !important; } .file-input { - @apply block w-full text-xs cursor-pointer rounded-lg border border-gray-300 dark:border-zinc-700 bg-gray-50 dark:bg-zinc-900 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-400 dark:focus:ring-blue-500; + @apply block w-full text-xs cursor-pointer rounded-lg border bg-sem-file-input-bg text-sem-file-input-text focus:outline-none focus:ring-2 focus:ring-sem-focus; + border-color: var(--mc-file-input-border); } .file-input::file-selector-button { - @apply mr-2 border-0 rounded-md bg-blue-600 text-white px-3 py-1 text-xs font-semibold cursor-pointer hover:bg-blue-500 transition; + @apply mr-2 border-0 rounded-md bg-sem-accent text-white px-3 py-1 text-xs font-semibold cursor-pointer hover:bg-sem-accent-hover transition; } .dark .file-input::file-selector-button { - @apply bg-blue-500 hover:bg-blue-400 text-white; + @apply bg-sem-accent hover:bg-sem-accent-hover text-white; } .no-scrollbar::-webkit-scrollbar { @@ -178,21 +152,13 @@ select.input-field option { } .custom-scrollbar::-webkit-scrollbar-thumb { - background: #cbd5e1; + background: var(--mc-custom-scroll-thumb); border-radius: 10px; border: none; } -.dark .custom-scrollbar::-webkit-scrollbar-thumb { - background: #3f3f46; -} - .custom-scrollbar::-webkit-scrollbar-thumb:hover { - background: #94a3b8; -} - -.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover { - background: #52525b; + background: var(--mc-custom-scroll-thumb-hover); } @keyframes spin-reverse { @@ -209,17 +175,24 @@ select.input-field option { } .context-menu-panel { - @apply fixed min-w-48 rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 shadow-xl; + @apply fixed min-w-48 rounded-xl border shadow-xl bg-sem-surface; + border-color: var(--mc-border); } .context-item { - @apply w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-zinc-200 hover:bg-gray-100 dark:hover:bg-zinc-800 flex items-center gap-2; + @apply w-full text-left px-3 py-2 text-sm flex items-center gap-2; + color: var(--mc-context-item-text); +} +.context-item:hover { + background-color: var(--mc-context-hover); } .context-menu-divider { - @apply border-t border-gray-100 dark:border-zinc-800 my-1; + border-color: var(--mc-context-divider); + @apply border-t my-1; } .context-menu-section-label { - @apply px-3 py-1.5 text-[10px] font-black text-gray-400 dark:text-zinc-500 uppercase tracking-widest; + @apply px-3 py-1.5 text-[10px] font-black uppercase tracking-widest; + color: var(--mc-context-section); } diff --git a/meshchatx/src/frontend/theme/designTokens.js b/meshchatx/src/frontend/theme/designTokens.js new file mode 100644 index 0000000..fa2e934 --- /dev/null +++ b/meshchatx/src/frontend/theme/designTokens.js @@ -0,0 +1,215 @@ +/** + * Semantic design tokens for MeshChatX. CSS custom properties (--mc-*) are the + * single source of truth for colors shared by Tailwind (`sem-*`), global CSS in + * style.css, and the Vuetify theme (see vuetifyThemesFromTokens). + */ + +const MESHCHAT_THEME_STYLE_ID = "meshchat-design-tokens"; + +/** @type {Record} */ +export const MESHCHAT_THEME_VARIABLES_LIGHT = { + "--mc-canvas": "#f8fafc", + "--mc-surface": "#ffffff", + "--mc-border": "#e5e7eb", + "--mc-border-card": "#e5e7eb", + "--mc-border-strong": "#d1d5db", + "--mc-text": "#111827", + "--mc-text-secondary": "#1f2937", + "--mc-text-muted": "#6b7280", + "--mc-text-label": "#1f2937", + "--mc-focus": "#60a5fa", + "--mc-focus-border": "#60a5fa", + "--mc-accent": "#2563eb", + "--mc-accent-hover": "#3b82f6", + "--mc-scrollbar-thumb": "#94a3b8", + "--mc-scrollbar-track": "#e2e8f0", + "--mc-scrollbar-thumb-border": "#e2e8f0", + "--mc-glass-surface": "rgb(255 255 255 / 0.95)", + "--mc-surface-muted": "rgb(249 250 251 / 0.9)", + "--mc-surface-raised": "rgb(255 255 255 / 0.8)", + "--mc-secondary-chip-bg": "#ffffff", + "--mc-secondary-chip-text": "#374151", + "--mc-secondary-chip-border": "#d1d5db", + "--mc-secondary-chip-hover-bg": "#f9fafb", + "--mc-secondary-chip-hover-text": "#111827", + "--mc-secondary-chip-hover-border": "#60a5fa", + "--mc-danger-chip-bg": "#fef2f2", + "--mc-danger-chip-text": "#b91c1c", + "--mc-danger-chip-border": "#fecaca", + "--mc-danger-chip-hover-bg": "#fee2e2", + "--mc-danger-chip-hover-text": "#991b1b", + "--mc-input-option-bg": "#ffffff", + "--mc-input-option-text": "#111827", + "--mc-file-input-bg": "#f9fafb", + "--mc-file-input-text": "#1f2937", + "--mc-file-input-border": "#d1d5db", + "--mc-address-action-bg": "#ffffff", + "--mc-address-action-text": "#374151", + "--mc-address-action-border": "#e5e7eb", + "--mc-address-action-hover-bg": "#f9fafb", + "--mc-address-action-hover-text": "#111827", + "--mc-address-action-hover-border": "#60a5fa", + "--mc-custom-scroll-thumb": "#cbd5e1", + "--mc-custom-scroll-thumb-hover": "#94a3b8", + "--mc-context-hover": "#f3f4f6", + "--mc-context-divider": "#f3f4f6", + "--mc-context-section": "#9ca3af", + "--mc-context-item-text": "#374151", + "--mc-info": "#0284c7", + "--mc-success": "#16a34a", + "--mc-warning": "#f97316", + "--mc-error": "#dc2626", +}; + +/** @type {Record} */ +export const MESHCHAT_THEME_VARIABLES_DARK = { + "--mc-canvas": "#09090b", + "--mc-surface": "#18181b", + "--mc-border": "#3f3f46", + "--mc-border-card": "#27272a", + "--mc-border-strong": "#52525b", + "--mc-text": "#f3f4f6", + "--mc-text-secondary": "#ffffff", + "--mc-text-muted": "#9ca3af", + "--mc-text-label": "#e5e7eb", + "--mc-focus": "#3b82f6", + "--mc-focus-border": "#3b82f6", + "--mc-accent": "#60a5fa", + "--mc-accent-hover": "#3b82f6", + "--mc-scrollbar-thumb": "#52525b", + "--mc-scrollbar-track": "#18181b", + "--mc-scrollbar-thumb-border": "#18181b", + "--mc-glass-surface": "rgb(24 24 27 / 0.85)", + "--mc-surface-muted": "rgb(24 24 27 / 0.8)", + "--mc-surface-raised": "rgb(24 24 27 / 0.7)", + "--mc-secondary-chip-bg": "#18181b", + "--mc-secondary-chip-text": "#f4f4f5", + "--mc-secondary-chip-border": "#3f3f46", + "--mc-secondary-chip-hover-bg": "#27272a", + "--mc-secondary-chip-hover-text": "#ffffff", + "--mc-secondary-chip-hover-border": "#3b82f6", + "--mc-danger-chip-bg": "rgb(69 10 10 / 0.2)", + "--mc-danger-chip-text": "#f87171", + "--mc-danger-chip-border": "rgb(127 29 29 / 0.5)", + "--mc-danger-chip-hover-bg": "rgb(127 29 29 / 0.4)", + "--mc-danger-chip-hover-text": "#fca5a5", + "--mc-input-option-bg": "#18181b", + "--mc-input-option-text": "#f3f4f6", + "--mc-file-input-bg": "#18181b", + "--mc-file-input-text": "#f3f4f6", + "--mc-file-input-border": "#3f3f46", + "--mc-address-action-bg": "#18181b", + "--mc-address-action-text": "#f4f4f5", + "--mc-address-action-border": "#3f3f46", + "--mc-address-action-hover-bg": "#27272a", + "--mc-address-action-hover-text": "#ffffff", + "--mc-address-action-hover-border": "#3b82f6", + "--mc-custom-scroll-thumb": "#3f3f46", + "--mc-custom-scroll-thumb-hover": "#52525b", + "--mc-context-hover": "#27272a", + "--mc-context-divider": "#27272a", + "--mc-context-section": "#71717a", + "--mc-context-item-text": "#e4e4e7", + "--mc-info": "#38bdf8", + "--mc-success": "#34d399", + "--mc-warning": "#fb923c", + "--mc-error": "#f87171", +}; + +/** + * Tailwind theme.extend.colors.sem — uses the same --mc-* variables as style.css. + * Keep keys in sync with usages in style.css and Vue (e.g. bg-sem-canvas). + */ +export function tailwindSemanticColorExtend() { + return { + canvas: "var(--mc-canvas)", + surface: "var(--mc-surface)", + border: "var(--mc-border)", + "border-card": "var(--mc-border-card)", + "border-strong": "var(--mc-border-strong)", + fg: "var(--mc-text)", + "fg-secondary": "var(--mc-text-secondary)", + "fg-muted": "var(--mc-text-muted)", + "fg-label": "var(--mc-text-label)", + accent: "var(--mc-accent)", + "accent-hover": "var(--mc-accent-hover)", + focus: "var(--mc-focus)", + glass: "var(--mc-glass-surface)", + "surface-muted": "var(--mc-surface-muted)", + "surface-raised": "var(--mc-surface-raised)", + info: "var(--mc-info)", + success: "var(--mc-success)", + warning: "var(--mc-warning)", + danger: "var(--mc-error)", + "focus-border": "var(--mc-focus-border)", + "file-input-bg": "var(--mc-file-input-bg)", + "file-input-text": "var(--mc-file-input-text)", + }; +} + +function serializeVarsBlock(vars) { + return Object.entries(vars) + .map(([k, v]) => ` ${k}: ${v};`) + .join("\n"); +} + +/** + * Injects :root and .dark variable blocks. Idempotent. Call once from main.js before mount. + * @param {Document} [doc] + */ +export function injectMeshchatThemeVariables(doc = typeof document !== "undefined" ? document : null) { + if (!doc?.head) { + return; + } + const existing = doc.getElementById(MESHCHAT_THEME_STYLE_ID); + if (existing) { + existing.remove(); + } + const el = doc.createElement("style"); + el.id = MESHCHAT_THEME_STYLE_ID; + el.textContent = [ + ":root {", + serializeVarsBlock(MESHCHAT_THEME_VARIABLES_LIGHT), + "}", + ".dark {", + serializeVarsBlock(MESHCHAT_THEME_VARIABLES_DARK), + "}", + ].join("\n"); + doc.head.appendChild(el); +} + +/** + * Vuetify 3 theme definition derived from the same hexes as CSS light/dark tokens. + */ +export function vuetifyThemesFromTokens() { + const L = MESHCHAT_THEME_VARIABLES_LIGHT; + const D = MESHCHAT_THEME_VARIABLES_DARK; + return { + light: { + dark: false, + colors: { + background: L["--mc-canvas"], + surface: L["--mc-surface"], + primary: L["--mc-accent"], + secondary: "#475569", + error: L["--mc-error"], + info: L["--mc-info"], + success: L["--mc-success"], + warning: L["--mc-warning"], + }, + }, + dark: { + dark: true, + colors: { + background: D["--mc-canvas"], + surface: D["--mc-surface"], + primary: D["--mc-accent"], + secondary: "#94a3b8", + error: D["--mc-error"], + info: D["--mc-info"], + success: D["--mc-success"], + warning: D["--mc-warning"], + }, + }, + }; +} diff --git a/tailwind.config.js b/tailwind.config.js index 430b325..ff3710a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,4 +1,5 @@ import formsPlugin from "@tailwindcss/forms"; +import { tailwindSemanticColorExtend } from "./meshchatx/src/frontend/theme/designTokens.js"; const frontendRoot = "./meshchatx/src/frontend"; @@ -11,7 +12,11 @@ module.exports = { "./meshchatx/src/backend/markdown_renderer.py", ], theme: { - extend: {}, + extend: { + colors: { + sem: tailwindSemanticColorExtend(), + }, + }, }, plugins: [formsPlugin], }; diff --git a/tests/frontend/designTokens.test.js b/tests/frontend/designTokens.test.js new file mode 100644 index 0000000..0a326c1 --- /dev/null +++ b/tests/frontend/designTokens.test.js @@ -0,0 +1,82 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { + injectMeshchatThemeVariables, + vuetifyThemesFromTokens, + MESHCHAT_THEME_VARIABLES_LIGHT, + MESHCHAT_THEME_VARIABLES_DARK, + tailwindSemanticColorExtend, +} from "../../meshchatx/src/frontend/theme/designTokens.js"; + +describe("design tokens", () => { + it("light and dark variable maps define the same keys", () => { + expect(Object.keys(MESHCHAT_THEME_VARIABLES_LIGHT).sort()).toEqual( + Object.keys(MESHCHAT_THEME_VARIABLES_DARK).sort() + ); + }); + + it("preserves legacy light palette anchors", () => { + expect(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-canvas"]).toBe("#f8fafc"); + expect(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-surface"]).toBe("#ffffff"); + expect(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-accent"]).toBe("#2563eb"); + expect(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-error"]).toBe("#dc2626"); + expect(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-info"]).toBe("#0284c7"); + expect(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-success"]).toBe("#16a34a"); + expect(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-warning"]).toBe("#f97316"); + }); + + it("preserves legacy dark palette anchors", () => { + expect(MESHCHAT_THEME_VARIABLES_DARK["--mc-canvas"]).toBe("#09090b"); + expect(MESHCHAT_THEME_VARIABLES_DARK["--mc-surface"]).toBe("#18181b"); + expect(MESHCHAT_THEME_VARIABLES_DARK["--mc-accent"]).toBe("#60a5fa"); + expect(MESHCHAT_THEME_VARIABLES_DARK["--mc-error"]).toBe("#f87171"); + expect(MESHCHAT_THEME_VARIABLES_DARK["--mc-info"]).toBe("#38bdf8"); + expect(MESHCHAT_THEME_VARIABLES_DARK["--mc-success"]).toBe("#34d399"); + expect(MESHCHAT_THEME_VARIABLES_DARK["--mc-warning"]).toBe("#fb923c"); + }); + + it("vuetifyThemesFromTokens tracks CSS canvas/surface/accent and status colors", () => { + const t = vuetifyThemesFromTokens(); + expect(t.light.colors.background).toBe(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-canvas"]); + expect(t.light.colors.surface).toBe(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-surface"]); + expect(t.light.colors.primary).toBe(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-accent"]); + expect(t.light.colors.error).toBe(MESHCHAT_THEME_VARIABLES_LIGHT["--mc-error"]); + expect(t.dark.colors.background).toBe(MESHCHAT_THEME_VARIABLES_DARK["--mc-canvas"]); + expect(t.dark.colors.surface).toBe(MESHCHAT_THEME_VARIABLES_DARK["--mc-surface"]); + expect(t.dark.colors.primary).toBe(MESHCHAT_THEME_VARIABLES_DARK["--mc-accent"]); + expect(t.dark.colors.error).toBe(MESHCHAT_THEME_VARIABLES_DARK["--mc-error"]); + }); + + it("tailwindSemanticColorExtend uses var() for every semantic color", () => { + const sem = tailwindSemanticColorExtend(); + for (const v of Object.values(sem)) { + expect(v).toMatch(/^var\(--mc-/); + } + }); +}); + +describe("injectMeshchatThemeVariables", () => { + beforeEach(() => { + document.getElementById("meshchat-design-tokens")?.remove(); + }); + + afterEach(() => { + document.getElementById("meshchat-design-tokens")?.remove(); + }); + + it("injects :root and .dark blocks with expected canvas values", () => { + injectMeshchatThemeVariables(document); + const el = document.getElementById("meshchat-design-tokens"); + expect(el).toBeTruthy(); + const text = el.textContent; + expect(text).toContain(":root"); + expect(text).toContain(".dark"); + expect(text).toContain("--mc-canvas: #f8fafc"); + expect(text).toContain("--mc-canvas: #09090b"); + }); + + it("is idempotent", () => { + injectMeshchatThemeVariables(document); + injectMeshchatThemeVariables(document); + expect(document.querySelectorAll("#meshchat-design-tokens").length).toBe(1); + }); +});