tsquare 0.1.0 → 0.2.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,376 @@
1
+ import {
2
+ compileWireframe,
3
+ componentDocs,
4
+ formatIssues,
5
+ renderWireframePng,
6
+ renderWireframeSvg,
7
+ wireframePrompt
8
+ } from "../chunks/chunk-L6GTIZQH.js";
9
+
10
+ // src/playground/index.ts
11
+ import { createServer } from "node:http";
12
+ import { readFile, readdir } from "node:fs/promises";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ // src/playground/reference.ts
17
+ var GROUPS = [
18
+ { name: "Canvas", components: ["Board", "Screen", "Note"] },
19
+ { name: "Layout", components: ["Stack", "Grid", "Card", "Divider", "Spacer"] },
20
+ { name: "Content", components: ["Heading", "Text", "Image", "Icon", "Avatar", "Badge"] },
21
+ { name: "Controls", components: ["Button", "Input", "Checkbox", "Radio", "Toggle", "Select"] },
22
+ { name: "Navigation & data", components: ["NavBar", "TabBar", "Tabs", "List", "ListItem", "Table"] },
23
+ { name: "Overlays", components: ["Modal", "Drawer"] }
24
+ ];
25
+ var EXAMPLES = {
26
+ Board: `board row "Checkout flow" gap=48 accent=blue
27
+ screen custom "Cart" width=240 height=200
28
+ heading "Cart" level=3
29
+ text lines=3
30
+ spacer
31
+ button primary "Checkout" fullWidth
32
+ screen custom "Payment" width=240 height=200
33
+ heading "Payment" level=3
34
+ toggle "Save card" on
35
+ spacer
36
+ button primary "Pay" fullWidth
37
+ note "accent colors primary buttons, toggles, checked controls and active tabs. Omit it for grayscale." width=180`,
38
+ Screen: `board
39
+ screen phone "phone"
40
+ heading "390 \xD7 844"
41
+ text "Phone and desktop screens show a status bar or browser bar (chrome). Turn it off with chrome=false." muted
42
+ screen custom "custom" width=260 height=200
43
+ heading "Any size" level=2
44
+ text "device=custom with width and height" muted`,
45
+ Note: `board gap=32
46
+ screen custom width=320 height=220
47
+ text lines=4
48
+ note "Notes can also sit inside a screen" color=blue
49
+ note "yellow (default)" width=130
50
+ note "blue" color=blue width=130
51
+ note "pink" color=pink width=130
52
+ note "green" color=green width=130`,
53
+ Stack: `board
54
+ screen custom width=560 height=300 padding=0
55
+ stack row grow
56
+ stack width=160 border=right fill padding=16 gap=12
57
+ text "Sidebar" bold
58
+ text "width, border, fill" sm muted
59
+ stack grow padding=16 gap=16
60
+ stack row justify=between align=center
61
+ heading "Toolbar" level=3
62
+ stack row gap=8
63
+ button secondary sm "Filter"
64
+ button primary sm "New"
65
+ stack row wrap gap=8
66
+ badge outline "row"
67
+ badge outline "wrap"
68
+ badge outline "gap"
69
+ text lines=3`,
70
+ Grid: `board
71
+ screen custom width=520 height=300
72
+ grid columns=3 gap=12
73
+ card
74
+ image height=60
75
+ text "Item" bold
76
+ card
77
+ image height=60
78
+ text "Item" bold
79
+ card
80
+ image height=60
81
+ text "Item" bold
82
+ card
83
+ image height=60
84
+ text "Item" bold
85
+ card
86
+ image height=60
87
+ text "Item" bold`,
88
+ Card: `board
89
+ screen custom width=520 height=170
90
+ stack row gap=16
91
+ card "outline (default)" grow
92
+ text lines=3
93
+ card "filled" variant=filled grow
94
+ text lines=3`,
95
+ Divider: `board
96
+ screen custom width=360 height=150
97
+ text "Above"
98
+ divider
99
+ text "Below"
100
+ stack row gap=12 align=center
101
+ text "Left"
102
+ divider vertical
103
+ text "Right"`,
104
+ Spacer: `board
105
+ screen custom width=360 height=260
106
+ heading "Fixed spacer" level=3
107
+ spacer size=24
108
+ text "24px gap above"
109
+ spacer
110
+ button primary "Pushed to the bottom" fullWidth`,
111
+ Heading: `board
112
+ screen custom width=360 height=170
113
+ heading "Level 1" level=1
114
+ heading "Level 2" level=2
115
+ heading "Level 3" level=3
116
+ heading "Centered" level=3 align=center`,
117
+ Text: `board
118
+ screen custom width=360 height=300
119
+ text "Body text, default size"
120
+ text lg "Large"
121
+ text sm "Small and muted" muted
122
+ text "Bold" bold
123
+ text "Placeholder lines when lines is set and there's no text:" sm muted
124
+ text lines=3`,
125
+ Image: `board
126
+ screen custom width=480 height=210
127
+ stack row gap=12
128
+ image "label" height=120 width=160
129
+ image height=120 width=120 rounded
130
+ image height=120
131
+ text "In a row, an image without a width is square." sm muted`,
132
+ Icon: `board
133
+ screen custom width=360 height=120
134
+ stack row gap=16 align=center
135
+ icon home
136
+ icon search
137
+ icon bell
138
+ icon settings
139
+ icon heart size=32
140
+ text "Any Lucide name in kebab-case (lucide.dev/icons)" sm muted`,
141
+ Avatar: `board
142
+ screen custom width=300 height=100
143
+ stack row gap=12 align=center
144
+ avatar "JD"
145
+ avatar "AB" size=48
146
+ avatar
147
+ avatar size=24`,
148
+ Badge: `board
149
+ screen custom width=340 height=110
150
+ stack row gap=8
151
+ badge "solid" variant=solid
152
+ badge outline "outline"
153
+ badge "3"
154
+ stack row gap=8
155
+ badge "Active" tone=success
156
+ badge "Pending" tone=warning
157
+ badge "Failed" tone=danger
158
+ badge outline "Failed" tone=danger`,
159
+ Button: `board
160
+ screen custom width=420 height=310
161
+ stack row gap=8 align=center
162
+ button primary "Primary"
163
+ button secondary "Secondary"
164
+ button ghost "Ghost"
165
+ stack row gap=8 align=center
166
+ button primary sm "Small"
167
+ button primary "Medium"
168
+ button primary lg "Large"
169
+ button secondary "With icon" icon=download
170
+ button primary "Full width" fullWidth
171
+ text "With a board accent, primary buttons fill with it and ghost buttons use it for text." sm muted`,
172
+ Input: `board
173
+ screen custom width=420 height=540
174
+ input "Email" placeholder="you@example.com" helper="We never share it"
175
+ input "Username" value="dana moore" error helper="No spaces allowed"
176
+ input password "Password" value=secret
177
+ input search placeholder="Search"
178
+ input "Message" multiline=3
179
+ stack row gap=8
180
+ input placeholder="grow fills the row" grow
181
+ button primary "Send"`,
182
+ Checkbox: `board
183
+ screen custom width=300 height=100
184
+ checkbox "Checked" checked
185
+ checkbox "Unchecked"`,
186
+ Radio: `board
187
+ screen custom width=300 height=130
188
+ radio "Selected" checked
189
+ radio "Not selected"
190
+ radio "Not selected"`,
191
+ Toggle: `board
192
+ screen custom width=300 height=100
193
+ toggle "On" on
194
+ toggle "Off" off`,
195
+ Select: `board
196
+ screen custom width=320 height=170
197
+ select "Country" value="Mexico"
198
+ select "Size" placeholder="Choose one" width=160`,
199
+ NavBar: `board
200
+ screen custom width=380 height=240 padding=0 gap=0
201
+ navbar "leading=menu" leading=menu actions=[search, bell]
202
+ navbar "leading=back, centered" leading=back align=center actions=[share]
203
+ navbar "leading=close" leading=close
204
+ navbar "leading=logo" leading=logo actions=[user]`,
205
+ TabBar: `board
206
+ screen phone "Tab bar sits at the bottom"
207
+ heading "Home"
208
+ text lines=4
209
+ tabbar items=[{label=Home icon=house}, {label=Search icon=search}, {label=Inbox icon=inbox}, {label=Profile icon=user}] active=0`,
210
+ Tabs: `board
211
+ screen custom width=400 height=150
212
+ tabs items=[Overview, Activity, Settings] active=0
213
+ text lines=2`,
214
+ List: `board
215
+ screen custom width=380 height=420
216
+ list
217
+ listitem "Dividers on (default)"
218
+ listitem "Second row"
219
+ list dividers=false
220
+ listitem "dividers=false"
221
+ listitem "Second row"
222
+ list
223
+ listitem
224
+ listitem`,
225
+ ListItem: `board
226
+ screen custom width=400 height=440
227
+ list
228
+ listitem "leading=icon" subtitle="trailing=chevron" leading=icon icon=settings trailing=chevron
229
+ listitem "leading=avatar" subtitle="trailing=text" leading=avatar trailing=text trailingText="9:41"
230
+ listitem "leading=image" subtitle="trailing=badge" leading=image trailing=badge trailingText="3"
231
+ listitem "leading=checkbox" leading=checkbox trailing=toggle
232
+ listitem "trailing=icon" trailing=icon trailingIcon=trash-2
233
+ listitem leading=image trailing=chevron`,
234
+ Table: `board
235
+ screen custom width=520 height=400
236
+ table columns=[Name, Role, Status] data=[["Ana Ruiz", Admin, Active], ["Ben Cho", Editor, Invited]]
237
+ text "Without data, rows are placeholder bars:" sm muted
238
+ table columns=[Name, Role, Status] rows=3`,
239
+ Modal: `board
240
+ screen phone "Modal"
241
+ heading "Settings"
242
+ text lines=6
243
+ modal "Delete account?"
244
+ text "This can't be undone." muted
245
+ stack row justify=end gap=8
246
+ button ghost "Cancel"
247
+ button primary "Delete"`,
248
+ Drawer: `board
249
+ screen phone "side=left"
250
+ navbar "Inbox" leading=menu
251
+ text lines=6
252
+ drawer left size=260
253
+ heading "Menu" level=3
254
+ list dividers=false
255
+ listitem "Inbox" leading=icon icon=inbox
256
+ listitem "Sent" leading=icon icon=send
257
+ screen phone "side=bottom"
258
+ navbar "Photo" leading=back
259
+ image height=300
260
+ drawer bottom "Share" size=280
261
+ list dividers=false
262
+ listitem "Copy link" leading=icon icon=link
263
+ listitem "Message" leading=icon icon=message-circle`
264
+ };
265
+
266
+ // src/playground/index.ts
267
+ var here = path.dirname(fileURLToPath(import.meta.url));
268
+ var packageRoot = path.resolve(here, "..", "..");
269
+ var MAX_BODY = 256 * 1024;
270
+ function checkReferenceExamples() {
271
+ const problems = [];
272
+ for (const c of componentDocs()) {
273
+ if (!EXAMPLES[c.name]) problems.push(`${c.name}: no example in src/playground/reference.ts`);
274
+ if (!GROUPS.some((g) => g.components.includes(c.name))) problems.push(`${c.name}: not in any group in src/playground/reference.ts`);
275
+ }
276
+ for (const [name, src] of Object.entries(EXAMPLES)) {
277
+ const { issues } = compileWireframe(src);
278
+ if (issues.length) problems.push(`${name} example:
279
+ ${formatIssues(issues)}`);
280
+ }
281
+ return problems;
282
+ }
283
+ var referenceCache = null;
284
+ function reference() {
285
+ referenceCache ??= Promise.all(
286
+ componentDocs().map(async (c) => {
287
+ const { spec } = compileWireframe(EXAMPLES[c.name]);
288
+ return { ...c, example: EXAMPLES[c.name], svg: await renderWireframeSvg(spec, { skipValidation: true }) };
289
+ })
290
+ ).then((components) => ({ groups: GROUPS, components }));
291
+ return referenceCache;
292
+ }
293
+ async function examples() {
294
+ const dir = path.join(packageRoot, "examples");
295
+ const files = (await readdir(dir)).filter((f) => f.endsWith(".tsq")).sort();
296
+ return Promise.all(files.map(async (f) => ({ name: f.replace(/\.tsq$/, ""), source: await readFile(path.join(dir, f), "utf8") })));
297
+ }
298
+ function readBody(req) {
299
+ return new Promise((resolve, reject) => {
300
+ let size = 0;
301
+ const chunks = [];
302
+ req.on("data", (c) => {
303
+ size += c.length;
304
+ if (size > MAX_BODY) reject(new Error("body too large"));
305
+ else chunks.push(c);
306
+ });
307
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
308
+ req.on("error", reject);
309
+ });
310
+ }
311
+ var json = (res, status, body) => {
312
+ res.writeHead(status, { "content-type": "application/json" });
313
+ res.end(JSON.stringify(body));
314
+ return true;
315
+ };
316
+ function createPlaygroundHandler() {
317
+ return async function handle(req, res) {
318
+ const url = new URL(req.url ?? "/", "http://localhost");
319
+ if (req.method === "GET" && url.pathname === "/") {
320
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
321
+ res.end(await readFile(path.join(here, "index.html")));
322
+ return true;
323
+ }
324
+ if (req.method === "GET" && url.pathname === "/mark.png") {
325
+ res.writeHead(200, { "content-type": "image/png", "cache-control": "max-age=3600" });
326
+ res.end(await readFile(path.join(packageRoot, "assets", "mark.png")));
327
+ return true;
328
+ }
329
+ if (req.method === "GET" && url.pathname === "/api/reference") return json(res, 200, await reference());
330
+ if (req.method === "GET" && url.pathname === "/api/examples") return json(res, 200, await examples());
331
+ if (req.method === "GET" && url.pathname === "/api/prompt") {
332
+ res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
333
+ res.end(wireframePrompt());
334
+ return true;
335
+ }
336
+ if (req.method === "POST" && (url.pathname === "/api/render" || url.pathname === "/api/png")) {
337
+ const { spec, issues } = compileWireframe(await readBody(req));
338
+ if (url.pathname === "/api/render") {
339
+ if (!spec || issues.length) return json(res, 200, { issues });
340
+ return json(res, 200, { issues, svg: await renderWireframeSvg(spec, { skipValidation: true }) });
341
+ }
342
+ if (!spec || issues.length) return json(res, 400, { issues });
343
+ const scale = Math.min(4, Math.max(1, Number(url.searchParams.get("scale") ?? 2)));
344
+ res.writeHead(200, { "content-type": "image/png" });
345
+ res.end(await renderWireframePng(spec, { skipValidation: true, scale }));
346
+ return true;
347
+ }
348
+ return false;
349
+ };
350
+ }
351
+ function startPlayground(port = 4321) {
352
+ const problems = checkReferenceExamples();
353
+ if (problems.length) {
354
+ console.error(`Reference examples need fixing:
355
+
356
+ ${problems.join("\n")}`);
357
+ process.exit(1);
358
+ }
359
+ const handle = createPlaygroundHandler();
360
+ const server = createServer((req, res) => {
361
+ handle(req, res).then((handled) => {
362
+ if (!handled) json(res, 404, { error: "not found" });
363
+ }).catch((err) => {
364
+ console.error(err);
365
+ if (!res.headersSent) json(res, 500, { error: String(err?.message ?? err) });
366
+ else res.end();
367
+ });
368
+ });
369
+ server.listen(port, "127.0.0.1", () => console.log(`tsquare playground \u2192 http://localhost:${port}`));
370
+ return server;
371
+ }
372
+ export {
373
+ checkReferenceExamples,
374
+ createPlaygroundHandler,
375
+ startPlayground
376
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Examples for the playground's Components tab and the generated component
3
+ * docs. Each one is a complete wireframe so it can be opened in the editor
4
+ * as-is. Every catalog component needs one, in a group, and each must compile:
5
+ * `npm run build` fails otherwise, and the playground refuses to start.
6
+ */
7
+ export declare const GROUPS: {
8
+ name: string;
9
+ components: string[];
10
+ }[];
11
+ export declare const EXAMPLES: Record<string, string>;
@@ -0,0 +1,2 @@
1
+ import type { Spec } from "@json-render/core";
2
+ export declare function printWireframeText(spec: Spec, indentUnit?: string): string;
@@ -0,0 +1,3 @@
1
+ import type { Spec } from "@json-render/core";
2
+ /** The worked example in the model prompt (also used by the format eval). */
3
+ export declare const exampleSpec: Spec;
@@ -0,0 +1,22 @@
1
+ export interface PropDoc {
2
+ name: string;
3
+ type: string;
4
+ required: boolean;
5
+ /** This prop is filled by a quoted string, e.g. the label of `button "Save"`. */
6
+ main: boolean;
7
+ description?: string;
8
+ }
9
+ /** Every component with its description and props, straight from the catalog. */
10
+ export declare function componentDocs(): {
11
+ name: string;
12
+ description: string;
13
+ props: PropDoc[];
14
+ }[];
15
+ export declare function componentReference(): string;
16
+ export declare const RULES = "## Rules\n1. The top element is a Board. The Board's children are Screens, plus optional Notes beside them.\n2. Each Screen is one view of the product. Use several Screens to show several views or states.\n3. A Screen lays out its children top to bottom. Use Stack (direction row or column) and Grid to arrange content.\n4. NavBar is pinned to the top of its Screen and TabBar to the bottom.\n5. Modal and Drawer are overlays. They must be direct children of a Screen.\n6. This is a low-fidelity wireframe. Prefer placeholders (Image boxes, Text with lines) over invented copy unless the copy matters.\n7. Only use the components and props listed below. All props are optional unless marked required.\n8. Wireframes are grayscale. The only UI colors: Board accent (one color for primary buttons, checked controls, toggles, active tabs and ghost buttons), Badge tone (success, warning, danger) and Input error. Only add an accent if the request asks for color or a brand.";
17
+ export declare const TEXT_FORMAT = "## Output format: wireframe text\nOne element per line. Indent children two spaces under their parent. The first line is the board.\n\nA line is the component name in lowercase, followed by arguments separated by spaces:\n- \"a quoted string\" sets the component's main text prop (marked \"main text\" below)\n- a bare word that is one of the component's option values sets that option: phone, desktop, primary, ghost, row, sm, left, bottom, password, \u2026\n- a bare prop name sets a boolean prop to true: checked, fullWidth, grow, muted. `off` and `unchecked` set on/checked to false\n- key=value sets any prop. Values: \"string\", number, true/false, bare word, [list, of, values], {key=value key=value}\n- `#` after a space starts a comment that runs to the end of the line\n\nLists: items are separated by commas, and an item can contain spaces without quotes: [All notes, Pinned, Shared]. To put a comma inside an item, quote the item: [\"$1,200\", \"Smith, J\"]. Inside quotes, write \\\" for a quote character. Numbers in a list of text are fine: [2023, 2024].\n\nIf a bare word could mean more than one prop, write it as key=value.";
18
+ export declare const EXAMPLE_REQUEST = "Two phone screens for a recipe app: a browse screen with search, category tabs, a grid of recipe cards and a tab bar; and a recipe detail screen with its options sheet open.";
19
+ /** The system prompt for writing wireframe text. */
20
+ export declare function wireframePrompt(): string;
21
+ /** Follow-up message asking a model to fix the problems in its last reply. */
22
+ export declare function repairPrompt(issues: string): string;
@@ -0,0 +1,20 @@
1
+ import type { Spec } from "@json-render/core";
2
+ export declare class SpecError extends Error {
3
+ issues: string[];
4
+ constructor(issues: string[]);
5
+ }
6
+ /** Catalog validation plus the structural rules the renderer depends on. */
7
+ export declare function checkSpec(spec: Spec): string[];
8
+ /** Work out the canvas size from the Board's screens (Satori needs it up front). */
9
+ export declare function boardSize(spec: Spec): {
10
+ width: number;
11
+ height: number;
12
+ };
13
+ export interface RenderWireframeOptions {
14
+ /** Skip validation (e.g. while a spec is still streaming in). */
15
+ skipValidation?: boolean;
16
+ }
17
+ export declare function renderWireframeSvg(spec: Spec, opts?: RenderWireframeOptions): Promise<string>;
18
+ export declare function renderWireframePng(spec: Spec, opts?: RenderWireframeOptions & {
19
+ scale?: number;
20
+ }): Promise<Buffer<ArrayBufferLike>>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Wireframe spec schema. Same flat element shape as the other json-render
3
+ * renderers (root + elements map), with wireframe-specific rules for the
4
+ * generated LLM prompt.
5
+ */
6
+ export declare const wireframeSchema: import("@json-render/core").Schema<{
7
+ spec: import("@json-render/core").SchemaType<"object", {
8
+ root: import("@json-render/core").SchemaType<"string", unknown>;
9
+ elements: import("@json-render/core").SchemaType<"record", import("@json-render/core").SchemaType<"object", {
10
+ type: import("@json-render/core").SchemaType<"ref", string>;
11
+ props: import("@json-render/core").SchemaType<"propsOf", string>;
12
+ children: import("@json-render/core").SchemaType<"array", import("@json-render/core").SchemaType<"string", unknown>>;
13
+ visible: {
14
+ optional: true;
15
+ kind: "any";
16
+ inner?: unknown;
17
+ };
18
+ repeat: {
19
+ optional: true;
20
+ kind: "any";
21
+ inner?: unknown;
22
+ };
23
+ }>>;
24
+ }>;
25
+ catalog: import("@json-render/core").SchemaType<"object", {
26
+ components: import("@json-render/core").SchemaType<"map", {
27
+ props: import("@json-render/core").SchemaType<"zod", unknown>;
28
+ slots: import("@json-render/core").SchemaType<"array", import("@json-render/core").SchemaType<"string", unknown>>;
29
+ description: import("@json-render/core").SchemaType<"string", unknown>;
30
+ example: import("@json-render/core").SchemaType<"any", unknown>;
31
+ }>;
32
+ }>;
33
+ }>;
@@ -0,0 +1,5 @@
1
+ /** Edit distance counting a swap of neighbouring letters as one edit ("serach" → search). */
2
+ export declare function editDistance(a: string, b: string): any;
3
+ /** Up to `limit` candidates within a small edit distance of `word`, closest first. */
4
+ export declare function closeMatches(word: string, candidates: string[], limit?: number): string[];
5
+ export declare function unknownComponentMessage(name: string, components: string[]): string;
package/dist/text.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Text syntax for wireframes. Compiles to the same flat json-render spec.
3
+ *
4
+ * board "Notes app"
5
+ * screen phone "Sign in" padding=28
6
+ * heading "Welcome back"
7
+ * input "Email" placeholder="you@example.com"
8
+ * stack row justify=between
9
+ * checkbox "Remember me" checked
10
+ * button "Forgot?" ghost sm
11
+ * button "Sign in" primary lg fullWidth
12
+ *
13
+ * Line = component name, then arguments, children indented below.
14
+ * Arguments:
15
+ * "quoted" → the component's main text prop (label, title, text…)
16
+ * bare word → an enum value (phone, primary, row, sm…) or a boolean prop set to true (checked)
17
+ * key=value → any prop; value is "string", number, true/false, word, [list] or {key=value …}
18
+ * list items are separated by commas only, so [Ana Torres, Admin] is two items;
19
+ * quote an item that contains a comma: ["$1,200", "Smith, J"]
20
+ * # comment
21
+ */
22
+ import type { Spec } from "@json-render/core";
23
+ /** Which prop a quoted string fills, per component. */
24
+ export declare const PRIMARY_PROP: Record<string, string>;
25
+ /** What a component accepts as bare words, for docs: option values (with their prop), ambiguous values, and boolean props. */
26
+ export declare function bareWords(component: string): {
27
+ options: {
28
+ value: string;
29
+ prop: string;
30
+ }[];
31
+ ambiguous: string[];
32
+ booleans: string[];
33
+ wordPrimary: boolean;
34
+ };
35
+ export interface TextIssue {
36
+ line: number;
37
+ message: string;
38
+ }
39
+ export interface ParseResult {
40
+ spec: Spec | null;
41
+ issues: TextIssue[];
42
+ /** element id → source line, for mapping validation errors back */
43
+ lines: Record<string, number>;
44
+ }
45
+ export declare function parseWireframeText(source: string): ParseResult;
@@ -0,0 +1,84 @@
1
+ board row "Notes app — mobile"
2
+ screen phone "1 · Sign in" padding=28 gap=18
3
+ spacer size=36
4
+ image width=64 height=64 rounded
5
+ heading "Welcome back" level=1
6
+ text "Sign in to sync your notes." muted
7
+ input email "Email" placeholder="you@example.com"
8
+ input password "Password" value=hunter22
9
+ stack row justify=between
10
+ checkbox "Remember me" checked
11
+ button ghost sm "Forgot?"
12
+ button primary lg "Sign in" fullWidth
13
+ stack row gap=12 align=center
14
+ stack grow
15
+ divider
16
+ text sm "or" muted
17
+ stack grow
18
+ divider
19
+ button secondary lg "Continue with Google" fullWidth icon=globe
20
+ spacer
21
+ text sm "New here? Create an account" align=center muted
22
+ screen phone "2 · Notes"
23
+ navbar "Notes" leading=menu actions=[bell, more-vertical]
24
+ input search placeholder="Search notes"
25
+ tabs items=[All, Pinned, Shared] active=0
26
+ list
27
+ listitem "Grocery list" subtitle="Eggs, oat milk, basil…" leading=icon icon=shopping-cart trailing=text trailingText="9:12"
28
+ listitem "Trip ideas" subtitle="Lisbon in October?" leading=icon icon=map trailing=badge trailingText="2"
29
+ listitem leading=image trailing=chevron
30
+ listitem leading=image trailing=chevron
31
+ listitem leading=image trailing=chevron
32
+ stack row justify=end grow align=end
33
+ button primary "New note" icon=plus
34
+ tabbar items=[{label=Notes icon=notebook-pen}, {label=Search icon=search}, {label=Shared icon=users}, {label=Profile icon=user}] active=0
35
+ screen phone "3 · Side menu (drawer)"
36
+ navbar "Notes" leading=menu actions=[bell, more-vertical]
37
+ list
38
+ listitem leading=image
39
+ listitem leading=image
40
+ listitem leading=image
41
+ listitem leading=image
42
+ listitem leading=image
43
+ listitem leading=image
44
+ drawer left size=290
45
+ stack row gap=12
46
+ avatar "DM" size=44
47
+ stack gap=2
48
+ text "Dana Moore" bold
49
+ text sm "dana@example.com" muted
50
+ divider
51
+ list dividers=false
52
+ listitem "All notes" leading=icon icon=notebook-pen trailing=text trailingText="24"
53
+ listitem "Folders" leading=icon icon=folder trailing=chevron
54
+ listitem "Archive" leading=icon icon=archive
55
+ listitem "Settings" leading=icon icon=settings
56
+ spacer
57
+ button secondary "Sign out" icon=log-out fullWidth
58
+ screen phone "4 · Discard draft (modal)"
59
+ navbar "New note" leading=close actions=[image, check] align=center
60
+ heading "Untitled" level=2
61
+ text lines=7
62
+ stack row gap=8
63
+ badge outline "work"
64
+ badge outline "ideas"
65
+ modal "Discard this draft?"
66
+ text "You'll lose what you've written so far." muted
67
+ stack row justify=end gap=8
68
+ button ghost "Keep editing"
69
+ button primary "Discard"
70
+ screen phone "5 · Note options (bottom sheet)"
71
+ navbar leading=back actions=[share, more-horizontal]
72
+ image "Cover image" height=180
73
+ heading "Trip ideas" level=1
74
+ text lines=5
75
+ drawer bottom "Note options" size=400
76
+ toggle "Pin to top" on
77
+ toggle "Share with link" on=false
78
+ select "Folder" value=Travel
79
+ stack gap=10
80
+ text sm "Reminder" bold
81
+ stack row gap=20
82
+ radio "None" checked
83
+ radio "Tomorrow"
84
+ radio "Pick date"
@@ -0,0 +1,54 @@
1
+ board "Notes app — web"
2
+ screen desktop "Workspace overview" padding=0 gap=0
3
+ stack row grow gap=0 align=stretch
4
+ stack width=240 padding=20 gap=6 border=right fill
5
+ stack row gap=10
6
+ image width=32 height=32 rounded
7
+ text "Notebook" bold
8
+ spacer size=12
9
+ list dividers=false
10
+ listitem "Overview" leading=icon icon=layout-dashboard
11
+ listitem "All notes" leading=icon icon=notebook-pen trailing=text trailingText="128"
12
+ listitem "Shared" leading=icon icon=users
13
+ listitem "Settings" leading=icon icon=settings
14
+ spacer
15
+ stack row gap=10
16
+ avatar "DM" size=32
17
+ text sm "Dana Moore"
18
+ stack grow padding=32 gap=24
19
+ stack row gap=12
20
+ heading "Overview" level=1
21
+ spacer
22
+ stack width=260
23
+ input search placeholder=Search
24
+ button "New note" icon=plus
25
+ grid columns=3 gap=20
26
+ card "Notes this week"
27
+ heading "42" level=1
28
+ text lines=1
29
+ card "Shared with you"
30
+ heading "7" level=1
31
+ text lines=1
32
+ card filled "Storage"
33
+ heading "1.2 GB" level=1
34
+ text lines=1
35
+ tabs items=[Recent, Pinned, Archived] active=0
36
+ table columns=[Title, Folder, Owner, Updated] data=[["Grocery list", Personal, Dana, "9:12 AM"], ["Trip ideas", Travel, Dana, Yesterday], ["Q4 planning", Work, Sam, Mon], ["Reading list", Personal, Dana, "Sep 20"]]
37
+ screen desktop "Filters (right drawer)" padding=32 gap=24
38
+ stack row gap=12
39
+ heading "All notes" level=1
40
+ spacer
41
+ button secondary "Filters" icon=sliders-horizontal
42
+ table columns=[Title, Folder, Owner, Updated] rows=9
43
+ drawer right "Filters" size=340
44
+ select "Folder" placeholder="Any folder"
45
+ select "Owner" value=Me
46
+ divider
47
+ checkbox "Pinned only" checked
48
+ checkbox "Has attachments"
49
+ checkbox "Shared with me"
50
+ spacer
51
+ stack row justify=end gap=8
52
+ button ghost "Reset"
53
+ button "Apply filters"
54
+ note "Filters open in a right drawer so the table keeps its width. Open question: persist filters per workspace?" color=yellow