first commit

This commit is contained in:
2026-07-12 15:55:18 +05:30
commit ee98026cc5
404 changed files with 44522 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
/**
* Theme system — design tokens that work SSR and client-side.
*
* Tokens are plain CSS custom properties (`--wire-<key>`) so they cascade and
* can be overridden by user CSS. Each theme is a flat token map; the framework
* ships default `light`/`dark` sets and the user's config deep-merges over them.
*
* The server renders `<html data-theme="…">` from the `wire-theme` cookie (no
* flash), and a tiny client runtime toggles/persists it. The reserved token key
* `color-scheme` is emitted as the native CSS property (not a variable) so form
* controls and scrollbars match the theme.
*/
export type ThemeTokens = Record<string, string>;
export interface ThemeConfig {
/** Name of the theme used when no `wire-theme` cookie is present. */
default?: string;
/** Named token maps. Deep-merged over the framework's built-in light/dark. */
themes?: Record<string, ThemeTokens>;
}
export interface ResolvedTheme {
default: string;
names: string[];
themes: Record<string, ThemeTokens>;
}
/** Cookie the resolved theme is read from / persisted to. */
export const THEME_COOKIE = "wire-theme";
export const THEME_CSS_HREF = "/__wrnexus/theme.css";
export const THEME_JS_HREF = "/__wrnexus/theme.js";
/** Built-in themes so components have tokens out of the box. */
export const DEFAULT_THEMES: Record<string, ThemeTokens> = {
light: {
"color-scheme": "light",
"color-bg": "#ffffff",
"color-surface": "#f6f7fb",
"color-surface-2": "#eceff6",
"color-text": "#0b1020",
"color-muted": "#5a6178",
"color-border": "#e2e6f0",
"color-primary": "#2563eb",
"color-primary-hover": "#1d4ed8",
"color-primary-contrast": "#ffffff",
"color-danger": "#dc2626",
"color-success": "#16a34a",
"color-warning": "#d97706",
radius: "8px",
"radius-sm": "5px",
"font-sans": "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
"shadow-1": "0 1px 2px rgba(16,24,40,0.06), 0 1px 3px rgba(16,24,40,0.1)",
},
dark: {
"color-scheme": "dark",
"color-bg": "#0b1020",
"color-surface": "#141a30",
"color-surface-2": "#1c243f",
"color-text": "#e7ecff",
"color-muted": "#9aa6d0",
"color-border": "#ffffff1f",
"color-primary": "#6c8cff",
"color-primary-hover": "#8aa2ff",
"color-primary-contrast": "#0b1020",
"color-danger": "#f87171",
"color-success": "#4ade80",
"color-warning": "#fbbf24",
radius: "10px",
"radius-sm": "6px",
"font-sans": "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
"shadow-1": "0 1px 2px rgba(0,0,0,0.3), 0 4px 16px rgba(0,0,0,0.35)",
},
};
/** Merge the user's theme config over the built-in defaults. */
export function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme {
const themes: Record<string, ThemeTokens> = {};
const names = new Set<string>([
...Object.keys(DEFAULT_THEMES),
...Object.keys(config?.themes ?? {}),
]);
for (const name of names) {
themes[name] = { ...(DEFAULT_THEMES[name] ?? {}), ...(config?.themes?.[name] ?? {}) };
}
const list = Object.keys(themes);
const preferred = config?.default && themes[config.default] ? config.default : undefined;
const fallback = themes.dark ? "dark" : list[0]!;
return { default: preferred ?? fallback, names: list, themes };
}
/** Pick a valid theme name from a cookie value, falling back to the default. */
export function resolveThemeName(cookieValue: string | undefined, theme: ResolvedTheme): string {
return cookieValue && theme.themes[cookieValue] ? cookieValue : theme.default;
}
function tokensToDeclarations(tokens: ThemeTokens): string {
return Object.entries(tokens)
.map(([key, value]) =>
key === "color-scheme" ? `color-scheme:${value};` : `--wire-${key}:${value};`,
)
.join("");
}
/** Generate the theme stylesheet: a `:root` default plus one block per theme. */
export function renderThemeCss(theme: ResolvedTheme): string {
const blocks: string[] = [];
const def = theme.themes[theme.default];
if (def) blocks.push(`:root{${tokensToDeclarations(def)}}`);
for (const name of theme.names) {
blocks.push(`[data-theme="${name}"]{${tokensToDeclarations(theme.themes[name]!)}}`);
}
return blocks.join("\n") + "\n";
}
/**
* Generate the client theme runtime. It exposes `window.wireTheme` and binds
* `[data-wire-theme-toggle]` / `[data-wire-theme-set]` elements. The configured
* theme names are baked in so `toggle()` cycles through them in order.
*/
export function renderThemeRuntime(theme: ResolvedTheme): string {
const names = JSON.stringify(theme.names);
return `(function(){
var COOKIE=${JSON.stringify(THEME_COOKIE)};
var THEMES=${names};
var el=document.documentElement;
function get(){return el.getAttribute("data-theme")||${JSON.stringify(theme.default)};}
function set(name){
if(THEMES.indexOf(name)<0)return;
el.setAttribute("data-theme",name);
document.cookie=COOKIE+"="+encodeURIComponent(name)+";path=/;max-age=31536000;samesite=lax";
}
function toggle(){var i=THEMES.indexOf(get());set(THEMES[(i+1)%THEMES.length]);}
function bind(root){
(root||document).querySelectorAll("[data-wire-theme-toggle]").forEach(function(n){
if(n.__wireThemeBound)return;n.__wireThemeBound=1;
n.addEventListener("click",function(){toggle();});
});
(root||document).querySelectorAll("[data-wire-theme-set]").forEach(function(n){
if(n.__wireThemeBound)return;n.__wireThemeBound=1;
n.addEventListener("click",function(){set(n.getAttribute("data-wire-theme-set"));});
});
}
window.wireTheme={get:get,set:set,toggle:toggle,bind:bind,themes:THEMES};
if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",function(){bind(document);});
else bind(document);
})();
`;
}