diff --git a/.publish/ai/package.json b/.publish/ai/package.json
index e59c74ed..c968bd09 100644
--- a/.publish/ai/package.json
+++ b/.publish/ai/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT",
diff --git a/.publish/authz/package.json b/.publish/authz/package.json
index 3c861948..6d008814 100644
--- a/.publish/authz/package.json
+++ b/.publish/authz/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/cli/package.json b/.publish/cli/package.json
index e490338e..ef0273d8 100644
--- a/.publish/cli/package.json
+++ b/.publish/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT",
@@ -28,18 +28,18 @@
"wrnexus": "./dist/index.js"
},
"dependencies": {
- "@wrnexus/core": "^0.5.10",
- "@wrnexus/router": "^0.5.10",
- "@wrnexus/csr": "^0.5.10",
- "@wrnexus/compiler": "^0.5.10",
- "@wrnexus/styles": "^0.5.10",
- "@wrnexus/dev-server": "^0.5.10",
- "@wrnexus/ui": "^0.5.10",
- "@wrnexus/validation": "^0.5.10",
- "@wrnexus/i18n": "^0.5.10",
- "@wrnexus/db": "^0.5.10",
- "@wrnexus/plugin": "^0.5.10",
- "@wrnexus/syntax": "^0.5.10"
+ "@wrnexus/core": "^0.5.11",
+ "@wrnexus/router": "^0.5.11",
+ "@wrnexus/csr": "^0.5.11",
+ "@wrnexus/compiler": "^0.5.11",
+ "@wrnexus/styles": "^0.5.11",
+ "@wrnexus/dev-server": "^0.5.11",
+ "@wrnexus/ui": "^0.5.11",
+ "@wrnexus/validation": "^0.5.11",
+ "@wrnexus/i18n": "^0.5.11",
+ "@wrnexus/db": "^0.5.11",
+ "@wrnexus/plugin": "^0.5.11",
+ "@wrnexus/syntax": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json
index cc0fbefd..5d0fe2c0 100644
--- a/.publish/compiler/package.json
+++ b/.publish/compiler/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
- "@wrnexus/syntax": "^0.5.10"
+ "@wrnexus/syntax": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/core/package.json b/.publish/core/package.json
index 59f9294e..08b72815 100644
--- a/.publish/core/package.json
+++ b/.publish/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/csr/package.json b/.publish/csr/package.json
index 6b17c97c..d1734bdc 100644
--- a/.publish/csr/package.json
+++ b/.publish/csr/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
- "@wrnexus/core": "^0.5.10"
+ "@wrnexus/core": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/db/package.json b/.publish/db/package.json
index f906adbc..47d37264 100644
--- a/.publish/db/package.json
+++ b/.publish/db/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json
index 2d68550a..a7186bd7 100644
--- a/.publish/dev-server/package.json
+++ b/.publish/dev-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT",
@@ -25,20 +25,20 @@
}
},
"dependencies": {
- "@wrnexus/core": "^0.5.10",
- "@wrnexus/dev-toolbar": "^0.5.10",
- "@wrnexus/router": "^0.5.10",
- "@wrnexus/ssr": "^0.5.10",
- "@wrnexus/csr": "^0.5.10",
- "@wrnexus/compiler": "^0.5.10",
- "@wrnexus/styles": "^0.5.10",
- "@wrnexus/ui": "^0.5.10",
- "@wrnexus/validation": "^0.5.10",
- "@wrnexus/i18n": "^0.5.10",
- "@wrnexus/db": "^0.5.10",
- "@wrnexus/pubsub": "^0.5.10",
- "@wrnexus/uploader": "^0.5.10",
- "@wrnexus/plugin": "^0.5.10"
+ "@wrnexus/core": "^0.5.11",
+ "@wrnexus/dev-toolbar": "^0.5.11",
+ "@wrnexus/router": "^0.5.11",
+ "@wrnexus/ssr": "^0.5.11",
+ "@wrnexus/csr": "^0.5.11",
+ "@wrnexus/compiler": "^0.5.11",
+ "@wrnexus/styles": "^0.5.11",
+ "@wrnexus/ui": "^0.5.11",
+ "@wrnexus/validation": "^0.5.11",
+ "@wrnexus/i18n": "^0.5.11",
+ "@wrnexus/db": "^0.5.11",
+ "@wrnexus/pubsub": "^0.5.11",
+ "@wrnexus/uploader": "^0.5.11",
+ "@wrnexus/plugin": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/dev-toolbar/package.json b/.publish/dev-toolbar/package.json
index ae4760e7..c62e6409 100644
--- a/.publish/dev-toolbar/package.json
+++ b/.publish/dev-toolbar/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json
index d2971bcb..b77bceb7 100644
--- a/.publish/encryption/package.json
+++ b/.publish/encryption/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json
index 876010e3..e6024211 100644
--- a/.publish/helpers/package.json
+++ b/.publish/helpers/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
- "@wrnexus/core": "^0.5.10"
+ "@wrnexus/core": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json
index 37c8ed19..294cd86e 100644
--- a/.publish/i18n/package.json
+++ b/.publish/i18n/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/i18n — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
- "@wrnexus/core": "^0.5.10"
+ "@wrnexus/core": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json
index 92647973..3279d10f 100644
--- a/.publish/jwt/package.json
+++ b/.publish/jwt/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json
index 5326aca8..e3727a8a 100644
--- a/.publish/mobile/package.json
+++ b/.publish/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
- "@wrnexus/native": "^0.5.10"
+ "@wrnexus/native": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/native/package.json b/.publish/native/package.json
index 02f2cc12..dd31a8cd 100644
--- a/.publish/native/package.json
+++ b/.publish/native/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json
index a332ef95..b9e2b743 100644
--- a/.publish/oauth/package.json
+++ b/.publish/oauth/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/plugin/package.json b/.publish/plugin/package.json
index bd494417..32c9d1de 100644
--- a/.publish/plugin/package.json
+++ b/.publish/plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/plugin",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/plugin — part of the WrNexus framework.",
"license": "MIT",
@@ -33,7 +33,7 @@
}
},
"dependencies": {
- "@wrnexus/syntax": "^0.5.10"
+ "@wrnexus/syntax": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json
index ba69a496..10892c0c 100644
--- a/.publish/pubsub/package.json
+++ b/.publish/pubsub/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/queue/package.json b/.publish/queue/package.json
index 1848a1b1..0dfcd1bd 100644
--- a/.publish/queue/package.json
+++ b/.publish/queue/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json
index 85938559..7ab4036e 100644
--- a/.publish/reactive/package.json
+++ b/.publish/reactive/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/router/package.json b/.publish/router/package.json
index 4fc9b4b3..95296d63 100644
--- a/.publish/router/package.json
+++ b/.publish/router/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT",
@@ -21,8 +21,8 @@
}
},
"dependencies": {
- "@wrnexus/compiler": "^0.5.10",
- "@wrnexus/core": "^0.5.10"
+ "@wrnexus/compiler": "^0.5.11",
+ "@wrnexus/core": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json
index a55019b1..168dca66 100644
--- a/.publish/ssr/package.json
+++ b/.publish/ssr/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
- "@wrnexus/core": "^0.5.10"
+ "@wrnexus/core": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/styles/package.json b/.publish/styles/package.json
index 90013b69..fa91e121 100644
--- a/.publish/styles/package.json
+++ b/.publish/styles/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT",
@@ -21,9 +21,9 @@
}
},
"dependencies": {
- "@wrnexus/uploader": "^0.5.10",
- "@wrnexus/core": "^0.5.10",
- "@wrnexus/plugin": "^0.5.10"
+ "@wrnexus/uploader": "^0.5.11",
+ "@wrnexus/core": "^0.5.11",
+ "@wrnexus/plugin": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/syntax/package.json b/.publish/syntax/package.json
index 6ede2db6..37f088f8 100644
--- a/.publish/syntax/package.json
+++ b/.publish/syntax/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/syntax — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/test/package.json b/.publish/test/package.json
index 53bd036b..8e694ef1 100644
--- a/.publish/test/package.json
+++ b/.publish/test/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json
index e4a9acef..612fa0a1 100644
--- a/.publish/tracking/package.json
+++ b/.publish/tracking/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/tracking",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT",
diff --git a/.publish/ui/package.json b/.publish/ui/package.json
index ee15435b..da46abcd 100644
--- a/.publish/ui/package.json
+++ b/.publish/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT",
@@ -26,7 +26,7 @@
"./ui.css": "./ui.css"
},
"dependencies": {
- "@wrnexus/core": "^0.5.10"
+ "@wrnexus/core": "^0.5.11"
},
"files": [
"dist",
diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json
index add50066..b34477a8 100644
--- a/.publish/uploader/package.json
+++ b/.publish/uploader/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/uploader",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/uploader — part of the WrNexus framework.",
"license": "MIT",
@@ -21,7 +21,7 @@
}
},
"dependencies": {
- "@wrnexus/core": "^0.5.10"
+ "@wrnexus/core": "^0.5.11"
},
"files": [
"dist"
diff --git a/.publish/validation/package.json b/.publish/validation/package.json
index 1ee8c4fb..55b5b329 100644
--- a/.publish/validation/package.json
+++ b/.publish/validation/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/validation",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"description": "@wrnexus/validation — part of the WrNexus framework.",
"license": "MIT",
diff --git a/packages/ai/package.json b/packages/ai/package.json
index 7bbfd91c..46da69bb 100644
--- a/packages/ai/package.json
+++ b/packages/ai/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ai",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
diff --git a/packages/auth/package.json b/packages/auth/package.json
index 342be3e9..9091ba40 100644
--- a/packages/auth/package.json
+++ b/packages/auth/package.json
@@ -1,6 +1,6 @@
{
"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.",
"type": "module",
"sideEffects": false,
diff --git a/packages/authz/package.json b/packages/authz/package.json
index 2cabfeff..62c30d51 100644
--- a/packages/authz/package.json
+++ b/packages/authz/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/captcha/package.json b/packages/captcha/package.json
index d24d45b2..ed53bb5d 100644
--- a/packages/captcha/package.json
+++ b/packages/captcha/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/captcha",
- "version": "0.5.10",
+ "version": "0.5.11",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module",
"sideEffects": false,
diff --git a/packages/cli/package.json b/packages/cli/package.json
index f1773608..76c08e34 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts
index c8028c69..3771d1a5 100644
--- a/packages/cli/src/update.ts
+++ b/packages/cli/src/update.ts
@@ -1129,6 +1129,14 @@ const MIGRATIONS: Migration[] = [
// 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. */
diff --git a/packages/compiler/package.json b/packages/compiler/package.json
index 17a1624a..e206863d 100644
--- a/packages/compiler/package.json
+++ b/packages/compiler/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/core/package.json b/packages/core/package.json
index 31b18de8..6fc9ce40 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/core",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/csr/package.json b/packages/csr/package.json
index 97854dcf..3dc677bd 100644
--- a/packages/csr/package.json
+++ b/packages/csr/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/db/package.json b/packages/db/package.json
index 11346ad4..192464b5 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json
index 2f673298..38367ffe 100644
--- a/packages/dev-server/package.json
+++ b/packages/dev-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/dev-server/src/runtime.ts b/packages/dev-server/src/runtime.ts
index c0bfdd2f..dcd46039 100644
--- a/packages/dev-server/src/runtime.ts
+++ b/packages/dev-server/src/runtime.ts
@@ -40,14 +40,16 @@ import { renderDocument, type RenderScript, type ScriptAsset } from "@wrnexus/ss
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
import { runtimeScriptsForMarkup } from "./plugin-assets.ts";
import {
+ ACCENT_COOKIE,
THEME_COOKIE,
THEME_CSS_HREF,
THEME_JS_HREF,
+ resolveAccentName,
resolveThemeName,
- type ResolvedTheme,
type MobileConfig,
- type PwaConfig,
type ObservabilityConfig,
+ type PwaConfig,
+ type ResolvedTheme,
type TenancyConfig,
} from "@wrnexus/styles";
import {
@@ -587,16 +589,37 @@ export const HMR_CLIENT_JS = `
function applyHtml(html) {
pendingSync = false;
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 to = doc.getElementById("app");
- if (from && to) morph(from, to);
+
+ if (from && to) {
+ morph(from, to);
+ }
+
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) window.__wrnexusHydrateScopes(document);
- if (window.__wrnexusHydrateCsrFetches) window.__wrnexusHydrateCsrFetches(document);
- window.dispatchEvent(new CustomEvent("wrnexus:hmr", { detail: { type: "html" } }));
+
+ if (window.__wrnexusHydrateScopes) {
+ window.__wrnexusHydrateScopes(document);
+ }
+
+ if (window.__wrnexusHydrateCsrFetches) {
+ window.__wrnexusHydrateCsrFetches(document);
+ }
}
// Minimal index-based DOM morph: preserve matching nodes (keeps state/focus),
@@ -1324,7 +1347,18 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
// attributes: no-flash theme (from cookie, validated) + active lang.
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)}"`);
const htmlAttrs = attrs.length ? ` ${attrs.join(" ")}` : undefined;
@@ -1699,7 +1733,10 @@ export function collectScripts(
scripts.push("/__wrnexus/reactive.js");
}
// 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);
}
// Validation: schema descriptors + the generic validator, only for pages with a form.
diff --git a/packages/dev-server/test/theme-accent-runtime.test.ts b/packages/dev-server/test/theme-accent-runtime.test.ts
new file mode 100644
index 00000000..b24a419f
--- /dev/null
+++ b/packages/dev-server/test/theme-accent-runtime.test.ts
@@ -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('')).toContain(
+ "/__wrnexus/theme.js",
+ );
+});
+
+test("accent clear controls load the shared theme runtime", () => {
+ expect(collectScripts("")).toContain(
+ "/__wrnexus/theme.js",
+ );
+});
diff --git a/packages/dev-toolbar/package.json b/packages/dev-toolbar/package.json
index fa3f610e..8a82d7de 100644
--- a/packages/dev-toolbar/package.json
+++ b/packages/dev-toolbar/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"sideEffects": false,
diff --git a/packages/encryption/package.json b/packages/encryption/package.json
index a9959297..fc710bee 100644
--- a/packages/encryption/package.json
+++ b/packages/encryption/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/helpers/package.json b/packages/helpers/package.json
index e588a7bd..2361cb08 100644
--- a/packages/helpers/package.json
+++ b/packages/helpers/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
index ce80bd24..8fbf131e 100644
--- a/packages/i18n/package.json
+++ b/packages/i18n/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/i18n",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/jwt/package.json b/packages/jwt/package.json
index ce46633a..2c0ec2f4 100644
--- a/packages/jwt/package.json
+++ b/packages/jwt/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/jwt",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index 96fd6e0f..7e33c453 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mobile",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/native/package.json b/packages/native/package.json
index b2125ea6..ca60cbbf 100644
--- a/packages/native/package.json
+++ b/packages/native/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/native",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/oauth/package.json b/packages/oauth/package.json
index 5c743748..f80c39d5 100644
--- a/packages/oauth/package.json
+++ b/packages/oauth/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index 8590c483..05b958de 100644
--- a/packages/plugin/package.json
+++ b/packages/plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/plugin",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json
index 1e5b3ca1..105b5463 100644
--- a/packages/pubsub/package.json
+++ b/packages/pubsub/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/pubsub",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/queue/package.json b/packages/queue/package.json
index 4b4b0ae0..a8503a5d 100644
--- a/packages/queue/package.json
+++ b/packages/queue/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/reactive/package.json b/packages/reactive/package.json
index 1d7f3152..4d58d505 100644
--- a/packages/reactive/package.json
+++ b/packages/reactive/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/reactive",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/router/package.json b/packages/router/package.json
index ad35bab8..389a241c 100644
--- a/packages/router/package.json
+++ b/packages/router/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/router",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/ssr/package.json b/packages/ssr/package.json
index 3dc66eed..d03a1899 100644
--- a/packages/ssr/package.json
+++ b/packages/ssr/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ssr",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/styles/package.json b/packages/styles/package.json
index 44898cfb..e82f54eb 100644
--- a/packages/styles/package.json
+++ b/packages/styles/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/styles/src/index.ts b/packages/styles/src/index.ts
index 33e99dd5..b119e930 100644
--- a/packages/styles/src/index.ts
+++ b/packages/styles/src/index.ts
@@ -154,3 +154,4 @@ export {
contrast,
} from "./audit.ts";
export type { CssTokenAudit, StyleSource, ContrastResult } from "./audit.ts";
+export * from "./theme.ts";
diff --git a/packages/styles/src/theme.ts b/packages/styles/src/theme.ts
index e688d0dd..bb88c5c6 100644
--- a/packages/styles/src/theme.ts
+++ b/packages/styles/src/theme.ts
@@ -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-`) so they cascade and
* can be overridden by user CSS. Each theme is a flat token map; the framework
* ships default `light`/`dark` sets and the user's config deep-merges over them.
*
- * The server renders `` from the `wire-theme` cookie (no
- * flash), and a tiny client runtime toggles/persists it. The reserved token key
- * `color-scheme` is emitted as the native CSS property (not a variable) so form
- * controls and scrollbars match the theme.
+ * The server renders both `` and
+ * `` from cookies, so the correct theme and accent are
+ * present before the first paint. The reserved token key `color-scheme` is
+ * emitted as the native CSS property instead of a custom property.
*/
export type ThemeTokens = Record;
@@ -40,9 +40,23 @@ export interface CustomThemePalette {
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 {
/** Built-in palette name, or a complete custom semantic color palette. */
palette?: ThemePaletteName | CustomThemePalette;
+ /** Runtime accent/palette switcher configuration. */
+ accent?: ThemeAccentConfig;
/** Name of the theme used when no `wire-theme` cookie is present. */
default?: string;
/** Named token maps. Deep-merged over the framework's built-in light/dark. */
@@ -53,13 +67,20 @@ export interface ResolvedTheme {
default: string;
names: string[];
themes: Record;
+ 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 ACCENT_COOKIE = "wire-accent";
export const THEME_CSS_HREF = "/__wrnexus/theme.css";
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 = {
blue: {
primary: "#2563eb",
@@ -159,24 +180,74 @@ export const THEME_PALETTES: Record = {
},
};
-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 {
"color-primary": palette.primary,
"color-primary-hover": palette.primaryHover,
+ "color-primary-active": palette.primaryHover,
"color-primary-contrast": palette.primaryContrast,
+ "color-on-primary": palette.primaryContrast,
+ ...semanticColorTokens("primary", palette.primary, scheme),
+
"color-secondary": palette.secondary,
"color-secondary-hover": palette.secondaryHover,
+ "color-secondary-active": palette.secondaryHover,
"color-secondary-contrast": palette.secondaryContrast,
+ "color-on-secondary": palette.secondaryContrast,
+ ...semanticColorTokens("secondary", palette.secondary, scheme),
+
"color-info": palette.info,
+ ...semanticColorTokens("info", palette.info, scheme),
+
"color-success": palette.success,
+ ...semanticColorTokens("success", palette.success, scheme),
+
"color-warning": palette.warning,
+ ...semanticColorTokens("warning", palette.warning, scheme),
+
"color-danger": 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 {
if (!value) return THEME_PALETTES.blue;
+
if (typeof value === "string") {
const palette = THEME_PALETTES[value];
if (!palette) {
@@ -186,16 +257,47 @@ function resolvePalette(value?: ThemeConfig["palette"]): CustomThemePalette {
}
return palette;
}
+
const missing = Object.keys(THEME_PALETTES.blue).filter((key) => {
const color = value[key as keyof CustomThemePalette];
return typeof color !== "string" || !color.trim();
});
+
if (missing.length) {
throw new Error(`Custom theme palette is missing required colors: ${missing.join(", ")}.`);
}
+
return value;
}
+function uniqueAccentNames(values?: ThemePaletteName[]): ThemePaletteName[] {
+ if (!values?.length) return [...THEME_PALETTE_NAMES];
+
+ const allowed = new Set();
+ 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. */
export const DEFAULT_THEMES: Record = {
light: {
@@ -208,12 +310,6 @@ export const DEFAULT_THEMES: Record = {
"color-text-muted": "#5a6178",
"color-text-subtle": "#737b91",
"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-surface": "#141a30",
"color-code-text": "#e7ecff",
@@ -230,17 +326,7 @@ export const DEFAULT_THEMES: Record = {
"color-text": "#0b1020",
"color-muted": "#5a6178",
"color-border": "#e2e6f0",
- "color-primary": "#2563eb",
- "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",
+ ...paletteTokens(THEME_PALETTES.blue, "light"),
radius: "8px",
"radius-sm": "5px",
"font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
@@ -256,12 +342,6 @@ export const DEFAULT_THEMES: Record = {
"color-text-muted": "#b3b3b3",
"color-text-subtle": "#8a8a8a",
"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-surface": "#101010",
"color-code-text": "#f5f5f5",
@@ -278,17 +358,7 @@ export const DEFAULT_THEMES: Record = {
"color-text": "#f5f5f5",
"color-muted": "#a3a3a3",
"color-border": "#ffffff1f",
- "color-primary": "#6c8cff",
- "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",
+ ...paletteTokens(THEME_PALETTES.blue, "dark"),
radius: "10px",
"radius-sm": "6px",
"font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
@@ -296,25 +366,43 @@ export const DEFAULT_THEMES: Record = {
},
};
+function themeScheme(tokens: ThemeTokens): ColorScheme {
+ return tokens["color-scheme"] === "dark" ? "dark" : "light";
+}
+
/** Merge the user's theme config over the built-in defaults. */
export function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme {
- const semanticTokens = paletteTokens(resolvePalette(config?.palette));
+ const palette = resolvePalette(config?.palette);
const themes: Record = {};
const names = new Set([
...Object.keys(DEFAULT_THEMES),
...Object.keys(config?.themes ?? {}),
]);
+
for (const name of names) {
+ const base = DEFAULT_THEMES[name] ?? DEFAULT_THEMES.light;
+ const scheme = themeScheme(base);
+
themes[name] = {
- ...(DEFAULT_THEMES[name] ?? DEFAULT_THEMES.light),
- ...semanticTokens,
+ ...base,
+ ...paletteTokens(palette, scheme),
...(config?.themes?.[name] ?? {}),
};
}
+
const list = Object.keys(themes);
const preferred = config?.default && themes[config.default] ? config.default : undefined;
const fallback = themes.dark ? "dark" : list[0]!;
- return { default: preferred ?? fallback, names: list, themes };
+ 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. */
@@ -322,6 +410,18 @@ export function resolveThemeName(cookieValue: string | undefined, theme: Resolve
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 {
return Object.entries(tokens)
.map(([key, value]) =>
@@ -330,14 +430,50 @@ function tokensToDeclarations(tokens: ThemeTokens): string {
.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 {
const blocks: string[] = [];
- const def = theme.themes[theme.default];
- if (def) blocks.push(`:root{${tokensToDeclarations(def)}}`);
+ const defaultTheme = theme.themes[theme.default];
+
+ if (defaultTheme) blocks.push(`:root{${tokensToDeclarations(defaultTheme)}}`);
+
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 (
blocks.join("\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
- * `[data-wire-theme-toggle]` / `[data-wire-theme-set]` elements. The configured
- * theme names are baked in so `toggle()` cycles through them in order.
+ * Generate the client theme runtime. It exposes `window.wireTheme` and
+ * `window.wireAccent`, and binds theme/accent controls.
+ *
+ * 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 {
const names = JSON.stringify(theme.names);
+ const accentNames = JSON.stringify(theme.accentNames);
+ const defaultAccent = JSON.stringify(theme.defaultAccent ?? null);
+
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 ACCENT_COOKIE="wire-accent";
- var COOKIE_MAX_AGE=31536000;
- var ACCENTS={
- blue:["#2563eb","#1d4ed8","#ffffff"],
- cyan:["#0891b2","#0e7490","#ffffff"],
- emerald:["#059669","#047857","#ffffff"],
- violet:["#7c3aed","#6d28d9","#ffffff"],
- rose:["#e11d48","#be123c","#ffffff"],
- amber:["#d97706","#b45309","#ffffff"]
- };
+ var ACCENTS=${accentNames};
+ var DEFAULT_THEME=${JSON.stringify(theme.default)};
+ var DEFAULT_ACCENT=${defaultAccent};
+ var MAX_AGE=31536000;
var el=document.documentElement;
+
function readCookie(name){
var prefix=encodeURIComponent(name)+"=";
var parts=document.cookie?document.cookie.split(";"):[];
for(var i=0;i=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){
- (root||document).querySelectorAll("[data-wire-theme-set]").forEach(function(n){
- n.setAttribute("aria-pressed",String(n.getAttribute("data-wire-theme-set")===get()));
+ var current=getTheme();
+ (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){
- n.setAttribute("aria-pressed",String(n.getAttribute("data-wire-accent-set")===name));
+
+ function syncAccentButtons(root){
+ 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;
el.setAttribute("data-theme",name);
- writeCookie(COOKIE,name);
+ writeCookie(THEME_COOKIE,name);
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){
- var value=ACCENTS[name];if(!value)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]);
+ if(ACCENTS.indexOf(name)<0)return;
el.setAttribute("data-accent",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){
- (root||document).querySelectorAll("[data-wire-theme-toggle]").forEach(function(n){
- if(n.__wireThemeBound)return;n.__wireThemeBound=1;
- n.addEventListener("click",function(){toggle();});
+ var target=root||document;
+
+ 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;
- n.addEventListener("click",function(){set(n.getAttribute("data-wire-theme-set"));});
+
+ target.querySelectorAll("[data-wire-theme-set]").forEach(function(node){
+ 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;
- n.addEventListener("click",function(){setAccent(n.getAttribute("data-wire-accent-set"));});
+
+ target.querySelectorAll("[data-wire-accent-set]").forEach(function(node){
+ 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";
- return ACCENTS[current]?current:"blue";
- }
- window.wireTheme={get:get,set:set,toggle:toggle,bind:bind,themes:THEMES};
- window.wireAccent={get:getAccent,set:setAccent,colors:Object.keys(ACCENTS)};
- setAccent(getAccent());
+
+ // SSR should already have these attributes. The cookie fallback only protects
+ // manually generated/static documents that did not pass through WRNexus SSR.
+ var initialTheme=getTheme();
+ if(!el.hasAttribute("data-theme"))el.setAttribute("data-theme",initialTheme);
+ var initialAccent=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);});
- if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",function(){bind(document);});
- else bind(document);
+ if(document.readyState==="loading"){
+ document.addEventListener("DOMContentLoaded",function(){bind(document);});
+ }else{
+ bind(document);
+ }
})();
`;
}
diff --git a/packages/styles/test/theme-accent.test.ts b/packages/styles/test/theme-accent.test.ts
new file mode 100644
index 00000000..315f4676
--- /dev/null
+++ b/packages/styles/test/theme-accent.test.ts
@@ -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");
+ });
+});
diff --git a/packages/syntax/package.json b/packages/syntax/package.json
index bc67aae1..651a2895 100644
--- a/packages/syntax/package.json
+++ b/packages/syntax/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
- "version": "0.5.10",
+ "version": "0.5.11",
"type": "module",
"main": "src/index.ts",
"exports": {
diff --git a/packages/test/package.json b/packages/test/package.json
index 87f9a816..41d319ab 100644
--- a/packages/test/package.json
+++ b/packages/test/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/tracking/package.json b/packages/tracking/package.json
index 6e629d60..8895d8df 100644
--- a/packages/tracking/package.json
+++ b/packages/tracking/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/tracking",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index fc7a9ef4..2b949c8f 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/uploader/package.json b/packages/uploader/package.json
index 7e06c6f2..7af404fa 100644
--- a/packages/uploader/package.json
+++ b/packages/uploader/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/uploader",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/packages/validation/package.json b/packages/validation/package.json
index d318c2d1..a361804a 100644
--- a/packages/validation/package.json
+++ b/packages/validation/package.json
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/validation",
- "version": "0.5.10",
+ "version": "0.5.11",
"private": true,
"type": "module",
"main": "src/index.ts",
diff --git a/update-package-versions.mjs b/update-package-versions.mjs
index fcff1723..909ee8b7 100644
--- a/update-package-versions.mjs
+++ b/update-package-versions.mjs
@@ -1,7 +1,7 @@
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
-const version = "0.5.10";
+const version = "0.5.11";
const dependencyGroups = [
"dependencies",