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.
- package/CHANGELOG.md +43 -0
- package/dist/catalog.d.ts +78 -0
- package/dist/chunks/{chunk-B6KIVOSH.js → chunk-HDSKRKDL.js} +369 -25
- package/dist/cli.js +3 -3
- package/dist/compile.d.ts +1 -0
- package/dist/components.d.ts +3 -0
- package/dist/flows.d.ts +66 -0
- package/dist/index.js +5 -4
- package/dist/playground/index.d.ts +1 -0
- package/dist/playground/index.html +8 -0
- package/dist/playground/index.js +30 -6
- package/dist/playground/playground.js +17 -17
- package/dist/prompt.d.ts +1 -1
- package/dist/render.d.ts +13 -10
- package/dist/upgrade.d.ts +1 -1
- package/examples/sign-up-flow.tsq +26 -0
- package/package.json +1 -1
- package/skill/tsquare/SKILL.md +4 -3
- package/skill/tsquare/reference.md +12 -1
package/dist/flows.d.ts
ADDED
|
@@ -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-
|
|
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 "
|
|
29
|
+
return "y" + deflateRawSync(Buffer.from(text, "utf8"), { level: 9 }).toString("base64url");
|
|
30
30
|
}
|
|
31
31
|
function decodeWireframe(data) {
|
|
32
|
-
|
|
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:
|
|
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>
|
package/dist/playground/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
renderWireframeSvg,
|
|
14
14
|
takesUniversalProps,
|
|
15
15
|
wireframePrompt
|
|
16
|
-
} from "../chunks/chunk-
|
|
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;
|