tsquare 0.5.0 → 0.6.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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Flow arrows: `flow <from> -> <to> "label"` lines drawn between elements or screens,
3
+ * on top of the board. Element positions come from the measuring pass (anchors.tsx);
4
+ * screens' boxes come from boardLayout. Routing:
5
+ * - the next screen to the right: right side to left side, turning in the gap between them
6
+ * - skipping screens or going back: through the gaps and a lane under the screens, so lines
7
+ * never cross a screen they don't belong to
8
+ * - within one screen: the sides facing each other
9
+ * Crossings between flows aren't avoided. Two flows at the same height between neighboring screens
10
+ * share a narrow gap: ends and labels move apart where they can, but a crowded gap can still overlap.
11
+ */
12
+ import { type ReactNode } from "react";
13
+ import type { Spec } from "@json-render/core";
14
+ import { type Palette } from "./colors.js";
15
+ import type { Rect } from "./anchors.js";
16
+ type Pt = {
17
+ x: number;
18
+ y: number;
19
+ };
20
+ type Side = "left" | "right" | "top" | "bottom";
21
+ export declare const flowMargin: (flows: number) => number;
22
+ export interface Route {
23
+ from: Side;
24
+ to: Side;
25
+ points: Pt[];
26
+ lane?: number;
27
+ }
28
+ /** Where a flow's ends are: each end's box and the frame of the screen it's in (a screen end is its own frame). */
29
+ export interface FlowBoxes {
30
+ from: Rect;
31
+ to: Rect;
32
+ fromScreen: Rect;
33
+ toScreen: Rect;
34
+ }
35
+ /**
36
+ * The sides and the right-angle path for a flow. Lines stay off screens they don't belong to:
37
+ * - same screen: right/left or top/bottom between the two elements
38
+ * - a neighboring screen: across the gap between the two screens
39
+ * - skipping over a screen: down the gap beside the source screen, along a lane under all the
40
+ * screens, and up the gap beside the target
41
+ * `lane` and `track` count the flows already using lanes and gaps, so each gets its own.
42
+ */
43
+ export declare function route(ends: FlowBoxes, screens: Rect[], gap: number, lane?: number, track?: number, nudge?: number): Route;
44
+ /**
45
+ * The SVG path for a routed flow, and a point a fraction along it (for the label). `pts` are the
46
+ * route's points, trimmed for the markers. "straight" and "curved" only cut across when the route
47
+ * stays beside its screens; a route through the lane under the screens keeps its corners (plain for
48
+ * straight, softened for curved), so it never crosses a screen it doesn't connect.
49
+ */
50
+ export declare function linePath(r: Route, pts: Pt[], line: string): {
51
+ d: string;
52
+ at: (t: number) => Pt;
53
+ };
54
+ /** A label pill's width, estimated from its text (labels are 12px semibold). */
55
+ export declare const flowLabelWidth: (label: string) => number;
56
+ /** A flow's line color: gray, an accent name, a hex color, or the board's accent. */
57
+ 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: {
60
+ key: string;
61
+ ends: FlowBoxes;
62
+ }[], screens: Rect[], gap: number, palette: Palette, size: {
63
+ width: number;
64
+ height: number;
65
+ }): ReactNode;
66
+ export {};
package/dist/index.js CHANGED
@@ -20,18 +20,19 @@ import {
20
20
  upgradeSpec,
21
21
  upgradeWireframe,
22
22
  wireframePrompt
23
- } from "./chunks/chunk-B6KIVOSH.js";
23
+ } from "./chunks/chunk-HDSKRKDL.js";
24
24
 
25
25
  // src/share.ts
26
26
  import { deflateRawSync, inflateRawSync } from "node:zlib";
27
27
  var MAX_SHARED_TEXT = 64e3;
28
28
  function encodeWireframe(text) {
29
- return "z" + deflateRawSync(Buffer.from(text, "utf8"), { level: 9 }).toString("base64url");
29
+ return "y" + deflateRawSync(Buffer.from(text, "utf8"), { level: 9 }).toString("base64url");
30
30
  }
31
31
  function decodeWireframe(data) {
32
- if (!data.startsWith("z")) throw new Error(`unknown encoding "${data.slice(0, 1)}" (expected a "z" prefix)`);
32
+ const prefix = data.slice(0, 1);
33
+ if (prefix !== "y" && prefix !== "z") throw new Error(`unknown encoding "${prefix}" (expected a "y" or "z" prefix)`);
33
34
  const text = inflateRawSync(Buffer.from(data.slice(1), "base64url"), { maxOutputLength: MAX_SHARED_TEXT }).toString("utf8");
34
- return upgradeWireframe(text, { fromLink: true });
35
+ return upgradeWireframe(text, { fromLink: prefix === "z" });
35
36
  }
36
37
  export {
37
38
  DEVICES,
@@ -16,6 +16,7 @@
16
16
  * GET /api/prompt the model prompt
17
17
  * POST /api/render wireframe text → { issues, svg?, width?, height?, items? } (items: where each screen sits)
18
18
  * POST /api/png?scale=2 wireframe text → image/png
19
+ * (both take ?flows=0 to leave out flow arrows)
19
20
  */
20
21
  import { type IncomingMessage, type ServerResponse } from "node:http";
21
22
  /** Problems with the reference examples: a component without an example or group, or an example that doesn't compile. */
@@ -100,6 +100,13 @@
100
100
  .btn.icon-only { padding: 5px 7px; }
101
101
  .btn.quiet { border-color: transparent; background: none; }
102
102
  .btn.quiet:hover { background: var(--fill); }
103
+ .switch { display: inline-flex; align-items: center; gap: 8px; margin-left: 6px; padding: 4px 6px; border: 0; background: none; border-radius: 6px; cursor: pointer; font: inherit; font-size: 13px; font-weight: 500; color: inherit; white-space: nowrap; }
104
+ .switch[hidden] { display: none; }
105
+ .switch:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
106
+ .switch .track { position: relative; width: 30px; height: 18px; border-radius: 9px; background: var(--line-strong); transition: background .15s; }
107
+ .switch .knob { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.25); transition: transform .15s; }
108
+ .switch[aria-checked="true"] .track { background: var(--primary); }
109
+ .switch[aria-checked="true"] .knob { transform: translateX(12px); }
103
110
 
104
111
  main { flex: 1; min-height: 0; display: flex; }
105
112
  .view { flex: 1; min-width: 0; display: none; }
@@ -259,6 +266,7 @@
259
266
  <button class="btn icon-only" id="zoom-out" type="button" aria-label="Zoom out" title="Zoom out"><span data-icon="zoom-out"></span></button>
260
267
  <button class="btn zoom-level" id="zoom-fit" type="button" title="Fit to the window">100%</button>
261
268
  <button class="btn icon-only" id="zoom-in" type="button" aria-label="Zoom in" title="Zoom in"><span data-icon="zoom-in"></span></button>
269
+ <button class="switch" id="flows" type="button" role="switch" aria-checked="true" title="Show or hide the flow arrows" hidden><span class="track"><span class="knob"></span></span>Flows</button>
262
270
  <span class="grow"></span>
263
271
  <div class="menu-wrap">
264
272
  <button class="btn" id="share" type="button" aria-haspopup="menu" aria-expanded="false" title="Copy a link" disabled><span data-icon="link"></span><span class="label-wide">Copy link</span></button>
@@ -13,7 +13,7 @@ import {
13
13
  renderWireframeSvg,
14
14
  takesUniversalProps,
15
15
  wireframePrompt
16
- } from "../chunks/chunk-B6KIVOSH.js";
16
+ } from "../chunks/chunk-HDSKRKDL.js";
17
17
 
18
18
  // src/playground/index.ts
19
19
  import { createServer } from "node:http";
@@ -75,7 +75,7 @@ function languageData() {
75
75
 
76
76
  // src/playground/reference.ts
77
77
  var GROUPS = [
78
- { name: "Canvas", components: ["Board", "Screen", "Note"] },
78
+ { name: "Canvas", components: ["Board", "Screen", "Note", "Flow"] },
79
79
  { name: "Layout", components: ["Stack", "Grid", "Card", "Accordion", "Divider", "Spacer"] },
80
80
  { name: "Content", components: ["Heading", "Text", "Bullets", "Image", "Chart", "Icon", "Avatar", "Badge"] },
81
81
  { name: "Controls", components: ["Button", "Input", "Checkbox", "Radio", "Toggle", "Select", "Slider", "Progress", "Calendar"] },
@@ -110,6 +110,23 @@ var EXAMPLES = {
110
110
  note "blue" color=blue width=130
111
111
  note "pink" color=pink width=130
112
112
  note "green" color=green width=130`,
113
+ Flow: `board "Sign-in flow"
114
+ screen custom "Sign in" width=260 height=300 #signin
115
+ heading "Welcome back" level=3
116
+ input "Email"
117
+ button primary "Sign in" fullWidth #submit
118
+ screen custom "Home" width=260 height=300 #home
119
+ stack row justify=between align=center
120
+ heading "Home" level=3
121
+ button ghost leadingIcon=settings #gear
122
+ text lines=3
123
+ screen custom "Settings" width=260 height=300 #settings
124
+ heading "Settings" level=3
125
+ toggle "Notifications" on
126
+ button secondary "Sign out" #signout
127
+ flow submit -> home "Sign in"
128
+ flow gear -> settings
129
+ flow signout -> signin "Sign out" dashed start=dot`,
113
130
  Stack: `board
114
131
  screen custom width=560 height=300 padding=0
115
132
  stack row grow
@@ -262,6 +279,12 @@ var EXAMPLES = {
262
279
  button primary "Next" trailingIcon=chevron-right
263
280
  button primary "Full width" fullWidth
264
281
  text "With a board accent, primary buttons fill with it and ghost buttons use it for text." sm muted
282
+ screen custom "tooltip" width=380 height=170
283
+ spacer size=34
284
+ stack row gap=8
285
+ button secondary "Draft" tooltip="Saves without publishing"
286
+ button primary "Publish"
287
+ text "Any element inside a screen can take tooltip=." sm muted
265
288
  screen custom "menu and open" width=380 height=300
266
289
  stack row justify=between align=center
267
290
  heading "Notes" level=3
@@ -475,7 +498,7 @@ function reference() {
475
498
  ).then((components) => ({ groups: GROUPS, components }));
476
499
  return referenceCache;
477
500
  }
478
- var EXAMPLE_ORDER = ["sign-in", "dashboard", "checkout", "states", "notes-mobile", "notes-web"];
501
+ var EXAMPLE_ORDER = ["sign-in", "dashboard", "checkout", "sign-up-flow", "states", "notes-mobile", "notes-web"];
479
502
  var rank = (name) => EXAMPLE_ORDER.indexOf(name) + 1 || EXAMPLE_ORDER.length + 1;
480
503
  async function examples() {
481
504
  const dir = path.join(packageRoot, "examples");
@@ -528,15 +551,16 @@ function createPlaygroundHandler() {
528
551
  }
529
552
  if (req.method === "POST" && (url.pathname === "/api/render" || url.pathname === "/api/png")) {
530
553
  const { spec, issues } = compileWireframe(await readBody(req));
554
+ const flows = url.searchParams.get("flows") !== "0";
531
555
  if (url.pathname === "/api/render") {
532
556
  if (!spec || issues.length) return json(res, 200, { issues });
533
- const { width, height, items } = boardLayout(spec);
534
- return json(res, 200, { issues, svg: await renderWireframeSvg(spec, { skipValidation: true }), width, height, items });
557
+ const { width, height, items } = boardLayout(spec, { flows });
558
+ return json(res, 200, { issues, svg: await renderWireframeSvg(spec, { skipValidation: true, flows }), width, height, items });
535
559
  }
536
560
  if (!spec || issues.length) return json(res, 400, { issues });
537
561
  const scale = Math.min(4, Math.max(1, Number(url.searchParams.get("scale") ?? 2)));
538
562
  res.writeHead(200, { "content-type": "image/png" });
539
- res.end(await renderWireframePng(spec, { skipValidation: true, scale }));
563
+ res.end(await renderWireframePng(spec, { skipValidation: true, scale, flows }));
540
564
  return true;
541
565
  }
542
566
  return false;