@akinon/pz-theme 2.0.115-beta.0 → 2.0.115
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/CHANGELOG.md +4 -4
- package/package.json +4 -4
- package/src/blocks/__tests__/menu-query-args.test.tsx +239 -0
- package/src/blocks/__tests__/nav-menu-ssr.test.tsx +260 -0
- package/src/blocks/menu-query-args.ts +105 -0
- package/src/blocks/nav-menu-block.tsx +21 -10
- package/src/chrome/__tests__/chrome-mode.test.ts +35 -2
- package/src/chrome/__tests__/menu-seed-source.test.ts +186 -0
- package/src/chrome/__tests__/use-chrome-mode.test.tsx +162 -0
- package/src/chrome/chrome-mode.ts +33 -2
- package/src/chrome/menu-seed-source.ts +89 -0
- package/src/chrome/use-chrome-mode.ts +44 -0
- package/src/custom/README-sections.md +32 -15
- package/src/custom/__tests__/custom-entry-exports.test.tsx +50 -0
- package/src/custom/index.ts +22 -6
- package/src/index.ts +2 -0
- package/src/menu-seed-context.tsx +38 -0
- package/src/theme-placeholder-wrapper.tsx +103 -91
- package/src/theme-placeholder.tsx +5 -0
- package/src/utils/__tests__/docked-state-styles.test.ts +88 -19
- package/src/utils/part-styles.ts +36 -4
|
@@ -39,6 +39,13 @@ import type { Section } from '../../theme-section';
|
|
|
39
39
|
* by emission order. Nothing here asserts that pair; there is no right answer
|
|
40
40
|
* to assert yet.
|
|
41
41
|
*
|
|
42
|
+
* THE STATE HAS TWO EXPRESSIONS, emitted as one rule. The `body` stamp is
|
|
43
|
+
* written from a layout effect and so is missing from the server HTML; the
|
|
44
|
+
* brand's header also renders the attribute on its own root (`useChromeMode`),
|
|
45
|
+
* and `:is(body:has(…))` reads it there, which is what makes the first paint
|
|
46
|
+
* dock. Everything the ladder above promises has to hold for EACH expression,
|
|
47
|
+
* so each is asserted below.
|
|
48
|
+
*
|
|
42
49
|
* WHY THERE IS NO `getComputedStyle` TEST BELOW, and why adding one would make
|
|
43
50
|
* this file worse: jsdom has no cascade worth the name. Its `getComputedStyle`
|
|
44
51
|
* resolves competing rules by SOURCE ORDER ALONE and ignores specificity
|
|
@@ -46,9 +53,17 @@ import type { Section } from '../../theme-section';
|
|
|
46
53
|
* cascade test would therefore assert the opposite of what a browser does, pass
|
|
47
54
|
* today for the wrong reason, and keep passing after someone deleted the very
|
|
48
55
|
* qualifier it was written to protect. So the weight is asserted directly.
|
|
56
|
+
*
|
|
57
|
+
* NOR IS THE `:has` EXPRESSION EVER HANDED TO THE DOM. jsdom's selector engine
|
|
58
|
+
* answers `:has()` inside `:is()` with no match at all (measured: zero results
|
|
59
|
+
* on a document the expression plainly matches), so a DOM assertion on it
|
|
60
|
+
* would fail for a reason that has nothing to do with docked. It is asserted as
|
|
61
|
+
* text, and its weight is worked out by hand.
|
|
49
62
|
*/
|
|
50
63
|
|
|
51
64
|
const DOCKED = 'body[data-chrome-mode~="docked"]';
|
|
65
|
+
/** The first-paint expression: the attribute on the brand's own chrome root. */
|
|
66
|
+
const DOCKED_HAS = ':is(body:has([data-chrome-mode~="docked"]))';
|
|
52
67
|
const SCROLLED = '[data-scroll-state~="scrolled"]';
|
|
53
68
|
const SECTION = '[data-section-id="sec-1"]';
|
|
54
69
|
|
|
@@ -79,18 +94,33 @@ const block = (id: string, styles: Record<string, unknown>, type = 'group') => (
|
|
|
79
94
|
});
|
|
80
95
|
|
|
81
96
|
/**
|
|
82
|
-
* Weighs the
|
|
97
|
+
* Weighs the selectors this file is about, and nothing else.
|
|
83
98
|
*
|
|
84
99
|
* Deliberately naive: it counts attribute selectors and leading type selectors,
|
|
85
|
-
* which is all
|
|
86
|
-
* a second implementation to get wrong, and the
|
|
87
|
-
* DOM — does not exist here (see the note
|
|
100
|
+
* which is all the `body` and `scrolled` expressions contain. A general
|
|
101
|
+
* specificity parser would be a second implementation to get wrong, and the
|
|
102
|
+
* honest alternative — asking the DOM — does not exist here (see the note
|
|
103
|
+
* above). The `:has` expression is brought within its reach by `unwrap` below.
|
|
88
104
|
*/
|
|
89
105
|
const weigh = (selector: string) => ({
|
|
90
106
|
attributes: (selector.match(/\[/g) || []).length,
|
|
91
107
|
types: (selector.match(/(^|[\s>+~])[a-z][a-z0-9-]*/g) || []).length
|
|
92
108
|
});
|
|
93
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Removes `:is(` / `:has(` and their closing parens, leaving the arguments in
|
|
112
|
+
* place.
|
|
113
|
+
*
|
|
114
|
+
* Exact for weight, by the spec rather than by approximation: `:is()` and
|
|
115
|
+
* `:has()` take the specificity of their most specific argument and add
|
|
116
|
+
* nothing of their own, and each here has exactly one argument. So
|
|
117
|
+
* `:is(body:has([a]))` weighs what `body[a]` weighs — (0,1,1) — and unwrapping
|
|
118
|
+
* is the hand calculation written down. Only safe on selectors with no other
|
|
119
|
+
* parentheses, which is true of every one it is given.
|
|
120
|
+
*/
|
|
121
|
+
const unwrap = (selector: string) =>
|
|
122
|
+
selector.replace(/:(is|has)\(/g, '').replace(/\)/g, '');
|
|
123
|
+
|
|
94
124
|
afterEach(() => {
|
|
95
125
|
document.body.removeAttribute('data-chrome-mode');
|
|
96
126
|
document.body.innerHTML = '';
|
|
@@ -105,14 +135,42 @@ describe('the `body` qualifier is load-bearing — do not tidy it away', () => {
|
|
|
105
135
|
expect(selector).toBe(`${DOCKED} ${SECTION}`);
|
|
106
136
|
});
|
|
107
137
|
|
|
108
|
-
it('
|
|
109
|
-
|
|
110
|
-
|
|
138
|
+
it('keeps the first-paint expression second, wrapped in :is(), after the body stamp', () => {
|
|
139
|
+
// Literal text for the same reason as above, and for two more. The FIRST
|
|
140
|
+
// expression is grepped for by pz-dynamic-widget, so it must stay first and
|
|
141
|
+
// byte-identical. The `:is()` wrapper looks removable and is not: the two
|
|
142
|
+
// expressions share one comma-separated rule, and without the forgiving
|
|
143
|
+
// `:is()` a browser that cannot parse `:has` would drop the body branch
|
|
144
|
+
// along with it.
|
|
145
|
+
expect(buildRootStateSelectors(SECTION, 'docked')).toEqual([
|
|
146
|
+
`${DOCKED} ${SECTION}`,
|
|
147
|
+
`${DOCKED_HAS} ${SECTION}`
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
111
150
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
151
|
+
it('weighs the :has expression exactly as the body stamp, so the ladder holds either way', () => {
|
|
152
|
+
const [body, has] = buildRootStateSelectors(SECTION, 'docked');
|
|
153
|
+
|
|
154
|
+
// By hand: `:is(X)` and `:has(X)` each weigh X, so the prefix is `body`
|
|
155
|
+
// (0,0,1) plus one attribute (0,1,0) — (0,1,1), the body stamp's weight.
|
|
156
|
+
// Unwrapping lands on the body stamp's selector itself, which is the
|
|
157
|
+
// strongest form that equality can take.
|
|
158
|
+
expect(unwrap(has)).toBe(body);
|
|
159
|
+
expect(weigh(unwrap(has))).toEqual(weigh(body));
|
|
160
|
+
expect(weigh(unwrap(has))).toEqual({ attributes: 2, types: 1 });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('outweighs the scrolled selector with every expression, tying on attributes alone', () => {
|
|
164
|
+
const [scrolled] = buildRootStateSelectors(SECTION, 'scrolled');
|
|
165
|
+
const dockedExpressions = buildRootStateSelectors(SECTION, 'docked');
|
|
166
|
+
|
|
167
|
+
expect(dockedExpressions).toHaveLength(2);
|
|
168
|
+
dockedExpressions.map(unwrap).forEach((docked) => {
|
|
169
|
+
// Same attribute count: this is the tie, and it is why order would decide.
|
|
170
|
+
expect(weigh(docked).attributes).toBe(weigh(scrolled).attributes);
|
|
171
|
+
// The type selector is the entire margin of victory.
|
|
172
|
+
expect(weigh(docked).types).toBeGreaterThan(weigh(scrolled).types);
|
|
173
|
+
});
|
|
116
174
|
});
|
|
117
175
|
|
|
118
176
|
it('matches the same band as scrolled on an inner page, so the tie is real', () => {
|
|
@@ -164,8 +222,11 @@ describe('the generated CSS', () => {
|
|
|
164
222
|
})
|
|
165
223
|
]);
|
|
166
224
|
|
|
225
|
+
// Both expressions in ONE rule, the body stamp first — which is also why
|
|
226
|
+
// the second must be the forgiving `:is()` form (see above).
|
|
167
227
|
expect(css).toContain(
|
|
168
|
-
`${DOCKED} ${SECTION} {\n
|
|
228
|
+
`${DOCKED} ${SECTION},\n${DOCKED_HAS} ${SECTION} {\n` +
|
|
229
|
+
' background-color: rgba(255, 255, 255, 1);\n}'
|
|
169
230
|
);
|
|
170
231
|
expect(css).toContain(
|
|
171
232
|
`${SCROLLED} ${SECTION} {\n background-color: rgba(245, 245, 245, 1);\n}`
|
|
@@ -182,7 +243,8 @@ describe('the generated CSS', () => {
|
|
|
182
243
|
]);
|
|
183
244
|
|
|
184
245
|
expect(css).toContain(
|
|
185
|
-
`${DOCKED} [data-block-id="bar"]
|
|
246
|
+
`${DOCKED} [data-block-id="bar"],\n` +
|
|
247
|
+
`${DOCKED_HAS} [data-block-id="bar"] {\n color: #4a0e26;\n}`
|
|
186
248
|
);
|
|
187
249
|
});
|
|
188
250
|
|
|
@@ -197,24 +259,28 @@ describe('the generated CSS', () => {
|
|
|
197
259
|
|
|
198
260
|
describe('docked as a part state', () => {
|
|
199
261
|
it('conditions a part on the page without losing the ownership guard', () => {
|
|
200
|
-
const
|
|
262
|
+
const selectors = buildPartSelectors('[data-section-id="sec-1"]', {
|
|
201
263
|
part: 'bar',
|
|
202
264
|
state: 'docked',
|
|
203
265
|
styles: {}
|
|
204
266
|
});
|
|
267
|
+
const guarded =
|
|
268
|
+
'[data-section-id="sec-1"] [data-part="bar"]' +
|
|
269
|
+
':not(:where([data-section-id="sec-1"] [data-block-id] *))';
|
|
205
270
|
|
|
206
271
|
// The whole compound, guard included, is substituted into the ancestor
|
|
207
272
|
// pattern — so a docked rule can no more reach into a nested block than the
|
|
208
|
-
// stateless one can.
|
|
273
|
+
// stateless one can. Into BOTH patterns: the first-paint expression gets
|
|
274
|
+
// the guard too.
|
|
209
275
|
//
|
|
210
276
|
// Asserted as text rather than by querying the DOM on purpose: the guard
|
|
211
277
|
// uses `:where()`, and the nwsapi build in this workspace's node_modules
|
|
212
278
|
// cannot parse it (it is the cause of the three known-red suites here). A
|
|
213
279
|
// DOM assertion would fail for a reason that has nothing to do with docked.
|
|
214
|
-
expect(
|
|
215
|
-
`${DOCKED}
|
|
216
|
-
|
|
217
|
-
);
|
|
280
|
+
expect(selectors).toEqual([
|
|
281
|
+
`${DOCKED} ${guarded}`,
|
|
282
|
+
`${DOCKED_HAS} ${guarded}`
|
|
283
|
+
]);
|
|
218
284
|
});
|
|
219
285
|
|
|
220
286
|
it('parses `bar/docked/color` as a part key and buckets it', () => {
|
|
@@ -237,6 +303,9 @@ describe('docked as a part state', () => {
|
|
|
237
303
|
expect(css).toContain(
|
|
238
304
|
`${DOCKED} [data-section-id="sec-1"] [data-part="bar"]`
|
|
239
305
|
);
|
|
306
|
+
expect(css).toContain(
|
|
307
|
+
`${DOCKED_HAS} [data-section-id="sec-1"] [data-part="bar"]`
|
|
308
|
+
);
|
|
240
309
|
});
|
|
241
310
|
});
|
|
242
311
|
|
package/src/utils/part-styles.ts
CHANGED
|
@@ -143,9 +143,9 @@ const STATE_SELECTORS: Record<string, string[]> = {
|
|
|
143
143
|
'overlay-open': ['body[data-overlay-state~="open"] &'],
|
|
144
144
|
/**
|
|
145
145
|
* The third page-level state: the chrome is DOCKED above the page rather than
|
|
146
|
-
* sitting over it. `
|
|
147
|
-
*
|
|
148
|
-
* which of its routes carry a hero.
|
|
146
|
+
* sitting over it. `data-chrome-mode` (see chrome/chrome-mode.ts) is rendered
|
|
147
|
+
* by the brand on its own chrome element and stamped on `body`, because only
|
|
148
|
+
* the brand knows which of its routes carry a hero.
|
|
149
149
|
*
|
|
150
150
|
* THE `body` QUALIFIER IS A DELIBERATE SPECIFICITY BUMP, not tidiness. On an
|
|
151
151
|
* inner page this and `scrolled` BOTH match at once, and unqualified they
|
|
@@ -171,8 +171,40 @@ const STATE_SELECTORS: Record<string, string[]> = {
|
|
|
171
171
|
* the answer. Whoever asks it should take that rung rather than dropping one
|
|
172
172
|
* of these two qualifiers, which only hands `scrolled` back a fight it should
|
|
173
173
|
* lose.
|
|
174
|
+
*
|
|
175
|
+
* THE SECOND EXPRESSION IS THE FIRST-PAINT SIGNAL. The `body` stamp is
|
|
176
|
+
* written from a layout effect, so it is not in the server HTML, and every
|
|
177
|
+
* inner page used to paint the transparent overlay header until hydration
|
|
178
|
+
* ran the effect. `useChromeMode` / `chromeModeAttrs` render the same
|
|
179
|
+
* attribute on the brand's own chrome element, which IS in the server HTML,
|
|
180
|
+
* and `body:has(…)` reads it from there. The first expression stays, byte for
|
|
181
|
+
* byte — pz-dynamic-widget greps for it, and it is what the designer pin, a
|
|
182
|
+
* browser without `:has` and hand-written brand CSS all match.
|
|
183
|
+
*
|
|
184
|
+
* SAME WEIGHT, so the ladder above holds whichever branch matched. `:is()`
|
|
185
|
+
* and `:has()` each weigh their most specific argument and add nothing of
|
|
186
|
+
* their own, so the prefix is `body` plus one attribute, (0,1,1), exactly
|
|
187
|
+
* the first expression's: `scrolled` < `docked` = `overlay-open` as before.
|
|
188
|
+
*
|
|
189
|
+
* WRAPPED IN `:is()` because a state's expressions are not separate rules:
|
|
190
|
+
* `generatePartCSS` and `generateRootStateCSS` (utils/index.ts) join them
|
|
191
|
+
* into ONE comma-separated selector list, and a browser drops a whole list
|
|
192
|
+
* when it cannot parse one member — one without `:has` would lose the `body`
|
|
193
|
+
* branch with it and never dock at all. `:is()` takes a forgiving selector
|
|
194
|
+
* list, so that browser discards only the `:has` branch (the `:is()` then
|
|
195
|
+
* matches nothing) and the `body` stamp still docks the header after
|
|
196
|
+
* hydration.
|
|
197
|
+
*
|
|
198
|
+
* A DESCENDANT MATCH, not `:has(> …)`: the chrome element sits inside the
|
|
199
|
+
* layout and the chrome slot, never directly under `body`. Emission is not a
|
|
200
|
+
* reason either way — pz-theme writes generated CSS through
|
|
201
|
+
* `dangerouslySetInnerHTML`, verbatim, which the quotes in both expressions
|
|
202
|
+
* already depend on — so the choice is about what matches.
|
|
174
203
|
*/
|
|
175
|
-
docked: [
|
|
204
|
+
docked: [
|
|
205
|
+
'body[data-chrome-mode~="docked"] &',
|
|
206
|
+
':is(body:has([data-chrome-mode~="docked"])) &'
|
|
207
|
+
]
|
|
176
208
|
};
|
|
177
209
|
|
|
178
210
|
export const PART_STYLE_STATES = Object.keys(STATE_SELECTORS);
|