tsquare 0.13.1 → 0.13.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.2
4
+
5
+ The language hasn't changed. Boards without notes on the board render exactly as in 0.13.1, and so do the 438 example and eval boards; boards where a note's height was misjudged are fixed.
6
+
7
+ - **Notes on the board get their real height** (#44). The layout estimated a note's height from its text's length, but the board draws the real wrap. In a grid, a row holding only notes could then be drawn a line or more taller or shorter than laid out, and every screen below it moved: flow arrows routed through the wrong place (an arrow's lane could cut across a screen), `measureElements` boxes missed the drawn screens, and a long note could be cut off at the bottom of the board, squeezing the title. Notes are now measured in a quick render of their own before the board is laid out (about 5 ms; none when a board has no notes).
8
+ - **`measureBoardLayout(spec, { flows })`**: the board's size and each screen's position, exact (async, since it renders the notes). Use it to size the image and place screen chips. `boardLayout` is unchanged: synchronous, with the estimate. `renderAll`, the playground and the MCP render tool use the exact one now.
9
+ - **A modal taller than its screen is measured below the status bar** (#45). The screen draws its modals, drawers and toasts below its status or browser bar, but `measureElements` cut them only to the frame, so a long form in a modal (16 fields on a phone) had a box over the status bar, and a click there returned the modal. They and their contents are now cut where they're drawn.
10
+ - **Measuring works in DOMs without `structuredClone`** (jsdom): boards with notes, tooltips or flows failed there with "structuredClone is not defined".
11
+
3
12
  ## 0.13.1
4
13
 
5
14
  The language and the renderer haven't changed: every wireframe renders exactly as in 0.13.0.
package/README.md CHANGED
@@ -151,7 +151,7 @@ const { issues } = compileWireframe(source); // problems by line, empt
151
151
  const png = await renderWireframe(source, { format: "png", scale: 2 });
152
152
  ```
153
153
 
154
- `tsquare/playground` exports the playground's request handler, for hosting it inside your own Node server. `tsquare/viewer` is the playground's zoomable board (zoom, pan, pinch, a chip per screen) for your own page: pass it the elements, then `show()` the SVG from `renderWireframeSvg` with the size and screens from `boardLayout`. It sets the styles its zoom math needs itself; give the stage a size, and optionally style the classes it toggles on the stage: `panning` (while dragging), `animate` (while fitting or zooming to a screen, e.g. `.animate > * { transition: transform .25s }`) and `stale` (from `setStale`, e.g. `.stale img { opacity: .35 }`). Call `destroy()` when you remove it. `languageData()` lists every component, prop, option word, icon and accent, for editor highlighting and autocomplete. In a browser bundle (esbuild, Vite, webpack) the same imports render SVG with no setup; PNG and share links need Node. On a page without a build step, `<script src="https://cdn.jsdelivr.net/npm/tsquare@0.13/dist/tsquare.min.js"></script>` draws every ```` ```tsquare ```` code block on the page (see [Embedding](docs/embedding.md#pages-written-in-markdown)). See [Using it with AI](docs/ai.md) for the prompt and the repair loop. `measureElements` gives each element's box on the board and its line in the text, and `elementsAt` what's under a click, for making a board clickable (see [Element positions](docs/element-positions.md)).
154
+ `tsquare/playground` exports the playground's request handler, for hosting it inside your own Node server. `tsquare/viewer` is the playground's zoomable board (zoom, pan, pinch, a chip per screen) for your own page: pass it the elements, then `show()` the SVG from `renderWireframeSvg` with the size and screens from `measureBoardLayout` (exact; `boardLayout` is a synchronous estimate that can be off by a line or more on boards with notes). It sets the styles its zoom math needs itself; give the stage a size, and optionally style the classes it toggles on the stage: `panning` (while dragging), `animate` (while fitting or zooming to a screen, e.g. `.animate > * { transition: transform .25s }`) and `stale` (from `setStale`, e.g. `.stale img { opacity: .35 }`). Call `destroy()` when you remove it. `languageData()` lists every component, prop, option word, icon and accent, for editor highlighting and autocomplete. In a browser bundle (esbuild, Vite, webpack) the same imports render SVG with no setup; PNG and share links need Node. On a page without a build step, `<script src="https://cdn.jsdelivr.net/npm/tsquare@0.13/dist/tsquare.min.js"></script>` draws every ```` ```tsquare ```` code block on the page (see [Embedding](docs/embedding.md#pages-written-in-markdown)). See [Using it with AI](docs/ai.md) for the prompt and the repair loop. `measureElements` gives each element's box on the board and its line in the text, and `elementsAt` what's under a click, for making a board clickable (see [Element positions](docs/element-positions.md)).
155
155
 
156
156
  ### The editor: `tsquare/editor`
157
157
 
@@ -21840,7 +21840,7 @@ function findAnchors(spec) {
21840
21840
  }
21841
21841
  var MARK = (i2) => `#fe${(i2 >> 8).toString(16).padStart(2, "0")}${(i2 & 255).toString(16).padStart(2, "0")}`;
21842
21842
  function tagForMeasuring(spec, anchors, bodies = []) {
21843
- const tagged = structuredClone(spec);
21843
+ const tagged = { ...spec, elements: Object.fromEntries(Object.entries(spec.elements).map(([k, el2]) => [k, { ...el2 }])) };
21844
21844
  const colors = /* @__PURE__ */ new Map();
21845
21845
  let n = 1;
21846
21846
  const tag = (id, part) => {
@@ -22932,7 +22932,8 @@ function boardLayout(spec, opts = {}) {
22932
22932
  }
22933
22933
  if (el2.type === "Note") {
22934
22934
  const w2 = props.width ?? NOTE_WIDTH;
22935
- return { type: "Note", name: String(props.text ?? ""), w: w2, h: LABEL_H + estimateNoteHeight(props.text ?? "", w2) };
22935
+ const h2 = typeof props.__height === "number" ? props.__height : estimateNoteHeight(props.text ?? "", w2);
22936
+ return { type: "Note", name: String(props.text ?? ""), w: w2, h: LABEL_H + h2 };
22936
22937
  }
22937
22938
  return null;
22938
22939
  });
@@ -22976,6 +22977,28 @@ function flowLabelGap(spec, laidOut, perRow) {
22976
22977
  }
22977
22978
  return widest ? widest + 32 : 0;
22978
22979
  }
22980
+ async function withNoteHeights(spec) {
22981
+ const els = spec.elements;
22982
+ const notes = (els[spec.root]?.children ?? []).filter((k) => els[k]?.type === "Note");
22983
+ if (!notes.length || notes.every((k) => typeof els[k].props?.__height === "number")) return spec;
22984
+ let root = "__notes";
22985
+ while (root in els) root += "_";
22986
+ const probe = {
22987
+ root,
22988
+ elements: { [root]: { type: "Board", props: { layout: "grid", columns: 1, padding: 0, gap: 0 }, children: notes }, ...Object.fromEntries(notes.map((k) => [k, els[k]])) }
22989
+ };
22990
+ const { tagged, colors } = tagForMeasuring(probe, notes.map((id) => ({ id, screen: id, kind: "tooltip", field: false })));
22991
+ const props = (k) => els[k].props ?? {};
22992
+ const width = Math.max(...notes.map((k) => props(k).width ?? NOTE_WIDTH));
22993
+ const height = notes.reduce((sum, k) => sum + LABEL_H + 3 * estimateNoteHeight(props(k).text ?? "", props(k).width ?? NOTE_WIDTH), 100);
22994
+ const svg = await renderToSvg(tagged, { registry: withMarkers(withPalette(paletteFor())), includeStandard: false, fonts: await loadFonts(), width, height });
22995
+ const boxes = readMarkers(svg, colors);
22996
+ const measured = notes.filter((k) => boxes[k]).map((k) => [k, { ...els[k], props: { ...props(k), __height: boxes[k].h } }]);
22997
+ return { ...spec, elements: { ...els, ...Object.fromEntries(measured) } };
22998
+ }
22999
+ async function measureBoardLayout(spec, opts = {}) {
23000
+ return boardLayout(await withNoteHeights(spec), opts);
23001
+ }
22979
23002
  function boardSize(spec, opts = {}) {
22980
23003
  const { width, height } = boardLayout(spec, opts);
22981
23004
  return { width, height };
@@ -22986,6 +23009,7 @@ async function renderWireframeSvg(spec, opts = {}) {
22986
23009
  const issues = checkSpec(spec);
22987
23010
  if (issues.length) throw new SpecError(issues);
22988
23011
  }
23012
+ spec = await withNoteHeights(spec);
22989
23013
  const flowsOn = opts.flows !== false;
22990
23014
  const { width, height } = boardLayout(spec, { flows: opts.flows });
22991
23015
  spec = withBoardGap(spec, opts.flows);
@@ -23420,7 +23444,7 @@ async function renderWireframeTool(args) {
23420
23444
  const { spec, issues } = compileWireframe(args.text);
23421
23445
  if (!spec || issues.length) return problems(issues);
23422
23446
  const flows = args.flows !== false;
23423
- const { width, height } = boardSize(spec, { flows });
23447
+ const { width, height } = await measureBoardLayout(spec, { flows });
23424
23448
  const scale = Math.min(1, MODEL_IMAGE_MAX / Math.max(width, height));
23425
23449
  const png = await renderWireframePng(spec, { skipValidation: true, flows, scale });
23426
23450
  const shown = scale < 1 ? `, shown at ${Math.round(width * scale)}\xD7${Math.round(height * scale)}` : "";
@@ -23506,7 +23530,7 @@ async function renderBlock(doc, source) {
23506
23530
  if (!spec || issues.length) return problems2(doc, issues.map((i2) => `line ${i2.line}: ${i2.message}`));
23507
23531
  try {
23508
23532
  const svg = await renderWireframeSvg(spec, { skipValidation: true });
23509
- const { width, height } = boardLayout(spec);
23533
+ const { width, height } = await measureBoardLayout(spec);
23510
23534
  const title = spec.elements[spec.root]?.props?.title;
23511
23535
  const figure = doc.createElement("figure");
23512
23536
  figure.className = "tsquare-board";
@@ -23566,13 +23590,15 @@ async function measureElements(spec, lines, opts = {}) {
23566
23590
  const issues = checkSpec(spec);
23567
23591
  if (issues.length) throw new SpecError(issues);
23568
23592
  }
23593
+ spec = await withNoteHeights(spec);
23569
23594
  const { width, height } = boardLayout(spec, { flows: opts.flows });
23570
23595
  spec = withBoardGap(spec, opts.flows);
23571
23596
  const els = spec.elements;
23572
23597
  const anchors = findAnchors(spec);
23573
23598
  const every = Object.keys(els).filter((k) => !["Board", "Screen", "Flow"].includes(els[k].type)).map((id) => ({ id, screen: id, kind: "tooltip", field: false }));
23574
23599
  const screens = Object.keys(els).filter((k) => els[k].type === "Screen");
23575
- const { tagged, colors } = tagForMeasuring(spec, [...anchors, ...every], screens);
23600
+ const inners = screens.map((id) => ({ id, screen: id, kind: "tooltip", field: false }));
23601
+ const { tagged, colors } = tagForMeasuring(spec, [...anchors, ...every, ...inners], screens);
23576
23602
  const svg = await renderToSvg(tagged, { registry: withMarkers(withPalette(paletteFor())), includeStandard: false, fonts: await loadFonts(), width, height });
23577
23603
  const boxes = readMarkers(svg, colors);
23578
23604
  const { items } = boardLayout(spec, { flows: opts.flows });
@@ -23618,9 +23644,10 @@ async function measureElements(spec, lines, opts = {}) {
23618
23644
  const kids = el2.children ?? [];
23619
23645
  if (el2.type === "Screen") {
23620
23646
  const frame = frames.get(k) ?? null;
23621
- const body = clip(boxes[`${k}#body`] ?? frame, frame);
23647
+ const inner = clip(boxes[k] ?? frame, frame);
23648
+ const body = clip(boxes[`${k}#body`] ?? inner, inner);
23622
23649
  const over = (c2) => OVERLAYS.has(els[c2]?.type);
23623
- for (const c2 of [...kids.filter((c3) => !over(c3)), ...kids.filter(over)]) visit(c2, k, k, BARS.has(els[c2]?.type) || over(c2) ? frame : body);
23650
+ for (const c2 of [...kids.filter((c3) => !over(c3)), ...kids.filter(over)]) visit(c2, k, k, BARS.has(els[c2]?.type) || over(c2) ? inner : body);
23624
23651
  } else {
23625
23652
  const inner = el2.type === "Drawer" && boxes[k] ? clip(boxes[k], cut) : cut;
23626
23653
  for (const c2 of kids) visit(c2, k, inScreen, inner);
@@ -23765,6 +23792,7 @@ export {
23765
23792
  printWireframeText as formatWireframe,
23766
23793
  languageData,
23767
23794
  mcpTools,
23795
+ measureBoardLayout,
23768
23796
  measureElements,
23769
23797
  parseWireframeText,
23770
23798
  registry,
@@ -2383,7 +2383,7 @@ function findAnchors(spec) {
2383
2383
  }
2384
2384
  var MARK = (i) => `#fe${(i >> 8).toString(16).padStart(2, "0")}${(i & 255).toString(16).padStart(2, "0")}`;
2385
2385
  function tagForMeasuring(spec, anchors, bodies = []) {
2386
- const tagged = structuredClone(spec);
2386
+ const tagged = { ...spec, elements: Object.fromEntries(Object.entries(spec.elements).map(([k, el]) => [k, { ...el }])) };
2387
2387
  const colors = /* @__PURE__ */ new Map();
2388
2388
  let n = 1;
2389
2389
  const tag = (id, part) => {
@@ -2985,7 +2985,8 @@ function boardLayout(spec, opts = {}) {
2985
2985
  }
2986
2986
  if (el.type === "Note") {
2987
2987
  const w = props.width ?? NOTE_WIDTH;
2988
- return { type: "Note", name: String(props.text ?? ""), w, h: LABEL_H + estimateNoteHeight(props.text ?? "", w) };
2988
+ const h = typeof props.__height === "number" ? props.__height : estimateNoteHeight(props.text ?? "", w);
2989
+ return { type: "Note", name: String(props.text ?? ""), w, h: LABEL_H + h };
2989
2990
  }
2990
2991
  return null;
2991
2992
  });
@@ -3029,6 +3030,28 @@ function flowLabelGap(spec, laidOut, perRow) {
3029
3030
  }
3030
3031
  return widest ? widest + 32 : 0;
3031
3032
  }
3033
+ async function withNoteHeights(spec) {
3034
+ const els = spec.elements;
3035
+ const notes = (els[spec.root]?.children ?? []).filter((k) => els[k]?.type === "Note");
3036
+ if (!notes.length || notes.every((k) => typeof els[k].props?.__height === "number")) return spec;
3037
+ let root = "__notes";
3038
+ while (root in els) root += "_";
3039
+ const probe = {
3040
+ root,
3041
+ elements: { [root]: { type: "Board", props: { layout: "grid", columns: 1, padding: 0, gap: 0 }, children: notes }, ...Object.fromEntries(notes.map((k) => [k, els[k]])) }
3042
+ };
3043
+ const { tagged, colors } = tagForMeasuring(probe, notes.map((id) => ({ id, screen: id, kind: "tooltip", field: false })));
3044
+ const props = (k) => els[k].props ?? {};
3045
+ const width = Math.max(...notes.map((k) => props(k).width ?? NOTE_WIDTH));
3046
+ const height = notes.reduce((sum, k) => sum + LABEL_H + 3 * estimateNoteHeight(props(k).text ?? "", props(k).width ?? NOTE_WIDTH), 100);
3047
+ const svg = await renderToSvg(tagged, { registry: withMarkers(withPalette(paletteFor())), includeStandard: false, fonts: await loadFonts(), width, height });
3048
+ const boxes = readMarkers(svg, colors);
3049
+ const measured = notes.filter((k) => boxes[k]).map((k) => [k, { ...els[k], props: { ...props(k), __height: boxes[k].h } }]);
3050
+ return { ...spec, elements: { ...els, ...Object.fromEntries(measured) } };
3051
+ }
3052
+ async function measureBoardLayout(spec, opts = {}) {
3053
+ return boardLayout(await withNoteHeights(spec), opts);
3054
+ }
3032
3055
  function boardSize(spec, opts = {}) {
3033
3056
  const { width, height } = boardLayout(spec, opts);
3034
3057
  return { width, height };
@@ -3039,6 +3062,7 @@ async function renderWireframeSvg(spec, opts = {}) {
3039
3062
  const issues = checkSpec(spec);
3040
3063
  if (issues.length) throw new SpecError(issues);
3041
3064
  }
3065
+ spec = await withNoteHeights(spec);
3042
3066
  const flowsOn = opts.flows !== false;
3043
3067
  const { width, height } = boardLayout(spec, { flows: opts.flows });
3044
3068
  spec = withBoardGap(spec, opts.flows);
@@ -3412,6 +3436,8 @@ export {
3412
3436
  SpecError,
3413
3437
  checkSpec,
3414
3438
  boardLayout,
3439
+ withNoteHeights,
3440
+ measureBoardLayout,
3415
3441
  boardSize,
3416
3442
  renderWireframeSvg,
3417
3443
  withBoardGap,
@@ -6,7 +6,7 @@ import {
6
6
  componentDefinitions,
7
7
  iconNames,
8
8
  takesUniversalProps
9
- } from "./chunk-K3Y2E2QA.js";
9
+ } from "./chunk-QYGOXTAG.js";
10
10
 
11
11
  // src/playground/language-data.ts
12
12
  function unwrap(t) {
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  printWireframeText,
7
7
  renderWireframe,
8
8
  wireframePrompt
9
- } from "./chunks/chunk-K3Y2E2QA.js";
9
+ } from "./chunks/chunk-QYGOXTAG.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { readFile, writeFile } from "node:fs/promises";
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { mcpTools, MCP_INSTRUCTIONS, LINK_KINDS, DEFAULT_LINKS, type LinkKind, w
7
7
  export { upgradeWireframe, upgradeSpec, RENAMED_PROPS, type UpgradeOptions } from "./upgrade.js";
8
8
  export { renderAll, DEFAULT_SELECTOR, type RenderAllOptions, type RenderAllResult } from "./dom.js";
9
9
  export { measureElements, elementsAt, elementsAtLine, type Measured, type MeasuredElement, type MeasuredFlow, type MeasuredBox, type MeasureOptions } from "./measure.js";
10
- export { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, boardLayout, type BoardItem, type RenderWireframeOptions } from "./render.js";
10
+ export { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, boardLayout, measureBoardLayout, type BoardItem, type RenderWireframeOptions } from "./render.js";
11
11
  export { parseWireframeText, type TextIssue, type ParseResult } from "./text.js";
12
12
  export { catalog, componentDefinitions } from "./catalog.js";
13
13
  export { registry } from "./components.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  languageData
3
- } from "./chunks/chunk-HVGRKWIV.js";
3
+ } from "./chunks/chunk-W5RDSAXT.js";
4
4
  import {
5
5
  DEVICES,
6
6
  LABEL_H,
@@ -21,6 +21,7 @@ import {
21
21
  languageReference,
22
22
  layoutFlows,
23
23
  loadFonts,
24
+ measureBoardLayout,
24
25
  overlayBoxes,
25
26
  paletteFor,
26
27
  parseWireframeText,
@@ -38,8 +39,9 @@ import {
38
39
  wireframePrompt,
39
40
  withBoardGap,
40
41
  withMarkers,
42
+ withNoteHeights,
41
43
  withPalette
42
- } from "./chunks/chunk-K3Y2E2QA.js";
44
+ } from "./chunks/chunk-QYGOXTAG.js";
43
45
 
44
46
  // src/deflate.ts
45
47
  import { deflateRawSync, inflateRawSync } from "node:zlib";
@@ -130,7 +132,7 @@ async function renderWireframeTool(args) {
130
132
  const { spec, issues } = compileWireframe(args.text);
131
133
  if (!spec || issues.length) return problems(issues);
132
134
  const flows = args.flows !== false;
133
- const { width, height } = boardSize(spec, { flows });
135
+ const { width, height } = await measureBoardLayout(spec, { flows });
134
136
  const scale = Math.min(1, MODEL_IMAGE_MAX / Math.max(width, height));
135
137
  const png = await renderWireframePng(spec, { skipValidation: true, flows, scale });
136
138
  const shown = scale < 1 ? `, shown at ${Math.round(width * scale)}\xD7${Math.round(height * scale)}` : "";
@@ -216,7 +218,7 @@ async function renderBlock(doc, source) {
216
218
  if (!spec || issues.length) return problems2(doc, issues.map((i) => `line ${i.line}: ${i.message}`));
217
219
  try {
218
220
  const svg = await renderWireframeSvg(spec, { skipValidation: true });
219
- const { width, height } = boardLayout(spec);
221
+ const { width, height } = await measureBoardLayout(spec);
220
222
  const title = spec.elements[spec.root]?.props?.title;
221
223
  const figure = doc.createElement("figure");
222
224
  figure.className = "tsquare-board";
@@ -277,13 +279,15 @@ async function measureElements(spec, lines, opts = {}) {
277
279
  const issues = checkSpec(spec);
278
280
  if (issues.length) throw new SpecError(issues);
279
281
  }
282
+ spec = await withNoteHeights(spec);
280
283
  const { width, height } = boardLayout(spec, { flows: opts.flows });
281
284
  spec = withBoardGap(spec, opts.flows);
282
285
  const els = spec.elements;
283
286
  const anchors = findAnchors(spec);
284
287
  const every = Object.keys(els).filter((k) => !["Board", "Screen", "Flow"].includes(els[k].type)).map((id) => ({ id, screen: id, kind: "tooltip", field: false }));
285
288
  const screens = Object.keys(els).filter((k) => els[k].type === "Screen");
286
- const { tagged, colors } = tagForMeasuring(spec, [...anchors, ...every], screens);
289
+ const inners = screens.map((id) => ({ id, screen: id, kind: "tooltip", field: false }));
290
+ const { tagged, colors } = tagForMeasuring(spec, [...anchors, ...every, ...inners], screens);
287
291
  const svg = await renderToSvg(tagged, { registry: withMarkers(withPalette(paletteFor())), includeStandard: false, fonts: await loadFonts(), width, height });
288
292
  const boxes = readMarkers(svg, colors);
289
293
  const { items } = boardLayout(spec, { flows: opts.flows });
@@ -329,9 +333,10 @@ async function measureElements(spec, lines, opts = {}) {
329
333
  const kids = el.children ?? [];
330
334
  if (el.type === "Screen") {
331
335
  const frame = frames.get(k) ?? null;
332
- const body = clip(boxes[`${k}#body`] ?? frame, frame);
336
+ const inner = clip(boxes[k] ?? frame, frame);
337
+ const body = clip(boxes[`${k}#body`] ?? inner, inner);
333
338
  const over = (c) => OVERLAYS.has(els[c]?.type);
334
- for (const c of [...kids.filter((c2) => !over(c2)), ...kids.filter(over)]) visit(c, k, k, BARS.has(els[c]?.type) || over(c) ? frame : body);
339
+ for (const c of [...kids.filter((c2) => !over(c2)), ...kids.filter(over)]) visit(c, k, k, BARS.has(els[c]?.type) || over(c) ? inner : body);
335
340
  } else {
336
341
  const inner = el.type === "Drawer" && boxes[k] ? clip(boxes[k], cut) : cut;
337
342
  for (const c of kids) visit(c, k, inScreen, inner);
@@ -424,6 +429,7 @@ export {
424
429
  printWireframeText as formatWireframe,
425
430
  languageData,
426
431
  mcpTools,
432
+ measureBoardLayout,
427
433
  measureElements,
428
434
  parseWireframeText,
429
435
  registry,
package/dist/measure.d.ts CHANGED
@@ -32,8 +32,8 @@ export interface MeasuredElement {
32
32
  /** The line of the element containing it; null for the board. */
33
33
  parentLine: number | null;
34
34
  /**
35
- * Where it's drawn: the part that shows, for content cut off by its screen (past the bottom, or
36
- * under a tab bar) or by a drawer's panel. A screen's box is its frame. Can be 0 wide or 0 tall
35
+ * Where it's drawn: the part that shows, for content cut off by its screen (past the bottom, under
36
+ * a tab bar, or under the status or browser bar) or by a drawer's panel. A screen's box is its frame. Can be 0 wide or 0 tall
37
37
  * (a spacer in a row): such an element is listed, but never under a point.
38
38
  */
39
39
  box: MeasuredBox;
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  languageData
3
- } from "../chunks/chunk-HVGRKWIV.js";
3
+ } from "../chunks/chunk-W5RDSAXT.js";
4
4
  import {
5
- boardLayout,
6
5
  compileWireframe,
7
6
  componentDocs,
8
7
  formatIssues,
8
+ measureBoardLayout,
9
9
  renderWireframePng,
10
10
  renderWireframeSvg,
11
11
  wireframePrompt
12
- } from "../chunks/chunk-K3Y2E2QA.js";
12
+ } from "../chunks/chunk-QYGOXTAG.js";
13
13
 
14
14
  // src/playground/index.ts
15
15
  import { createServer } from "node:http";
@@ -498,7 +498,7 @@ function createPlaygroundHandler() {
498
498
  const flows = url.searchParams.get("flows") !== "0";
499
499
  if (url.pathname === "/api/render") {
500
500
  if (!spec || issues.length) return json(res, 200, { issues });
501
- const { width, height, items } = boardLayout(spec, { flows });
501
+ const { width, height, items } = await measureBoardLayout(spec, { flows });
502
502
  return json(res, 200, { issues, svg: await renderWireframeSvg(spec, { skipValidation: true, flows }), width, height, items });
503
503
  }
504
504
  if (!spec || issues.length) return json(res, 400, { issues });
package/dist/render.d.ts CHANGED
@@ -30,6 +30,26 @@ export declare function boardLayout(spec: Spec, opts?: {
30
30
  items: BoardItem[];
31
31
  gap: number;
32
32
  };
33
+ /**
34
+ * The spec with each note on the board carrying its drawn height (`__height`), measured by rendering
35
+ * the notes alone. boardLayout otherwise estimates a note's height from its text's length, but the
36
+ * board draws the real wrap: a notes-only row in a grid then moved every screen below it off its
37
+ * layout, and a long note could be cut off at the bottom (#44). No render when the board has no notes.
38
+ */
39
+ export declare function withNoteHeights(spec: Spec): Promise<Spec>;
40
+ /**
41
+ * boardLayout with each note's drawn height, so the canvas size and every screen's position match
42
+ * the rendered board exactly. Use it to size the image and place screen chips; boardLayout is the
43
+ * synchronous estimate, off by a line or more when a note wraps differently than its length suggests.
44
+ */
45
+ export declare function measureBoardLayout(spec: Spec, opts?: {
46
+ flows?: boolean;
47
+ }): Promise<{
48
+ width: number;
49
+ height: number;
50
+ items: BoardItem[];
51
+ gap: number;
52
+ }>;
33
53
  /** The canvas size for a board. */
34
54
  export declare function boardSize(spec: Spec, opts?: {
35
55
  flows?: boolean;