chore(release): redesign UI tokens and publish styles 0.8.26, ui 0.8.86
Visual-only redesign ("instrument panel"): cool graphite neutrals for light
and dark, layered hairline elevation, larger radii, a mono token for
eyebrows/data, keycap-style solid and secondary buttons, recessed input
wells and tab tracks, a stronger dialog, and a theme-driven focus outline.
No props, outputs or behaviour change.
Also records the already-published csr 0.8.42, styles 0.8.25 and ui 0.8.85
changes (optional chaining/nullish in the reactive runtime, shared
theme/accent cookie sync, BackToTop hidden attribute).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4eaf416be
commit
1179fc2810
16 files changed
+322
-108
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.8.23",
|
||||
"version": "0.8.26",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
+124
-57
@@ -457,89 +457,106 @@ function resolveDefaultAccent(
|
||||
|
||||
/** Built-in themes so components have tokens out of the box. */
|
||||
export const DEFAULT_THEMES: Record<string, ThemeTokens> = {
|
||||
/*
|
||||
* "Instrument panel" neutrals: cool graphite greys with a faint blue cast so
|
||||
* every accent palette sits on them without muddying. Elevation is layered
|
||||
* (a hairline contact shadow plus a soft ambient one) instead of one blur.
|
||||
*/
|
||||
light: {
|
||||
"color-scheme": "light",
|
||||
"color-bg": "#f7f8fa",
|
||||
"color-background": "#f7f8fa",
|
||||
"color-foreground": "#20232b",
|
||||
"color-bg": "#f2f3f6",
|
||||
"color-background": "#f2f3f6",
|
||||
"color-foreground": "#14161b",
|
||||
"color-surface-raised": "#ffffff",
|
||||
"color-surface-muted": "#eef0f4",
|
||||
"color-surface-soft": "#f6f7f9",
|
||||
"color-text-muted": "#606776",
|
||||
"color-text-subtle": "#6b7280",
|
||||
"color-border-strong": "#b9c0cc",
|
||||
"color-code-background": "#20232b",
|
||||
"color-code-surface": "#141a30",
|
||||
"color-code-text": "#e7ecff",
|
||||
"color-code-muted": "#9aa6d0",
|
||||
"color-code-border": "#27304d",
|
||||
"shadow-sm": "0 1px 2px rgba(16,24,40,0.06)",
|
||||
"shadow-md": "0 8px 24px rgba(16,24,40,0.10)",
|
||||
"shadow-lg": "0 20px 50px rgba(16,24,40,0.14)",
|
||||
"color-surface-muted": "#eaecf0",
|
||||
"color-surface-soft": "#f7f8fa",
|
||||
"color-text-muted": "#565d6b",
|
||||
"color-text-subtle": "#6c7381",
|
||||
"color-border-strong": "#c2c7d0",
|
||||
"color-code-background": "#14161b",
|
||||
"color-code-surface": "#0f1116",
|
||||
"color-code-text": "#e6e9f0",
|
||||
"color-code-muted": "#8e96a6",
|
||||
"color-code-border": "#262a33",
|
||||
"shadow-sm": "0 1px 0 rgba(20,22,27,0.03),0 1px 2px rgba(20,22,27,0.06)",
|
||||
"shadow-md": "0 1px 2px rgba(20,22,27,0.05),0 8px 24px -6px rgba(20,22,27,0.12)",
|
||||
"shadow-lg": "0 2px 6px rgba(20,22,27,0.06),0 24px 56px -12px rgba(20,22,27,0.22)",
|
||||
"space-section": "5rem",
|
||||
"space-section-sm": "3.5rem",
|
||||
"container-max": "80rem",
|
||||
"color-surface": "#ffffff",
|
||||
"color-surface-2": "#eef0f4",
|
||||
"color-text": "#20232b",
|
||||
"color-muted": "#606776",
|
||||
"color-border": "#e1e4ea",
|
||||
"color-surface-2": "#eaecf0",
|
||||
"color-text": "#14161b",
|
||||
"color-muted": "#565d6b",
|
||||
"color-border": "#dde0e6",
|
||||
...paletteTokens(THEME_PALETTES.blue, "light"),
|
||||
radius: "10px",
|
||||
"radius-sm": "6px",
|
||||
radius: "12px",
|
||||
"radius-sm": "8px",
|
||||
"radius-md": "10px",
|
||||
"radius-lg": "12px",
|
||||
"radius-lg": "16px",
|
||||
"control-height-sm": "2rem",
|
||||
"control-height": "2.5rem",
|
||||
"control-height-lg": "2.75rem",
|
||||
"control-height-lg": "2.875rem",
|
||||
"space-panel": "1.25rem",
|
||||
"font-size-ui": "0.875rem",
|
||||
"font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
|
||||
"shadow-1": "0 1px 3px rgba(16,24,40,0.05)",
|
||||
"shadow-2": "0 8px 24px rgba(16,24,40,0.10)",
|
||||
"shadow-3": "0 20px 50px rgba(16,24,40,0.14)",
|
||||
"font-mono": '"JetBrains Mono","SFMono-Regular",ui-monospace,Menlo,Consolas,monospace',
|
||||
"shadow-1": "0 1px 0 rgba(20,22,27,0.03),0 1px 2px rgba(20,22,27,0.06)",
|
||||
"shadow-2": "0 1px 2px rgba(20,22,27,0.05),0 8px 24px -6px rgba(20,22,27,0.12)",
|
||||
"shadow-3": "0 2px 6px rgba(20,22,27,0.06),0 24px 56px -12px rgba(20,22,27,0.22)",
|
||||
// Signature: the machined top edge on solid controls, and a two-ring focus
|
||||
// halo that separates the ring from whatever surface the control sits on.
|
||||
"highlight-edge": "inset 0 1px 0 rgba(255,255,255,0.22)",
|
||||
"shadow-focus":
|
||||
"0 0 0 2px var(--wrn-color-bg),0 0 0 4px color-mix(in srgb,var(--wrn-color-focus) 72%,transparent)",
|
||||
"color-selection": "color-mix(in srgb,var(--wrn-color-primary) 22%,transparent)",
|
||||
},
|
||||
dark: {
|
||||
"color-scheme": "dark",
|
||||
"color-bg": "#101114",
|
||||
"color-background": "#101114",
|
||||
"color-foreground": "#f0f1f4",
|
||||
"color-surface-raised": "#22252d",
|
||||
"color-surface-muted": "#242730",
|
||||
"color-surface-soft": "#1c1f26",
|
||||
"color-text-muted": "#b5bac5",
|
||||
"color-text-subtle": "#959cab",
|
||||
"color-border-strong": "#555d6c",
|
||||
"color-code-background": "#050505",
|
||||
"color-code-surface": "#101010",
|
||||
"color-code-text": "#f0f1f4",
|
||||
"color-code-muted": "#a7aeba",
|
||||
"color-code-border": "#ffffff24",
|
||||
"shadow-sm": "0 1px 2px rgba(0,0,0,0.28)",
|
||||
"shadow-md": "0 10px 30px rgba(0,0,0,0.35)",
|
||||
"shadow-lg": "0 24px 64px rgba(0,0,0,0.45)",
|
||||
"color-bg": "#0c0d10",
|
||||
"color-background": "#0c0d10",
|
||||
"color-foreground": "#edeef1",
|
||||
"color-surface-raised": "#1f2229",
|
||||
"color-surface-muted": "#1c1f25",
|
||||
"color-surface-soft": "#121418",
|
||||
"color-text-muted": "#a9afba",
|
||||
"color-text-subtle": "#8a919e",
|
||||
"color-border-strong": "#3d434e",
|
||||
"color-code-background": "#08090b",
|
||||
"color-code-surface": "#0e1013",
|
||||
"color-code-text": "#edeef1",
|
||||
"color-code-muted": "#8e96a6",
|
||||
"color-code-border": "#ffffff1f",
|
||||
"shadow-sm": "0 1px 0 rgba(0,0,0,0.3),0 1px 2px rgba(0,0,0,0.3)",
|
||||
"shadow-md": "0 1px 2px rgba(0,0,0,0.3),0 10px 30px -6px rgba(0,0,0,0.5)",
|
||||
"shadow-lg": "0 2px 6px rgba(0,0,0,0.3),0 28px 64px -12px rgba(0,0,0,0.65)",
|
||||
"space-section": "5rem",
|
||||
"space-section-sm": "3.5rem",
|
||||
"container-max": "80rem",
|
||||
"color-surface": "#181a20",
|
||||
"color-surface-2": "#20232b",
|
||||
"color-text": "#f0f1f4",
|
||||
"color-muted": "#a7aeba",
|
||||
"color-border": "#30343e",
|
||||
"color-surface": "#15171c",
|
||||
"color-surface-2": "#1c1f25",
|
||||
"color-text": "#edeef1",
|
||||
"color-muted": "#a3a9b5",
|
||||
"color-border": "#272b33",
|
||||
...paletteTokens(THEME_PALETTES.blue, "dark"),
|
||||
radius: "10px",
|
||||
"radius-sm": "6px",
|
||||
radius: "12px",
|
||||
"radius-sm": "8px",
|
||||
"radius-md": "10px",
|
||||
"radius-lg": "12px",
|
||||
"radius-lg": "16px",
|
||||
"control-height-sm": "2rem",
|
||||
"control-height": "2.5rem",
|
||||
"control-height-lg": "2.75rem",
|
||||
"control-height-lg": "2.875rem",
|
||||
"space-panel": "1.25rem",
|
||||
"font-size-ui": "0.875rem",
|
||||
"font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
|
||||
"shadow-1": "0 1px 3px rgba(0,0,0,0.18)",
|
||||
"shadow-2": "0 10px 30px rgba(0,0,0,0.35)",
|
||||
"shadow-3": "0 24px 64px rgba(0,0,0,0.45)",
|
||||
"font-mono": '"JetBrains Mono","SFMono-Regular",ui-monospace,Menlo,Consolas,monospace',
|
||||
"shadow-1": "0 1px 0 rgba(0,0,0,0.3),0 1px 2px rgba(0,0,0,0.3)",
|
||||
"shadow-2": "0 1px 2px rgba(0,0,0,0.3),0 10px 30px -6px rgba(0,0,0,0.5)",
|
||||
"shadow-3": "0 2px 6px rgba(0,0,0,0.3),0 28px 64px -12px rgba(0,0,0,0.65)",
|
||||
"highlight-edge": "inset 0 1px 0 rgba(255,255,255,0.12)",
|
||||
"shadow-focus":
|
||||
"0 0 0 2px var(--wrn-color-bg),0 0 0 4px color-mix(in srgb,var(--wrn-color-focus) 80%,transparent)",
|
||||
"color-selection": "color-mix(in srgb,var(--wrn-color-primary) 34%,transparent)",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -620,6 +637,7 @@ function selectorValue(value: string): string {
|
||||
function globalThemeCss(): string {
|
||||
return (
|
||||
'\nhtml,body{font-family:var(--wrn-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n' +
|
||||
"::selection{background:var(--wrn-color-selection);}\n" +
|
||||
"wrn-slot{display:contents;}\n" +
|
||||
"[data-for]{display:none !important;}\n"
|
||||
);
|
||||
@@ -754,6 +772,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
||||
|
||||
function writeCookie(name,value,options){
|
||||
options=options||{};
|
||||
if(options.domain && options.domain!=="localhost")deleteCookie(name,{path:options.path||"/"});
|
||||
var maxAge=options.maxAge===undefined?MAX_AGE:Math.max(0,Math.floor(options.maxAge));
|
||||
document.cookie=encodeURIComponent(name)+"="+encodeURIComponent(value)+
|
||||
";max-age="+maxAge+cookieOptions(options);
|
||||
@@ -783,6 +802,18 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
||||
}
|
||||
};
|
||||
|
||||
// Retire host-only preferences from older apps. An existing parent-domain
|
||||
// value wins; otherwise preserve the user's legacy choice in the new scope.
|
||||
function migrateCookie(name,options){
|
||||
if(!options.domain || options.domain==="localhost")return;
|
||||
var previous=readCookie(name);
|
||||
deleteCookie(name,{path:options.path||"/"});
|
||||
if(readCookie(name)===null && previous!==null)writeCookie(name,previous,options);
|
||||
}
|
||||
migrateCookie(THEME_COOKIE,THEME_COOKIE_OPTIONS);
|
||||
migrateCookie(ACCENT_COOKIE,ACCENT_COOKIE_OPTIONS);
|
||||
migrateCookie("wrn-lang",configuredCookieOptions("language"));
|
||||
|
||||
function syncStylesheet(themeName,accentName){
|
||||
var link=document.querySelector("link[data-wrnexus-theme]");
|
||||
if(!link)return;
|
||||
@@ -818,6 +849,9 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
||||
if(THEMES.indexOf(name)<0)return;
|
||||
el.setAttribute("data-theme",name);
|
||||
writeCookie(THEME_COOKIE,name,THEME_COOKIE_OPTIONS);
|
||||
writeCookie("wrn-theme-mode","manual",THEME_COOKIE_OPTIONS);
|
||||
deleteCookie("wrn-theme-system");
|
||||
deleteCookie("wrn-theme-system",THEME_COOKIE_OPTIONS);
|
||||
syncStylesheet(name,getAccent());
|
||||
syncThemeButtons(document);
|
||||
window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:name}}));
|
||||
@@ -893,6 +927,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
||||
get:getTheme,
|
||||
set:setTheme,
|
||||
toggle:toggleTheme,
|
||||
followSystem:function(){writeCookie("wrn-theme-mode","system",THEME_COOKIE_OPTIONS);syncSharedPreferences();},
|
||||
bind:bind,
|
||||
themes:THEMES.slice()
|
||||
};
|
||||
@@ -904,7 +939,39 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
||||
colors:ACCENTS.slice()
|
||||
};
|
||||
|
||||
window.addEventListener("wrnexus:navigated",function(){bind(document);});
|
||||
function syncSharedPreferences(){
|
||||
var themeName=readCookie(THEME_COOKIE),accentName=readCookie(ACCENT_COOKIE);
|
||||
if(readCookie("wrn-theme-mode")==="system" && window.matchMedia){
|
||||
themeName=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";
|
||||
if(readCookie(THEME_COOKIE)!==themeName){
|
||||
writeCookie(THEME_COOKIE,themeName,THEME_COOKIE_OPTIONS);
|
||||
writeCookie("wrn-theme-system",themeName,THEME_COOKIE_OPTIONS);
|
||||
}
|
||||
}
|
||||
var changed=false;
|
||||
if(themeName && THEMES.indexOf(themeName)>=0 && themeName!==getTheme()){
|
||||
el.setAttribute("data-theme",themeName);changed=true;
|
||||
window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:themeName,shared:true}}));
|
||||
}
|
||||
if(accentName && ACCENTS.indexOf(accentName)>=0 && accentName!==getAccent()){
|
||||
el.setAttribute("data-accent",accentName);changed=true;
|
||||
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:accentName,shared:true}}));
|
||||
}
|
||||
if(changed){syncStylesheet(getTheme(),getAccent());syncThemeButtons(document);syncAccentButtons(document);}
|
||||
var language=readCookie("wrn-lang"),i18n=window.__wrnI18n;
|
||||
if(language && i18n && i18n.langs && i18n.langs.indexOf(language)>=0 && language!==i18n.lang)location.reload();
|
||||
}
|
||||
window.addEventListener("focus",syncSharedPreferences);
|
||||
window.addEventListener("pageshow",syncSharedPreferences);
|
||||
document.addEventListener("visibilitychange",function(){if(!document.hidden)syncSharedPreferences();});
|
||||
if(window.cookieStore)window.cookieStore.addEventListener("change",syncSharedPreferences);
|
||||
if(window.matchMedia)window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",syncSharedPreferences);
|
||||
// Cookie Store is not available in every supported browser. Poll only visible
|
||||
// pages, without rewriting cookies or emitting a cross-tab feedback loop.
|
||||
window.setInterval(function(){if(!document.hidden)syncSharedPreferences();},1500);
|
||||
syncSharedPreferences();
|
||||
|
||||
window.addEventListener("wrnexus:navigated",function(){bind(document);syncSharedPreferences();});
|
||||
if(document.readyState==="loading"){
|
||||
document.addEventListener("DOMContentLoaded",function(){bind(document);});
|
||||
}else{
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { Browser } from "happy-dom";
|
||||
import { renderThemeRuntime, resolveThemeConfig } from "../src/theme.ts";
|
||||
|
||||
test("shared preferences replace stale host cookies and update another open app", async () => {
|
||||
const browser = new Browser({ settings: { disableCSSFileLoading: true } });
|
||||
try {
|
||||
const first = browser.newPage();
|
||||
first.url = "https://account.example.com/";
|
||||
const second = browser.newPage();
|
||||
second.url = "https://link.example.com/";
|
||||
const a = first.mainFrame.window;
|
||||
const b = second.mainFrame.window;
|
||||
// Happy DOM coalesces equal-name host/domain cookies. Model their distinct
|
||||
// browser storage keys so the legacy-shadow migration is exercised.
|
||||
const jar = new Map<string, { name: string; value: string; domain: string; hostOnly: boolean }>();
|
||||
function cookies(win: typeof a) {
|
||||
Object.defineProperty(win.document, "cookie", {
|
||||
get() { return [...jar.values()].filter(c => c.hostOnly ? c.domain === win.location.hostname : win.location.hostname === c.domain || win.location.hostname.endsWith("." + c.domain)).map(c => `${c.name}=${c.value}`).join("; "); },
|
||||
set(raw: string) {
|
||||
const [pair, ...options] = raw.split(";");
|
||||
const split = pair!.indexOf("=");
|
||||
const name = pair!.slice(0, split), value = pair!.slice(split + 1);
|
||||
const domainOption = options.find(v => v.trim().toLowerCase().startsWith("domain="));
|
||||
const domain = domainOption ? domainOption.trim().slice(7).replace(/^\./, "") : win.location.hostname;
|
||||
const key = `${name}|${domain}`;
|
||||
if(options.some(v => v.trim().toLowerCase() === "max-age=0")) jar.delete(key);
|
||||
else jar.set(key, { name, value, domain, hostOnly: !domainOption });
|
||||
},
|
||||
});
|
||||
}
|
||||
cookies(a); cookies(b);
|
||||
a.document.cookie = "wrn-theme=light;path=/";
|
||||
b.document.cookie = "wrn-theme=light;path=/";
|
||||
a.document.cookie = "wrn-theme=dark;path=/;domain=.example.com;secure";
|
||||
a.document.cookie = "wrn-accent=rose;path=/;domain=.example.com;secure";
|
||||
const js = renderThemeRuntime(resolveThemeConfig({ palette: "emerald" }, { defaults: { domain: ".example.com", secure: true } }));
|
||||
for (const win of [a, b]) {
|
||||
win.document.documentElement.setAttribute("data-theme", "light");
|
||||
win.document.documentElement.setAttribute("data-accent", "emerald");
|
||||
win.eval(js);
|
||||
expect(win.document.documentElement.getAttribute("data-theme")).toBe("dark");
|
||||
expect(win.document.cookie.match(/wrn-theme=/g)?.length).toBe(1);
|
||||
}
|
||||
a.eval('wrnTheme.set("light");wrnAccent.set("violet");wrnCookies.set("wrn-lang","hi","language")');
|
||||
expect(b.document.cookie).toContain("wrn-theme-mode=manual");
|
||||
b.dispatchEvent(new b.Event("focus"));
|
||||
expect(b.document.documentElement.getAttribute("data-theme")).toBe("light");
|
||||
expect(b.document.documentElement.getAttribute("data-accent")).toBe("violet");
|
||||
expect(b.document.cookie).toContain("wrn-lang=hi");
|
||||
a.eval('wrnTheme.followSystem()');
|
||||
expect(b.document.cookie).toContain("wrn-theme-mode=system");
|
||||
const outsider = browser.newPage();
|
||||
outsider.url = "https://unrelated.example.net/";
|
||||
cookies(outsider.mainFrame.window);
|
||||
expect(outsider.mainFrame.window.document.cookie).toBe("");
|
||||
} finally { await browser.close(); }
|
||||
});
|
||||
@@ -62,9 +62,9 @@ test("built-in themes use Plus Jakarta Sans and layered charcoal dark surfaces",
|
||||
'"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
|
||||
);
|
||||
expect(t.themes.dark["font-sans"]).toBe('"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif');
|
||||
expect(t.themes.dark["color-bg"]).toBe("#101114");
|
||||
expect(t.themes.dark["color-surface"]).toBe("#181a20");
|
||||
expect(t.themes.dark["color-surface-2"]).toBe("#20232b");
|
||||
expect(t.themes.dark["color-bg"]).toBe("#0c0d10");
|
||||
expect(t.themes.dark["color-surface"]).toBe("#15171c");
|
||||
expect(t.themes.dark["color-surface-2"]).toBe("#1c1f25");
|
||||
});
|
||||
|
||||
test("resolveThemeName validates against configured names", () => {
|
||||
|
||||
Reference in new issue
Block a user