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.
@@ -706,6 +706,7 @@ function markRoot(root, color) {
706
706
  const kids = Children.toArray(node.props.children);
707
707
  return cloneElement(node, {}, ...kids, /* @__PURE__ */ jsx(Marker, { color }, "__marker"));
708
708
  }
709
+ var MARKS_ITSELF = /* @__PURE__ */ new Set(["Screen", "Modal", "Drawer", "Toast"]);
709
710
  function withMarkers(reg) {
710
711
  return Object.fromEntries(
711
712
  Object.entries(reg).map(([name, C]) => [
@@ -713,7 +714,7 @@ function withMarkers(reg) {
713
714
  (props) => {
714
715
  const root = C(props);
715
716
  const color = props.element?.props?.__mark;
716
- return color && name !== "Screen" ? markRoot(root, color) : root;
717
+ return color && !MARKS_ITSELF.has(name) ? markRoot(root, color) : root;
717
718
  }
718
719
  ])
719
720
  );
@@ -836,6 +837,16 @@ function BrowserBar() {
836
837
  }
837
838
  );
838
839
  }
840
+ function mayOverflow(name, width) {
841
+ let w = 0;
842
+ for (const ch of name) {
843
+ if (ch.charCodeAt(0) >= 128 || /[MWmw@%&]/.test(ch)) w += 16.5;
844
+ else if (/[A-Z]/.test(ch)) w += 12.5;
845
+ else if (/[ .,:;'!|il1jft()[\]{}"`-]/.test(ch)) w += 8.5;
846
+ else w += 11;
847
+ }
848
+ return w > width;
849
+ }
839
850
  function Screen({ element, children }) {
840
851
  const p = element.props;
841
852
  const s = screenSize(p);
@@ -851,7 +862,12 @@ function Screen({ element, children }) {
851
862
  else body.push(c);
852
863
  }
853
864
  return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", flexShrink: 0 }, children: [
854
- /* @__PURE__ */ jsx(Box, { style: { height: LABEL_H, fontSize: 16, fontWeight: 600, color: theme.text }, children: p.name ?? "" }),
865
+ mayOverflow(p.name ?? "", s.width) ? (
866
+ // the board's layout sizes each column by its screen, so a name that may be wider than the
867
+ // screen gets the screen's width and ends in "…" instead of widening the column. Only then:
868
+ // the clip changes how the text is antialiased, and names that fit should render as before.
869
+ /* @__PURE__ */ jsx(Box, { style: { width: s.width, height: LABEL_H, fontSize: 16, fontWeight: 600, color: theme.text }, children: /* @__PURE__ */ jsx("div", { style: { overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis" }, children: p.name }) })
870
+ ) : /* @__PURE__ */ jsx(Box, { style: { height: LABEL_H, fontSize: 16, fontWeight: 600, color: theme.text }, children: p.name ?? "" }),
855
871
  /* @__PURE__ */ jsxs(
856
872
  Box,
857
873
  {
@@ -872,19 +888,22 @@ function Screen({ element, children }) {
872
888
  s.chrome && s.device === "desktop" ? /* @__PURE__ */ jsx(BrowserBar, {}) : null,
873
889
  /* @__PURE__ */ jsxs(Box, { style: { position: "relative", flexGrow: 1, flexBasis: "0px", flexDirection: "column", overflow: "hidden" }, children: [
874
890
  top,
875
- /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, flexBasis: "0px", flexDirection: "column", overflow: "hidden" }, children: /* @__PURE__ */ jsx(
876
- Box,
877
- {
878
- style: {
879
- flexGrow: 1,
880
- flexShrink: 0,
881
- flexDirection: "column",
882
- padding: s.padding,
883
- gap: p.gap ?? 14
884
- },
885
- children: withDir(body, "column")
886
- }
887
- ) }),
891
+ /* @__PURE__ */ jsxs(Box, { style: { flexGrow: 1, flexBasis: "0px", flexDirection: "column", overflow: "hidden" }, children: [
892
+ /* @__PURE__ */ jsx(
893
+ Box,
894
+ {
895
+ style: {
896
+ flexGrow: 1,
897
+ flexShrink: 0,
898
+ flexDirection: "column",
899
+ padding: s.padding,
900
+ gap: p.gap ?? 14
901
+ },
902
+ children: withDir(body, "column")
903
+ }
904
+ ),
905
+ p.__markBody ? /* @__PURE__ */ jsx(Marker, { color: p.__markBody }) : null
906
+ ] }),
888
907
  bottom,
889
908
  overlays.map((o2) => isValidElement(o2) ? cloneElement(o2, { aboveTabBar: bottom.length > 0 }) : o2),
890
909
  p.__mark ? /* @__PURE__ */ jsx(Marker, { color: p.__mark }) : null
@@ -1739,7 +1758,8 @@ function Modal({ element, children }) {
1739
1758
  },
1740
1759
  children: [
1741
1760
  p.title ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 18, fontWeight: 600 }, children: p.title }) : null,
1742
- withDir(children, "column")
1761
+ withDir(children, "column"),
1762
+ p.__mark ? /* @__PURE__ */ jsx(Marker, { color: p.__mark }) : null
1743
1763
  ]
1744
1764
  }
1745
1765
  ) });
@@ -1774,7 +1794,8 @@ function Drawer({ element, children }) {
1774
1794
  /* @__PURE__ */ jsx(Box, { style: { fontSize: 18, fontWeight: 600 }, children: p.title }),
1775
1795
  /* @__PURE__ */ jsx(IconGlyph, { name: "x", size: 22, color: theme.muted })
1776
1796
  ] }) : null,
1777
- withDir(children, "column")
1797
+ withDir(children, "column"),
1798
+ p.__mark ? /* @__PURE__ */ jsx(Marker, { color: p.__mark }) : null
1778
1799
  ]
1779
1800
  }
1780
1801
  ) });
@@ -1787,7 +1808,8 @@ function Toast({ element, aboveTabBar }) {
1787
1808
  return /* @__PURE__ */ jsx(Box, { style: { position: "absolute", left: 16, right: 16, justifyContent: side, ...top ? { top: 16 } : { bottom: aboveTabBar ? 92 : 24 } }, children: /* @__PURE__ */ jsxs(Box, { style: { alignItems: "center", gap: 12, padding: "12px 16px", borderRadius: 12, backgroundColor: theme.ink, color: theme.onPrimary, fontSize: 14, boxShadow: "0 6px 20px rgba(0,0,0,0.22)", maxWidth: 420 }, children: [
1788
1809
  p.icon ? /* @__PURE__ */ jsx(IconGlyph, { name: p.icon, size: 18, color: theme.onPrimary }) : null,
1789
1810
  /* @__PURE__ */ jsx(Box, { style: { flexShrink: 1 }, children: p.text ?? "" }),
1790
- p.action ? /* @__PURE__ */ jsx(Box, { style: { fontWeight: 700, marginLeft: 8, flexShrink: 0 }, children: p.action }) : null
1811
+ p.action ? /* @__PURE__ */ jsx(Box, { style: { fontWeight: 700, marginLeft: 8, flexShrink: 0 }, children: p.action }) : null,
1812
+ p.__mark ? /* @__PURE__ */ jsx(Marker, { color: p.__mark }) : null
1791
1813
  ] }) });
1792
1814
  }
1793
1815
  function Flow() {
@@ -2360,22 +2382,23 @@ function findAnchors(spec) {
2360
2382
  return [...out, ...tips];
2361
2383
  }
2362
2384
  var MARK = (i) => `#fe${(i >> 8).toString(16).padStart(2, "0")}${(i & 255).toString(16).padStart(2, "0")}`;
2363
- function tagForMeasuring(spec, anchors) {
2385
+ function tagForMeasuring(spec, anchors, bodies = []) {
2364
2386
  const tagged = structuredClone(spec);
2365
2387
  const colors = /* @__PURE__ */ new Map();
2366
2388
  let n = 1;
2367
- const tag = (id, field) => {
2368
- const key = field ? `${id}#field` : id;
2389
+ const tag = (id, part) => {
2390
+ const key = part ? `${id}#${part}` : id;
2369
2391
  if (colors.has(key)) return;
2370
2392
  const color = MARK(n++);
2371
2393
  colors.set(key, color);
2372
2394
  const el = tagged.elements[id];
2373
- el.props = { ...el.props ?? {}, [field ? "__markField" : "__mark"]: color };
2395
+ el.props = { ...el.props ?? {}, [part === "field" ? "__markField" : part === "body" ? "__markBody" : "__mark"]: color };
2374
2396
  };
2375
2397
  for (const a of anchors) {
2376
- tag(a.screen, false);
2377
- tag(a.id, a.field);
2398
+ tag(a.screen, "");
2399
+ tag(a.id, a.field ? "field" : "");
2378
2400
  }
2401
+ for (const id of bodies) tag(id, "body");
2379
2402
  return { tagged, colors };
2380
2403
  }
2381
2404
  function readMarkers(svg, colors) {
@@ -2434,35 +2457,54 @@ function tooltipSize(text) {
2434
2457
  const lines = Math.max(1, Math.ceil(text.length * 7.4 / 216));
2435
2458
  return { w: width, h: lines * 18 + 12 + 6 };
2436
2459
  }
2460
+ function placeOverlay(spec, a, boxes) {
2461
+ const screen = boxes[a.screen];
2462
+ const anchor = boxes[a.field ? `${a.id}#field` : a.id];
2463
+ if (!screen || !anchor) return null;
2464
+ const p = propsOf(spec.elements, a.id);
2465
+ if (a.kind === "options") {
2466
+ const options = p.options?.length ? p.options : [p.value ?? "Option 1", "Option 2", "Option 3"];
2467
+ 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
+ }
2470
+ if (a.kind === "menu") {
2471
+ const items = p.menu.map((i) => typeof i === "string" ? { label: i } : i);
2472
+ const width = a.end ? 200 : Math.max(180, anchor.w);
2473
+ const inRightHalf = anchor.x + anchor.w / 2 > screen.x + screen.w / 2;
2474
+ 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
+ }
2477
+ if (a.kind === "calendar") {
2478
+ const date = parseDate(p.value);
2479
+ const { start, days } = monthGrid(date?.month);
2480
+ const weeks = Math.ceil((start + days) / 7);
2481
+ const size2 = { w: 296, h: 92 + weeks * 38 };
2482
+ return { screen, anchor, size: size2, pos: place(anchor, size2, screen, { prefer: "below", align: "start", gap: 4 }), date };
2483
+ }
2484
+ const size = tooltipSize(p.tooltip);
2485
+ return { screen, anchor, size, pos: place(anchor, size, screen, { prefer: "above", align: "center", gap: 4 }) };
2486
+ }
2487
+ function overlayBoxes(spec, anchors, boxes) {
2488
+ return anchors.flatMap((a) => {
2489
+ const o2 = placeOverlay(spec, a, boxes);
2490
+ return o2 ? [{ id: a.id, box: { x: o2.screen.x + o2.pos.x, y: o2.screen.y + o2.pos.y, w: o2.size.w, h: o2.size.h } }] : [];
2491
+ });
2492
+ }
2437
2493
  function overlayLayer(spec, anchors, boxes, c, Calendar2) {
2438
2494
  const byScreen = /* @__PURE__ */ new Map();
2439
2495
  for (const a of anchors) {
2440
- const screen = boxes[a.screen];
2441
- const anchor = boxes[a.field ? `${a.id}#field` : a.id];
2442
- if (!screen || !anchor) continue;
2496
+ const o2 = placeOverlay(spec, a, boxes);
2497
+ if (!o2) continue;
2498
+ const { screen, anchor, size, pos } = o2;
2443
2499
  const p = propsOf(spec.elements, a.id);
2444
2500
  let node = null;
2445
- let pos = null;
2446
2501
  if (a.kind === "options") {
2447
- const options = p.options?.length ? p.options : [p.value ?? "Option 1", "Option 2", "Option 3"];
2448
- const size = { w: anchor.w, h: options.length * ROW + 8 };
2449
- pos = place(anchor, size, screen, { prefer: "below", align: "start", gap: 4 });
2450
- node = /* @__PURE__ */ jsx2(ListCard, { items: options.map((label) => ({ label })), width: size.w, selected: p.value, c });
2502
+ node = /* @__PURE__ */ jsx2(ListCard, { items: o2.options.map((label) => ({ label })), width: size.w, selected: p.value, c });
2451
2503
  } else if (a.kind === "menu") {
2452
- const items = p.menu.map((i) => typeof i === "string" ? { label: i } : i);
2453
- const width = a.end ? 200 : Math.max(180, anchor.w);
2454
- const inRightHalf = anchor.x + anchor.w / 2 > screen.x + screen.w / 2;
2455
- pos = place(anchor, { w: width, h: items.length * ROW + 8 }, screen, { prefer: "below", align: a.end || inRightHalf ? "end" : "start", gap: a.end ? -6 : 4 });
2456
- node = /* @__PURE__ */ jsx2(ListCard, { items, width, c });
2504
+ node = /* @__PURE__ */ jsx2(ListCard, { items: o2.items, width: size.w, c });
2457
2505
  } else if (a.kind === "calendar") {
2458
- const date = parseDate(p.value);
2459
- const { start, days } = monthGrid(date?.month);
2460
- const weeks = Math.ceil((start + days) / 7);
2461
- pos = place(anchor, { w: 296, h: 92 + weeks * 38 }, screen, { prefer: "below", align: "start", gap: 4 });
2462
- node = /* @__PURE__ */ jsx2("div", { style: { display: "flex", boxShadow: "0 8px 24px rgba(0,0,0,0.18)", borderRadius: 12 }, children: Calendar2({ element: { type: "Calendar", props: { month: date?.month ?? "Month", selected: date?.day } }, colors: c }) });
2506
+ node = /* @__PURE__ */ jsx2("div", { style: { display: "flex", boxShadow: "0 8px 24px rgba(0,0,0,0.18)", borderRadius: 12 }, children: Calendar2({ element: { type: "Calendar", props: { month: o2.date?.month ?? "Month", selected: o2.date?.day } }, colors: c }) });
2463
2507
  } else {
2464
- const size = tooltipSize(p.tooltip);
2465
- pos = place(anchor, size, screen, { prefer: "above", align: "center", gap: 4 });
2466
2508
  node = /* @__PURE__ */ jsx2(Tooltip, { text: p.tooltip, side: pos.side, width: size.w, arrowX: anchor.x + anchor.w / 2 - (screen.x + pos.x) });
2467
2509
  }
2468
2510
  const list = byScreen.get(a.screen) ?? [];
@@ -2604,9 +2646,7 @@ function flowColor(color, palette) {
2604
2646
  const s = String(color);
2605
2647
  return ACCENTS[s] ?? s;
2606
2648
  }
2607
- function flowLayer(spec, flows, screens, gap, palette, size) {
2608
- const shapes = [];
2609
- const labels = [];
2649
+ function layoutFlows(spec, flows, screens, gap, palette) {
2610
2650
  const placed = [];
2611
2651
  const drawn = [];
2612
2652
  const tips = flows.flatMap(({ ends: ends2 }) => {
@@ -2625,20 +2665,16 @@ function flowLayer(spec, flows, screens, gap, palette, size) {
2625
2665
  return r;
2626
2666
  });
2627
2667
  const ends = routes.flatMap((r) => [r.points[0], r.points[r.points.length - 1]]);
2628
- flows.forEach(({ key }, i) => {
2668
+ return flows.map(({ key }, i) => {
2629
2669
  const r = routes[i];
2630
2670
  const p = spec.elements[key].props ?? {};
2631
- const color = flowColor(p.color, palette);
2632
2671
  const start = p.start ?? "none", end = p.end ?? "arrow";
2633
2672
  const pts = [...r.points];
2634
2673
  pts[0] = add(pts[0], DIR[r.from], INSET[start]);
2635
2674
  pts[pts.length - 1] = add(pts[pts.length - 1], DIR[r.to], INSET[end]);
2636
- const { d, at } = linePath(r, pts, p.line ?? "rounded");
2637
- shapes.push(
2638
- /* @__PURE__ */ jsx3("path", { d, fill: "none", stroke: color, strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round", ...p.dashed ? { strokeDasharray: "7 6" } : {} }, `${key}-line`),
2639
- marker(start, r.points[0], DIR[r.from], color, `${key}-start`),
2640
- marker(end, r.points[r.points.length - 1], DIR[r.to], color, `${key}-end`)
2641
- );
2675
+ const line = p.line ?? "rounded";
2676
+ const { d, at } = linePath(r, pts, line);
2677
+ const out = { key, route: r, points: pts, d, color: flowColor(p.color, palette), start, end, line, dashed: !!p.dashed };
2642
2678
  if (p.label) {
2643
2679
  const w = flowLabelWidth(String(p.label));
2644
2680
  const covers = (c) => placed.filter((q) => c.x - w / 2 < q.x + q.w && q.x < c.x + w / 2 && c.y - 14 < q.y + q.h && q.y < c.y + 14).length + ends.filter((q) => Math.abs(q.x - c.x) < w / 2 + 12 && Math.abs(q.y - c.y) < 24).length;
@@ -2648,13 +2684,58 @@ function flowLayer(spec, flows, screens, gap, palette, size) {
2648
2684
  const c = at(t), n = covers(c);
2649
2685
  if (n < best) [mid, best] = [c, n];
2650
2686
  }
2651
- const top = mid.y - 12;
2652
- placed.push({ x: mid.x - w / 2, y: top, w, h: 24 });
2687
+ const box = { x: mid.x - w / 2, y: mid.y - 12, w, h: 24 };
2688
+ placed.push(box);
2689
+ out.label = { text: String(p.label), ...box };
2690
+ }
2691
+ return out;
2692
+ });
2693
+ }
2694
+ function flowPolyline(f) {
2695
+ const r = f.route, pts = f.points;
2696
+ const s0 = pts[0], e0 = pts[pts.length - 1];
2697
+ let out;
2698
+ if (r.lane == null && f.line === "straight") out = [s0, e0];
2699
+ else if (r.lane == null && f.line === "curved") {
2700
+ const { at } = linePath(r, pts, f.line);
2701
+ out = Array.from({ length: 25 }, (_, i) => at(i / 24));
2702
+ } else {
2703
+ const radius = f.line === "curved" ? 40 : f.line === "rounded" ? 14 : 0;
2704
+ const q = simplify(pts);
2705
+ out = [q[0]];
2706
+ for (let i = 1; i < q.length; i++) {
2707
+ const p = q[i];
2708
+ if (radius > 0 && i < q.length - 1) {
2709
+ const prev = q[i - 1], next = q[i + 1];
2710
+ const rr = Math.min(radius, Math.hypot(p.x - prev.x, p.y - prev.y) / 2, Math.hypot(next.x - p.x, next.y - p.y) / 2);
2711
+ const into = { x: Math.sign(p.x - prev.x), y: Math.sign(p.y - prev.y) }, outd = { x: Math.sign(next.x - p.x), y: Math.sign(next.y - p.y) };
2712
+ const a = { x: p.x - into.x * rr, y: p.y - into.y * rr }, b = { x: p.x + outd.x * rr, y: p.y + outd.y * rr };
2713
+ for (let k = 0; k <= 6; k++) {
2714
+ const t = k / 6;
2715
+ out.push({ x: (1 - t) ** 2 * a.x + 2 * (1 - t) * t * p.x + t ** 2 * b.x, y: (1 - t) ** 2 * a.y + 2 * (1 - t) * t * p.y + t ** 2 * b.y });
2716
+ }
2717
+ } else out.push(p);
2718
+ }
2719
+ }
2720
+ const first = r.points[0], last = r.points[r.points.length - 1];
2721
+ return [first, ...out, last].filter((p, i, all) => i === 0 || Math.hypot(p.x - all[i - 1].x, p.y - all[i - 1].y) >= 0.01);
2722
+ }
2723
+ function flowLayer(layout, size) {
2724
+ const shapes = [];
2725
+ const labels = [];
2726
+ for (const f of layout) {
2727
+ const r = f.route, { key, color } = f;
2728
+ shapes.push(
2729
+ /* @__PURE__ */ jsx3("path", { d: f.d, fill: "none", stroke: color, strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round", ...f.dashed ? { strokeDasharray: "7 6" } : {} }, `${key}-line`),
2730
+ marker(f.start, r.points[0], DIR[r.from], color, `${key}-start`),
2731
+ marker(f.end, r.points[r.points.length - 1], DIR[r.to], color, `${key}-end`)
2732
+ );
2733
+ if (f.label) {
2653
2734
  labels.push(
2654
- /* @__PURE__ */ jsx3("div", { style: { position: "absolute", left: mid.x - w / 2, top, width: w, height: 24, display: "flex", alignItems: "center", justifyContent: "center", borderRadius: 12, backgroundColor: theme.paper, border: `1.5px solid ${color}`, color, fontSize: 12, fontWeight: 600 }, children: p.label }, `${key}-label`)
2735
+ /* @__PURE__ */ jsx3("div", { style: { position: "absolute", left: f.label.x, top: f.label.y, width: f.label.w, height: 24, display: "flex", alignItems: "center", justifyContent: "center", borderRadius: 12, backgroundColor: theme.paper, border: `1.5px solid ${color}`, color, fontSize: 12, fontWeight: 600 }, children: f.label.text }, `${key}-label`)
2655
2736
  );
2656
2737
  }
2657
- });
2738
+ }
2658
2739
  return /* @__PURE__ */ jsxs3("div", { style: { position: "absolute", top: 0, left: 0, width: size.width, height: size.height, display: "flex" }, children: [
2659
2740
  /* @__PURE__ */ jsx3("svg", { width: size.width, height: size.height, viewBox: `0 0 ${size.width} ${size.height}`, style: { position: "absolute", top: 0, left: 0 }, children: shapes }),
2660
2741
  labels
@@ -2955,10 +3036,8 @@ async function renderWireframeSvg(spec, opts = {}) {
2955
3036
  if (issues.length) throw new SpecError(issues);
2956
3037
  }
2957
3038
  const flowsOn = opts.flows !== false;
2958
- const { width, height, gap } = boardLayout(spec, { flows: opts.flows });
2959
- const board = spec.elements[spec.root];
2960
- if ((board.props ?? {}).gap == null && gap !== BOARD_GAP)
2961
- spec = { ...spec, elements: { ...spec.elements, [spec.root]: { ...board, props: { ...board.props, gap } } } };
3039
+ const { width, height } = boardLayout(spec, { flows: opts.flows });
3040
+ spec = withBoardGap(spec, opts.flows);
2962
3041
  const palette = paletteFor(spec.elements[spec.root]?.props?.accent);
2963
3042
  const registry2 = withPalette(palette);
2964
3043
  const draw = async (s, reg) => renderToSvg(s, { registry: reg, includeStandard: false, fonts: await loadFonts(), width, height });
@@ -2966,13 +3045,17 @@ async function renderWireframeSvg(spec, opts = {}) {
2966
3045
  const { anchors, boxes } = await measureAnchors(spec, flows?.measure ?? []);
2967
3046
  if (!anchors.length && !flows?.list.length) return draw(spec, registry2);
2968
3047
  const layers = [];
2969
- if (flows?.list.length) {
2970
- const resolved = flows.list.map(({ key, from, to }) => ({ key, ends: { from: flows.box(from, boxes) ?? flows.screenBox(from), to: flows.box(to, boxes) ?? flows.screenBox(to), fromScreen: flows.screenBox(from), toScreen: flows.screenBox(to) } })).filter((f) => !!(f.ends.from && f.ends.to && f.ends.fromScreen && f.ends.toScreen));
2971
- layers.push(flowLayer(spec, resolved, flows.screens, flows.gap, palette, { width, height }));
2972
- }
3048
+ if (flows?.list.length) layers.push(flowLayer(layoutFlows(spec, flows.resolve(boxes), flows.screens, flows.gap, palette), { width, height }));
2973
3049
  if (anchors.length) layers.push(overlayLayer(spec, anchors, boxes, palette, registry2.Calendar));
2974
3050
  return draw(spec, withTopLayer(registry2, layers));
2975
3051
  }
3052
+ function withBoardGap(spec, flows) {
3053
+ const { gap } = boardLayout(spec, { flows });
3054
+ const board = spec.elements[spec.root];
3055
+ if ((board.props ?? {}).gap == null && gap !== BOARD_GAP)
3056
+ return { ...spec, elements: { ...spec.elements, [spec.root]: { ...board, props: { ...board.props, gap } } } };
3057
+ return spec;
3058
+ }
2976
3059
  function flowEnds(spec) {
2977
3060
  const els = spec.elements;
2978
3061
  const byId = /* @__PURE__ */ new Map();
@@ -3000,9 +3083,12 @@ function flowEnds(spec) {
3000
3083
  list,
3001
3084
  measure,
3002
3085
  screens: [...screenRects.values()],
3003
- screenBox: (key) => screenRects.get(screenOf(key) ?? ""),
3004
3086
  gap: (els[spec.root].props ?? {}).gap ?? BOARD_GAP,
3005
- box: (key, boxes) => els[key].type === "Screen" ? screenRects.get(key) : boxes[key]
3087
+ resolve(boxes) {
3088
+ const box = (key) => els[key].type === "Screen" ? screenRects.get(key) : boxes[key];
3089
+ const screenBox = (key) => screenRects.get(screenOf(key) ?? "");
3090
+ return list.map(({ key, from, to }) => ({ key, ends: { from: box(from) ?? screenBox(from), to: box(to) ?? screenBox(to), fromScreen: screenBox(from), toScreen: screenBox(to) } })).filter((f) => !!(f.ends.from && f.ends.to && f.ends.fromScreen && f.ends.toScreen));
3091
+ }
3006
3092
  };
3007
3093
  }
3008
3094
  async function measureAnchors(spec, extra = []) {
@@ -3046,7 +3132,7 @@ function compileWireframe(source) {
3046
3132
  return true;
3047
3133
  });
3048
3134
  unique.sort((a, b) => a.line - b.line);
3049
- return { spec: parsed.spec, issues: unique };
3135
+ return { spec: parsed.spec, issues: unique, lines: parsed.lines };
3050
3136
  }
3051
3137
  function formatIssues(issues) {
3052
3138
  return issues.map((i) => `line ${i.line}: ${i.message}`).join("\n");
@@ -3295,23 +3381,37 @@ Reply with the corrected wireframe in a single code block.`;
3295
3381
  export {
3296
3382
  theme,
3297
3383
  DEVICES,
3384
+ LABEL_H,
3298
3385
  ACCENTS,
3386
+ paletteFor,
3299
3387
  iconNames,
3300
3388
  UNIVERSAL_PROPS,
3301
3389
  takesUniversalProps,
3302
3390
  componentDefinitions,
3303
3391
  catalog,
3392
+ withPalette,
3393
+ withMarkers,
3304
3394
  registry,
3395
+ findAnchors,
3396
+ tagForMeasuring,
3397
+ readMarkers,
3398
+ overlayBoxes,
3399
+ layoutFlows,
3400
+ flowPolyline,
3401
+ loadFonts,
3305
3402
  PRIMARY_PROP,
3306
3403
  bareWords,
3307
3404
  parseWireframeText,
3308
3405
  RENAMED_PROPS,
3309
3406
  upgradeWireframe,
3310
3407
  upgradeSpec,
3408
+ SpecError,
3311
3409
  checkSpec,
3312
3410
  boardLayout,
3313
3411
  boardSize,
3314
3412
  renderWireframeSvg,
3413
+ withBoardGap,
3414
+ flowEnds,
3315
3415
  renderWireframePng,
3316
3416
  compileWireframe,
3317
3417
  formatIssues,
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  printWireframeText,
7
7
  renderWireframe,
8
8
  wireframePrompt
9
- } from "./chunks/chunk-H4Y6VBEX.js";
9
+ } from "./chunks/chunk-YOZFCNSK.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { readFile, writeFile } from "node:fs/promises";
package/dist/compile.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface CompileResult {
5
5
  spec: Spec | null;
6
6
  /** Every problem, tied to a source line. Empty means the wireframe is valid. */
7
7
  issues: TextIssue[];
8
+ /** Each element's line in the text, by its key in the spec (for measureElements). */
9
+ lines: Record<string, number>;
8
10
  }
9
11
  /**
10
12
  * Parse wireframe text and validate it against the catalog, reporting every
package/dist/flows.d.ts CHANGED
@@ -13,7 +13,7 @@ import { type ReactNode } from "react";
13
13
  import type { Spec } from "@json-render/core";
14
14
  import { type Palette } from "./colors.js";
15
15
  import type { Rect } from "./anchors.js";
16
- type Pt = {
16
+ export type Pt = {
17
17
  x: number;
18
18
  y: number;
19
19
  };
@@ -41,6 +41,7 @@ export interface FlowBoxes {
41
41
  * `lane` and `track` count the flows already using lanes and gaps, so each gets its own.
42
42
  */
43
43
  export declare function route(ends: FlowBoxes, screens: Rect[], gap: number, lane?: number, track?: number, nudge?: number): Route;
44
+ type End = "none" | "arrow" | "dot" | "circle" | "bar";
44
45
  /**
45
46
  * The SVG path for a routed flow, and a point a fraction along it (for the label). `pts` are the
46
47
  * route's points, trimmed for the markers. "straight" and "curved" only cut across when the route
@@ -55,11 +56,37 @@ export declare function linePath(r: Route, pts: Pt[], line: string): {
55
56
  export declare const flowLabelWidth: (label: string) => number;
56
57
  /** A flow's line color: gray, an accent name, a hex color, or the board's accent. */
57
58
  export declare function flowColor(color: unknown, palette: Palette): string;
58
- /** The top layer with every flow: one SVG for the lines and markers, labels as text pills. */
59
- export declare function flowLayer(spec: Spec, flows: {
59
+ /** A flow laid out: its route, the drawn line, its ends and its label pill, all in board pixels. */
60
+ export interface FlowLayout {
61
+ key: string;
62
+ route: Route;
63
+ /** The route's points with the ends pulled back for their markers: what the line is drawn through. */
64
+ points: Pt[];
65
+ d: string;
66
+ color: string;
67
+ start: End;
68
+ end: End;
69
+ line: string;
70
+ dashed: boolean;
71
+ label?: {
72
+ text: string;
73
+ } & Rect;
74
+ }
75
+ /**
76
+ * Where every flow goes: routes (each in its own lane and gap track), the line through them, and
77
+ * each label's spot. flowLayer draws this; measureElements reads it for hit-testing.
78
+ */
79
+ export declare function layoutFlows(spec: Spec, flows: {
60
80
  key: string;
61
81
  ends: FlowBoxes;
62
- }[], screens: Rect[], gap: number, palette: Palette, size: {
82
+ }[], screens: Rect[], gap: number, palette: Palette): FlowLayout[];
83
+ /**
84
+ * The line a flow draws, as points close enough to the drawn curve for hit-testing (corners and
85
+ * curves sampled), from the tip of its start marker to the tip of its end marker.
86
+ */
87
+ export declare function flowPolyline(f: FlowLayout): Pt[];
88
+ /** The top layer with every flow: one SVG for the lines and markers, labels as text pills. */
89
+ export declare function flowLayer(layout: FlowLayout[], size: {
63
90
  width: number;
64
91
  height: number;
65
92
  }): ReactNode;
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { encodeWireframe, decodeWireframe, MAX_SHARED_TEXT } from "./share.js";
6
6
  export { mcpTools, MCP_INSTRUCTIONS, LINK_KINDS, DEFAULT_LINKS, type LinkKind, wireframeGuide, renderWireframeTool, shareWireframeTool, callMcpTool, type ToolResult, type ToolContent, type ToolOptions, type McpToolName } from "./mcp.js";
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
+ export { measureElements, elementsAt, elementsAtLine, type Measured, type MeasuredElement, type MeasuredFlow, type MeasuredBox, type MeasureOptions } from "./measure.js";
9
10
  export { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, boardLayout, type BoardItem, type RenderWireframeOptions } from "./render.js";
10
11
  export { parseWireframeText, type TextIssue, type ParseResult } from "./text.js";
11
12
  export { catalog, componentDefinitions } from "./catalog.js";