feat(theme): introduce semantic design tokens and integrate with Tailwind and Vuetify themes

This commit is contained in:
Ivan committed 2026-04-14 19:30:51 -05:00
1 parent 301c35c3d2
commit 2e043ec1c2
5 files changed
+362 -111

No files matched your search

+4 -28
View File
@@ -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(),
},
});
+55 -82
View File
@@ -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);
}
@@ -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<string, string>} */
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<string, string>} */
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"],
},
},
};
}
+6 -1
View File
@@ -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],
};
+82
View File
@@ -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);
});
});