@react-x11/components 0.12.0 → 0.13.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.
Files changed (80) hide show
  1. package/dist/html/controls.d.ts +12 -3
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +15 -55
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +134 -20
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +519 -122
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/parse.d.ts +29 -2
  10. package/dist/html/css/parse.d.ts.map +1 -1
  11. package/dist/html/css/parse.js +277 -9
  12. package/dist/html/css/parse.js.map +1 -1
  13. package/dist/html/css/style.d.ts +14 -0
  14. package/dist/html/css/style.d.ts.map +1 -1
  15. package/dist/html/css/style.js +2 -3
  16. package/dist/html/css/style.js.map +1 -1
  17. package/dist/html/css/ua.d.ts +11 -1
  18. package/dist/html/css/ua.d.ts.map +1 -1
  19. package/dist/html/css/ua.js +59 -7
  20. package/dist/html/css/ua.js.map +1 -1
  21. package/dist/html/dom.d.ts.map +1 -1
  22. package/dist/html/dom.js +3 -1
  23. package/dist/html/dom.js.map +1 -1
  24. package/dist/html/fonts.d.ts +25 -1
  25. package/dist/html/fonts.d.ts.map +1 -1
  26. package/dist/html/fonts.js +99 -6
  27. package/dist/html/fonts.js.map +1 -1
  28. package/dist/html/form.d.ts +200 -0
  29. package/dist/html/form.d.ts.map +1 -0
  30. package/dist/html/form.js +801 -0
  31. package/dist/html/form.js.map +1 -0
  32. package/dist/html/index.d.ts +12 -0
  33. package/dist/html/index.d.ts.map +1 -1
  34. package/dist/html/index.js +30 -301
  35. package/dist/html/index.js.map +1 -1
  36. package/dist/html/layout/block.d.ts.map +1 -1
  37. package/dist/html/layout/block.js +5 -1
  38. package/dist/html/layout/block.js.map +1 -1
  39. package/dist/html/layout/boxes.d.ts +31 -1
  40. package/dist/html/layout/boxes.d.ts.map +1 -1
  41. package/dist/html/layout/boxes.js +52 -8
  42. package/dist/html/layout/boxes.js.map +1 -1
  43. package/dist/html/layout/flex.js +4 -1
  44. package/dist/html/layout/flex.js.map +1 -1
  45. package/dist/html/layout/inline.d.ts.map +1 -1
  46. package/dist/html/layout/inline.js +10 -0
  47. package/dist/html/layout/inline.js.map +1 -1
  48. package/dist/html/node.d.ts +87 -15
  49. package/dist/html/node.d.ts.map +1 -1
  50. package/dist/html/node.js +510 -152
  51. package/dist/html/node.js.map +1 -1
  52. package/dist/html/paint.d.ts +22 -1
  53. package/dist/html/paint.d.ts.map +1 -1
  54. package/dist/html/paint.js +290 -20
  55. package/dist/html/paint.js.map +1 -1
  56. package/dist/html/widgets.d.ts +29 -0
  57. package/dist/html/widgets.d.ts.map +1 -0
  58. package/dist/html/widgets.js +620 -0
  59. package/dist/html/widgets.js.map +1 -0
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js.map +1 -1
  63. package/package.json +2 -2
  64. package/src/html/controls.ts +29 -49
  65. package/src/html/css/cascade.ts +591 -140
  66. package/src/html/css/parse.ts +324 -9
  67. package/src/html/css/style.ts +24 -3
  68. package/src/html/css/ua.ts +62 -8
  69. package/src/html/dom.ts +3 -1
  70. package/src/html/fonts.ts +116 -6
  71. package/src/html/form.ts +962 -0
  72. package/src/html/index.ts +49 -338
  73. package/src/html/layout/block.ts +3 -0
  74. package/src/html/layout/boxes.ts +78 -11
  75. package/src/html/layout/flex.ts +4 -1
  76. package/src/html/layout/inline.ts +10 -0
  77. package/src/html/node.ts +498 -135
  78. package/src/html/paint.ts +330 -29
  79. package/src/html/widgets.ts +735 -0
  80. package/src/index.ts +1 -0
@@ -48,6 +48,7 @@ import type { FontFamilies } from '../fonts.js';
48
48
  import { parseDeclarations } from './parse.js';
49
49
  import { CounterStyles, counterStyleRule } from './counter-styles.js';
50
50
  import type { CounterStyleRule } from './counter-styles.js';
51
+ import { PALETTE_CHROME } from './ua.js';
51
52
  import type { UnitContext } from './values.js';
52
53
  import { viewportUnit } from './values.js';
53
54
  import { customProperties, substituteIn } from './vars.js';
@@ -93,6 +94,41 @@ export interface SharedStyle {
93
94
  key: number;
94
95
  }
95
96
 
97
+ /**
98
+ * The styles of the build before, for a build that only some elements'
99
+ * styles can differ in (`Cascade.beginSharing`): what each element's came
100
+ * to, and the elements to style again — `restyle`, each with everything in
101
+ * it, since a style is its parent's and the rules', and a rule reads an
102
+ * element's ancestors.
103
+ */
104
+ export interface KeptStyles {
105
+ styles: ReadonlyMap<Element, { style: ComputedStyle; inFlex: boolean }>;
106
+ restyle: ReadonlySet<Element>;
107
+ }
108
+
109
+ /** What the pointer entering or leaving an element can change
110
+ * (`Cascade.hoverTouches`): nothing, the element and what is in it, or
111
+ * those and the siblings after it. */
112
+ export type HoverTouch = 0 | 1 | 2;
113
+ export const HOVER_UNTOUCHED = 0;
114
+ export const HOVER_TOUCHED = 1;
115
+ export const HOVER_FOLLOWED = 2;
116
+
117
+ /** Which pseudo-elements' rules test the pointer (`Cascade.hoverPseudo`). */
118
+ export const HOVER_PSEUDO_NONE = 0;
119
+ export const HOVER_PSEUDO_GENERATED = 1;
120
+ export const HOVER_PSEUDO_OTHER = 2;
121
+
122
+ /** The pseudo-elements a pointer move can restyle: `::selection`'s style
123
+ * is worked out as a selection is painted. */
124
+ const POINTER_PSEUDO_ELEMENTS = [
125
+ 'before',
126
+ 'after',
127
+ 'first-letter',
128
+ 'first-line',
129
+ 'marker',
130
+ ] as const;
131
+
96
132
  /** A compiled matcher, kept beside the rule it came from. */
97
133
  interface IndexedRule {
98
134
  rule: StyleRule;
@@ -132,6 +168,11 @@ class RuleIndex {
132
168
  /** Whether any rule in here is pointer-sensitive, so the renderer knows
133
169
  * whether a pointer move can change the cascade at all. */
134
170
  hoverSensitive = false;
171
+ /** The rules that test `:hover`, in buckets of their own: the ones whose
172
+ * answers a pointer move can change, which is all that is asked to know
173
+ * whether an element's did (`Cascade.pointerChanged`). Null until there
174
+ * is one. */
175
+ pointer: RuleIndex | null = null;
135
176
  /** The buckets holding a rule an element cannot share its style under
136
177
  * (`UNSHAREABLE`): the ids, classes and tags whose elements compute
137
178
  * their own, and whether the universal bucket makes every element do so. */
@@ -185,26 +226,44 @@ class RuleIndex {
185
226
  else if (key.kind === 'tag') this.ownStyleTags.add(key.name);
186
227
  else this.ownStyleEverywhere = true;
187
228
  }
188
- const bucket =
189
- key.kind === 'id'
190
- ? mapBucket(this.byId, key.name)
191
- : key.kind === 'class'
192
- ? mapBucket(this.byClass, key.name)
193
- : key.kind === 'tag'
194
- ? mapBucket(this.byTag, key.name)
195
- : this.universal;
196
- bucket.push(indexed);
229
+ this._bucket(key).push(indexed);
230
+ if (HOVER.test(rule.selector)) {
231
+ this.pointer ??= new RuleIndex();
232
+ this.pointer.size += 1;
233
+ this.pointer._bucket(key).push(indexed);
234
+ }
235
+ }
236
+
237
+ private _bucket(key: ReturnType<typeof rightmostKey>): IndexedRule[] {
238
+ return key.kind === 'id'
239
+ ? mapBucket(this.byId, key.name)
240
+ : key.kind === 'class'
241
+ ? mapBucket(this.byClass, key.name)
242
+ : key.kind === 'tag'
243
+ ? mapBucket(this.byTag, key.name)
244
+ : this.universal;
197
245
  }
198
246
  }
199
247
 
200
248
  /** The pseudo-elements a rule can style here. */
201
249
  type PseudoElement =
202
- 'before' | 'after' | 'first-letter' | 'first-line' | 'marker';
250
+ 'before' | 'after' | 'first-letter' | 'first-line' | 'marker' | 'selection';
203
251
 
204
252
  /** A selector's trailing `::before`, `::after`, `::first-letter`,
205
- * `::first-line` or `::marker`, or CSS 2's single-colon spelling of any of
206
- * its four. */
207
- const PSEUDO_ELEMENT = /::?(before|after|first-letter|first-line)$|::marker$/i;
253
+ * `::first-line`, `::marker` or `::selection`, or CSS 2's single-colon
254
+ * spelling of any of its four. */
255
+ const PSEUDO_ELEMENT =
256
+ /::?(before|after|first-letter|first-line)$|::(marker|selection)$/i;
257
+
258
+ /**
259
+ * What a `::selection` makes of the text it covers (CSS Pseudo 4, 3.2): the
260
+ * colour the text is drawn in, null for its own, and the band under it,
261
+ * null for none.
262
+ */
263
+ export interface SelectionStyle {
264
+ color: string | null;
265
+ background: string | null;
266
+ }
208
267
 
209
268
  /**
210
269
  * A rule for a pseudo-element, as the pseudo-element it styles and a rule
@@ -226,7 +285,7 @@ function splitPseudoElement(
226
285
  ? `${trimmed} *`
227
286
  : trimmed;
228
287
  return {
229
- which: (m[1] ?? 'marker').toLowerCase() as PseudoElement,
288
+ which: (m[1] ?? m[2]).toLowerCase() as PseudoElement,
230
289
  rule: { ...rule, selector },
231
290
  };
232
291
  }
@@ -234,34 +293,63 @@ function splitPseudoElement(
234
293
  /**
235
294
  * The compounds of a selector that test `:hover`, each without it (and
236
295
  * without an `:active` beside it), and whether one is followed by a sibling
237
- * combinator, which reaches the element's later siblings. `nested` where a
238
- * `:hover` sits inside a functional pseudo-class — `:not(:hover)`,
239
- * `:is(:hover)` — and reaches elements no compound names.
296
+ * combinator, which reaches the element's later siblings.
297
+ *
298
+ * A `:hover` inside `:is()`, `:where()` or `:not()` is found where it is:
299
+ * each takes a selector list and matches the element it is written on, so
300
+ * a list entry of one compound tests that element — `a:not(:hover)` is a
301
+ * compound of `a`'s that tests the pointer — and an entry with combinators
302
+ * in it names an ancestor or an earlier sibling, as it would outside.
303
+ * Tailwind 4 writes every `group-hover:` that way, `:is(:where(.group):hover
304
+ * *)`, and a page of them built its document again on every move. Whatever
305
+ * the nesting, the element whose hover changed is one a compound found here
306
+ * matches, and what changes with it is itself, what is in it, and what
307
+ * follows it where a sibling combinator is in play: a selector reads an
308
+ * element, its ancestors and the siblings before each of them, and nothing
309
+ * else. `nested` is left for a function this cannot read a selector list
310
+ * in — `:nth-child(… of :hover)`, `:host()` — which reaches elements no
311
+ * compound names.
240
312
  *
241
- * A `:hover` in a compound's `:has()` is the one exception, since what it
242
- * reaches is known: the compound, without its `:has()`, is an ancestor of
313
+ * A `:hover` in a compound's `:has()` is the other way about, since what
314
+ * it tests is below: the compound, without its `:has()`, is an ancestor of
243
315
  * the element the pointer is over — or an earlier sibling of it or of an
244
316
  * ancestor, where the argument starts at a sibling (Selectors 4, 4.5). So
245
- * it is given as an anchor (`has`) rather than making the rule `nested`.
317
+ * it is given as an anchor (`has`).
246
318
  *
247
319
  * `:active` is not the pointer's here: nothing sets it (`setPointer` is
248
320
  * handed none), so a selector that tests only it never changes as the
249
321
  * pointer moves — Wikipedia's buttons' `:focus:not(:active)` among them.
250
322
  * A press that sets it would have to be counted here too.
251
323
  */
252
- export function pointerCompounds(selector: string): {
324
+ export function pointerCompounds(selector: string): PointerCompounds {
325
+ const out: PointerCompounds = {
326
+ compounds: [],
327
+ siblings: false,
328
+ nested: false,
329
+ has: [],
330
+ followed: [],
331
+ };
332
+ scanComplex(selector, out);
333
+ return out;
334
+ }
335
+
336
+ export interface PointerCompounds {
253
337
  compounds: string[];
254
338
  siblings: boolean;
255
339
  nested: boolean;
256
340
  has: { anchor: string; siblings: boolean }[];
257
- } {
258
- const out = {
259
- compounds: [] as string[],
260
- siblings: false,
261
- nested: false,
262
- has: [] as { anchor: string; siblings: boolean }[],
263
- };
264
- // the compounds at the top level, each with the combinator after it
341
+ /** The compounds and anchors a sibling combinator follows: the ones
342
+ * whose elements' later siblings change with them. */
343
+ followed: string[];
344
+ }
345
+
346
+ /** The functions that take a selector list and match the element they are
347
+ * written on (Selectors 4, 4.2 to 4.4), under the names css-select and
348
+ * older sheets know them by. */
349
+ const MATCHES_ANY = /^(?:is|where|not|matches|any|-webkit-any|-moz-any)$/i;
350
+
351
+ /** The compounds of a complex selector, each with the combinator after it. */
352
+ function compoundsOf(selector: string): { text: string; next: string }[] {
265
353
  const parts: { text: string; next: string }[] = [];
266
354
  let start = 0;
267
355
  let depth = 0;
@@ -287,85 +375,188 @@ export function pointerCompounds(selector: string): {
287
375
  }
288
376
  }
289
377
  split(selector.length, '');
290
- for (const { text, next } of parts) {
291
- let bare = '';
292
- let pointer = false;
293
- depth = 0;
294
- quote = '';
295
- /** Where a top-level `:has(` starts in `bare`, and its argument in
296
- * `text`, while one is open. */
297
- let hasAt = -1;
298
- let argAt = -1;
299
- let hasPointer = false;
300
- let anchored = false;
301
- const args: string[] = [];
302
- for (let i = 0; i < text.length; i += 1) {
303
- const c = text[i];
304
- if (quote) {
305
- if (c === quote && text[i - 1] !== '\\') quote = '';
306
- bare += c;
307
- continue;
378
+ return parts;
379
+ }
380
+
381
+ /** A selector list's entries: split at its top-level commas. */
382
+ function selectorsOf(list: string): string[] {
383
+ const entries: string[] = [];
384
+ let start = 0;
385
+ let depth = 0;
386
+ let quote = '';
387
+ for (let i = 0; i < list.length; i += 1) {
388
+ const c = list[i];
389
+ if (quote) {
390
+ if (c === quote && list[i - 1] !== '\\') quote = '';
391
+ } else if (c === '\\') i = escapeEnd(list, i) - 1;
392
+ else if (c === '"' || c === "'") quote = c;
393
+ else if (c === '(' || c === '[') depth += 1;
394
+ else if (c === ')' || c === ']') depth = Math.max(0, depth - 1);
395
+ else if (c === ',' && depth === 0) {
396
+ entries.push(list.slice(start, i));
397
+ start = i + 1;
398
+ }
399
+ }
400
+ entries.push(list.slice(start));
401
+ return entries;
402
+ }
403
+
404
+ /** Where the parenthesis opened at `open` closes, or the text's end. */
405
+ function closeOf(text: string, open: number): number {
406
+ let depth = 0;
407
+ let quote = '';
408
+ for (let i = open; i < text.length; i += 1) {
409
+ const c = text[i];
410
+ if (quote) {
411
+ if (c === quote && text[i - 1] !== '\\') quote = '';
412
+ } else if (c === '\\') i = escapeEnd(text, i) - 1;
413
+ else if (c === '"' || c === "'") quote = c;
414
+ else if (c === '(' || c === '[') depth += 1;
415
+ else if (c === ')' || c === ']') {
416
+ depth -= 1;
417
+ if (depth === 0) return i;
418
+ }
419
+ }
420
+ return text.length;
421
+ }
422
+
423
+ /** `pointerCompounds` for one complex selector, into `out`. */
424
+ function scanComplex(selector: string, out: PointerCompounds): void {
425
+ for (const { text, next } of compoundsOf(selector)) {
426
+ const compoundsFrom = out.compounds.length;
427
+ const hasFrom = out.has.length;
428
+ const found = scanCompound(text, out);
429
+ const sibling = next === '+' || next === '~';
430
+ if (found.args.length) {
431
+ out.has.push({
432
+ anchor: found.bare.trim() || '*',
433
+ siblings: found.args.some((arg) =>
434
+ selectorsOf(arg).some((one) => /^\s*[+~]/.test(one)),
435
+ ),
436
+ });
437
+ }
438
+ if (found.pointer) out.compounds.push(found.bare.trim() || '*');
439
+ // what follows the element this compound matches follows one whose
440
+ // hover, or whose answer to a `:has()`, changed
441
+ // — the one this compound matches, or one a selector nested in it
442
+ // named, which is more than it reaches and never less
443
+ if (sibling && (found.pointer || found.below || found.args.length)) {
444
+ out.siblings = true;
445
+ for (let i = compoundsFrom; i < out.compounds.length; i += 1) {
446
+ out.followed.push(out.compounds[i]);
447
+ }
448
+ for (let i = hasFrom; i < out.has.length; i += 1) {
449
+ out.followed.push(out.has[i].anchor);
308
450
  }
309
- if (c === '\\') {
310
- const end = escapeEnd(text, i);
311
- bare += text.slice(i, end);
312
- i = end - 1;
451
+ }
452
+ }
453
+ }
454
+
455
+ /**
456
+ * What one compound says of the pointer: the compound without what tests
457
+ * it (`bare`), whether it tests the element's own hover (`pointer`), the
458
+ * arguments of each `:has()` of the element's that tests it below (`args`),
459
+ * and whether a selector nested in it named another element's (`below`),
460
+ * whose compounds went into `out`.
461
+ */
462
+ function scanCompound(
463
+ text: string,
464
+ out: PointerCompounds,
465
+ ): { bare: string; pointer: boolean; below: boolean; args: string[] } {
466
+ let bare = '';
467
+ let pointer = false;
468
+ let below = false;
469
+ const args: string[] = [];
470
+ let bracket = 0;
471
+ let quote = '';
472
+ for (let i = 0; i < text.length; i += 1) {
473
+ const c = text[i];
474
+ if (quote) {
475
+ if (c === quote && text[i - 1] !== '\\') quote = '';
476
+ bare += c;
477
+ continue;
478
+ }
479
+ if (c === '\\') {
480
+ const end = escapeEnd(text, i);
481
+ bare += text.slice(i, end);
482
+ i = end - 1;
483
+ continue;
484
+ }
485
+ if (c === '"' || c === "'") quote = c;
486
+ else if (c === '[') bracket += 1;
487
+ else if (c === ']') bracket = Math.max(0, bracket - 1);
488
+ else if (c === ':' && bracket === 0 && text[i + 1] !== ':') {
489
+ const m = POINTER_PSEUDO_AT.exec(text.slice(i));
490
+ if (m) {
491
+ if (m[0].length === 6) pointer = true;
492
+ i += m[0].length - 1;
313
493
  continue;
314
494
  }
315
- if (c === '"' || c === "'") quote = c;
316
- else if (c === '(' || c === '[') {
317
- if (depth === 0 && c === '(' && /:has$/i.test(bare)) {
318
- hasAt = bare.length - 4;
319
- argAt = i + 1;
320
- hasPointer = false;
321
- }
322
- depth += 1;
323
- } else if (c === ')' || c === ']') {
324
- depth = Math.max(0, depth - 1);
325
- if (depth === 0 && hasAt >= 0 && c === ')') {
326
- // the compound is kept without it: its argument is what the
327
- // pointer changes
328
- if (hasPointer) {
329
- anchored = true;
330
- args.push(text.slice(argAt, i));
331
- bare = bare.slice(0, hasAt);
332
- } else bare += c;
333
- hasAt = -1;
495
+ const fn = FUNCTION_AT.exec(text.slice(i));
496
+ if (fn) {
497
+ const open = i + fn[0].length - 1;
498
+ const close = closeOf(text, open);
499
+ const arg = text.slice(open + 1, close);
500
+ const whole = text.slice(i, close + 1);
501
+ i = close;
502
+ if (!HOVER.test(arg)) {
503
+ bare += whole;
334
504
  continue;
335
505
  }
336
- } else if (c === ':' && text[i + 1] !== ':') {
337
- const m = POINTER_PSEUDO_AT.exec(text.slice(i));
338
- if (m) {
339
- const hover = m[0].length === 6;
340
- if (depth > 0) {
341
- // whatever holds it in the argument, the element it tests is
342
- // in the anchor's subtree or a later sibling's
343
- if (hover && hasAt >= 0) hasPointer = true;
344
- else if (hover) out.nested = true;
345
- } else {
346
- if (hover) pointer = true;
347
- i += m[0].length - 1;
348
- continue;
506
+ const name = fn[1].toLowerCase();
507
+ if (name === 'has') {
508
+ // the compound is kept without it: its argument is what the
509
+ // pointer changes, and whatever holds the `:hover` in there, the
510
+ // element it tests is in the anchor's subtree or a later
511
+ // sibling's
512
+ args.push(arg);
513
+ } else if (MATCHES_ANY.test(name)) {
514
+ // Kept as what the entries that test this element's hover ask
515
+ // of it besides — `:where(a:hover)` is a compound of `a`'s, and
516
+ // so is `:not(a:hover)`, which only an `a` can change its answer
517
+ // to. Tailwind's typography writes its links `.prose
518
+ // :where(a:hover):not(:where(.not-prose, .not-prose *))`, and
519
+ // without the `a` that was a compound every element matched.
520
+ const own: string[] = [];
521
+ let any = false;
522
+ for (const entry of selectorsOf(arg)) {
523
+ if (!HOVER.test(entry)) continue;
524
+ const parts = compoundsOf(entry);
525
+ if (parts.length !== 1) {
526
+ scanComplex(entry, out);
527
+ below = true;
528
+ continue;
529
+ }
530
+ // of this element itself
531
+ const one = scanCompound(parts[0].text, out);
532
+ if (one.below) below = true;
533
+ if (!one.pointer && !one.args.length) continue;
534
+ if (one.pointer) pointer = true;
535
+ args.push(...one.args);
536
+ const rest = one.bare.trim();
537
+ if (rest && rest !== '*') own.push(rest);
538
+ else any = true;
349
539
  }
540
+ if (own.length && !any) bare += `:is(${own.join(',')})`;
541
+ } else {
542
+ out.nested = true;
543
+ bare += whole;
350
544
  }
545
+ continue;
351
546
  }
352
- bare += c;
353
- }
354
- if (anchored) {
355
- out.has.push({
356
- anchor: bare.trim() || '*',
357
- siblings: args.some((arg) =>
358
- arg.split(',').some((one) => /^\s*[+~]/.test(one)),
359
- ),
360
- });
361
547
  }
362
- if (!pointer) continue;
363
- out.compounds.push(bare.trim() || '*');
364
- if (next === '+' || next === '~') out.siblings = true;
548
+ bare += c;
365
549
  }
366
- return out;
550
+ return { bare, pointer, below, args };
367
551
  }
368
552
 
553
+ /** A functional pseudo-class's name and its opening parenthesis, at the
554
+ * start of a string. */
555
+ const FUNCTION_AT = /^:([\w-]+)\(/;
556
+
557
+ /** `:hover` anywhere in a selector. */
558
+ const HOVER = /:hover(?![\w-])/i;
559
+
369
560
  /** `:hover` or `:active` at the start of a string, and nothing longer. */
370
561
  const POINTER_PSEUDO_AT = /^:(?:hover|active)(?![\w-])/i;
371
562
 
@@ -569,26 +760,34 @@ export class Cascade {
569
760
  'first-letter': new RuleIndex(),
570
761
  'first-line': new RuleIndex(),
571
762
  marker: new RuleIndex(),
763
+ selection: new RuleIndex(),
572
764
  };
573
765
  private _adapter: CssSelectAdapter;
574
766
  /** The compounds of the selectors that test the pointer, each without
575
767
  * its `:hover` or `:active` (`pointerCompounds`), and their matchers,
576
768
  * compiled when a pointer move first asks (`hoverTouches`). */
577
- private _hoverCompounds = new Set<string>();
578
- private _hoverMatchers: ((el: Element) => boolean)[] | null = null;
769
+ private _hoverCompounds = new Map<string, boolean>();
770
+ private _hoverMatchers:
771
+ { match: (el: Element) => boolean; followed: boolean }[] | null = null;
579
772
  /** Whether a pointer move can be restyled where it happened
580
773
  * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
581
774
  * pointer inside a functional pseudo-class, which reaches elements no
582
775
  * compound names. */
583
776
  hoverLocal = true;
584
- /** Whether a compound that tests the pointer is followed by a sibling
585
- * combinator, so an element's later siblings restyle with it. */
586
- hoverSiblings = false;
587
- /** The compounds whose `:has()` tests the pointer, each without it, and
588
- * whether its argument starts at a sibling (`hoverAnchors`). */
589
- private _hoverAnchors = new Map<string, boolean>();
777
+ /** The compounds whose `:has()` tests the pointer, each without it,
778
+ * whether its argument starts at a sibling, and whether a sibling
779
+ * combinator follows it (`hoverAnchors`). */
780
+ private _hoverAnchors = new Map<
781
+ string,
782
+ { siblings: boolean; followed: boolean }
783
+ >();
590
784
  private _anchorMatchers:
591
- { match: (el: Element) => boolean; siblings: boolean }[] | null = null;
785
+ | {
786
+ match: (el: Element) => boolean;
787
+ siblings: boolean;
788
+ followed: boolean;
789
+ }[]
790
+ | null = null;
592
791
  private _pointer: PointerState = NO_POINTER;
593
792
  readonly initial: ComputedStyle;
594
793
  readonly look: RootLook;
@@ -627,6 +826,12 @@ export class Cascade {
627
826
  /** The families the document loads itself (`fonts.ts`), or null for a
628
827
  * document with no `@font-face`. */
629
828
  private _families: FontFamilies | null;
829
+ /** The root element's computed style: its `font-size` is what a `rem`
830
+ * is and its line height what an `rlh` is (CSS Values 4, 6.1.1), in
831
+ * every element but the root, whose own declarations measure them from
832
+ * the initial values. Set as the root's style is computed, which a build
833
+ * does ahead of every element under it. */
834
+ private _root: ComputedStyle | null = null;
630
835
  private _mapFamilies: ((list: string) => string) | undefined;
631
836
  /** Whether any declaration has a length in `lh` or `rlh`: only then is
632
837
  * the line height settled ahead of the declarations that read it. */
@@ -673,6 +878,9 @@ export class Cascade {
673
878
  this._noteHover((pseudo?.rule ?? rule).selector);
674
879
  }
675
880
  for (const bp of sheet.breakpoints) breakpoints.add(bp);
881
+ // a query on the viewport's height reads it as a `vh` does
882
+ if (sheet.readsHeight) this.readsViewportHeight = true;
883
+ if (sheet.readsWidth) this.readsViewportWidth = true;
676
884
  }
677
885
  this.breakpoints = [...breakpoints].sort((a, b) => a - b);
678
886
  this.counterStyles = new CounterStyles(counterStyles);
@@ -727,19 +935,50 @@ export class Cascade {
727
935
  this._pointer = pointer;
728
936
  }
729
937
 
938
+ /**
939
+ * Which pseudo-elements' rules test the pointer: none's
940
+ * (`HOVER_PSEUDO_NONE`); a `::before`'s or an `::after`'s only
941
+ * (`HOVER_PSEUDO_GENERATED`), whose boxes a move restyles where they are
942
+ * as it does an element's; or a `::first-line`'s, a `::first-letter`'s
943
+ * or a `::marker`'s (`HOVER_PSEUDO_OTHER`), which layout styles and a
944
+ * move cannot.
945
+ */
946
+ get hoverPseudo(): 0 | 1 | 2 {
947
+ if (
948
+ this._pseudo['first-letter'].hoverSensitive ||
949
+ this._pseudo['first-line'].hoverSensitive ||
950
+ this._pseudo.marker.hoverSensitive
951
+ ) {
952
+ return HOVER_PSEUDO_OTHER;
953
+ }
954
+ return this._pseudo.before.hoverSensitive ||
955
+ this._pseudo.after.hoverSensitive
956
+ ? HOVER_PSEUDO_GENERATED
957
+ : HOVER_PSEUDO_NONE;
958
+ }
959
+
730
960
  /**
731
961
  * Whether the pointer entering or leaving `el` can change a style: one of
732
962
  * the compounds a selector tests the pointer in matches it, the pointer
733
963
  * aside. A move between two paragraphs of a page whose only such rule is
734
- * `a:hover` touches nothing, and restyles nothing.
964
+ * `a:hover` touches nothing, and restyles nothing. `HOVER_FOLLOWED`
965
+ * where a sibling combinator follows one that matches, so the element's
966
+ * later siblings can change with it — those rules' elements alone: a
967
+ * sheet with one `.peer:hover ~ *` in it does not make every row of a
968
+ * table restyle the rows after it.
735
969
  */
736
- hoverTouches(el: Element): boolean {
737
- this._hoverMatchers ??= [...this._hoverCompounds].flatMap((c) => {
738
- const match = this._compile(c);
739
- return match ? [match] : [];
740
- });
741
- for (const match of this._hoverMatchers) if (match(el)) return true;
742
- return false;
970
+ hoverTouches(el: Element): HoverTouch {
971
+ // the followed ones first, so the first that matches is the answer
972
+ this._hoverMatchers ??= [...this._hoverCompounds]
973
+ .sort(([, a], [, b]) => Number(b) - Number(a))
974
+ .flatMap(([compound, followed]) => {
975
+ const match = this._compile(compound);
976
+ return match ? [{ match, followed }] : [];
977
+ });
978
+ for (const { match, followed } of this._hoverMatchers) {
979
+ if (match(el)) return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
980
+ }
981
+ return HOVER_UNTOUCHED;
743
982
  }
744
983
 
745
984
  /**
@@ -747,22 +986,27 @@ export class Cascade {
747
986
  * hovered or left: the ancestors of it a compound whose `:has()` tests
748
987
  * the pointer names, and their earlier siblings and its own where the
749
988
  * argument starts at a sibling — a superset, which is restyled and found
750
- * the same where it did not. Into `into`.
989
+ * the same where it did not. Into `into`, each with whether its later
990
+ * siblings can change with it (`hoverTouches`).
751
991
  */
752
- hoverAnchors(el: Element, into: Element[]): void {
992
+ hoverAnchors(el: Element, into: Map<Element, HoverTouch>): void {
753
993
  if (!this._hoverAnchors.size) return;
754
- this._anchorMatchers ??= [...this._hoverAnchors].flatMap(
755
- ([anchor, siblings]) => {
994
+ this._anchorMatchers ??= [...this._hoverAnchors]
995
+ .sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
996
+ .flatMap(([anchor, { siblings, followed }]) => {
756
997
  const match = this._compile(anchor);
757
- return match ? [{ match, siblings }] : [];
758
- },
759
- );
998
+ return match ? [{ match, siblings, followed }] : [];
999
+ });
1000
+ const note = (at: Element, followed: boolean): void => {
1001
+ if (followed) into.set(at, HOVER_FOLLOWED);
1002
+ else if (!into.has(at)) into.set(at, HOVER_TOUCHED);
1003
+ };
760
1004
  const siblings = this._anchorMatchers.some((m) => m.siblings);
761
1005
  for (let at: Element | null = el; at;) {
762
1006
  if (at !== el) {
763
- for (const { match } of this._anchorMatchers) {
1007
+ for (const { match, followed } of this._anchorMatchers) {
764
1008
  if (match(at)) {
765
- into.push(at);
1009
+ note(at, followed);
766
1010
  break;
767
1011
  }
768
1012
  }
@@ -770,9 +1014,9 @@ export class Cascade {
770
1014
  if (siblings) {
771
1015
  for (let s = at.prev; s; s = s.prev) {
772
1016
  if (!isTag(s as Element)) continue;
773
- for (const { match, siblings: apart } of this._anchorMatchers) {
774
- if (apart && match(s as Element)) {
775
- into.push(s as Element);
1017
+ for (const m of this._anchorMatchers) {
1018
+ if (m.siblings && m.match(s as Element)) {
1019
+ note(s as Element, m.followed);
776
1020
  break;
777
1021
  }
778
1022
  }
@@ -787,14 +1031,20 @@ export class Cascade {
787
1031
  if (!/:(?:hover|active)/i.test(selector)) return;
788
1032
  const found = pointerCompounds(selector);
789
1033
  if (found.nested) this.hoverLocal = false;
790
- if (found.siblings) this.hoverSiblings = true;
791
- for (const c of found.compounds) this._hoverCompounds.add(c);
792
- for (const { anchor, siblings } of found.has) {
793
- this._hoverAnchors.set(
794
- anchor,
795
- siblings || (this._hoverAnchors.get(anchor) ?? false),
1034
+ const followed = new Set(found.followed);
1035
+ for (const c of found.compounds) {
1036
+ this._hoverCompounds.set(
1037
+ c,
1038
+ followed.has(c) || (this._hoverCompounds.get(c) ?? false),
796
1039
  );
797
1040
  }
1041
+ for (const { anchor, siblings } of found.has) {
1042
+ const was = this._hoverAnchors.get(anchor);
1043
+ this._hoverAnchors.set(anchor, {
1044
+ siblings: siblings || (was?.siblings ?? false),
1045
+ followed: followed.has(anchor) || (was?.followed ?? false),
1046
+ });
1047
+ }
798
1048
  }
799
1049
 
800
1050
  /** A selector compiled as a rule's is, or null where css-select refuses
@@ -831,12 +1081,35 @@ export class Cascade {
831
1081
  private _ids = new WeakMap<object, number>();
832
1082
  private _nextId = 1;
833
1083
 
834
- /** A box tree is about to be built: the styles shared in the last build
835
- * were computed against a pointer and a viewport that may have moved. */
836
- beginSharing(): void {
1084
+ /** The styles kept from the build before, while a build that keeps them
1085
+ * runs, and what each kept element shares under in this one. */
1086
+ private _kept: KeptStyles | null = null;
1087
+ private _keptShared = new Map<Element, SharedStyle>();
1088
+
1089
+ /**
1090
+ * A box tree is about to be built: the styles shared in the last build
1091
+ * were computed against a pointer and a viewport that may have moved.
1092
+ *
1093
+ * With `kept`, only its `restyle` elements are matched: a build the
1094
+ * pointer asked for — a hover that moved something — changes the styles
1095
+ * of the elements the move reached, and every other element's is what it
1096
+ * was. Matching them all again was most of such a build: a frame of 550 ms
1097
+ * on a page with a Tailwind sheet, and 150 without it.
1098
+ */
1099
+ beginSharing(kept: KeptStyles | null = null): void {
837
1100
  this._shared.clear();
838
1101
  this._sharedByMatch.clear();
839
- this._customs.clear();
1102
+ // the kept styles' custom properties are the sets in here, which the
1103
+ // elements styled under them find again
1104
+ if (!kept) this._customs.clear();
1105
+ this._kept = kept;
1106
+ this._keptShared.clear();
1107
+ }
1108
+
1109
+ /** The build is over: nothing later is answered from what it kept. */
1110
+ endSharing(): void {
1111
+ this._kept = null;
1112
+ this._keptShared.clear();
840
1113
  }
841
1114
 
842
1115
  /**
@@ -924,6 +1197,22 @@ export class Cascade {
924
1197
  parentKey: number,
925
1198
  inFlexContainer: boolean,
926
1199
  ): SharedStyle {
1200
+ const kept = this._kept;
1201
+ if (kept !== null && !kept.restyle.has(el)) {
1202
+ // An element the change did not reach keeps its style, under a key
1203
+ // of its own: a key stands for an element's ancestors, which the
1204
+ // elements styled again under it have in common, and two kept
1205
+ // elements with one style need not — a hover restyled in place
1206
+ // leaves a hovered card's children the styles its neighbours' have.
1207
+ let shared = this._keptShared.get(el);
1208
+ if (shared !== undefined) return shared;
1209
+ const was = kept.styles.get(el);
1210
+ if (was !== undefined && was.inFlex === inFlexContainer) {
1211
+ shared = { style: was.style, key: this._nextShareKey++ };
1212
+ this._keptShared.set(el, shared);
1213
+ return shared;
1214
+ }
1215
+ }
927
1216
  let key = `${parentKey}\u0001${inFlexContainer ? 1 : 0}`;
928
1217
  const tag = tagOf(el);
929
1218
  key += `\u0001${tag.length}:${tag}`;
@@ -1087,6 +1376,56 @@ export class Cascade {
1087
1376
  return out;
1088
1377
  }
1089
1378
 
1379
+ /** Whether any rule styles a `::selection`: a document with none draws
1380
+ * a selection in the palette's colour, and asks nothing. */
1381
+ get hasSelection(): boolean {
1382
+ return this._pseudo.selection.size > 0;
1383
+ }
1384
+
1385
+ /**
1386
+ * An element's `::selection`, over its parent's (`parent`, null for the
1387
+ * palette's own): the colour and the background the rules that reach it
1388
+ * set, and its parent's for either they leave alone. Both inherit along
1389
+ * the chain of highlights rather than the elements' (CSS Pseudo 4, 3.5,
1390
+ * "highlight inheritance"), which is what Chrome draws: a `<span>` in a
1391
+ * `div::selection { background: red }` is selected in red, and a `<p>`
1392
+ * whose own rule sets only a colour keeps the red under it.
1393
+ *
1394
+ * The palette's highlight is taken only where no rule has set either
1395
+ * (3.6, "paired defaults"): one that sets a colour and no background is
1396
+ * selected over none, as Chrome has it. A rule that sets neither — a
1397
+ * `text-shadow` only — leaves the element where its parent was.
1398
+ */
1399
+ selectionStyle(
1400
+ el: Element,
1401
+ style: ComputedStyle,
1402
+ parent: SelectionStyle | null,
1403
+ ): SelectionStyle | null {
1404
+ const index = this._pseudo.selection;
1405
+ if (!index.size || !index.reaches(el)) return parent;
1406
+ const candidates: Candidate[] = [];
1407
+ this._matchInto(index, el, candidates);
1408
+ let color = false;
1409
+ let background = false;
1410
+ for (const candidate of candidates) {
1411
+ for (const d of candidate.declarations) {
1412
+ if (d.prop === 'color') color = true;
1413
+ else if (d.prop === 'background' || d.prop === 'background-color') {
1414
+ background = true;
1415
+ }
1416
+ }
1417
+ }
1418
+ if (!color && !background) return parent;
1419
+ candidates.sort(byCascade);
1420
+ const own = this._computeStyle(el, style, false, candidates);
1421
+ return {
1422
+ color: color ? own.color : (parent?.color ?? null),
1423
+ background: background
1424
+ ? own.backgroundColor
1425
+ : (parent?.background ?? null),
1426
+ };
1427
+ }
1428
+
1090
1429
  /**
1091
1430
  * An element's `::first-line` style, inheriting from its own, or null
1092
1431
  * when no rule reaches it (CSS 2.1 5.12.1).
@@ -1180,9 +1519,11 @@ export class Cascade {
1180
1519
  // size, so a `font-size: 1.2em` in the cascade resolves against the
1181
1520
  // right em, and again after the font size is settled so every *other*
1182
1521
  // em-relative length in the same rule resolves against this element's.
1522
+ const root = isRootElement(el);
1523
+ const rootStyle = root ? this.initial : (this._root ?? this.initial);
1183
1524
  const ctxParent: UnitContext = {
1184
1525
  em: parentStyle.fontSize,
1185
- rem: this.initial.fontSize,
1526
+ rem: rootStyle.fontSize,
1186
1527
  initial: this.initial,
1187
1528
  vw: this.viewportWidth,
1188
1529
  vh: this.viewportHeight,
@@ -1191,7 +1532,7 @@ export class Cascade {
1191
1532
  ch: () => this._chOf(parentStyle),
1192
1533
  families: this._mapFamilies,
1193
1534
  lh: () => this._lineHeightOf(parentStyle),
1194
- rlh: () => this._lineHeightOf(this.initial),
1535
+ rlh: () => this._lineHeightOf(rootStyle),
1195
1536
  };
1196
1537
  // The family, the weight and the slant go with the size: together they
1197
1538
  // pick the face an `ex`, a `ch` or an `lh` in any declaration is
@@ -1249,9 +1590,12 @@ export class Cascade {
1249
1590
  fontStyle: style.fontStyle,
1250
1591
  };
1251
1592
  let line: LineSource | null = null;
1593
+ // the root's other declarations measure a `rem` from its own size
1594
+ if (root) this._root = style;
1252
1595
  const ctx: UnitContext = {
1253
1596
  ...ctxParent,
1254
1597
  em: style.fontSize,
1598
+ rem: root ? style.fontSize : ctxParent.rem,
1255
1599
  ex: () => this._exOf(face),
1256
1600
  ch: () => this._chOf(face),
1257
1601
  lh: () => this._lineHeightOf(line ?? style),
@@ -1382,22 +1726,68 @@ export class Cascade {
1382
1726
  return { style: asRoot(this.styleFor(synthetic, parent, false)), html };
1383
1727
  }
1384
1728
 
1729
+ /**
1730
+ * Whether the pointer's move from `was` to where it is changed which
1731
+ * rules match `el`, or its `::before`, `::after` or another of its
1732
+ * pseudo-elements: the rules that test `:hover` asked under both, and
1733
+ * nothing else, since no other rule's answer is the pointer's. An element
1734
+ * they answer the same for, under a parent whose style is what it was, has
1735
+ * the style it had — which is most of what a move reaches: the rows of a
1736
+ * hovered table body, what is in a hovered card.
1737
+ */
1738
+ pointerChanged(el: Element, was: PointerState): boolean {
1739
+ const now = this._pointer;
1740
+ const after = this._pointerAnswers(el);
1741
+ this._pointer = was;
1742
+ const before = this._pointerAnswers(el);
1743
+ this._pointer = now;
1744
+ return before !== after;
1745
+ }
1746
+
1747
+ /** The ids of the rules testing the pointer that match `el` as things
1748
+ * stand, the element's and each pseudo-element's. */
1749
+ private _pointerAnswers(el: Element): string {
1750
+ let answers = '';
1751
+ const ask = (index: RuleIndex, name: string): void => {
1752
+ const pointer = index.pointer;
1753
+ if (!pointer || !pointer.reaches(el)) return;
1754
+ const matched: number[] = [];
1755
+ this._matchInto(pointer, el, null, matched);
1756
+ if (matched.length) answers += `${name}${matched.join(',')};`;
1757
+ };
1758
+ ask(this._index, '');
1759
+ for (const which of POINTER_PSEUDO_ELEMENTS)
1760
+ ask(this._pseudo[which], which);
1761
+ return answers;
1762
+ }
1763
+
1385
1764
  /** The rules of `index` that match `el`, pushed onto `out` as candidates,
1386
1765
  * and their ids onto `matched` in the order they were tried. */
1387
1766
  private _matchInto(
1388
1767
  index: RuleIndex,
1389
1768
  el: Element,
1390
- out: Candidate[],
1769
+ out: Candidate[] | null,
1391
1770
  matched?: number[],
1392
1771
  ): void {
1393
1772
  // A media query's width is CSS pixels; the viewport is kept in device.
1394
1773
  const width = this.viewportWidth / this.scale;
1774
+ const height = this.viewportHeight / this.scale;
1395
1775
 
1396
1776
  const consider = (bucket: IndexedRule[] | undefined): void => {
1397
1777
  if (!bucket) return;
1398
1778
  for (const indexed of bucket) {
1399
1779
  const rule = indexed.rule;
1400
- if (!mediaMatches(rule.media, width, this.look.colorScheme)) continue;
1780
+ if (
1781
+ !mediaMatches(
1782
+ rule.media,
1783
+ width,
1784
+ this.look.colorScheme,
1785
+ height,
1786
+ this.scale,
1787
+ )
1788
+ ) {
1789
+ continue;
1790
+ }
1401
1791
  if (!indexed.compiled) {
1402
1792
  indexed.compiled = true;
1403
1793
  try {
@@ -1411,6 +1801,7 @@ export class Cascade {
1411
1801
  }
1412
1802
  if (!indexed.match || !indexed.match(el)) continue;
1413
1803
  matched?.push(indexed.id);
1804
+ if (out === null) continue;
1414
1805
  const origin = rule.order < 0 ? Origin.UserAgent : Origin.Author;
1415
1806
  pushRule(out, rule, origin);
1416
1807
  }
@@ -1478,10 +1869,59 @@ export class Cascade {
1478
1869
  }
1479
1870
 
1480
1871
  out.sort(byCascade);
1872
+ dropPaletteChrome(out);
1481
1873
  return out;
1482
1874
  }
1483
1875
  }
1484
1876
 
1877
+ /**
1878
+ * Takes the palette's control chrome (`PALETTE_CHROME`) out of an element's
1879
+ * candidates where the page styles its background or border, so what shows
1880
+ * through is the web's UA values the page was written against. The rule is
1881
+ * Blink's (`LayoutTheme::IsControlStyled`): a background or border longhand
1882
+ * set by the page, radius included, drops the control's native appearance;
1883
+ * so does `appearance: none`. `out` is in cascade order, so the UA's
1884
+ * candidates are the ones before the first of any other origin.
1885
+ */
1886
+ function dropPaletteChrome(out: Candidate[]): void {
1887
+ let chrome = false;
1888
+ let i = 0;
1889
+ for (; i < out.length && out[i].origin === Origin.UserAgent; i += 1) {
1890
+ if (PALETTE_CHROME.has(out[i].declarations)) chrome = true;
1891
+ }
1892
+ if (!chrome) return;
1893
+ let styled = false;
1894
+ for (let j = i; j < out.length && !styled; j += 1) {
1895
+ styled = pick(out[j]).some(stylesChrome);
1896
+ }
1897
+ if (!styled) return;
1898
+ let kept = 0;
1899
+ for (const c of out) {
1900
+ if (c.origin === Origin.UserAgent && PALETTE_CHROME.has(c.declarations)) {
1901
+ continue;
1902
+ }
1903
+ out[kept++] = c;
1904
+ }
1905
+ out.length = kept;
1906
+ }
1907
+
1908
+ /** Whether a declaration styles what a control's native look draws: Blink's
1909
+ * `is_background` and `is_border` properties and their shorthands, and an
1910
+ * `appearance` of `none`. */
1911
+ function stylesChrome(d: Declaration): boolean {
1912
+ const prop = d.prop;
1913
+ if (prop.startsWith('background')) {
1914
+ return prop !== 'background-repeat' && prop !== 'background-blend-mode';
1915
+ }
1916
+ if (prop.startsWith('border')) {
1917
+ return prop !== 'border-collapse' && prop !== 'border-spacing';
1918
+ }
1919
+ return (
1920
+ (prop === 'appearance' || prop === '-webkit-appearance') &&
1921
+ d.value.trim().toLowerCase() === 'none'
1922
+ );
1923
+ }
1924
+
1485
1925
  /** An implied element's style as the root box's: its look, not its role —
1486
1926
  * the root is still a block, in flow, and the initial containing block. */
1487
1927
  function asRoot(style: ComputedStyle): ComputedStyle {
@@ -1807,7 +2247,12 @@ function presentationHints(el: Element): Declaration[] {
1807
2247
  // `width`/`height` are lengths on the replaced and table elements and mean
1808
2248
  // nothing anywhere else, which is what stops a `<input width>` from
1809
2249
  // becoming a CSS width the widget then disagrees with.
1810
- if (SIZED.has(tag)) {
2250
+ if (
2251
+ SIZED.has(tag) ||
2252
+ // an image button's are its image's, as an `<img>`'s are (HTML 15.4.3)
2253
+ (tag === 'input' &&
2254
+ (attr(el, 'type') ?? '').trim().toLowerCase() === 'image')
2255
+ ) {
1811
2256
  const width = attr(el, 'width');
1812
2257
  if (width) push('width', lengthAttr(width));
1813
2258
  const height = attr(el, 'height');
@@ -2047,3 +2492,9 @@ const ABSOLUTE_SIZES = new Set([
2047
2492
  'xxx-large',
2048
2493
  'initial',
2049
2494
  ]);
2495
+
2496
+ /** Whether an element is the document's root: its parent the document, or
2497
+ * none, as the `<html>` the cascade supplies where the markup has none. */
2498
+ function isRootElement(el: Element): boolean {
2499
+ return !el.parent || el.parent.type === 'root';
2500
+ }