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.
- package/README.md +34 -8
- package/assets/mark.png +0 -0
- package/bin/tsquare.js +2 -6
- package/dist/catalog.d.ts +1055 -0
- package/dist/chunks/chunk-46MGJLGB.js +2126 -0
- package/{src/cli.ts → dist/cli.js} +28 -24
- package/dist/colors.d.ts +29 -0
- package/dist/compile.d.ts +24 -0
- package/dist/components.d.ts +83 -0
- package/dist/icons.d.ts +12 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +54 -0
- package/dist/layout.d.ts +66 -0
- package/dist/playground/index.d.ts +38 -0
- package/dist/playground/index.html +325 -0
- package/dist/playground/index.js +466 -0
- package/dist/playground/language-data.d.ts +28 -0
- package/dist/playground/playground.js +47 -0
- package/dist/playground/reference.d.ts +11 -0
- package/dist/print.d.ts +2 -0
- package/dist/prompt-example.d.ts +3 -0
- package/dist/prompt.d.ts +22 -0
- package/dist/render.d.ts +40 -0
- package/dist/schema.d.ts +33 -0
- package/dist/share.d.ts +4 -0
- package/dist/suggest.d.ts +5 -0
- package/dist/text.d.ts +50 -0
- package/examples/checkout.tsq +15 -0
- package/examples/dashboard.tsq +27 -0
- package/examples/notes-mobile.tsq +84 -0
- package/examples/notes-web.tsq +54 -0
- package/examples/sign-in.tsq +14 -0
- package/examples/states.tsq +16 -0
- package/package.json +31 -7
- package/skill/tsquare/SKILL.md +5 -4
- package/skill/tsquare/reference.md +7 -6
- package/src/catalog.ts +0 -329
- package/src/colors.ts +0 -82
- package/src/compile.ts +0 -64
- package/src/components.tsx +0 -1057
- package/src/icons.ts +0 -54
- package/src/index.ts +0 -11
- package/src/layout.ts +0 -63
- package/src/print.ts +0 -73
- package/src/prompt-example.ts +0 -43
- package/src/prompt.ts +0 -121
- package/src/render.ts +0 -178
- package/src/schema.ts +0 -43
- package/src/suggest.ts +0 -34
- 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;
|