diff --git a/bun.lock b/bun.lock
index 67c02f66..036efa1d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -372,7 +372,7 @@
},
"packages/csr": {
"name": "@wrnexus/csr",
- "version": "0.8.41",
+ "version": "0.8.42",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
@@ -717,7 +717,7 @@
},
"packages/styles": {
"name": "@wrnexus/styles",
- "version": "0.8.23",
+ "version": "0.8.26",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -749,7 +749,7 @@
},
"packages/ui": {
"name": "@wrnexus/ui",
- "version": "0.8.84",
+ "version": "0.8.86",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
diff --git a/packages/csr/package.json b/packages/csr/package.json
index 69f992ab..a7f9fe53 100644
--- a/packages/csr/package.json
+++ b/packages/csr/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
- "version": "0.8.41",
+ "version": "0.8.42",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts
index ed00582f..4b9d1243 100644
--- a/packages/csr/src/reactive-runtime.ts
+++ b/packages/csr/src/reactive-runtime.ts
@@ -5542,7 +5542,11 @@ export const REACTIVE_RUNTIME = String.raw`
function parsePostfix() {
var node = parsePrimary();
for (;;) {
- if (match(".")) {
+ var optional = match("?.");
+ if (optional && !is("[") && !is("(")) {
+ var optionalProp = next().value;
+ node = { value: node.value == null ? undefined : node.value[optionalProp], obj: node.value };
+ } else if (match(".")) {
var prop = next().value;
node = { value: node.value == null ? undefined : node.value[prop], obj: node.value };
} else if (match("[")) {
@@ -5617,6 +5621,10 @@ export const REACTIVE_RUNTIME = String.raw`
}
function parseTernary() {
var c = parseOr();
+ while (match("??")) {
+ var fallback = parseOr();
+ c = c == null ? fallback : c;
+ }
if (match("?")) { var a = parseTernary(); expect(":"); var b = parseTernary(); return c ? a : b; }
return c;
}
@@ -5782,6 +5790,12 @@ export const REACTIVE_RUNTIME = String.raw`
var two = input.slice(i, i + 2);
+ if (two === "?." || two === "??") {
+ tokens.push({ type: "operator", value: two });
+ i += 2;
+ continue;
+ }
+
if (
two === "==" ||
two === "!=" ||
diff --git a/packages/csr/test/reactive.test.ts b/packages/csr/test/reactive.test.ts
index df2e2f24..eb4d41fb 100644
--- a/packages/csr/test/reactive.test.ts
+++ b/packages/csr/test/reactive.test.ts
@@ -37,6 +37,14 @@ function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Wind
const REPLACED_GLOBALS = ["window", "document", "location", "fetch", "MutationObserver"];
+test("optional properties and nullish fallbacks hydrate without crashing", () => {
+ const win = mount(`
`);
+ expect(win.document.querySelector("b")?.textContent).toBe("0");
+ expect(win.document.querySelector("i")?.textContent).toBe("0");
+ expect(win.document.querySelector("em")?.textContent).toBe("ok");
+ expect(win.document.querySelector("u")?.textContent).toBe("true");
+});
+
restoreGlobalsAfterAll(REPLACED_GLOBALS);
beforeEach(() => {
diff --git a/packages/styles/package.json b/packages/styles/package.json
index ffc88fbf..d8468f66 100644
--- a/packages/styles/package.json
+++ b/packages/styles/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
- "version": "0.8.23",
+ "version": "0.8.26",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/styles/src/theme.ts b/packages/styles/src/theme.ts
index 338f679b..3efcd071 100644
--- a/packages/styles/src/theme.ts
+++ b/packages/styles/src/theme.ts
@@ -457,89 +457,106 @@ function resolveDefaultAccent(
/** Built-in themes so components have tokens out of the box. */
export const DEFAULT_THEMES: Record = {
+ /*
+ * "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{
diff --git a/packages/styles/test/shared-preferences.test.ts b/packages/styles/test/shared-preferences.test.ts
new file mode 100644
index 00000000..f6627923
--- /dev/null
+++ b/packages/styles/test/shared-preferences.test.ts
@@ -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();
+ 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(); }
+});
diff --git a/packages/styles/test/theme.test.ts b/packages/styles/test/theme.test.ts
index cbd36696..b55c6311 100644
--- a/packages/styles/test/theme.test.ts
+++ b/packages/styles/test/theme.test.ts
@@ -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", () => {
diff --git a/packages/ui/components/BackToTop.wrn b/packages/ui/components/BackToTop.wrn
index 1e0a9550..a4f3382e 100644
--- a/packages/ui/components/BackToTop.wrn
+++ b/packages/ui/components/BackToTop.wrn
@@ -36,6 +36,7 @@ component BackToTop {
data-show-label='{showLabel ? "true" : "false"}'
data-progress='{showProgress ? "true" : "false"}'
data-show='{alwaysVisible || visible}'
+ hidden='{!alwaysVisible && !visible}'
class='wrn-back-to-top {class}'
style='--wrn-back-to-top-progress: {progress}%;'
@window:scroll='visible = window.scrollY >= threshold; progress = Math.min(100, Math.max(0, Math.round((window.scrollY / Math.max(1, document.documentElement.scrollHeight - window.innerHeight)) * 100)))'
@@ -58,6 +59,10 @@ component BackToTop {
}
style {
+ .wrn-back-to-top[hidden] {
+ display: none;
+ }
+
.wrn-back-to-top {
--back-accent: var(--wrn-color-primary);
--back-accent-hover: var(--wrn-color-primary-hover);
diff --git a/packages/ui/components/Card.wrn b/packages/ui/components/Card.wrn
index abc5d6c8..f3016045 100644
--- a/packages/ui/components/Card.wrn
+++ b/packages/ui/components/Card.wrn
@@ -377,9 +377,9 @@ component Card {
width: 100%;
min-width: 0;
border: 1px solid var(--wrn-color-border);
- border-radius: var(--wrn-radius-md);
+ border-radius: var(--wrn-radius);
background: var(--wrn-color-surface);
- box-shadow: none;
+ box-shadow: var(--wrn-shadow-1);
transition:
transform var(--wrn-motion-base) var(--wrn-ease-emphasized),
border-color var(--wrn-motion-base) ease,
@@ -404,7 +404,7 @@ component Card {
align-items: center;
justify-content: space-between;
padding: 0.85rem var(--wrn-card-padding);
- background: var(--wrn-color-surface-2);
+ background: var(--wrn-color-surface-soft, var(--wrn-color-surface-2));
color: var(--wrn-color-muted);
font-size: 0.875rem;
}
diff --git a/packages/ui/components/Tabs.wrn b/packages/ui/components/Tabs.wrn
index c600192f..43b47475 100644
--- a/packages/ui/components/Tabs.wrn
+++ b/packages/ui/components/Tabs.wrn
@@ -223,7 +223,9 @@ component Tabs {
padding: 0.25rem;
border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-md);
- background: var(--wrn-color-surface-soft);
+ background: var(--wrn-color-surface-muted, var(--wrn-color-surface-soft));
+ /* Recessed track; the selected tab sits proud of it like a keycap. */
+ box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.05);
/* A long tablist scrolls rather than wrapping into an unusable stack. */
overflow-x: auto;
}
@@ -279,7 +281,9 @@ component Tabs {
background: var(--wrn-color-surface);
/* Small text on the accent alone misses 4.5:1, so it blends toward the text colour. */
color: color-mix(in srgb, var(--tabs-accent) 72%, var(--wrn-color-text));
- box-shadow: var(--wrn-shadow-1);
+ box-shadow:
+ 0 0 0 1px color-mix(in srgb, var(--wrn-color-border) 80%, transparent),
+ var(--wrn-shadow-1);
}
.wrn-tabs__tab[aria-disabled="true"] {
diff --git a/packages/ui/components/button.wrn b/packages/ui/components/button.wrn
index d85c4b07..9547f0f4 100644
--- a/packages/ui/components/button.wrn
+++ b/packages/ui/components/button.wrn
@@ -205,9 +205,10 @@ label: string = "Button"
font-size: 0.875rem;
font-weight: 600;
line-height: 1;
+ letter-spacing: -0.006em;
border: 1px solid transparent;
border-radius: var(--wrn-radius-sm);
- padding: 0 0.75rem;
+ padding: 0 0.875rem;
cursor: pointer;
text-decoration: none;
min-height: var(--wrn-control-height, 2.5rem);
@@ -219,10 +220,10 @@ label: string = "Button"
transform var(--wrn-motion-base) var(--wrn-ease-emphasized);
}
.wrn-btn:active {
- transform: scale(0.98);
+ transform: translateY(1px);
}
.wrn-btn:focus-visible {
- outline: 2px solid var(--wrn-color-primary);
+ outline: 2px solid color-mix(in srgb, var(--wrn-btn-color) 80%, transparent);
outline-offset: 2px;
}
.wrn-btn--color-primary {
@@ -254,23 +255,48 @@ label: string = "Button"
.wrn-btn--default,
.wrn-btn--solid,
.wrn-btn--primary {
+ /* Keycap: a lit top edge and a weighted bottom border give solid actions
+ a physical press; :active drops the key 1px and flattens the edge. */
color: var(--wrn-btn-contrast);
- background: var(--wrn-btn-color);
- box-shadow: var(--wrn-shadow-sm);
+ background: linear-gradient(
+ 180deg,
+ color-mix(in srgb, var(--wrn-btn-color) 92%, white),
+ var(--wrn-btn-color)
+ );
+ border-color: color-mix(in srgb, var(--wrn-btn-color) 82%, black);
+ border-bottom-color: color-mix(in srgb, var(--wrn-btn-color) 64%, black);
+ box-shadow:
+ var(--wrn-highlight-edge, inset 0 1px 0 rgb(255 255 255 / 0.2)),
+ 0 1px 2px color-mix(in srgb, var(--wrn-btn-color) 30%, transparent);
}
.wrn-btn--variant-default:hover,
.wrn-btn--variant-solid:hover,
.wrn-btn--default:hover,
.wrn-btn--solid:hover,
.wrn-btn--primary:hover {
- background: color-mix(in srgb, var(--wrn-btn-color) 84%, black);
- box-shadow: var(--wrn-shadow-sm);
+ background: linear-gradient(
+ 180deg,
+ color-mix(in srgb, var(--wrn-btn-color) 94%, white),
+ color-mix(in srgb, var(--wrn-btn-color) 88%, black)
+ );
+ box-shadow:
+ var(--wrn-highlight-edge, inset 0 1px 0 rgb(255 255 255 / 0.2)),
+ 0 2px 6px -1px color-mix(in srgb, var(--wrn-btn-color) 38%, transparent);
+ }
+ .wrn-btn--variant-default:active,
+ .wrn-btn--variant-solid:active,
+ .wrn-btn--default:active,
+ .wrn-btn--solid:active,
+ .wrn-btn--primary:active {
+ background: color-mix(in srgb, var(--wrn-btn-color) 86%, black);
+ box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.18);
}
.wrn-btn--variant-outline,
.wrn-btn--outline {
color: var(--wrn-btn-color);
- background: transparent;
- border-color: color-mix(in srgb, var(--wrn-btn-color) 55%, var(--wrn-color-border));
+ background: var(--wrn-color-surface);
+ border-color: color-mix(in srgb, var(--wrn-btn-color) 40%, var(--wrn-color-border));
+ box-shadow: var(--wrn-shadow-1);
}
.wrn-btn--variant-outline:hover,
.wrn-btn--outline:hover {
@@ -304,9 +330,15 @@ label: string = "Button"
}
.wrn-btn--variant-secondary,
.wrn-btn--secondary {
+ /* Neutral keycap: surface body with the same machined edge. */
color: var(--wrn-color-text);
- background: color-mix(in srgb, var(--wrn-btn-color) 10%, var(--wrn-color-surface));
- border-color: color-mix(in srgb, var(--wrn-btn-color) 24%, var(--wrn-color-border));
+ background: linear-gradient(
+ 180deg,
+ var(--wrn-color-surface),
+ color-mix(in srgb, var(--wrn-btn-color) 5%, var(--wrn-color-surface-soft, var(--wrn-color-surface)))
+ );
+ border-color: var(--wrn-color-border);
+ border-bottom-color: var(--wrn-color-border-strong, var(--wrn-color-border));
box-shadow: var(--wrn-shadow-1);
}
.wrn-btn--variant-secondary:hover,
diff --git a/packages/ui/components/input.wrn b/packages/ui/components/input.wrn
index 8a9385b9..c5e673b4 100644
--- a/packages/ui/components/input.wrn
+++ b/packages/ui/components/input.wrn
@@ -295,9 +295,14 @@ component Input {
border-radius: var(--wrn-radius-sm, 6px);
box-shadow: none;
}
+ .wrn-next--field:not([data-invalid="true"]) :where(input, textarea, select):hover:not(:disabled):not(:focus-visible) {
+ border-color: var(--wrn-color-input-border-hover, var(--wrn-color-border-strong));
+ }
.wrn-next--field :where(input, textarea, select):focus-visible {
border-color: var(--wrn-color-primary);
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 16%, transparent);
+ box-shadow:
+ inset 0 1px 2px rgb(0 0 0 / 0.04),
+ 0 0 0 3px color-mix(in srgb, var(--wrn-color-primary) 18%, transparent);
}
.wrn-next--field textarea {
resize: vertical;
@@ -349,12 +354,17 @@ component Input {
min-width: 0;
}
.wrn-next__field-control > :is(input, textarea, select) {
+ /* Recessed well: the inverse of the raised keycap button. */
padding-inline: 0.85rem;
- border-color: var(--wrn-color-border);
+ border-color: var(--wrn-color-input-border, var(--wrn-color-border));
border-radius: var(--wrn-radius-sm);
- background: var(--wrn-color-surface);
+ background: var(--wrn-color-input-background, var(--wrn-color-surface));
+ box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.04);
font-size: 0.8125rem;
}
+ .wrn-next__field-control > :is(input, textarea)::placeholder {
+ color: var(--wrn-color-input-placeholder, var(--wrn-color-muted));
+ }
.wrn-next__field-control[data-has-icon="true"][data-icon-position="start"] > :is(input, textarea, select) {
padding-inline-start: 2.5rem;
}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index f97c252c..c577380d 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
- "version": "0.8.84",
+ "version": "0.8.86",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/ui/test/account-shell-regressions.test.ts b/packages/ui/test/account-shell-regressions.test.ts
index 5fb7397a..1783532c 100644
--- a/packages/ui/test/account-shell-regressions.test.ts
+++ b/packages/ui/test/account-shell-regressions.test.ts
@@ -8,6 +8,20 @@ import { Window } from "happy-dom";
const source = (name: string) => readFileSync(uiComponentPath(name), "utf8");
describe("account shell UI regressions", () => {
+ test("BackToTop is hidden in server HTML unless explicitly always visible", async () => {
+ for (const alwaysVisible of [false, true]) {
+ const html = await renderComponent(source("BackToTop"), { alwaysVisible });
+ const server = new Window();
+ try {
+ server.document.body.innerHTML = html;
+ expect(server.document.querySelector(".wrn-back-to-top")?.hasAttribute("hidden")).toBe(
+ !alwaysVisible,
+ );
+ } finally {
+ await server.happyDOM.close();
+ }
+ }
+ });
test("Drawer has a self-contained close icon and a compact centered mobile header", async () => {
const dom = mountHtml(
await renderComponent(source("Drawer"), { title: "Account center", open: true, duration: 0 }),
diff --git a/packages/ui/ui.css b/packages/ui/ui.css
index ccb21bf3..779121bb 100644
--- a/packages/ui/ui.css
+++ b/packages/ui/ui.css
@@ -111,8 +111,8 @@
transition-duration: 0.01ms !important;
}
}
-:where(button, a):focus-visible {
- outline: 2px solid var(--wrn-color-primary);
+:where(button, a, summary):focus-visible {
+ outline: 2px solid var(--wrn-color-focus);
outline-offset: 2px;
}
/* --- Component-system foundations --------------------------------------- */
@@ -122,15 +122,15 @@
}
.wrn-type--display {
font-size: clamp(2.25rem, 7vw, 4.75rem);
- line-height: 1.02;
- font-weight: 800;
- letter-spacing: -0.04em;
+ line-height: 1;
+ font-weight: 750;
+ letter-spacing: -0.045em;
}
.wrn-type--heading {
font-size: clamp(1.25rem, 2vw, 1.75rem);
- line-height: 1.1;
- font-weight: 600;
- letter-spacing: -0.025em;
+ line-height: 1.15;
+ font-weight: 650;
+ letter-spacing: -0.028em;
}
.wrn-type--lead {
font-size: clamp(1.05rem, 2vw, 1.25rem);
@@ -172,8 +172,8 @@
}
.wrn-label {
color: var(--wrn-color-text);
- font-size: 0.875rem;
- font-weight: 650;
+ font-size: 0.8125rem;
+ font-weight: 600;
line-height: 1.4;
}
.wrn-required,
@@ -210,23 +210,24 @@
display: grid;
place-items: center;
padding: 1rem;
- background: rgb(0 0 0 / 0.55);
- backdrop-filter: blur(4px);
+ background: rgb(12 13 16 / 0.52);
+ backdrop-filter: blur(6px);
}
.wrn-dialog {
width: min(100%, 32rem);
max-height: calc(100dvh - 2rem);
overflow: auto;
- padding: clamp(1.25rem, 4vw, 2rem);
+ padding: clamp(1.25rem, 4vw, 1.75rem);
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
border: 1px solid var(--wrn-color-border);
- border-radius: var(--wrn-radius);
- box-shadow: var(--wrn-shadow-1);
+ border-radius: var(--wrn-radius-lg);
+ box-shadow: var(--wrn-shadow-3);
}
.wrn-dialog__title {
margin: 0;
- font-size: 1.35rem;
+ font-size: 1.25rem;
+ font-weight: 650;
line-height: 1.25;
}
.wrn-dialog__description {
@@ -287,10 +288,11 @@
box-shadow: var(--wrn-shadow-1);
}
.wrn-eyebrow {
- color: var(--wrn-color-primary);
- font-size: 0.75rem;
- font-weight: 750;
- letter-spacing: 0.1em;
+ color: var(--wrn-color-primary-text);
+ font-family: var(--wrn-font-mono, monospace);
+ font-size: 0.6875rem;
+ font-weight: 600;
+ letter-spacing: 0.08em;
text-transform: uppercase;
}
.wrn-auth-card > .wrn-eyebrow {
@@ -579,8 +581,8 @@
}
.wrn-well,
.wrn-inset-panel {
- background: var(--wrn-color-surface-2);
- box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.06);
+ background: var(--wrn-color-surface-soft);
+ box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.05);
}
.wrn-scroll-area {
max-width: 100%;