@hypen-space/web 0.4.980 → 0.5.3

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 (229) hide show
  1. package/README.md +20 -13
  2. package/dist/canvas/accessibility.d.ts +118 -11
  3. package/dist/canvas/accessibility.js +470 -108
  4. package/dist/canvas/accessibility.js.map +1 -1
  5. package/dist/canvas/dispatch.d.ts +24 -0
  6. package/dist/canvas/dispatch.js +62 -0
  7. package/dist/canvas/dispatch.js.map +1 -0
  8. package/dist/canvas/editing.d.ts +178 -0
  9. package/dist/canvas/editing.js +590 -0
  10. package/dist/canvas/editing.js.map +1 -0
  11. package/dist/canvas/events.d.ts +28 -23
  12. package/dist/canvas/events.js +90 -103
  13. package/dist/canvas/events.js.map +1 -1
  14. package/dist/canvas/focus.d.ts +78 -0
  15. package/dist/canvas/focus.js +182 -0
  16. package/dist/canvas/focus.js.map +1 -0
  17. package/dist/canvas/index.d.ts +3 -2
  18. package/dist/canvas/index.js +3 -2
  19. package/dist/canvas/index.js.map +1 -1
  20. package/dist/canvas/layout.js +68 -98
  21. package/dist/canvas/layout.js.map +1 -1
  22. package/dist/canvas/paint.d.ts +16 -3
  23. package/dist/canvas/paint.js +449 -110
  24. package/dist/canvas/paint.js.map +1 -1
  25. package/dist/canvas/renderer.d.ts +28 -4
  26. package/dist/canvas/renderer.js +201 -81
  27. package/dist/canvas/renderer.js.map +1 -1
  28. package/dist/canvas/selection.js +31 -177
  29. package/dist/canvas/selection.js.map +1 -1
  30. package/dist/canvas/text-geometry.d.ts +72 -0
  31. package/dist/canvas/text-geometry.js +244 -0
  32. package/dist/canvas/text-geometry.js.map +1 -0
  33. package/dist/canvas/text.js +44 -18
  34. package/dist/canvas/text.js.map +1 -1
  35. package/dist/canvas/types.d.ts +10 -1
  36. package/dist/canvas/utils.d.ts +0 -23
  37. package/dist/canvas/utils.js +3 -67
  38. package/dist/canvas/utils.js.map +1 -1
  39. package/dist/canvas/variants.d.ts +52 -0
  40. package/dist/canvas/variants.js +156 -0
  41. package/dist/canvas/variants.js.map +1 -0
  42. package/dist/dom/a11y-styles.d.ts +20 -0
  43. package/dist/dom/a11y-styles.js +61 -0
  44. package/dist/dom/a11y-styles.js.map +1 -0
  45. package/dist/dom/applicators/aria.d.ts +19 -0
  46. package/dist/dom/applicators/aria.js +36 -0
  47. package/dist/dom/applicators/aria.js.map +1 -0
  48. package/dist/dom/applicators/events.d.ts +7 -0
  49. package/dist/dom/applicators/events.js +36 -8
  50. package/dist/dom/applicators/events.js.map +1 -1
  51. package/dist/dom/applicators/font.d.ts +11 -6
  52. package/dist/dom/applicators/font.js +16 -27
  53. package/dist/dom/applicators/font.js.map +1 -1
  54. package/dist/dom/applicators/index.js +51 -45
  55. package/dist/dom/applicators/index.js.map +1 -1
  56. package/dist/dom/canvas/index.js +2 -2
  57. package/dist/dom/canvas/index.js.map +1 -1
  58. package/dist/dom/components/app.js +2 -2
  59. package/dist/dom/components/app.js.map +1 -1
  60. package/dist/dom/components/audio.js +2 -2
  61. package/dist/dom/components/audio.js.map +1 -1
  62. package/dist/dom/components/avatar.js +3 -4
  63. package/dist/dom/components/avatar.js.map +1 -1
  64. package/dist/dom/components/badge.js +3 -4
  65. package/dist/dom/components/badge.js.map +1 -1
  66. package/dist/dom/components/button.js +2 -2
  67. package/dist/dom/components/button.js.map +1 -1
  68. package/dist/dom/components/card.js +2 -2
  69. package/dist/dom/components/card.js.map +1 -1
  70. package/dist/dom/components/center.js +2 -2
  71. package/dist/dom/components/center.js.map +1 -1
  72. package/dist/dom/components/checkbox.js +4 -5
  73. package/dist/dom/components/checkbox.js.map +1 -1
  74. package/dist/dom/components/column.js +2 -2
  75. package/dist/dom/components/column.js.map +1 -1
  76. package/dist/dom/components/container.js +2 -2
  77. package/dist/dom/components/container.js.map +1 -1
  78. package/dist/dom/components/divider.js +2 -2
  79. package/dist/dom/components/divider.js.map +1 -1
  80. package/dist/dom/components/grid.js +10 -10
  81. package/dist/dom/components/grid.js.map +1 -1
  82. package/dist/dom/components/heading.js +4 -6
  83. package/dist/dom/components/heading.js.map +1 -1
  84. package/dist/dom/components/hypenapp.d.ts +25 -1
  85. package/dist/dom/components/hypenapp.js +215 -247
  86. package/dist/dom/components/hypenapp.js.map +1 -1
  87. package/dist/dom/components/icon.js +4 -5
  88. package/dist/dom/components/icon.js.map +1 -1
  89. package/dist/dom/components/image.js +2 -2
  90. package/dist/dom/components/image.js.map +1 -1
  91. package/dist/dom/components/index.d.ts +2 -2
  92. package/dist/dom/components/index.js +9 -2
  93. package/dist/dom/components/index.js.map +1 -1
  94. package/dist/dom/components/input.js +2 -2
  95. package/dist/dom/components/input.js.map +1 -1
  96. package/dist/dom/components/link.js +2 -2
  97. package/dist/dom/components/link.js.map +1 -1
  98. package/dist/dom/components/list.js +2 -2
  99. package/dist/dom/components/list.js.map +1 -1
  100. package/dist/dom/components/paragraph.js +3 -4
  101. package/dist/dom/components/paragraph.js.map +1 -1
  102. package/dist/dom/components/progressbar.js +3 -3
  103. package/dist/dom/components/progressbar.js.map +1 -1
  104. package/dist/dom/components/route.js +2 -2
  105. package/dist/dom/components/route.js.map +1 -1
  106. package/dist/dom/components/router.js +2 -2
  107. package/dist/dom/components/router.js.map +1 -1
  108. package/dist/dom/components/row.js +10 -10
  109. package/dist/dom/components/row.js.map +1 -1
  110. package/dist/dom/components/select.js +3 -4
  111. package/dist/dom/components/select.js.map +1 -1
  112. package/dist/dom/components/slider.js +2 -2
  113. package/dist/dom/components/slider.js.map +1 -1
  114. package/dist/dom/components/spacer.js +2 -2
  115. package/dist/dom/components/spacer.js.map +1 -1
  116. package/dist/dom/components/spinner.js +27 -13
  117. package/dist/dom/components/spinner.js.map +1 -1
  118. package/dist/dom/components/stack.js +10 -10
  119. package/dist/dom/components/stack.js.map +1 -1
  120. package/dist/dom/components/switch.js +5 -6
  121. package/dist/dom/components/switch.js.map +1 -1
  122. package/dist/dom/components/tabs.d.ts +27 -0
  123. package/dist/dom/components/tabs.js +69 -0
  124. package/dist/dom/components/tabs.js.map +1 -0
  125. package/dist/dom/components/text.js +4 -7
  126. package/dist/dom/components/text.js.map +1 -1
  127. package/dist/dom/components/textarea.js +2 -2
  128. package/dist/dom/components/textarea.js.map +1 -1
  129. package/dist/dom/components/video.js +2 -2
  130. package/dist/dom/components/video.js.map +1 -1
  131. package/dist/dom/components/visuallyhidden.d.ts +9 -0
  132. package/dist/dom/components/visuallyhidden.js +26 -0
  133. package/dist/dom/components/visuallyhidden.js.map +1 -0
  134. package/dist/dom/debug.js +6 -8
  135. package/dist/dom/debug.js.map +1 -1
  136. package/dist/dom/index.d.ts +0 -1
  137. package/dist/dom/index.js +0 -1
  138. package/dist/dom/index.js.map +1 -1
  139. package/dist/dom/operability.d.ts +100 -0
  140. package/dist/dom/operability.js +298 -0
  141. package/dist/dom/operability.js.map +1 -0
  142. package/dist/dom/renderer.d.ts +98 -5
  143. package/dist/dom/renderer.js +400 -66
  144. package/dist/dom/renderer.js.map +1 -1
  145. package/dist/dom/route-focus.d.ts +42 -0
  146. package/dist/dom/route-focus.js +88 -0
  147. package/dist/dom/route-focus.js.map +1 -0
  148. package/dist/dom/semantics.d.ts +35 -0
  149. package/dist/dom/semantics.js +184 -0
  150. package/dist/dom/semantics.js.map +1 -0
  151. package/dist/variants.d.ts +90 -0
  152. package/dist/variants.js +206 -0
  153. package/dist/variants.js.map +1 -0
  154. package/package.json +2 -2
  155. package/src/canvas/QUICKSTART.md +16 -11
  156. package/src/canvas/README.md +63 -34
  157. package/src/canvas/accessibility.ts +507 -115
  158. package/src/canvas/dispatch.ts +78 -0
  159. package/src/canvas/editing.ts +697 -0
  160. package/src/canvas/events.ts +104 -115
  161. package/src/canvas/focus.ts +216 -0
  162. package/src/canvas/index.ts +8 -9
  163. package/src/canvas/layout.ts +68 -103
  164. package/src/canvas/paint.ts +509 -109
  165. package/src/canvas/renderer.ts +239 -99
  166. package/src/canvas/selection.ts +52 -210
  167. package/src/canvas/text-geometry.ts +311 -0
  168. package/src/canvas/text.ts +45 -18
  169. package/src/canvas/types.ts +28 -1
  170. package/src/canvas/utils.ts +3 -69
  171. package/src/canvas/variants.ts +172 -0
  172. package/src/dom/README.md +8 -7
  173. package/src/dom/a11y-styles.ts +65 -0
  174. package/src/dom/applicators/aria.ts +41 -0
  175. package/src/dom/applicators/events.ts +39 -7
  176. package/src/dom/applicators/font.ts +16 -29
  177. package/src/dom/applicators/index.ts +61 -46
  178. package/src/dom/canvas/index.ts +2 -2
  179. package/src/dom/components/app.ts +2 -2
  180. package/src/dom/components/audio.ts +2 -2
  181. package/src/dom/components/avatar.ts +3 -4
  182. package/src/dom/components/badge.ts +3 -4
  183. package/src/dom/components/button.ts +2 -2
  184. package/src/dom/components/card.ts +2 -2
  185. package/src/dom/components/center.ts +2 -2
  186. package/src/dom/components/checkbox.ts +4 -5
  187. package/src/dom/components/column.ts +2 -2
  188. package/src/dom/components/container.ts +2 -2
  189. package/src/dom/components/divider.ts +2 -2
  190. package/src/dom/components/grid.ts +10 -10
  191. package/src/dom/components/heading.ts +4 -6
  192. package/src/dom/components/hypenapp.ts +246 -274
  193. package/src/dom/components/icon.ts +4 -5
  194. package/src/dom/components/image.ts +2 -2
  195. package/src/dom/components/index.ts +10 -3
  196. package/src/dom/components/input.ts +2 -2
  197. package/src/dom/components/link.ts +2 -2
  198. package/src/dom/components/list.ts +2 -2
  199. package/src/dom/components/paragraph.ts +3 -4
  200. package/src/dom/components/progressbar.ts +3 -3
  201. package/src/dom/components/route.ts +2 -2
  202. package/src/dom/components/router.ts +2 -2
  203. package/src/dom/components/row.ts +10 -10
  204. package/src/dom/components/select.ts +3 -4
  205. package/src/dom/components/slider.ts +2 -2
  206. package/src/dom/components/spacer.ts +2 -2
  207. package/src/dom/components/spinner.ts +34 -16
  208. package/src/dom/components/stack.ts +10 -10
  209. package/src/dom/components/switch.ts +5 -6
  210. package/src/dom/components/tabs.ts +76 -0
  211. package/src/dom/components/text.ts +4 -7
  212. package/src/dom/components/textarea.ts +2 -2
  213. package/src/dom/components/video.ts +2 -2
  214. package/src/dom/components/visuallyhidden.ts +30 -0
  215. package/src/dom/debug.ts +6 -8
  216. package/src/dom/index.ts +0 -8
  217. package/src/dom/operability.ts +312 -0
  218. package/src/dom/renderer.ts +457 -66
  219. package/src/dom/route-focus.ts +98 -0
  220. package/src/dom/semantics.ts +199 -0
  221. package/src/variants.ts +249 -0
  222. package/dist/canvas/input.d.ts +0 -76
  223. package/dist/canvas/input.js +0 -207
  224. package/dist/canvas/input.js.map +0 -1
  225. package/dist/client.d.ts +0 -36
  226. package/dist/client.js +0 -47
  227. package/dist/client.js.map +0 -1
  228. package/src/canvas/input.ts +0 -251
  229. package/src/client.ts +0 -88
@@ -7,15 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { VirtualNode, Layout, BoxSpacing } from "./types.js";
10
- import {
11
- parseSpacing,
12
- parseSize,
13
- cssLengthToPx,
14
- cssLengthToDimension,
15
- resolveLineHeight,
16
- setCurrentViewport,
17
- getCurrentViewport,
18
- } from "./utils.js";
10
+ import { parseSpacing, parseSize, cssLengthToPx, cssLengthToDimension } from "./utils.js";
19
11
  import { measureText } from "./text.js";
20
12
  import { getImageNaturalAspect } from "./paint.js";
21
13
 
@@ -55,6 +47,19 @@ export async function initTaffyLayout(): Promise<boolean> {
55
47
  taffyInitPromise = (async () => {
56
48
  try {
57
49
  taffy = await import("taffy-layout");
50
+ // In a server runtime (Bun/Node — including test runs where a JSDOM
51
+ // `window` may be globally registered by another test file), load the
52
+ // WASM from node_modules via fs. This path is deterministic; the
53
+ // fetch-based branch below depends on a reachable origin/CDN and must
54
+ // never be selected just because a test polyfilled `window`.
55
+ const isServerRuntime =
56
+ typeof process !== "undefined" &&
57
+ !!(process.versions?.bun || process.versions?.node);
58
+ if (isServerRuntime) {
59
+ await taffy.loadTaffy();
60
+ taffyReady = true;
61
+ return;
62
+ }
58
63
  // Bundlers (esp. Bun's browser bundler) sometimes inline a file:// URL
59
64
  // for import.meta.url inside taffy_wasm.js, which the browser blocks
60
65
  // ("Not allowed to load local resource"). The package's own `loadTaffy`
@@ -62,26 +67,40 @@ export async function initTaffyLayout(): Promise<boolean> {
62
67
  // raw `__wbg_init` (default export of the `taffy-layout/wasm` subpath)
63
68
  // directly with an explicit Response.
64
69
  let usedExplicit = false;
65
- if (typeof window !== "undefined" && typeof fetch === "function") {
66
- const candidates = [
67
- new URL("/taffy_wasm_bg.wasm", window.location.origin),
68
- new URL("https://cdn.jsdelivr.net/npm/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
69
- new URL("https://unpkg.com/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
70
- ];
71
- const rawWasm = await import(
72
- /* @vite-ignore */ "taffy-layout/wasm" as string
73
- );
74
- const wbgInit = rawWasm.default as (input?: any) => Promise<unknown>;
75
- for (const url of candidates) {
76
- try {
77
- const res = await fetch(url);
78
- if (!res.ok) continue;
79
- await wbgInit({ module_or_path: res });
80
- usedExplicit = true;
81
- break;
82
- } catch {
83
- // Try next candidate
70
+ // Only take the fetch path in a real browser. Test environments (and
71
+ // anything else that installs a fake `window` global) must fall through
72
+ // to `loadTaffy()`, which resolves the WASM from the package on disk —
73
+ // a fake window without `location.origin` used to throw here and
74
+ // silently disable Taffy for the rest of the process.
75
+ const isRealBrowser =
76
+ typeof window !== "undefined" &&
77
+ typeof fetch === "function" &&
78
+ typeof window.location?.origin === "string" &&
79
+ window.document?.defaultView === window;
80
+ if (isRealBrowser) {
81
+ try {
82
+ const candidates = [
83
+ new URL("/taffy_wasm_bg.wasm", window.location.origin),
84
+ new URL("https://cdn.jsdelivr.net/npm/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
85
+ new URL("https://unpkg.com/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
86
+ ];
87
+ const rawWasm = await import(
88
+ /* @vite-ignore */ "taffy-layout/wasm" as string
89
+ );
90
+ const wbgInit = rawWasm.default as (input?: any) => Promise<unknown>;
91
+ for (const url of candidates) {
92
+ try {
93
+ const res = await fetch(url);
94
+ if (!res.ok) continue;
95
+ await wbgInit({ module_or_path: res });
96
+ usedExplicit = true;
97
+ break;
98
+ } catch {
99
+ // Try next candidate
100
+ }
84
101
  }
102
+ } catch {
103
+ // Fall through to loadTaffy()
85
104
  }
86
105
  }
87
106
  if (!usedExplicit) {
@@ -180,22 +199,9 @@ function buildTaffyStyle(
180
199
  // `List` is the DSL's vertical-stack iterator (see dom/components/list.ts
181
200
  // — `flex-direction: column` is its default). Match that here so feeds
182
201
  // like Notifications stack their rows vertically instead of flowing
183
- // sideways. The legacy `direction` prop can flip it to row, and so can
184
- // a Tailwind `flex-row` class which lands on `props.flexDirection`
185
- // (without this, top-tab strips built as `List.flex-row` stacked
186
- // vertically because the List default won the tie-break).
187
- const isListColumn =
188
- type === "list" &&
189
- props.direction !== "horizontal" &&
190
- props.flexDirection !== "row";
191
- // Button defaults to `flex-direction: column` to match the DOM renderer
192
- // (see dom/components/button.ts). With Taffy's default `align-items:
193
- // stretch`, treating Button as a row caused inner `Column { Icon, Text }`
194
- // children to collapse onto each other (icon overlapping label). Only
195
- // apply the column default when no explicit `flexDirection` is set so a
196
- // Tailwind `flex-row` on a button still wins.
197
- const isButtonColumn = type === "button" && props.flexDirection === undefined;
198
- const isColumn = !isStack && (type === "column" || isListColumn || isButtonColumn || props.flexDirection === "column");
202
+ // sideways. The `direction` prop can still flip it to row.
203
+ const isListColumn = type === "list" && props.direction !== "horizontal";
204
+ const isColumn = !isStack && (type === "column" || isListColumn || props.flexDirection === "column");
199
205
  const isGrid = !isStack && (type === "grid" || props.display === "grid");
200
206
 
201
207
  if (isStack) {
@@ -386,7 +392,7 @@ function buildTaffyStyle(
386
392
  // Taffy defaults to `box-sizing: border-box`, so we add the
387
393
  // padding/border ourselves into `size.height`.
388
394
  const fontSize = cssLengthToPx(props.fontSize) ?? 16;
389
- const lineHeight = resolveLineHeight(props.lineHeight, fontSize) ?? fontSize * 1.5;
395
+ const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.5;
390
396
  const minRows = type === "textarea" ? Math.max(1, Number(props.rows) || 3) : 1;
391
397
  const padTop = cssLengthToPx(props.paddingTop ?? props.padding) ?? 0;
392
398
  const padBottom = cssLengthToPx(props.paddingBottom ?? props.padding) ?? 0;
@@ -408,30 +414,7 @@ function buildTaffyStyle(
408
414
  (parent.type.toLowerCase() === "grid" || parent.props.display === "grid");
409
415
  const widthIn = parentIsGridLayout && props.width === "100%" ? undefined : props.width;
410
416
  const heightIn = parentIsGridLayout && props.height === "100%" ? undefined : props.height;
411
-
412
- // Mirror the DOM rule `[data-hypen-type="row"]:has(> [data-hypen-flex])
413
- // { width: 100% }` (and the symmetric column case). When a flex
414
- // container has no explicit main-axis size and any child opts into
415
- // weighted growth (`flex` / `flexGrow > 0`), Taffy would otherwise
416
- // size the container to its min-content — so a Row of `flex-1`
417
- // segmented-control buttons stayed pill-sized instead of stretching
418
- // across the parent. Only do this when at least one child explicitly
419
- // asks for flex, matching the DOM `:has(> [data-hypen-flex])` guard.
420
- const isRowContainer = !isStack && !isGrid && !isColumn;
421
- const isColumnContainer = !isStack && !isGrid && isColumn;
422
- const childAsksFlex = node.children.some((c) => {
423
- const fg = parseFloat(c.props.flexGrow);
424
- if (Number.isFinite(fg) && fg > 0) return true;
425
- const f = parseFloat(c.props.flex);
426
- return Number.isFinite(f) && f > 0;
427
- });
428
- let resolvedWidth = toDimension(widthIn);
429
- let resolvedHeight = toDimension(heightIn);
430
- if (childAsksFlex) {
431
- if (isRowContainer && widthIn === undefined) resolvedWidth = "100%";
432
- if (isColumnContainer && heightIn === undefined) resolvedHeight = "100%";
433
- }
434
- style.size = { width: resolvedWidth, height: resolvedHeight };
417
+ style.size = { width: toDimension(widthIn), height: toDimension(heightIn) };
435
418
  }
436
419
 
437
420
  // --- Min / Max constraints -------------------------------------------------
@@ -806,17 +789,12 @@ function buildTree(
806
789
  // container with size 0×0, and downstream siblings were placed on top of
807
790
  // each other ("1,431 likes" overlapping the caption row was the visible
808
791
  // symptom).
809
- // `props[0] != null` not `props[0]` truthy. The number `0`, the empty
810
- // string, and `false` are valid text content; falsy guards here let
811
- // those cases skip the measurement leaf and inherit a 0-height container,
812
- // which made siblings stack on top of each other (a bound `0` overlapping
813
- // the next line was the visible symptom in calorie-counter's hero).
814
- if (node.type.toLowerCase() === "text" && props[0] != null && node.children.length === 0) {
815
- const text = String(props[0]);
792
+ if (node.type.toLowerCase() === "text" && props[0] && node.children.length === 0) {
793
+ const text = String(props[0] || "");
816
794
  const fontSize = cssLengthToPx(props.fontSize) ?? 16;
817
795
  const fontWeight = props.fontWeight || "normal";
818
796
  const fontFamily = props.fontFamily || "system-ui, sans-serif";
819
- const lineHeight = resolveLineHeight(props.lineHeight, fontSize) ?? fontSize * 1.2;
797
+ const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.2;
820
798
 
821
799
  const p = parseSpacing(props.padding || 0);
822
800
  if (props.paddingTop !== undefined) p.top = cssLengthToPx(props.paddingTop) ?? 0;
@@ -1130,30 +1108,18 @@ export function computeLayout(
1130
1108
  x: number = 0,
1131
1109
  y: number = 0,
1132
1110
  ): void {
1133
- // Publish the active viewport so `cssLengthToPx` can resolve `vw` / `vh`
1134
- // against the actual canvas dimensions during this layout pass. Without
1135
- // this, `vh` falls back to `window.innerHeight` (wrong inside an iframe
1136
- // or scaled preview pane) or, worse, to a unitless pixel value that
1137
- // collapses entire layouts. Layout is synchronous, so a try/finally
1138
- // restoring the previous slot is safe under nested or re-entrant calls.
1139
- const prevViewport = getCurrentViewport();
1140
- setCurrentViewport({ width: availableWidth, height: availableHeight });
1141
- try {
1142
- if (taffyReady && taffy) {
1111
+ if (taffyReady && taffy) {
1112
+ computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
1113
+ // After the first pass, Taffy's grid auto-row sizing for aspect-ratio
1114
+ // image leaves can collapse rows (the leaf's max-content height is 0
1115
+ // before column widths are known). Detect that pattern and re-run
1116
+ // with explicit `gridAutoRows` derived from the now-known column
1117
+ // width see `annotateCollapsedAspectGrids`.
1118
+ if (annotateCollapsedAspectGrids(node)) {
1143
1119
  computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
1144
- // After the first pass, Taffy's grid auto-row sizing for aspect-ratio
1145
- // image leaves can collapse rows (the leaf's max-content height is 0
1146
- // before column widths are known). Detect that pattern and re-run
1147
- // with explicit `gridAutoRows` derived from the now-known column
1148
- // width — see `annotateCollapsedAspectGrids`.
1149
- if (annotateCollapsedAspectGrids(node)) {
1150
- computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
1151
- }
1152
- } else {
1153
- computeLayoutFallback(ctx, node, availableWidth, availableHeight, x, y);
1154
1120
  }
1155
- } finally {
1156
- setCurrentViewport(prevViewport);
1121
+ } else {
1122
+ computeLayoutFallback(ctx, node, availableWidth, availableHeight, x, y);
1157
1123
  }
1158
1124
  }
1159
1125
 
@@ -1243,13 +1209,12 @@ function computeLayoutFallback(
1243
1209
  if (height === null) height = size;
1244
1210
  }
1245
1211
 
1246
- // Same `!= null` rule as above — `0` / `""` / `false` are valid text.
1247
- if (node.type.toLowerCase() === "text" && node.props[0] != null) {
1248
- const text = String(node.props[0]);
1212
+ if (node.type.toLowerCase() === "text" && node.props[0]) {
1213
+ const text = String(node.props[0] || "");
1249
1214
  const fontSize = cssLengthToPx(props.fontSize) ?? 16;
1250
1215
  const fontWeight = props.fontWeight || "normal";
1251
1216
  const fontFamily = props.fontFamily || "system-ui, sans-serif";
1252
- const lineHeight = resolveLineHeight(props.lineHeight, fontSize) ?? fontSize * 1.2;
1217
+ const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.2;
1253
1218
 
1254
1219
  const maxWidth = width || availableAfterMargin.width - padding.left - padding.right;
1255
1220
  const maxLinesRaw = props.maxLines;