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
package/src/catalog.ts DELETED
@@ -1,329 +0,0 @@
1
- import { defineCatalog } from "@json-render/core";
2
- import { z } from "zod";
3
- import { ACCENTS, accentMessage, isAccent } from "./colors";
4
- import { isIcon, unknownIconMessage } from "./icons";
5
- import { wireframeSchema } from "./schema";
6
-
7
- // Every prop is optional so hand-written specs stay short.
8
- const o = <T extends z.ZodType>(t: T) => t.nullish();
9
-
10
- const align = z.enum(["start", "center", "end", "stretch"]);
11
- const justify = z.enum(["start", "center", "end", "between", "around"]);
12
- const iconName = z
13
- .string()
14
- .refine(isIcon, { error: (iss) => unknownIconMessage(String(iss.input)) })
15
- .describe("Lucide icon name in kebab-case, e.g. menu, search, arrow-left, settings, bell, user");
16
-
17
- export const componentDefinitions = {
18
- // ── Canvas ────────────────────────────────────────────────────────────
19
- Board: {
20
- props: z.object({
21
- title: o(z.string()),
22
- layout: o(z.enum(["row", "grid"])).describe("row = all screens side by side; grid = wrap every `columns` screens"),
23
- columns: o(z.number().int().min(1)),
24
- gap: o(z.number()),
25
- padding: o(z.number()),
26
- accent: o(z.string().refine(isAccent, { error: (iss) => accentMessage(String(iss.input)) })).describe(
27
- `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.`,
28
- ),
29
- }),
30
- slots: ["default"],
31
- description: "Root canvas (artboard). Holds Screens side by side, plus optional Notes. Must be the root element.",
32
- example: { title: "Checkout flow", layout: "row", gap: 64 },
33
- },
34
- Screen: {
35
- props: z.object({
36
- name: o(z.string()).describe("Label shown above the screen"),
37
- device: o(z.enum(["phone", "tablet", "desktop", "custom"])),
38
- width: o(z.number()).describe("Only for device=custom"),
39
- height: o(z.number()).describe("Only for device=custom"),
40
- chrome: o(z.boolean()).describe("Phone status bar / desktop browser bar. Default true for phone and desktop."),
41
- padding: o(z.number()),
42
- gap: o(z.number()),
43
- }),
44
- slots: ["default"],
45
- description: "One view of the product in a device frame. Children stack vertically.",
46
- example: { name: "Home", device: "phone" },
47
- },
48
- Note: {
49
- props: z.object({
50
- text: z.string(),
51
- color: o(z.enum(["yellow", "blue", "pink", "green"])),
52
- width: o(z.number()),
53
- }),
54
- slots: [],
55
- description: "Sticky-note annotation. Put it on the Board beside screens, or inside a Screen next to what it explains.",
56
- example: { text: "Empty state TBD", color: "yellow" },
57
- },
58
-
59
- // ── Layout ────────────────────────────────────────────────────────────
60
- Stack: {
61
- props: z.object({
62
- direction: o(z.enum(["row", "column"])),
63
- gap: o(z.number()),
64
- padding: o(z.number()),
65
- align: o(align),
66
- justify: o(justify),
67
- wrap: o(z.boolean()),
68
- grow: o(z.boolean()).describe("Fill remaining space in the parent"),
69
- width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 240 for a sidebar"),
70
- border: o(z.enum(["none", "right", "left", "top", "bottom", "all"])),
71
- fill: o(z.boolean()).describe("Light gray background"),
72
- }),
73
- slots: ["default"],
74
- description: "Flex container. The main layout primitive (rows, columns, sidebars, toolbars).",
75
- example: { direction: "row", gap: 12, align: "center" },
76
- },
77
- Grid: {
78
- props: z.object({
79
- columns: z.number().int().min(1),
80
- gap: o(z.number()),
81
- padding: o(z.number()),
82
- }),
83
- slots: ["default"],
84
- description: "Equal-width columns that wrap. Good for card grids and galleries.",
85
- example: { columns: 3, gap: 16 },
86
- },
87
- Card: {
88
- props: z.object({
89
- title: o(z.string()),
90
- padding: o(z.number()),
91
- gap: o(z.number()),
92
- variant: o(z.enum(["outline", "filled"])),
93
- grow: o(z.boolean()),
94
- width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 320. Default fills the space."),
95
- }),
96
- slots: ["default"],
97
- description: "Bordered container with optional title. Children stack vertically.",
98
- example: { title: "Revenue", variant: "outline" },
99
- },
100
- Divider: {
101
- props: z.object({ vertical: o(z.boolean()) }),
102
- slots: [],
103
- description: "Thin separator line.",
104
- example: {},
105
- },
106
- Spacer: {
107
- props: z.object({
108
- size: o(z.number()).describe("Fixed size in px. Omit to fill remaining space."),
109
- }),
110
- slots: [],
111
- description: "Empty space. Without a size it grows to push siblings apart.",
112
- example: { size: 16 },
113
- },
114
-
115
- // ── Content ───────────────────────────────────────────────────────────
116
- Heading: {
117
- props: z.object({
118
- text: z.string(),
119
- level: o(z.union([z.literal(1), z.literal(2), z.literal(3)])),
120
- align: o(z.enum(["left", "center", "right"])),
121
- }),
122
- slots: [],
123
- description: "Heading text, level 1 (largest) to 3.",
124
- example: { text: "Welcome back", level: 1 },
125
- },
126
- Text: {
127
- props: z.object({
128
- text: o(z.string()),
129
- lines: o(z.number().int().min(1)).describe("Draw N placeholder lines instead of real text"),
130
- size: o(z.enum(["sm", "md", "lg"])),
131
- muted: o(z.boolean()),
132
- bold: o(z.boolean()),
133
- align: o(z.enum(["left", "center", "right"])),
134
- }),
135
- slots: [],
136
- description: "Body text, or placeholder lines when `lines` is set and `text` is not.",
137
- example: { lines: 3 },
138
- },
139
- Image: {
140
- props: z.object({
141
- height: o(z.number()),
142
- width: o(z.union([z.number(), z.string()])).describe("Default fills the container width"),
143
- label: o(z.string()),
144
- rounded: o(z.boolean()),
145
- }),
146
- slots: [],
147
- description: "Image placeholder: a box with an X through it.",
148
- example: { height: 180, label: "Hero photo" },
149
- },
150
- Icon: {
151
- props: z.object({
152
- name: o(iconName),
153
- size: o(z.number()),
154
- }),
155
- slots: [],
156
- description: "Line icon from Lucide.",
157
- example: { name: "search", size: 20 },
158
- },
159
- Avatar: {
160
- props: z.object({
161
- initials: o(z.string()),
162
- size: o(z.number()),
163
- }),
164
- slots: [],
165
- description: "Round avatar with initials or a person silhouette.",
166
- example: { initials: "JD", size: 40 },
167
- },
168
- Badge: {
169
- props: z.object({
170
- label: z.string(),
171
- variant: o(z.enum(["solid", "outline"])),
172
- tone: o(z.enum(["neutral", "success", "warning", "danger"])).describe("Status color. Default neutral (the accent for solid badges)"),
173
- }),
174
- slots: [],
175
- description: "Small pill label for counts or statuses.",
176
- example: { label: "New", variant: "solid" },
177
- },
178
-
179
- // ── Controls ──────────────────────────────────────────────────────────
180
- Button: {
181
- props: z.object({
182
- label: o(z.string()),
183
- variant: o(z.enum(["primary", "secondary", "ghost"])),
184
- size: o(z.enum(["sm", "md", "lg"])),
185
- icon: o(iconName),
186
- fullWidth: o(z.boolean()),
187
- }),
188
- slots: [],
189
- description: "Button. Primary is filled, secondary is outlined, ghost is text only.",
190
- example: { label: "Continue", variant: "primary", fullWidth: true },
191
- },
192
- Input: {
193
- props: z.object({
194
- label: o(z.string()),
195
- placeholder: o(z.string()),
196
- value: o(z.string()),
197
- type: o(z.enum(["text", "password", "search", "email"])),
198
- multiline: o(z.number().int().min(1)).describe("Number of rows; 2 or more makes a textarea"),
199
- helper: o(z.string()),
200
- error: o(z.boolean()).describe("Validation error: red border and red helper text"),
201
- grow: o(z.boolean()).describe("Fill the remaining space in a row"),
202
- width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 320. Default fills the space."),
203
- }),
204
- slots: [],
205
- description: "Text field with optional label. Set multiline for a textarea.",
206
- example: { label: "Email", placeholder: "you@example.com" },
207
- },
208
- Checkbox: {
209
- props: z.object({ label: o(z.string()), checked: o(z.boolean()) }),
210
- slots: [],
211
- description: "Checkbox with label.",
212
- example: { label: "Remember me", checked: true },
213
- },
214
- Radio: {
215
- props: z.object({ label: o(z.string()), checked: o(z.boolean()) }),
216
- slots: [],
217
- description: "Radio button with label.",
218
- example: { label: "Monthly", checked: true },
219
- },
220
- Toggle: {
221
- props: z.object({ label: o(z.string()), on: o(z.boolean()) }),
222
- slots: [],
223
- description: "On/off switch with label on the left.",
224
- example: { label: "Notifications", on: true },
225
- },
226
- Select: {
227
- props: z.object({
228
- label: o(z.string()),
229
- value: o(z.string()),
230
- placeholder: o(z.string()),
231
- width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 320. Default fills the space."),
232
- }),
233
- slots: [],
234
- description: "Dropdown field (closed state).",
235
- example: { label: "Country", value: "United States" },
236
- },
237
-
238
- // ── Navigation & data ─────────────────────────────────────────────────
239
- NavBar: {
240
- props: z.object({
241
- title: o(z.string()),
242
- leading: o(z.enum(["none", "menu", "back", "close", "logo"])),
243
- actions: o(z.array(iconName)).describe("Icon names shown on the right"),
244
- align: o(z.enum(["left", "center"])),
245
- }),
246
- slots: [],
247
- description: "Top app bar. Place first in a Screen.",
248
- example: { title: "Inbox", leading: "menu", actions: ["search", "more-vertical"] },
249
- },
250
- TabBar: {
251
- props: z.object({
252
- items: z.array(z.object({ label: z.string(), icon: o(iconName) })),
253
- active: o(z.number().int()),
254
- }),
255
- slots: [],
256
- description: "Bottom tab bar for mobile. Place last in a Screen (before overlays).",
257
- example: { items: [{ label: "Home", icon: "home" }, { label: "Profile", icon: "user" }], active: 0 },
258
- },
259
- Tabs: {
260
- props: z.object({
261
- items: z.array(z.string()),
262
- active: o(z.number().int()),
263
- }),
264
- slots: [],
265
- description: "In-page tabs with an underline on the active one.",
266
- example: { items: ["Overview", "Activity", "Settings"], active: 0 },
267
- },
268
- List: {
269
- props: z.object({ dividers: o(z.boolean()), grow: o(z.boolean()) }),
270
- slots: ["default"],
271
- description: "Vertical list of ListItems.",
272
- example: { dividers: true },
273
- },
274
- ListItem: {
275
- props: z.object({
276
- title: o(z.string()),
277
- subtitle: o(z.string()),
278
- leading: o(z.enum(["none", "icon", "avatar", "image", "checkbox"])),
279
- icon: o(iconName),
280
- trailing: o(z.enum(["none", "chevron", "toggle", "text", "badge", "icon"])),
281
- trailingText: o(z.string()),
282
- trailingIcon: o(iconName),
283
- }),
284
- slots: [],
285
- description: "Row in a List. Omit title for a placeholder bar.",
286
- example: { title: "Account", subtitle: "Email, password", leading: "icon", icon: "user", trailing: "chevron" },
287
- },
288
- Table: {
289
- props: z.object({
290
- columns: z.array(z.string()),
291
- rows: o(z.number().int()).describe("Placeholder rows when `data` is not given"),
292
- data: o(z.array(z.array(z.string()))),
293
- }),
294
- slots: [],
295
- description: "Table with headers. Rows are placeholder bars unless `data` is given.",
296
- example: { columns: ["Name", "Status", "Date"], rows: 5 },
297
- },
298
-
299
- // ── Overlays ──────────────────────────────────────────────────────────
300
- Modal: {
301
- props: z.object({
302
- title: o(z.string()),
303
- width: o(z.number()),
304
- scrim: o(z.boolean()).describe("Dim the screen behind. Default true."),
305
- }),
306
- slots: ["default"],
307
- description: "Centered dialog over the screen. Must be a direct child of a Screen, listed last.",
308
- example: { title: "Delete note?", width: 320 },
309
- },
310
- Drawer: {
311
- props: z.object({
312
- side: o(z.enum(["left", "right", "bottom"])),
313
- title: o(z.string()),
314
- size: o(z.number()).describe("Width for left/right, height for bottom"),
315
- scrim: o(z.boolean()).describe("Dim the screen behind. Default true."),
316
- handle: o(z.boolean()).describe("Grab handle on bottom sheets. Default true for bottom."),
317
- }),
318
- slots: ["default"],
319
- description:
320
- "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.",
321
- example: { side: "left", title: "Menu", size: 300 },
322
- },
323
- };
324
-
325
- export const catalog = defineCatalog(wireframeSchema, {
326
- components: componentDefinitions,
327
- });
328
-
329
- export type ComponentName = keyof typeof componentDefinitions;
package/src/colors.ts DELETED
@@ -1,82 +0,0 @@
1
- /**
2
- * The only colors a wireframe can set: a board-level accent and a few fixed
3
- * status tones. Everything else stays grayscale.
4
- */
5
- import { theme } from "./layout";
6
- import { closeMatches } from "./suggest";
7
-
8
- /** Named accents. Each keeps white text above 4.5:1 contrast. */
9
- export const ACCENTS = {
10
- blue: "#2563eb",
11
- indigo: "#4f46e5",
12
- violet: "#7c3aed",
13
- pink: "#db2777",
14
- red: "#dc2626",
15
- orange: "#c2410c",
16
- green: "#15803d",
17
- teal: "#0f766e",
18
- } as const;
19
-
20
- /** Status tones for badges and input errors. They don't change with the accent. */
21
- export const TONES = {
22
- success: "#15803d",
23
- warning: "#b45309",
24
- danger: "#dc2626",
25
- } as const;
26
-
27
- const HEX = /^#[0-9a-fA-F]{6}$/;
28
-
29
- export const isAccent = (v: string) => v in ACCENTS || HEX.test(v);
30
-
31
- export function accentMessage(v: string) {
32
- const names = Object.keys(ACCENTS);
33
- const close = closeMatches(v, names);
34
- return `unknown accent "${v}"` + (close.length ? ` (did you mean ${close.join(", ")}?)` : "") +
35
- `; use one of ${names.join(", ")}, or a hex color like #1a73e8`;
36
- }
37
-
38
- function luminance(hex: string) {
39
- const [r, g, b] = [1, 3, 5]
40
- .map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
41
- .map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
42
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
43
- }
44
-
45
- function contrast(a: string, b: string) {
46
- const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
47
- return (hi + 0.05) / (lo + 0.05);
48
- }
49
-
50
- /** Mix a color toward black until it reads on white (for text, underlines, icons). */
51
- function readableOnWhite(hex: string) {
52
- let [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
53
- let out = hex;
54
- for (let k = 0; k < 20 && contrast(out, "#ffffff") < 4.5; k++) {
55
- [r, g, b] = [r, g, b].map((v) => Math.round(v * 0.9));
56
- out = "#" + [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
57
- }
58
- return out;
59
- }
60
-
61
- export interface Palette {
62
- /** Fills for primary buttons, solid badges, checked controls, toggles that are on. */
63
- accent: string;
64
- /** Text and icons drawn on the accent fill. */
65
- onAccent: string;
66
- /** Accent as text or a line on white: active tabs, the active tab-bar item, ghost buttons. Dark gray without an accent. */
67
- accentText: string;
68
- }
69
-
70
- /** Colors for one board. Without an accent this is exactly the grayscale theme. */
71
- export function paletteFor(accent?: string | null): Palette {
72
- if (!accent || !isAccent(accent)) {
73
- return { accent: theme.primary, onAccent: theme.onPrimary, accentText: theme.ink };
74
- }
75
- const hex = (ACCENTS as Record<string, string>)[accent] ?? accent.toLowerCase();
76
- return {
77
- accent: hex,
78
- // hex accents can be light (a brand yellow): switch to dark text when white wouldn't read
79
- onAccent: contrast(hex, "#ffffff") >= 3 ? "#ffffff" : theme.ink,
80
- accentText: readableOnWhite(hex),
81
- };
82
- }
package/src/compile.ts DELETED
@@ -1,64 +0,0 @@
1
- import type { Spec } from "@json-render/core";
2
- import { checkSpec, renderWireframePng, renderWireframeSvg } from "./render";
3
- import { parseWireframeText, type TextIssue } from "./text";
4
-
5
- export interface CompileResult {
6
- /** The compiled spec, or null if the text couldn't be parsed at all. */
7
- spec: Spec | null;
8
- /** Every problem, tied to a source line. Empty means the wireframe is valid. */
9
- issues: TextIssue[];
10
- }
11
-
12
- /**
13
- * Parse wireframe text and validate it against the catalog, reporting every
14
- * problem by line number so it can be shown to a person or fed back to a model.
15
- */
16
- export function compileWireframe(source: string): CompileResult {
17
- const parsed = parseWireframeText(source);
18
- const issues: TextIssue[] = [...parsed.issues];
19
-
20
- if (parsed.spec) {
21
- for (const msg of checkSpec(parsed.spec)) {
22
- // checkSpec reports "<element id>[.props…]: message"; map ids back to lines
23
- const m = msg.match(/^([\w-]+)(?:\.props[^:]*)?: (.*)$/);
24
- const line = m ? parsed.lines[m[1]] : undefined;
25
- const text = m && line ? (m[2].startsWith(`${parsed.spec.elements[m[1]].type} `) ? m[2] : `${parsed.spec.elements[m[1]].type}: ${m[2]}`) : msg;
26
- // "Stack "stack-4"" → "the Stack on line 4"
27
- const readable = text.replace(/(\w+) "([\w-]+)"/g, (all, type, id) =>
28
- parsed.lines[id] ? `the ${type} on line ${parsed.lines[id]}` : all,
29
- );
30
- issues.push({ line: line ?? 1, message: readable });
31
- }
32
- }
33
-
34
- // The parser and the validator can both flag the same unknown prop.
35
- const seen = new Set<string>();
36
- const unique = issues.filter((i) => {
37
- const key = `${i.line}|${i.message.replace(/^\w+: /, "").replace(/ \(.*$/, "")}`;
38
- if (seen.has(key)) return false;
39
- seen.add(key);
40
- return true;
41
- });
42
- unique.sort((a, b) => a.line - b.line);
43
- return { spec: parsed.spec, issues: unique };
44
- }
45
-
46
- /** One issue per line, e.g. for an error message or a model repair prompt. */
47
- export function formatIssues(issues: TextIssue[]) {
48
- return issues.map((i) => `line ${i.line}: ${i.message}`).join("\n");
49
- }
50
-
51
- export class WireframeError extends Error {
52
- constructor(public issues: TextIssue[]) {
53
- super(`Invalid wireframe:\n${formatIssues(issues)}`);
54
- }
55
- }
56
-
57
- /** Compile and render wireframe text. Throws WireframeError (with line-numbered issues) if it's invalid. */
58
- export async function renderWireframe(source: string, opts: { format?: "svg" | "png"; scale?: number } = {}) {
59
- const { spec, issues } = compileWireframe(source);
60
- if (!spec || issues.length) throw new WireframeError(issues);
61
- return opts.format === "png"
62
- ? renderWireframePng(spec, { skipValidation: true, scale: opts.scale })
63
- : renderWireframeSvg(spec, { skipValidation: true });
64
- }