@akinon/pz-theme 2.0.92-beta.0 → 2.0.92
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
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.92
|
|
3
|
+
## 2.0.92
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- @akinon/next@2.0.92
|
|
9
|
-
- @akinon/theme-contract@2.0.92
|
|
7
|
+
- abd46ed1: ZERO-4997: Let an open overlay outrank the scrolled state
|
|
8
|
+
- @akinon/next@2.0.92
|
|
9
|
+
- @akinon/theme-contract@2.0.92
|
|
10
10
|
|
|
11
11
|
## 2.0.91
|
|
12
12
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.92
|
|
4
|
+
"version": "2.0.92",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.92
|
|
11
|
+
"@akinon/next": "2.0.92",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.92
|
|
16
|
+
"@akinon/theme-contract": "2.0.92",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"tailwind-merge": "^2.5.4",
|
|
20
20
|
"zod": "4.4.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@akinon/next": "2.0.92
|
|
23
|
+
"@akinon/next": "2.0.92",
|
|
24
24
|
"@testing-library/jest-dom": "5.16.4",
|
|
25
25
|
"@testing-library/react": "16.3.2",
|
|
26
26
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -24,12 +24,20 @@ import type { Section } from '../../theme-section';
|
|
|
24
24
|
* ON AN INNER PAGE BOTH STATES MATCH AT ONCE. The shopper scrolls a docked
|
|
25
25
|
* header, so `[data-scroll-state~="scrolled"]` and `body[data-chrome-mode~=
|
|
26
26
|
* "docked"]` are both true of the same band, and both rules set the same
|
|
27
|
-
* property.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the
|
|
27
|
+
* property. Unqualified they would both weigh (0,2,0) against an element
|
|
28
|
+
* selector; the `body` makes this one (0,2,1), so the mode wins on specificity
|
|
29
|
+
* rather than on whichever rule the generator happened to write last. That is
|
|
30
|
+
* the right answer and not merely a chosen one: `scrolled` describes how far
|
|
31
|
+
* the page has slid under a bar that floats over it, and once the bar is docked
|
|
32
|
+
* the question has no meaning.
|
|
33
|
+
*
|
|
34
|
+
* `overlay-open` CARRIES THE SAME QUALIFIER, for the same reason and against
|
|
35
|
+
* the same opponent (ZERO-4997; see scrolled-state-styles.test.ts). So the
|
|
36
|
+
* ladder reads: `scrolled` at (0,2,0) on the bottom, unable to leave it because
|
|
37
|
+
* its attribute is not on `body`, and these two above it at (0,2,1) — TIED with
|
|
38
|
+
* each other, which means a docked header with an open panel is still settled
|
|
39
|
+
* by emission order. Nothing here asserts that pair; there is no right answer
|
|
40
|
+
* to assert yet.
|
|
33
41
|
*
|
|
34
42
|
* WHY THERE IS NO `getComputedStyle` TEST BELOW, and why adding one would make
|
|
35
43
|
* this file worse: jsdom has no cascade worth the name. Its `getComputedStyle`
|
|
@@ -89,7 +97,7 @@ afterEach(() => {
|
|
|
89
97
|
});
|
|
90
98
|
|
|
91
99
|
describe('the `body` qualifier is load-bearing — do not tidy it away', () => {
|
|
92
|
-
it('keeps `body` in the docked selector, which is the only thing
|
|
100
|
+
it('keeps `body` in the docked selector, which is the only thing outranking scrolled here', () => {
|
|
93
101
|
const [selector] = buildRootStateSelectors(SECTION, 'docked');
|
|
94
102
|
|
|
95
103
|
// Asserted as literal text, because the qualifier reads like a redundant
|
|
@@ -3,6 +3,7 @@ import { stripHidingStyles } from '../checkout-safety';
|
|
|
3
3
|
import {
|
|
4
4
|
buildPartSelectors,
|
|
5
5
|
buildRootStateKey,
|
|
6
|
+
buildRootStateSelectors,
|
|
6
7
|
parseRootStateKey,
|
|
7
8
|
splitPartStyles
|
|
8
9
|
} from '../part-styles';
|
|
@@ -210,7 +211,8 @@ describe('checkout safety', () => {
|
|
|
210
211
|
});
|
|
211
212
|
|
|
212
213
|
describe('the overlay-open state', () => {
|
|
213
|
-
|
|
214
|
+
// Carries `body` deliberately; the block at the end of this file is why.
|
|
215
|
+
const OPEN = 'body[data-overlay-state~="open"]';
|
|
214
216
|
|
|
215
217
|
it('conditions a part on the page, exactly as scrolled does', () => {
|
|
216
218
|
const [selector] = buildPartSelectors('[data-block-id="blk-1"]', {
|
|
@@ -259,3 +261,213 @@ describe('the overlay-open state', () => {
|
|
|
259
261
|
expect(document.querySelectorAll(selector)).toHaveLength(0);
|
|
260
262
|
});
|
|
261
263
|
});
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* `overlay-open` outranking `scrolled`, which is the whole point of the `body`
|
|
267
|
+
* in its selector.
|
|
268
|
+
*
|
|
269
|
+
* THE BUG IT CLOSES, reported from a live storefront: scroll the home page far
|
|
270
|
+
* enough that the header takes its scrolled paint, THEN open the mega menu, and
|
|
271
|
+
* the band stays scroll-dark. Open the menu without scrolling first and it
|
|
272
|
+
* turns white. Both states are true of the same band at the same moment and
|
|
273
|
+
* both set the same property, so one has to lose — and unqualified both weigh
|
|
274
|
+
* (0,2,0), which left the winner to whichever rule the generator wrote last.
|
|
275
|
+
* That order is not a decision made in this package: root states are bucketed
|
|
276
|
+
* as `splitPartStyles` iterates the style object, so it is the order the state
|
|
277
|
+
* keys happen to sit in the merchant's stored composition JSON.
|
|
278
|
+
*
|
|
279
|
+
* WHAT IS ASSERTED, THEREFORE, IS THE RELATIONSHIP AND NOT THE STRING: that the
|
|
280
|
+
* overlay colour is the one a browser would paint, with the keys stored in BOTH
|
|
281
|
+
* orders. A test that only checked the selector text would go green again the
|
|
282
|
+
* moment someone re-derived the same tie a different way, and a test that only
|
|
283
|
+
* used the failing order could not tell a fix from a lucky emission.
|
|
284
|
+
*
|
|
285
|
+
* WHY NOT `getComputedStyle`: jsdom has no cascade worth the name — it resolves
|
|
286
|
+
* competing rules by source order alone and ignores specificity entirely (this
|
|
287
|
+
* was measured; see docked-state-styles.test.ts). A cascade test here would
|
|
288
|
+
* assert the opposite of a browser, pass today for the wrong reason, and keep
|
|
289
|
+
* passing after someone deleted the qualifier it exists to protect.
|
|
290
|
+
*/
|
|
291
|
+
describe('an open overlay outranks the scrolled state', () => {
|
|
292
|
+
const SECTION = '[data-section-id="sec-1"]';
|
|
293
|
+
const OPEN_WHITE = 'rgba(255, 255, 255, 1)';
|
|
294
|
+
const SCROLLED_GREY = 'rgba(245, 245, 245, 1)';
|
|
295
|
+
|
|
296
|
+
// Read back out of the table rather than retyped. Only the first case below
|
|
297
|
+
// asserts the selector as text; everything after it is about the relationship
|
|
298
|
+
// between the two, and retyping either one would quietly turn those cases
|
|
299
|
+
// into assertions about this file instead of about the table.
|
|
300
|
+
const overlaySelector = () =>
|
|
301
|
+
buildRootStateSelectors(SECTION, 'overlay-open')[0];
|
|
302
|
+
const scrolledSelector = () =>
|
|
303
|
+
buildRootStateSelectors(SECTION, 'scrolled')[0];
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Weighs the two selectors this block is about, and nothing else.
|
|
307
|
+
*
|
|
308
|
+
* Naive on purpose — it counts attribute selectors and leading type
|
|
309
|
+
* selectors, which is all either selector contains. A general specificity
|
|
310
|
+
* parser would be a second implementation to get wrong, and the honest
|
|
311
|
+
* alternative, asking the DOM, does not exist here.
|
|
312
|
+
*/
|
|
313
|
+
const weigh = (selector: string) => ({
|
|
314
|
+
attributes: (selector.match(/\[/g) || []).length,
|
|
315
|
+
types: (selector.match(/(^|[\s>+~])[a-z][a-z0-9-]*/g) || []).length
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const declaredBackground = (css: string, selector: string) => {
|
|
319
|
+
const at = css.indexOf(`${selector} {`);
|
|
320
|
+
if (at === -1) return null;
|
|
321
|
+
|
|
322
|
+
const rule = css.slice(at, css.indexOf('}', at));
|
|
323
|
+
const declaration = rule.match(/background-color: ([^;]+);/);
|
|
324
|
+
|
|
325
|
+
return declaration ? declaration[1] : null;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Which of the two competing declarations a browser would actually paint.
|
|
330
|
+
*
|
|
331
|
+
* A mini-cascade, and honest about being one: the heavier selector wins, and
|
|
332
|
+
* a TIE falls to whichever rule was written last. Modelling the tie-break is
|
|
333
|
+
* not decoration — without it, a build where the `body` had been deleted
|
|
334
|
+
* would still come out white in the overlay-last order below, and the pair of
|
|
335
|
+
* cases would stop proving order-independence.
|
|
336
|
+
*/
|
|
337
|
+
const painted = (css: string) => {
|
|
338
|
+
const overlay = overlaySelector();
|
|
339
|
+
const scrolled = scrolledSelector();
|
|
340
|
+
|
|
341
|
+
if (weigh(overlay).types !== weigh(scrolled).types) {
|
|
342
|
+
return declaredBackground(
|
|
343
|
+
css,
|
|
344
|
+
weigh(overlay).types > weigh(scrolled).types ? overlay : scrolled
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return declaredBackground(
|
|
349
|
+
css,
|
|
350
|
+
css.indexOf(`${overlay} {`) > css.indexOf(`${scrolled} {`)
|
|
351
|
+
? overlay
|
|
352
|
+
: scrolled
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const band = (styles: Record<string, unknown>) =>
|
|
357
|
+
generateThemeCSS([section('sec-1', styles)]);
|
|
358
|
+
|
|
359
|
+
it('keeps `body` in the overlay selector, which is the entire tie-break', () => {
|
|
360
|
+
// The one literal assertion here, because the qualifier reads like a
|
|
361
|
+
// redundant ancestor to anyone who has not read why it is there.
|
|
362
|
+
expect(overlaySelector()).toBe(
|
|
363
|
+
`body[data-overlay-state~="open"] ${SECTION}`
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('outweighs the scrolled selector, which it ties with on attributes alone', () => {
|
|
368
|
+
// Same attribute count: this is the tie, and it is why order would decide.
|
|
369
|
+
expect(weigh(overlaySelector()).attributes).toBe(
|
|
370
|
+
weigh(scrolledSelector()).attributes
|
|
371
|
+
);
|
|
372
|
+
// The type selector is the entire margin of victory.
|
|
373
|
+
expect(weigh(overlaySelector()).types).toBeGreaterThan(
|
|
374
|
+
weigh(scrolledSelector()).types
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('paints the overlay colour with the SCROLLED key stored last', () => {
|
|
379
|
+
// The order that failed in production: scrolled is emitted after overlay,
|
|
380
|
+
// so nothing about the emission is saving this — only the weight is.
|
|
381
|
+
const css = band({
|
|
382
|
+
'background-color': 'transparent',
|
|
383
|
+
':overlay-open/background-color': '#ffffff',
|
|
384
|
+
':scrolled/background-color': '#f5f5f5'
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(css.indexOf(`${scrolledSelector()} {`)).toBeGreaterThan(
|
|
388
|
+
css.indexOf(`${overlaySelector()} {`)
|
|
389
|
+
);
|
|
390
|
+
expect(painted(css)).toBe(OPEN_WHITE);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('paints the overlay colour with the OVERLAY key stored last too', () => {
|
|
394
|
+
// The mirror case, which a merchant reaches simply by editing the two
|
|
395
|
+
// controls in the other order. Same answer — which is what makes the pair
|
|
396
|
+
// proof rather than luck. On a build with the `body` deleted THIS case
|
|
397
|
+
// still comes out white, because emission order happens to favour the
|
|
398
|
+
// overlay here; only the case above turns red. Keep both, or the next
|
|
399
|
+
// reader has no way to tell a fix from a fortunate order.
|
|
400
|
+
const css = band({
|
|
401
|
+
'background-color': 'transparent',
|
|
402
|
+
':scrolled/background-color': '#f5f5f5',
|
|
403
|
+
':overlay-open/background-color': '#ffffff'
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(css.indexOf(`${overlaySelector()} {`)).toBeGreaterThan(
|
|
407
|
+
css.indexOf(`${scrolledSelector()} {`)
|
|
408
|
+
);
|
|
409
|
+
expect(painted(css)).toBe(OPEN_WHITE);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('still emits the scrolled rule, which the shopper sees before opening', () => {
|
|
413
|
+
// The overlay only outranks it WHILE a panel is open. Losing the scrolled
|
|
414
|
+
// rule entirely would be the opposite bug, and just as visible.
|
|
415
|
+
const css = band({
|
|
416
|
+
':scrolled/background-color': '#f5f5f5',
|
|
417
|
+
':overlay-open/background-color': '#ffffff'
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(declaredBackground(css, scrolledSelector())).toBe(SCROLLED_GREY);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('matches the same band as scrolled at once, so the tie is real', () => {
|
|
424
|
+
// Not hypothetical: this is the reported sequence — the page is scrolled
|
|
425
|
+
// and the panel is open, so both rules target one element and one property.
|
|
426
|
+
document.body.setAttribute('data-overlay-state', 'open');
|
|
427
|
+
document.body.innerHTML =
|
|
428
|
+
'<div data-chrome-slot="header" data-scroll-state="scrolled">' +
|
|
429
|
+
'<div data-section-id="sec-1"></div>' +
|
|
430
|
+
'</div>';
|
|
431
|
+
|
|
432
|
+
expect(document.querySelectorAll(overlaySelector())).toHaveLength(1);
|
|
433
|
+
expect(document.querySelectorAll(scrolledSelector())).toHaveLength(1);
|
|
434
|
+
|
|
435
|
+
document.body.removeAttribute('data-overlay-state');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('matches nothing when no panel is open, so scrolled keeps the band', () => {
|
|
439
|
+
// Every composition that styled only `scrolled` has to render as it did.
|
|
440
|
+
document.body.removeAttribute('data-overlay-state');
|
|
441
|
+
document.body.innerHTML = '<div data-section-id="sec-1"></div>';
|
|
442
|
+
|
|
443
|
+
expect(document.querySelectorAll(overlaySelector())).toHaveLength(0);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* THE OTHER HALF OF THE BUMP, pinned because it is a bargain rather than a
|
|
448
|
+
* free win. A page state already composes an ancestor selector on top of
|
|
449
|
+
* whatever an element-matched state weighs, so `overlay-open` and `hover` on
|
|
450
|
+
* one part used to TIE — order-dependent, i.e. a coin-flip. The qualifier
|
|
451
|
+
* settles it in the page state's favour, which is the same bargain `docked`
|
|
452
|
+
* has always made. The visible cost is a header link that stops answering the
|
|
453
|
+
* pointer while the panel is open, and it is asserted here so nobody
|
|
454
|
+
* rediscovers it as a bug.
|
|
455
|
+
*/
|
|
456
|
+
it('outranks an element-matched state on the same part', () => {
|
|
457
|
+
const partSelector = (state: string) =>
|
|
458
|
+
buildPartSelectors('[data-block-id="blk-1"]', {
|
|
459
|
+
part: 'navLink',
|
|
460
|
+
state,
|
|
461
|
+
styles: {}
|
|
462
|
+
})[0];
|
|
463
|
+
|
|
464
|
+
const overlayPart = partSelector('overlay-open');
|
|
465
|
+
const hoverPart = partSelector('hover');
|
|
466
|
+
|
|
467
|
+
// The page state carries one ancestor selector the element state does not,
|
|
468
|
+
// and the `body` qualifier adds the type selector on top of it.
|
|
469
|
+
expect(overlayPart).toContain('body[data-overlay-state~="open"]');
|
|
470
|
+
expect(hoverPart).not.toContain('body[');
|
|
471
|
+
expect(hoverPart).toContain(':hover');
|
|
472
|
+
});
|
|
473
|
+
});
|
package/src/utils/part-styles.ts
CHANGED
|
@@ -80,26 +80,81 @@ const STATE_SELECTORS: Record<string, string[]> = {
|
|
|
80
80
|
// through an ancestor and no renderer has to stamp anything for them to
|
|
81
81
|
// apply. That is what lets a header become opaque on scroll, which nothing in
|
|
82
82
|
// a composition could previously say.
|
|
83
|
+
//
|
|
84
|
+
// THIS ONE IS THE BOTTOM RUNG of the page-state ladder, at (0,2,0), and it is
|
|
85
|
+
// left there on purpose. It COULD be lifted today — `chrome/scroll-state.tsx`
|
|
86
|
+
// only ever stamps the attribute on `div` elements, so `div[…] &` would be
|
|
87
|
+
// accurate this afternoon. It is not done because that pins the chrome slot's
|
|
88
|
+
// TAG NAME, which the slot is free to change and which nothing would fail
|
|
89
|
+
// loudly about; `body` is guaranteed by the document. Both states below
|
|
90
|
+
// deliberately outrank it — read them for why that is the right way round and
|
|
91
|
+
// not an accident.
|
|
83
92
|
scrolled: ['[data-scroll-state~="scrolled"] &'],
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The same trick for the other half of that header: transparent over the
|
|
95
|
+
* hero, opaque while its menu panel is open. The panel cannot express it —
|
|
96
|
+
* the header is not inside the panel — and the header cannot know it without
|
|
97
|
+
* the page saying so.
|
|
98
|
+
*
|
|
99
|
+
* THE `body` QUALIFIER IS A DELIBERATE SPECIFICITY BUMP, not tidiness — the
|
|
100
|
+
* same bump `docked` carries below, added for the same reason. A shopper who
|
|
101
|
+
* scrolls the home page and THEN opens the mega menu satisfies `scrolled` and
|
|
102
|
+
* `overlay-open` at once, on the same band, for the same property. Left
|
|
103
|
+
* unqualified both weigh (0,2,0), so the winner is whichever rule the
|
|
104
|
+
* generator emitted last — and that order is nobody's decision: root states
|
|
105
|
+
* are bucketed in iteration order, so it is the order the state keys happen
|
|
106
|
+
* to sit in the merchant's stored composition JSON. Reported as a header that
|
|
107
|
+
* stays scroll-dark behind an open menu, but only if you scrolled first.
|
|
108
|
+
* `body` makes this (0,2,1), which beats `scrolled` whatever that order is.
|
|
109
|
+
* The qualifier is accurate rather than hopeful: `hooks/use-overlay.ts` sets
|
|
110
|
+
* the attribute on `document.body`. And an open panel outranking a scroll
|
|
111
|
+
* position is the correct answer rather than an arbitrary one — the panel is
|
|
112
|
+
* the thing in front of the shopper, so it decides how the bar it hangs from
|
|
113
|
+
* is painted.
|
|
114
|
+
*
|
|
115
|
+
* IT ALSO OUTRANKS THE ELEMENT-MATCHED STATES on the same element — `hover`,
|
|
116
|
+
* `focus`, `selected` and the rest — because a page state composes an extra
|
|
117
|
+
* ancestor selector on top of whatever they weigh, and the qualifier adds one
|
|
118
|
+
* more rung to that. Unqualified it TIED with them, so this replaces a
|
|
119
|
+
* coin-flip with a rule rather than taking anything away that was reliable.
|
|
120
|
+
* The visible consequence: a header link that carries both an `overlay-open`
|
|
121
|
+
* colour and a `hover` colour keeps the overlay colour while the panel is
|
|
122
|
+
* open, and stops answering the pointer until it closes. That is the same
|
|
123
|
+
* bargain `docked` has always made, which is why it is spelled the same way
|
|
124
|
+
* — but it is a bargain, so an author who wants hover to survive an open
|
|
125
|
+
* panel must not author the property in both places.
|
|
126
|
+
*/
|
|
127
|
+
'overlay-open': ['body[data-overlay-state~="open"] &'],
|
|
89
128
|
/**
|
|
90
129
|
* The third page-level state: the chrome is DOCKED above the page rather than
|
|
91
130
|
* sitting over it. `body` carries `data-chrome-mode` (see
|
|
92
131
|
* chrome/chrome-mode.ts), stamped by the brand, because only the brand knows
|
|
93
132
|
* which of its routes carry a hero.
|
|
94
133
|
*
|
|
95
|
-
* THE `body` QUALIFIER IS A DELIBERATE SPECIFICITY BUMP, not tidiness.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
134
|
+
* THE `body` QUALIFIER IS A DELIBERATE SPECIFICITY BUMP, not tidiness. On an
|
|
135
|
+
* inner page this and `scrolled` BOTH match at once, and unqualified they
|
|
136
|
+
* would tie at (0,2,0), leaving the winner to whichever declaration the
|
|
137
|
+
* generator happened to emit last. Adding the element name makes this
|
|
138
|
+
* (0,2,1), so the mode wins. That is the correct answer rather than an
|
|
139
|
+
* arbitrary one: `scrolled` describes how far the page has moved under a bar
|
|
140
|
+
* that floats over it, and once the bar is docked that question no longer has
|
|
141
|
+
* a meaning.
|
|
142
|
+
*
|
|
143
|
+
* WHAT THIS LADDER STILL DOES NOT SETTLE: `docked` and `overlay-open` now
|
|
144
|
+
* weigh the same, so THEY tie with each other, and a docked header with an
|
|
145
|
+
* open panel is decided by emission order again — the composition's key
|
|
146
|
+
* order, exactly the coin-flip the qualifiers were added to remove. That is
|
|
147
|
+
* unexposed rather than fixed. In the brand this was traced from, both states
|
|
148
|
+
* paint the same background and the same text colour, so the tie lands on the
|
|
149
|
+
* same pixels either way; a brand that wants its docked header to look
|
|
150
|
+
* different with the menu open will meet it. A rung IS reachable without a
|
|
151
|
+
* new attribute — repeating an attribute selector
|
|
152
|
+
* (`body[data-overlay-state~="open"][data-overlay-state]`) weighs (0,3,1) —
|
|
153
|
+
* and it is left unspent deliberately: which of the two should win is a
|
|
154
|
+
* product question nobody has been asked, and spending the rung guesses at
|
|
155
|
+
* the answer. Whoever asks it should take that rung rather than dropping one
|
|
156
|
+
* of these two qualifiers, which only hands `scrolled` back a fight it should
|
|
157
|
+
* lose.
|
|
103
158
|
*/
|
|
104
159
|
docked: ['body[data-chrome-mode~="docked"] &']
|
|
105
160
|
};
|