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,466 @@
1
+ import {
2
+ ACCENTS,
3
+ PRIMARY_PROP,
4
+ bareWords,
5
+ boardLayout,
6
+ compileWireframe,
7
+ componentDefinitions,
8
+ componentDocs,
9
+ formatIssues,
10
+ iconNames,
11
+ renderWireframePng,
12
+ renderWireframeSvg,
13
+ wireframePrompt
14
+ } from "../chunks/chunk-46MGJLGB.js";
15
+
16
+ // src/playground/index.ts
17
+ import { createServer } from "node:http";
18
+ import { readFile, readdir } from "node:fs/promises";
19
+ import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ // src/playground/language-data.ts
23
+ function unwrap(t) {
24
+ let cur = t;
25
+ while (cur && ["optional", "nullable", "default"].includes(cur.def?.type)) cur = cur.def.innerType;
26
+ return cur;
27
+ }
28
+ function describe(name, schema) {
29
+ const s = unwrap(schema);
30
+ const description = schema.description ?? s?.description;
31
+ const iconish = /Lucide icon|Icon names/i.test(description ?? "");
32
+ switch (s?.def?.type) {
33
+ case "enum":
34
+ return { name, kind: "enum", values: s.options, description };
35
+ case "boolean":
36
+ return { name, kind: "boolean", values: ["true", "false"], description };
37
+ case "number":
38
+ return { name, kind: "number", description };
39
+ case "array": {
40
+ const el = unwrap(s.def.element);
41
+ const hasIcon = el?.def?.type === "object" && "icon" in el.shape;
42
+ return { name, kind: "list", icon: iconish || hasIcon, description };
43
+ }
44
+ case "object":
45
+ return { name, kind: "object", description };
46
+ default:
47
+ return { name, kind: "string", icon: iconish || void 0, accent: name === "accent" || void 0, description };
48
+ }
49
+ }
50
+ var cache = null;
51
+ function languageData() {
52
+ if (cache) return cache;
53
+ const components = Object.entries(componentDefinitions).map(([type, def]) => {
54
+ const shape = def.props.shape;
55
+ const props = Object.entries(shape).map(([name, schema]) => describe(name, schema));
56
+ if (type === "Icon") props.find((p) => p.name === "name").icon = true;
57
+ const words = bareWords(type);
58
+ const off = words.booleans.flatMap((b) => b === "on" ? ["off"] : b === "checked" ? ["unchecked"] : [`no-${b}`]);
59
+ return {
60
+ name: type.toLowerCase(),
61
+ description: def.description,
62
+ main: PRIMARY_PROP[type],
63
+ children: def.slots.length > 0,
64
+ props,
65
+ words: [...words.options.map((o) => o.value), ...words.booleans, ...off]
66
+ };
67
+ });
68
+ cache = { components, icons: [...new Set(iconNames)].sort(), accents: Object.keys(ACCENTS) };
69
+ return cache;
70
+ }
71
+
72
+ // src/playground/reference.ts
73
+ var GROUPS = [
74
+ { name: "Canvas", components: ["Board", "Screen", "Note"] },
75
+ { name: "Layout", components: ["Stack", "Grid", "Card", "Divider", "Spacer"] },
76
+ { name: "Content", components: ["Heading", "Text", "Image", "Icon", "Avatar", "Badge"] },
77
+ { name: "Controls", components: ["Button", "Input", "Checkbox", "Radio", "Toggle", "Select"] },
78
+ { name: "Navigation & data", components: ["NavBar", "TabBar", "Tabs", "List", "ListItem", "Table"] },
79
+ { name: "Overlays", components: ["Modal", "Drawer"] }
80
+ ];
81
+ var EXAMPLES = {
82
+ Board: `board row "Checkout flow" gap=48 accent=blue
83
+ screen custom "Cart" width=240 height=200
84
+ heading "Cart" level=3
85
+ text lines=3
86
+ spacer
87
+ button primary "Checkout" fullWidth
88
+ screen custom "Payment" width=240 height=200
89
+ heading "Payment" level=3
90
+ toggle "Save card" on
91
+ spacer
92
+ button primary "Pay" fullWidth
93
+ note "accent colors primary buttons, toggles, checked controls and active tabs. Omit it for grayscale." width=180`,
94
+ Screen: `board
95
+ screen phone "phone"
96
+ heading "390 \xD7 844"
97
+ text "Phone and desktop screens show a status bar or browser bar (chrome). Turn it off with chrome=false." muted
98
+ screen custom "custom" width=260 height=200
99
+ heading "Any size" level=2
100
+ text "device=custom with width and height" muted`,
101
+ Note: `board gap=32
102
+ screen custom width=320 height=220
103
+ text lines=4
104
+ note "Notes can also sit inside a screen" color=blue
105
+ note "yellow (default)" width=130
106
+ note "blue" color=blue width=130
107
+ note "pink" color=pink width=130
108
+ note "green" color=green width=130`,
109
+ Stack: `board
110
+ screen custom width=560 height=300 padding=0
111
+ stack row grow
112
+ stack width=160 border=right fill padding=16 gap=12
113
+ text "Sidebar" bold
114
+ text "width, border, fill" sm muted
115
+ stack grow padding=16 gap=16
116
+ stack row justify=between align=center
117
+ heading "Toolbar" level=3
118
+ stack row gap=8
119
+ button secondary sm "Filter"
120
+ button primary sm "New"
121
+ stack row wrap gap=8
122
+ badge outline "row"
123
+ badge outline "wrap"
124
+ badge outline "gap"
125
+ text lines=3`,
126
+ Grid: `board
127
+ screen custom width=520 height=300
128
+ grid columns=3 gap=12
129
+ card
130
+ image height=60
131
+ text "Item" bold
132
+ card
133
+ image height=60
134
+ text "Item" bold
135
+ card
136
+ image height=60
137
+ text "Item" bold
138
+ card
139
+ image height=60
140
+ text "Item" bold
141
+ card
142
+ image height=60
143
+ text "Item" bold`,
144
+ Card: `board
145
+ screen custom width=520 height=170
146
+ stack row gap=16
147
+ card "outline (default)" grow
148
+ text lines=3
149
+ card "filled" variant=filled grow
150
+ text lines=3`,
151
+ Divider: `board
152
+ screen custom width=360 height=150
153
+ text "Above"
154
+ divider
155
+ text "Below"
156
+ stack row gap=12 align=center
157
+ text "Left"
158
+ divider vertical
159
+ text "Right"`,
160
+ Spacer: `board
161
+ screen custom width=360 height=260
162
+ heading "Fixed spacer" level=3
163
+ spacer size=24
164
+ text "24px gap above"
165
+ spacer
166
+ button primary "Pushed to the bottom" fullWidth`,
167
+ Heading: `board
168
+ screen custom width=360 height=170
169
+ heading "Level 1" level=1
170
+ heading "Level 2" level=2
171
+ heading "Level 3" level=3
172
+ heading "Centered" level=3 align=center`,
173
+ Text: `board
174
+ screen custom width=360 height=300
175
+ text "Body text, default size"
176
+ text lg "Large"
177
+ text sm "Small and muted" muted
178
+ text "Bold" bold
179
+ text "Placeholder lines when lines is set and there's no text:" sm muted
180
+ text lines=3`,
181
+ Image: `board
182
+ screen custom width=480 height=210
183
+ stack row gap=12
184
+ image "label" height=120 width=160
185
+ image height=120 width=120 rounded
186
+ image height=120
187
+ text "In a row, an image without a width is square." sm muted`,
188
+ Icon: `board
189
+ screen custom width=360 height=120
190
+ stack row gap=16 align=center
191
+ icon home
192
+ icon search
193
+ icon bell
194
+ icon settings
195
+ icon heart size=32
196
+ text "Any Lucide name in kebab-case (lucide.dev/icons)" sm muted`,
197
+ Avatar: `board
198
+ screen custom width=300 height=100
199
+ stack row gap=12 align=center
200
+ avatar "JD"
201
+ avatar "AB" size=48
202
+ avatar
203
+ avatar size=24`,
204
+ Badge: `board
205
+ screen custom width=340 height=110
206
+ stack row gap=8
207
+ badge "solid" variant=solid
208
+ badge outline "outline"
209
+ badge "3"
210
+ stack row gap=8
211
+ badge "Active" tone=success
212
+ badge "Pending" tone=warning
213
+ badge "Failed" tone=danger
214
+ badge outline "Failed" tone=danger`,
215
+ Button: `board
216
+ screen custom width=420 height=310
217
+ stack row gap=8 align=center
218
+ button primary "Primary"
219
+ button secondary "Secondary"
220
+ button ghost "Ghost"
221
+ stack row gap=8 align=center
222
+ button primary sm "Small"
223
+ button primary "Medium"
224
+ button primary lg "Large"
225
+ stack row gap=8 align=center
226
+ button secondary "Back" leadingIcon=chevron-left
227
+ button primary "Next" trailingIcon=chevron-right
228
+ button primary "Full width" fullWidth
229
+ text "With a board accent, primary buttons fill with it and ghost buttons use it for text." sm muted`,
230
+ Input: `board
231
+ screen custom width=420 height=540
232
+ input "Email" placeholder="you@example.com" helper="We never share it"
233
+ input "Username" value="dana moore" error helper="No spaces allowed"
234
+ input password "Password" value=secret
235
+ input search placeholder="Search"
236
+ input "Message" multiline=3
237
+ stack row gap=8
238
+ input placeholder="grow fills the row" grow
239
+ button primary "Send"`,
240
+ Checkbox: `board
241
+ screen custom width=300 height=100
242
+ checkbox "Checked" checked
243
+ checkbox "Unchecked"`,
244
+ Radio: `board
245
+ screen custom width=300 height=130
246
+ radio "Selected" checked
247
+ radio "Not selected"
248
+ radio "Not selected"`,
249
+ Toggle: `board
250
+ screen custom width=300 height=100
251
+ toggle "On" on
252
+ toggle "Off" off`,
253
+ Select: `board
254
+ screen custom width=320 height=170
255
+ select "Country" value="Mexico"
256
+ select "Size" placeholder="Choose one" width=160`,
257
+ NavBar: `board
258
+ screen custom width=380 height=240 padding=0 gap=0
259
+ navbar "leading=menu" leading=menu actions=[search, bell]
260
+ navbar "leading=back, centered" leading=back align=center actions=[share]
261
+ navbar "leading=close" leading=close
262
+ navbar "leading=logo" leading=logo actions=[user]`,
263
+ TabBar: `board
264
+ screen phone "Tab bar sits at the bottom"
265
+ heading "Home"
266
+ text lines=4
267
+ tabbar items=[{label=Home icon=house}, {label=Search icon=search}, {label=Inbox icon=inbox}, {label=Profile icon=user}] active=0`,
268
+ Tabs: `board
269
+ screen custom width=400 height=150
270
+ tabs items=[Overview, Activity, Settings] active=0
271
+ text lines=2`,
272
+ List: `board
273
+ screen custom width=380 height=420
274
+ list
275
+ listitem "Dividers on (default)"
276
+ listitem "Second row"
277
+ list dividers=false
278
+ listitem "dividers=false"
279
+ listitem "Second row"
280
+ list
281
+ listitem
282
+ listitem`,
283
+ ListItem: `board
284
+ screen custom width=400 height=440
285
+ list
286
+ listitem "leadingIcon=settings" subtitle="trailing=chevron" leadingIcon=settings trailing=chevron
287
+ listitem "leading=avatar" subtitle="trailing=text" leading=avatar trailing=text trailingText="9:41"
288
+ listitem "leading=image" subtitle="trailing=badge" leading=image trailing=badge trailingText="3"
289
+ listitem "leading=checkbox" leading=checkbox trailing=toggle
290
+ listitem "trailingIcon=trash-2" trailingIcon=trash-2
291
+ listitem leading=image trailing=chevron`,
292
+ Table: `board
293
+ screen custom width=520 height=400
294
+ table columns=[Name, Role, Status] data=[["Ana Ruiz", Admin, Active], ["Ben Cho", Editor, Invited]]
295
+ text "Without data, rows are placeholder bars:" sm muted
296
+ table columns=[Name, Role, Status] rows=3`,
297
+ Modal: `board
298
+ screen phone "Modal"
299
+ heading "Settings"
300
+ text lines=6
301
+ modal "Delete account?"
302
+ text "This can't be undone." muted
303
+ stack row justify=end gap=8
304
+ button ghost "Cancel"
305
+ button primary "Delete"`,
306
+ Drawer: `board
307
+ screen phone "side=left"
308
+ navbar "Inbox" leading=menu
309
+ text lines=6
310
+ drawer left size=260
311
+ heading "Menu" level=3
312
+ list dividers=false
313
+ listitem "Inbox" leadingIcon=inbox
314
+ listitem "Sent" leadingIcon=send
315
+ screen phone "side=bottom"
316
+ navbar "Photo" leading=back
317
+ image height=300
318
+ drawer bottom "Share" size=280
319
+ list dividers=false
320
+ listitem "Copy link" leadingIcon=link
321
+ listitem "Message" leadingIcon=message-circle`
322
+ };
323
+
324
+ // src/playground/index.ts
325
+ var here = path.dirname(fileURLToPath(import.meta.url));
326
+ var packageRoot = path.resolve(here, "..", "..");
327
+ var MAX_BODY = 256 * 1024;
328
+ function checkReferenceExamples() {
329
+ const problems = [];
330
+ for (const c of componentDocs()) {
331
+ if (!EXAMPLES[c.name]) problems.push(`${c.name}: no example in src/playground/reference.ts`);
332
+ if (!GROUPS.some((g) => g.components.includes(c.name))) problems.push(`${c.name}: not in any group in src/playground/reference.ts`);
333
+ }
334
+ for (const [name, src] of Object.entries(EXAMPLES)) {
335
+ const { issues } = compileWireframe(src);
336
+ if (issues.length) problems.push(`${name} example:
337
+ ${formatIssues(issues)}`);
338
+ }
339
+ return problems;
340
+ }
341
+ var scriptCache = null;
342
+ function playgroundScript() {
343
+ scriptCache ??= (async () => {
344
+ const built = path.join(here, "playground.js");
345
+ try {
346
+ return await readFile(built, "utf8");
347
+ } catch {
348
+ const { build } = await import("esbuild");
349
+ const out = await build({ ...PLAYGROUND_BUNDLE, entryPoints: [path.join(here, "app.ts")], write: false });
350
+ return out.outputFiles[0].text;
351
+ }
352
+ })();
353
+ return scriptCache;
354
+ }
355
+ var PLAYGROUND_BUNDLE = {
356
+ bundle: true,
357
+ format: "esm",
358
+ platform: "browser",
359
+ target: "es2022",
360
+ minify: true,
361
+ legalComments: "eof"
362
+ };
363
+ var referenceCache = null;
364
+ function reference() {
365
+ referenceCache ??= Promise.all(
366
+ componentDocs().map(async (c) => {
367
+ const { spec } = compileWireframe(EXAMPLES[c.name]);
368
+ return { ...c, example: EXAMPLES[c.name], svg: await renderWireframeSvg(spec, { skipValidation: true }) };
369
+ })
370
+ ).then((components) => ({ groups: GROUPS, components }));
371
+ return referenceCache;
372
+ }
373
+ var EXAMPLE_ORDER = ["sign-in", "dashboard", "checkout", "states", "notes-mobile", "notes-web"];
374
+ var rank = (name) => EXAMPLE_ORDER.indexOf(name) + 1 || EXAMPLE_ORDER.length + 1;
375
+ async function examples() {
376
+ const dir = path.join(packageRoot, "examples");
377
+ const files = (await readdir(dir)).filter((f) => f.endsWith(".tsq")).sort((a, b) => rank(a.slice(0, -4)) - rank(b.slice(0, -4)) || a.localeCompare(b));
378
+ return Promise.all(files.map(async (f) => ({ name: f.replace(/\.tsq$/, ""), source: await readFile(path.join(dir, f), "utf8") })));
379
+ }
380
+ function readBody(req) {
381
+ return new Promise((resolve, reject) => {
382
+ let size = 0;
383
+ const chunks = [];
384
+ req.on("data", (c) => {
385
+ size += c.length;
386
+ if (size > MAX_BODY) reject(new Error("body too large"));
387
+ else chunks.push(c);
388
+ });
389
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
390
+ req.on("error", reject);
391
+ });
392
+ }
393
+ var json = (res, status, body) => {
394
+ res.writeHead(status, { "content-type": "application/json" });
395
+ res.end(JSON.stringify(body));
396
+ return true;
397
+ };
398
+ function createPlaygroundHandler() {
399
+ return async function handle(req, res) {
400
+ const url = new URL(req.url ?? "/", "http://localhost");
401
+ if (req.method === "GET" && url.pathname === "/") {
402
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
403
+ res.end(await readFile(path.join(here, "index.html")));
404
+ return true;
405
+ }
406
+ if (req.method === "GET" && url.pathname === "/playground.js") {
407
+ res.writeHead(200, { "content-type": "text/javascript; charset=utf-8" });
408
+ res.end(await playgroundScript());
409
+ return true;
410
+ }
411
+ if (req.method === "GET" && url.pathname === "/api/language") return json(res, 200, languageData());
412
+ if (req.method === "GET" && url.pathname === "/mark.png") {
413
+ res.writeHead(200, { "content-type": "image/png", "cache-control": "max-age=3600" });
414
+ res.end(await readFile(path.join(packageRoot, "assets", "mark.png")));
415
+ return true;
416
+ }
417
+ if (req.method === "GET" && url.pathname === "/api/reference") return json(res, 200, await reference());
418
+ if (req.method === "GET" && url.pathname === "/api/examples") return json(res, 200, await examples());
419
+ if (req.method === "GET" && url.pathname === "/api/prompt") {
420
+ res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
421
+ res.end(wireframePrompt());
422
+ return true;
423
+ }
424
+ if (req.method === "POST" && (url.pathname === "/api/render" || url.pathname === "/api/png")) {
425
+ const { spec, issues } = compileWireframe(await readBody(req));
426
+ if (url.pathname === "/api/render") {
427
+ if (!spec || issues.length) return json(res, 200, { issues });
428
+ const { width, height, items } = boardLayout(spec);
429
+ return json(res, 200, { issues, svg: await renderWireframeSvg(spec, { skipValidation: true }), width, height, items });
430
+ }
431
+ if (!spec || issues.length) return json(res, 400, { issues });
432
+ const scale = Math.min(4, Math.max(1, Number(url.searchParams.get("scale") ?? 2)));
433
+ res.writeHead(200, { "content-type": "image/png" });
434
+ res.end(await renderWireframePng(spec, { skipValidation: true, scale }));
435
+ return true;
436
+ }
437
+ return false;
438
+ };
439
+ }
440
+ function startPlayground(port = 4321) {
441
+ const problems = checkReferenceExamples();
442
+ if (problems.length) {
443
+ console.error(`Reference examples need fixing:
444
+
445
+ ${problems.join("\n")}`);
446
+ process.exit(1);
447
+ }
448
+ const handle = createPlaygroundHandler();
449
+ const server = createServer((req, res) => {
450
+ handle(req, res).then((handled) => {
451
+ if (!handled) json(res, 404, { error: "not found" });
452
+ }).catch((err) => {
453
+ console.error(err);
454
+ if (!res.headersSent) json(res, 500, { error: String(err?.message ?? err) });
455
+ else res.end();
456
+ });
457
+ });
458
+ server.listen(port, "127.0.0.1", () => console.log(`tsquare playground \u2192 http://localhost:${port}`));
459
+ return server;
460
+ }
461
+ export {
462
+ PLAYGROUND_BUNDLE,
463
+ checkReferenceExamples,
464
+ createPlaygroundHandler,
465
+ startPlayground
466
+ };
@@ -0,0 +1,28 @@
1
+ export interface PropInfo {
2
+ name: string;
3
+ kind: "string" | "number" | "boolean" | "enum" | "list" | "object";
4
+ /** Allowed values for enums (and booleans: true/false). */
5
+ values?: string[];
6
+ /** The value is a Lucide icon name (or a list of them). */
7
+ icon?: boolean;
8
+ /** The value is an accent color. */
9
+ accent?: boolean;
10
+ description?: string;
11
+ }
12
+ export interface ComponentInfo {
13
+ /** Lowercase, as written: "listitem". */
14
+ name: string;
15
+ description: string;
16
+ /** The prop a quoted string fills, if any. */
17
+ main?: string;
18
+ children: boolean;
19
+ props: PropInfo[];
20
+ /** Bare words this component accepts: option values, switch names and their off forms. */
21
+ words: string[];
22
+ }
23
+ export interface LanguageData {
24
+ components: ComponentInfo[];
25
+ icons: string[];
26
+ accents: string[];
27
+ }
28
+ export declare function languageData(): LanguageData;