@react-x11/components 0.16.0 → 0.17.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 (41) hide show
  1. package/dist/html/css/animation.d.ts +91 -0
  2. package/dist/html/css/animation.d.ts.map +1 -0
  3. package/dist/html/css/animation.js +430 -0
  4. package/dist/html/css/animation.js.map +1 -0
  5. package/dist/html/css/cascade.d.ts +27 -4
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +94 -6
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/parse.d.ts +35 -0
  10. package/dist/html/css/parse.d.ts.map +1 -1
  11. package/dist/html/css/parse.js +115 -2
  12. package/dist/html/css/parse.js.map +1 -1
  13. package/dist/html/css/style.d.ts +5 -0
  14. package/dist/html/css/style.d.ts.map +1 -1
  15. package/dist/html/css/style.js +57 -7
  16. package/dist/html/css/style.js.map +1 -1
  17. package/dist/html/css/ua.js +12 -8
  18. package/dist/html/css/ua.js.map +1 -1
  19. package/dist/html/layout/boxes.d.ts +13 -0
  20. package/dist/html/layout/boxes.d.ts.map +1 -1
  21. package/dist/html/layout/boxes.js +28 -0
  22. package/dist/html/layout/boxes.js.map +1 -1
  23. package/dist/html/layout/flex.d.ts.map +1 -1
  24. package/dist/html/layout/flex.js +237 -67
  25. package/dist/html/layout/flex.js.map +1 -1
  26. package/dist/html/layout/inline.d.ts.map +1 -1
  27. package/dist/html/layout/inline.js +41 -11
  28. package/dist/html/layout/inline.js.map +1 -1
  29. package/dist/html/node.d.ts.map +1 -1
  30. package/dist/html/node.js +12 -0
  31. package/dist/html/node.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/html/css/animation.ts +466 -0
  34. package/src/html/css/cascade.ts +130 -7
  35. package/src/html/css/parse.ts +151 -2
  36. package/src/html/css/style.ts +69 -7
  37. package/src/html/css/ua.ts +12 -8
  38. package/src/html/layout/boxes.ts +40 -0
  39. package/src/html/layout/flex.ts +251 -75
  40. package/src/html/layout/inline.ts +43 -9
  41. package/src/html/node.ts +12 -0
@@ -77,6 +77,43 @@ export interface Stylesheet {
77
77
  /** `@counter-style` rules, in order: a name and its descriptors, which
78
78
  * the cascade reads into counter styles (CSS Counter Styles 3, 3). */
79
79
  counterStyles?: { prelude: string; declarations: Declaration[] }[];
80
+ /** `@keyframes` rules, in order (`KeyframesRule`). */
81
+ keyframes?: KeyframesRule[];
82
+ }
83
+
84
+ /**
85
+ * One `@keyframes` rule (CSS Animations 1, 3): the frames of the animation
86
+ * an `animation-name` names. Which rule a name finds is the cascade's to
87
+ * say (`Cascade.keyframes`): the last of its name whose media hold, in the
88
+ * latest layer, and never a prefixed one over one that is not.
89
+ */
90
+ export interface KeyframesRule {
91
+ /** As written: a name is case-sensitive, as a custom ident is. */
92
+ name: string;
93
+ /** `@-webkit-keyframes`, which Chrome reads and which never takes the
94
+ * place of an `@keyframes` of the same name, wherever either comes. */
95
+ prefixed: boolean;
96
+ /** The frames in the order written, which is the order two at one
97
+ * offset cascade in. */
98
+ frames: Keyframe[];
99
+ /** The `@media` blocks the rule sits under, as a style rule's. */
100
+ media: MediaCondition[][] | null;
101
+ /** The cascade layer the rule is in, as a style rule's. */
102
+ layer: readonly number[] | null;
103
+ }
104
+
105
+ /** One frame of a `@keyframes`: a block of declarations at the offsets its
106
+ * selector names. */
107
+ export interface Keyframe {
108
+ /** Where in an iteration the frame is, 0 to 1: one per selector in its
109
+ * list, `from` 0 and `to` 1. */
110
+ offsets: number[];
111
+ /** What the frame sets: no `!important` one, which a frame ignores, and
112
+ * none of the animation's or a transition's own properties. */
113
+ declarations: Declaration[];
114
+ /** The `animation-timing-function` the frame eases to the next one by,
115
+ * as written; null where it gives none and the animation's is used. */
116
+ easing: string | null;
80
117
  }
81
118
 
82
119
  /**
@@ -322,9 +359,22 @@ export function parseStylesheet(
322
359
  prelude: at.prelude,
323
360
  declarations: parseDeclarations(at.block),
324
361
  });
362
+ } else if (
363
+ (name === 'keyframes' || name === '-webkit-keyframes') &&
364
+ at.block !== null
365
+ ) {
366
+ const keyframes = parseKeyframes(at.prelude, at.block);
367
+ if (keyframes) {
368
+ (sheet.keyframes ??= []).push({
369
+ ...keyframes,
370
+ prefixed: name !== 'keyframes',
371
+ media,
372
+ layer,
373
+ });
374
+ }
325
375
  }
326
- // @keyframes, @page: nothing to do, and the block was already
327
- // consumed.
376
+ // @page, and an at-rule nobody knows: nothing to do, and the block
377
+ // was already consumed.
328
378
  continue;
329
379
  }
330
380
 
@@ -1764,6 +1814,105 @@ const GENERIC_FAMILIES = new Set([
1764
1814
  'default',
1765
1815
  ]);
1766
1816
 
1817
+ /** The properties a keyframe does not set: the animation's own, and a
1818
+ * transition's (CSS Animations 1, 3). */
1819
+ const NOT_IN_KEYFRAMES = /^(?:-webkit-)?(?:animation|transition)(?:-|$)/;
1820
+
1821
+ /** A `<number-token>` percentage, `+12.5%` or `1e2%`. */
1822
+ const KEYFRAME_PERCENT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?%$/i;
1823
+
1824
+ /**
1825
+ * A `@keyframes` block's name and frames (CSS Animations 1, 3), or null
1826
+ * where its prelude is no name. A frame whose selector is not a list of
1827
+ * `from`, `to` and percentages from 0 to 100 is dropped whole, as a style
1828
+ * rule with a bad selector is, and the frames around it stand.
1829
+ */
1830
+ function parseKeyframes(
1831
+ prelude: string,
1832
+ block: string,
1833
+ ): { name: string; frames: Keyframe[] } | null {
1834
+ const name = keyframesName(prelude);
1835
+ if (name === null) return null;
1836
+ const frames: Keyframe[] = [];
1837
+ let i = 0;
1838
+ const n = block.length;
1839
+ while (i < n) {
1840
+ i = skipTrivia(block, i);
1841
+ if (i >= n) break;
1842
+ // a frame holds declarations and nothing else
1843
+ if (block[i] === '@' && startsIdent(block, i + 1)) {
1844
+ i = readAtRule(block, i).end;
1845
+ continue;
1846
+ }
1847
+ const braceAt = scanTo(block, i, '{');
1848
+ if (braceAt >= n) break;
1849
+ const selector = block.slice(i, braceAt);
1850
+ const body = readBlock(block, braceAt);
1851
+ i = body.end;
1852
+ const offsets = keyframeOffsets(selector);
1853
+ if (!offsets) continue;
1854
+ let easing: string | null = null;
1855
+ const declarations: Declaration[] = [];
1856
+ for (const d of parseDeclarations(body.body)) {
1857
+ if (d.important) continue;
1858
+ if (
1859
+ d.prop === 'animation-timing-function' ||
1860
+ d.prop === '-webkit-animation-timing-function'
1861
+ ) {
1862
+ easing = d.value;
1863
+ } else if (!NOT_IN_KEYFRAMES.test(d.prop)) {
1864
+ declarations.push(d);
1865
+ }
1866
+ }
1867
+ frames.push({ offsets, declarations, easing });
1868
+ }
1869
+ return { name, frames };
1870
+ }
1871
+
1872
+ /** A `@keyframes` prelude's name: a string, or an identifier that is not
1873
+ * `none` or a CSS-wide keyword; null for anything else. */
1874
+ function keyframesName(prelude: string): string | null {
1875
+ const v = prelude.trim();
1876
+ if (!v) return null;
1877
+ if (v[0] === '"' || v[0] === "'") {
1878
+ const name = unquote(v);
1879
+ return name !== v && name ? name : null;
1880
+ }
1881
+ if (!startsIdent(v, 0)) return null;
1882
+ const ident = readIdent(v, 0);
1883
+ if (ident.end !== v.length) return null;
1884
+ return KEYFRAMES_RESERVED.has(ident.value.toLowerCase()) ? null : ident.value;
1885
+ }
1886
+
1887
+ const KEYFRAMES_RESERVED = new Set([
1888
+ 'none',
1889
+ 'inherit',
1890
+ 'initial',
1891
+ 'unset',
1892
+ 'revert',
1893
+ 'revert-layer',
1894
+ 'default',
1895
+ ]);
1896
+
1897
+ /** A keyframe selector's offsets, 0 to 1, or null where any of its list is
1898
+ * not one. */
1899
+ function keyframeOffsets(selector: string): number[] | null {
1900
+ const out: number[] = [];
1901
+ for (const part of selector.split(',')) {
1902
+ const v = part.trim().toLowerCase();
1903
+ if (v === 'from') out.push(0);
1904
+ else if (v === 'to') out.push(1);
1905
+ else if (KEYFRAME_PERCENT.test(v)) {
1906
+ const pct = Number(v.slice(0, -1));
1907
+ if (!(pct >= 0 && pct <= 100)) return null;
1908
+ out.push(pct / 100);
1909
+ } else {
1910
+ return null;
1911
+ }
1912
+ }
1913
+ return out;
1914
+ }
1915
+
1767
1916
  /**
1768
1917
  * A `@font-face` block, or null when it names no family or no source — a
1769
1918
  * rule CSS Fonts 4 (4.3) drops whole. A rule whose sources are all
@@ -41,6 +41,13 @@ import type { ContentItem, CounterChange } from './content.js';
41
41
  import type { CustomProps } from './vars.js';
42
42
  import { parseRotate, parseScale, parseTransform } from './transform.js';
43
43
  import type { TransformFunction } from './transform.js';
44
+ import {
45
+ ANIMATION_LONGHANDS,
46
+ NO_ANIMATIONS,
47
+ animationLonghand,
48
+ parseAnimation,
49
+ } from './animation.js';
50
+ import type { Animations } from './animation.js';
44
51
  import {
45
52
  LIGHT_DARK,
46
53
  SYSTEM_COLOR,
@@ -590,6 +597,10 @@ export interface ComputedStyle {
590
597
  /** `transform-origin`: the point a transform turns and scales about,
591
598
  * from the border box's top left, a percentage of its width and height. */
592
599
  transformOrigin: [Len, Len];
600
+ /** `animation` and its longhands, each a list (`css/animation.ts`):
601
+ * `NO_ANIMATIONS`, shared, where nothing sets one. What they leave on
602
+ * the style is in its other fields already (`Cascade._computeStyle`). */
603
+ animations: Animations;
593
604
  zIndex: number | 'auto';
594
605
  /** A keyword, a length to raise the box by, or a percentage of its own
595
606
  * line height. */
@@ -1085,6 +1096,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
1085
1096
  scale: null,
1086
1097
  transform: null,
1087
1098
  transformOrigin: [{ pct: 50 }, { pct: 50 }],
1099
+ animations: NO_ANIMATIONS,
1088
1100
  zIndex: AUTO,
1089
1101
  verticalAlign: 'baseline',
1090
1102
 
@@ -1438,13 +1450,23 @@ const BORDER_WIDTH_KEYWORDS: Record<string, number> = {
1438
1450
  thick: 5,
1439
1451
  };
1440
1452
 
1441
- /** `-webkit-` names that are the multicol properties' own. */
1442
- const MULTICOL_ALIASES: Record<string, string> = {
1453
+ /** `-webkit-` names that are the multicol properties' own, and the
1454
+ * animation's. */
1455
+ const PREFIXED_ALIASES: Record<string, string> = {
1443
1456
  '-webkit-columns': 'columns',
1444
1457
  '-webkit-column-count': 'column-count',
1445
1458
  '-webkit-column-width': 'column-width',
1446
1459
  '-webkit-column-gap': 'column-gap',
1447
1460
  '-webkit-column-break-inside': 'break-inside',
1461
+ '-webkit-animation': 'animation',
1462
+ '-webkit-animation-name': 'animation-name',
1463
+ '-webkit-animation-duration': 'animation-duration',
1464
+ '-webkit-animation-timing-function': 'animation-timing-function',
1465
+ '-webkit-animation-delay': 'animation-delay',
1466
+ '-webkit-animation-iteration-count': 'animation-iteration-count',
1467
+ '-webkit-animation-direction': 'animation-direction',
1468
+ '-webkit-animation-fill-mode': 'animation-fill-mode',
1469
+ '-webkit-animation-play-state': 'animation-play-state',
1448
1470
  };
1449
1471
 
1450
1472
  /**
@@ -1463,11 +1485,11 @@ export function applyDeclaration(
1463
1485
  ctx: UnitContext,
1464
1486
  ): void {
1465
1487
  const written = prop.toLowerCase();
1466
- // the multicol properties under the names WebKit had them by, which
1467
- // Blink still reads, each an alias of the property (`-webkit-column-
1468
- // count` in its `css_properties.json5`): a page written for it alone
1469
- // names no other
1470
- const name = MULTICOL_ALIASES[written] ?? written;
1488
+ // the multicol and animation properties under the names WebKit had them
1489
+ // by, which Blink still reads, each an alias of the property (`-webkit-
1490
+ // column-count` in its `css_properties.json5`): a page written for it
1491
+ // alone names no other
1492
+ const name = PREFIXED_ALIASES[written] ?? written;
1471
1493
  let value = rawValue.trim();
1472
1494
  if (!value) return;
1473
1495
  // a logical property is the physical one it stands for, the CSS-wide
@@ -1906,6 +1928,27 @@ export function applyDeclaration(
1906
1928
  if (a !== null) style.opacity = a;
1907
1929
  return;
1908
1930
  }
1931
+ case 'animation': {
1932
+ const animations = parseAnimation(value);
1933
+ if (animations) style.animations = animations;
1934
+ return;
1935
+ }
1936
+ case 'animation-name':
1937
+ case 'animation-duration':
1938
+ case 'animation-timing-function':
1939
+ case 'animation-delay':
1940
+ case 'animation-iteration-count':
1941
+ case 'animation-direction':
1942
+ case 'animation-fill-mode':
1943
+ case 'animation-play-state': {
1944
+ const animations = animationLonghand(
1945
+ style.animations,
1946
+ ANIMATION_LONGHANDS[name],
1947
+ value,
1948
+ );
1949
+ if (animations) style.animations = animations;
1950
+ return;
1951
+ }
1909
1952
  case 'translate': {
1910
1953
  // `none`, or across, down, and a depth nothing here has
1911
1954
  if (value.trim().toLowerCase() === 'none') {
@@ -5646,6 +5689,14 @@ export function initialOne(
5646
5689
  style.mask = { ...style.mask, [list]: initial.mask[list] };
5647
5690
  return;
5648
5691
  }
5692
+ const animation = ANIMATION_LONGHANDS[PREFIXED_ALIASES[name] ?? name];
5693
+ if (animation) {
5694
+ style.animations = {
5695
+ ...style.animations,
5696
+ [animation]: initial.animations[animation],
5697
+ };
5698
+ return;
5699
+ }
5649
5700
  const keys = INHERIT_TARGETS[name];
5650
5701
  if (!keys) return;
5651
5702
  for (const key of keys) {
@@ -5663,6 +5714,15 @@ function inheritOne(
5663
5714
  style.mask = { ...style.mask, [list]: parent.mask[list] };
5664
5715
  return;
5665
5716
  }
5717
+ // each longhand of the animation is a list of its own, as the mask's are
5718
+ const animation = ANIMATION_LONGHANDS[PREFIXED_ALIASES[name] ?? name];
5719
+ if (animation) {
5720
+ style.animations = {
5721
+ ...style.animations,
5722
+ [animation]: parent.animations[animation],
5723
+ };
5724
+ return;
5725
+ }
5666
5726
  const keys = INHERIT_TARGETS[name];
5667
5727
  if (!keys) return;
5668
5728
  // A border colour left to `currentColor` inherits as the keyword and
@@ -5717,6 +5777,8 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
5717
5777
  'border-image': ['borderImage'],
5718
5778
  mask: ['mask'],
5719
5779
  '-webkit-mask': ['mask'],
5780
+ animation: ['animations'],
5781
+ '-webkit-animation': ['animations'],
5720
5782
  'border-image-source': ['borderImage'],
5721
5783
  'border-image-slice': ['borderImage'],
5722
5784
  'border-image-width': ['borderImage'],
@@ -211,12 +211,16 @@ col { display: table-column; }
211
211
  /* The form controls are real widgets rather than drawn boxes, so what the
212
212
  UA sheet owes them is a *box* of about the right size in the flow — the
213
213
  widget is painted into it by the component above. 'inline-block' is what
214
- makes a label and its input share a line. */
214
+ makes a label and its input share a line. Chrome's margins and alignment:
215
+ none round a field, a select, a button or a text area, each on its line's
216
+ baseline — a field's text on the text beside it (\`CONTROL_BASELINES\`) —
217
+ and a meter or a progress bar a fifth of an em under it. */
215
218
  input, button, select, textarea, meter, progress {
216
219
  display: inline-block;
217
- vertical-align: middle;
218
- margin: 3px 2px;
220
+ vertical-align: baseline;
221
+ margin: 0;
219
222
  }
223
+ meter, progress { vertical-align: -0.2em; }
220
224
  /* A control's text is a system font's, not its parent's: Chrome gives these
221
225
  four \`font: -webkit-small-control\`, Arial at the default size less 2pt
222
226
  (13.33px) whatever the text around it is set in, and Gecko \`-moz-field\`
@@ -231,8 +235,6 @@ input, button, select, textarea {
231
235
  font-size: ${look.controlFontSize ?? look.fontSize}px;
232
236
  }
233
237
  textarea { font-family: ${mono}; }
234
- /* Chrome's own UA margins for the checkables, near enough: they are the
235
- controls that sit hard against their label text otherwise. */
236
238
  /* HTML's rendering section (15.3.10): a control's text keeps none of the
237
239
  spacing, the line height, the case or the indent of the text around it.
238
240
  A button in a paragraph of 'line-height: 1.5' is its own font's line
@@ -258,8 +260,11 @@ input:focus-visible, textarea:focus-visible {
258
260
  outline: auto 1px -webkit-focus-ring-color;
259
261
  outline-offset: 0;
260
262
  }
261
- input[type=checkbox] { margin: 3px 4px 3px 4px; }
262
- input[type=radio] { margin: 3px 4px 3px 5px; }
263
+ /* Chrome's own margins for the checkables and a range, which sit on their
264
+ border box's bottom edge, the margin under the line */
265
+ input[type=checkbox] { margin: 3px 3px 3px 4px; }
266
+ input[type=radio] { margin: 3px 3px 0 5px; }
267
+ input[type=range] { margin: 2px; }
263
268
  input[type=hidden] { display: none; }
264
269
  /* A <button> is drawn rather than mounted: its content is the document's —
265
270
  an icon, a label in spans, a pill of the page's own design, which is what
@@ -280,7 +285,6 @@ button {
280
285
  cursor: pointer;
281
286
  }
282
287
  button[disabled] { color: ${look.mutedColor}; cursor: default; }
283
- textarea { vertical-align: top; }
284
288
  fieldset { margin: 0 2px; padding: 0.35em 0.75em 0.6em; border: 1px solid ${look.borderColor}; }
285
289
  legend { display: block; padding: 0 2px; }
286
290
  label { cursor: pointer; }
@@ -562,6 +562,40 @@ export type ReplacedKind =
562
562
  /** An inline `<svg>`: a drawing, sized by what it says of its size. */
563
563
  | 'svg';
564
564
 
565
+ /**
566
+ * Where a control sits on the line, as Chrome sets it: a field, a select
567
+ * or an input button on the baseline of the text it shows, its line in its
568
+ * face centred in its content box — that face's ascent and descent — and a
569
+ * checkbox, a radio button or a range on its border box's bottom, its
570
+ * bottom margin under the line. A `<textarea>` is none of these, and sits
571
+ * on its bottom margin edge as a box that scrolls does.
572
+ */
573
+ export const CONTROL_BASELINES = new WeakMap<
574
+ Box,
575
+ InlineDecoration | 'border'
576
+ >();
577
+
578
+ function controlBaseline(
579
+ el: Element,
580
+ kind: ReplacedKind,
581
+ style: ComputedStyle,
582
+ options: BuildOptions,
583
+ ): InlineDecoration | 'border' | null {
584
+ if (kind === 'checkbox' || kind === 'radio') return 'border';
585
+ if (kind === 'input' && (attr(el, 'type') ?? '').toLowerCase() === 'range') {
586
+ return 'border';
587
+ }
588
+ if (kind !== 'input' && kind !== 'select' && kind !== 'button') return null;
589
+ // a list box shows rows, and sits on its bottom edge
590
+ if (kind === 'select' && Number(attr(el, 'size') ?? 1) > 1) return null;
591
+ return (
592
+ options.faceExtent?.(style) ?? {
593
+ ascent: style.fontSize * 0.8,
594
+ descent: style.fontSize * 0.2,
595
+ }
596
+ );
597
+ }
598
+
565
599
  /** A replaced box's intrinsic dimensions, in device pixels. */
566
600
  export interface Intrinsic {
567
601
  /** Its size — or, on an axis `missing` names, the default object size of
@@ -853,6 +887,10 @@ export interface BuildOptions {
853
887
  * pixels: what a list marker's image with no size of its own is sized
854
888
  * by (`_markerImageSize`). Where it is not known, 0.8em. */
855
889
  faceAscent?(style: ComputedStyle): number | undefined;
890
+ /** How far a style's first face reaches above and below its baseline,
891
+ * in device pixels: where a control sets its text, which is where its
892
+ * baseline is (`CONTROL_BASELINES`). */
893
+ faceExtent?(style: ComputedStyle): InlineDecoration | undefined;
856
894
  /** The size a real widget wants, so the box in the flow is the size the
857
895
  * control will be drawn at. */
858
896
  controlSize(
@@ -1486,6 +1524,8 @@ class Builder {
1486
1524
  ratio: 0,
1487
1525
  };
1488
1526
  this._controls.push(box);
1527
+ const baseline = controlBaseline(el, replaced, style, this._options);
1528
+ if (baseline) CONTROL_BASELINES.set(box, baseline);
1489
1529
  // A control's value is the widget's, not the document's: putting it in
1490
1530
  // the selection index would make Ctrl+A copy the contents of every text
1491
1531
  // field, which no document viewer does.