tsquare 0.1.0 → 0.3.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.
Files changed (50) hide show
  1. package/README.md +34 -8
  2. package/assets/mark.png +0 -0
  3. package/bin/tsquare.js +2 -6
  4. package/dist/catalog.d.ts +1055 -0
  5. package/dist/chunks/chunk-46MGJLGB.js +2126 -0
  6. package/{src/cli.ts → dist/cli.js} +28 -24
  7. package/dist/colors.d.ts +29 -0
  8. package/dist/compile.d.ts +24 -0
  9. package/dist/components.d.ts +83 -0
  10. package/dist/icons.d.ts +12 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.js +54 -0
  13. package/dist/layout.d.ts +66 -0
  14. package/dist/playground/index.d.ts +38 -0
  15. package/dist/playground/index.html +325 -0
  16. package/dist/playground/index.js +466 -0
  17. package/dist/playground/language-data.d.ts +28 -0
  18. package/dist/playground/playground.js +47 -0
  19. package/dist/playground/reference.d.ts +11 -0
  20. package/dist/print.d.ts +2 -0
  21. package/dist/prompt-example.d.ts +3 -0
  22. package/dist/prompt.d.ts +22 -0
  23. package/dist/render.d.ts +40 -0
  24. package/dist/schema.d.ts +33 -0
  25. package/dist/share.d.ts +4 -0
  26. package/dist/suggest.d.ts +5 -0
  27. package/dist/text.d.ts +50 -0
  28. package/examples/checkout.tsq +15 -0
  29. package/examples/dashboard.tsq +27 -0
  30. package/examples/notes-mobile.tsq +84 -0
  31. package/examples/notes-web.tsq +54 -0
  32. package/examples/sign-in.tsq +14 -0
  33. package/examples/states.tsq +16 -0
  34. package/package.json +31 -7
  35. package/skill/tsquare/SKILL.md +5 -4
  36. package/skill/tsquare/reference.md +7 -6
  37. package/src/catalog.ts +0 -329
  38. package/src/colors.ts +0 -82
  39. package/src/compile.ts +0 -64
  40. package/src/components.tsx +0 -1057
  41. package/src/icons.ts +0 -54
  42. package/src/index.ts +0 -11
  43. package/src/layout.ts +0 -63
  44. package/src/print.ts +0 -73
  45. package/src/prompt-example.ts +0 -43
  46. package/src/prompt.ts +0 -121
  47. package/src/render.ts +0 -178
  48. package/src/schema.ts +0 -43
  49. package/src/suggest.ts +0 -34
  50. package/src/text.ts +0 -333
@@ -0,0 +1,2126 @@
1
+ // src/layout.ts
2
+ var theme = {
3
+ font: "Inter",
4
+ board: "#e6e8eb",
5
+ paper: "#ffffff",
6
+ ink: "#1f2328",
7
+ text: "#3d434a",
8
+ muted: "#868d96",
9
+ line: "#d0d4d9",
10
+ lineStrong: "#9ca3ab",
11
+ fill: "#f0f2f4",
12
+ fill2: "#e1e4e8",
13
+ primary: "#2b3035",
14
+ onPrimary: "#ffffff",
15
+ scrim: "rgba(31, 35, 40, 0.38)",
16
+ notes: {
17
+ yellow: "#fdf1a8",
18
+ blue: "#cfe6fb",
19
+ pink: "#fbd3e0",
20
+ green: "#d4f1d0"
21
+ }
22
+ };
23
+ var DEVICES = {
24
+ phone: { width: 390, height: 844, radius: 36, padding: 16 },
25
+ tablet: { width: 820, height: 1180, radius: 24, padding: 24 },
26
+ desktop: { width: 1280, height: 800, radius: 10, padding: 24 },
27
+ custom: { width: 800, height: 600, radius: 8, padding: 20 }
28
+ };
29
+ var LABEL_H = 40;
30
+ var TITLE_H = 64;
31
+ var BOARD_PADDING = 56;
32
+ var BOARD_GAP = 64;
33
+ var NOTE_WIDTH = 220;
34
+ function screenSize(props = {}) {
35
+ const device = props.device ?? "phone";
36
+ const base = DEVICES[device] ?? DEVICES.phone;
37
+ return {
38
+ device,
39
+ width: props.width ?? base.width,
40
+ height: props.height ?? base.height,
41
+ radius: base.radius,
42
+ padding: props.padding ?? base.padding,
43
+ chrome: props.chrome ?? (device === "phone" || device === "desktop")
44
+ };
45
+ }
46
+ function estimateNoteHeight(text, width) {
47
+ const charsPerLine = Math.max(8, Math.floor((width - 28) / 7.4));
48
+ const lines = text.split("\n").reduce((n, para) => n + Math.max(1, Math.ceil(para.length / charsPerLine)), 0);
49
+ return 28 + lines * 21;
50
+ }
51
+
52
+ // src/catalog.ts
53
+ import { defineCatalog } from "@json-render/core";
54
+ import { z } from "zod";
55
+
56
+ // src/suggest.ts
57
+ function editDistance(a, b) {
58
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
59
+ for (let j = 1; j <= b.length; j++) d[0][j] = j;
60
+ for (let i = 1; i <= a.length; i++) {
61
+ for (let j = 1; j <= b.length; j++) {
62
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
63
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
64
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
65
+ }
66
+ }
67
+ return d[a.length][b.length];
68
+ }
69
+ function closeMatches(word, candidates, limit = 3) {
70
+ const q = word.toLowerCase();
71
+ const maxEdits = Math.max(1, Math.floor(q.length / 3));
72
+ return candidates.map((c) => [c, editDistance(q, c.toLowerCase())]).filter(([, d]) => d <= maxEdits).sort((a, b) => a[1] - b[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([c]) => c);
73
+ }
74
+ function unknownComponentMessage(name, components) {
75
+ const close = closeMatches(name, components);
76
+ return close.length ? `unknown component "${name}" (did you mean ${close.join(", ")}?)` : `unknown component "${name}" (components: ${components.join(", ")})`;
77
+ }
78
+
79
+ // src/colors.ts
80
+ var ACCENTS = {
81
+ blue: "#2563eb",
82
+ indigo: "#4f46e5",
83
+ violet: "#7c3aed",
84
+ pink: "#db2777",
85
+ red: "#dc2626",
86
+ orange: "#c2410c",
87
+ green: "#15803d",
88
+ teal: "#0f766e"
89
+ };
90
+ var TONES = {
91
+ success: "#15803d",
92
+ warning: "#b45309",
93
+ danger: "#dc2626"
94
+ };
95
+ var HEX = /^#[0-9a-fA-F]{6}$/;
96
+ var isAccent = (v) => v in ACCENTS || HEX.test(v);
97
+ function accentMessage(v) {
98
+ const names = Object.keys(ACCENTS);
99
+ const close = closeMatches(v, names);
100
+ return `unknown accent "${v}"` + (close.length ? ` (did you mean ${close.join(", ")}?)` : "") + `; use one of ${names.join(", ")}, or a hex color like #1a73e8`;
101
+ }
102
+ function luminance(hex) {
103
+ const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((v) => v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
104
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
105
+ }
106
+ function contrast(a, b) {
107
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
108
+ return (hi + 0.05) / (lo + 0.05);
109
+ }
110
+ function readableOnWhite(hex) {
111
+ let [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
112
+ let out = hex;
113
+ for (let k = 0; k < 20 && contrast(out, "#ffffff") < 4.5; k++) {
114
+ [r, g, b] = [r, g, b].map((v) => Math.round(v * 0.9));
115
+ out = "#" + [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
116
+ }
117
+ return out;
118
+ }
119
+ function paletteFor(accent) {
120
+ if (!accent || !isAccent(accent)) {
121
+ return { accent: theme.primary, onAccent: theme.onPrimary, accentText: theme.ink };
122
+ }
123
+ const hex = ACCENTS[accent] ?? accent.toLowerCase();
124
+ return {
125
+ accent: hex,
126
+ // hex accents can be light (a brand yellow): switch to dark text when white wouldn't read
127
+ onAccent: contrast(hex, "#ffffff") >= 3 ? "#ffffff" : theme.ink,
128
+ accentText: readableOnWhite(hex)
129
+ };
130
+ }
131
+
132
+ // src/icons.ts
133
+ import { icons } from "lucide";
134
+ var pascal = (s) => s.replace(/(^|[-_ ])(\w)/g, (_, __, c) => c.toUpperCase());
135
+ var kebab = (s) => s.replace(/([a-z])([A-Z0-9])/g, "$1-$2").replace(/([0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
136
+ var table = icons;
137
+ var iconNames = Object.keys(table).map(kebab);
138
+ function iconNode(name) {
139
+ return table[pascal(name)];
140
+ }
141
+ var isIcon = (name) => iconNode(name) !== void 0;
142
+ function suggestIcons(name, limit = 3) {
143
+ const q = name.toLowerCase().replace(/[_ ]/g, "-");
144
+ const words = q.split("-").filter((w) => w.length > 2);
145
+ const maxEdits = Math.max(1, Math.floor(q.length / 3));
146
+ const scored = [];
147
+ for (const n of iconNames) {
148
+ const parts = n.split("-");
149
+ const shared = words.filter((w) => parts.includes(w)).length;
150
+ const wordScore = shared ? 1 - 0.4 * (shared / words.length) + 0.05 * (parts.length - shared) : Infinity;
151
+ const d = editDistance(q, n);
152
+ const score = Math.min(wordScore, d <= maxEdits ? 3 * d / q.length : Infinity);
153
+ if (score < Infinity) scored.push([n, score]);
154
+ }
155
+ return scored.sort((a, b) => a[1] - b[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([n]) => n);
156
+ }
157
+ function unknownIconMessage(name) {
158
+ const close = suggestIcons(name);
159
+ return close.length ? `unknown icon "${name}" (did you mean ${close.join(", ")}?)` : `unknown icon "${name}" (use a Lucide name in kebab-case, e.g. search, user, settings)`;
160
+ }
161
+
162
+ // src/schema.ts
163
+ import { defineSchema } from "@json-render/core";
164
+ var wireframeSchema = defineSchema(
165
+ (s) => ({
166
+ spec: s.object({
167
+ root: s.string(),
168
+ elements: s.record(
169
+ s.object({
170
+ type: s.ref("catalog.components"),
171
+ props: s.propsOf("catalog.components"),
172
+ children: s.array(s.string()),
173
+ visible: { ...s.any(), ...s.optional() },
174
+ repeat: { ...s.any(), ...s.optional() }
175
+ })
176
+ )
177
+ }),
178
+ catalog: s.object({
179
+ components: s.map({
180
+ props: s.zod(),
181
+ slots: s.array(s.string()),
182
+ description: s.string(),
183
+ example: s.any()
184
+ })
185
+ })
186
+ }),
187
+ {
188
+ defaultRules: [
189
+ "The root element MUST be a Board. A Board's children are Screens (and optional Notes placed beside them).",
190
+ "Every Screen MUST be a direct child of the Board. Each Screen is one view of the product; use several Screens to show several views or states.",
191
+ "Screens lay out their children top to bottom. Use Stack (direction row/column) and Grid inside a Screen to arrange content.",
192
+ "Modal and Drawer are overlays. They MUST be direct children of a Screen, and SHOULD be listed last in the Screen's children so they draw on top.",
193
+ "NavBar goes first in a Screen's children and TabBar goes last (before any overlay).",
194
+ "This is a low-fidelity wireframe: prefer placeholders (Image boxes, Text with lines) over invented copy, unless the copy matters to the design.",
195
+ 'Every element MUST include a "children" array. Leaf elements use an empty array: "children": [].',
196
+ "Every id listed in a children array MUST exist as its own element."
197
+ ]
198
+ }
199
+ );
200
+
201
+ // src/catalog.ts
202
+ var o = (t) => t.nullish();
203
+ var align = z.enum(["start", "center", "end", "stretch"]);
204
+ var justify = z.enum(["start", "center", "end", "between", "around"]);
205
+ var iconName = z.string().refine(isIcon, { error: (iss) => unknownIconMessage(String(iss.input)) }).describe("Lucide icon name in kebab-case, e.g. menu, search, arrow-left, settings, bell, user");
206
+ var componentDefinitions = {
207
+ // ── Canvas ────────────────────────────────────────────────────────────
208
+ Board: {
209
+ props: z.object({
210
+ title: o(z.string()),
211
+ layout: o(z.enum(["row", "grid"])).describe("row = all screens side by side; grid = wrap every `columns` screens"),
212
+ columns: o(z.number().int().min(1)),
213
+ gap: o(z.number()),
214
+ padding: o(z.number()),
215
+ accent: o(z.string().refine(isAccent, { error: (iss) => accentMessage(String(iss.input)) })).describe(
216
+ `The one UI color: primary buttons, solid badges, checked controls, toggles, active tabs, ghost buttons. ${Object.keys(ACCENTS).join(", ")}, or a hex color like #1a73e8. Omit for grayscale.`
217
+ )
218
+ }),
219
+ slots: ["default"],
220
+ description: "Root canvas (artboard). Holds Screens side by side, plus optional Notes. Must be the root element.",
221
+ example: { title: "Checkout flow", layout: "row", gap: 64 }
222
+ },
223
+ Screen: {
224
+ props: z.object({
225
+ name: o(z.string()).describe("Label shown above the screen"),
226
+ device: o(z.enum(["phone", "tablet", "desktop", "custom"])),
227
+ width: o(z.number()).describe("Overrides the device width"),
228
+ height: o(z.number()).describe("Overrides the device height, e.g. height=1400 for a long scrolling page"),
229
+ chrome: o(z.boolean()).describe("Phone status bar / desktop browser bar. Default true for phone and desktop."),
230
+ padding: o(z.number()),
231
+ gap: o(z.number())
232
+ }),
233
+ slots: ["default"],
234
+ description: "One view of the product in a device frame. Children stack vertically.",
235
+ example: { name: "Home", device: "phone" }
236
+ },
237
+ Note: {
238
+ props: z.object({
239
+ text: z.string(),
240
+ color: o(z.enum(["yellow", "blue", "pink", "green"])),
241
+ width: o(z.number())
242
+ }),
243
+ slots: [],
244
+ description: "Sticky-note annotation. Put it on the Board beside screens, or inside a Screen next to what it explains.",
245
+ example: { text: "Empty state TBD", color: "yellow" }
246
+ },
247
+ // ── Layout ────────────────────────────────────────────────────────────
248
+ Stack: {
249
+ props: z.object({
250
+ direction: o(z.enum(["row", "column"])),
251
+ gap: o(z.number()),
252
+ padding: o(z.number()),
253
+ align: o(align),
254
+ justify: o(justify),
255
+ wrap: o(z.boolean()),
256
+ grow: o(z.boolean()).describe("Fill remaining space in the parent"),
257
+ width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 240 for a sidebar"),
258
+ border: o(z.enum(["none", "right", "left", "top", "bottom", "all"])),
259
+ fill: o(z.boolean()).describe("Light gray background")
260
+ }),
261
+ slots: ["default"],
262
+ description: "Flex container. The main layout primitive (rows, columns, sidebars, toolbars).",
263
+ example: { direction: "row", gap: 12, align: "center" }
264
+ },
265
+ Grid: {
266
+ props: z.object({
267
+ columns: z.number().int().min(1),
268
+ gap: o(z.number()),
269
+ padding: o(z.number())
270
+ }),
271
+ slots: ["default"],
272
+ description: "Equal-width columns that wrap. Good for card grids and galleries.",
273
+ example: { columns: 3, gap: 16 }
274
+ },
275
+ Card: {
276
+ props: z.object({
277
+ title: o(z.string()),
278
+ padding: o(z.number()),
279
+ gap: o(z.number()),
280
+ variant: o(z.enum(["outline", "filled"])),
281
+ grow: o(z.boolean()),
282
+ width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 320. Default fills the space.")
283
+ }),
284
+ slots: ["default"],
285
+ description: "Bordered container with optional title. Children stack vertically.",
286
+ example: { title: "Revenue", variant: "outline" }
287
+ },
288
+ Divider: {
289
+ props: z.object({ vertical: o(z.boolean()) }),
290
+ slots: [],
291
+ description: "Thin separator line.",
292
+ example: {}
293
+ },
294
+ Spacer: {
295
+ props: z.object({
296
+ size: o(z.number()).describe("Fixed size in px. Omit to fill remaining space.")
297
+ }),
298
+ slots: [],
299
+ description: "Empty space. Without a size it grows to push siblings apart.",
300
+ example: { size: 16 }
301
+ },
302
+ // ── Content ───────────────────────────────────────────────────────────
303
+ Heading: {
304
+ props: z.object({
305
+ text: z.string(),
306
+ level: o(z.union([z.literal(1), z.literal(2), z.literal(3)])),
307
+ align: o(z.enum(["left", "center", "right"]))
308
+ }),
309
+ slots: [],
310
+ description: "Heading text, level 1 (largest) to 3.",
311
+ example: { text: "Welcome back", level: 1 }
312
+ },
313
+ Text: {
314
+ props: z.object({
315
+ text: o(z.string()),
316
+ lines: o(z.number().int().min(1)).describe("Draw N placeholder lines instead of real text"),
317
+ size: o(z.enum(["sm", "md", "lg"])),
318
+ muted: o(z.boolean()),
319
+ bold: o(z.boolean()),
320
+ align: o(z.enum(["left", "center", "right"]))
321
+ }),
322
+ slots: [],
323
+ description: "Body text, or placeholder lines when `lines` is set and `text` is not.",
324
+ example: { lines: 3 }
325
+ },
326
+ Image: {
327
+ props: z.object({
328
+ height: o(z.number()),
329
+ width: o(z.union([z.number(), z.string()])).describe("Default fills the container width"),
330
+ label: o(z.string()),
331
+ rounded: o(z.boolean())
332
+ }),
333
+ slots: [],
334
+ description: "Image placeholder: a box with an X through it.",
335
+ example: { height: 180, label: "Hero photo" }
336
+ },
337
+ Icon: {
338
+ props: z.object({
339
+ name: o(iconName),
340
+ size: o(z.number())
341
+ }),
342
+ slots: [],
343
+ description: "Line icon from Lucide.",
344
+ example: { name: "search", size: 20 }
345
+ },
346
+ Avatar: {
347
+ props: z.object({
348
+ initials: o(z.string()),
349
+ size: o(z.number())
350
+ }),
351
+ slots: [],
352
+ description: "Round avatar with initials or a person silhouette.",
353
+ example: { initials: "JD", size: 40 }
354
+ },
355
+ Badge: {
356
+ props: z.object({
357
+ label: z.string(),
358
+ variant: o(z.enum(["solid", "outline"])),
359
+ tone: o(z.enum(["neutral", "success", "warning", "danger"])).describe("Status color. Default neutral (the accent for solid badges)")
360
+ }),
361
+ slots: [],
362
+ description: "Small pill label for counts or statuses.",
363
+ example: { label: "New", variant: "solid" }
364
+ },
365
+ // ── Controls ──────────────────────────────────────────────────────────
366
+ Button: {
367
+ props: z.object({
368
+ label: o(z.string()),
369
+ variant: o(z.enum(["primary", "secondary", "ghost"])),
370
+ size: o(z.enum(["sm", "md", "lg"])),
371
+ leadingIcon: o(iconName).describe("Lucide icon before the label, e.g. chevron-left for Back"),
372
+ trailingIcon: o(iconName).describe("Lucide icon after the label, e.g. chevron-right for Next"),
373
+ fullWidth: o(z.boolean())
374
+ }),
375
+ slots: [],
376
+ description: "Button. Primary is filled, secondary is outlined, ghost is text only.",
377
+ example: { label: "Continue", variant: "primary", fullWidth: true }
378
+ },
379
+ Input: {
380
+ props: z.object({
381
+ label: o(z.string()),
382
+ placeholder: o(z.string()),
383
+ value: o(z.string()),
384
+ type: o(z.enum(["text", "password", "search", "email"])),
385
+ multiline: o(z.number().int().min(1)).describe("Number of rows; 2 or more makes a textarea"),
386
+ helper: o(z.string()),
387
+ error: o(z.boolean()).describe("Validation error: red border and red helper text"),
388
+ grow: o(z.boolean()).describe("Fill the remaining space in a row"),
389
+ width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 320. Default fills the space.")
390
+ }),
391
+ slots: [],
392
+ description: "Text field with optional label. Set multiline for a textarea.",
393
+ example: { label: "Email", placeholder: "you@example.com" }
394
+ },
395
+ Checkbox: {
396
+ props: z.object({ label: o(z.string()), checked: o(z.boolean()) }),
397
+ slots: [],
398
+ description: "Checkbox with label.",
399
+ example: { label: "Remember me", checked: true }
400
+ },
401
+ Radio: {
402
+ props: z.object({ label: o(z.string()), checked: o(z.boolean()) }),
403
+ slots: [],
404
+ description: "Radio button with label.",
405
+ example: { label: "Monthly", checked: true }
406
+ },
407
+ Toggle: {
408
+ props: z.object({ label: o(z.string()), on: o(z.boolean()) }),
409
+ slots: [],
410
+ description: "On/off switch with label on the left.",
411
+ example: { label: "Notifications", on: true }
412
+ },
413
+ Select: {
414
+ props: z.object({
415
+ label: o(z.string()),
416
+ value: o(z.string()),
417
+ placeholder: o(z.string()),
418
+ width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 320. Default fills the space.")
419
+ }),
420
+ slots: [],
421
+ description: "Dropdown field (closed state).",
422
+ example: { label: "Country", value: "United States" }
423
+ },
424
+ // ── Navigation & data ─────────────────────────────────────────────────
425
+ NavBar: {
426
+ props: z.object({
427
+ title: o(z.string()),
428
+ leading: o(z.enum(["none", "menu", "back", "close", "logo"])),
429
+ actions: o(z.array(iconName)).describe("Icon names shown on the right"),
430
+ align: o(z.enum(["left", "center"]))
431
+ }),
432
+ slots: [],
433
+ description: "Top app bar. Place first in a Screen.",
434
+ example: { title: "Inbox", leading: "menu", actions: ["search", "more-vertical"] }
435
+ },
436
+ TabBar: {
437
+ props: z.object({
438
+ items: z.array(z.object({ label: z.string(), icon: o(iconName) })),
439
+ active: o(z.number().int())
440
+ }),
441
+ slots: [],
442
+ description: "Bottom tab bar for mobile. Place last in a Screen (before overlays).",
443
+ example: { items: [{ label: "Home", icon: "home" }, { label: "Profile", icon: "user" }], active: 0 }
444
+ },
445
+ Tabs: {
446
+ props: z.object({
447
+ items: z.array(z.string()),
448
+ active: o(z.number().int())
449
+ }),
450
+ slots: [],
451
+ description: "In-page tabs with an underline on the active one.",
452
+ example: { items: ["Overview", "Activity", "Settings"], active: 0 }
453
+ },
454
+ List: {
455
+ props: z.object({ dividers: o(z.boolean()), grow: o(z.boolean()) }),
456
+ slots: ["default"],
457
+ description: "Vertical list of ListItems.",
458
+ example: { dividers: true }
459
+ },
460
+ ListItem: {
461
+ props: z.object({
462
+ title: o(z.string()),
463
+ subtitle: o(z.string()),
464
+ leading: o(z.enum(["none", "icon", "avatar", "image", "checkbox"])),
465
+ leadingIcon: o(iconName).describe("Lucide icon on the left; setting it implies leading=icon"),
466
+ trailing: o(z.enum(["none", "chevron", "toggle", "text", "badge", "icon"])),
467
+ trailingText: o(z.string()).describe("Text for trailing=text or badge; setting it implies trailing=text"),
468
+ trailingIcon: o(iconName).describe("Lucide icon on the right; setting it implies trailing=icon")
469
+ }),
470
+ slots: [],
471
+ description: "Row in a List. Omit title for a placeholder bar.",
472
+ example: { title: "Account", subtitle: "Email, password", leadingIcon: "user", trailing: "chevron" }
473
+ },
474
+ Table: {
475
+ props: z.object({
476
+ columns: z.array(z.string()),
477
+ rows: o(z.number().int()).describe("Placeholder rows when `data` is not given"),
478
+ data: o(z.array(z.array(z.string())))
479
+ }),
480
+ slots: [],
481
+ description: "Table with headers. Rows are placeholder bars unless `data` is given.",
482
+ example: { columns: ["Name", "Status", "Date"], rows: 5 }
483
+ },
484
+ // ── Overlays ──────────────────────────────────────────────────────────
485
+ Modal: {
486
+ props: z.object({
487
+ title: o(z.string()),
488
+ width: o(z.number()),
489
+ scrim: o(z.boolean()).describe("Dim the screen behind. Default true.")
490
+ }),
491
+ slots: ["default"],
492
+ description: "Centered dialog over the screen. Must be a direct child of a Screen, listed last.",
493
+ example: { title: "Delete note?", width: 320 }
494
+ },
495
+ Drawer: {
496
+ props: z.object({
497
+ side: o(z.enum(["left", "right", "bottom"])),
498
+ title: o(z.string()),
499
+ size: o(z.number()).describe("Width for left/right, height for bottom"),
500
+ scrim: o(z.boolean()).describe("Dim the screen behind. Default true."),
501
+ handle: o(z.boolean()).describe("Grab handle on bottom sheets. Default true for bottom.")
502
+ }),
503
+ slots: ["default"],
504
+ description: "Panel sliding in from an edge: side nav (left), filters/details (right), or bottom sheet. Must be a direct child of a Screen, listed last.",
505
+ example: { side: "left", title: "Menu", size: 300 }
506
+ }
507
+ };
508
+ var catalog = defineCatalog(wireframeSchema, {
509
+ components: componentDefinitions
510
+ });
511
+ function listItemEnds(p) {
512
+ return {
513
+ leading: p.leading ?? (p.leadingIcon ? "icon" : void 0),
514
+ trailing: p.trailing ?? (p.trailingIcon ? "icon" : p.trailingText ? "text" : void 0)
515
+ };
516
+ }
517
+ var RENAMED_PROPS = {
518
+ Button: { icon: "leadingIcon" },
519
+ ListItem: { icon: "leadingIcon" }
520
+ };
521
+ function upgradeRenamedProps(spec) {
522
+ for (const el of Object.values(spec.elements)) {
523
+ const renames = RENAMED_PROPS[el.type];
524
+ if (!renames || !el.props) continue;
525
+ for (const [from, to] of Object.entries(renames)) {
526
+ if (from in el.props) {
527
+ if (!(to in el.props)) el.props[to] = el.props[from];
528
+ delete el.props[from];
529
+ }
530
+ }
531
+ }
532
+ return spec;
533
+ }
534
+
535
+ // src/components.tsx
536
+ import React, { Children, cloneElement, isValidElement } from "react";
537
+ import { jsx, jsxs } from "react/jsx-runtime";
538
+ var GRAY = paletteFor();
539
+ function withPalette(colors) {
540
+ return Object.fromEntries(
541
+ Object.entries(registry).map(([name, C]) => [name, (props) => C({ ...props, colors })])
542
+ );
543
+ }
544
+ var flexAlign = { start: "flex-start", center: "center", end: "flex-end", stretch: "stretch" };
545
+ var flexJustify = {
546
+ start: "flex-start",
547
+ center: "center",
548
+ end: "flex-end",
549
+ between: "space-between",
550
+ around: "space-around"
551
+ };
552
+ function Box({ style, children }) {
553
+ const clean = { display: "flex" };
554
+ for (const [k, v] of Object.entries(style ?? {})) if (v !== void 0 && v !== null) clean[k] = v;
555
+ return /* @__PURE__ */ jsx("div", { style: clean, children });
556
+ }
557
+ function withDir(children, dir, stretch = false, aligned = false) {
558
+ return Children.toArray(children).map((c) => isValidElement(c) ? cloneElement(c, { dir, stretch, aligned }) : c);
559
+ }
560
+ function fillRow(inRow, edge = true) {
561
+ if (!inRow) return {};
562
+ return { alignSelf: edge ? "stretch" : "flex-start" };
563
+ }
564
+ function grow(on, dir) {
565
+ if (!on) return { flexGrow: 0 };
566
+ return dir === "row" ? { flexGrow: 1, flexBasis: "0px" } : { flexGrow: 1 };
567
+ }
568
+ function hug(dir, aligned = false) {
569
+ return dir === "row" || aligned ? {} : { alignSelf: "flex-start" };
570
+ }
571
+ function IconGlyph({ name, size = 20, color = theme.ink, strokeWidth = 2 }) {
572
+ const node = name ? iconNode(name) : null;
573
+ return /* @__PURE__ */ jsx(
574
+ "svg",
575
+ {
576
+ width: size,
577
+ height: size,
578
+ viewBox: "0 0 24 24",
579
+ fill: "none",
580
+ stroke: color,
581
+ strokeWidth,
582
+ strokeLinecap: "round",
583
+ strokeLinejoin: "round",
584
+ style: { flexShrink: 0 },
585
+ children: node ? node.map(
586
+ ([tag, attrs], i) => React.createElement(tag, { key: i, ...attrs })
587
+ ) : /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" })
588
+ }
589
+ );
590
+ }
591
+ function Board({ element, children }) {
592
+ const p = element.props;
593
+ const gap = p.gap ?? BOARD_GAP;
594
+ const items = Children.toArray(children);
595
+ const perRow = p.layout === "grid" ? Math.max(1, p.columns ?? 3) : Math.max(1, items.length);
596
+ const rows = [];
597
+ for (let i = 0; i < items.length; i += perRow) rows.push(items.slice(i, i + perRow));
598
+ return /* @__PURE__ */ jsxs(
599
+ Box,
600
+ {
601
+ style: {
602
+ flexDirection: "column",
603
+ width: "100%",
604
+ height: "100%",
605
+ padding: p.padding ?? BOARD_PADDING,
606
+ backgroundColor: theme.board,
607
+ fontFamily: theme.font,
608
+ color: theme.ink
609
+ },
610
+ children: [
611
+ p.title ? /* @__PURE__ */ jsx(Box, { style: { height: TITLE_H, fontSize: 28, fontWeight: 600, color: theme.ink }, children: p.title }) : null,
612
+ /* @__PURE__ */ jsx(Box, { style: { flexDirection: "column", gap }, children: rows.map((row, i) => /* @__PURE__ */ jsx(Box, { style: { flexDirection: "row", alignItems: "flex-start", gap }, children: row }, i)) })
613
+ ]
614
+ }
615
+ );
616
+ }
617
+ function StatusBar() {
618
+ return /* @__PURE__ */ jsxs(
619
+ Box,
620
+ {
621
+ style: {
622
+ height: 34,
623
+ flexShrink: 0,
624
+ padding: "0 26px",
625
+ alignItems: "center",
626
+ justifyContent: "space-between",
627
+ fontSize: 14,
628
+ fontWeight: 600
629
+ },
630
+ children: [
631
+ /* @__PURE__ */ jsx(Box, { children: "9:41" }),
632
+ /* @__PURE__ */ jsxs(Box, { style: { gap: 5, alignItems: "center" }, children: [
633
+ /* @__PURE__ */ jsx(IconGlyph, { name: "signal", size: 15, strokeWidth: 2.4 }),
634
+ /* @__PURE__ */ jsx(IconGlyph, { name: "wifi", size: 15, strokeWidth: 2.4 }),
635
+ /* @__PURE__ */ jsx(IconGlyph, { name: "battery-full", size: 17, strokeWidth: 2 })
636
+ ] })
637
+ ]
638
+ }
639
+ );
640
+ }
641
+ function BrowserBar() {
642
+ return /* @__PURE__ */ jsxs(
643
+ Box,
644
+ {
645
+ style: {
646
+ height: 40,
647
+ flexShrink: 0,
648
+ padding: "0 14px",
649
+ gap: 16,
650
+ alignItems: "center",
651
+ backgroundColor: theme.fill,
652
+ borderBottom: `1px solid ${theme.line}`
653
+ },
654
+ children: [
655
+ /* @__PURE__ */ jsx(Box, { style: { gap: 7 }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(Box, { style: { width: 11, height: 11, borderRadius: 6, backgroundColor: theme.fill2, border: `1px solid ${theme.lineStrong}` } }, i)) }),
656
+ /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, justifyContent: "center" }, children: /* @__PURE__ */ jsx(Box, { style: { width: 420, height: 24, borderRadius: 12, backgroundColor: theme.paper, border: `1px solid ${theme.line}` } }) }),
657
+ /* @__PURE__ */ jsx(Box, { style: { width: 57 } })
658
+ ]
659
+ }
660
+ );
661
+ }
662
+ function Screen({ element, children }) {
663
+ const p = element.props;
664
+ const s = screenSize(p);
665
+ const top = [];
666
+ const body = [];
667
+ const bottom = [];
668
+ const overlays = [];
669
+ for (const c of Children.toArray(children)) {
670
+ const type = isValidElement(c) ? c.props.element?.type : null;
671
+ if (type === "NavBar") top.push(c);
672
+ else if (type === "TabBar") bottom.push(c);
673
+ else if (type === "Modal" || type === "Drawer") overlays.push(c);
674
+ else body.push(c);
675
+ }
676
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", flexShrink: 0 }, children: [
677
+ /* @__PURE__ */ jsx(Box, { style: { height: LABEL_H, fontSize: 16, fontWeight: 600, color: theme.text }, children: p.name ?? "" }),
678
+ /* @__PURE__ */ jsxs(
679
+ Box,
680
+ {
681
+ style: {
682
+ width: s.width,
683
+ height: s.height,
684
+ flexDirection: "column",
685
+ backgroundColor: theme.paper,
686
+ border: `1.5px solid ${theme.lineStrong}`,
687
+ borderRadius: s.radius,
688
+ overflow: "hidden",
689
+ boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
690
+ color: theme.ink,
691
+ fontSize: 15
692
+ },
693
+ children: [
694
+ s.chrome && s.device === "phone" ? /* @__PURE__ */ jsx(StatusBar, {}) : null,
695
+ s.chrome && s.device === "desktop" ? /* @__PURE__ */ jsx(BrowserBar, {}) : null,
696
+ /* @__PURE__ */ jsxs(Box, { style: { position: "relative", flexGrow: 1, flexDirection: "column", overflow: "hidden" }, children: [
697
+ top,
698
+ /* @__PURE__ */ jsx(
699
+ Box,
700
+ {
701
+ style: {
702
+ flexGrow: 1,
703
+ flexDirection: "column",
704
+ padding: s.padding,
705
+ gap: p.gap ?? 14,
706
+ overflow: "hidden"
707
+ },
708
+ children: withDir(body, "column")
709
+ }
710
+ ),
711
+ bottom,
712
+ overlays
713
+ ] })
714
+ ]
715
+ }
716
+ )
717
+ ] });
718
+ }
719
+ function Note({ element, dir, aligned }) {
720
+ const p = element.props;
721
+ const onBoard = dir === void 0;
722
+ return /* @__PURE__ */ jsx(
723
+ Box,
724
+ {
725
+ style: {
726
+ width: p.width ?? (onBoard ? NOTE_WIDTH : void 0),
727
+ marginTop: onBoard ? LABEL_H : 0,
728
+ flexShrink: 0,
729
+ padding: 14,
730
+ backgroundColor: theme.notes[p.color ?? "yellow"] ?? theme.notes.yellow,
731
+ boxShadow: "0 2px 6px rgba(0,0,0,0.12)",
732
+ fontSize: 14,
733
+ lineHeight: 1.5,
734
+ color: theme.ink,
735
+ ...onBoard ? {} : hug(dir, aligned)
736
+ },
737
+ children: p.text
738
+ }
739
+ );
740
+ }
741
+ var borderSide = (side) => {
742
+ const b = `1px solid ${theme.line}`;
743
+ switch (side) {
744
+ case "all":
745
+ return { border: b };
746
+ case "left":
747
+ return { borderLeft: b };
748
+ case "right":
749
+ return { borderRight: b };
750
+ case "top":
751
+ return { borderTop: b };
752
+ case "bottom":
753
+ return { borderBottom: b };
754
+ default:
755
+ return {};
756
+ }
757
+ };
758
+ function Stack({ element, children, dir: parentDir, stretch }) {
759
+ const p = element.props;
760
+ const dir = p.direction ?? "column";
761
+ return /* @__PURE__ */ jsx(
762
+ Box,
763
+ {
764
+ style: {
765
+ flexDirection: dir,
766
+ gap: p.gap ?? (dir === "row" ? 12 : 12),
767
+ padding: p.padding ?? 0,
768
+ alignItems: flexAlign[p.align ?? (dir === "row" ? "center" : "stretch")],
769
+ justifyContent: flexJustify[p.justify ?? "start"],
770
+ flexWrap: p.wrap ? "wrap" : "nowrap",
771
+ ...grow(p.grow, parentDir),
772
+ flexShrink: p.width ? 0 : 1,
773
+ width: p.width ?? void 0,
774
+ minHeight: 0,
775
+ backgroundColor: p.fill ? theme.fill : void 0,
776
+ ...borderSide(p.border),
777
+ // stacks with a visible edge (sidebars, panels) fill the row's height; others sit at the top
778
+ ...fillRow(stretch, p.width != null || !!p.fill || !!p.border && p.border !== "none")
779
+ },
780
+ children: withDir(children, dir, dir === "row" && p.align == null, dir === "column" && p.align != null && p.align !== "stretch")
781
+ }
782
+ );
783
+ }
784
+ function Grid({ element, children }) {
785
+ const p = element.props;
786
+ const gap = p.gap ?? 16;
787
+ const cols = Math.max(1, p.columns ?? 2);
788
+ return /* @__PURE__ */ jsx(Box, { style: { padding: p.padding ?? 0, flexDirection: "column" }, children: /* @__PURE__ */ jsx(Box, { style: { flexDirection: "row", flexWrap: "wrap", margin: -gap / 2 }, children: Children.toArray(children).map((c, i) => /* @__PURE__ */ jsx(Box, { style: { width: `${100 / cols}%`, padding: gap / 2, flexDirection: "column" }, children: withDir(c, "column") }, i)) }) });
789
+ }
790
+ function Card({ element, children, dir, stretch }) {
791
+ const p = element.props;
792
+ const filled = p.variant === "filled";
793
+ return /* @__PURE__ */ jsxs(
794
+ Box,
795
+ {
796
+ style: {
797
+ flexDirection: "column",
798
+ gap: p.gap ?? 10,
799
+ padding: p.padding ?? 16,
800
+ borderRadius: 10,
801
+ border: filled ? "none" : `1.5px solid ${theme.line}`,
802
+ backgroundColor: filled ? theme.fill : theme.paper,
803
+ ...grow(p.grow, dir),
804
+ width: p.width ?? void 0,
805
+ flexShrink: p.width ? 0 : 1,
806
+ ...fillRow(stretch)
807
+ },
808
+ children: [
809
+ p.title ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 15, fontWeight: 600 }, children: p.title }) : null,
810
+ withDir(children, "column")
811
+ ]
812
+ }
813
+ );
814
+ }
815
+ function Divider({ element, dir }) {
816
+ const vertical = element.props.vertical ?? dir === "row";
817
+ return vertical ? /* @__PURE__ */ jsx(Box, { style: { width: 1, alignSelf: "stretch", backgroundColor: theme.line, flexShrink: 0 } }) : /* @__PURE__ */ jsx(Box, { style: { height: 1, alignSelf: "stretch", backgroundColor: theme.line, flexShrink: 0 } });
818
+ }
819
+ function Spacer({ element }) {
820
+ const size = element.props.size;
821
+ return size != null ? /* @__PURE__ */ jsx(Box, { style: { flexBasis: `${size}px`, flexGrow: 0, flexShrink: 0 } }) : /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1 } });
822
+ }
823
+ function Heading({ element }) {
824
+ const p = element.props;
825
+ const size = { 1: 26, 2: 20, 3: 16 }[p.level ?? 1] ?? 26;
826
+ return /* @__PURE__ */ jsx(
827
+ Box,
828
+ {
829
+ style: {
830
+ fontSize: size,
831
+ fontWeight: 600,
832
+ lineHeight: 1.25,
833
+ color: theme.ink,
834
+ justifyContent: p.align === "center" ? "center" : p.align === "right" ? "flex-end" : "flex-start",
835
+ textAlign: p.align ?? "left"
836
+ },
837
+ children: p.text
838
+ }
839
+ );
840
+ }
841
+ var LINE_WIDTHS = [100, 94, 97, 89, 95, 91];
842
+ function PlaceholderLines({ count, height = 8, gap = 9, color = theme.fill2, last = 62 }) {
843
+ return /* @__PURE__ */ jsx(Box, { style: { flexDirection: "column", gap, alignSelf: "stretch" }, children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsx(
844
+ Box,
845
+ {
846
+ style: {
847
+ height,
848
+ borderRadius: height / 2,
849
+ backgroundColor: color,
850
+ width: `${i === count - 1 && count > 1 ? last : LINE_WIDTHS[i % LINE_WIDTHS.length]}%`
851
+ }
852
+ },
853
+ i
854
+ )) });
855
+ }
856
+ function Text({ element }) {
857
+ const p = element.props;
858
+ const size = { sm: 13, md: 15, lg: 18 }[p.size ?? "md"] ?? 15;
859
+ if (p.text == null) {
860
+ return /* @__PURE__ */ jsx(PlaceholderLines, { count: p.lines ?? 2, height: Math.round(size * 0.55), gap: Math.round(size * 0.6) });
861
+ }
862
+ return /* @__PURE__ */ jsx(
863
+ Box,
864
+ {
865
+ style: {
866
+ fontSize: size,
867
+ lineHeight: 1.45,
868
+ color: p.muted ? theme.muted : theme.text,
869
+ fontWeight: p.bold ? 600 : 400,
870
+ justifyContent: p.align === "center" ? "center" : p.align === "right" ? "flex-end" : "flex-start",
871
+ textAlign: p.align ?? "left"
872
+ },
873
+ children: p.text
874
+ }
875
+ );
876
+ }
877
+ function Image({ element, dir }) {
878
+ const p = element.props;
879
+ const h = p.height ?? 160;
880
+ const w = p.width ?? (dir === "row" ? h : "100%");
881
+ return /* @__PURE__ */ jsxs(
882
+ Box,
883
+ {
884
+ style: {
885
+ position: "relative",
886
+ width: w,
887
+ height: h,
888
+ flexShrink: 0,
889
+ backgroundColor: theme.fill,
890
+ border: `1.5px solid ${theme.line}`,
891
+ borderRadius: p.rounded ? 12 : 4,
892
+ overflow: "hidden",
893
+ alignItems: "center",
894
+ justifyContent: "center"
895
+ },
896
+ children: [
897
+ /* @__PURE__ */ jsxs(
898
+ "svg",
899
+ {
900
+ width: "100%",
901
+ height: "100%",
902
+ viewBox: "0 0 100 100",
903
+ preserveAspectRatio: "none",
904
+ style: { position: "absolute", top: 0, left: 0 },
905
+ children: [
906
+ /* @__PURE__ */ jsx("line", { x1: "0", y1: "0", x2: "100", y2: "100", stroke: theme.line, strokeWidth: "1.5", vectorEffect: "non-scaling-stroke" }),
907
+ /* @__PURE__ */ jsx("line", { x1: "100", y1: "0", x2: "0", y2: "100", stroke: theme.line, strokeWidth: "1.5", vectorEffect: "non-scaling-stroke" })
908
+ ]
909
+ }
910
+ ),
911
+ p.label ? /* @__PURE__ */ jsx(
912
+ Box,
913
+ {
914
+ style: {
915
+ padding: "3px 8px",
916
+ fontSize: 12,
917
+ color: theme.muted,
918
+ backgroundColor: theme.fill,
919
+ borderRadius: 4
920
+ },
921
+ children: p.label
922
+ }
923
+ ) : null
924
+ ]
925
+ }
926
+ );
927
+ }
928
+ function Icon({ element, dir, aligned }) {
929
+ const p = element.props;
930
+ return /* @__PURE__ */ jsx(Box, { style: { flexShrink: 0, ...hug(dir, aligned) }, children: /* @__PURE__ */ jsx(IconGlyph, { name: p.name, size: p.size ?? 20 }) });
931
+ }
932
+ function AvatarCircle({ initials, size = 40 }) {
933
+ return /* @__PURE__ */ jsx(
934
+ Box,
935
+ {
936
+ style: {
937
+ width: size,
938
+ height: size,
939
+ borderRadius: size / 2,
940
+ backgroundColor: theme.fill2,
941
+ alignItems: "center",
942
+ justifyContent: "center",
943
+ flexShrink: 0,
944
+ fontSize: Math.round(size * 0.38),
945
+ fontWeight: 600,
946
+ color: theme.text,
947
+ overflow: "hidden"
948
+ },
949
+ children: initials ? initials.slice(0, 2).toUpperCase() : /* @__PURE__ */ jsx(IconGlyph, { name: "user", size: Math.round(size * 0.55), color: theme.muted })
950
+ }
951
+ );
952
+ }
953
+ function Avatar({ element, dir, aligned }) {
954
+ return /* @__PURE__ */ jsx(Box, { style: hug(dir, aligned), children: /* @__PURE__ */ jsx(AvatarCircle, { initials: element.props.initials, size: element.props.size ?? 40 }) });
955
+ }
956
+ function BadgePill({ label, variant, tone, c = GRAY }) {
957
+ const solid = variant !== "outline";
958
+ const toneColor = tone && tone !== "neutral" ? TONES[tone] : void 0;
959
+ return /* @__PURE__ */ jsx(
960
+ Box,
961
+ {
962
+ style: {
963
+ height: 22,
964
+ padding: "0 9px",
965
+ borderRadius: 11,
966
+ alignItems: "center",
967
+ fontSize: 12,
968
+ fontWeight: 600,
969
+ flexShrink: 0,
970
+ backgroundColor: solid ? toneColor ?? c.accent : "transparent",
971
+ color: solid ? toneColor ? "#ffffff" : c.onAccent : toneColor ?? theme.text,
972
+ border: solid ? "none" : `1.5px solid ${toneColor ?? theme.lineStrong}`
973
+ },
974
+ children: label
975
+ }
976
+ );
977
+ }
978
+ function Badge({ element, dir, aligned, colors }) {
979
+ return /* @__PURE__ */ jsx(Box, { style: hug(dir, aligned), children: /* @__PURE__ */ jsx(BadgePill, { label: element.props.label, variant: element.props.variant, tone: element.props.tone, c: colors }) });
980
+ }
981
+ function Button({ element, dir, aligned, colors: c = GRAY }) {
982
+ const p = element.props;
983
+ const variant = p.variant ?? "primary";
984
+ const h = { sm: 32, md: 42, lg: 50 }[p.size ?? "md"] ?? 42;
985
+ const fg = variant === "primary" ? c.onAccent : variant === "ghost" ? c.accentText : theme.ink;
986
+ return /* @__PURE__ */ jsxs(
987
+ Box,
988
+ {
989
+ style: {
990
+ height: h,
991
+ padding: `0 ${Math.round(h * 0.45)}px`,
992
+ gap: 8,
993
+ borderRadius: 8,
994
+ alignItems: "center",
995
+ justifyContent: "center",
996
+ flexShrink: 0,
997
+ fontSize: h < 36 ? 13 : 15,
998
+ fontWeight: 600,
999
+ color: fg,
1000
+ backgroundColor: variant === "primary" ? c.accent : "transparent",
1001
+ border: variant === "secondary" ? `1.5px solid ${theme.lineStrong}` : "none",
1002
+ ...p.fullWidth ? { alignSelf: "stretch" } : hug(dir, aligned)
1003
+ },
1004
+ children: [
1005
+ p.leadingIcon ? /* @__PURE__ */ jsx(IconGlyph, { name: p.leadingIcon, size: h < 36 ? 16 : 18, color: fg }) : null,
1006
+ p.label ? /* @__PURE__ */ jsx(Box, { children: p.label }) : null,
1007
+ p.trailingIcon ? /* @__PURE__ */ jsx(IconGlyph, { name: p.trailingIcon, size: h < 36 ? 16 : 18, color: fg }) : null
1008
+ ]
1009
+ }
1010
+ );
1011
+ }
1012
+ function fieldWidth(width, dir) {
1013
+ if (width != null) return { width, flexShrink: 0 };
1014
+ return dir === "row" ? { flexGrow: 1, flexBasis: "0px", minWidth: 120 } : {};
1015
+ }
1016
+ function FieldLabel({ text }) {
1017
+ return text ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 13, fontWeight: 600, color: theme.text }, children: text }) : null;
1018
+ }
1019
+ function Field({ children, height = 42, top = false, error = false }) {
1020
+ return /* @__PURE__ */ jsx(
1021
+ Box,
1022
+ {
1023
+ style: {
1024
+ height,
1025
+ padding: top ? "10px 12px" : "0 12px",
1026
+ gap: 8,
1027
+ alignItems: top ? "flex-start" : "center",
1028
+ border: `1.5px solid ${error ? TONES.danger : theme.lineStrong}`,
1029
+ borderRadius: 8,
1030
+ backgroundColor: theme.paper,
1031
+ fontSize: 15
1032
+ },
1033
+ children
1034
+ }
1035
+ );
1036
+ }
1037
+ function Input({ element, dir }) {
1038
+ const p = element.props;
1039
+ const rows = p.multiline ?? 1;
1040
+ const isPassword = p.type === "password";
1041
+ const shown = p.value ?? null;
1042
+ const content = isPassword && shown ? "\u2022".repeat(Math.min(shown.length, 12)) : shown;
1043
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 6, ...fieldWidth(p.width, dir), ...p.grow ? { flexGrow: 1 } : {} }, children: [
1044
+ /* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
1045
+ /* @__PURE__ */ jsxs(Field, { height: rows > 1 ? rows * 22 + 20 : 42, top: rows > 1, error: !!p.error, children: [
1046
+ p.type === "search" ? /* @__PURE__ */ jsx(IconGlyph, { name: "search", size: 18, color: theme.muted }) : null,
1047
+ /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, color: content ? theme.ink : theme.muted }, children: content ?? p.placeholder ?? (isPassword ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : "") }),
1048
+ isPassword ? /* @__PURE__ */ jsx(IconGlyph, { name: "eye-off", size: 18, color: theme.muted }) : null
1049
+ ] }),
1050
+ p.helper ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 12, color: p.error ? TONES.danger : theme.muted }, children: p.helper }) : null
1051
+ ] });
1052
+ }
1053
+ function Select({ element, dir }) {
1054
+ const p = element.props;
1055
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 6, ...fieldWidth(p.width, dir) }, children: [
1056
+ /* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
1057
+ /* @__PURE__ */ jsxs(Field, { children: [
1058
+ /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, color: p.value ? theme.ink : theme.muted }, children: p.value ?? p.placeholder ?? "Select\u2026" }),
1059
+ /* @__PURE__ */ jsx(IconGlyph, { name: "chevron-down", size: 18, color: theme.text })
1060
+ ] })
1061
+ ] });
1062
+ }
1063
+ function CheckMark({ checked, round = false, c = GRAY }) {
1064
+ return /* @__PURE__ */ jsx(
1065
+ Box,
1066
+ {
1067
+ style: {
1068
+ width: 20,
1069
+ height: 20,
1070
+ flexShrink: 0,
1071
+ borderRadius: round ? 10 : 5,
1072
+ border: `1.5px solid ${checked && !round ? c.accent : theme.lineStrong}`,
1073
+ backgroundColor: checked && !round ? c.accent : theme.paper,
1074
+ alignItems: "center",
1075
+ justifyContent: "center"
1076
+ },
1077
+ children: checked ? round ? /* @__PURE__ */ jsx(Box, { style: { width: 10, height: 10, borderRadius: 5, backgroundColor: c.accent } }) : /* @__PURE__ */ jsx(IconGlyph, { name: "check", size: 14, color: c.onAccent, strokeWidth: 3 }) : null
1078
+ }
1079
+ );
1080
+ }
1081
+ function Checkbox({ element, dir, aligned, colors }) {
1082
+ const p = element.props;
1083
+ return /* @__PURE__ */ jsxs(Box, { style: { gap: 10, alignItems: "center", ...hug(dir, aligned) }, children: [
1084
+ /* @__PURE__ */ jsx(CheckMark, { checked: p.checked, c: colors }),
1085
+ p.label ? /* @__PURE__ */ jsx(Box, { style: { color: theme.text }, children: p.label }) : null
1086
+ ] });
1087
+ }
1088
+ function Radio({ element, dir, aligned, colors }) {
1089
+ const p = element.props;
1090
+ return /* @__PURE__ */ jsxs(Box, { style: { gap: 10, alignItems: "center", ...hug(dir, aligned) }, children: [
1091
+ /* @__PURE__ */ jsx(CheckMark, { checked: p.checked, round: true, c: colors }),
1092
+ p.label ? /* @__PURE__ */ jsx(Box, { style: { color: theme.text }, children: p.label }) : null
1093
+ ] });
1094
+ }
1095
+ function Switch({ on, c = GRAY }) {
1096
+ return /* @__PURE__ */ jsx(
1097
+ Box,
1098
+ {
1099
+ style: {
1100
+ width: 42,
1101
+ height: 24,
1102
+ borderRadius: 12,
1103
+ padding: 3,
1104
+ flexShrink: 0,
1105
+ backgroundColor: on ? c.accent : theme.fill2,
1106
+ justifyContent: on ? "flex-end" : "flex-start"
1107
+ },
1108
+ children: /* @__PURE__ */ jsx(Box, { style: { width: 18, height: 18, borderRadius: 9, backgroundColor: theme.paper } })
1109
+ }
1110
+ );
1111
+ }
1112
+ function Toggle({ element, colors }) {
1113
+ const p = element.props;
1114
+ return /* @__PURE__ */ jsxs(Box, { style: { gap: 12, alignItems: "center", justifyContent: "space-between" }, children: [
1115
+ p.label ? /* @__PURE__ */ jsx(Box, { style: { color: theme.text }, children: p.label }) : null,
1116
+ /* @__PURE__ */ jsx(Switch, { on: p.on, c: colors })
1117
+ ] });
1118
+ }
1119
+ var LEADING_ICON = { menu: "menu", back: "arrow-left", close: "x" };
1120
+ function NavBar({ element }) {
1121
+ const p = element.props;
1122
+ const leading = p.leading === "logo" ? /* @__PURE__ */ jsx(Box, { style: { width: 30, height: 30, borderRadius: 8, backgroundColor: theme.fill2 } }) : p.leading && p.leading !== "none" ? /* @__PURE__ */ jsx(IconGlyph, { name: LEADING_ICON[p.leading], size: 22 }) : null;
1123
+ const actions = /* @__PURE__ */ jsx(Box, { style: { gap: 18, alignItems: "center", justifyContent: "flex-end" }, children: (p.actions ?? []).map((a, i) => /* @__PURE__ */ jsx(IconGlyph, { name: a, size: 22 }, i)) });
1124
+ const title = p.title ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 17, fontWeight: 600 }, children: p.title }) : null;
1125
+ const centered = p.align === "center";
1126
+ return /* @__PURE__ */ jsx(
1127
+ Box,
1128
+ {
1129
+ style: {
1130
+ height: 56,
1131
+ flexShrink: 0,
1132
+ padding: "0 16px",
1133
+ gap: 14,
1134
+ alignItems: "center",
1135
+ borderBottom: `1px solid ${theme.line}`,
1136
+ backgroundColor: theme.paper
1137
+ },
1138
+ children: centered ? [
1139
+ /* @__PURE__ */ jsx(Box, { style: { width: 90, alignItems: "center" }, children: leading }, "l"),
1140
+ /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, justifyContent: "center" }, children: title }, "c"),
1141
+ /* @__PURE__ */ jsx(Box, { style: { width: 90, justifyContent: "flex-end" }, children: actions }, "r")
1142
+ ] : [
1143
+ leading ? /* @__PURE__ */ jsx(Box, { children: leading }, "l") : null,
1144
+ title ? /* @__PURE__ */ jsx(Box, { children: title }, "t") : null,
1145
+ /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1 } }, "s"),
1146
+ /* @__PURE__ */ jsx(Box, { children: actions }, "a")
1147
+ ]
1148
+ }
1149
+ );
1150
+ }
1151
+ function TabBar({ element, colors: c = GRAY }) {
1152
+ const p = element.props;
1153
+ const active = p.active ?? 0;
1154
+ return /* @__PURE__ */ jsx(
1155
+ Box,
1156
+ {
1157
+ style: {
1158
+ height: 76,
1159
+ flexShrink: 0,
1160
+ padding: "8px 0 18px",
1161
+ borderTop: `1px solid ${theme.line}`,
1162
+ backgroundColor: theme.paper
1163
+ },
1164
+ children: (p.items ?? []).map((item, i) => {
1165
+ const color = i === active ? c.accentText : theme.muted;
1166
+ return /* @__PURE__ */ jsxs(
1167
+ Box,
1168
+ {
1169
+ style: { flexGrow: 1, flexBasis: "0px", flexDirection: "column", alignItems: "center", gap: 4, color, fontSize: 11, fontWeight: i === active ? 600 : 400 },
1170
+ children: [
1171
+ /* @__PURE__ */ jsx(IconGlyph, { name: item.icon, size: 22, color }),
1172
+ /* @__PURE__ */ jsx(Box, { children: item.label })
1173
+ ]
1174
+ },
1175
+ i
1176
+ );
1177
+ })
1178
+ }
1179
+ );
1180
+ }
1181
+ function Tabs({ element, colors: c = GRAY }) {
1182
+ const p = element.props;
1183
+ const active = p.active ?? 0;
1184
+ return /* @__PURE__ */ jsx(Box, { style: { borderBottom: `1px solid ${theme.line}`, gap: 4 }, children: (p.items ?? []).map((label, i) => /* @__PURE__ */ jsx(
1185
+ Box,
1186
+ {
1187
+ style: {
1188
+ padding: "10px 14px",
1189
+ marginBottom: -1,
1190
+ fontSize: 14,
1191
+ fontWeight: i === active ? 600 : 400,
1192
+ color: i === active ? c.accentText : theme.muted,
1193
+ borderBottom: i === active ? `2.5px solid ${c.accentText}` : "2.5px solid transparent"
1194
+ },
1195
+ children: label
1196
+ },
1197
+ i
1198
+ )) });
1199
+ }
1200
+ function List({ element, children, dir, stretch }) {
1201
+ const dividers = element.props.dividers ?? true;
1202
+ const items = withDir(children, "column");
1203
+ return /* @__PURE__ */ jsx(Box, { style: { flexDirection: "column", ...grow(element.props.grow, dir), ...fillRow(stretch, false) }, children: items.map((c, i) => /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column" }, children: [
1204
+ i > 0 && dividers ? /* @__PURE__ */ jsx(Box, { style: { height: 1, backgroundColor: theme.line } }) : null,
1205
+ c
1206
+ ] }, i)) });
1207
+ }
1208
+ function ListItem({ element, colors }) {
1209
+ const p = element.props;
1210
+ const ends = listItemEnds(p);
1211
+ const leading = (() => {
1212
+ switch (ends.leading) {
1213
+ case "icon":
1214
+ return /* @__PURE__ */ jsx(Box, { style: { width: 38, height: 38, borderRadius: 10, backgroundColor: theme.fill, alignItems: "center", justifyContent: "center", flexShrink: 0 }, children: /* @__PURE__ */ jsx(IconGlyph, { name: p.leadingIcon, size: 20 }) });
1215
+ case "avatar":
1216
+ return /* @__PURE__ */ jsx(AvatarCircle, { size: 40 });
1217
+ case "image":
1218
+ return /* @__PURE__ */ jsx(Box, { style: { width: 48, height: 48, borderRadius: 6, backgroundColor: theme.fill, border: `1.5px solid ${theme.line}`, flexShrink: 0 } });
1219
+ case "checkbox":
1220
+ return /* @__PURE__ */ jsx(CheckMark, { c: colors });
1221
+ default:
1222
+ return null;
1223
+ }
1224
+ })();
1225
+ const trailing = (() => {
1226
+ switch (ends.trailing) {
1227
+ case "chevron":
1228
+ return /* @__PURE__ */ jsx(IconGlyph, { name: "chevron-right", size: 20, color: theme.muted });
1229
+ case "toggle":
1230
+ return /* @__PURE__ */ jsx(Switch, { on: true, c: colors });
1231
+ case "text":
1232
+ return /* @__PURE__ */ jsx(Box, { style: { fontSize: 13, color: theme.muted, flexShrink: 0 }, children: p.trailingText ?? "" });
1233
+ case "badge":
1234
+ return /* @__PURE__ */ jsx(BadgePill, { label: p.trailingText ?? "1", c: colors });
1235
+ case "icon":
1236
+ return /* @__PURE__ */ jsx(IconGlyph, { name: p.trailingIcon ?? "more-horizontal", size: 20, color: theme.muted });
1237
+ default:
1238
+ return null;
1239
+ }
1240
+ })();
1241
+ return /* @__PURE__ */ jsxs(Box, { style: { minHeight: 60, padding: "10px 0", gap: 14, alignItems: "center" }, children: [
1242
+ leading,
1243
+ /* @__PURE__ */ jsxs(Box, { style: { flexGrow: 1, flexShrink: 1, flexDirection: "column", gap: 4 }, children: [
1244
+ p.title ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 15, fontWeight: 600, color: theme.ink }, children: p.title }) : /* @__PURE__ */ jsx(Box, { style: { height: 9, width: "55%", borderRadius: 5, backgroundColor: theme.fill2 } }),
1245
+ p.subtitle ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 13, color: theme.muted }, children: p.subtitle }) : p.title ? null : /* @__PURE__ */ jsx(Box, { style: { height: 7, width: "80%", borderRadius: 4, backgroundColor: theme.fill, marginTop: 4 } })
1246
+ ] }),
1247
+ trailing
1248
+ ] });
1249
+ }
1250
+ var CELL_WIDTHS = [70, 45, 60, 38, 52];
1251
+ function Table({ element }) {
1252
+ const p = element.props;
1253
+ const cols = p.columns ?? [];
1254
+ const data = p.data ?? null;
1255
+ const rowCount = data ? data.length : p.rows ?? 4;
1256
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", border: `1.5px solid ${theme.line}`, borderRadius: 8, overflow: "hidden", fontSize: 14 }, children: [
1257
+ /* @__PURE__ */ jsx(Box, { style: { backgroundColor: theme.fill }, children: cols.map((c, i) => /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, flexBasis: "0px", padding: "10px 14px", fontSize: 13, fontWeight: 600, color: theme.text }, children: c }, i)) }),
1258
+ Array.from({ length: rowCount }, (_, r) => /* @__PURE__ */ jsx(Box, { style: { borderTop: `1px solid ${theme.line}`, minHeight: 42, alignItems: "center" }, children: cols.map((_2, c) => /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, flexBasis: "0px", padding: "10px 14px", color: theme.text }, children: data ? data[r]?.[c] ?? "" : /* @__PURE__ */ jsx(
1259
+ Box,
1260
+ {
1261
+ style: {
1262
+ height: 8,
1263
+ borderRadius: 4,
1264
+ backgroundColor: c === 0 ? theme.fill2 : theme.fill,
1265
+ width: `${CELL_WIDTHS[(r + c * 2) % CELL_WIDTHS.length]}%`
1266
+ }
1267
+ }
1268
+ ) }, c)) }, r))
1269
+ ] });
1270
+ }
1271
+ function Scrim({ show, children, style }) {
1272
+ return /* @__PURE__ */ jsx(
1273
+ Box,
1274
+ {
1275
+ style: {
1276
+ position: "absolute",
1277
+ top: 0,
1278
+ left: 0,
1279
+ right: 0,
1280
+ bottom: 0,
1281
+ backgroundColor: show ? theme.scrim : "transparent",
1282
+ ...style
1283
+ },
1284
+ children
1285
+ }
1286
+ );
1287
+ }
1288
+ function Modal({ element, children }) {
1289
+ const p = element.props;
1290
+ return /* @__PURE__ */ jsx(Scrim, { show: p.scrim ?? true, style: { alignItems: "center", justifyContent: "center", padding: 24 }, children: /* @__PURE__ */ jsxs(
1291
+ Box,
1292
+ {
1293
+ style: {
1294
+ width: p.width ?? 320,
1295
+ flexDirection: "column",
1296
+ gap: 14,
1297
+ padding: 22,
1298
+ borderRadius: 14,
1299
+ backgroundColor: theme.paper,
1300
+ boxShadow: "0 8px 30px rgba(0,0,0,0.18)"
1301
+ },
1302
+ children: [
1303
+ p.title ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 18, fontWeight: 600 }, children: p.title }) : null,
1304
+ withDir(children, "column")
1305
+ ]
1306
+ }
1307
+ ) });
1308
+ }
1309
+ function Drawer({ element, children }) {
1310
+ const p = element.props;
1311
+ const side = p.side ?? "left";
1312
+ const bottom = side === "bottom";
1313
+ const size = p.size ?? (bottom ? 380 : 300);
1314
+ const panel = bottom ? { left: 0, right: 0, bottom: 0, minHeight: size, maxHeight: "92%", borderRadius: "18px 18px 0 0", boxShadow: "0 -6px 24px rgba(0,0,0,0.14)" } : {
1315
+ top: 0,
1316
+ bottom: 0,
1317
+ [side]: 0,
1318
+ width: size,
1319
+ boxShadow: side === "left" ? "6px 0 24px rgba(0,0,0,0.14)" : "-6px 0 24px rgba(0,0,0,0.14)"
1320
+ };
1321
+ return /* @__PURE__ */ jsx(Scrim, { show: p.scrim ?? true, children: /* @__PURE__ */ jsxs(
1322
+ Box,
1323
+ {
1324
+ style: {
1325
+ position: "absolute",
1326
+ flexDirection: "column",
1327
+ backgroundColor: theme.paper,
1328
+ padding: bottom ? "10px 20px 20px" : 20,
1329
+ gap: 14,
1330
+ overflow: "hidden",
1331
+ ...panel
1332
+ },
1333
+ children: [
1334
+ bottom && (p.handle ?? true) ? /* @__PURE__ */ jsx(Box, { style: { justifyContent: "center", paddingBottom: 4 }, children: /* @__PURE__ */ jsx(Box, { style: { width: 40, height: 5, borderRadius: 3, backgroundColor: theme.fill2 } }) }) : null,
1335
+ p.title ? /* @__PURE__ */ jsxs(Box, { style: { alignItems: "center", justifyContent: "space-between" }, children: [
1336
+ /* @__PURE__ */ jsx(Box, { style: { fontSize: 18, fontWeight: 600 }, children: p.title }),
1337
+ /* @__PURE__ */ jsx(IconGlyph, { name: "x", size: 22, color: theme.muted })
1338
+ ] }) : null,
1339
+ withDir(children, "column")
1340
+ ]
1341
+ }
1342
+ ) });
1343
+ }
1344
+ var registry = {
1345
+ Board,
1346
+ Screen,
1347
+ Note,
1348
+ Stack,
1349
+ Grid,
1350
+ Card,
1351
+ Divider,
1352
+ Spacer,
1353
+ Heading,
1354
+ Text,
1355
+ Image,
1356
+ Icon,
1357
+ Avatar,
1358
+ Badge,
1359
+ Button,
1360
+ Input,
1361
+ Checkbox,
1362
+ Radio,
1363
+ Toggle,
1364
+ Select,
1365
+ NavBar,
1366
+ TabBar,
1367
+ Tabs,
1368
+ List,
1369
+ ListItem,
1370
+ Table,
1371
+ Modal,
1372
+ Drawer
1373
+ };
1374
+
1375
+ // src/text.ts
1376
+ var PRIMARY_PROP = {
1377
+ Board: "title",
1378
+ Screen: "name",
1379
+ Note: "text",
1380
+ Card: "title",
1381
+ Heading: "text",
1382
+ Text: "text",
1383
+ Image: "label",
1384
+ Icon: "name",
1385
+ Avatar: "initials",
1386
+ Badge: "label",
1387
+ Button: "label",
1388
+ Input: "label",
1389
+ Checkbox: "label",
1390
+ Radio: "label",
1391
+ Toggle: "label",
1392
+ Select: "label",
1393
+ NavBar: "title",
1394
+ ListItem: "title",
1395
+ Modal: "title",
1396
+ Drawer: "title"
1397
+ };
1398
+ function unwrap(t) {
1399
+ let cur = t;
1400
+ while (cur && ["optional", "nullable", "default"].includes(cur.def?.type)) cur = cur.def.innerType;
1401
+ return cur;
1402
+ }
1403
+ function textWhereExpected(schema, value) {
1404
+ const s = unwrap(schema);
1405
+ switch (s?.def?.type) {
1406
+ case "string":
1407
+ return typeof value === "number" ? String(value) : value;
1408
+ case "array":
1409
+ return Array.isArray(value) ? value.map((v) => textWhereExpected(s.def.element, v)) : value;
1410
+ case "object":
1411
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
1412
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, k in s.shape ? textWhereExpected(s.shape[k], v) : v]));
1413
+ default:
1414
+ return value;
1415
+ }
1416
+ }
1417
+ var COMPONENTS = /* @__PURE__ */ new Map();
1418
+ for (const [name, def] of Object.entries(componentDefinitions)) {
1419
+ const shape = def.props.shape;
1420
+ const enumValues = /* @__PURE__ */ new Map();
1421
+ const ambiguous = /* @__PURE__ */ new Set();
1422
+ const booleans2 = /* @__PURE__ */ new Set();
1423
+ for (const [prop, schema] of Object.entries(shape)) {
1424
+ const inner = unwrap(schema);
1425
+ const type = inner?.def?.type;
1426
+ if (type === "enum") {
1427
+ for (const v of inner.options) {
1428
+ if (enumValues.has(v)) ambiguous.add(v);
1429
+ else enumValues.set(v, prop);
1430
+ }
1431
+ } else if (type === "boolean") {
1432
+ booleans2.add(prop);
1433
+ }
1434
+ }
1435
+ for (const v of ambiguous) enumValues.delete(v);
1436
+ COMPONENTS.set(name.toLowerCase(), { name, props: shape, enumValues, ambiguous, booleans: booleans2 });
1437
+ }
1438
+ function belongsElsewhere(component, word, asProp) {
1439
+ const info = COMPONENTS.get(component.toLowerCase());
1440
+ if (!info) return "";
1441
+ const props = [], options = [];
1442
+ for (const other of COMPONENTS.values()) {
1443
+ if (other === info) continue;
1444
+ if (word in other.props && (asProp || other.booleans.has(word))) props.push(other.name);
1445
+ else if (!asProp && (other.enumValues.has(word) || other.ambiguous.has(word))) options.push(other.name);
1446
+ }
1447
+ const names = (xs) => xs.length > 3 ? `${xs.slice(0, 3).join(", ")}\u2026` : xs.join(", ").replace(/, ([^,]*)$/, " and $1");
1448
+ const a = (name) => /^[AEIOU]/.test(name) ? `an ${name}` : `a ${name}`;
1449
+ let msg = props.length ? `${word} is a ${names(props)} prop, not ${a(info.name)} one` : options.length ? `${word} is a ${names(options)} option, not ${a(info.name)} one` : "";
1450
+ const width = info.props.width;
1451
+ if (msg && word === "fullWidth" && /fills the space/i.test(width?.description ?? unwrap(width)?.description ?? "")) {
1452
+ msg += `; ${a(info.name)} already fills its width (width=\u2026 sets a fixed one), so remove it`;
1453
+ }
1454
+ return msg;
1455
+ }
1456
+ function bareWords(component) {
1457
+ const info = COMPONENTS.get(component.toLowerCase());
1458
+ if (!info) throw new Error(`unknown component ${component}`);
1459
+ return {
1460
+ options: [...info.enumValues].map(([value, prop]) => ({ value, prop })),
1461
+ ambiguous: [...info.ambiguous],
1462
+ booleans: [...info.booleans],
1463
+ wordPrimary: WORD_PRIMARY.has(info.name)
1464
+ };
1465
+ }
1466
+ var normalizeType = (s) => s.toLowerCase().replace(/[-_]/g, "");
1467
+ var WORD_PRIMARY = /* @__PURE__ */ new Set(["Icon", "Avatar"]);
1468
+ var ANTONYMS = { off: "on", unchecked: "checked" };
1469
+ function tokenize(src) {
1470
+ const out = [];
1471
+ let i = 0;
1472
+ while (i < src.length) {
1473
+ const c = src[i];
1474
+ if (c === " " || c === " ") {
1475
+ i++;
1476
+ continue;
1477
+ }
1478
+ if (c === '"' || c === "'") {
1479
+ let j2 = i + 1, s = "";
1480
+ while (j2 < src.length && src[j2] !== c) {
1481
+ if (src[j2] === "\\" && j2 + 1 < src.length) {
1482
+ s += src[j2 + 1];
1483
+ j2 += 2;
1484
+ continue;
1485
+ }
1486
+ s += src[j2++];
1487
+ }
1488
+ if (j2 >= src.length) throw new Error("unterminated string");
1489
+ out.push({ kind: "str", value: s, start: i, end: j2 + 1 });
1490
+ i = j2 + 1;
1491
+ continue;
1492
+ }
1493
+ if ("=[]{}:,".includes(c)) {
1494
+ out.push({ kind: "sym", value: c, start: i, end: i + 1 });
1495
+ i++;
1496
+ continue;
1497
+ }
1498
+ let j = i;
1499
+ while (j < src.length && !` ,=[]{}":`.includes(src[j])) j++;
1500
+ const w = src.slice(i, j);
1501
+ out.push({ kind: /^-?\d+(\.\d+)?$/.test(w) ? "num" : "word", value: w, start: i, end: j });
1502
+ i = j;
1503
+ }
1504
+ return out;
1505
+ }
1506
+ var Cursor = class {
1507
+ constructor(toks, src) {
1508
+ this.toks = toks;
1509
+ this.src = src;
1510
+ }
1511
+ toks;
1512
+ src;
1513
+ i = 0;
1514
+ peek(o2 = 0) {
1515
+ return this.toks[this.i + o2];
1516
+ }
1517
+ next() {
1518
+ return this.toks[this.i++];
1519
+ }
1520
+ done() {
1521
+ return this.i >= this.toks.length;
1522
+ }
1523
+ isSym(v, o2 = 0) {
1524
+ const t = this.peek(o2);
1525
+ return t?.kind === "sym" && t.value === v;
1526
+ }
1527
+ /** Commas only separate list items; elsewhere (between arguments, between {…} fields) they're ignored. */
1528
+ skipCommas() {
1529
+ while (this.isSym(",")) this.i++;
1530
+ }
1531
+ };
1532
+ var scalar = (t) => t.kind === "num" ? Number(t.value) : t.kind === "word" && t.value === "true" ? true : t.kind === "word" && t.value === "false" ? false : t.kind === "word" && t.value === "null" ? null : t.value;
1533
+ function parseListItem(c) {
1534
+ const first = c.peek();
1535
+ if (!first) throw new Error("unclosed [");
1536
+ if (first.kind === "str" || c.isSym("[") || c.isSym("{")) {
1537
+ const v = parseValue(c);
1538
+ if (!c.done() && !c.isSym(",") && !c.isSym("]")) {
1539
+ throw new Error(`expected , or ] after ${JSON.stringify(v)} in a list (quote the whole item, or separate items with commas)`);
1540
+ }
1541
+ return v;
1542
+ }
1543
+ const toks = [];
1544
+ while (!c.done() && !c.isSym(",") && !c.isSym("]")) {
1545
+ const t = c.peek();
1546
+ if (t.kind === "sym" && "[]{}".includes(t.value)) throw new Error(`unexpected "${t.value}" in a list item; quote the item`);
1547
+ if (t.kind === "str") throw new Error(`unexpected quote in a list item; quote the whole item`);
1548
+ toks.push(c.next());
1549
+ }
1550
+ if (!toks.length) throw new Error("empty item in a list (two commas in a row?)");
1551
+ if (toks.length === 1) return scalar(toks[0]);
1552
+ return c.src.slice(toks[0].start, toks[toks.length - 1].end);
1553
+ }
1554
+ function parseValue(c) {
1555
+ const t = c.next();
1556
+ if (!t) throw new Error("missing value");
1557
+ if (t.kind === "str") return t.value;
1558
+ if (t.kind === "num") return Number(t.value);
1559
+ if (t.kind === "word") return scalar(t);
1560
+ if (t.value === "[") {
1561
+ const arr = [];
1562
+ while (!c.isSym("]")) {
1563
+ if (c.done()) throw new Error("unclosed [");
1564
+ arr.push(parseListItem(c));
1565
+ if (c.isSym(",")) c.next();
1566
+ }
1567
+ c.next();
1568
+ return arr;
1569
+ }
1570
+ if (t.value === "{") {
1571
+ const obj = {};
1572
+ while (!c.isSym("}")) {
1573
+ c.skipCommas();
1574
+ if (c.isSym("}")) break;
1575
+ if (c.done()) throw new Error("unclosed {");
1576
+ const k = c.next();
1577
+ if (!k || k.kind === "sym") throw new Error("expected key in {\u2026}");
1578
+ if (!(c.isSym("=") || c.isSym(":"))) throw new Error(`expected = after ${k.value}`);
1579
+ c.next();
1580
+ obj[k.value] = parseValue(c);
1581
+ }
1582
+ c.next();
1583
+ return obj;
1584
+ }
1585
+ throw new Error(`unexpected "${t.value}"`);
1586
+ }
1587
+ function parseWireframeText(source) {
1588
+ const issues = [];
1589
+ const elements = {};
1590
+ const lines = {};
1591
+ const stack = [];
1592
+ let root = null;
1593
+ let n = 0;
1594
+ let lastElementLine = 0;
1595
+ const srcLines = source.replace(/\r\n?/g, "\n").split("\n");
1596
+ srcLines.forEach((raw, idx) => {
1597
+ const lineNo = idx + 1;
1598
+ const trimmed = raw.trim();
1599
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("```")) return;
1600
+ const indent = raw.length - raw.trimStart().length;
1601
+ const target = lastElementLine ? `line ${lastElementLine}` : "its component's line";
1602
+ const wrappedProp = trimmed.match(/^([A-Za-z][\w-]*)\s*[=:]/);
1603
+ if (wrappedProp) {
1604
+ issues.push({ line: lineNo, message: `${wrappedProp[1]}=\u2026: props go on the same line as their component; move it to the end of ${target}` });
1605
+ return;
1606
+ }
1607
+ if (/^[\[\]{}]/.test(trimmed)) {
1608
+ issues.push({ line: lineNo, message: `a list must stay on one line; join this to ${target}` });
1609
+ return;
1610
+ }
1611
+ const m = trimmed.match(/^([A-Za-z][\w-]*)(.*)$/);
1612
+ if (!m) {
1613
+ issues.push({ line: lineNo, message: `expected a component name, got "${trimmed}"` });
1614
+ return;
1615
+ }
1616
+ lastElementLine = lineNo;
1617
+ const info = COMPONENTS.get(normalizeType(m[1]));
1618
+ if (!info) {
1619
+ issues.push({ line: lineNo, message: unknownComponentMessage(m[1], [...COMPONENTS.keys()]) });
1620
+ return;
1621
+ }
1622
+ const props = {};
1623
+ try {
1624
+ const c = new Cursor(tokenize(m[2]), m[2]);
1625
+ while (!c.done()) {
1626
+ c.skipCommas();
1627
+ if (c.done()) break;
1628
+ const t = c.peek();
1629
+ if (t.kind !== "sym" && (c.isSym("=", 1) || c.isSym(":", 1))) {
1630
+ c.next();
1631
+ c.next();
1632
+ const key = RENAMED_PROPS[info.name]?.[t.value] ?? t.value;
1633
+ if (!(key in info.props)) {
1634
+ const elsewhere = belongsElsewhere(info.name, key, true);
1635
+ issues.push({ line: lineNo, message: `${info.name} has no prop "${key}"` + (elsewhere ? `: ${elsewhere}` : "") + ` (its props: ${Object.keys(info.props).join(", ")})` });
1636
+ }
1637
+ props[key] = key in info.props ? textWhereExpected(info.props[key], parseValue(c)) : parseValue(c);
1638
+ continue;
1639
+ }
1640
+ c.next();
1641
+ if (t.kind === "str") {
1642
+ const p = PRIMARY_PROP[info.name];
1643
+ if (!p) issues.push({ line: lineNo, message: `${info.name} takes no text; use key="\u2026"` });
1644
+ else if (p in props) issues.push({ line: lineNo, message: `more than one text for ${info.name}` });
1645
+ else props[p] = t.value;
1646
+ } else if (t.kind === "word" && info.enumValues.has(t.value)) {
1647
+ props[info.enumValues.get(t.value)] = t.value;
1648
+ } else if (t.kind === "word" && info.booleans.has(t.value)) {
1649
+ props[t.value] = true;
1650
+ } else if (t.kind === "word" && t.value.startsWith("no-") && info.booleans.has(t.value.slice(3))) {
1651
+ props[t.value.slice(3)] = false;
1652
+ } else if (t.kind === "word" && ANTONYMS[t.value] && info.booleans.has(ANTONYMS[t.value])) {
1653
+ props[ANTONYMS[t.value]] = false;
1654
+ } else if (t.kind === "word" && WORD_PRIMARY.has(info.name) && !(PRIMARY_PROP[info.name] in props)) {
1655
+ props[PRIMARY_PROP[info.name]] = t.value;
1656
+ } else if (t.kind === "word" && t.value.startsWith("#")) {
1657
+ issues.push({ line: lineNo, message: `"${t.value}": a comment must be on its own line, starting with #; to show # as text, quote it` });
1658
+ break;
1659
+ } else {
1660
+ const options = [...info.enumValues.keys(), ...info.booleans];
1661
+ const elsewhere = t.kind === "word" ? belongsElsewhere(info.name, t.value, false) : "";
1662
+ issues.push({
1663
+ line: lineNo,
1664
+ message: (elsewhere ? `${info.name}: ${elsewhere}` : `${info.name}: don't know what "${t.value}" is`) + (options.length ? ` (bare words it accepts: ${options.join(", ")})` : "") + (PRIMARY_PROP[info.name] && !(PRIMARY_PROP[info.name] in props) ? `; quote text like "${t.value}"` : "")
1665
+ });
1666
+ }
1667
+ }
1668
+ } catch (e) {
1669
+ issues.push({ line: lineNo, message: e.message });
1670
+ return;
1671
+ }
1672
+ const id = `${info.name.toLowerCase()}-${++n}`;
1673
+ elements[id] = { type: info.name, props, children: [] };
1674
+ lines[id] = lineNo;
1675
+ while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
1676
+ const parent = stack[stack.length - 1];
1677
+ if (parent) {
1678
+ elements[parent.id].children.push(id);
1679
+ } else if (root === null) {
1680
+ root = id;
1681
+ } else {
1682
+ issues.push({ line: lineNo, message: "only one top-level element (the board) is allowed; indent this line" });
1683
+ }
1684
+ stack.push({ indent, id });
1685
+ });
1686
+ if (root === null && !issues.length) issues.push({ line: 1, message: "empty wireframe" });
1687
+ return { spec: root ? { root, elements } : null, issues, lines };
1688
+ }
1689
+
1690
+ // src/render.ts
1691
+ import { readFile } from "node:fs/promises";
1692
+ import { createRequire } from "node:module";
1693
+ import { renderToSvg } from "@json-render/image/render";
1694
+ var fontFile = (name) => createRequire(import.meta.url).resolve(`@fontsource/inter/files/${name}`);
1695
+ var fontCache = null;
1696
+ function loadFonts() {
1697
+ fontCache ??= Promise.all([
1698
+ readFile(fontFile("inter-latin-400-normal.woff")),
1699
+ readFile(fontFile("inter-latin-600-normal.woff"))
1700
+ ]).then(([regular, semibold]) => [
1701
+ { name: "Inter", data: regular, weight: 400, style: "normal" },
1702
+ { name: "Inter", data: semibold, weight: 600, style: "normal" }
1703
+ ]);
1704
+ return fontCache;
1705
+ }
1706
+ var SpecError = class extends Error {
1707
+ constructor(issues) {
1708
+ super(`Invalid wireframe spec:
1709
+ - ${issues.join("\n - ")}`);
1710
+ this.issues = issues;
1711
+ }
1712
+ issues;
1713
+ };
1714
+ function checkSpec(spec) {
1715
+ upgradeRenamedProps(spec);
1716
+ const issues = [];
1717
+ const result = catalog.validate(spec);
1718
+ if (!result.success) {
1719
+ const err = result.error;
1720
+ const zodIssues = err?.issues ?? [];
1721
+ if (zodIssues.length) {
1722
+ for (const i of zodIssues) issues.push(`${(i.path ?? []).join(".")}: ${i.message}`);
1723
+ } else {
1724
+ issues.push(String(err ?? "catalog validation failed"));
1725
+ }
1726
+ }
1727
+ const root = spec.elements?.[spec.root];
1728
+ if (!root) {
1729
+ issues.push(`root "${spec.root}" is not in elements`);
1730
+ return issues;
1731
+ }
1732
+ if (root.type !== "Board") issues.push(`root must be a Board (got ${root.type})`);
1733
+ for (const [id, el] of Object.entries(spec.elements)) {
1734
+ const def = componentDefinitions[el.type];
1735
+ if (!def) {
1736
+ issues.push(`${id}: ${unknownComponentMessage(el.type, Object.keys(componentDefinitions))}`);
1737
+ } else {
1738
+ const parsed = def.props.safeParse(el.props ?? {});
1739
+ if (!parsed.success) {
1740
+ for (const i of parsed.error.issues) {
1741
+ issues.push(`${id}.props${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`);
1742
+ }
1743
+ }
1744
+ for (const key of Object.keys(el.props ?? {})) {
1745
+ if (!(key in def.props.shape)) {
1746
+ const elsewhere = belongsElsewhere(el.type, key, true);
1747
+ issues.push(`${id}.props: ${el.type} has no prop "${key}"${elsewhere ? `: ${elsewhere}` : ""}`);
1748
+ }
1749
+ }
1750
+ }
1751
+ for (const child of el.children ?? []) {
1752
+ if (!spec.elements[child]) issues.push(`${id}: child "${child}" does not exist`);
1753
+ }
1754
+ if (el.type === "Screen" && !(root.children ?? []).includes(id)) {
1755
+ issues.push(`${id}: Screen must be a direct child of the Board`);
1756
+ }
1757
+ if (el.type === "Modal" || el.type === "Drawer") {
1758
+ const parent = Object.entries(spec.elements).find(([, p]) => p.children?.includes(id));
1759
+ if (parent && parent[1].type !== "Screen") {
1760
+ issues.push(`${id}: ${el.type} must be a direct child of a Screen (found in ${parent[1].type} "${parent[0]}")`);
1761
+ }
1762
+ }
1763
+ if (el.type === "ListItem") {
1764
+ const p = el.props ?? {};
1765
+ const { leading, trailing } = listItemEnds(p);
1766
+ const conflict = (prop, needs, kind, has) => issues.push(`${id}: ${prop} only shows with ${needs} (this item has ${kind}=${has}); remove one of them`);
1767
+ if (p.leadingIcon && leading !== "icon") conflict("leadingIcon", "leading=icon", "leading", leading);
1768
+ if (p.trailingIcon && trailing !== "icon") conflict("trailingIcon", "trailing=icon", "trailing", trailing);
1769
+ if (p.trailingText && trailing !== "text" && trailing !== "badge") conflict("trailingText", "trailing=text or badge", "trailing", trailing);
1770
+ }
1771
+ if (el.type === "Table") {
1772
+ const { columns, data } = el.props ?? {};
1773
+ if (Array.isArray(columns) && Array.isArray(data)) {
1774
+ const bad = data.map((row, i) => ({ row: i + 1, cells: Array.isArray(row) ? row.length : 1 })).filter((r) => r.cells !== columns.length);
1775
+ if (bad.length) {
1776
+ const cells = (n) => `${n} cell${n === 1 ? "" : "s"}`;
1777
+ const rows = bad.length === 1 ? `data row ${bad[0].row} has ${cells(bad[0].cells)}` : `data rows ${bad.map((r) => `${r.row} (${cells(r.cells)})`).join(", ")} don't match`;
1778
+ const hints = [
1779
+ bad.some((r) => r.cells > columns.length) && `quote cells that contain a comma, e.g. ["$1,200", Paid]`,
1780
+ bad.some((r) => r.cells < columns.length) && `use "" for an empty cell`
1781
+ ].filter(Boolean);
1782
+ issues.push(`${id}: Table has ${columns.length} columns, but ${rows} (${hints.join("; ")})`);
1783
+ }
1784
+ }
1785
+ }
1786
+ }
1787
+ return issues;
1788
+ }
1789
+ function boardLayout(spec) {
1790
+ const board = spec.elements[spec.root];
1791
+ const p = board.props ?? {};
1792
+ const gap = p.gap ?? BOARD_GAP;
1793
+ const pad = p.padding ?? BOARD_PADDING;
1794
+ const kids = (board.children ?? []).map((id) => spec.elements[id]).filter(Boolean);
1795
+ const boxes = kids.map((el) => {
1796
+ const props = el.props ?? {};
1797
+ if (el.type === "Screen") {
1798
+ const s = screenSize(props);
1799
+ return { type: "Screen", name: String(props.name ?? ""), w: s.width, h: s.height + LABEL_H };
1800
+ }
1801
+ if (el.type === "Note") {
1802
+ const w = props.width ?? NOTE_WIDTH;
1803
+ return { type: "Note", name: String(props.text ?? ""), w, h: LABEL_H + estimateNoteHeight(props.text ?? "", w) };
1804
+ }
1805
+ return null;
1806
+ });
1807
+ const perRow = p.layout === "grid" ? Math.max(1, p.columns ?? 3) : Math.max(1, boxes.length);
1808
+ const items = [];
1809
+ let width = 0;
1810
+ let y = pad + (p.title ? TITLE_H : 0);
1811
+ for (let i = 0; i < boxes.length; i += perRow) {
1812
+ const row = boxes.slice(i, i + perRow);
1813
+ if (i > 0) y += gap;
1814
+ let x = pad;
1815
+ for (const b of row) {
1816
+ if (b) items.push({ type: b.type, name: b.name, x, y, width: b.w, height: b.h });
1817
+ x += (b?.w ?? 0) + gap;
1818
+ }
1819
+ width = Math.max(width, row.reduce((sum, b) => sum + (b?.w ?? 0), 0) + gap * (row.length - 1));
1820
+ y += Math.max(...row.map((b) => b?.h ?? 0));
1821
+ }
1822
+ return { width: Math.ceil(width + pad * 2), height: Math.ceil(y + pad), items };
1823
+ }
1824
+ function boardSize(spec) {
1825
+ const { width, height } = boardLayout(spec);
1826
+ return { width, height };
1827
+ }
1828
+ async function renderWireframeSvg(spec, opts = {}) {
1829
+ upgradeRenamedProps(spec);
1830
+ if (!opts.skipValidation) {
1831
+ const issues = checkSpec(spec);
1832
+ if (issues.length) throw new SpecError(issues);
1833
+ }
1834
+ const { width, height } = boardSize(spec);
1835
+ return renderToSvg(spec, {
1836
+ registry: withPalette(paletteFor(spec.elements[spec.root]?.props?.accent)),
1837
+ includeStandard: false,
1838
+ fonts: await loadFonts(),
1839
+ width,
1840
+ height
1841
+ });
1842
+ }
1843
+ async function renderWireframePng(spec, opts = {}) {
1844
+ const svg = await renderWireframeSvg(spec, opts);
1845
+ const { Resvg } = await import("@resvg/resvg-js");
1846
+ const resvg = new Resvg(svg, { fitTo: { mode: "zoom", value: opts.scale ?? 1 } });
1847
+ return resvg.render().asPng();
1848
+ }
1849
+
1850
+ // src/compile.ts
1851
+ function compileWireframe(source) {
1852
+ const parsed = parseWireframeText(source);
1853
+ const issues = [...parsed.issues];
1854
+ if (parsed.spec) {
1855
+ for (const msg of checkSpec(parsed.spec)) {
1856
+ const m = msg.match(/^([\w-]+)(?:\.props[^:]*)?: (.*)$/);
1857
+ const line = m ? parsed.lines[m[1]] : void 0;
1858
+ const text = m && line ? m[2].startsWith(`${parsed.spec.elements[m[1]].type} `) ? m[2] : `${parsed.spec.elements[m[1]].type}: ${m[2]}` : msg;
1859
+ const readable = text.replace(
1860
+ /(\w+) "([\w-]+)"/g,
1861
+ (all, type, id) => parsed.lines[id] ? `the ${type} on line ${parsed.lines[id]}` : all
1862
+ );
1863
+ issues.push({ line: line ?? 1, message: readable });
1864
+ }
1865
+ }
1866
+ const seen = /* @__PURE__ */ new Set();
1867
+ const unique = issues.filter((i) => {
1868
+ const key = `${i.line}|${i.message.replace(/^\w+: /, "").replace(/(has no prop "[^"]*").*$/, "$1").replace(/ \(.*$/, "")}`;
1869
+ if (seen.has(key)) return false;
1870
+ seen.add(key);
1871
+ return true;
1872
+ });
1873
+ unique.sort((a, b) => a.line - b.line);
1874
+ return { spec: parsed.spec, issues: unique };
1875
+ }
1876
+ function formatIssues(issues) {
1877
+ return issues.map((i) => `line ${i.line}: ${i.message}`).join("\n");
1878
+ }
1879
+ var WireframeError = class extends Error {
1880
+ constructor(issues) {
1881
+ super(`Invalid wireframe:
1882
+ ${formatIssues(issues)}`);
1883
+ this.issues = issues;
1884
+ }
1885
+ issues;
1886
+ };
1887
+ async function renderWireframe(source, opts = {}) {
1888
+ const { spec, issues } = compileWireframe(source);
1889
+ if (!spec || issues.length) throw new WireframeError(issues);
1890
+ return opts.format === "png" ? renderWireframePng(spec, { skipValidation: true, scale: opts.scale }) : renderWireframeSvg(spec, { skipValidation: true });
1891
+ }
1892
+
1893
+ // src/print.ts
1894
+ var BARE_WORD = /^[A-Za-z][\w-]*$/;
1895
+ var BARE_PROPS = /* @__PURE__ */ new Set(["device", "variant", "direction", "size", "side", "type", "layout"]);
1896
+ function unwrap2(t) {
1897
+ let cur = t;
1898
+ while (cur && ["optional", "nullable", "default"].includes(cur.def?.type)) cur = cur.def.innerType;
1899
+ return cur;
1900
+ }
1901
+ function fmt(v) {
1902
+ if (typeof v === "string") return BARE_WORD.test(v) && !["true", "false", "null"].includes(v) ? v : JSON.stringify(v);
1903
+ if (typeof v === "number" || typeof v === "boolean" || v === null) return String(v);
1904
+ if (Array.isArray(v)) return `[${v.map(fmt).join(", ")}]`;
1905
+ if (typeof v === "object") {
1906
+ return `{${Object.entries(v).map(([k, x]) => `${k}=${fmt(x)}`).join(" ")}}`;
1907
+ }
1908
+ return JSON.stringify(v);
1909
+ }
1910
+ function bareEnums(type) {
1911
+ const shape = componentDefinitions[type].props.shape;
1912
+ const seen = /* @__PURE__ */ new Map();
1913
+ const dup = /* @__PURE__ */ new Set();
1914
+ for (const [prop, s] of Object.entries(shape)) {
1915
+ const inner = unwrap2(s);
1916
+ if (inner?.def?.type === "enum") {
1917
+ for (const v of inner.options) seen.has(v) ? dup.add(v) : seen.set(v, prop);
1918
+ }
1919
+ }
1920
+ for (const d of dup) seen.delete(d);
1921
+ return seen;
1922
+ }
1923
+ function booleans(type) {
1924
+ const shape = componentDefinitions[type].props.shape;
1925
+ return new Set(Object.entries(shape).filter(([, s]) => unwrap2(s)?.def?.type === "boolean").map(([k]) => k));
1926
+ }
1927
+ function printWireframeText(spec, indentUnit = " ") {
1928
+ const out = [];
1929
+ const walk = (id, depth) => {
1930
+ const el = spec.elements[id];
1931
+ const props = { ...el.props ?? {} };
1932
+ const parts = [el.type.toLowerCase()];
1933
+ const enums = bareEnums(el.type);
1934
+ const bools = booleans(el.type);
1935
+ const primary = PRIMARY_PROP[el.type];
1936
+ for (const [k, v] of Object.entries(props)) {
1937
+ if (typeof v === "string" && BARE_PROPS.has(k) && enums.get(v) === k) {
1938
+ parts.push(v);
1939
+ delete props[k];
1940
+ }
1941
+ }
1942
+ if (primary && typeof props[primary] === "string") {
1943
+ parts.push(JSON.stringify(props[primary]));
1944
+ delete props[primary];
1945
+ }
1946
+ for (const [k, v] of Object.entries(props)) {
1947
+ if (v === void 0 || v === null) continue;
1948
+ if (v === true && bools.has(k)) parts.push(k);
1949
+ else parts.push(`${k}=${fmt(v)}`);
1950
+ }
1951
+ out.push(indentUnit.repeat(depth) + parts.join(" "));
1952
+ for (const c of el.children ?? []) walk(c, depth + 1);
1953
+ };
1954
+ walk(spec.root, 0);
1955
+ return out.join("\n") + "\n";
1956
+ }
1957
+
1958
+ // src/prompt-example.ts
1959
+ var exampleSpec = {
1960
+ root: "board",
1961
+ elements: {
1962
+ board: { type: "Board", props: { title: "Recipe app" }, children: ["browse", "detail", "note"] },
1963
+ browse: { type: "Screen", props: { name: "Browse", device: "phone" }, children: ["b-nav", "b-search", "b-tabs", "b-grid", "b-tabbar"] },
1964
+ "b-nav": { type: "NavBar", props: { title: "Recipes", leading: "menu", actions: ["bell"] }, children: [] },
1965
+ "b-search": { type: "Input", props: { type: "search", placeholder: "Search recipes" }, children: [] },
1966
+ "b-tabs": { type: "Tabs", props: { items: ["All", "Quick", "Vegetarian"], active: 0 }, children: [] },
1967
+ "b-grid": { type: "Grid", props: { columns: 2, gap: 12 }, children: ["card-1", "card-2"] },
1968
+ "card-1": { type: "Card", props: { padding: 10 }, children: ["c1-img", "c1-title", "c1-meta"] },
1969
+ "c1-img": { type: "Image", props: { height: 110 }, children: [] },
1970
+ "c1-title": { type: "Text", props: { text: "Tomato soup", bold: true }, children: [] },
1971
+ "c1-meta": { type: "Text", props: { text: "25 min", size: "sm", muted: true }, children: [] },
1972
+ "card-2": { type: "Card", props: { padding: 10 }, children: ["c2-img", "c2-title"] },
1973
+ "c2-img": { type: "Image", props: { height: 110 }, children: [] },
1974
+ "c2-title": { type: "Text", props: { lines: 2 }, children: [] },
1975
+ "b-tabbar": {
1976
+ type: "TabBar",
1977
+ props: { items: [{ label: "Browse", icon: "book-open" }, { label: "Saved", icon: "heart" }, { label: "Profile", icon: "user" }], active: 0 },
1978
+ children: []
1979
+ },
1980
+ detail: { type: "Screen", props: { name: "Recipe", device: "phone" }, children: ["d-nav", "d-img", "d-title", "d-tags", "d-body", "d-cook", "d-sheet"] },
1981
+ "d-nav": { type: "NavBar", props: { leading: "back", actions: ["share", "more-horizontal"] }, children: [] },
1982
+ "d-img": { type: "Image", props: { height: 200, label: "Photo" }, children: [] },
1983
+ "d-title": { type: "Heading", props: { text: "Tomato soup", level: 1 }, children: [] },
1984
+ "d-tags": { type: "Stack", props: { direction: "row", gap: 8 }, children: ["tag-1", "tag-2"] },
1985
+ "tag-1": { type: "Badge", props: { label: "25 min", variant: "outline" }, children: [] },
1986
+ "tag-2": { type: "Badge", props: { label: "Vegan", variant: "outline" }, children: [] },
1987
+ "d-body": { type: "Text", props: { lines: 4 }, children: [] },
1988
+ "d-cook": { type: "Button", props: { label: "Start cooking", variant: "primary", fullWidth: true }, children: [] },
1989
+ "d-sheet": { type: "Drawer", props: { side: "bottom", title: "Options", size: 260 }, children: ["opt-1", "opt-2", "opt-3"] },
1990
+ "opt-1": { type: "Toggle", props: { label: "Metric units", on: true }, children: [] },
1991
+ "opt-2": { type: "Select", props: { label: "Servings", value: "4" }, children: [] },
1992
+ "opt-3": { type: "Checkbox", props: { label: "Add to shopping list" }, children: [] },
1993
+ note: { type: "Note", props: { text: "Detail screen shown with the options sheet open." }, children: [] }
1994
+ }
1995
+ };
1996
+
1997
+ // src/prompt.ts
1998
+ function unwrap3(t) {
1999
+ let cur = t;
2000
+ while (cur && ["optional", "nullable", "default"].includes(cur.def?.type)) cur = cur.def.innerType;
2001
+ return cur;
2002
+ }
2003
+ function describeType(t) {
2004
+ const s = unwrap3(t);
2005
+ switch (s?.def?.type) {
2006
+ case "string":
2007
+ return "string";
2008
+ case "number":
2009
+ return "number";
2010
+ case "boolean":
2011
+ return "boolean";
2012
+ case "enum":
2013
+ return s.options.map((o2) => `"${o2}"`).join(" | ");
2014
+ case "literal":
2015
+ return s.def.values.map((v) => JSON.stringify(v)).join(" | ");
2016
+ case "union":
2017
+ return s.def.options.map(describeType).join(" | ");
2018
+ case "array":
2019
+ return `array of ${describeType(s.def.element)}`;
2020
+ case "object":
2021
+ return `{ ${Object.entries(s.shape).map(([k, v]) => `${k}: ${describeType(v)}`).join(", ")} }`;
2022
+ default:
2023
+ return "any";
2024
+ }
2025
+ }
2026
+ function componentDocs() {
2027
+ return Object.entries(componentDefinitions).map(([name, def]) => ({
2028
+ name,
2029
+ description: def.description,
2030
+ props: Object.entries(def.props.shape).map(([prop, schema]) => ({
2031
+ name: prop,
2032
+ type: describeType(schema),
2033
+ required: !["optional", "nullable"].includes(schema.def?.type),
2034
+ main: PRIMARY_PROP[name] === prop,
2035
+ description: schema.description ?? unwrap3(schema)?.description
2036
+ }))
2037
+ }));
2038
+ }
2039
+ function componentReference() {
2040
+ const lines = [];
2041
+ for (const c of componentDocs()) {
2042
+ lines.push(`### ${c.name}`, c.description);
2043
+ for (const p of c.props) {
2044
+ lines.push(`- ${p.name}${p.required ? " (required)" : ""}: ${p.type}${p.main ? " (main text)" : ""}${p.description ? ` \u2014 ${p.description}` : ""}`);
2045
+ }
2046
+ lines.push("");
2047
+ }
2048
+ return lines.join("\n");
2049
+ }
2050
+ var RULES = `## Rules
2051
+ 1. The top element is a Board. The Board's children are Screens, plus optional Notes beside them.
2052
+ 2. Each Screen is one view of the product. Use several Screens to show several views or states.
2053
+ 3. A Screen lays out its children top to bottom. Use Stack (direction row or column) and Grid to arrange content.
2054
+ 4. NavBar is pinned to the top of its Screen and TabBar to the bottom.
2055
+ 5. Modal and Drawer are overlays. They must be direct children of a Screen.
2056
+ 6. This is a low-fidelity wireframe. Prefer placeholders (Image boxes, Text with lines) over invented copy unless the copy matters.
2057
+ 7. Only use the components and props listed below. All props are optional unless marked required.
2058
+ 8. 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.`;
2059
+ var TEXT_FORMAT = `## Output format: wireframe text
2060
+ One element per line. Indent children two spaces under their parent. The first line is the board.
2061
+
2062
+ A line is the component name in lowercase, followed by arguments separated by spaces:
2063
+ - "a quoted string" sets the component's main text prop (marked "main text" below)
2064
+ - 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
2065
+ - a bare prop name sets that component's yes/no prop to true, only on components that have it (see each component's props below): checked (checkbox, radio), fullWidth (button), muted (text), grow (stack, card, input, list). \`off\` and \`unchecked\` set on/checked to false
2066
+ - key=value sets any prop. Values: "string", number, true/false, bare word, [list, of, values], {key=value key=value}
2067
+
2068
+ Comments: a line that starts with # is a comment. A comment must be on its own line, never after an element on the same line. Anywhere else, # is ordinary text: [#1001, #1002], "Order #12345", accent=#1a73e8.
2069
+
2070
+ Lists: 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].
2071
+
2072
+ If a bare word could mean more than one prop, write it as key=value.`;
2073
+ var 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.";
2074
+ function wireframePrompt() {
2075
+ return [
2076
+ "You write low-fidelity UI wireframes as specs that a renderer turns into images.",
2077
+ "Reply with only the spec in a single code block, with no explanation.",
2078
+ "",
2079
+ TEXT_FORMAT,
2080
+ "",
2081
+ RULES,
2082
+ "",
2083
+ "## Example",
2084
+ `Request: ${EXAMPLE_REQUEST}`,
2085
+ "",
2086
+ "```tsquare",
2087
+ printWireframeText(exampleSpec).trimEnd(),
2088
+ "```",
2089
+ "",
2090
+ "## Components",
2091
+ "",
2092
+ componentReference()
2093
+ ].join("\n");
2094
+ }
2095
+ function repairPrompt(issues) {
2096
+ return `The wireframe has these problems:
2097
+ ${issues}
2098
+
2099
+ Reply with the corrected wireframe in a single code block.`;
2100
+ }
2101
+
2102
+ export {
2103
+ theme,
2104
+ DEVICES,
2105
+ ACCENTS,
2106
+ iconNames,
2107
+ componentDefinitions,
2108
+ catalog,
2109
+ registry,
2110
+ PRIMARY_PROP,
2111
+ bareWords,
2112
+ parseWireframeText,
2113
+ checkSpec,
2114
+ boardLayout,
2115
+ boardSize,
2116
+ renderWireframeSvg,
2117
+ renderWireframePng,
2118
+ compileWireframe,
2119
+ formatIssues,
2120
+ WireframeError,
2121
+ renderWireframe,
2122
+ printWireframeText,
2123
+ componentDocs,
2124
+ wireframePrompt,
2125
+ repairPrompt
2126
+ };