tsquare 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  languageData
3
- } from "./chunks/chunk-2FU5Z5ON.js";
3
+ } from "./chunks/chunk-PGX7ANZH.js";
4
4
  import {
5
5
  DEVICES,
6
+ LABEL_H,
7
+ PRIMARY_PROP,
6
8
  RENAMED_PROPS,
9
+ SpecError,
7
10
  WireframeError,
8
11
  boardLayout,
9
12
  boardSize,
@@ -11,20 +14,32 @@ import {
11
14
  checkSpec,
12
15
  compileWireframe,
13
16
  componentDefinitions,
17
+ findAnchors,
18
+ flowEnds,
19
+ flowPolyline,
14
20
  formatIssues,
15
21
  languageReference,
22
+ layoutFlows,
23
+ loadFonts,
24
+ overlayBoxes,
25
+ paletteFor,
16
26
  parseWireframeText,
17
27
  printWireframeText,
28
+ readMarkers,
18
29
  registry,
19
30
  renderWireframe,
20
31
  renderWireframePng,
21
32
  renderWireframeSvg,
22
33
  repairPrompt,
34
+ tagForMeasuring,
23
35
  theme,
24
36
  upgradeSpec,
25
37
  upgradeWireframe,
26
- wireframePrompt
27
- } from "./chunks/chunk-H4Y6VBEX.js";
38
+ wireframePrompt,
39
+ withBoardGap,
40
+ withMarkers,
41
+ withPalette
42
+ } from "./chunks/chunk-YOZFCNSK.js";
28
43
 
29
44
  // src/deflate.ts
30
45
  import { deflateRawSync, inflateRawSync } from "node:zlib";
@@ -240,6 +255,151 @@ function addStyles(doc) {
240
255
  style.textContent = STYLES;
241
256
  doc.head.append(style);
242
257
  }
258
+
259
+ // src/measure.ts
260
+ import { renderToSvg } from "@json-render/image/render";
261
+ var isFlow = (e) => e.type === "Flow";
262
+ var COVERS_SCREEN = /* @__PURE__ */ new Set(["Modal", "Drawer"]);
263
+ var OVERLAYS = /* @__PURE__ */ new Set(["Modal", "Drawer", "Toast"]);
264
+ var CONTAINERS = new Set(Object.entries(componentDefinitions).filter(([, d]) => d.slots.length).map(([name]) => name));
265
+ var BARS = /* @__PURE__ */ new Set(["NavBar", "TabBar"]);
266
+ var toBox = (r) => ({ x: round(r.x), y: round(r.y), width: round(r.w), height: round(r.h) });
267
+ var round = (n) => Math.round(n * 100) / 100;
268
+ function clip(r, to) {
269
+ if (!to) return r;
270
+ const x = Math.max(r.x, to.x), y = Math.max(r.y, to.y);
271
+ const w = Math.min(r.x + r.w, to.x + to.w) - x, h = Math.min(r.y + r.h, to.y + to.h) - y;
272
+ return w >= 0 && h >= 0 && (w > 0 || r.w === 0) && (h > 0 || r.h === 0) ? { x, y, w, h } : null;
273
+ }
274
+ async function measureElements(spec, lines, opts = {}) {
275
+ upgradeSpec(spec);
276
+ if (!opts.skipValidation) {
277
+ const issues = checkSpec(spec);
278
+ if (issues.length) throw new SpecError(issues);
279
+ }
280
+ const { width, height } = boardLayout(spec, { flows: opts.flows });
281
+ spec = withBoardGap(spec, opts.flows);
282
+ const els = spec.elements;
283
+ const anchors = findAnchors(spec);
284
+ const every = Object.keys(els).filter((k) => !["Board", "Screen", "Flow"].includes(els[k].type)).map((id) => ({ id, screen: id, kind: "tooltip", field: false }));
285
+ const screens = Object.keys(els).filter((k) => els[k].type === "Screen");
286
+ const { tagged, colors } = tagForMeasuring(spec, [...anchors, ...every], screens);
287
+ const svg = await renderToSvg(tagged, { registry: withMarkers(withPalette(paletteFor())), includeStandard: false, fonts: await loadFonts(), width, height });
288
+ const boxes = readMarkers(svg, colors);
289
+ const { items } = boardLayout(spec, { flows: opts.flows });
290
+ const laidOut = (els[spec.root].children ?? []).filter((k) => els[k]?.type === "Screen" || els[k]?.type === "Note");
291
+ const frames = /* @__PURE__ */ new Map(), bands = /* @__PURE__ */ new Map();
292
+ laidOut.forEach((k, i) => {
293
+ const it = items[i];
294
+ if (!it || els[k].type !== "Screen") return;
295
+ frames.set(k, { x: it.x, y: it.y + LABEL_H, w: it.width, h: it.height - LABEL_H });
296
+ bands.set(k, { x: it.x, y: it.y, w: it.width, h: LABEL_H });
297
+ });
298
+ const overlays = /* @__PURE__ */ new Map();
299
+ for (const o of overlayBoxes(spec, anchors, boxes)) overlays.set(o.id, [...overlays.get(o.id) ?? [], o.box]);
300
+ const lineOf = (k) => k == null ? null : lines[k] ?? 0;
301
+ const endLine = (k) => Math.max(lines[k] ?? 0, ...(els[k].children ?? []).filter((c) => els[c]).map(endLine));
302
+ const labelOf = (k) => {
303
+ const p = els[k].props ?? {};
304
+ const v = p[PRIMARY_PROP[els[k].type] ?? ""];
305
+ return typeof v === "string" ? v : void 0;
306
+ };
307
+ const out = [];
308
+ const visit = (k, parent, screen, cut) => {
309
+ const el = els[k];
310
+ if (!el || el.type === "Flow") return;
311
+ const p = el.props ?? {};
312
+ const base = { line: lineOf(k), endLine: endLine(k), ...typeof p.id === "string" ? { id: p.id } : {}, type: el.type, ...labelOf(k) != null ? { label: labelOf(k) } : {}, screenLine: lineOf(screen), parentLine: lineOf(parent) };
313
+ let entry = null;
314
+ if (el.type === "Board") entry = { ...base, box: { x: 0, y: 0, width, height } };
315
+ else if (el.type === "Screen") {
316
+ const frame = frames.get(k);
317
+ if (frame) entry = { ...base, box: toBox(frame), labelBox: toBox(bands.get(k)) };
318
+ } else {
319
+ const r = boxes[k];
320
+ const seen = r && clip(r, cut);
321
+ if (seen) {
322
+ const view = screen ? boxes[screen] ?? frames.get(screen) : void 0;
323
+ const os = (overlays.get(k) ?? []).map((o) => clip(o, view)).filter((o) => !!o && o.w > 0 && o.h > 0);
324
+ entry = { ...base, box: toBox(seen), ...os.length ? { overlays: os.map(toBox) } : {}, ...COVERS_SCREEN.has(el.type) ? { scrim: p.scrim ?? true } : {} };
325
+ }
326
+ }
327
+ if (entry) out.push(entry);
328
+ const inScreen = el.type === "Screen" ? k : screen;
329
+ const kids = el.children ?? [];
330
+ if (el.type === "Screen") {
331
+ const frame = frames.get(k) ?? null;
332
+ const body = clip(boxes[`${k}#body`] ?? frame, frame);
333
+ 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);
335
+ } else {
336
+ const inner = el.type === "Drawer" && boxes[k] ? clip(boxes[k], cut) : cut;
337
+ for (const c of kids) visit(c, k, inScreen, inner);
338
+ }
339
+ };
340
+ visit(spec.root, null, null, null);
341
+ if (opts.flows !== false) {
342
+ const flows = flowEnds(spec);
343
+ const resolved = flows.resolve(boxes);
344
+ const ends = new Map(flows.list.map((f) => [f.key, f]));
345
+ layoutFlows(spec, resolved, flows.screens, flows.gap, paletteFor()).forEach((f) => {
346
+ const p = els[f.key].props ?? {};
347
+ const e = ends.get(f.key);
348
+ out.push({
349
+ line: lineOf(f.key),
350
+ endLine: lineOf(f.key),
351
+ type: "Flow",
352
+ ...f.label ? { label: f.label.text } : {},
353
+ from: String(p.from),
354
+ to: String(p.to),
355
+ fromLine: lineOf(e.from),
356
+ toLine: lineOf(e.to),
357
+ path: flowPolyline(f).map((q) => [round(q.x), round(q.y)]),
358
+ ...f.label ? { labelBox: toBox(f.label) } : {}
359
+ });
360
+ });
361
+ }
362
+ return out;
363
+ }
364
+ var inside = (b, x, y) => !!b && b.width > 0 && b.height > 0 && x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
365
+ var center = (b) => [b.x + b.width / 2, b.y + b.height / 2];
366
+ function distanceToPath(path, x, y) {
367
+ let best = Infinity;
368
+ for (let i = 1; i < path.length; i++) {
369
+ const [ax, ay] = path[i - 1], [bx, by] = path[i];
370
+ const dx = bx - ax, dy = by - ay, len = dx * dx + dy * dy;
371
+ const t = len ? Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / len)) : 0;
372
+ best = Math.min(best, Math.hypot(x - ax - t * dx, y - ay - t * dy));
373
+ }
374
+ return best;
375
+ }
376
+ var isContainer = (e) => CONTAINERS.has(e.type);
377
+ function elementsAt(elements, x, y, opts = {}) {
378
+ const tolerance = opts.tolerance ?? 6;
379
+ const order = new Map(elements.map((e, i) => [e, i]));
380
+ const topFirst = (a, b) => order.get(b) - order.get(a);
381
+ const covered = /* @__PURE__ */ new Set();
382
+ const screens = elements.filter((e) => e.type === "Screen");
383
+ const screenAround = (b) => screens.find((s) => inside(s.box, ...center(b)));
384
+ for (const [i, m] of elements.entries()) {
385
+ if (isFlow(m) || !OVERLAYS.has(m.type)) continue;
386
+ const screen = screenAround(m.box);
387
+ if (!screen || !inside(m.box, x, y) && !(m.scrim && inside(screen.box, x, y))) continue;
388
+ for (const e of elements.slice(0, i)) if (!isFlow(e) && e.type !== "Screen" && e.type !== "Board" && screenAround(e.box) === screen) covered.add(e);
389
+ }
390
+ const owners = [], labels = [], leaves = [], lines = [], containers = [];
391
+ for (const e of elements) {
392
+ if (isFlow(e)) {
393
+ if (inside(e.labelBox, x, y)) labels.push(e);
394
+ else if (distanceToPath(e.path, x, y) <= tolerance) lines.push(e);
395
+ } else if (e.overlays?.some((o) => inside(o, x, y))) owners.push(e);
396
+ else if (!covered.has(e) && (inside(e.box, x, y) || inside(e.labelBox, x, y))) (isContainer(e) ? containers : leaves).push(e);
397
+ }
398
+ return [owners, labels, leaves, lines, containers].flatMap((g) => g.sort(topFirst));
399
+ }
400
+ function elementsAtLine(elements, line) {
401
+ return elements.filter((e) => e.line > 0 && e.line <= line && line <= e.endLine).sort((a, b) => b.line - a.line);
402
+ }
243
403
  export {
244
404
  DEFAULT_LINKS,
245
405
  DEFAULT_SELECTOR,
@@ -257,11 +417,14 @@ export {
257
417
  compileWireframe,
258
418
  componentDefinitions,
259
419
  decodeWireframe,
420
+ elementsAt,
421
+ elementsAtLine,
260
422
  encodeWireframe,
261
423
  formatIssues,
262
424
  printWireframeText as formatWireframe,
263
425
  languageData,
264
426
  mcpTools,
427
+ measureElements,
265
428
  parseWireframeText,
266
429
  registry,
267
430
  renderAll,
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Element positions: where every element and flow of a board is drawn, for laying hit boxes over
3
+ * the image (click an element to comment on it or ask for a change), and what is under a point.
4
+ *
5
+ * measureElements renders the board once more with a marker on every element (the measuring pass
6
+ * of anchored overlays, see anchors.tsx) and reads the boxes back. Flow lines come from the same
7
+ * layout the renderer draws them with. elementsAt and elementsAtLine are plain lookups on its result,
8
+ * so measure once per text and look up as often as needed (they also run on the JSON in a browser).
9
+ */
10
+ import type { Spec } from "@json-render/core";
11
+ import { type ComponentName } from "./catalog.js";
12
+ /** A box on the board, in board pixels: the same coordinates as the rendered SVG. */
13
+ export interface MeasuredBox {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ }
19
+ export interface MeasuredElement {
20
+ /** The element's line in the wireframe text; 0 when `lines` doesn't have it. */
21
+ line: number;
22
+ /** The last line of its block (its children included); `line` when it has no children. */
23
+ endLine: number;
24
+ /** Its #id, when the text gives one. */
25
+ id?: string;
26
+ /** "Button", "Screen", … */
27
+ type: Exclude<ComponentName, "Flow">;
28
+ /** Its main text: a button's label, a screen's name, a heading's text. */
29
+ label?: string;
30
+ /** The line of the screen it's on; null for the board, screens and notes beside them. */
31
+ screenLine: number | null;
32
+ /** The line of the element containing it; null for the board. */
33
+ parentLine: number | null;
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
37
+ * (a spacer in a row): such an element is listed, but never under a point.
38
+ */
39
+ box: MeasuredBox;
40
+ /** Screens: the band above the frame with the screen's name. */
41
+ labelBox?: MeasuredBox;
42
+ /** Its open menu, option list or date picker, and its tooltip, in drawing order (sizes partly estimated from the text). */
43
+ overlays?: MeasuredBox[];
44
+ /** Modals and drawers: whether the screen behind is dimmed (on unless the text says no-scrim). */
45
+ scrim?: boolean;
46
+ }
47
+ export interface MeasuredFlow {
48
+ /** The flow's line in the wireframe text; 0 when `lines` doesn't have it. */
49
+ line: number;
50
+ /** Always `line`: a flow has no children. */
51
+ endLine: number;
52
+ type: "Flow";
53
+ label?: string;
54
+ /** The ids it connects, as written in the flow line. */
55
+ from: string;
56
+ to: string;
57
+ /** The lines of the elements (or screens) it connects. */
58
+ fromLine: number;
59
+ toLine: number;
60
+ /** The drawn line as points, from the tip of one end's marker to the other's; curves and corners are sampled. */
61
+ path: [number, number][];
62
+ /** The label pill. */
63
+ labelBox?: MeasuredBox;
64
+ }
65
+ export type Measured = MeasuredElement | MeasuredFlow;
66
+ export interface MeasureOptions {
67
+ /** As passed to renderWireframeSvg (default true): flow labels can widen the gaps between screens, so the boxes only match a render with the same setting. */
68
+ flows?: boolean;
69
+ /** Skip validation (as renderWireframeSvg). */
70
+ skipValidation?: boolean;
71
+ }
72
+ /**
73
+ * Every element and flow on the board and where it's drawn, in drawing order (what's drawn later
74
+ * sits on top), starting with the board. `lines` maps the spec's element keys to source lines:
75
+ * compileWireframe returns it.
76
+ * Elements that aren't drawn (content entirely cut off, children of a closed accordion) are left out.
77
+ */
78
+ export declare function measureElements(spec: Spec, lines: Record<string, number>, opts?: MeasureOptions): Promise<[MeasuredElement, ...Measured[]]>;
79
+ /**
80
+ * Everything under a point (board pixels), most likely meant first:
81
+ * 1. an open menu, option list, date picker or tooltip: the element it belongs to
82
+ * 2. a flow's label
83
+ * 3. elements that hold no others (buttons, inputs, text, …), topmost first
84
+ * 4. a flow's line, within `tolerance` px of it (default 6; pass more when the board is shown zoomed out)
85
+ * 5. stacks, cards, modals and drawers, then the screen, then the board
86
+ * A modal, drawer or toast hides the screen's content under it. One that dims its screen (modals and
87
+ * drawers, unless no-scrim) hides all of it: on the dimmed part, only the screen and the board.
88
+ */
89
+ export declare function elementsAt(elements: Measured[], x: number, y: number, opts?: {
90
+ tolerance?: number;
91
+ }): Measured[];
92
+ /**
93
+ * The element on a line and every block around it, innermost first: for a code view that follows
94
+ * the caret. A comment or blank line between an element's children gives that element. A flow's
95
+ * line gives the flow, then the board.
96
+ */
97
+ export declare function elementsAtLine(elements: Measured[], line: number): Measured[];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  languageData
3
- } from "../chunks/chunk-2FU5Z5ON.js";
3
+ } from "../chunks/chunk-PGX7ANZH.js";
4
4
  import {
5
5
  boardLayout,
6
6
  compileWireframe,
@@ -9,7 +9,7 @@ import {
9
9
  renderWireframePng,
10
10
  renderWireframeSvg,
11
11
  wireframePrompt
12
- } from "../chunks/chunk-H4Y6VBEX.js";
12
+ } from "../chunks/chunk-YOZFCNSK.js";
13
13
 
14
14
  // src/playground/index.ts
15
15
  import { createServer } from "node:http";
package/dist/render.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Spec } from "@json-render/core";
2
2
  import { type Anchor, type Rect } from "./anchors.js";
3
+ import { type FlowBoxes } from "./flows.js";
3
4
  export declare class SpecError extends Error {
4
5
  issues: string[];
5
6
  constructor(issues: string[]);
@@ -43,6 +44,29 @@ export interface RenderWireframeOptions {
43
44
  flows?: boolean;
44
45
  }
45
46
  export declare function renderWireframeSvg(spec: Spec, opts?: RenderWireframeOptions): Promise<string>;
47
+ /**
48
+ * The spec as it's drawn: a labeled flow between neighboring screens can widen the board's gap
49
+ * (boardLayout), and the Board component reads the gap from its props.
50
+ */
51
+ export declare function withBoardGap(spec: Spec, flows?: boolean): Spec;
52
+ /**
53
+ * The flows on a board and how to find their ends: element ends are measured (`measure`), screen
54
+ * ends come from the board layout. `resolve(boxes)` gives each drawable flow's end boxes once measured.
55
+ */
56
+ export declare function flowEnds(spec: Spec): {
57
+ list: {
58
+ key: string;
59
+ from: string;
60
+ to: string;
61
+ }[];
62
+ measure: string[];
63
+ screens: Rect[];
64
+ gap: any;
65
+ resolve(boxes: Record<string, Rect>): {
66
+ key: string;
67
+ ends: FlowBoxes;
68
+ }[];
69
+ };
46
70
  /**
47
71
  * The measuring pass: the board's anchored elements (and their screens) and their boxes on the
48
72
  * canvas, keyed by element id (`<id>#field` for a field). No render at all when there are none.