@react-x11/components 0.14.0 → 0.14.1

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 (51) hide show
  1. package/dist/html/css/cascade.d.ts +42 -1
  2. package/dist/html/css/cascade.d.ts.map +1 -1
  3. package/dist/html/css/cascade.js +310 -12
  4. package/dist/html/css/cascade.js.map +1 -1
  5. package/dist/html/css/shapes.d.ts +44 -0
  6. package/dist/html/css/shapes.d.ts.map +1 -0
  7. package/dist/html/css/shapes.js +194 -0
  8. package/dist/html/css/shapes.js.map +1 -0
  9. package/dist/html/css/style.d.ts.map +1 -1
  10. package/dist/html/css/style.js +1 -15
  11. package/dist/html/css/style.js.map +1 -1
  12. package/dist/html/layout/block.d.ts +16 -5
  13. package/dist/html/layout/block.d.ts.map +1 -1
  14. package/dist/html/layout/block.js +26 -5
  15. package/dist/html/layout/block.js.map +1 -1
  16. package/dist/html/layout/boxes.d.ts +13 -0
  17. package/dist/html/layout/boxes.d.ts.map +1 -1
  18. package/dist/html/layout/boxes.js +18 -0
  19. package/dist/html/layout/boxes.js.map +1 -1
  20. package/dist/html/layout/css-grid.d.ts.map +1 -1
  21. package/dist/html/layout/css-grid.js +58 -10
  22. package/dist/html/layout/css-grid.js.map +1 -1
  23. package/dist/html/layout/flex.js +19 -6
  24. package/dist/html/layout/flex.js.map +1 -1
  25. package/dist/html/layout/table.d.ts.map +1 -1
  26. package/dist/html/layout/table.js +38 -16
  27. package/dist/html/layout/table.js.map +1 -1
  28. package/dist/html/node.d.ts +1 -0
  29. package/dist/html/node.d.ts.map +1 -1
  30. package/dist/html/node.js +57 -12
  31. package/dist/html/node.js.map +1 -1
  32. package/dist/html/paint.d.ts +4 -1
  33. package/dist/html/paint.d.ts.map +1 -1
  34. package/dist/html/paint.js +269 -84
  35. package/dist/html/paint.js.map +1 -1
  36. package/dist/html/svg.d.ts +27 -10
  37. package/dist/html/svg.d.ts.map +1 -1
  38. package/dist/html/svg.js +259 -46
  39. package/dist/html/svg.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/html/css/cascade.ts +343 -8
  42. package/src/html/css/shapes.ts +239 -0
  43. package/src/html/css/style.ts +1 -13
  44. package/src/html/layout/block.ts +27 -5
  45. package/src/html/layout/boxes.ts +30 -0
  46. package/src/html/layout/css-grid.ts +77 -13
  47. package/src/html/layout/flex.ts +22 -9
  48. package/src/html/layout/table.ts +45 -18
  49. package/src/html/node.ts +54 -11
  50. package/src/html/paint.ts +379 -90
  51. package/src/html/svg.ts +318 -39
@@ -0,0 +1,239 @@
1
+ // What a style sheet says of the shapes in a drawing.
2
+ //
3
+ // An inline `<svg>` is a replaced box: the box builder stops at it, and no
4
+ // computed style is made for what is inside. `fill`, `stroke` and their kin
5
+ // are properties all the same (SVG 2, 6.2 and 13.2), which a rule of the
6
+ // document's sets on a `<path>` as it would on a paragraph — a logo's
7
+ // `.dark .logo path { fill: #fff }` — and a drawing exported with a style
8
+ // sheet of its own, `.st0 { fill: #fff }` on `<path class="st0">`, has all
9
+ // its colours in one. So the cascade is run for them narrowly
10
+ // (`Cascade.shapeStyles`): over the rules that declare one of the
11
+ // properties here, and down to values `SvgView` reads from a `style`
12
+ // attribute, which is where the drawing's copy of an element carries them
13
+ // (`svg.ts`). A `ComputedStyle` per `<path>` would be a few hundred
14
+ // properties for the dozen a shape has, on a page of hundreds of icons.
15
+ import type { Element } from 'domhandler';
16
+ import { LIGHT_DARK, lightDark } from './color.js';
17
+ import { inkColor, parseAlpha, parseColor, parseLength } from './values.js';
18
+ import type { UnitContext } from './values.js';
19
+
20
+ /** What the rules give one element of a drawing: each property they set,
21
+ * as `SvgView` reads it — `inherit` for one they put back to what the
22
+ * element inherits, over its own attribute. */
23
+ export type ShapeStyle = Readonly<Record<string, string>>;
24
+
25
+ /** What the rules give the elements of one drawing, and all of it as a
26
+ * string: what a drawing's tree was last made with (`SvgDrawing`). */
27
+ export interface ShapeStyles {
28
+ readonly of: ReadonlyMap<Element, ShapeStyle>;
29
+ readonly key: string;
30
+ }
31
+
32
+ /** Each property read, and whether it is inherited. */
33
+ const PROPS: Record<string, boolean> = {
34
+ fill: true,
35
+ stroke: true,
36
+ 'stroke-width': true,
37
+ 'stroke-linecap': true,
38
+ 'stroke-linejoin': true,
39
+ 'stroke-miterlimit': true,
40
+ 'fill-rule': true,
41
+ 'fill-opacity': true,
42
+ 'stroke-opacity': true,
43
+ color: true,
44
+ visibility: true,
45
+ opacity: false,
46
+ display: false,
47
+ 'stop-color': false,
48
+ 'stop-opacity': false,
49
+ };
50
+
51
+ /** Whether a rule with this declaration is one to keep for drawings. */
52
+ export function isShapeProp(prop: string): boolean {
53
+ return PROPS[prop] !== undefined;
54
+ }
55
+
56
+ /** The properties the drawing's box has from its computed style already:
57
+ * its paint, which `paintSvg` hands the drawing, and what makes it a box,
58
+ * shown and see-through as one. */
59
+ export const ROOT_BOX_PROPS: ReadonlySet<string> = new Set([
60
+ 'fill',
61
+ 'stroke',
62
+ 'color',
63
+ 'display',
64
+ 'visibility',
65
+ 'opacity',
66
+ ]);
67
+
68
+ /** Each at its initial value (SVG 2, 13; CSS Color 4; CSS Display 3). */
69
+ const INITIAL: Record<string, string> = {
70
+ fill: '#000000',
71
+ stroke: 'none',
72
+ 'stroke-width': '1',
73
+ 'stroke-linecap': 'butt',
74
+ 'stroke-linejoin': 'miter',
75
+ 'stroke-miterlimit': '4',
76
+ 'fill-rule': 'nonzero',
77
+ 'fill-opacity': '1',
78
+ 'stroke-opacity': '1',
79
+ visibility: 'visible',
80
+ opacity: '1',
81
+ display: 'inline',
82
+ 'stop-color': '#000000',
83
+ 'stop-opacity': '1',
84
+ };
85
+
86
+ /** The elements a rule's subject can be, where it names one by its type,
87
+ * for the rule to reach into a drawing: the ones `SvgView` draws, or
88
+ * reads a paint from. `a` is both languages'. */
89
+ export const SHAPE_TAGS: ReadonlySet<string> = new Set([
90
+ 'svg',
91
+ 'g',
92
+ 'a',
93
+ 'path',
94
+ 'rect',
95
+ 'circle',
96
+ 'ellipse',
97
+ 'line',
98
+ 'polyline',
99
+ 'polygon',
100
+ 'text',
101
+ 'tspan',
102
+ 'use',
103
+ 'symbol',
104
+ 'switch',
105
+ 'image',
106
+ 'stop',
107
+ ]);
108
+
109
+ /** What a shape's values are read against: the drawing's own `color` and
110
+ * colour scheme, and its font size for a length in `em`. */
111
+ export interface ShapeContext {
112
+ color: string;
113
+ scheme: 'light' | 'dark';
114
+ units: UnitContext;
115
+ }
116
+
117
+ /**
118
+ * A `fill` or a `stroke` (SVG 2, 13.2): `none`, a colour, or a `url()`
119
+ * naming a gradient in the drawing, with what follows it, its fallback,
120
+ * left off. Null for anything else, `context-fill` and `context-stroke`
121
+ * among it, which only a marker or a `<use>` has a context for.
122
+ */
123
+ export function svgPaint(value: string): string | null {
124
+ const v = value.trim();
125
+ if (v.toLowerCase() === 'none') return 'none';
126
+ const url = /^url\(\s*(['"]?)(#[^'")\s]+)\1\s*\)/i.exec(v);
127
+ if (url) return `url(${url[2]})`;
128
+ return parseColor(v);
129
+ }
130
+
131
+ const NUMBER = /^[+-]?(?:\d*\.\d+|\d+)(?:e[+-]?\d+)?$/i;
132
+
133
+ /** A word of a `display` that makes a box (CSS Display 3, 2). */
134
+ const DISPLAY =
135
+ /^(?:block|inline|run-in|flow|flow-root|table|flex|grid|ruby|list-item|contents|inline-[a-z]+|table-[a-z-]+|ruby-[a-z-]+|-webkit-[a-z-]+)$/;
136
+
137
+ const LINE_JOINS: Record<string, string> = {
138
+ miter: 'miter',
139
+ round: 'round',
140
+ bevel: 'bevel',
141
+ // the two a canvas has no join for end in a miter, clipped or not
142
+ 'miter-clip': 'miter',
143
+ arcs: 'miter',
144
+ };
145
+
146
+ /**
147
+ * One declaration's value as a drawing reads it, or null for a value the
148
+ * property does not take, which leaves the declaration out of the cascade
149
+ * as any invalid one is left. `inherit` for a CSS-wide keyword that puts an
150
+ * inherited property back to what the element inherits.
151
+ */
152
+ export function shapeValue(
153
+ prop: string,
154
+ raw: string,
155
+ ctx: ShapeContext,
156
+ ): string | null {
157
+ let value = raw.trim();
158
+ const wide = value.toLowerCase();
159
+ if (wide === 'inherit') return PROPS[prop] ? 'inherit' : null;
160
+ if (wide === 'initial') {
161
+ return prop === 'color'
162
+ ? (ctx.units.initial?.color ?? '#000000')
163
+ : INITIAL[prop];
164
+ }
165
+ if (wide === 'unset' || wide === 'revert' || wide === 'revert-layer') {
166
+ return PROPS[prop] ? 'inherit' : INITIAL[prop];
167
+ }
168
+ if (LIGHT_DARK.test(value)) {
169
+ const picked = lightDark(value, ctx.scheme);
170
+ if (picked === null) return null;
171
+ value = picked;
172
+ }
173
+ switch (prop) {
174
+ case 'fill':
175
+ case 'stroke': {
176
+ const paint = svgPaint(value);
177
+ // `currentColor` is the shape's own `color`, which the drawing knows
178
+ return paint === null || paint === 'currentColor'
179
+ ? paint
180
+ : inkColor(paint, ctx.color);
181
+ }
182
+ case 'color':
183
+ case 'stop-color': {
184
+ const color = parseColor(value);
185
+ return color === null ? null : inkColor(color, ctx.color);
186
+ }
187
+ case 'stroke-width': {
188
+ // a number is user units, as a length in pixels is; a percentage is
189
+ // of the viewport's diagonal, which only the drawing has
190
+ const px = NUMBER.test(value)
191
+ ? Number(value)
192
+ : lengthIn(value, ctx.units);
193
+ return px !== null && px >= 0 ? String(px) : null;
194
+ }
195
+ case 'stroke-miterlimit': {
196
+ const n = NUMBER.test(value) ? Number(value) : NaN;
197
+ return n >= 1 ? String(n) : null;
198
+ }
199
+ case 'fill-opacity':
200
+ case 'stroke-opacity':
201
+ case 'stop-opacity':
202
+ case 'opacity': {
203
+ const a = parseAlpha(value);
204
+ return a === null ? null : String(a);
205
+ }
206
+ case 'stroke-linecap': {
207
+ const v = value.toLowerCase();
208
+ return v === 'butt' || v === 'round' || v === 'square' ? v : null;
209
+ }
210
+ case 'stroke-linejoin':
211
+ return LINE_JOINS[value.toLowerCase()] ?? null;
212
+ case 'fill-rule': {
213
+ const v = value.toLowerCase();
214
+ return v === 'nonzero' || v === 'evenodd' ? v : null;
215
+ }
216
+ case 'visibility': {
217
+ const v = value.toLowerCase();
218
+ if (v === 'visible') return v;
219
+ return v === 'hidden' || v === 'collapse' ? 'hidden' : null;
220
+ }
221
+ case 'display': {
222
+ // `none`, or some box: which one is nothing to a shape
223
+ const v = value.toLowerCase();
224
+ if (v === 'none') return v;
225
+ return v.split(/\s+/).every((word) => DISPLAY.test(word))
226
+ ? 'inline'
227
+ : null;
228
+ }
229
+ default:
230
+ return null;
231
+ }
232
+ }
233
+
234
+ /** A length in user units, which are CSS pixels; null for none, and for a
235
+ * percentage. */
236
+ function lengthIn(value: string, units: UnitContext): number | null {
237
+ const length = parseLength(value, units);
238
+ return typeof length === 'number' ? length / units.scale : null;
239
+ }
@@ -43,6 +43,7 @@ import type { CustomProps } from './vars.js';
43
43
  import { parseRotate, parseScale, parseTransform } from './transform.js';
44
44
  import type { TransformFunction } from './transform.js';
45
45
  import { LIGHT_DARK, lightDark, usedColorScheme } from './color.js';
46
+ import { svgPaint } from './shapes.js';
46
47
 
47
48
  export type Display =
48
49
  | 'none'
@@ -5154,19 +5155,6 @@ const POINTER_EVENTS = new Set([
5154
5155
  'stroke',
5155
5156
  'all',
5156
5157
  ]);
5157
- /**
5158
- * A `fill` or a `stroke` (SVG 2, 13.2): `none`, a colour, or a `url()`
5159
- * naming a gradient in the drawing, with what follows it, its fallback,
5160
- * left off. Null for anything else, `context-fill` and `context-stroke`
5161
- * among it, which only a marker or a `<use>` has a context for.
5162
- */
5163
- function svgPaint(value: string): string | null {
5164
- const v = value.trim();
5165
- if (v.toLowerCase() === 'none') return 'none';
5166
- const url = /^url\(\s*(['"]?)(#[^'")\s]+)\1\s*\)/i.exec(v);
5167
- if (url) return `url(${url[2]})`;
5168
- return parseColor(v);
5169
- }
5170
5158
 
5171
5159
  const OBJECT_FITS = new Set(['fill', 'contain', 'cover', 'none', 'scale-down']);
5172
5160
 
@@ -2324,8 +2324,9 @@ function stretchHeight(box: Box): number | null {
2324
2324
  * stretched item's, a flexed one's in a container of a definite height (CSS
2325
2325
  * Flexbox 9.8), a grid item's stretched down its area (CSS Grid 1, 11.1) —
2326
2326
  * which what is in it takes its percentages of, and which an item that is
2327
- * a flex or grid container itself lays its own items out in. Set by
2328
- * `flex.ts` and `css-grid.ts` for the item's own layout alone.
2327
+ * a flex or grid container itself lays its own items out in, and a table
2328
+ * shares out among its rows. Set by `flex.ts` and `css-grid.ts` for the
2329
+ * item's own layout alone.
2329
2330
  */
2330
2331
  export const FLEXED_HEIGHT = new WeakMap<Box, number>();
2331
2332
 
@@ -2333,12 +2334,23 @@ export const FLEXED_HEIGHT = new WeakMap<Box, number>();
2333
2334
  * The content height a flex layout gave an item without making it definite:
2334
2335
  * flexed along a column that has no height of its own, which 9.8 leaves
2335
2336
  * indefinite. An item that is a flex or grid container lays its own items
2336
- * out in it all the same — it is its used size (9.7) — and nothing in it
2337
- * takes a percentage of it. Set by `flex.ts` for the item's own layout
2338
- * alone.
2337
+ * out in it all the same — it is its used size (9.7) — as a table shares
2338
+ * it among its rows, and nothing in it takes a percentage of it. Set by
2339
+ * `flex.ts` for the item's own layout alone.
2339
2340
  */
2340
2341
  export const USED_HEIGHT = new WeakMap<Box, number>();
2341
2342
 
2343
+ /**
2344
+ * A table a grid stretches across its area, whose `auto` width is then the
2345
+ * one it is laid out at. `normal` stretches any grid item but a replaced
2346
+ * one (CSS Grid 1, 6.2), a table among them, where in a block's flow a
2347
+ * table's `auto` width is its columns' (CSS 2.1 17.5.2; Blink leaves tables
2348
+ * out of the boxes a block stretches, `space_utils.cc`, and stretches them
2349
+ * in a grid, `grid_item.cc`). Set by `css-grid.ts` for the item's own
2350
+ * layout alone, and never while its widths are measured.
2351
+ */
2352
+ export const STRETCHED_ACROSS = new WeakSet<Box>();
2353
+
2342
2354
  /**
2343
2355
  * Hand a box's children the height their percentages resolve against —
2344
2356
  * through inline boxes, which are no containing block, to the inline-block
@@ -3167,6 +3179,16 @@ export function intrinsicWidth(box: Box, seen?: { cut: boolean }): number {
3167
3179
  if (contained(box, CONTAIN_WIDTH)) {
3168
3180
  return box.style.containIntrinsicWidth ?? 0;
3169
3181
  }
3182
+ // a table is as wide as its layout made it: its columns, the spacing
3183
+ // either side of them and between, and its captions' least (`layoutTable`)
3184
+ // — at no width limit its widest, and at none its narrowest. What it
3185
+ // holds says less: its rows run from the first column to the last, the
3186
+ // spacing either side outside them, and measured by them a table in a
3187
+ // flex row, a grid, a float or an `inline-table` came to that much too
3188
+ // narrow, and its columns gave it up
3189
+ if (box.kind === 'table' && Number.isFinite(box.width)) {
3190
+ return Math.max(0, box.width - box.horizontalExtra);
3191
+ }
3170
3192
  // a grid laid out at no width limit has its columns at their widest, and
3171
3193
  // is as wide as they are — an item that runs past its column makes it no
3172
3194
  // wider — or, where it has none, as where its items end
@@ -38,6 +38,7 @@ import type { CollapsedTable } from './collapse.js';
38
38
  import { quoteAt } from '../css/content.js';
39
39
  import type { CounterStyles } from '../css/counter-styles.js';
40
40
  import type { ContentItem } from '../css/content.js';
41
+ import type { ShapeStyles } from '../css/shapes.js';
41
42
  import {
42
43
  CONTAIN_STYLE,
43
44
  FIRST_LINE_INHERITED,
@@ -713,6 +714,15 @@ export const COLUMN_ROWS = new WeakMap<
713
714
  * that would ask the three maps above of every box ask first. */
714
715
  export const columned = { any: false };
715
716
 
717
+ /** What the document's rules give the elements inside an inline `<svg>`
718
+ * (`Cascade.shapeStyles`), null where they give none. */
719
+ export type ShapeStyler = (box: Box) => ShapeStyles | null;
720
+
721
+ /** What `BoxTree.shapeStyler` answered for each drawing's box it has been
722
+ * asked of, which are the ones painted: a box restyled where it is has
723
+ * its answer put here again (`HtmlViewNode._hoverInPlace`). */
724
+ export const SHAPE_STYLES = new WeakMap<Box, ShapeStyles | null>();
725
+
716
726
  /** The element a pseudo-element's box is generated from, which has no
717
727
  * element of its own (`Box.pseudo`): whose the images its styles name are,
718
728
  * when the host is asked for them. */
@@ -774,6 +784,11 @@ export interface BoxTree {
774
784
  * one: worked out as a selection is first painted over it, not as the
775
785
  * tree is built, since most builds are never selected in. */
776
786
  selectionStyler: SelectionStyler | null;
787
+ /** What the rules give the elements in each inline `<svg>`
788
+ * (`ShapeStyler`), which have no boxes and no styles, where any rule
789
+ * could reach one: worked out as a drawing is first painted, since a
790
+ * page of icons shows few of them at once. */
791
+ shapeStyler: ShapeStyler | null;
777
792
  /** Whether a float or an out-of-flow box sits in an inline box: where
778
793
  * none does, layout looks for them among a block's own children and
779
794
  * goes through no inline box to find them. */
@@ -939,6 +954,7 @@ class Builder {
939
954
  firstLine: this._firstLine,
940
955
  firstLineStyler: this._firstLine ? this._firstLineStyler() : null,
941
956
  selectionStyler: cascade.hasSelection ? this._selectionStyler() : null,
957
+ shapeStyler: cascade.stylesShapes ? this._shapeStyler() : null,
942
958
  nestedOutOfLine: this._nestedOutOfLine,
943
959
  movedInline: this._movedInline,
944
960
  clipText: this._clipText,
@@ -971,6 +987,20 @@ class Builder {
971
987
  };
972
988
  }
973
989
 
990
+ /** `BoxTree.shapeStyler`, over this build's cascade: each drawing's
991
+ * once, from its box's style as it is when first asked. */
992
+ private _shapeStyler(): ShapeStyler {
993
+ const cascade = this._options.cascade;
994
+ return (box) => {
995
+ let shapes = SHAPE_STYLES.get(box);
996
+ if (shapes === undefined) {
997
+ shapes = box.el ? cascade.shapeStyles(box.el, box.style) : null;
998
+ SHAPE_STYLES.set(box, shapes);
999
+ }
1000
+ return shapes;
1001
+ };
1002
+ }
1003
+
974
1004
  /** `BoxTree.firstLineStyler`, over this build's cascade. */
975
1005
  private _firstLineStyler(): FirstLineStyler {
976
1006
  const cascade = this._options.cascade;
@@ -31,6 +31,7 @@ import { Box, GRID_TRACKS, isBlank } from './boxes.js';
31
31
  import {
32
32
  FLEXED_HEIGHT,
33
33
  MIN_CONTENT_PROBE,
34
+ STRETCHED_ACROSS,
34
35
  USED_HEIGHT,
35
36
  centreButton,
36
37
  clampHeight,
@@ -60,6 +61,9 @@ interface Item {
60
61
  /** Min-content and max-content widths, margin box, taken when asked. */
61
62
  min: number;
62
63
  max: number;
64
+ /** Whether it is a table stretched across its area (`STRETCHED_ACROSS`),
65
+ * which `layoutItem` decides. */
66
+ across: boolean;
63
67
  }
64
68
 
65
69
  /**
@@ -203,6 +207,11 @@ export function layoutGrid(
203
207
  const extra = style.boxSizing === 'border-box' ? 0 : box.horizontalExtra;
204
208
  widest = narrowest = Math.max(style.width + extra, box.horizontalExtra);
205
209
  } else {
210
+ // A percentage is of an area the columns are still being sized for,
211
+ // and what it comes to depends on what the item makes them: it is
212
+ // `auto` for that (CSS Sizing 3, 5.2.1), and the item's content says
213
+ // how wide it is. Measured at the width a percentage made of the
214
+ // probe's, an item `width: 100%` made its `auto` column nothing.
206
215
  const fresh = box.intrinsicMaxContent < 0;
207
216
  widest = maxContentOf(box, ctx);
208
217
  if (box.intrinsicMinContent < 0) {
@@ -210,8 +219,7 @@ export function layoutGrid(
210
219
  // which is the max-content one only if it was made just now
211
220
  const exact =
212
221
  fresh && ctx.fonts ? exactMinContent(box, ctx.fonts) : null;
213
- box.intrinsicMinContent =
214
- exact ?? measureIntrinsicWidth(box, ctx, MIN_CONTENT_PROBE);
222
+ box.intrinsicMinContent = exact ?? minContentOf(box, ctx);
215
223
  }
216
224
  narrowest = box.intrinsicMinContent;
217
225
  }
@@ -401,21 +409,27 @@ export function layoutGrid(
401
409
  // purpose"): a card's `margin-top: auto` takes what its row is taller
402
410
  // than the card by, its `flex: 1` grows into it (CSS Flexbox 8.1,
403
411
  // 9.7). Its content's own height is the same layout, and kept.
412
+ // A table shares out among its rows what the area is taller than
413
+ // they are (`layoutTable`), and is no shorter than they are however
414
+ // short its area: its rows are the least it can be, as they are
415
+ // where it has a height of its own (CSS 2.1 17.5.3).
404
416
  if (
405
417
  child.kind !== 'replaced' &&
406
418
  (wide !== null ||
407
419
  percentHeightsIn(child) ||
408
- (child.kind === 'flex' && height !== child.height))
420
+ (child.kind === 'flex' && height !== child.height) ||
421
+ (child.kind === 'table' && height > child.height + 0.01))
409
422
  ) {
410
423
  FLEXED_HEIGHT.set(child, Math.max(0, height - child.verticalExtra));
411
424
  try {
412
- ctx.layoutSubtree(child, width, area);
425
+ layoutAcross(item, width, area, ctx);
413
426
  } finally {
414
427
  FLEXED_HEIGHT.delete(child);
415
428
  }
416
429
  }
417
430
  child.width = width;
418
- child.height = height;
431
+ child.height =
432
+ child.kind === 'table' ? Math.max(height, child.height) : height;
419
433
  centreButton(child);
420
434
  }
421
435
  moveTo(
@@ -596,6 +610,7 @@ function place(
596
610
  cols: p.bs,
597
611
  min: -1,
598
612
  max: -1,
613
+ across: false,
599
614
  }
600
615
  : {
601
616
  box: p.box,
@@ -605,6 +620,7 @@ function place(
605
620
  cols: p.as,
606
621
  min: -1,
607
622
  max: -1,
623
+ across: false,
608
624
  },
609
625
  );
610
626
  }
@@ -641,14 +657,37 @@ function extent(
641
657
  }
642
658
 
643
659
  /** An item's max-content width, its border box's: measured once in the
644
- * box's life, as a table cell's is (`Box.intrinsicMaxContent`). */
660
+ * box's life, as a table cell's is (`Box.intrinsicMaxContent`). Its
661
+ * content's, whatever width it has of its own (`minContentOf`). */
645
662
  function maxContentOf(box: Box, ctx: LayoutContext): number {
646
663
  if (box.intrinsicMaxContent < 0) {
647
- box.intrinsicMaxContent = measureIntrinsicWidth(box, ctx, Infinity);
664
+ box.intrinsicMaxContent = measureIntrinsicWidth(
665
+ box,
666
+ ctx,
667
+ Infinity,
668
+ box.kind !== 'replaced',
669
+ );
648
670
  }
649
671
  return box.intrinsicMaxContent;
650
672
  }
651
673
 
674
+ /**
675
+ * An item's min-content width, its border box's. What it is measured for is
676
+ * what it contributes to its columns, and the width a percentage makes of
677
+ * them is not known then: a box that is not replaced is as wide as its
678
+ * content for that, whatever its own `width` says, and a replaced element
679
+ * takes its percentage of nothing (CSS Sizing 3, 5.2.1). A width that is a
680
+ * length is weighed apart (`measure` in `layoutGrid`).
681
+ */
682
+ function minContentOf(box: Box, ctx: LayoutContext): number {
683
+ return measureIntrinsicWidth(
684
+ box,
685
+ ctx,
686
+ MIN_CONTENT_PROBE,
687
+ box.kind !== 'replaced',
688
+ );
689
+ }
690
+
652
691
  /**
653
692
  * Lay an item out in its area: stretched across it where its width is
654
693
  * `auto` and it is stretched, or at its content's width, no wider than
@@ -693,11 +732,7 @@ function layoutItem(
693
732
  probed = true;
694
733
  }
695
734
  if (child.intrinsicMinContent < 0) {
696
- child.intrinsicMinContent = measureIntrinsicWidth(
697
- child,
698
- ctx,
699
- MIN_CONTENT_PROBE,
700
- );
735
+ child.intrinsicMinContent = minContentOf(child, ctx);
701
736
  probed = true;
702
737
  }
703
738
  if (probed) resolveEdges(child, area);
@@ -757,7 +792,15 @@ function layoutItem(
757
792
  width = Math.min(widest, Math.max(narrowest, room));
758
793
  }
759
794
  if (child.kind !== 'replaced') {
760
- ctx.layoutSubtree(child, width, area);
795
+ // a table stretched across is as wide as its area, which in a block's
796
+ // flow it would shrink from to its columns (`STRETCHED_ACROSS`)
797
+ item.across =
798
+ across &&
799
+ child.kind === 'table' &&
800
+ style.width === AUTO &&
801
+ !keyword &&
802
+ through === null;
803
+ layoutAcross(item, width, area, ctx);
761
804
  return;
762
805
  }
763
806
  // a replaced element sizes itself from its style: then it is given the
@@ -776,6 +819,27 @@ function layoutItem(
776
819
  }
777
820
  }
778
821
 
822
+ /** Lay an item out at a width: a table stretched across its area at that
823
+ * width as its own (`STRETCHED_ACROSS`), for this layout alone. */
824
+ function layoutAcross(
825
+ item: Item,
826
+ width: number,
827
+ area: number,
828
+ ctx: LayoutContext,
829
+ ): void {
830
+ const child = item.box;
831
+ if (!item.across) {
832
+ ctx.layoutSubtree(child, width, area);
833
+ return;
834
+ }
835
+ STRETCHED_ACROSS.add(child);
836
+ try {
837
+ ctx.layoutSubtree(child, width, area);
838
+ } finally {
839
+ STRETCHED_ACROSS.delete(child);
840
+ }
841
+ }
842
+
779
843
  /** What the percentages in a box's padding come to across it, its edges
780
844
  * resolved: what a width of its content's, measured with them of nothing,
781
845
  * is short of its border box by. */
@@ -590,14 +590,19 @@ function layoutItemAt(
590
590
  // size all the same (9.7), and `min-h-screen flex flex-col` around a
591
591
  // `flex-1 flex items-center` centres what is in that. At its content's
592
592
  // own height there is nothing to share, and the layout it has is that
593
- // one: the tallest card of a row is laid out the once.
593
+ // one: the tallest card of a row is laid out the once. A table shares
594
+ // what it is given past its rows among them (`layoutTable`), and a
595
+ // height short of them is none it can have: it is laid out again where
596
+ // it was given more.
594
597
  const given = definite ?? (column ? height : null);
595
- const shares =
596
- box.kind === 'flex' &&
597
- given !== null &&
598
- !(
599
- Math.abs(naturalHeight(box, ctx, width, containing, laid) - given) <= 0.01
600
- );
598
+ let shares = false;
599
+ if (given !== null && (box.kind === 'flex' || box.kind === 'table')) {
600
+ const natural = naturalHeight(box, ctx, width, containing, laid);
601
+ shares =
602
+ box.kind === 'flex'
603
+ ? !(Math.abs(natural - given) <= 0.01)
604
+ : !(natural >= given - 0.01);
605
+ }
601
606
  if (given !== null) {
602
607
  const inner = Math.max(0, given - box.verticalExtra);
603
608
  if (Object.is(inner, percentBaseInside(box))) {
@@ -630,8 +635,16 @@ function layoutItemAt(
630
635
  // A stretched item is taller than its content, and the box has to say so
631
636
  // or its background stops short of the row — and it is no taller than its
632
637
  // line where its content is, or than what its ratio makes of its width:
633
- // its height is the line's (CSS Flexbox 9.4, step 11).
634
- if (height > box.height || column || definite !== null) box.height = height;
638
+ // its height is the line's (CSS Flexbox 9.4, step 11). But for a table,
639
+ // whose rows are the least it can be, however short its line (CSS 2.1
640
+ // 17.5.3, as a height of its own is; Blink: "Tables can't shrink below
641
+ // their min-intrinsic size").
642
+ if (
643
+ height > box.height ||
644
+ ((column || definite !== null) && box.kind !== 'table')
645
+ ) {
646
+ box.height = height;
647
+ }
635
648
  // and a button's content is centred in the height it came to
636
649
  centreButton(box);
637
650
  moveTo(box, x, y);