@wix/web5-core 1.61.0 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/client/applyThemeOverrides.js +45 -14
- package/dist/cjs/client/applyThemeOverrides.js.map +1 -1
- package/dist/cjs/client/themeDebug.js +138 -0
- package/dist/cjs/client/themeDebug.js.map +1 -0
- package/dist/cjs/index.js +13 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/styles/base-tokens.css +8 -1
- package/dist/cjs/theme/tokenContract.js +163 -0
- package/dist/cjs/theme/tokenContract.js.map +1 -0
- package/dist/esm/client/applyThemeOverrides.js +45 -14
- package/dist/esm/client/applyThemeOverrides.js.map +1 -1
- package/dist/esm/client/themeDebug.js +131 -0
- package/dist/esm/client/themeDebug.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/styles/base-tokens.css +8 -1
- package/dist/esm/theme/tokenContract.js +163 -0
- package/dist/esm/theme/tokenContract.js.map +1 -0
- package/dist/types/client/applyThemeOverrides.d.ts +8 -6
- package/dist/types/client/applyThemeOverrides.d.ts.map +1 -1
- package/dist/types/client/themeDebug.d.ts +16 -0
- package/dist/types/client/themeDebug.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/theme/tokenContract.d.ts +103 -0
- package/dist/types/theme/tokenContract.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/styles/base-tokens.css +8 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.TOKEN_NAME_PATTERN = exports.THEME_TOKEN_CONTRACT = exports.BRAND_TOKENS = void 0;
|
|
5
|
+
exports.bucketOf = bucketOf;
|
|
6
|
+
exports.hostAliasFor = hostAliasFor;
|
|
7
|
+
/**
|
|
8
|
+
* The theme-token contract (DL #193).
|
|
9
|
+
*
|
|
10
|
+
* One home for the answer to "who is allowed to set this token, and who wins
|
|
11
|
+
* when more than one of them does". Three things need that answer and must not
|
|
12
|
+
* each keep their own copy:
|
|
13
|
+
*
|
|
14
|
+
* - **`applyThemeOverrides`** decides, per key, whether to write the real
|
|
15
|
+
* token or its `--web5-host-*` alias.
|
|
16
|
+
* - **the seed's `validate-templates`** fails a template that sets a brand
|
|
17
|
+
* token.
|
|
18
|
+
* - **the shop owner's panel** renders a control per `editable` entry.
|
|
19
|
+
*
|
|
20
|
+
* A leaf module beside `hostScope.ts`, and for the same reason: both are data
|
|
21
|
+
* that several packages must agree on, and both have to be readable from plain
|
|
22
|
+
* Node tooling. Requiring this package's ENTRY from Node fails — the CSS import
|
|
23
|
+
* in it is a syntax error to the CJS loader, which is what `scope-css.ts`
|
|
24
|
+
* documents — but a deep import of a leaf like this one works, so the seed's
|
|
25
|
+
* validator can read it without loading a component library.
|
|
26
|
+
*
|
|
27
|
+
* It was briefly its own package. That was justified by `embed-loader` needing
|
|
28
|
+
* the list without taking core's eleven dependencies — and the loader turned
|
|
29
|
+
* out not to need it at all, because `applyThemeOverrides` has owned runtime
|
|
30
|
+
* token injection here since DL #131.
|
|
31
|
+
*
|
|
32
|
+
* ## The two buckets
|
|
33
|
+
*
|
|
34
|
+
* The split is about who WINS, not about what may be imported. A platform
|
|
35
|
+
* adapter is free to read anything the store states; the bucket decides what
|
|
36
|
+
* happens when a template has an opinion about the same token.
|
|
37
|
+
*
|
|
38
|
+
* - `brand` — the store's identity: colour roles and font families. The
|
|
39
|
+
* imported value is written as the real token, last, so it wins
|
|
40
|
+
* unconditionally. A template is not permitted to set these.
|
|
41
|
+
* - `host` — the store's shape: corner radius today. The imported value is
|
|
42
|
+
* written as `--web5-host-<name>` and consumed by core as a `var()`
|
|
43
|
+
* fallback, so a template stating the real token beats it and a template
|
|
44
|
+
* that stays silent inherits the store's.
|
|
45
|
+
*
|
|
46
|
+
* A token absent from this map is treated as `host`. That is deliberate and
|
|
47
|
+
* safe: an alias nothing consumes renders nothing, so an adapter that learns
|
|
48
|
+
* to read a token core has never heard of cannot override a template by
|
|
49
|
+
* accident. The wiring line in core is the real gate for a host token, not the
|
|
50
|
+
* entry here.
|
|
51
|
+
*
|
|
52
|
+
* ## Why all but one are brand
|
|
53
|
+
*
|
|
54
|
+
* This is not a new taxonomy. Twenty of these are the set
|
|
55
|
+
* `web50-shopify-adapter` already emits, with a bucket attached; the
|
|
56
|
+
* twenty-first, `--heading`, has been in core's catalogue since DL #131 and is
|
|
57
|
+
* consumed by the seed, but no adapter fills it yet. Exactly one token changes
|
|
58
|
+
* hands relative to the behaviour before DL #193 — `--radius`, which used to
|
|
59
|
+
* win against the template that had chosen a different one. That single
|
|
60
|
+
* misfiling is the whole of the bug this contract exists to fix.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/** How a value is spelled, which is what makes validation possible. */
|
|
64
|
+
|
|
65
|
+
/** Which layer wins when both a store and a template state this token. */
|
|
66
|
+
|
|
67
|
+
const brandColor = label => ({
|
|
68
|
+
bucket: 'brand',
|
|
69
|
+
type: 'color-hsl',
|
|
70
|
+
editable: false,
|
|
71
|
+
...(label === undefined ? {} : {
|
|
72
|
+
label
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Every token a platform adapter may meaningfully emit today.
|
|
78
|
+
*
|
|
79
|
+
* Growth is adapter-driven on purpose: a token joins when an adapter can
|
|
80
|
+
* actually read it, not on spec. `theme_overrides` is capped at 32 entries by
|
|
81
|
+
* its proto, so there are twelve slots left and no reason to spend them on
|
|
82
|
+
* speculation. `--spacing` is the tempting next one and should be resisted —
|
|
83
|
+
* it scales every `p-*`, `m-*` and `gap-*` in the bundle from one number.
|
|
84
|
+
*/
|
|
85
|
+
const THEME_TOKEN_CONTRACT = exports.THEME_TOKEN_CONTRACT = Object.freeze({
|
|
86
|
+
// ── brand · colour roles (18) ────────────────────────────────────────────
|
|
87
|
+
'--background': brandColor(),
|
|
88
|
+
'--foreground': brandColor(),
|
|
89
|
+
'--card': brandColor(),
|
|
90
|
+
'--card-foreground': brandColor(),
|
|
91
|
+
'--popover': brandColor(),
|
|
92
|
+
'--popover-foreground': brandColor(),
|
|
93
|
+
'--primary': brandColor(),
|
|
94
|
+
'--primary-foreground': brandColor(),
|
|
95
|
+
'--secondary': brandColor(),
|
|
96
|
+
'--secondary-foreground': brandColor(),
|
|
97
|
+
'--muted': brandColor(),
|
|
98
|
+
'--muted-foreground': brandColor(),
|
|
99
|
+
'--accent': brandColor(),
|
|
100
|
+
'--accent-foreground': brandColor(),
|
|
101
|
+
'--border': brandColor(),
|
|
102
|
+
'--input': brandColor(),
|
|
103
|
+
'--ring': brandColor(),
|
|
104
|
+
/**
|
|
105
|
+
* Brand-shaped and supported end to end, but no adapter fills it yet: the
|
|
106
|
+
* seed consumes it (`--color-heading-fg: hsl(var(--heading, var(--foreground)))`)
|
|
107
|
+
* and core has catalogued it since DL #131, while `web50-shopify-adapter`
|
|
108
|
+
* emits the other twenty. A store that states a distinct heading colour has
|
|
109
|
+
* somewhere for it to go the moment an adapter learns to read one.
|
|
110
|
+
*/
|
|
111
|
+
'--heading': brandColor(),
|
|
112
|
+
// ── brand · type (2) ─────────────────────────────────────────────────────
|
|
113
|
+
'--font-sans': {
|
|
114
|
+
bucket: 'brand',
|
|
115
|
+
type: 'font-stack',
|
|
116
|
+
editable: false
|
|
117
|
+
},
|
|
118
|
+
'--font-display': {
|
|
119
|
+
bucket: 'brand',
|
|
120
|
+
type: 'font-stack',
|
|
121
|
+
editable: false
|
|
122
|
+
},
|
|
123
|
+
// ── host · shape (1) ─────────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* The one token whose bucket changed, and the reason this contract exists.
|
|
126
|
+
* Editable because it is the one value a store and a template are known to
|
|
127
|
+
* disagree about in a way a merchant can see and wants to settle.
|
|
128
|
+
*/
|
|
129
|
+
'--radius': {
|
|
130
|
+
bucket: 'host',
|
|
131
|
+
type: 'length',
|
|
132
|
+
editable: true,
|
|
133
|
+
label: 'Corner rounding'
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The tokens written directly, which is the only question the loader has to
|
|
139
|
+
* answer per key — everything else takes the alias path.
|
|
140
|
+
*/
|
|
141
|
+
const BRAND_TOKENS = exports.BRAND_TOKENS = Object.freeze(new Set(Object.entries(THEME_TOKEN_CONTRACT).filter(([, entry]) => entry.bucket === 'brand').map(([token]) => token)));
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A token name we are willing to compose into a CSS declaration.
|
|
145
|
+
*
|
|
146
|
+
* The keys reaching the loader came out of a backend map, which came out of an
|
|
147
|
+
* adapter reading a merchant's theme file. A key carrying `:` or `;` would
|
|
148
|
+
* write arbitrary declarations onto the mount, so the shape is checked where
|
|
149
|
+
* the declaration is built rather than trusted from the source.
|
|
150
|
+
*/
|
|
151
|
+
const TOKEN_NAME_PATTERN = exports.TOKEN_NAME_PATTERN = /^--[a-z0-9-]+$/;
|
|
152
|
+
|
|
153
|
+
/** `--radius` → `--web5-host-radius`. Derived, never stored: one fewer thing to get wrong. */
|
|
154
|
+
function hostAliasFor(token) {
|
|
155
|
+
return `--web5-host-${token.slice(2)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Whether this token is written directly (brand) or as an alias (host, and anything unknown). */
|
|
159
|
+
function bucketOf(token) {
|
|
160
|
+
var _THEME_TOKEN_CONTRACT;
|
|
161
|
+
return ((_THEME_TOKEN_CONTRACT = THEME_TOKEN_CONTRACT[token]) == null ? void 0 : _THEME_TOKEN_CONTRACT.bucket) ?? 'host';
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=tokenContract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["brandColor","label","bucket","type","editable","undefined","THEME_TOKEN_CONTRACT","exports","Object","freeze","BRAND_TOKENS","Set","entries","filter","entry","map","token","TOKEN_NAME_PATTERN","hostAliasFor","slice","bucketOf","_THEME_TOKEN_CONTRACT"],"sources":["../../../src/theme/tokenContract.ts"],"sourcesContent":["/**\n * The theme-token contract (DL #193).\n *\n * One home for the answer to \"who is allowed to set this token, and who wins\n * when more than one of them does\". Three things need that answer and must not\n * each keep their own copy:\n *\n * - **`applyThemeOverrides`** decides, per key, whether to write the real\n * token or its `--web5-host-*` alias.\n * - **the seed's `validate-templates`** fails a template that sets a brand\n * token.\n * - **the shop owner's panel** renders a control per `editable` entry.\n *\n * A leaf module beside `hostScope.ts`, and for the same reason: both are data\n * that several packages must agree on, and both have to be readable from plain\n * Node tooling. Requiring this package's ENTRY from Node fails — the CSS import\n * in it is a syntax error to the CJS loader, which is what `scope-css.ts`\n * documents — but a deep import of a leaf like this one works, so the seed's\n * validator can read it without loading a component library.\n *\n * It was briefly its own package. That was justified by `embed-loader` needing\n * the list without taking core's eleven dependencies — and the loader turned\n * out not to need it at all, because `applyThemeOverrides` has owned runtime\n * token injection here since DL #131.\n *\n * ## The two buckets\n *\n * The split is about who WINS, not about what may be imported. A platform\n * adapter is free to read anything the store states; the bucket decides what\n * happens when a template has an opinion about the same token.\n *\n * - `brand` — the store's identity: colour roles and font families. The\n * imported value is written as the real token, last, so it wins\n * unconditionally. A template is not permitted to set these.\n * - `host` — the store's shape: corner radius today. The imported value is\n * written as `--web5-host-<name>` and consumed by core as a `var()`\n * fallback, so a template stating the real token beats it and a template\n * that stays silent inherits the store's.\n *\n * A token absent from this map is treated as `host`. That is deliberate and\n * safe: an alias nothing consumes renders nothing, so an adapter that learns\n * to read a token core has never heard of cannot override a template by\n * accident. The wiring line in core is the real gate for a host token, not the\n * entry here.\n *\n * ## Why all but one are brand\n *\n * This is not a new taxonomy. Twenty of these are the set\n * `web50-shopify-adapter` already emits, with a bucket attached; the\n * twenty-first, `--heading`, has been in core's catalogue since DL #131 and is\n * consumed by the seed, but no adapter fills it yet. Exactly one token changes\n * hands relative to the behaviour before DL #193 — `--radius`, which used to\n * win against the template that had chosen a different one. That single\n * misfiling is the whole of the bug this contract exists to fix.\n */\n\n/** How a value is spelled, which is what makes validation possible. */\nexport type TokenType =\n /** An HSL triplet with no `hsl()` wrapper — `0 0% 9%`. The core bridge wraps it. */\n | 'color-hsl'\n /** A CSS font stack — `'Poppins', ui-sans-serif, sans-serif`. */\n | 'font-stack'\n /** A CSS length. MUST carry a unit: a bare `0` turns `calc(var(--radius) - 4px)`\n * into a type error and takes the derived radius scale out entirely. */\n | 'length';\n\n/** Which layer wins when both a store and a template state this token. */\nexport type TokenBucket = 'brand' | 'host';\n\nexport interface TokenContractEntry {\n bucket: TokenBucket;\n type: TokenType;\n /** May the shop owner set this from the panel? */\n editable: boolean;\n /** Shown in the owner's panel. Absent for entries that are not editable. */\n label?: string;\n}\n\nconst brandColor = (label?: string): TokenContractEntry => ({\n bucket: 'brand',\n type: 'color-hsl',\n editable: false,\n ...(label === undefined ? {} : { label }),\n});\n\n/**\n * Every token a platform adapter may meaningfully emit today.\n *\n * Growth is adapter-driven on purpose: a token joins when an adapter can\n * actually read it, not on spec. `theme_overrides` is capped at 32 entries by\n * its proto, so there are twelve slots left and no reason to spend them on\n * speculation. `--spacing` is the tempting next one and should be resisted —\n * it scales every `p-*`, `m-*` and `gap-*` in the bundle from one number.\n */\nexport const THEME_TOKEN_CONTRACT: Readonly<Record<string, TokenContractEntry>> =\n Object.freeze({\n // ── brand · colour roles (18) ────────────────────────────────────────────\n '--background': brandColor(),\n '--foreground': brandColor(),\n '--card': brandColor(),\n '--card-foreground': brandColor(),\n '--popover': brandColor(),\n '--popover-foreground': brandColor(),\n '--primary': brandColor(),\n '--primary-foreground': brandColor(),\n '--secondary': brandColor(),\n '--secondary-foreground': brandColor(),\n '--muted': brandColor(),\n '--muted-foreground': brandColor(),\n '--accent': brandColor(),\n '--accent-foreground': brandColor(),\n '--border': brandColor(),\n '--input': brandColor(),\n '--ring': brandColor(),\n\n /**\n * Brand-shaped and supported end to end, but no adapter fills it yet: the\n * seed consumes it (`--color-heading-fg: hsl(var(--heading, var(--foreground)))`)\n * and core has catalogued it since DL #131, while `web50-shopify-adapter`\n * emits the other twenty. A store that states a distinct heading colour has\n * somewhere for it to go the moment an adapter learns to read one.\n */\n '--heading': brandColor(),\n\n // ── brand · type (2) ─────────────────────────────────────────────────────\n '--font-sans': { bucket: 'brand', type: 'font-stack', editable: false },\n '--font-display': { bucket: 'brand', type: 'font-stack', editable: false },\n\n // ── host · shape (1) ─────────────────────────────────────────────────────\n /**\n * The one token whose bucket changed, and the reason this contract exists.\n * Editable because it is the one value a store and a template are known to\n * disagree about in a way a merchant can see and wants to settle.\n */\n '--radius': {\n bucket: 'host',\n type: 'length',\n editable: true,\n label: 'Corner rounding',\n },\n });\n\n/**\n * The tokens written directly, which is the only question the loader has to\n * answer per key — everything else takes the alias path.\n */\nexport const BRAND_TOKENS: ReadonlySet<string> = Object.freeze(\n new Set(\n Object.entries(THEME_TOKEN_CONTRACT)\n .filter(([, entry]) => entry.bucket === 'brand')\n .map(([token]) => token),\n ),\n) as ReadonlySet<string>;\n\n/**\n * A token name we are willing to compose into a CSS declaration.\n *\n * The keys reaching the loader came out of a backend map, which came out of an\n * adapter reading a merchant's theme file. A key carrying `:` or `;` would\n * write arbitrary declarations onto the mount, so the shape is checked where\n * the declaration is built rather than trusted from the source.\n */\nexport const TOKEN_NAME_PATTERN = /^--[a-z0-9-]+$/;\n\n/** `--radius` → `--web5-host-radius`. Derived, never stored: one fewer thing to get wrong. */\nexport function hostAliasFor(token: string): string {\n return `--web5-host-${token.slice(2)}`;\n}\n\n/** Whether this token is written directly (brand) or as an alias (host, and anything unknown). */\nexport function bucketOf(token: string): TokenBucket {\n return THEME_TOKEN_CONTRACT[token]?.bucket ?? 'host';\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -11,11 +11,23 @@
|
|
|
11
11
|
* compiled bundle stylesheet uses — host-page styling is never touched, and
|
|
12
12
|
* equal specificity + later document order makes the override win. Callers
|
|
13
13
|
* must therefore apply AFTER the client bundle's CSS is in the document.
|
|
14
|
-
* - **
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* - **Bucketed, since DL #193**: a `brand` token (colour roles, font families)
|
|
15
|
+
* is written as itself, so the store's identity beats the template's. Every
|
|
16
|
+
* other key — `host` tokens and anything the contract has never heard of —
|
|
17
|
+
* is written as its `--web5-host-*` alias, which core's stylesheets consume
|
|
18
|
+
* as a `var()` fallback. A template stating the real token therefore wins on
|
|
19
|
+
* shape, and an unknown key is inert rather than dangerous: a custom property
|
|
20
|
+
* nothing references renders nothing, so an adapter that learns to read a new
|
|
21
|
+
* token cannot silently override a template.
|
|
22
|
+
* - **The token set stays open, but the destination changed.** Before DL #193
|
|
23
|
+
* an unrecognised key was applied verbatim, so a template could consume
|
|
24
|
+
* `var(--foo)` and grow a token with no release. It now arrives as
|
|
25
|
+
* `--web5-host-foo`, so the template consumes `var(--web5-host-foo, <default>)`
|
|
26
|
+
* instead. One line different, and the provenance is explicit — the wiring
|
|
27
|
+
* line is what activates a host token, not the contract entry.
|
|
28
|
+
* - The mandatory `--` prefix means an override can only define custom
|
|
29
|
+
* properties — it can never set a real CSS property inside the scope. The
|
|
30
|
+
* server enforces the same shape (plus value sanitation) at write time.
|
|
19
31
|
* - **Values are inert**: entries are written with
|
|
20
32
|
* `CSSStyleDeclaration.setProperty`, never string-concatenated into CSS, so
|
|
21
33
|
* a hostile value cannot terminate the declaration or open a new rule.
|
|
@@ -23,6 +35,8 @@
|
|
|
23
35
|
* re-apply; empty/absent input removes it.
|
|
24
36
|
*/
|
|
25
37
|
import { WEB5_SCOPE } from '../hostScope.js';
|
|
38
|
+
import { traceThemeOverrides } from './themeDebug.js';
|
|
39
|
+
import { THEME_TOKEN_CONTRACT, bucketOf, hostAliasFor } from '../theme/tokenContract.js';
|
|
26
40
|
|
|
27
41
|
/** A themeOverrides key: always a CSS custom property. */
|
|
28
42
|
|
|
@@ -33,20 +47,24 @@ import { WEB5_SCOPE } from '../hostScope.js';
|
|
|
33
47
|
*/
|
|
34
48
|
|
|
35
49
|
/**
|
|
36
|
-
* The known token catalog
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
50
|
+
* The known token catalog. GUIDANCE, NOT A GATE — `applyThemeOverrides` still
|
|
51
|
+
* applies any well-formed custom property, so the vocabulary can grow without a
|
|
52
|
+
* release here.
|
|
53
|
+
*
|
|
54
|
+
* Derived from `@wix/web5-token-contract` rather than restated, because two
|
|
55
|
+
* hand-maintained copies of one list is how they drift. The contract also
|
|
56
|
+
* carries what this Set cannot: which bucket each token is in, and therefore
|
|
57
|
+
* who wins when a store and a template disagree.
|
|
42
58
|
*/
|
|
43
|
-
export const THEME_OVERRIDE_TOKENS = new Set(
|
|
59
|
+
export const THEME_OVERRIDE_TOKENS = new Set(Object.keys(THEME_TOKEN_CONTRACT));
|
|
44
60
|
|
|
45
61
|
/** Mirrors the server's Consts.ThemeOverrideKeyPattern. */
|
|
46
62
|
const KEY_PATTERN = /^--[a-z][a-z0-9-]{0,62}$/;
|
|
47
63
|
const MARKER_ATTR = 'data-web5-theme-overrides';
|
|
48
64
|
export function applyThemeOverrides(overrides) {
|
|
49
|
-
if (typeof document === 'undefined')
|
|
65
|
+
if (typeof document === 'undefined') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
50
68
|
const existing = document.head.querySelector(`style[${MARKER_ATTR}]`);
|
|
51
69
|
const entries = Object.entries(overrides ?? {}).filter(_ref => {
|
|
52
70
|
let [key] = _ref;
|
|
@@ -58,6 +76,10 @@ export function applyThemeOverrides(overrides) {
|
|
|
58
76
|
});
|
|
59
77
|
if (entries.length === 0) {
|
|
60
78
|
existing == null || existing.remove();
|
|
79
|
+
// Nothing to apply is itself a traceable answer: it means every token on
|
|
80
|
+
// the page is the template's, which is otherwise indistinguishable from
|
|
81
|
+
// tracing having failed.
|
|
82
|
+
traceThemeOverrides([]);
|
|
61
83
|
return;
|
|
62
84
|
}
|
|
63
85
|
const style = document.createElement('style');
|
|
@@ -70,9 +92,15 @@ export function applyThemeOverrides(overrides) {
|
|
|
70
92
|
}
|
|
71
93
|
sheet.insertRule(`${WEB5_SCOPE} {}`, 0);
|
|
72
94
|
const rule = sheet.cssRules[0];
|
|
95
|
+
const applied = [];
|
|
73
96
|
for (const [key, value] of entries) {
|
|
97
|
+
// Brand wins over the template, so it is written as the token the
|
|
98
|
+
// stylesheets actually read. Everything else lands in the host namespace,
|
|
99
|
+
// where core consumes it only as a fallback the template can beat.
|
|
100
|
+
const target = bucketOf(key) === 'brand' ? key : hostAliasFor(key);
|
|
74
101
|
try {
|
|
75
|
-
rule.style.setProperty(
|
|
102
|
+
rule.style.setProperty(target, value);
|
|
103
|
+
applied.push([key, value]);
|
|
76
104
|
} catch {
|
|
77
105
|
// An engine that rejects the value leaves the token at its baked
|
|
78
106
|
// default — degraded theming, never broken CSS.
|
|
@@ -81,5 +109,8 @@ export function applyThemeOverrides(overrides) {
|
|
|
81
109
|
// Replace-on-reapply: the fresh element is appended (so it stays last in
|
|
82
110
|
// document order) before the stale one is dropped.
|
|
83
111
|
existing == null || existing.remove();
|
|
112
|
+
// AFTER the stale element is gone, so the resolved values traced below are
|
|
113
|
+
// the ones the page will actually render. Off unless explicitly enabled.
|
|
114
|
+
traceThemeOverrides(applied);
|
|
84
115
|
}
|
|
85
116
|
//# sourceMappingURL=applyThemeOverrides.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["WEB5_SCOPE","THEME_OVERRIDE_TOKENS","Set","KEY_PATTERN","MARKER_ATTR","applyThemeOverrides","overrides","document","existing","head","querySelector","entries","
|
|
1
|
+
{"version":3,"names":["WEB5_SCOPE","traceThemeOverrides","THEME_TOKEN_CONTRACT","bucketOf","hostAliasFor","THEME_OVERRIDE_TOKENS","Set","Object","keys","KEY_PATTERN","MARKER_ATTR","applyThemeOverrides","overrides","document","existing","head","querySelector","entries","filter","_ref","key","wellFormed","test","console","warn","length","remove","style","createElement","setAttribute","appendChild","sheet","insertRule","rule","cssRules","applied","value","target","setProperty","push"],"sources":["../../../src/client/applyThemeOverrides.ts"],"sourcesContent":["/**\n * Runtime theme token injection (DL #131).\n *\n * `Configuration.themeOverrides` carries per-store CSS custom-property\n * overrides (`--primary`, `--radius`, `--heading`, ...) computed from the\n * store's own theme (e.g. the Shopify `settings_data.json` extractor). This\n * applies them to the page so they win over the bundle's baked `theme.css`:\n *\n * - **Scoped, not `:root`**: the rule targets `WEB5_SCOPE`, the same\n * `:is(#root, .w5-root, [data-web5-placement], .debug-view)` selector every\n * compiled bundle stylesheet uses — host-page styling is never touched, and\n * equal specificity + later document order makes the override win. Callers\n * must therefore apply AFTER the client bundle's CSS is in the document.\n * - **Bucketed, since DL #193**: a `brand` token (colour roles, font families)\n * is written as itself, so the store's identity beats the template's. Every\n * other key — `host` tokens and anything the contract has never heard of —\n * is written as its `--web5-host-*` alias, which core's stylesheets consume\n * as a `var()` fallback. A template stating the real token therefore wins on\n * shape, and an unknown key is inert rather than dangerous: a custom property\n * nothing references renders nothing, so an adapter that learns to read a new\n * token cannot silently override a template.\n * - **The token set stays open, but the destination changed.** Before DL #193\n * an unrecognised key was applied verbatim, so a template could consume\n * `var(--foo)` and grow a token with no release. It now arrives as\n * `--web5-host-foo`, so the template consumes `var(--web5-host-foo, <default>)`\n * instead. One line different, and the provenance is explicit — the wiring\n * line is what activates a host token, not the contract entry.\n * - The mandatory `--` prefix means an override can only define custom\n * properties — it can never set a real CSS property inside the scope. The\n * server enforces the same shape (plus value sanitation) at write time.\n * - **Values are inert**: entries are written with\n * `CSSStyleDeclaration.setProperty`, never string-concatenated into CSS, so\n * a hostile value cannot terminate the declaration or open a new rule.\n * - **Idempotent**: one marker element per document, replaced wholesale on\n * re-apply; empty/absent input removes it.\n */\nimport { WEB5_SCOPE } from '../hostScope';\nimport { traceThemeOverrides } from './themeDebug';\nimport {\n THEME_TOKEN_CONTRACT,\n bucketOf,\n hostAliasFor,\n} from '../theme/tokenContract';\n\n/** A themeOverrides key: always a CSS custom property. */\nexport type ThemeOverrideKey = `--${string}`;\n\n/**\n * Writer-side shape for a theme override map. The catalog below documents\n * the tokens the seed consumes today; the type stays open so new tokens\n * flow end-to-end with no release anywhere but the seed.\n */\nexport type ThemeOverrides = Record<ThemeOverrideKey, string>;\n\n/**\n * The known token catalog. GUIDANCE, NOT A GATE — `applyThemeOverrides` still\n * applies any well-formed custom property, so the vocabulary can grow without a\n * release here.\n *\n * Derived from `@wix/web5-token-contract` rather than restated, because two\n * hand-maintained copies of one list is how they drift. The contract also\n * carries what this Set cannot: which bucket each token is in, and therefore\n * who wins when a store and a template disagree.\n */\nexport const THEME_OVERRIDE_TOKENS: ReadonlySet<string> = new Set(\n Object.keys(THEME_TOKEN_CONTRACT),\n);\n\n/** Mirrors the server's Consts.ThemeOverrideKeyPattern. */\nconst KEY_PATTERN = /^--[a-z][a-z0-9-]{0,62}$/;\n\nconst MARKER_ATTR = 'data-web5-theme-overrides';\n\nexport function applyThemeOverrides(\n overrides?: Record<string, string> | null,\n): void {\n if (typeof document === 'undefined') {\n return;\n }\n\n const existing = document.head.querySelector(`style[${MARKER_ATTR}]`);\n const entries = Object.entries(overrides ?? {}).filter(([key]) => {\n const wellFormed = KEY_PATTERN.test(key);\n if (!wellFormed) {\n console.warn(\n `[web5-theme] Skipping malformed theme override key: ${key}`,\n );\n }\n return wellFormed;\n });\n\n if (entries.length === 0) {\n existing?.remove();\n // Nothing to apply is itself a traceable answer: it means every token on\n // the page is the template's, which is otherwise indistinguishable from\n // tracing having failed.\n traceThemeOverrides([]);\n return;\n }\n\n const style = document.createElement('style');\n style.setAttribute(MARKER_ATTR, '');\n document.head.appendChild(style);\n const sheet = style.sheet;\n if (!sheet) {\n style.remove();\n return;\n }\n sheet.insertRule(`${WEB5_SCOPE} {}`, 0);\n const rule = sheet.cssRules[0] as CSSStyleRule;\n const applied: [string, string][] = [];\n for (const [key, value] of entries) {\n // Brand wins over the template, so it is written as the token the\n // stylesheets actually read. Everything else lands in the host namespace,\n // where core consumes it only as a fallback the template can beat.\n const target = bucketOf(key) === 'brand' ? key : hostAliasFor(key);\n try {\n rule.style.setProperty(target, value);\n applied.push([key, value]);\n } catch {\n // An engine that rejects the value leaves the token at its baked\n // default — degraded theming, never broken CSS.\n }\n }\n // Replace-on-reapply: the fresh element is appended (so it stays last in\n // document order) before the stale one is dropped.\n existing?.remove();\n // AFTER the stale element is gone, so the resolved values traced below are\n // the ones the page will actually render. Off unless explicitly enabled.\n traceThemeOverrides(applied);\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,UAAU,QAAQ,cAAc;AACzC,SAASC,mBAAmB,QAAQ,cAAc;AAClD,SACEC,oBAAoB,EACpBC,QAAQ,EACRC,YAAY,QACP,wBAAwB;;AAE/B;;AAGA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,qBAA0C,GAAG,IAAIC,GAAG,CAC/DC,MAAM,CAACC,IAAI,CAACN,oBAAoB,CAClC,CAAC;;AAED;AACA,MAAMO,WAAW,GAAG,0BAA0B;AAE9C,MAAMC,WAAW,GAAG,2BAA2B;AAE/C,OAAO,SAASC,mBAAmBA,CACjCC,SAAyC,EACnC;EACN,IAAI,OAAOC,QAAQ,KAAK,WAAW,EAAE;IACnC;EACF;EAEA,MAAMC,QAAQ,GAAGD,QAAQ,CAACE,IAAI,CAACC,aAAa,CAAC,SAASN,WAAW,GAAG,CAAC;EACrE,MAAMO,OAAO,GAAGV,MAAM,CAACU,OAAO,CAACL,SAAS,IAAI,CAAC,CAAC,CAAC,CAACM,MAAM,CAACC,IAAA,IAAW;IAAA,IAAV,CAACC,GAAG,CAAC,GAAAD,IAAA;IAC3D,MAAME,UAAU,GAAGZ,WAAW,CAACa,IAAI,CAACF,GAAG,CAAC;IACxC,IAAI,CAACC,UAAU,EAAE;MACfE,OAAO,CAACC,IAAI,CACV,uDAAuDJ,GAAG,EAC5D,CAAC;IACH;IACA,OAAOC,UAAU;EACnB,CAAC,CAAC;EAEF,IAAIJ,OAAO,CAACQ,MAAM,KAAK,CAAC,EAAE;IACxBX,QAAQ,YAARA,QAAQ,CAAEY,MAAM,CAAC,CAAC;IAClB;IACA;IACA;IACAzB,mBAAmB,CAAC,EAAE,CAAC;IACvB;EACF;EAEA,MAAM0B,KAAK,GAAGd,QAAQ,CAACe,aAAa,CAAC,OAAO,CAAC;EAC7CD,KAAK,CAACE,YAAY,CAACnB,WAAW,EAAE,EAAE,CAAC;EACnCG,QAAQ,CAACE,IAAI,CAACe,WAAW,CAACH,KAAK,CAAC;EAChC,MAAMI,KAAK,GAAGJ,KAAK,CAACI,KAAK;EACzB,IAAI,CAACA,KAAK,EAAE;IACVJ,KAAK,CAACD,MAAM,CAAC,CAAC;IACd;EACF;EACAK,KAAK,CAACC,UAAU,CAAC,GAAGhC,UAAU,KAAK,EAAE,CAAC,CAAC;EACvC,MAAMiC,IAAI,GAAGF,KAAK,CAACG,QAAQ,CAAC,CAAC,CAAiB;EAC9C,MAAMC,OAA2B,GAAG,EAAE;EACtC,KAAK,MAAM,CAACf,GAAG,EAAEgB,KAAK,CAAC,IAAInB,OAAO,EAAE;IAClC;IACA;IACA;IACA,MAAMoB,MAAM,GAAGlC,QAAQ,CAACiB,GAAG,CAAC,KAAK,OAAO,GAAGA,GAAG,GAAGhB,YAAY,CAACgB,GAAG,CAAC;IAClE,IAAI;MACFa,IAAI,CAACN,KAAK,CAACW,WAAW,CAACD,MAAM,EAAED,KAAK,CAAC;MACrCD,OAAO,CAACI,IAAI,CAAC,CAACnB,GAAG,EAAEgB,KAAK,CAAC,CAAC;IAC5B,CAAC,CAAC,MAAM;MACN;MACA;IAAA;EAEJ;EACA;EACA;EACAtB,QAAQ,YAARA,QAAQ,CAAEY,MAAM,CAAC,CAAC;EAClB;EACA;EACAzB,mBAAmB,CAACkC,OAAO,CAAC;AAC9B","ignoreList":[]}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme-override tracing (debug-only).
|
|
3
|
+
*
|
|
4
|
+
* Answers the one question the token pipeline cannot otherwise be asked:
|
|
5
|
+
* **which layer actually won?**
|
|
6
|
+
*
|
|
7
|
+
* Since DL #193 a `brand` token is written as itself and everything else as its
|
|
8
|
+
* `--web5-host-*` alias, which core's stylesheets consume as a `var()` fallback
|
|
9
|
+
* a template can beat. That makes the outcome a cascade decision, and a cascade
|
|
10
|
+
* decision is invisible — there is no callback, no return value, and nothing in
|
|
11
|
+
* the DOM to read. Worse, `applyThemeOverrides` writes through the CSSOM
|
|
12
|
+
* (`insertRule` + `setProperty`, never `textContent`) so that a hostile value
|
|
13
|
+
* can never terminate a declaration, which means DevTools renders the marker as
|
|
14
|
+
* `<style data-web5-theme-overrides="">` — an empty tag, with the rules real but
|
|
15
|
+
* nowhere in the DOM tree. Every part of that is deliberate and every part of it
|
|
16
|
+
* looks broken.
|
|
17
|
+
*
|
|
18
|
+
* So this reads the resolved value back off the mount **after** the rule is in,
|
|
19
|
+
* and reports what the browser actually decided:
|
|
20
|
+
*
|
|
21
|
+
* [web5:theme] --radius host wrote --web5-host-radius=0rem → 1.25rem TEMPLATE
|
|
22
|
+
* [web5:theme] --primary brand wrote --primary=0 0% 9% → 0 0% 9% STORE
|
|
23
|
+
*
|
|
24
|
+
* Off by default. Enable with `?web5DebugTheme=1` (one-shot, wins over storage)
|
|
25
|
+
* or `localStorage["web5_debug_theme"] = "1"` (sticky) — the same shape as
|
|
26
|
+
* `matchDebug`, so there is one convention to learn rather than two.
|
|
27
|
+
*
|
|
28
|
+
* It is gated rather than always-on because reading a computed value forces a
|
|
29
|
+
* style recalculation, and this runs during page setup. One read is taken for
|
|
30
|
+
* the whole batch, not one per token.
|
|
31
|
+
*/
|
|
32
|
+
import { WEB5_SCOPES } from '../hostScope.js';
|
|
33
|
+
import { bucketOf, hostAliasFor } from '../theme/tokenContract.js';
|
|
34
|
+
export const THEME_DEBUG_KEY = 'web5_debug_theme';
|
|
35
|
+
|
|
36
|
+
/** Query param that forces theme tracing on, overriding localStorage. */
|
|
37
|
+
export const THEME_DEBUG_QUERY_PARAM = 'web5DebugTheme';
|
|
38
|
+
const LOG_PREFIX = '[web5:theme]';
|
|
39
|
+
const isTruthy = value => value === '1' || value === 'true';
|
|
40
|
+
let cached = null;
|
|
41
|
+
|
|
42
|
+
/** Whether theme tracing is enabled. Memoized — resolved once per page load. */
|
|
43
|
+
export const isThemeDebugEnabled = () => {
|
|
44
|
+
if (cached !== null) {
|
|
45
|
+
return cached;
|
|
46
|
+
}
|
|
47
|
+
cached = false;
|
|
48
|
+
try {
|
|
49
|
+
if (isTruthy(new URLSearchParams(window.location.search).get(THEME_DEBUG_QUERY_PARAM))) {
|
|
50
|
+
cached = true;
|
|
51
|
+
return cached;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// window / URLSearchParams unavailable (SSR, non-DOM environments).
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
cached = isTruthy(localStorage.getItem(THEME_DEBUG_KEY));
|
|
58
|
+
} catch {
|
|
59
|
+
// localStorage can throw in privacy mode / non-DOM environments.
|
|
60
|
+
}
|
|
61
|
+
return cached;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Test seam — the flag is memoized for the page, so tests must be able to clear it. */
|
|
65
|
+
export const resetThemeDebugCache = () => {
|
|
66
|
+
cached = null;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Report what each override did, after the rule is live.
|
|
70
|
+
*
|
|
71
|
+
* `applied` is what `applyThemeOverrides` actually wrote — already filtered for
|
|
72
|
+
* key shape, so anything malformed has been dropped and warned about before
|
|
73
|
+
* reaching here.
|
|
74
|
+
*/
|
|
75
|
+
export function traceThemeOverrides(applied) {
|
|
76
|
+
if (!isThemeDebugEnabled()) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (applied.length === 0) {
|
|
80
|
+
// Silence here would be indistinguishable from tracing being broken, and
|
|
81
|
+
// "no overrides reached the page" is itself the answer often enough — a
|
|
82
|
+
// store with nothing imported means every token is the template's.
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.info(`${LOG_PREFIX} no overrides applied — nothing usable arrived for this store, so every token is whatever the template and core defaults say`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let mount = null;
|
|
88
|
+
try {
|
|
89
|
+
mount = document.querySelector(WEB5_SCOPES.join(','));
|
|
90
|
+
} catch {
|
|
91
|
+
// Malformed selector cannot happen with the constant, but querySelector is
|
|
92
|
+
// the one call here that can throw, and a debug aid must never be the thing
|
|
93
|
+
// that breaks a page.
|
|
94
|
+
}
|
|
95
|
+
if (!mount) {
|
|
96
|
+
console.warn(`${LOG_PREFIX} no mount found (${WEB5_SCOPES.join(', ')}) — tokens were written, but their resolved values cannot be read`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// One computed-style read for the whole batch: the expensive part is the style
|
|
101
|
+
// recalculation it forces, not the per-property lookups off the result.
|
|
102
|
+
const computed = getComputedStyle(mount);
|
|
103
|
+
const rows = applied.map(_ref => {
|
|
104
|
+
let [token, value] = _ref;
|
|
105
|
+
const bucket = bucketOf(token);
|
|
106
|
+
const target = bucket === 'brand' ? token : hostAliasFor(token);
|
|
107
|
+
const resolved = computed.getPropertyValue(token).trim();
|
|
108
|
+
const wanted = value.trim();
|
|
109
|
+
return {
|
|
110
|
+
token,
|
|
111
|
+
bucket,
|
|
112
|
+
'written as': target,
|
|
113
|
+
'store said': wanted,
|
|
114
|
+
'resolves to': resolved || '(nothing reads it)',
|
|
115
|
+
winner: !resolved ? 'NOBODY — no stylesheet consumes this token' : resolved === wanted ? 'STORE' : 'TEMPLATE'
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
const overridden = rows.filter(r => r.winner === 'TEMPLATE').length;
|
|
119
|
+
const inert = rows.filter(r => r.winner.startsWith('NOBODY')).length;
|
|
120
|
+
|
|
121
|
+
/* eslint-disable no-console */
|
|
122
|
+
console.groupCollapsed(`${LOG_PREFIX} ${rows.length} override(s) applied · ${overridden} kept by the template · ${inert} inert`);
|
|
123
|
+
console.table(rows);
|
|
124
|
+
if (inert > 0) {
|
|
125
|
+
console.info(`${LOG_PREFIX} "inert" means the value was written but no stylesheet reads that token — ` + `for a host token that means core has no \`var(--web5-host-…)\` wiring for it yet.`);
|
|
126
|
+
}
|
|
127
|
+
console.info(`${LOG_PREFIX} the marker <style data-web5-theme-overrides> looks empty in DevTools on purpose: ` + `rules are inserted through the CSSOM, never as text. Read them with ` + `document.querySelector('style[data-web5-theme-overrides]').sheet.cssRules[0].cssText`);
|
|
128
|
+
console.groupEnd();
|
|
129
|
+
/* eslint-enable no-console */
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=themeDebug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["WEB5_SCOPES","bucketOf","hostAliasFor","THEME_DEBUG_KEY","THEME_DEBUG_QUERY_PARAM","LOG_PREFIX","isTruthy","value","cached","isThemeDebugEnabled","URLSearchParams","window","location","search","get","localStorage","getItem","resetThemeDebugCache","traceThemeOverrides","applied","length","console","info","mount","document","querySelector","join","warn","computed","getComputedStyle","rows","map","_ref","token","bucket","target","resolved","getPropertyValue","trim","wanted","winner","overridden","filter","r","inert","startsWith","groupCollapsed","table","groupEnd"],"sources":["../../../src/client/themeDebug.ts"],"sourcesContent":["/**\n * Theme-override tracing (debug-only).\n *\n * Answers the one question the token pipeline cannot otherwise be asked:\n * **which layer actually won?**\n *\n * Since DL #193 a `brand` token is written as itself and everything else as its\n * `--web5-host-*` alias, which core's stylesheets consume as a `var()` fallback\n * a template can beat. That makes the outcome a cascade decision, and a cascade\n * decision is invisible — there is no callback, no return value, and nothing in\n * the DOM to read. Worse, `applyThemeOverrides` writes through the CSSOM\n * (`insertRule` + `setProperty`, never `textContent`) so that a hostile value\n * can never terminate a declaration, which means DevTools renders the marker as\n * `<style data-web5-theme-overrides=\"\">` — an empty tag, with the rules real but\n * nowhere in the DOM tree. Every part of that is deliberate and every part of it\n * looks broken.\n *\n * So this reads the resolved value back off the mount **after** the rule is in,\n * and reports what the browser actually decided:\n *\n * [web5:theme] --radius host wrote --web5-host-radius=0rem → 1.25rem TEMPLATE\n * [web5:theme] --primary brand wrote --primary=0 0% 9% → 0 0% 9% STORE\n *\n * Off by default. Enable with `?web5DebugTheme=1` (one-shot, wins over storage)\n * or `localStorage[\"web5_debug_theme\"] = \"1\"` (sticky) — the same shape as\n * `matchDebug`, so there is one convention to learn rather than two.\n *\n * It is gated rather than always-on because reading a computed value forces a\n * style recalculation, and this runs during page setup. One read is taken for\n * the whole batch, not one per token.\n */\nimport { WEB5_SCOPES } from '../hostScope';\nimport { bucketOf, hostAliasFor } from '../theme/tokenContract';\n\nexport const THEME_DEBUG_KEY = 'web5_debug_theme';\n\n/** Query param that forces theme tracing on, overriding localStorage. */\nexport const THEME_DEBUG_QUERY_PARAM = 'web5DebugTheme';\n\nconst LOG_PREFIX = '[web5:theme]';\n\nconst isTruthy = (value: unknown): boolean => value === '1' || value === 'true';\n\nlet cached: boolean | null = null;\n\n/** Whether theme tracing is enabled. Memoized — resolved once per page load. */\nexport const isThemeDebugEnabled = (): boolean => {\n if (cached !== null) {\n return cached;\n }\n cached = false;\n try {\n if (\n isTruthy(\n new URLSearchParams(window.location.search).get(\n THEME_DEBUG_QUERY_PARAM,\n ),\n )\n ) {\n cached = true;\n return cached;\n }\n } catch {\n // window / URLSearchParams unavailable (SSR, non-DOM environments).\n }\n try {\n cached = isTruthy(localStorage.getItem(THEME_DEBUG_KEY));\n } catch {\n // localStorage can throw in privacy mode / non-DOM environments.\n }\n return cached;\n};\n\n/** Test seam — the flag is memoized for the page, so tests must be able to clear it. */\nexport const resetThemeDebugCache = (): void => {\n cached = null;\n};\n\ninterface TraceRow {\n token: string;\n bucket: string;\n 'written as': string;\n 'store said': string;\n 'resolves to': string;\n winner: string;\n}\n\n/**\n * Report what each override did, after the rule is live.\n *\n * `applied` is what `applyThemeOverrides` actually wrote — already filtered for\n * key shape, so anything malformed has been dropped and warned about before\n * reaching here.\n */\nexport function traceThemeOverrides(applied: [string, string][]): void {\n if (!isThemeDebugEnabled()) {\n return;\n }\n if (applied.length === 0) {\n // Silence here would be indistinguishable from tracing being broken, and\n // \"no overrides reached the page\" is itself the answer often enough — a\n // store with nothing imported means every token is the template's.\n // eslint-disable-next-line no-console\n console.info(\n `${LOG_PREFIX} no overrides applied — nothing usable arrived for this store, so every token is whatever the template and core defaults say`,\n );\n return;\n }\n let mount: Element | null = null;\n try {\n mount = document.querySelector(WEB5_SCOPES.join(','));\n } catch {\n // Malformed selector cannot happen with the constant, but querySelector is\n // the one call here that can throw, and a debug aid must never be the thing\n // that breaks a page.\n }\n if (!mount) {\n console.warn(\n `${LOG_PREFIX} no mount found (${WEB5_SCOPES.join(\n ', ',\n )}) — tokens were written, but their resolved values cannot be read`,\n );\n return;\n }\n\n // One computed-style read for the whole batch: the expensive part is the style\n // recalculation it forces, not the per-property lookups off the result.\n const computed = getComputedStyle(mount);\n const rows: TraceRow[] = applied.map(([token, value]) => {\n const bucket = bucketOf(token);\n const target = bucket === 'brand' ? token : hostAliasFor(token);\n const resolved = computed.getPropertyValue(token).trim();\n const wanted = value.trim();\n return {\n token,\n bucket,\n 'written as': target,\n 'store said': wanted,\n 'resolves to': resolved || '(nothing reads it)',\n winner: !resolved\n ? 'NOBODY — no stylesheet consumes this token'\n : resolved === wanted\n ? 'STORE'\n : 'TEMPLATE',\n };\n });\n\n const overridden = rows.filter((r) => r.winner === 'TEMPLATE').length;\n const inert = rows.filter((r) => r.winner.startsWith('NOBODY')).length;\n\n /* eslint-disable no-console */\n console.groupCollapsed(\n `${LOG_PREFIX} ${rows.length} override(s) applied · ${overridden} kept by the template · ${inert} inert`,\n );\n console.table(rows);\n if (inert > 0) {\n console.info(\n `${LOG_PREFIX} \"inert\" means the value was written but no stylesheet reads that token — ` +\n `for a host token that means core has no \\`var(--web5-host-…)\\` wiring for it yet.`,\n );\n }\n console.info(\n `${LOG_PREFIX} the marker <style data-web5-theme-overrides> looks empty in DevTools on purpose: ` +\n `rules are inserted through the CSSOM, never as text. Read them with ` +\n `document.querySelector('style[data-web5-theme-overrides]').sheet.cssRules[0].cssText`,\n );\n console.groupEnd();\n /* eslint-enable no-console */\n}\n"],"mappings":"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","ignoreList":[]}
|
package/dist/esm/index.js
CHANGED
|
@@ -142,6 +142,8 @@ export { getClientBundleOverride, isTrustedBundleHost } from './client/clientBun
|
|
|
142
142
|
export { TEMPLATES_CDN_BASE, TEMPLATES_MANIFEST_URL, getTemplateOverride, isTemplatePickerRequested, isValidTemplateId, resolveClientBundleUrl } from './client/clientBundleUrl.js';
|
|
143
143
|
export { mergeClientConfig } from './client/mergeClientConfig.js';
|
|
144
144
|
export { applyThemeOverrides, THEME_OVERRIDE_TOKENS } from './client/applyThemeOverrides.js';
|
|
145
|
+
export { THEME_TOKEN_CONTRACT, BRAND_TOKENS, TOKEN_NAME_PATTERN, bucketOf, hostAliasFor } from './theme/tokenContract.js';
|
|
146
|
+
export { isThemeDebugEnabled, THEME_DEBUG_KEY, THEME_DEBUG_QUERY_PARAM } from './client/themeDebug.js';
|
|
145
147
|
|
|
146
148
|
// Placement renderer + DI helper (DL #088 D3.2, D3.4)
|
|
147
149
|
export { PlacementResponseRenderer, PlacementSmoothHeight } from './components/placement/PlacementResponseRenderer.js';
|