mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-10-23 03:51:13 +00:00
feat(theme): introduce semantic design tokens and integrate with Tailwind and Vuetify themes
This commit is contained in:
5 files changed
+362
-111
No files matched your search
@@ -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(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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],
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user