tsquare 0.13.0 → 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 +15 -0
- package/README.md +1 -1
- package/dist/anchors.d.ts +2 -1
- package/dist/browser/index.js +42 -10
- package/dist/chunks/{chunk-YOZFCNSK.js → chunk-QYGOXTAG.js} +35 -5
- package/dist/chunks/{chunk-PGX7ANZH.js → chunk-W5RDSAXT.js} +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -7
- package/dist/measure.d.ts +3 -3
- package/dist/playground/index.js +4 -4
- package/dist/render.d.ts +20 -0
- package/dist/tsquare.min.js +72 -72
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.13.1
|
|
13
|
+
|
|
14
|
+
The language and the renderer haven't changed: every wireframe renders exactly as in 0.13.0.
|
|
15
|
+
|
|
16
|
+
- **An open menu's or option list's box is the list as drawn** (#43). `measureElements` sized it from the estimate used to place the list (36px a row), but the list is drawn at 32px a row plus padding and a border, so a 10-item menu's box ran 37px past it, and a click just below an open menu returned the menu's owner. The box now has the drawn height, opening down or up.
|
|
17
|
+
|
|
3
18
|
## 0.13.0
|
|
4
19
|
|
|
5
20
|
The language hasn't changed. Every wireframe renders exactly as in 0.12.4 except one kind: a flow arrow to a modal, drawer or toast now ends at its card, panel or pill, and a tooltip on one sits over it (they used to be placed against the dimmed screen or the full-width row around it). None of the 438 example and eval boards has either; they render byte for byte the same.
|
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
|
|
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
|
|
package/dist/anchors.d.ts
CHANGED
|
@@ -71,7 +71,8 @@ export declare function parseDate(value: unknown): {
|
|
|
71
71
|
} | null;
|
|
72
72
|
/**
|
|
73
73
|
* Each anchored overlay's box on the board, by the id of the element it belongs to (an element with
|
|
74
|
-
* an open menu and a tooltip gets both).
|
|
74
|
+
* an open menu and a tooltip gets both). Menus and option lists get the height they're drawn at; a
|
|
75
|
+
* tooltip's comes from its text and a date picker's from its weeks (within a few px).
|
|
75
76
|
*/
|
|
76
77
|
export declare function overlayBoxes(spec: Spec, anchors: Anchor[], boxes: Record<string, Rect>): {
|
|
77
78
|
id: string;
|
package/dist/browser/index.js
CHANGED
|
@@ -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 =
|
|
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) => {
|
|
@@ -21879,6 +21879,7 @@ function place(anchor, size, screen, opts) {
|
|
|
21879
21879
|
return { x: x3 - screen.x, y: y - screen.y, bottom: screen.y + screen.h - (anchor.y - opts.gap), side };
|
|
21880
21880
|
}
|
|
21881
21881
|
var ROW = 36;
|
|
21882
|
+
var listHeight = (rows) => rows * (ROW - 4) + 2 * 4 + 2 * 1.5;
|
|
21882
21883
|
function ListCard({ items, width, selected, c: c2 }) {
|
|
21883
21884
|
return /* @__PURE__ */ jsx4("div", { style: { display: "flex", flexDirection: "column", width, padding: 4, backgroundColor: theme.paper, border: `1.5px solid ${theme.lineStrong}`, borderRadius: 8, boxShadow: "0 8px 24px rgba(0,0,0,0.18)" }, children: items.map((item, i2) => {
|
|
21884
21885
|
const on2 = selected != null && item.label === selected;
|
|
@@ -21922,14 +21923,14 @@ function placeOverlay(spec, a, boxes) {
|
|
|
21922
21923
|
if (a.kind === "options") {
|
|
21923
21924
|
const options = p.options?.length ? p.options : [p.value ?? "Option 1", "Option 2", "Option 3"];
|
|
21924
21925
|
const size2 = { w: anchor.w, h: options.length * ROW + 8 };
|
|
21925
|
-
return { screen, anchor, size: size2, pos: place(anchor, size2, screen, { prefer: "below", align: "start", gap: 4 }), options };
|
|
21926
|
+
return { screen, anchor, size: size2, drawnH: listHeight(options.length), pos: place(anchor, size2, screen, { prefer: "below", align: "start", gap: 4 }), options };
|
|
21926
21927
|
}
|
|
21927
21928
|
if (a.kind === "menu") {
|
|
21928
21929
|
const items = p.menu.map((i2) => typeof i2 === "string" ? { label: i2 } : i2);
|
|
21929
21930
|
const width = a.end ? 200 : Math.max(180, anchor.w);
|
|
21930
21931
|
const inRightHalf = anchor.x + anchor.w / 2 > screen.x + screen.w / 2;
|
|
21931
21932
|
const size2 = { w: width, h: items.length * ROW + 8 };
|
|
21932
|
-
return { screen, anchor, size: size2, pos: place(anchor, size2, screen, { prefer: "below", align: a.end || inRightHalf ? "end" : "start", gap: a.end ? -6 : 4 }), items };
|
|
21933
|
+
return { screen, anchor, size: size2, drawnH: listHeight(items.length), pos: place(anchor, size2, screen, { prefer: "below", align: a.end || inRightHalf ? "end" : "start", gap: a.end ? -6 : 4 }), items };
|
|
21933
21934
|
}
|
|
21934
21935
|
if (a.kind === "calendar") {
|
|
21935
21936
|
const date = parseDate(p.value);
|
|
@@ -21944,7 +21945,10 @@ function placeOverlay(spec, a, boxes) {
|
|
|
21944
21945
|
function overlayBoxes(spec, anchors, boxes) {
|
|
21945
21946
|
return anchors.flatMap((a) => {
|
|
21946
21947
|
const o2 = placeOverlay(spec, a, boxes);
|
|
21947
|
-
|
|
21948
|
+
if (!o2) return [];
|
|
21949
|
+
const h2 = "drawnH" in o2 && o2.drawnH != null ? o2.drawnH : o2.size.h;
|
|
21950
|
+
const top = o2.screen.y + o2.pos.y + (o2.pos.side === "above" ? o2.size.h - h2 : 0);
|
|
21951
|
+
return [{ id: a.id, box: { x: o2.screen.x + o2.pos.x, y: top, w: o2.size.w, h: h2 } }];
|
|
21948
21952
|
});
|
|
21949
21953
|
}
|
|
21950
21954
|
function overlayLayer(spec, anchors, boxes, c2, Calendar2) {
|
|
@@ -22928,7 +22932,8 @@ function boardLayout(spec, opts = {}) {
|
|
|
22928
22932
|
}
|
|
22929
22933
|
if (el2.type === "Note") {
|
|
22930
22934
|
const w2 = props.width ?? NOTE_WIDTH;
|
|
22931
|
-
|
|
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 };
|
|
22932
22937
|
}
|
|
22933
22938
|
return null;
|
|
22934
22939
|
});
|
|
@@ -22972,6 +22977,28 @@ function flowLabelGap(spec, laidOut, perRow) {
|
|
|
22972
22977
|
}
|
|
22973
22978
|
return widest ? widest + 32 : 0;
|
|
22974
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
|
+
}
|
|
22975
23002
|
function boardSize(spec, opts = {}) {
|
|
22976
23003
|
const { width, height } = boardLayout(spec, opts);
|
|
22977
23004
|
return { width, height };
|
|
@@ -22982,6 +23009,7 @@ async function renderWireframeSvg(spec, opts = {}) {
|
|
|
22982
23009
|
const issues = checkSpec(spec);
|
|
22983
23010
|
if (issues.length) throw new SpecError(issues);
|
|
22984
23011
|
}
|
|
23012
|
+
spec = await withNoteHeights(spec);
|
|
22985
23013
|
const flowsOn = opts.flows !== false;
|
|
22986
23014
|
const { width, height } = boardLayout(spec, { flows: opts.flows });
|
|
22987
23015
|
spec = withBoardGap(spec, opts.flows);
|
|
@@ -23416,7 +23444,7 @@ async function renderWireframeTool(args) {
|
|
|
23416
23444
|
const { spec, issues } = compileWireframe(args.text);
|
|
23417
23445
|
if (!spec || issues.length) return problems(issues);
|
|
23418
23446
|
const flows = args.flows !== false;
|
|
23419
|
-
const { width, height } =
|
|
23447
|
+
const { width, height } = await measureBoardLayout(spec, { flows });
|
|
23420
23448
|
const scale = Math.min(1, MODEL_IMAGE_MAX / Math.max(width, height));
|
|
23421
23449
|
const png = await renderWireframePng(spec, { skipValidation: true, flows, scale });
|
|
23422
23450
|
const shown = scale < 1 ? `, shown at ${Math.round(width * scale)}\xD7${Math.round(height * scale)}` : "";
|
|
@@ -23502,7 +23530,7 @@ async function renderBlock(doc, source) {
|
|
|
23502
23530
|
if (!spec || issues.length) return problems2(doc, issues.map((i2) => `line ${i2.line}: ${i2.message}`));
|
|
23503
23531
|
try {
|
|
23504
23532
|
const svg = await renderWireframeSvg(spec, { skipValidation: true });
|
|
23505
|
-
const { width, height } =
|
|
23533
|
+
const { width, height } = await measureBoardLayout(spec);
|
|
23506
23534
|
const title = spec.elements[spec.root]?.props?.title;
|
|
23507
23535
|
const figure = doc.createElement("figure");
|
|
23508
23536
|
figure.className = "tsquare-board";
|
|
@@ -23562,13 +23590,15 @@ async function measureElements(spec, lines, opts = {}) {
|
|
|
23562
23590
|
const issues = checkSpec(spec);
|
|
23563
23591
|
if (issues.length) throw new SpecError(issues);
|
|
23564
23592
|
}
|
|
23593
|
+
spec = await withNoteHeights(spec);
|
|
23565
23594
|
const { width, height } = boardLayout(spec, { flows: opts.flows });
|
|
23566
23595
|
spec = withBoardGap(spec, opts.flows);
|
|
23567
23596
|
const els = spec.elements;
|
|
23568
23597
|
const anchors = findAnchors(spec);
|
|
23569
23598
|
const every = Object.keys(els).filter((k) => !["Board", "Screen", "Flow"].includes(els[k].type)).map((id) => ({ id, screen: id, kind: "tooltip", field: false }));
|
|
23570
23599
|
const screens = Object.keys(els).filter((k) => els[k].type === "Screen");
|
|
23571
|
-
const
|
|
23600
|
+
const inners = screens.map((id) => ({ id, screen: id, kind: "tooltip", field: false }));
|
|
23601
|
+
const { tagged, colors } = tagForMeasuring(spec, [...anchors, ...every, ...inners], screens);
|
|
23572
23602
|
const svg = await renderToSvg(tagged, { registry: withMarkers(withPalette(paletteFor())), includeStandard: false, fonts: await loadFonts(), width, height });
|
|
23573
23603
|
const boxes = readMarkers(svg, colors);
|
|
23574
23604
|
const { items } = boardLayout(spec, { flows: opts.flows });
|
|
@@ -23614,9 +23644,10 @@ async function measureElements(spec, lines, opts = {}) {
|
|
|
23614
23644
|
const kids = el2.children ?? [];
|
|
23615
23645
|
if (el2.type === "Screen") {
|
|
23616
23646
|
const frame = frames.get(k) ?? null;
|
|
23617
|
-
const
|
|
23647
|
+
const inner = clip(boxes[k] ?? frame, frame);
|
|
23648
|
+
const body = clip(boxes[`${k}#body`] ?? inner, inner);
|
|
23618
23649
|
const over = (c2) => OVERLAYS.has(els[c2]?.type);
|
|
23619
|
-
for (const c2 of [...kids.filter((c3) => !over(c3)), ...kids.filter(over)]) visit(c2, k, k, BARS.has(els[c2]?.type) || over(c2) ?
|
|
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);
|
|
23620
23651
|
} else {
|
|
23621
23652
|
const inner = el2.type === "Drawer" && boxes[k] ? clip(boxes[k], cut) : cut;
|
|
23622
23653
|
for (const c2 of kids) visit(c2, k, inScreen, inner);
|
|
@@ -23761,6 +23792,7 @@ export {
|
|
|
23761
23792
|
printWireframeText as formatWireframe,
|
|
23762
23793
|
languageData,
|
|
23763
23794
|
mcpTools,
|
|
23795
|
+
measureBoardLayout,
|
|
23764
23796
|
measureElements,
|
|
23765
23797
|
parseWireframeText,
|
|
23766
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 =
|
|
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) => {
|
|
@@ -2422,6 +2422,7 @@ function place(anchor, size, screen, opts) {
|
|
|
2422
2422
|
return { x: x - screen.x, y: y - screen.y, bottom: screen.y + screen.h - (anchor.y - opts.gap), side };
|
|
2423
2423
|
}
|
|
2424
2424
|
var ROW = 36;
|
|
2425
|
+
var listHeight = (rows) => rows * (ROW - 4) + 2 * 4 + 2 * 1.5;
|
|
2425
2426
|
function ListCard({ items, width, selected, c }) {
|
|
2426
2427
|
return /* @__PURE__ */ jsx2("div", { style: { display: "flex", flexDirection: "column", width, padding: 4, backgroundColor: theme.paper, border: `1.5px solid ${theme.lineStrong}`, borderRadius: 8, boxShadow: "0 8px 24px rgba(0,0,0,0.18)" }, children: items.map((item, i) => {
|
|
2427
2428
|
const on = selected != null && item.label === selected;
|
|
@@ -2465,14 +2466,14 @@ function placeOverlay(spec, a, boxes) {
|
|
|
2465
2466
|
if (a.kind === "options") {
|
|
2466
2467
|
const options = p.options?.length ? p.options : [p.value ?? "Option 1", "Option 2", "Option 3"];
|
|
2467
2468
|
const size2 = { w: anchor.w, h: options.length * ROW + 8 };
|
|
2468
|
-
return { screen, anchor, size: size2, pos: place(anchor, size2, screen, { prefer: "below", align: "start", gap: 4 }), options };
|
|
2469
|
+
return { screen, anchor, size: size2, drawnH: listHeight(options.length), pos: place(anchor, size2, screen, { prefer: "below", align: "start", gap: 4 }), options };
|
|
2469
2470
|
}
|
|
2470
2471
|
if (a.kind === "menu") {
|
|
2471
2472
|
const items = p.menu.map((i) => typeof i === "string" ? { label: i } : i);
|
|
2472
2473
|
const width = a.end ? 200 : Math.max(180, anchor.w);
|
|
2473
2474
|
const inRightHalf = anchor.x + anchor.w / 2 > screen.x + screen.w / 2;
|
|
2474
2475
|
const size2 = { w: width, h: items.length * ROW + 8 };
|
|
2475
|
-
return { screen, anchor, size: size2, pos: place(anchor, size2, screen, { prefer: "below", align: a.end || inRightHalf ? "end" : "start", gap: a.end ? -6 : 4 }), items };
|
|
2476
|
+
return { screen, anchor, size: size2, drawnH: listHeight(items.length), pos: place(anchor, size2, screen, { prefer: "below", align: a.end || inRightHalf ? "end" : "start", gap: a.end ? -6 : 4 }), items };
|
|
2476
2477
|
}
|
|
2477
2478
|
if (a.kind === "calendar") {
|
|
2478
2479
|
const date = parseDate(p.value);
|
|
@@ -2487,7 +2488,10 @@ function placeOverlay(spec, a, boxes) {
|
|
|
2487
2488
|
function overlayBoxes(spec, anchors, boxes) {
|
|
2488
2489
|
return anchors.flatMap((a) => {
|
|
2489
2490
|
const o2 = placeOverlay(spec, a, boxes);
|
|
2490
|
-
|
|
2491
|
+
if (!o2) return [];
|
|
2492
|
+
const h = "drawnH" in o2 && o2.drawnH != null ? o2.drawnH : o2.size.h;
|
|
2493
|
+
const top = o2.screen.y + o2.pos.y + (o2.pos.side === "above" ? o2.size.h - h : 0);
|
|
2494
|
+
return [{ id: a.id, box: { x: o2.screen.x + o2.pos.x, y: top, w: o2.size.w, h } }];
|
|
2491
2495
|
});
|
|
2492
2496
|
}
|
|
2493
2497
|
function overlayLayer(spec, anchors, boxes, c, Calendar2) {
|
|
@@ -2981,7 +2985,8 @@ function boardLayout(spec, opts = {}) {
|
|
|
2981
2985
|
}
|
|
2982
2986
|
if (el.type === "Note") {
|
|
2983
2987
|
const w = props.width ?? NOTE_WIDTH;
|
|
2984
|
-
|
|
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 };
|
|
2985
2990
|
}
|
|
2986
2991
|
return null;
|
|
2987
2992
|
});
|
|
@@ -3025,6 +3030,28 @@ function flowLabelGap(spec, laidOut, perRow) {
|
|
|
3025
3030
|
}
|
|
3026
3031
|
return widest ? widest + 32 : 0;
|
|
3027
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
|
+
}
|
|
3028
3055
|
function boardSize(spec, opts = {}) {
|
|
3029
3056
|
const { width, height } = boardLayout(spec, opts);
|
|
3030
3057
|
return { width, height };
|
|
@@ -3035,6 +3062,7 @@ async function renderWireframeSvg(spec, opts = {}) {
|
|
|
3035
3062
|
const issues = checkSpec(spec);
|
|
3036
3063
|
if (issues.length) throw new SpecError(issues);
|
|
3037
3064
|
}
|
|
3065
|
+
spec = await withNoteHeights(spec);
|
|
3038
3066
|
const flowsOn = opts.flows !== false;
|
|
3039
3067
|
const { width, height } = boardLayout(spec, { flows: opts.flows });
|
|
3040
3068
|
spec = withBoardGap(spec, opts.flows);
|
|
@@ -3408,6 +3436,8 @@ export {
|
|
|
3408
3436
|
SpecError,
|
|
3409
3437
|
checkSpec,
|
|
3410
3438
|
boardLayout,
|
|
3439
|
+
withNoteHeights,
|
|
3440
|
+
measureBoardLayout,
|
|
3411
3441
|
boardSize,
|
|
3412
3442
|
renderWireframeSvg,
|
|
3413
3443
|
withBoardGap,
|
package/dist/cli.js
CHANGED
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-
|
|
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-
|
|
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 } =
|
|
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 } =
|
|
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
|
|
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
|
|
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) ?
|
|
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,14 +32,14 @@ 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,
|
|
36
|
-
*
|
|
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;
|
|
40
40
|
/** Screens: the band above the frame with the screen's name. */
|
|
41
41
|
labelBox?: MeasuredBox;
|
|
42
|
-
/** Its open menu, option list or date picker, and its tooltip, in drawing order (
|
|
42
|
+
/** Its open menu, option list or date picker, and its tooltip, in drawing order (a tooltip's and a date picker's size within a few px). */
|
|
43
43
|
overlays?: MeasuredBox[];
|
|
44
44
|
/** Modals and drawers: whether the screen behind is dimmed (on unless the text says no-scrim). */
|
|
45
45
|
scrim?: boolean;
|
package/dist/playground/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
languageData
|
|
3
|
-
} from "../chunks/chunk-
|
|
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-
|
|
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 } =
|
|
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;
|