@react-x11/components 0.16.1 → 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.
@@ -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'],
@@ -252,11 +252,7 @@ export function layoutFlex(
252
252
  const direction =
253
253
  box.style.direction === 'rtl' ? Y.DIRECTION_RTL : Y.DIRECTION_LTR;
254
254
  const row = box.style.flexDirection.startsWith('row');
255
- // Yoga aligns the lines of a box that wraps, or of a row that aligns by
256
- // baselines, in one pass of its own, which drops a margin
257
- // (`lineMarginFix`) — and not those of a column that does not wrap,
258
- // whatever it aligns by, whose margins Yoga keeps
259
- let baselines =
255
+ const baselines =
260
256
  row &&
261
257
  (box.style.alignItems === 'baseline' ||
262
258
  items.some(({ box: child }) => child.style.alignSelf === 'baseline'));
@@ -273,35 +269,41 @@ export function layoutFlex(
273
269
  if (aligned) node.setAlignSelf(ALIGN[aligned]);
274
270
  }
275
271
  } else if (baselines) {
276
- // Yoga takes a row that does not wrap through that pass as well, and
277
- // sets its line there as tall as its items and not the row: an item
278
- // centred or at the end of a row of a height was centred or ended in
279
- // the items' height. It has sized that line already, as the most any
280
- // item reaches down to its bottom edge — a baseline, to Yoga — and the
281
- // largest bottom margin besides, whichever items those are: a row of
282
- // no height of its own was as tall as both. And it sets an item with
283
- // an `auto` margin by its baseline: at the top, where the margin puts
284
- // it at the bottom, and every item beside it as far down as its height
285
- // reached. That item takes no part in aligning the line by baselines
286
- // (9.4 step 8), and Yoga is told the alignment its margins come to.
287
- // With fewer than two items left that do take part, they come to their
288
- // line's start (8.3), which Yoga is told as well: then nothing in the
289
- // row is aligned by its baseline, Yoga takes no such pass, and the line
290
- // is the row. Two or more are lined up by their baselines once those
291
- // are known (`baselineLines`), and the row laid out again with none.
272
+ // Yoga takes a row aligned by baselines through that pass whether it
273
+ // wraps or not, and sets an item with an `auto` margin there by its
274
+ // baseline: at the top, where the margin puts it at the bottom, and
275
+ // every item beside it as far down as its height reached. That item
276
+ // takes no part in aligning the line by baselines (9.4 step 8), and
277
+ // Yoga is told the alignment its margins come to.
292
278
  for (const { box: child, node } of items) {
293
279
  const aligned = autoAcross(child.style, false);
294
280
  if (aligned) node.setAlignSelf(ALIGN[aligned]);
295
281
  }
296
- const aligned = items.filter(({ box: child }) =>
297
- byBaseline(box.style, child.style),
298
- );
299
- if (aligned.length < 2) {
300
- for (const { node } of aligned) node.setAlignSelf(Y.ALIGN_FLEX_START);
301
- if (box.style.alignItems === 'baseline') {
302
- root.setAlignItems(Y.ALIGN_FLEX_START);
303
- }
304
- baselines = false;
282
+ }
283
+ // Items aligned by their baselines, which Yoga cannot see, are told they
284
+ // are at their lines' starts, which is where one is that shares its line
285
+ // with no other (8.3); those that do share one are lined up by their
286
+ // baselines once those are known (`baselineLines`), and the row laid out
287
+ // again. Told an item is aligned by its baseline, Yoga sizes each line
288
+ // of the row as the most any item on it reaches down to its bottom edge
289
+ // — a baseline, to Yoga — and the largest bottom margin besides,
290
+ // whichever items those are, and stretches the items on the line to
291
+ // that: a row of no height of its own was as tall as both, and
292
+ // `align-content` had that much less room to share out. And it takes a
293
+ // row that does not wrap through its pass for lines, which sets the one
294
+ // line there as tall as its items and not the row: an item centred or at
295
+ // the end of a row of a height was centred or ended in the items'
296
+ // height. Told none is, it takes neither. A row whose lines wrap in
297
+ // reverse is left to Yoga.
298
+ const started = new Set<Box>();
299
+ if (baselines && box.style.flexWrap !== 'wrap-reverse') {
300
+ for (const { box: child, node } of items) {
301
+ if (!byBaseline(box.style, child.style)) continue;
302
+ node.setAlignSelf(Y.ALIGN_FLEX_START);
303
+ started.add(child);
304
+ }
305
+ if (box.style.alignItems === 'baseline') {
306
+ root.setAlignItems(Y.ALIGN_FLEX_START);
305
307
  }
306
308
  }
307
309
  // frozen at its least size on the one line of a box that does not
@@ -449,8 +451,11 @@ export function layoutFlex(
449
451
  }
450
452
  }
451
453
 
452
- // whether Yoga's last layout took its pass for lines (`placeItems`)
453
- let lined = box.style.flexWrap !== 'nowrap' || baselines;
454
+ // Yoga aligns the lines of a box that wraps in one pass of its own, which
455
+ // drops a margin (`lineMarginFix`), and keeps the margins of one that
456
+ // does not, which it takes through that pass only where it is told an
457
+ // item is aligned by its baseline: here, never
458
+ const lined = box.style.flexWrap !== 'nowrap';
454
459
  let bottom = placeItems(
455
460
  box,
456
461
  ctx,
@@ -459,31 +464,20 @@ export function layoutFlex(
459
464
  own,
460
465
  lined,
461
466
  contentWidth,
462
- undefined,
467
+ started,
463
468
  crossed ? crossLines(box.style, items, contentWidth, columnGap) : undefined,
464
469
  );
465
- // Items aligned by their baselines, which Yoga cannot see, are aligned
466
- // with their baselines known (`baselineLines`)
467
- // by the flex layout again, set at their lines' starts with the margins
468
- // that put their baselines together, which makes each line as tall as
469
- // that does
470
- const lines = baselines ? baselineLines(box.style, items) : null;
471
- const started = new Set<Box>();
470
+ // Items that share a line and are aligned by their baselines are laid
471
+ // out by the flex layout again, set at their lines' starts with the
472
+ // margins that put their baselines together (`baselineLines`), which
473
+ // makes each line as tall as that does
474
+ const lines = started.size > 1 ? baselineLines(box.style, items) : null;
472
475
  if (lines) {
473
- // every one of them, and the box's own `baseline` too, which is what
474
- // takes Yoga down that pass
475
476
  for (const line of lines) {
476
- line.forEach(({ box: child, node }, i) => {
477
- node.setAlignSelf(Y.ALIGN_FLEX_START);
477
+ line.forEach(({ node }, i) => {
478
478
  node.setMargin(Y.EDGE_TOP, line.margins[i]);
479
- started.add(child);
480
479
  });
481
480
  }
482
- if (box.style.alignItems === 'baseline') {
483
- root.setAlignItems(Y.ALIGN_FLEX_START);
484
- }
485
- // so it takes it only where the row wraps
486
- lined = box.style.flexWrap !== 'nowrap';
487
481
  ctx.flexDepth = depth + 1;
488
482
  try {
489
483
  calculate();
@@ -716,6 +710,12 @@ function placeItems(
716
710
  let bottom = 0;
717
711
  // the lines of a row that wraps in reverse are turned round to run up it
718
712
  const reverse = row && box.style.flexWrap === 'wrap-reverse';
713
+ // the left edge a column's items aligned by baselines share, where it
714
+ // does not wrap (`baselineEdge`); a column that wraps is `crossed`
715
+ const edge =
716
+ row || box.style.flexWrap !== 'nowrap'
717
+ ? NaN
718
+ : baselineEdge(box.style, items);
719
719
  for (const { box: child, node, laid } of items) {
720
720
  const placed = crossed?.get(child);
721
721
  const across =
@@ -725,7 +725,11 @@ function placeItems(
725
725
  const itemHeight = row ? heightOf(node, laid) : laid.main;
726
726
  const left =
727
727
  box.contentX +
728
- (placed ? placed.x : node.getComputedLeft() + (row ? 0 : across));
728
+ (placed
729
+ ? placed.x
730
+ : !Number.isNaN(edge) && byBaseline(box.style, child.style, false)
731
+ ? edge
732
+ : node.getComputedLeft() + (row ? 0 : across));
729
733
  // which Yoga turns an item round in by the height it laid it out at, and
730
734
  // not the one it is (`unspace`)
731
735
  const top =
@@ -2868,17 +2872,18 @@ function contentBottom(box: Box): number {
2868
2872
  * its bottom edge, so it lined them up by their bottoms. With the items laid
2869
2873
  * out, each line's, and for each the top margin that sets it at the line's
2870
2874
  * start with its first baseline where the lowest-reaching one's is — or
2871
- * null where no line has two to align.
2875
+ * null where no line has two to align. Not for a row whose lines wrap in
2876
+ * reverse, which grow from their ends and are left to Yoga (`layoutFlex`).
2872
2877
  */
2873
- function baselineLines<T extends { box: Box }>(
2878
+ function baselineLines<T extends { box: Box; node: YogaNode }>(
2874
2879
  style: ComputedStyle,
2875
2880
  items: readonly T[],
2876
2881
  ): (T[] & { margins: number[] })[] | null {
2877
- // a line that grows from its end is aligned from there, and left to Yoga
2878
- if (style.flexWrap === 'wrap-reverse') return null;
2879
2882
  const wraps = style.flexWrap !== 'nowrap';
2880
- // along a line the items go the way the main axis does, and the next
2881
- // line starts where one does not
2883
+ // along a line the items go the way the main axis does, each margin box
2884
+ // after the last one's end, and the next line starts before it (9.3): at
2885
+ // the line's start, where an item with a margin before it was further
2886
+ // along than one alone on the line before
2882
2887
  const back =
2883
2888
  (style.flexDirection === 'row-reverse') !== (style.direction === 'rtl');
2884
2889
  const out: (T[] & { margins: number[] })[] = [];
@@ -2900,12 +2905,21 @@ function baselineLines<T extends { box: Box }>(
2900
2905
  out.push(Object.assign(line, { margins }));
2901
2906
  };
2902
2907
  for (const item of items) {
2903
- const box = item.box;
2904
- if (wraps && line.length && (back ? box.x >= last : box.x <= last)) {
2908
+ const { box, node } = item;
2909
+ const left = node.getComputedLeft() - node.getComputedMargin(Y.EDGE_LEFT);
2910
+ const right =
2911
+ node.getComputedLeft() +
2912
+ node.getComputedWidth() +
2913
+ node.getComputedMargin(Y.EDGE_RIGHT);
2914
+ if (
2915
+ wraps &&
2916
+ line.length &&
2917
+ (back ? right > last + 0.01 : left < last - 0.01)
2918
+ ) {
2905
2919
  close();
2906
2920
  line = [];
2907
2921
  }
2908
- last = box.x;
2922
+ last = back ? left : right;
2909
2923
  if (byBaseline(style, box.style)) line.push(item);
2910
2924
  }
2911
2925
  close();
@@ -2916,16 +2930,51 @@ function baselineLines<T extends { box: Box }>(
2916
2930
  /** Whether an item takes part in aligning its line by baselines (CSS
2917
2931
  * Flexbox 8.3, 9.4 step 8): aligned by its baseline, with no `auto`
2918
2932
  * margin across the line, which takes it over its alignment. */
2919
- function byBaseline(container: ComputedStyle, item: ComputedStyle): boolean {
2933
+ function byBaseline(
2934
+ container: ComputedStyle,
2935
+ item: ComputedStyle,
2936
+ /** Whether the line is a row's, across which its margins are its top
2937
+ * and bottom ones; a column's are its left and right ones. */
2938
+ row = true,
2939
+ ): boolean {
2920
2940
  const aligned =
2921
2941
  item.alignSelf === AUTO ? container.alignItems : item.alignSelf;
2922
2942
  return (
2923
2943
  aligned === 'baseline' &&
2924
- item.marginTop !== AUTO &&
2925
- item.marginBottom !== AUTO
2944
+ (row ? item.marginTop : item.marginLeft) !== AUTO &&
2945
+ (row ? item.marginBottom : item.marginRight) !== AUTO
2926
2946
  );
2927
2947
  }
2928
2948
 
2949
+ /**
2950
+ * Where the items a column that does not wrap aligns by baselines have
2951
+ * their left edges, from its content box's: NaN where it has none.
2952
+ *
2953
+ * A horizontal item has no baseline across a column, and one is made from
2954
+ * its border box — the line-under edge of the vertical writing mode it is
2955
+ * taken to have, its left, whichever way its text runs (CSS Box Alignment
2956
+ * 3, 9.1). So the items share their left edges, and are set together at
2957
+ * the line's cross start, the column's left or, where it runs right to
2958
+ * left, its right (9.3; CSS Flexbox 8.3), as Chrome sets them: as far from
2959
+ * there as the most any of them reaches past its edge on that side. Yoga
2960
+ * never aligns a column by baselines, and sets each at its line's start
2961
+ * with its margin there, so that is the furthest any of them is from the
2962
+ * start: the rightmost left edge, or from the right the leftmost.
2963
+ */
2964
+ function baselineEdge(
2965
+ container: ComputedStyle,
2966
+ items: readonly { box: Box; node: YogaNode }[],
2967
+ ): number {
2968
+ const rtl = container.direction === 'rtl';
2969
+ let edge = NaN;
2970
+ for (const { box, node } of items) {
2971
+ if (!byBaseline(container, box.style, false)) continue;
2972
+ const left = node.getComputedLeft();
2973
+ if (!(rtl ? left >= edge : left <= edge)) edge = left;
2974
+ }
2975
+ return edge;
2976
+ }
2977
+
2929
2978
  /**
2930
2979
  * Whether Yoga made a line of a row that wraps the row's height, where its
2931
2980
  * items make it another (CSS Flexbox 9.4, steps 7 and 8; `layoutFlex`).
@@ -3035,13 +3084,13 @@ function autoAcross(
3035
3084
 
3036
3085
  /**
3037
3086
  * How far an item is from where it belongs across its line, where Yoga
3038
- * (3.2.1) aligned the lines in the pass it takes for a box that wraps or
3039
- * aligns by baselines: it sets an item aligned to its line's start there
3040
- * as though it had no margin at that side, and one centred as though it had
3041
- * none at either — `items-start` in a wrapping row put every card's top
3042
- * margin under it. By the margin at the start of the line's cross axis, and
3043
- * by half what it exceeds the one at the end by, as Yoga holds them: for an
3044
- * item set at its line's start (`started`), its baseline's.
3087
+ * (3.2.1) aligned the lines in the pass it takes for a box that wraps: it
3088
+ * sets an item aligned to its line's start there as though it had no
3089
+ * margin at that side, and one centred as though it had none at either —
3090
+ * `items-start` in a wrapping row put every card's top margin under it. By
3091
+ * the margin at the start of the line's cross axis, and by half what it
3092
+ * exceeds the one at the end by, as Yoga holds them: for an item set at
3093
+ * its line's start (`started`), its baseline's.
3045
3094
  *
3046
3095
  * A row whose lines wrap in reverse Yoga lays out as though they wrapped
3047
3096
  * forward, and then turns each item over across the box, its margins with
@@ -3050,9 +3099,7 @@ function autoAcross(
3050
3099
  * stretched, is its bottom margin down from its line's top where it is its
3051
3100
  * top one; and one centred, half the difference. A column's, whose cross
3052
3101
  * axis runs right to left, or that wraps in reverse, is left as Yoga has
3053
- * it, and so is an item an `auto` margin aligns in a row that does not
3054
- * wrap: where Yoga still takes that pass for such a row, its items are
3055
- * laid out again without it, aligned by their baselines (`layoutFlex`).
3102
+ * it, and so is an item an `auto` margin aligns across a column.
3056
3103
  */
3057
3104
  function lineMarginFix(
3058
3105
  container: ComputedStyle,
@@ -3073,10 +3120,7 @@ function lineMarginFix(
3073
3120
  let align: string | null;
3074
3121
  if (started) align = 'flex-start';
3075
3122
  else if (lead === AUTO || trail === AUTO) {
3076
- align =
3077
- row && container.flexWrap !== 'nowrap'
3078
- ? autoAcross(style, reverse)
3079
- : null;
3123
+ align = row ? autoAcross(style, reverse) : null;
3080
3124
  } else {
3081
3125
  align = style.alignSelf === AUTO ? container.alignItems : style.alignSelf;
3082
3126
  // which sets one whose height is a percentage of none at its line's
@@ -3446,9 +3490,10 @@ interface Across {
3446
3490
  * item with an `auto` margin across its line takes the room the line has
3447
3491
  * past it on that side (8.1), and none where there is none; a stretched
3448
3492
  * item is as wide as its line less its margins, within its limits (9.4,
3449
- * step 11), and as tall as the column made it; and the rest are where
3450
- * `align-self` says, a baseline at the line's start, as a column has none
3451
- * across it to align by.
3493
+ * step 11), and as tall as the column made it; those aligned by baselines
3494
+ * share their left edges, set together at the line's start, where a column
3495
+ * that does not wrap sets them (`baselineEdge`); and the rest are where
3496
+ * `align-self` says.
3452
3497
  */
3453
3498
  function crossLines(
3454
3499
  style: ComputedStyle,
@@ -3482,9 +3527,25 @@ function crossLines(
3482
3527
  lead.push(fromRight ? r : l);
3483
3528
  trail.push(fromRight ? l : r);
3484
3529
  });
3485
- const across = lines.map((line) =>
3486
- line.reduce((most, i) => Math.max(most, sizes[i] + lead[i] + trail[i]), 0),
3487
- );
3530
+ // A line is as wide as its widest item, margins and all, and as the
3531
+ // items it aligns by baselines come to, sharing their left edges: the
3532
+ // most any reaches from the line's start to that edge, and the most any
3533
+ // reaches past it (`baselineEdge`; 9.4 step 8). `reach` is each line's
3534
+ // first, where those items' left edges are from its start.
3535
+ const reach: number[] = [];
3536
+ const across = lines.map((line) => {
3537
+ let most = 0;
3538
+ let before = -Infinity;
3539
+ let after = -Infinity;
3540
+ for (const i of line) {
3541
+ most = Math.max(most, sizes[i] + lead[i] + trail[i]);
3542
+ if (!byBaseline(style, items[i].box.style, false)) continue;
3543
+ before = Math.max(before, lead[i] + (fromRight ? sizes[i] : 0));
3544
+ after = Math.max(after, trail[i] + (fromRight ? 0 : sizes[i]));
3545
+ }
3546
+ reach.push(before);
3547
+ return Math.max(most, before + after);
3548
+ });
3488
3549
  const count = lines.length;
3489
3550
  let room = width - gap * (count - 1);
3490
3551
  for (const size of across) room -= size;
@@ -3542,6 +3603,8 @@ function crossLines(
3542
3603
  if (itemWidth > most) itemWidth = most;
3543
3604
  if (itemWidth < least) itemWidth = least;
3544
3605
  itemWidth = Math.max(itemWidth, box.horizontalExtra);
3606
+ } else if (byBaseline(style, own, false)) {
3607
+ offset = reach[k] - (fromRight ? itemWidth : 0);
3545
3608
  } else {
3546
3609
  const align = own.alignSelf === AUTO ? style.alignItems : own.alignSelf;
3547
3610
  if (align === 'center') offset += free / 2;