@react-x11/components 0.11.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 (126) hide show
  1. package/dist/flow/node.d.ts +14 -6
  2. package/dist/flow/node.d.ts.map +1 -1
  3. package/dist/flow/node.js +82 -64
  4. package/dist/flow/node.js.map +1 -1
  5. package/dist/html/controls.d.ts +38 -10
  6. package/dist/html/controls.d.ts.map +1 -1
  7. package/dist/html/controls.js +43 -58
  8. package/dist/html/controls.js.map +1 -1
  9. package/dist/html/css/cascade.d.ts +134 -20
  10. package/dist/html/css/cascade.d.ts.map +1 -1
  11. package/dist/html/css/cascade.js +538 -128
  12. package/dist/html/css/cascade.js.map +1 -1
  13. package/dist/html/css/color.d.ts +19 -0
  14. package/dist/html/css/color.d.ts.map +1 -1
  15. package/dist/html/css/color.js +86 -0
  16. package/dist/html/css/color.js.map +1 -1
  17. package/dist/html/css/parse.d.ts +40 -2
  18. package/dist/html/css/parse.d.ts.map +1 -1
  19. package/dist/html/css/parse.js +391 -13
  20. package/dist/html/css/parse.js.map +1 -1
  21. package/dist/html/css/style.d.ts +52 -1
  22. package/dist/html/css/style.d.ts.map +1 -1
  23. package/dist/html/css/style.js +328 -11
  24. package/dist/html/css/style.js.map +1 -1
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -7
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +10 -1
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js +27 -2
  32. package/dist/html/css/values.js.map +1 -1
  33. package/dist/html/css/vars.d.ts +5 -0
  34. package/dist/html/css/vars.d.ts.map +1 -1
  35. package/dist/html/css/vars.js +2 -2
  36. package/dist/html/css/vars.js.map +1 -1
  37. package/dist/html/dom.d.ts.map +1 -1
  38. package/dist/html/dom.js +38 -3
  39. package/dist/html/dom.js.map +1 -1
  40. package/dist/html/fonts.d.ts +25 -1
  41. package/dist/html/fonts.d.ts.map +1 -1
  42. package/dist/html/fonts.js +99 -6
  43. package/dist/html/fonts.js.map +1 -1
  44. package/dist/html/form.d.ts +200 -0
  45. package/dist/html/form.d.ts.map +1 -0
  46. package/dist/html/form.js +801 -0
  47. package/dist/html/form.js.map +1 -0
  48. package/dist/html/index.d.ts +12 -0
  49. package/dist/html/index.d.ts.map +1 -1
  50. package/dist/html/index.js +30 -264
  51. package/dist/html/index.js.map +1 -1
  52. package/dist/html/layout/block.d.ts +11 -2
  53. package/dist/html/layout/block.d.ts.map +1 -1
  54. package/dist/html/layout/block.js +186 -19
  55. package/dist/html/layout/block.js.map +1 -1
  56. package/dist/html/layout/boxes.d.ts +65 -7
  57. package/dist/html/layout/boxes.d.ts.map +1 -1
  58. package/dist/html/layout/boxes.js +87 -22
  59. package/dist/html/layout/boxes.js.map +1 -1
  60. package/dist/html/layout/cache.d.ts +2 -2
  61. package/dist/html/layout/cache.d.ts.map +1 -1
  62. package/dist/html/layout/cache.js +8 -3
  63. package/dist/html/layout/cache.js.map +1 -1
  64. package/dist/html/layout/flex.js +4 -1
  65. package/dist/html/layout/flex.js.map +1 -1
  66. package/dist/html/layout/floats.d.ts +4 -0
  67. package/dist/html/layout/floats.d.ts.map +1 -1
  68. package/dist/html/layout/floats.js +26 -0
  69. package/dist/html/layout/floats.js.map +1 -1
  70. package/dist/html/layout/inline.d.ts +26 -2
  71. package/dist/html/layout/inline.d.ts.map +1 -1
  72. package/dist/html/layout/inline.js +1232 -115
  73. package/dist/html/layout/inline.js.map +1 -1
  74. package/dist/html/node.d.ts +120 -15
  75. package/dist/html/node.d.ts.map +1 -1
  76. package/dist/html/node.js +993 -174
  77. package/dist/html/node.js.map +1 -1
  78. package/dist/html/paint.d.ts +75 -2
  79. package/dist/html/paint.d.ts.map +1 -1
  80. package/dist/html/paint.js +836 -200
  81. package/dist/html/paint.js.map +1 -1
  82. package/dist/html/surfaces.d.ts +7 -2
  83. package/dist/html/surfaces.d.ts.map +1 -1
  84. package/dist/html/surfaces.js +16 -0
  85. package/dist/html/surfaces.js.map +1 -1
  86. package/dist/html/widgets.d.ts +29 -0
  87. package/dist/html/widgets.d.ts.map +1 -0
  88. package/dist/html/widgets.js +620 -0
  89. package/dist/html/widgets.js.map +1 -0
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/internal/window.d.ts +22 -0
  94. package/dist/internal/window.d.ts.map +1 -1
  95. package/dist/internal/window.js +35 -8
  96. package/dist/internal/window.js.map +1 -1
  97. package/dist/richtext/node.d.ts +7 -0
  98. package/dist/richtext/node.d.ts.map +1 -1
  99. package/dist/richtext/node.js.map +1 -1
  100. package/package.json +6 -4
  101. package/src/flow/node.ts +80 -64
  102. package/src/html/controls.ts +79 -58
  103. package/src/html/css/cascade.ts +610 -146
  104. package/src/html/css/color.ts +87 -0
  105. package/src/html/css/parse.ts +431 -13
  106. package/src/html/css/style.ts +367 -11
  107. package/src/html/css/ua.ts +76 -8
  108. package/src/html/css/values.ts +31 -3
  109. package/src/html/css/vars.ts +2 -2
  110. package/src/html/dom.ts +41 -3
  111. package/src/html/fonts.ts +116 -6
  112. package/src/html/form.ts +962 -0
  113. package/src/html/index.ts +49 -300
  114. package/src/html/layout/block.ts +231 -16
  115. package/src/html/layout/boxes.ts +133 -32
  116. package/src/html/layout/cache.ts +8 -3
  117. package/src/html/layout/flex.ts +4 -1
  118. package/src/html/layout/floats.ts +24 -0
  119. package/src/html/layout/inline.ts +1445 -108
  120. package/src/html/node.ts +1010 -166
  121. package/src/html/paint.ts +980 -270
  122. package/src/html/surfaces.ts +21 -1
  123. package/src/html/widgets.ts +735 -0
  124. package/src/index.ts +1 -0
  125. package/src/internal/window.ts +36 -8
  126. package/src/richtext/node.ts +7 -0
@@ -48,7 +48,9 @@ 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';
53
+ import { viewportUnit } from './values.js';
52
54
  import { customProperties, substituteIn } from './vars.js';
53
55
  import type { CustomProps } from './vars.js';
54
56
 
@@ -92,6 +94,41 @@ export interface SharedStyle {
92
94
  key: number;
93
95
  }
94
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
+
95
132
  /** A compiled matcher, kept beside the rule it came from. */
96
133
  interface IndexedRule {
97
134
  rule: StyleRule;
@@ -131,6 +168,11 @@ class RuleIndex {
131
168
  /** Whether any rule in here is pointer-sensitive, so the renderer knows
132
169
  * whether a pointer move can change the cascade at all. */
133
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;
134
176
  /** The buckets holding a rule an element cannot share its style under
135
177
  * (`UNSHAREABLE`): the ids, classes and tags whose elements compute
136
178
  * their own, and whether the universal bucket makes every element do so. */
@@ -184,26 +226,44 @@ class RuleIndex {
184
226
  else if (key.kind === 'tag') this.ownStyleTags.add(key.name);
185
227
  else this.ownStyleEverywhere = true;
186
228
  }
187
- const bucket =
188
- key.kind === 'id'
189
- ? mapBucket(this.byId, key.name)
190
- : key.kind === 'class'
191
- ? mapBucket(this.byClass, key.name)
192
- : key.kind === 'tag'
193
- ? mapBucket(this.byTag, key.name)
194
- : this.universal;
195
- 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;
196
245
  }
197
246
  }
198
247
 
199
248
  /** The pseudo-elements a rule can style here. */
200
249
  type PseudoElement =
201
- 'before' | 'after' | 'first-letter' | 'first-line' | 'marker';
250
+ 'before' | 'after' | 'first-letter' | 'first-line' | 'marker' | 'selection';
202
251
 
203
252
  /** A selector's trailing `::before`, `::after`, `::first-letter`,
204
- * `::first-line` or `::marker`, or CSS 2's single-colon spelling of any of
205
- * its four. */
206
- 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
+ }
207
267
 
208
268
  /**
209
269
  * A rule for a pseudo-element, as the pseudo-element it styles and a rule
@@ -225,7 +285,7 @@ function splitPseudoElement(
225
285
  ? `${trimmed} *`
226
286
  : trimmed;
227
287
  return {
228
- which: (m[1] ?? 'marker').toLowerCase() as PseudoElement,
288
+ which: (m[1] ?? m[2]).toLowerCase() as PseudoElement,
229
289
  rule: { ...rule, selector },
230
290
  };
231
291
  }
@@ -233,34 +293,63 @@ function splitPseudoElement(
233
293
  /**
234
294
  * The compounds of a selector that test `:hover`, each without it (and
235
295
  * without an `:active` beside it), and whether one is followed by a sibling
236
- * combinator, which reaches the element's later siblings. `nested` where a
237
- * `:hover` sits inside a functional pseudo-class — `:not(:hover)`,
238
- * `: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.
239
312
  *
240
- * A `:hover` in a compound's `:has()` is the one exception, since what it
241
- * 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
242
315
  * the element the pointer is over — or an earlier sibling of it or of an
243
316
  * ancestor, where the argument starts at a sibling (Selectors 4, 4.5). So
244
- * it is given as an anchor (`has`) rather than making the rule `nested`.
317
+ * it is given as an anchor (`has`).
245
318
  *
246
319
  * `:active` is not the pointer's here: nothing sets it (`setPointer` is
247
320
  * handed none), so a selector that tests only it never changes as the
248
321
  * pointer moves — Wikipedia's buttons' `:focus:not(:active)` among them.
249
322
  * A press that sets it would have to be counted here too.
250
323
  */
251
- 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 {
252
337
  compounds: string[];
253
338
  siblings: boolean;
254
339
  nested: boolean;
255
340
  has: { anchor: string; siblings: boolean }[];
256
- } {
257
- const out = {
258
- compounds: [] as string[],
259
- siblings: false,
260
- nested: false,
261
- has: [] as { anchor: string; siblings: boolean }[],
262
- };
263
- // 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 }[] {
264
353
  const parts: { text: string; next: string }[] = [];
265
354
  let start = 0;
266
355
  let depth = 0;
@@ -286,85 +375,188 @@ export function pointerCompounds(selector: string): {
286
375
  }
287
376
  }
288
377
  split(selector.length, '');
289
- for (const { text, next } of parts) {
290
- let bare = '';
291
- let pointer = false;
292
- depth = 0;
293
- quote = '';
294
- /** Where a top-level `:has(` starts in `bare`, and its argument in
295
- * `text`, while one is open. */
296
- let hasAt = -1;
297
- let argAt = -1;
298
- let hasPointer = false;
299
- let anchored = false;
300
- const args: string[] = [];
301
- for (let i = 0; i < text.length; i += 1) {
302
- const c = text[i];
303
- if (quote) {
304
- if (c === quote && text[i - 1] !== '\\') quote = '';
305
- bare += c;
306
- 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]);
307
447
  }
308
- if (c === '\\') {
309
- const end = escapeEnd(text, i);
310
- bare += text.slice(i, end);
311
- i = end - 1;
448
+ for (let i = hasFrom; i < out.has.length; i += 1) {
449
+ out.followed.push(out.has[i].anchor);
450
+ }
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;
312
493
  continue;
313
494
  }
314
- if (c === '"' || c === "'") quote = c;
315
- else if (c === '(' || c === '[') {
316
- if (depth === 0 && c === '(' && /:has$/i.test(bare)) {
317
- hasAt = bare.length - 4;
318
- argAt = i + 1;
319
- hasPointer = false;
320
- }
321
- depth += 1;
322
- } else if (c === ')' || c === ']') {
323
- depth = Math.max(0, depth - 1);
324
- if (depth === 0 && hasAt >= 0 && c === ')') {
325
- // the compound is kept without it: its argument is what the
326
- // pointer changes
327
- if (hasPointer) {
328
- anchored = true;
329
- args.push(text.slice(argAt, i));
330
- bare = bare.slice(0, hasAt);
331
- } else bare += c;
332
- 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;
333
504
  continue;
334
505
  }
335
- } else if (c === ':' && text[i + 1] !== ':') {
336
- const m = POINTER_PSEUDO_AT.exec(text.slice(i));
337
- if (m) {
338
- const hover = m[0].length === 6;
339
- if (depth > 0) {
340
- // whatever holds it in the argument, the element it tests is
341
- // in the anchor's subtree or a later sibling's
342
- if (hover && hasAt >= 0) hasPointer = true;
343
- else if (hover) out.nested = true;
344
- } else {
345
- if (hover) pointer = true;
346
- i += m[0].length - 1;
347
- 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;
348
539
  }
540
+ if (own.length && !any) bare += `:is(${own.join(',')})`;
541
+ } else {
542
+ out.nested = true;
543
+ bare += whole;
349
544
  }
545
+ continue;
350
546
  }
351
- bare += c;
352
547
  }
353
- if (anchored) {
354
- out.has.push({
355
- anchor: bare.trim() || '*',
356
- siblings: args.some((arg) =>
357
- arg.split(',').some((one) => /^\s*[+~]/.test(one)),
358
- ),
359
- });
360
- }
361
- if (!pointer) continue;
362
- out.compounds.push(bare.trim() || '*');
363
- if (next === '+' || next === '~') out.siblings = true;
548
+ bare += c;
364
549
  }
365
- return out;
550
+ return { bare, pointer, below, args };
366
551
  }
367
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
+
368
560
  /** `:hover` or `:active` at the start of a string, and nothing longer. */
369
561
  const POINTER_PSEUDO_AT = /^:(?:hover|active)(?![\w-])/i;
370
562
 
@@ -568,26 +760,34 @@ export class Cascade {
568
760
  'first-letter': new RuleIndex(),
569
761
  'first-line': new RuleIndex(),
570
762
  marker: new RuleIndex(),
763
+ selection: new RuleIndex(),
571
764
  };
572
765
  private _adapter: CssSelectAdapter;
573
766
  /** The compounds of the selectors that test the pointer, each without
574
767
  * its `:hover` or `:active` (`pointerCompounds`), and their matchers,
575
768
  * compiled when a pointer move first asks (`hoverTouches`). */
576
- private _hoverCompounds = new Set<string>();
577
- 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;
578
772
  /** Whether a pointer move can be restyled where it happened
579
773
  * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
580
774
  * pointer inside a functional pseudo-class, which reaches elements no
581
775
  * compound names. */
582
776
  hoverLocal = true;
583
- /** Whether a compound that tests the pointer is followed by a sibling
584
- * combinator, so an element's later siblings restyle with it. */
585
- hoverSiblings = false;
586
- /** The compounds whose `:has()` tests the pointer, each without it, and
587
- * whether its argument starts at a sibling (`hoverAnchors`). */
588
- 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
+ >();
589
784
  private _anchorMatchers:
590
- { match: (el: Element) => boolean; siblings: boolean }[] | null = null;
785
+ | {
786
+ match: (el: Element) => boolean;
787
+ siblings: boolean;
788
+ followed: boolean;
789
+ }[]
790
+ | null = null;
591
791
  private _pointer: PointerState = NO_POINTER;
592
792
  readonly initial: ComputedStyle;
593
793
  readonly look: RootLook;
@@ -626,6 +826,12 @@ export class Cascade {
626
826
  /** The families the document loads itself (`fonts.ts`), or null for a
627
827
  * document with no `@font-face`. */
628
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;
629
835
  private _mapFamilies: ((list: string) => string) | undefined;
630
836
  /** Whether any declaration has a length in `lh` or `rlh`: only then is
631
837
  * the line height settled ahead of the declarations that read it. */
@@ -672,6 +878,9 @@ export class Cascade {
672
878
  this._noteHover((pseudo?.rule ?? rule).selector);
673
879
  }
674
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;
675
884
  }
676
885
  this.breakpoints = [...breakpoints].sort((a, b) => a - b);
677
886
  this.counterStyles = new CounterStyles(counterStyles);
@@ -703,7 +912,7 @@ export class Cascade {
703
912
  for (const d of declarations) {
704
913
  if (!VIEWPORT_UNIT.test(d.value)) continue;
705
914
  for (const m of d.value.matchAll(VIEWPORT_UNITS)) {
706
- const unit = m[1].toLowerCase();
915
+ const unit = viewportUnit(m[1]);
707
916
  if (unit !== 'vh') this.readsViewportWidth = true;
708
917
  if (unit !== 'vw') this.readsViewportHeight = true;
709
918
  }
@@ -726,19 +935,50 @@ export class Cascade {
726
935
  this._pointer = pointer;
727
936
  }
728
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
+
729
960
  /**
730
961
  * Whether the pointer entering or leaving `el` can change a style: one of
731
962
  * the compounds a selector tests the pointer in matches it, the pointer
732
963
  * aside. A move between two paragraphs of a page whose only such rule is
733
- * `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.
734
969
  */
735
- hoverTouches(el: Element): boolean {
736
- this._hoverMatchers ??= [...this._hoverCompounds].flatMap((c) => {
737
- const match = this._compile(c);
738
- return match ? [match] : [];
739
- });
740
- for (const match of this._hoverMatchers) if (match(el)) return true;
741
- 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;
742
982
  }
743
983
 
744
984
  /**
@@ -746,22 +986,27 @@ export class Cascade {
746
986
  * hovered or left: the ancestors of it a compound whose `:has()` tests
747
987
  * the pointer names, and their earlier siblings and its own where the
748
988
  * argument starts at a sibling — a superset, which is restyled and found
749
- * 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`).
750
991
  */
751
- hoverAnchors(el: Element, into: Element[]): void {
992
+ hoverAnchors(el: Element, into: Map<Element, HoverTouch>): void {
752
993
  if (!this._hoverAnchors.size) return;
753
- this._anchorMatchers ??= [...this._hoverAnchors].flatMap(
754
- ([anchor, siblings]) => {
994
+ this._anchorMatchers ??= [...this._hoverAnchors]
995
+ .sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
996
+ .flatMap(([anchor, { siblings, followed }]) => {
755
997
  const match = this._compile(anchor);
756
- return match ? [{ match, siblings }] : [];
757
- },
758
- );
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
+ };
759
1004
  const siblings = this._anchorMatchers.some((m) => m.siblings);
760
1005
  for (let at: Element | null = el; at;) {
761
1006
  if (at !== el) {
762
- for (const { match } of this._anchorMatchers) {
1007
+ for (const { match, followed } of this._anchorMatchers) {
763
1008
  if (match(at)) {
764
- into.push(at);
1009
+ note(at, followed);
765
1010
  break;
766
1011
  }
767
1012
  }
@@ -769,9 +1014,9 @@ export class Cascade {
769
1014
  if (siblings) {
770
1015
  for (let s = at.prev; s; s = s.prev) {
771
1016
  if (!isTag(s as Element)) continue;
772
- for (const { match, siblings: apart } of this._anchorMatchers) {
773
- if (apart && match(s as Element)) {
774
- 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);
775
1020
  break;
776
1021
  }
777
1022
  }
@@ -786,14 +1031,20 @@ export class Cascade {
786
1031
  if (!/:(?:hover|active)/i.test(selector)) return;
787
1032
  const found = pointerCompounds(selector);
788
1033
  if (found.nested) this.hoverLocal = false;
789
- if (found.siblings) this.hoverSiblings = true;
790
- for (const c of found.compounds) this._hoverCompounds.add(c);
791
- for (const { anchor, siblings } of found.has) {
792
- this._hoverAnchors.set(
793
- anchor,
794
- 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),
795
1039
  );
796
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
+ }
797
1048
  }
798
1049
 
799
1050
  /** A selector compiled as a rule's is, or null where css-select refuses
@@ -830,12 +1081,35 @@ export class Cascade {
830
1081
  private _ids = new WeakMap<object, number>();
831
1082
  private _nextId = 1;
832
1083
 
833
- /** A box tree is about to be built: the styles shared in the last build
834
- * were computed against a pointer and a viewport that may have moved. */
835
- 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 {
836
1100
  this._shared.clear();
837
1101
  this._sharedByMatch.clear();
838
- 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();
839
1113
  }
840
1114
 
841
1115
  /**
@@ -923,6 +1197,22 @@ export class Cascade {
923
1197
  parentKey: number,
924
1198
  inFlexContainer: boolean,
925
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
+ }
926
1216
  let key = `${parentKey}\u0001${inFlexContainer ? 1 : 0}`;
927
1217
  const tag = tagOf(el);
928
1218
  key += `\u0001${tag.length}:${tag}`;
@@ -1086,6 +1376,56 @@ export class Cascade {
1086
1376
  return out;
1087
1377
  }
1088
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
+
1089
1429
  /**
1090
1430
  * An element's `::first-line` style, inheriting from its own, or null
1091
1431
  * when no rule reaches it (CSS 2.1 5.12.1).
@@ -1179,9 +1519,11 @@ export class Cascade {
1179
1519
  // size, so a `font-size: 1.2em` in the cascade resolves against the
1180
1520
  // right em, and again after the font size is settled so every *other*
1181
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);
1182
1524
  const ctxParent: UnitContext = {
1183
1525
  em: parentStyle.fontSize,
1184
- rem: this.initial.fontSize,
1526
+ rem: rootStyle.fontSize,
1185
1527
  initial: this.initial,
1186
1528
  vw: this.viewportWidth,
1187
1529
  vh: this.viewportHeight,
@@ -1190,13 +1532,14 @@ export class Cascade {
1190
1532
  ch: () => this._chOf(parentStyle),
1191
1533
  families: this._mapFamilies,
1192
1534
  lh: () => this._lineHeightOf(parentStyle),
1193
- rlh: () => this._lineHeightOf(this.initial),
1535
+ rlh: () => this._lineHeightOf(rootStyle),
1194
1536
  };
1195
1537
  // The family, the weight and the slant go with the size: together they
1196
1538
  // pick the face an `ex`, a `ch` or an `lh` in any declaration is
1197
1539
  // measured in, however the declarations are ordered. A `width: 10ex`
1198
1540
  // in a sheet under an inline `font-weight: 900` was measured in the
1199
- // family's regular face.
1541
+ // family's regular face. The colour scheme goes ahead of the rest for
1542
+ // the same reason: every `light-dark()` among them is read by it.
1200
1543
  let sized = false;
1201
1544
  let keyword = false;
1202
1545
  for (const c of candidates) {
@@ -1204,7 +1547,8 @@ export class Cascade {
1204
1547
  if (
1205
1548
  d.prop === 'font-family' ||
1206
1549
  d.prop === 'font-weight' ||
1207
- d.prop === 'font-style'
1550
+ d.prop === 'font-style' ||
1551
+ d.prop === 'color-scheme'
1208
1552
  ) {
1209
1553
  this._apply(style, parentStyle, d, ctxParent);
1210
1554
  } else if (d.prop === 'font-size' || d.prop === 'font') {
@@ -1246,9 +1590,12 @@ export class Cascade {
1246
1590
  fontStyle: style.fontStyle,
1247
1591
  };
1248
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;
1249
1595
  const ctx: UnitContext = {
1250
1596
  ...ctxParent,
1251
1597
  em: style.fontSize,
1598
+ rem: root ? style.fontSize : ctxParent.rem,
1252
1599
  ex: () => this._exOf(face),
1253
1600
  ch: () => this._chOf(face),
1254
1601
  lh: () => this._lineHeightOf(line ?? style),
@@ -1379,22 +1726,68 @@ export class Cascade {
1379
1726
  return { style: asRoot(this.styleFor(synthetic, parent, false)), html };
1380
1727
  }
1381
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
+
1382
1764
  /** The rules of `index` that match `el`, pushed onto `out` as candidates,
1383
1765
  * and their ids onto `matched` in the order they were tried. */
1384
1766
  private _matchInto(
1385
1767
  index: RuleIndex,
1386
1768
  el: Element,
1387
- out: Candidate[],
1769
+ out: Candidate[] | null,
1388
1770
  matched?: number[],
1389
1771
  ): void {
1390
1772
  // A media query's width is CSS pixels; the viewport is kept in device.
1391
1773
  const width = this.viewportWidth / this.scale;
1774
+ const height = this.viewportHeight / this.scale;
1392
1775
 
1393
1776
  const consider = (bucket: IndexedRule[] | undefined): void => {
1394
1777
  if (!bucket) return;
1395
1778
  for (const indexed of bucket) {
1396
1779
  const rule = indexed.rule;
1397
- 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
+ }
1398
1791
  if (!indexed.compiled) {
1399
1792
  indexed.compiled = true;
1400
1793
  try {
@@ -1408,6 +1801,7 @@ export class Cascade {
1408
1801
  }
1409
1802
  if (!indexed.match || !indexed.match(el)) continue;
1410
1803
  matched?.push(indexed.id);
1804
+ if (out === null) continue;
1411
1805
  const origin = rule.order < 0 ? Origin.UserAgent : Origin.Author;
1412
1806
  pushRule(out, rule, origin);
1413
1807
  }
@@ -1475,10 +1869,59 @@ export class Cascade {
1475
1869
  }
1476
1870
 
1477
1871
  out.sort(byCascade);
1872
+ dropPaletteChrome(out);
1478
1873
  return out;
1479
1874
  }
1480
1875
  }
1481
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
+
1482
1925
  /** An implied element's style as the root box's: its look, not its role —
1483
1926
  * the root is still a block, in flow, and the initial containing block. */
1484
1927
  function asRoot(style: ComputedStyle): ComputedStyle {
@@ -1490,10 +1933,20 @@ function asRoot(style: ComputedStyle): ComputedStyle {
1490
1933
 
1491
1934
  /** `:root` is the `<html>` element: the one a browser implies around a
1492
1935
  * fragment, whose style the root box takes (`rootStyle`), and never a
1493
- * fragment's top-level elements, which css-select would take for it. */
1936
+ * fragment's top-level elements, which css-select would take for it.
1937
+ *
1938
+ * `:focus` and its two kin match nothing: no element of the document takes
1939
+ * the focus — a control's widget does, beside it. css-select has none of
1940
+ * the three and throws on them, which dropped every rule that named one,
1941
+ * `:not(:focus)` among them: Wikipedia's skip link hides with
1942
+ * `.mw-jump-link:not(:focus)`, and without it stood in the flow at the top
1943
+ * of every page. */
1494
1944
  const PSEUDOS = {
1495
1945
  root: (el: Element) =>
1496
1946
  el.name === 'html' && !(el.parent && isTag(el.parent as Element)),
1947
+ focus: () => false,
1948
+ 'focus-visible': () => false,
1949
+ 'focus-within': () => false,
1497
1950
  // css-select's, but its ranges and tags lower-cased as ASCII has it, and
1498
1951
  // no other script (CSS 2.1 4.1.3): Unicode's took `:lang(\u212Al)`, a
1499
1952
  // Kelvin sign for the K, for `:lang(kl)`
@@ -1527,8 +1980,8 @@ const PSEUDOS = {
1527
1980
 
1528
1981
  /** A length in a unit of the viewport: a number, then the unit, and no
1529
1982
  * more of a name after it. */
1530
- const VIEWPORT_UNIT = /\d(?:vw|vh|vmin|vmax)(?![\w-])/i;
1531
- const VIEWPORT_UNITS = /\d(vw|vh|vmin|vmax)(?![\w-])/gi;
1983
+ const VIEWPORT_UNIT = /\d[sld]?v(?:w|h|i|b|min|max)(?![\w-])/i;
1984
+ const VIEWPORT_UNITS = /\d([sld]?v(?:w|h|i|b|min|max))(?![\w-])/gi;
1532
1985
 
1533
1986
  /**
1534
1987
  * A selector with its `[attr~=""]` made one that matches nothing: an empty
@@ -1794,7 +2247,12 @@ function presentationHints(el: Element): Declaration[] {
1794
2247
  // `width`/`height` are lengths on the replaced and table elements and mean
1795
2248
  // nothing anywhere else, which is what stops a `<input width>` from
1796
2249
  // becoming a CSS width the widget then disagrees with.
1797
- 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
+ ) {
1798
2256
  const width = attr(el, 'width');
1799
2257
  if (width) push('width', lengthAttr(width));
1800
2258
  const height = attr(el, 'height');
@@ -2034,3 +2492,9 @@ const ABSOLUTE_SIZES = new Set([
2034
2492
  'xxx-large',
2035
2493
  'initial',
2036
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
+ }