release: WRNexusJS 0.5.11

This commit is contained in:
2026-07-30 15:03:52 +05:30
parent d1b0c55b53
commit f37303b22d
67 changed files with 530 additions and 213 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.", "description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+13 -13
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.", "description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -28,18 +28,18 @@
"wrnexus": "./dist/index.js" "wrnexus": "./dist/index.js"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.5.10", "@wrnexus/core": "^0.5.11",
"@wrnexus/router": "^0.5.10", "@wrnexus/router": "^0.5.11",
"@wrnexus/csr": "^0.5.10", "@wrnexus/csr": "^0.5.11",
"@wrnexus/compiler": "^0.5.10", "@wrnexus/compiler": "^0.5.11",
"@wrnexus/styles": "^0.5.10", "@wrnexus/styles": "^0.5.11",
"@wrnexus/dev-server": "^0.5.10", "@wrnexus/dev-server": "^0.5.11",
"@wrnexus/ui": "^0.5.10", "@wrnexus/ui": "^0.5.11",
"@wrnexus/validation": "^0.5.10", "@wrnexus/validation": "^0.5.11",
"@wrnexus/i18n": "^0.5.10", "@wrnexus/i18n": "^0.5.11",
"@wrnexus/db": "^0.5.10", "@wrnexus/db": "^0.5.11",
"@wrnexus/plugin": "^0.5.10", "@wrnexus/plugin": "^0.5.11",
"@wrnexus/syntax": "^0.5.10" "@wrnexus/syntax": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.", "description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/syntax": "^0.5.10" "@wrnexus/syntax": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.", "description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.", "description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.5.10" "@wrnexus/core": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.", "description": "@wrnexus/db — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+15 -15
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.", "description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -25,20 +25,20 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.5.10", "@wrnexus/core": "^0.5.11",
"@wrnexus/dev-toolbar": "^0.5.10", "@wrnexus/dev-toolbar": "^0.5.11",
"@wrnexus/router": "^0.5.10", "@wrnexus/router": "^0.5.11",
"@wrnexus/ssr": "^0.5.10", "@wrnexus/ssr": "^0.5.11",
"@wrnexus/csr": "^0.5.10", "@wrnexus/csr": "^0.5.11",
"@wrnexus/compiler": "^0.5.10", "@wrnexus/compiler": "^0.5.11",
"@wrnexus/styles": "^0.5.10", "@wrnexus/styles": "^0.5.11",
"@wrnexus/ui": "^0.5.10", "@wrnexus/ui": "^0.5.11",
"@wrnexus/validation": "^0.5.10", "@wrnexus/validation": "^0.5.11",
"@wrnexus/i18n": "^0.5.10", "@wrnexus/i18n": "^0.5.11",
"@wrnexus/db": "^0.5.10", "@wrnexus/db": "^0.5.11",
"@wrnexus/pubsub": "^0.5.10", "@wrnexus/pubsub": "^0.5.11",
"@wrnexus/uploader": "^0.5.10", "@wrnexus/uploader": "^0.5.11",
"@wrnexus/plugin": "^0.5.10" "@wrnexus/plugin": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-toolbar", "name": "@wrnexus/dev-toolbar",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.", "description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.", "description": "@wrnexus/encryption — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.5.10" "@wrnexus/core": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/i18n — part of the WrNexus framework.", "description": "@wrnexus/i18n — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.5.10" "@wrnexus/core": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.", "description": "@wrnexus/jwt — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.", "description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/native": "^0.5.10" "@wrnexus/native": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.", "description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.", "description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/plugin", "name": "@wrnexus/plugin",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/plugin — part of the WrNexus framework.", "description": "@wrnexus/plugin — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -33,7 +33,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/syntax": "^0.5.10" "@wrnexus/syntax": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.", "description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.", "description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.", "description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.", "description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,8 +21,8 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/compiler": "^0.5.10", "@wrnexus/compiler": "^0.5.11",
"@wrnexus/core": "^0.5.10" "@wrnexus/core": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.", "description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.5.10" "@wrnexus/core": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.", "description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,9 +21,9 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/uploader": "^0.5.10", "@wrnexus/uploader": "^0.5.11",
"@wrnexus/core": "^0.5.10", "@wrnexus/core": "^0.5.11",
"@wrnexus/plugin": "^0.5.10" "@wrnexus/plugin": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/syntax", "name": "@wrnexus/syntax",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/syntax — part of the WrNexus framework.", "description": "@wrnexus/syntax — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.", "description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.", "description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.", "description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -26,7 +26,7 @@
"./ui.css": "./ui.css" "./ui.css": "./ui.css"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.5.10" "@wrnexus/core": "^0.5.11"
}, },
"files": [ "files": [
"dist", "dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/uploader — part of the WrNexus framework.", "description": "@wrnexus/uploader — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.5.10" "@wrnexus/core": "^0.5.11"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"description": "@wrnexus/validation — part of the WrNexus framework.", "description": "@wrnexus/validation — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.5.10", "version": "0.5.11",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.", "description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/captcha", "name": "@wrnexus/captcha",
"version": "0.5.10", "version": "0.5.11",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.", "description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+8
View File
@@ -1129,6 +1129,14 @@ const MIGRATIONS: Migration[] = [
// Gateway, helper, validation, and UI improvements are package-managed. // Gateway, helper, validation, and UI improvements are package-managed.
}, },
}, },
{
version: "0.5.11",
id: "latest-release-after-upgrade",
description: "Latest New Packages and release ready packages",
apply() {
// Gateway, helper, validation, and UI improvements are package-managed.
},
},
]; ];
/** Release tooling uses this to require an explicit migration entry per version. */ /** Release tooling uses this to require an explicit migration entry per version. */
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+48 -11
View File
@@ -40,14 +40,16 @@ import { renderDocument, type RenderScript, type ScriptAsset } from "@wrnexus/ss
import type { ClientRuntimeDefinition } from "@wrnexus/plugin"; import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
import { runtimeScriptsForMarkup } from "./plugin-assets.ts"; import { runtimeScriptsForMarkup } from "./plugin-assets.ts";
import { import {
ACCENT_COOKIE,
THEME_COOKIE, THEME_COOKIE,
THEME_CSS_HREF, THEME_CSS_HREF,
THEME_JS_HREF, THEME_JS_HREF,
resolveAccentName,
resolveThemeName, resolveThemeName,
type ResolvedTheme,
type MobileConfig, type MobileConfig,
type PwaConfig,
type ObservabilityConfig, type ObservabilityConfig,
type PwaConfig,
type ResolvedTheme,
type TenancyConfig, type TenancyConfig,
} from "@wrnexus/styles"; } from "@wrnexus/styles";
import { import {
@@ -587,16 +589,37 @@ export const HMR_CLIENT_JS = `
function applyHtml(html) { function applyHtml(html) {
pendingSync = false; pendingSync = false;
var doc = new DOMParser().parseFromString(html, "text/html"); var doc = new DOMParser().parseFromString(html, "text/html");
if (doc.title) document.title = doc.title;
if (doc.title) {
document.title = doc.title;
}
["data-theme", "data-accent", "lang"].forEach(function (name) {
var value = doc.documentElement.getAttribute(name);
if (value == null) {
document.documentElement.removeAttribute(name);
} else {
document.documentElement.setAttribute(name, value);
}
});
var from = document.getElementById("app"); var from = document.getElementById("app");
var to = doc.getElementById("app"); var to = doc.getElementById("app");
if (from && to) morph(from, to);
if (from && to) {
morph(from, to);
}
swapCss("html"); swapCss("html");
// Components are re-rendered server-side into the new HTML; re-hydrate
// their reactive scopes (and any browser-side API fetches) in place. if (window.__wrnexusHydrateScopes) {
if (window.__wrnexusHydrateScopes) window.__wrnexusHydrateScopes(document); window.__wrnexusHydrateScopes(document);
if (window.__wrnexusHydrateCsrFetches) window.__wrnexusHydrateCsrFetches(document); }
window.dispatchEvent(new CustomEvent("wrnexus:hmr", { detail: { type: "html" } }));
if (window.__wrnexusHydrateCsrFetches) {
window.__wrnexusHydrateCsrFetches(document);
}
} }
// Minimal index-based DOM morph: preserve matching nodes (keeps state/focus), // Minimal index-based DOM morph: preserve matching nodes (keeps state/focus),
@@ -1324,7 +1347,18 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
// <html> attributes: no-flash theme (from cookie, validated) + active lang. // <html> attributes: no-flash theme (from cookie, validated) + active lang.
const attrs: string[] = []; const attrs: string[] = [];
if (deps.theme) attrs.push(`data-theme="${resolvedTheme}"`);
if (deps.theme) {
const themeName = resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme);
attrs.push(`data-theme="${themeName}"`);
const accentName = resolveAccentName(ctx.cookies.get(ACCENT_COOKIE), deps.theme);
if (accentName) {
attrs.push(`data-accent="${accentName}"`);
}
}
attrs.push(`lang="${safeLanguageTag(language)}"`); attrs.push(`lang="${safeLanguageTag(language)}"`);
const htmlAttrs = attrs.length ? ` ${attrs.join(" ")}` : undefined; const htmlAttrs = attrs.length ? ` ${attrs.join(" ")}` : undefined;
@@ -1699,7 +1733,10 @@ export function collectScripts(
scripts.push("/__wrnexus/reactive.js"); scripts.push("/__wrnexus/reactive.js");
} }
// The theme runtime is only needed when the page can switch themes. // The theme runtime is only needed when the page can switch themes.
if (/\bdata-wire-theme-(toggle|set)\b/.test(body)) { if (
/\bdata-wire-theme-(toggle|set)\b/.test(body) ||
/\bdata-wire-accent-(set|clear)\b/.test(body)
) {
scripts.push(THEME_JS_HREF); scripts.push(THEME_JS_HREF);
} }
// Validation: schema descriptors + the generic validator, only for pages with a form. // Validation: schema descriptors + the generic validator, only for pages with a form.
@@ -0,0 +1,14 @@
import { expect, test } from "bun:test";
import { collectScripts } from "../src/runtime.ts";
test("accent controls load the shared theme runtime", () => {
expect(collectScripts('<button data-wire-accent-set="rose">Rose</button>')).toContain(
"/__wrnexus/theme.js",
);
});
test("accent clear controls load the shared theme runtime", () => {
expect(collectScripts("<button data-wire-accent-clear>Reset</button>")).toContain(
"/__wrnexus/theme.js",
);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-toolbar", "name": "@wrnexus/dev-toolbar",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/plugin", "name": "@wrnexus/plugin",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1
View File
@@ -154,3 +154,4 @@ export {
contrast, contrast,
} from "./audit.ts"; } from "./audit.ts";
export type { CssTokenAudit, StyleSource, ContrastResult } from "./audit.ts"; export type { CssTokenAudit, StyleSource, ContrastResult } from "./audit.ts";
export * from "./theme.ts";
+309 -101
View File
@@ -1,14 +1,14 @@
/** /**
* Theme system design tokens that work SSR and client-side. * Theme system - design tokens that work SSR and client-side.
* *
* Tokens are plain CSS custom properties (`--wire-<key>`) so they cascade and * 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 * 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. * 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 * The server renders both `<html data-theme="...">` and
* flash), and a tiny client runtime toggles/persists it. The reserved token key * `<html data-accent="...">` from cookies, so the correct theme and accent are
* `color-scheme` is emitted as the native CSS property (not a variable) so form * present before the first paint. The reserved token key `color-scheme` is
* controls and scrollbars match the theme. * emitted as the native CSS property instead of a custom property.
*/ */
export type ThemeTokens = Record<string, string>; export type ThemeTokens = Record<string, string>;
@@ -40,9 +40,23 @@ export interface CustomThemePalette {
danger: string; danger: string;
} }
export interface ThemeAccentConfig {
/**
* Accent used when no `wire-accent` cookie is present.
*
* - Omitted: use the named `palette`, or `blue` when no palette is configured.
* - `false`: keep the configured base palette until the user explicitly picks an accent.
*/
default?: ThemePaletteName | false;
/** Runtime-selectable accent names. Defaults to every built-in THEME_PALETTE. */
options?: ThemePaletteName[];
}
export interface ThemeConfig { export interface ThemeConfig {
/** Built-in palette name, or a complete custom semantic color palette. */ /** Built-in palette name, or a complete custom semantic color palette. */
palette?: ThemePaletteName | CustomThemePalette; palette?: ThemePaletteName | CustomThemePalette;
/** Runtime accent/palette switcher configuration. */
accent?: ThemeAccentConfig;
/** Name of the theme used when no `wire-theme` cookie is present. */ /** Name of the theme used when no `wire-theme` cookie is present. */
default?: string; default?: string;
/** Named token maps. Deep-merged over the framework's built-in light/dark. */ /** Named token maps. Deep-merged over the framework's built-in light/dark. */
@@ -53,13 +67,20 @@ export interface ResolvedTheme {
default: string; default: string;
names: string[]; names: string[];
themes: Record<string, ThemeTokens>; themes: Record<string, ThemeTokens>;
defaultAccent?: ThemePaletteName;
accentNames: ThemePaletteName[];
} }
/** Cookie the resolved theme is read from / persisted to. */ /** Cookies used by the SSR renderer and client runtime. */
export const THEME_COOKIE = "wire-theme"; export const THEME_COOKIE = "wire-theme";
export const ACCENT_COOKIE = "wire-accent";
export const THEME_CSS_HREF = "/__wrnexus/theme.css"; export const THEME_CSS_HREF = "/__wrnexus/theme.css";
export const THEME_JS_HREF = "/__wrnexus/theme.js"; export const THEME_JS_HREF = "/__wrnexus/theme.js";
/**
* Single source of truth for both configured palettes and runtime accents.
* Do not create a second hard-coded ACCENTS map in the browser runtime.
*/
export const THEME_PALETTES: Record<ThemePaletteName, CustomThemePalette> = { export const THEME_PALETTES: Record<ThemePaletteName, CustomThemePalette> = {
blue: { blue: {
primary: "#2563eb", primary: "#2563eb",
@@ -159,24 +180,74 @@ export const THEME_PALETTES: Record<ThemePaletteName, CustomThemePalette> = {
}, },
}; };
function paletteTokens(palette: CustomThemePalette): ThemeTokens { type ColorScheme = "light" | "dark";
function colorMix(color: string, amount: number, base: "white" | "black"): string {
return `color-mix(in srgb, ${color} ${amount}%, ${base})`;
}
function semanticColorTokens(
name: "primary" | "secondary" | "info" | "success" | "warning" | "danger",
color: string,
scheme: ColorScheme,
): ThemeTokens {
const dark = scheme === "dark";
return {
[`color-${name}-soft`]: colorMix(color, dark ? 16 : 8, dark ? "black" : "white"),
[`color-${name}-muted`]: colorMix(color, dark ? 28 : 16, dark ? "black" : "white"),
[`color-${name}-text`]: dark ? colorMix(color, 64, "white") : colorMix(color, 82, "black"),
};
}
function paletteTokens(palette: CustomThemePalette, scheme: ColorScheme): ThemeTokens {
return { return {
"color-primary": palette.primary, "color-primary": palette.primary,
"color-primary-hover": palette.primaryHover, "color-primary-hover": palette.primaryHover,
"color-primary-active": palette.primaryHover,
"color-primary-contrast": palette.primaryContrast, "color-primary-contrast": palette.primaryContrast,
"color-on-primary": palette.primaryContrast,
...semanticColorTokens("primary", palette.primary, scheme),
"color-secondary": palette.secondary, "color-secondary": palette.secondary,
"color-secondary-hover": palette.secondaryHover, "color-secondary-hover": palette.secondaryHover,
"color-secondary-active": palette.secondaryHover,
"color-secondary-contrast": palette.secondaryContrast, "color-secondary-contrast": palette.secondaryContrast,
"color-on-secondary": palette.secondaryContrast,
...semanticColorTokens("secondary", palette.secondary, scheme),
"color-info": palette.info, "color-info": palette.info,
...semanticColorTokens("info", palette.info, scheme),
"color-success": palette.success, "color-success": palette.success,
...semanticColorTokens("success", palette.success, scheme),
"color-warning": palette.warning, "color-warning": palette.warning,
...semanticColorTokens("warning", palette.warning, scheme),
"color-danger": palette.danger, "color-danger": palette.danger,
"color-error": palette.danger, "color-error": palette.danger,
...semanticColorTokens("danger", palette.danger, scheme),
"color-error-soft": colorMix(
palette.danger,
scheme === "dark" ? 16 : 8,
scheme === "dark" ? "black" : "white",
),
"color-error-muted": colorMix(
palette.danger,
scheme === "dark" ? 28 : 16,
scheme === "dark" ? "black" : "white",
),
"color-error-text":
scheme === "dark"
? colorMix(palette.danger, 64, "white")
: colorMix(palette.danger, 82, "black"),
}; };
} }
function resolvePalette(value?: ThemeConfig["palette"]): CustomThemePalette { function resolvePalette(value?: ThemeConfig["palette"]): CustomThemePalette {
if (!value) return THEME_PALETTES.blue; if (!value) return THEME_PALETTES.blue;
if (typeof value === "string") { if (typeof value === "string") {
const palette = THEME_PALETTES[value]; const palette = THEME_PALETTES[value];
if (!palette) { if (!palette) {
@@ -186,16 +257,47 @@ function resolvePalette(value?: ThemeConfig["palette"]): CustomThemePalette {
} }
return palette; return palette;
} }
const missing = Object.keys(THEME_PALETTES.blue).filter((key) => { const missing = Object.keys(THEME_PALETTES.blue).filter((key) => {
const color = value[key as keyof CustomThemePalette]; const color = value[key as keyof CustomThemePalette];
return typeof color !== "string" || !color.trim(); return typeof color !== "string" || !color.trim();
}); });
if (missing.length) { if (missing.length) {
throw new Error(`Custom theme palette is missing required colors: ${missing.join(", ")}.`); throw new Error(`Custom theme palette is missing required colors: ${missing.join(", ")}.`);
} }
return value; return value;
} }
function uniqueAccentNames(values?: ThemePaletteName[]): ThemePaletteName[] {
if (!values?.length) return [...THEME_PALETTE_NAMES];
const allowed = new Set<ThemePaletteName>();
for (const value of values) {
if (THEME_PALETTE_NAMES.includes(value)) allowed.add(value);
}
return allowed.size ? [...allowed] : [...THEME_PALETTE_NAMES];
}
function resolveDefaultAccent(
config: ThemeConfig | undefined,
accentNames: ThemePaletteName[],
): ThemePaletteName | undefined {
if (config?.accent?.default === false) return undefined;
const explicit = config?.accent?.default;
if (explicit && accentNames.includes(explicit)) return explicit;
const palette = config?.palette;
if (typeof palette === "string" && accentNames.includes(palette)) return palette;
if (!palette && accentNames.includes("blue")) return "blue";
return undefined;
}
/** Built-in themes so components have tokens out of the box. */ /** Built-in themes so components have tokens out of the box. */
export const DEFAULT_THEMES: Record<string, ThemeTokens> = { export const DEFAULT_THEMES: Record<string, ThemeTokens> = {
light: { light: {
@@ -208,12 +310,6 @@ export const DEFAULT_THEMES: Record<string, ThemeTokens> = {
"color-text-muted": "#5a6178", "color-text-muted": "#5a6178",
"color-text-subtle": "#737b91", "color-text-subtle": "#737b91",
"color-border-strong": "#c7cedd", "color-border-strong": "#c7cedd",
"color-primary-soft": "#eff6ff",
"color-secondary-soft": "#f1f5f9",
"color-info-soft": "#e0f2fe",
"color-success-soft": "#ecfdf3",
"color-warning-soft": "#fffbeb",
"color-danger-soft": "#fef2f2",
"color-code-background": "#0b1020", "color-code-background": "#0b1020",
"color-code-surface": "#141a30", "color-code-surface": "#141a30",
"color-code-text": "#e7ecff", "color-code-text": "#e7ecff",
@@ -230,17 +326,7 @@ export const DEFAULT_THEMES: Record<string, ThemeTokens> = {
"color-text": "#0b1020", "color-text": "#0b1020",
"color-muted": "#5a6178", "color-muted": "#5a6178",
"color-border": "#e2e6f0", "color-border": "#e2e6f0",
"color-primary": "#2563eb", ...paletteTokens(THEME_PALETTES.blue, "light"),
"color-primary-hover": "#1d4ed8",
"color-primary-contrast": "#ffffff",
"color-secondary": "#475569",
"color-secondary-hover": "#334155",
"color-secondary-contrast": "#ffffff",
"color-info": "#0284c7",
"color-danger": "#dc2626",
"color-success": "#16a34a",
"color-warning": "#d97706",
"color-error": "#dc2626",
radius: "8px", radius: "8px",
"radius-sm": "5px", "radius-sm": "5px",
"font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif', "font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
@@ -256,12 +342,6 @@ export const DEFAULT_THEMES: Record<string, ThemeTokens> = {
"color-text-muted": "#b3b3b3", "color-text-muted": "#b3b3b3",
"color-text-subtle": "#8a8a8a", "color-text-subtle": "#8a8a8a",
"color-border-strong": "#ffffff38", "color-border-strong": "#ffffff38",
"color-primary-soft": "#6c8cff24",
"color-secondary-soft": "#a78bfa24",
"color-info-soft": "#38bdf824",
"color-success-soft": "#4ade8024",
"color-warning-soft": "#fbbf2424",
"color-danger-soft": "#f8717124",
"color-code-background": "#050505", "color-code-background": "#050505",
"color-code-surface": "#101010", "color-code-surface": "#101010",
"color-code-text": "#f5f5f5", "color-code-text": "#f5f5f5",
@@ -278,17 +358,7 @@ export const DEFAULT_THEMES: Record<string, ThemeTokens> = {
"color-text": "#f5f5f5", "color-text": "#f5f5f5",
"color-muted": "#a3a3a3", "color-muted": "#a3a3a3",
"color-border": "#ffffff1f", "color-border": "#ffffff1f",
"color-primary": "#6c8cff", ...paletteTokens(THEME_PALETTES.blue, "dark"),
"color-primary-hover": "#8aa2ff",
"color-primary-contrast": "#000000",
"color-secondary": "#a78bfa",
"color-secondary-hover": "#c4b5fd",
"color-secondary-contrast": "#000000",
"color-info": "#38bdf8",
"color-danger": "#f87171",
"color-success": "#4ade80",
"color-warning": "#fbbf24",
"color-error": "#f87171",
radius: "10px", radius: "10px",
"radius-sm": "6px", "radius-sm": "6px",
"font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif', "font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
@@ -296,25 +366,43 @@ export const DEFAULT_THEMES: Record<string, ThemeTokens> = {
}, },
}; };
function themeScheme(tokens: ThemeTokens): ColorScheme {
return tokens["color-scheme"] === "dark" ? "dark" : "light";
}
/** Merge the user's theme config over the built-in defaults. */ /** Merge the user's theme config over the built-in defaults. */
export function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme { export function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme {
const semanticTokens = paletteTokens(resolvePalette(config?.palette)); const palette = resolvePalette(config?.palette);
const themes: Record<string, ThemeTokens> = {}; const themes: Record<string, ThemeTokens> = {};
const names = new Set<string>([ const names = new Set<string>([
...Object.keys(DEFAULT_THEMES), ...Object.keys(DEFAULT_THEMES),
...Object.keys(config?.themes ?? {}), ...Object.keys(config?.themes ?? {}),
]); ]);
for (const name of names) { for (const name of names) {
const base = DEFAULT_THEMES[name] ?? DEFAULT_THEMES.light;
const scheme = themeScheme(base);
themes[name] = { themes[name] = {
...(DEFAULT_THEMES[name] ?? DEFAULT_THEMES.light), ...base,
...semanticTokens, ...paletteTokens(palette, scheme),
...(config?.themes?.[name] ?? {}), ...(config?.themes?.[name] ?? {}),
}; };
} }
const list = Object.keys(themes); const list = Object.keys(themes);
const preferred = config?.default && themes[config.default] ? config.default : undefined; const preferred = config?.default && themes[config.default] ? config.default : undefined;
const fallback = themes.dark ? "dark" : list[0]!; const fallback = themes.dark ? "dark" : list[0]!;
return { default: preferred ?? fallback, names: list, themes }; const accentNames = uniqueAccentNames(config?.accent?.options);
const defaultAccent = resolveDefaultAccent(config, accentNames);
return {
default: preferred ?? fallback,
names: list,
themes,
defaultAccent,
accentNames,
};
} }
/** Pick a valid theme name from a cookie value, falling back to the default. */ /** Pick a valid theme name from a cookie value, falling back to the default. */
@@ -322,6 +410,18 @@ export function resolveThemeName(cookieValue: string | undefined, theme: Resolve
return cookieValue && theme.themes[cookieValue] ? cookieValue : theme.default; return cookieValue && theme.themes[cookieValue] ? cookieValue : theme.default;
} }
/** Pick a valid accent name from a cookie value, falling back to the configured default. */
export function resolveAccentName(
cookieValue: string | undefined,
theme: ResolvedTheme,
): ThemePaletteName | undefined {
if (cookieValue && theme.accentNames.includes(cookieValue as ThemePaletteName)) {
return cookieValue as ThemePaletteName;
}
return theme.defaultAccent;
}
function tokensToDeclarations(tokens: ThemeTokens): string { function tokensToDeclarations(tokens: ThemeTokens): string {
return Object.entries(tokens) return Object.entries(tokens)
.map(([key, value]) => .map(([key, value]) =>
@@ -330,14 +430,50 @@ function tokensToDeclarations(tokens: ThemeTokens): string {
.join(""); .join("");
} }
/** Generate the theme stylesheet: a `:root` default plus one block per theme. */ function selectorValue(value: string): string {
return JSON.stringify(value);
}
/**
* Generate the theme stylesheet.
*
* Theme selectors are emitted first. Accent selectors are emitted afterwards,
* so a selected accent consistently overrides every semantic palette token,
* including soft/muted/text variants, before the first paint.
*/
export function renderThemeCss(theme: ResolvedTheme): string { export function renderThemeCss(theme: ResolvedTheme): string {
const blocks: string[] = []; const blocks: string[] = [];
const def = theme.themes[theme.default]; const defaultTheme = theme.themes[theme.default];
if (def) blocks.push(`:root{${tokensToDeclarations(def)}}`);
if (defaultTheme) blocks.push(`:root{${tokensToDeclarations(defaultTheme)}}`);
for (const name of theme.names) { for (const name of theme.names) {
blocks.push(`[data-theme="${name}"]{${tokensToDeclarations(theme.themes[name]!)}}`); blocks.push(
`[data-theme=${selectorValue(name)}]{${tokensToDeclarations(theme.themes[name]!)}}`,
);
} }
for (const accentName of theme.accentNames) {
const palette = THEME_PALETTES[accentName];
if (defaultTheme) {
blocks.push(
`:root[data-accent=${selectorValue(accentName)}]{${tokensToDeclarations(
paletteTokens(palette, themeScheme(defaultTheme)),
)}}`,
);
}
for (const themeName of theme.names) {
const themeTokens = theme.themes[themeName]!;
blocks.push(
`[data-theme=${selectorValue(themeName)}][data-accent=${selectorValue(
accentName,
)}]{${tokensToDeclarations(paletteTokens(palette, themeScheme(themeTokens)))}}`,
);
}
}
return ( return (
blocks.join("\n") + blocks.join("\n") +
'\nhtml,body{font-family:var(--wire-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n' '\nhtml,body{font-family:var(--wire-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n'
@@ -345,94 +481,166 @@ export function renderThemeCss(theme: ResolvedTheme): string {
} }
/** /**
* Generate the client theme runtime. It exposes `window.wireTheme` and binds * Generate the client theme runtime. It exposes `window.wireTheme` and
* `[data-wire-theme-toggle]` / `[data-wire-theme-set]` elements. The configured * `window.wireAccent`, and binds theme/accent controls.
* theme names are baked in so `toggle()` cycles through them in order. *
* The runtime changes only data attributes and cookies. It never writes inline
* CSS variables and never uses localStorage, so CSS and SSR remain the single
* source of truth.
*/ */
export function renderThemeRuntime(theme: ResolvedTheme): string { export function renderThemeRuntime(theme: ResolvedTheme): string {
const names = JSON.stringify(theme.names); const names = JSON.stringify(theme.names);
const accentNames = JSON.stringify(theme.accentNames);
const defaultAccent = JSON.stringify(theme.defaultAccent ?? null);
return `(function(){ return `(function(){
var COOKIE=${JSON.stringify(THEME_COOKIE)}; var THEME_COOKIE=${JSON.stringify(THEME_COOKIE)};
var ACCENT_COOKIE=${JSON.stringify(ACCENT_COOKIE)};
var THEMES=${names}; var THEMES=${names};
var ACCENT_COOKIE="wire-accent"; var ACCENTS=${accentNames};
var COOKIE_MAX_AGE=31536000; var DEFAULT_THEME=${JSON.stringify(theme.default)};
var ACCENTS={ var DEFAULT_ACCENT=${defaultAccent};
blue:["#2563eb","#1d4ed8","#ffffff"], var MAX_AGE=31536000;
cyan:["#0891b2","#0e7490","#ffffff"],
emerald:["#059669","#047857","#ffffff"],
violet:["#7c3aed","#6d28d9","#ffffff"],
rose:["#e11d48","#be123c","#ffffff"],
amber:["#d97706","#b45309","#ffffff"]
};
var el=document.documentElement; var el=document.documentElement;
function readCookie(name){ function readCookie(name){
var prefix=encodeURIComponent(name)+"="; var prefix=encodeURIComponent(name)+"=";
var parts=document.cookie?document.cookie.split(";"):[]; var parts=document.cookie?document.cookie.split(";"):[];
for(var i=0;i<parts.length;i++){ for(var i=0;i<parts.length;i++){
var part=parts[i].trim(); var part=parts[i].trim();
if(part.indexOf(prefix)===0){ if(part.indexOf(prefix)!==0)continue;
try{return decodeURIComponent(part.slice(prefix.length));}catch(_){return part.slice(prefix.length);} var value=part.slice(prefix.length);
} try{return decodeURIComponent(value);}catch(_){return value;}
} }
return null; return null;
} }
function writeCookie(name,value){ function writeCookie(name,value){
var secure=location.protocol==="https:"?";secure":""; var secure=location.protocol==="https:"?";secure":"";
document.cookie=encodeURIComponent(name)+"="+encodeURIComponent(value)+";path=/;max-age="+COOKIE_MAX_AGE+";samesite=lax"+secure; document.cookie=encodeURIComponent(name)+"="+encodeURIComponent(value)+
";path=/;max-age="+MAX_AGE+";samesite=lax"+secure;
} }
function get(){return el.getAttribute("data-theme")||${JSON.stringify(theme.default)};}
function deleteCookie(name){
var secure=location.protocol==="https:"?";secure":"";
document.cookie=encodeURIComponent(name)+"=;path=/;max-age=0;samesite=lax"+secure;
}
function getTheme(){
var current=el.getAttribute("data-theme")||readCookie(THEME_COOKIE)||DEFAULT_THEME;
return THEMES.indexOf(current)>=0?current:DEFAULT_THEME;
}
function getAccent(){
var current=el.getAttribute("data-accent")||readCookie(ACCENT_COOKIE)||DEFAULT_ACCENT;
return current&&ACCENTS.indexOf(current)>=0?current:null;
}
function syncThemeButtons(root){ function syncThemeButtons(root){
(root||document).querySelectorAll("[data-wire-theme-set]").forEach(function(n){ var current=getTheme();
n.setAttribute("aria-pressed",String(n.getAttribute("data-wire-theme-set")===get())); (root||document).querySelectorAll("[data-wire-theme-set]").forEach(function(node){
node.setAttribute("aria-pressed",String(node.getAttribute("data-wire-theme-set")===current));
}); });
} }
function syncAccentButtons(root,name){
(root||document).querySelectorAll("[data-wire-accent-set]").forEach(function(n){ function syncAccentButtons(root){
n.setAttribute("aria-pressed",String(n.getAttribute("data-wire-accent-set")===name)); var current=getAccent();
(root||document).querySelectorAll("[data-wire-accent-set]").forEach(function(node){
node.setAttribute("aria-pressed",String(node.getAttribute("data-wire-accent-set")===current));
}); });
} }
function set(name){
function setTheme(name){
if(THEMES.indexOf(name)<0)return; if(THEMES.indexOf(name)<0)return;
el.setAttribute("data-theme",name); el.setAttribute("data-theme",name);
writeCookie(COOKIE,name); writeCookie(THEME_COOKIE,name);
syncThemeButtons(document); syncThemeButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:name}}));
} }
function toggle(){var i=THEMES.indexOf(get());set(THEMES[(i+1)%THEMES.length]);}
function toggleTheme(){
var index=THEMES.indexOf(getTheme());
setTheme(THEMES[(index+1)%THEMES.length]);
}
function setAccent(name){ function setAccent(name){
var value=ACCENTS[name];if(!value)return; if(ACCENTS.indexOf(name)<0)return;
el.style.setProperty("--wire-color-primary",value[0]);
el.style.setProperty("--wire-color-primary-hover",value[1]);
el.style.setProperty("--wire-color-primary-contrast",value[2]);
el.setAttribute("data-accent",name); el.setAttribute("data-accent",name);
writeCookie(ACCENT_COOKIE,name); writeCookie(ACCENT_COOKIE,name);
syncAccentButtons(document,name); syncAccentButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:name}}));
} }
function clearAccent(){
el.removeAttribute("data-accent");
deleteCookie(ACCENT_COOKIE);
syncAccentButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:null}}));
}
function bind(root){ function bind(root){
(root||document).querySelectorAll("[data-wire-theme-toggle]").forEach(function(n){ var target=root||document;
if(n.__wireThemeBound)return;n.__wireThemeBound=1;
n.addEventListener("click",function(){toggle();}); target.querySelectorAll("[data-wire-theme-toggle]").forEach(function(node){
if(node.__wireThemeBound)return;
node.__wireThemeBound=1;
node.addEventListener("click",toggleTheme);
}); });
(root||document).querySelectorAll("[data-wire-theme-set]").forEach(function(n){
if(n.__wireThemeBound)return;n.__wireThemeBound=1; target.querySelectorAll("[data-wire-theme-set]").forEach(function(node){
n.addEventListener("click",function(){set(n.getAttribute("data-wire-theme-set"));}); if(node.__wireThemeBound)return;
node.__wireThemeBound=1;
node.addEventListener("click",function(){
setTheme(node.getAttribute("data-wire-theme-set"));
});
}); });
(root||document).querySelectorAll("[data-wire-accent-set]").forEach(function(n){
if(n.__wireAccentBound)return;n.__wireAccentBound=1; target.querySelectorAll("[data-wire-accent-set]").forEach(function(node){
n.addEventListener("click",function(){setAccent(n.getAttribute("data-wire-accent-set"));}); if(node.__wireAccentBound)return;
node.__wireAccentBound=1;
node.addEventListener("click",function(){
setAccent(node.getAttribute("data-wire-accent-set"));
});
}); });
syncThemeButtons(root);
syncAccentButtons(root,el.getAttribute("data-accent")||"blue"); target.querySelectorAll("[data-wire-accent-clear]").forEach(function(node){
if(node.__wireAccentBound)return;
node.__wireAccentBound=1;
node.addEventListener("click",clearAccent);
});
syncThemeButtons(target);
syncAccentButtons(target);
} }
function getAccent(){
var current=el.getAttribute("data-accent")||readCookie(ACCENT_COOKIE)||"blue"; // SSR should already have these attributes. The cookie fallback only protects
return ACCENTS[current]?current:"blue"; // manually generated/static documents that did not pass through WRNexus SSR.
} var initialTheme=getTheme();
window.wireTheme={get:get,set:set,toggle:toggle,bind:bind,themes:THEMES}; if(!el.hasAttribute("data-theme"))el.setAttribute("data-theme",initialTheme);
window.wireAccent={get:getAccent,set:setAccent,colors:Object.keys(ACCENTS)}; var initialAccent=getAccent();
setAccent(getAccent()); if(initialAccent&&!el.hasAttribute("data-accent"))el.setAttribute("data-accent",initialAccent);
window.wireTheme={
get:getTheme,
set:setTheme,
toggle:toggleTheme,
bind:bind,
themes:THEMES.slice()
};
window.wireAccent={
get:getAccent,
set:setAccent,
clear:clearAccent,
colors:ACCENTS.slice()
};
window.addEventListener("wrnexus:navigated",function(){bind(document);}); window.addEventListener("wrnexus:navigated",function(){bind(document);});
if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",function(){bind(document);}); if(document.readyState==="loading"){
else bind(document); document.addEventListener("DOMContentLoaded",function(){bind(document);});
}else{
bind(document);
}
})(); })();
`; `;
} }
+49
View File
@@ -0,0 +1,49 @@
import { describe, expect, test } from "bun:test";
import {
ACCENT_COOKIE,
renderThemeCss,
renderThemeRuntime,
resolveAccentName,
resolveThemeConfig,
} from "../src/theme.ts";
describe("theme accents", () => {
test("uses the configured named palette as the default accent", () => {
const theme = resolveThemeConfig({ palette: "rose", default: "light" });
expect(theme.defaultAccent).toBe("rose");
expect(resolveAccentName(undefined, theme)).toBe("rose");
});
test("validates the accent cookie against enabled palettes", () => {
const theme = resolveThemeConfig({
palette: "rose",
accent: { options: ["rose", "emerald"] },
});
expect(resolveAccentName("emerald", theme)).toBe("emerald");
expect(resolveAccentName("blue", theme)).toBe("rose");
expect(resolveAccentName("not-real", theme)).toBe("rose");
});
test("generates complete palette selectors for light and dark themes", () => {
const css = renderThemeCss(resolveThemeConfig({ palette: "rose" }));
expect(css).toContain('[data-theme="light"][data-accent="emerald"]');
expect(css).toContain('[data-theme="dark"][data-accent="emerald"]');
expect(css).toContain("--wire-color-primary-soft:");
expect(css).toContain("--wire-color-primary-muted:");
expect(css).toContain("--wire-color-primary-text:");
expect(css).toContain("--wire-color-on-primary:");
expect(css).toContain("--wire-color-secondary-soft:");
});
test("runtime persists accents with cookies and never writes inline tokens", () => {
const runtime = renderThemeRuntime(resolveThemeConfig({ palette: "rose" }));
expect(runtime).toContain(ACCENT_COOKIE);
expect(runtime).toContain('el.setAttribute("data-accent",name)');
expect(runtime).not.toContain("localStorage");
expect(runtime).not.toContain("style.setProperty");
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/syntax", "name": "@wrnexus/syntax",
"version": "0.5.10", "version": "0.5.11",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.5.10", "version": "0.5.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
const version = "0.5.10"; const version = "0.5.11";
const dependencyGroups = [ const dependencyGroups = [
"dependencies", "dependencies",