From f37303b22d865820095f1a6b2cc9c03d4aa2a80d Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Thu, 30 Jul 2026 15:03:52 +0530 Subject: [PATCH] release: WRNexusJS 0.5.11 --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/package.json | 26 +- .publish/compiler/package.json | 4 +- .publish/core/package.json | 2 +- .publish/csr/package.json | 4 +- .publish/db/package.json | 2 +- .publish/dev-server/package.json | 30 +- .publish/dev-toolbar/package.json | 2 +- .publish/encryption/package.json | 2 +- .publish/helpers/package.json | 4 +- .publish/i18n/package.json | 4 +- .publish/jwt/package.json | 2 +- .publish/mobile/package.json | 4 +- .publish/native/package.json | 2 +- .publish/oauth/package.json | 2 +- .publish/plugin/package.json | 4 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 2 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +- .publish/ssr/package.json | 4 +- .publish/styles/package.json | 8 +- .publish/syntax/package.json | 2 +- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 +- .publish/uploader/package.json | 4 +- .publish/validation/package.json | 2 +- packages/ai/package.json | 2 +- packages/auth/package.json | 2 +- packages/authz/package.json | 2 +- packages/captcha/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/update.ts | 8 + packages/compiler/package.json | 2 +- packages/core/package.json | 2 +- packages/csr/package.json | 2 +- packages/db/package.json | 2 +- packages/dev-server/package.json | 2 +- packages/dev-server/src/runtime.ts | 59 ++- .../test/theme-accent-runtime.test.ts | 14 + packages/dev-toolbar/package.json | 2 +- packages/encryption/package.json | 2 +- packages/helpers/package.json | 2 +- packages/i18n/package.json | 2 +- packages/jwt/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/plugin/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/router/package.json | 2 +- packages/ssr/package.json | 2 +- packages/styles/package.json | 2 +- packages/styles/src/index.ts | 1 + packages/styles/src/theme.ts | 410 +++++++++++++----- packages/styles/test/theme-accent.test.ts | 49 +++ packages/syntax/package.json | 2 +- packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/package.json | 2 +- update-package-versions.mjs | 2 +- 67 files changed, 530 insertions(+), 213 deletions(-) create mode 100644 packages/dev-server/test/theme-accent-runtime.test.ts create mode 100644 packages/styles/test/theme-accent.test.ts 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",