complete performance and reliability follow-ups

This commit is contained in:
2026-08-09 23:04:36 +05:30
parent 8f19a5eb2b
commit 232d8e6734
31 changed files with 525 additions and 726 deletions
+3
View File
@@ -61,10 +61,13 @@ export {
THEME_PALETTE_NAMES,
THEME_COOKIE,
THEME_CSS_HREF,
THEME_CSS_PREFIX,
THEME_JS_HREF,
activeThemeCssHref,
resolveThemeConfig,
resolveThemeName,
renderThemeCss,
renderActiveThemeCss,
renderThemeRuntime,
defineThemeTokens,
themeVar,
+52 -30
View File
@@ -121,6 +121,7 @@ export interface ResolvedTheme {
export const THEME_COOKIE = "wire-theme";
export const ACCENT_COOKIE = "wire-accent";
export const THEME_CSS_HREF = "/__wrnexus/theme.css";
export const THEME_CSS_PREFIX = "/__wrnexus/theme/";
export const THEME_JS_HREF = "/__wrnexus/theme.js";
/**
@@ -519,6 +520,44 @@ function selectorValue(value: string): string {
return JSON.stringify(value);
}
function globalThemeCss(): string {
return (
'\nhtml,body{font-family:var(--wire-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n' +
":root{--wire-radius-sm:0.55rem;--wire-radius-md:0.9rem;--wire-radius-lg:1.35rem;" +
"--wire-shadow-1:0 1px 2px color-mix(in srgb, black 22%, transparent)," +
"0 10px 30px color-mix(in srgb, black 14%, transparent);}\n" +
"wrn-slot{display:contents;}\n" +
"[data-for]{display:none !important;}\n"
);
}
/** URL for the small stylesheet containing only one active theme/accent pair. */
export function activeThemeCssHref(themeName: string, accentName?: string): string {
return `${THEME_CSS_PREFIX}${encodeURIComponent(themeName)}/${accentName ? encodeURIComponent(accentName) : "_"}.css`;
}
/** Render only the tokens needed for the current SSR-selected theme and accent. */
export function renderActiveThemeCss(
theme: ResolvedTheme,
themeName: string,
accentName?: string,
): string {
const selectedTheme = theme.themes[themeName];
if (!selectedTheme) throw new Error(`Unknown theme '${themeName}'.`);
const blocks = [`:root{${tokensToDeclarations(selectedTheme)}}`];
if (accentName) {
if (!theme.accentNames.includes(accentName as ThemePaletteName)) {
throw new Error(`Unknown theme accent '${accentName}'.`);
}
blocks.push(
`:root{${tokensToDeclarations(
paletteTokens(THEME_PALETTES[accentName as ThemePaletteName], themeScheme(selectedTheme)),
)}}`,
);
}
return blocks.join("\n") + globalThemeCss();
}
/**
* Generate the theme stylesheet.
*
@@ -559,36 +598,7 @@ export function renderThemeCss(theme: ResolvedTheme): string {
}
}
return (
blocks.join("\n") +
'\nhtml,body{font-family:var(--wire-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n' +
// <wrn-slot> wraps content spliced into a component's <slot> so the client
// runtime can tell which scope authored it. It is a pure ownership marker
// and must never introduce a box: an unknown element would otherwise
// default to display:inline and break the component's own flex/grid layout.
/*
* Shape and elevation tokens the Wire UI stylesheet depends on.
*
* ui.css references --wire-radius-sm (51 times) and --wire-radius-md (21)
* but nothing ever defined them, so every component using them fell back
* to square corners in any app that did not declare them itself.
* --wire-shadow-1 had the same problem: the showcase declared it
* privately, so the library looked right there and flat everywhere else.
* Defining them with the rest of the theme keeps a component looking the
* same in every app; an app can still override them.
*/
":root{--wire-radius-sm:0.55rem;--wire-radius-md:0.9rem;--wire-radius-lg:1.35rem;" +
"--wire-shadow-1:0 1px 2px color-mix(in srgb, black 22%, transparent)," +
"0 10px 30px color-mix(in srgb, black 14%, transparent);}\n" +
"wrn-slot{display:contents;}\n" +
// A [data-for] element is the loop TEMPLATE, not a rendered row: the server
// ships it with mustaches unresolved ({item.title}) and hydration replaces
// it with a comment marker. Painted as-is it flashes one blank, literal
// row on every page load before the runtime boots. Hiding it costs
// nothing after hydration -- the runtime strips data-for from the clones
// it renders, so this only ever matches the template itself.
"[data-for]{display:none !important;}\n"
);
return blocks.join("\n") + globalThemeCss();
}
/**
@@ -603,6 +613,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
const names = JSON.stringify(theme.names);
const accentNames = JSON.stringify(theme.accentNames);
const defaultAccent = JSON.stringify(theme.defaultAccent ?? null);
const cssPrefix = JSON.stringify(THEME_CSS_PREFIX);
return `(function(){
var THEME_COOKIE=${JSON.stringify(THEME_COOKIE)};
@@ -611,6 +622,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
var ACCENTS=${accentNames};
var DEFAULT_THEME=${JSON.stringify(theme.default)};
var DEFAULT_ACCENT=${defaultAccent};
var THEME_CSS_PREFIX=${cssPrefix};
var MAX_AGE=31536000;
var el=document.documentElement;
@@ -637,6 +649,13 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
document.cookie=encodeURIComponent(name)+"=;path=/;max-age=0;samesite=lax"+secure;
}
function syncStylesheet(themeName,accentName){
var link=document.querySelector("link[data-wrnexus-theme]");
if(!link)return;
var accent=accentName?encodeURIComponent(accentName):"_";
link.href=THEME_CSS_PREFIX+encodeURIComponent(themeName)+"/"+accent+".css";
}
function getTheme(){
var current=el.getAttribute("data-theme")||readCookie(THEME_COOKIE)||DEFAULT_THEME;
return THEMES.indexOf(current)>=0?current:DEFAULT_THEME;
@@ -665,6 +684,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
if(THEMES.indexOf(name)<0)return;
el.setAttribute("data-theme",name);
writeCookie(THEME_COOKIE,name);
syncStylesheet(name,getAccent());
syncThemeButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:name}}));
}
@@ -678,6 +698,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
if(ACCENTS.indexOf(name)<0)return;
el.setAttribute("data-accent",name);
writeCookie(ACCENT_COOKIE,name);
syncStylesheet(getTheme(),name);
syncAccentButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:name}}));
}
@@ -685,6 +706,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
function clearAccent(){
el.removeAttribute("data-accent");
deleteCookie(ACCENT_COOKIE);
syncStylesheet(getTheme(),null);
syncAccentButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:null}}));
}
+13
View File
@@ -5,6 +5,8 @@ import {
resolveThemeConfig,
resolveThemeName,
renderThemeCss,
renderActiveThemeCss,
activeThemeCssHref,
renderThemeRuntime,
} from "../src/index.ts";
@@ -85,6 +87,17 @@ test("renderThemeCss emits :root + per-theme blocks and --wire-* vars", () => {
expect(css).toContain("font-family:var(--wire-font-sans");
});
test("active theme CSS contains only the selected theme and accent", () => {
const theme = resolveThemeConfig({ default: "dark", palette: "rose" });
const full = renderThemeCss(theme);
const active = renderActiveThemeCss(theme, "dark", "rose");
expect(active).toContain(":root{");
expect(active).not.toContain('[data-theme="light"]');
expect(active.length).toBeLessThan(full.length / 4);
expect(activeThemeCssHref("dark", "rose")).toBe("/__wrnexus/theme/dark/rose.css");
expect(() => renderActiveThemeCss(theme, "missing")).toThrow("Unknown theme");
});
test("renderThemeRuntime bakes the theme names for cycling", () => {
const t = resolveThemeConfig();
const js = renderThemeRuntime(t);