tsquare 0.2.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 +20 -6
- package/dist/catalog.d.ts +42 -8
- package/dist/chunks/{chunk-L6GTIZQH.js → chunk-46MGJLGB.js} +256 -159
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +17 -1
- package/dist/playground/index.d.ts +12 -1
- package/dist/playground/index.html +180 -356
- package/dist/playground/index.js +100 -10
- package/dist/playground/language-data.d.ts +28 -0
- package/dist/playground/playground.js +47 -0
- package/dist/prompt.d.ts +1 -1
- package/dist/render.d.ts +21 -1
- package/dist/share.d.ts +4 -0
- package/dist/text.d.ts +6 -1
- package/examples/checkout.tsq +15 -0
- package/examples/dashboard.tsq +27 -0
- package/examples/notes-mobile.tsq +9 -9
- package/examples/notes-web.tsq +6 -6
- package/examples/sign-in.tsq +14 -0
- package/examples/states.tsq +16 -0
- package/package.json +9 -1
- package/skill/tsquare/SKILL.md +3 -2
- package/skill/tsquare/reference.md +5 -4
|
@@ -368,7 +368,8 @@ var componentDefinitions = {
|
|
|
368
368
|
label: o(z.string()),
|
|
369
369
|
variant: o(z.enum(["primary", "secondary", "ghost"])),
|
|
370
370
|
size: o(z.enum(["sm", "md", "lg"])),
|
|
371
|
-
|
|
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"),
|
|
372
373
|
fullWidth: o(z.boolean())
|
|
373
374
|
}),
|
|
374
375
|
slots: [],
|
|
@@ -461,14 +462,14 @@ var componentDefinitions = {
|
|
|
461
462
|
title: o(z.string()),
|
|
462
463
|
subtitle: o(z.string()),
|
|
463
464
|
leading: o(z.enum(["none", "icon", "avatar", "image", "checkbox"])),
|
|
464
|
-
|
|
465
|
+
leadingIcon: o(iconName).describe("Lucide icon on the left; setting it implies leading=icon"),
|
|
465
466
|
trailing: o(z.enum(["none", "chevron", "toggle", "text", "badge", "icon"])),
|
|
466
|
-
trailingText: o(z.string()),
|
|
467
|
-
trailingIcon: o(iconName)
|
|
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")
|
|
468
469
|
}),
|
|
469
470
|
slots: [],
|
|
470
471
|
description: "Row in a List. Omit title for a placeholder bar.",
|
|
471
|
-
example: { title: "Account", subtitle: "Email, password",
|
|
472
|
+
example: { title: "Account", subtitle: "Email, password", leadingIcon: "user", trailing: "chevron" }
|
|
472
473
|
},
|
|
473
474
|
Table: {
|
|
474
475
|
props: z.object({
|
|
@@ -507,6 +508,29 @@ var componentDefinitions = {
|
|
|
507
508
|
var catalog = defineCatalog(wireframeSchema, {
|
|
508
509
|
components: componentDefinitions
|
|
509
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
|
+
}
|
|
510
534
|
|
|
511
535
|
// src/components.tsx
|
|
512
536
|
import React, { Children, cloneElement, isValidElement } from "react";
|
|
@@ -978,8 +1002,9 @@ function Button({ element, dir, aligned, colors: c = GRAY }) {
|
|
|
978
1002
|
...p.fullWidth ? { alignSelf: "stretch" } : hug(dir, aligned)
|
|
979
1003
|
},
|
|
980
1004
|
children: [
|
|
981
|
-
p.
|
|
982
|
-
p.label ? /* @__PURE__ */ jsx(Box, { children: p.label }) : null
|
|
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
|
|
983
1008
|
]
|
|
984
1009
|
}
|
|
985
1010
|
);
|
|
@@ -1182,10 +1207,11 @@ function List({ element, children, dir, stretch }) {
|
|
|
1182
1207
|
}
|
|
1183
1208
|
function ListItem({ element, colors }) {
|
|
1184
1209
|
const p = element.props;
|
|
1210
|
+
const ends = listItemEnds(p);
|
|
1185
1211
|
const leading = (() => {
|
|
1186
|
-
switch (
|
|
1212
|
+
switch (ends.leading) {
|
|
1187
1213
|
case "icon":
|
|
1188
|
-
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.
|
|
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 }) });
|
|
1189
1215
|
case "avatar":
|
|
1190
1216
|
return /* @__PURE__ */ jsx(AvatarCircle, { size: 40 });
|
|
1191
1217
|
case "image":
|
|
@@ -1197,7 +1223,7 @@ function ListItem({ element, colors }) {
|
|
|
1197
1223
|
}
|
|
1198
1224
|
})();
|
|
1199
1225
|
const trailing = (() => {
|
|
1200
|
-
switch (
|
|
1226
|
+
switch (ends.trailing) {
|
|
1201
1227
|
case "chevron":
|
|
1202
1228
|
return /* @__PURE__ */ jsx(IconGlyph, { name: "chevron-right", size: 20, color: theme.muted });
|
|
1203
1229
|
case "toggle":
|
|
@@ -1346,145 +1372,6 @@ var registry = {
|
|
|
1346
1372
|
Drawer
|
|
1347
1373
|
};
|
|
1348
1374
|
|
|
1349
|
-
// src/render.ts
|
|
1350
|
-
import { readFile } from "node:fs/promises";
|
|
1351
|
-
import { createRequire } from "node:module";
|
|
1352
|
-
import { renderToSvg } from "@json-render/image/render";
|
|
1353
|
-
var fontFile = (name) => createRequire(import.meta.url).resolve(`@fontsource/inter/files/${name}`);
|
|
1354
|
-
var fontCache = null;
|
|
1355
|
-
function loadFonts() {
|
|
1356
|
-
fontCache ??= Promise.all([
|
|
1357
|
-
readFile(fontFile("inter-latin-400-normal.woff")),
|
|
1358
|
-
readFile(fontFile("inter-latin-600-normal.woff"))
|
|
1359
|
-
]).then(([regular, semibold]) => [
|
|
1360
|
-
{ name: "Inter", data: regular, weight: 400, style: "normal" },
|
|
1361
|
-
{ name: "Inter", data: semibold, weight: 600, style: "normal" }
|
|
1362
|
-
]);
|
|
1363
|
-
return fontCache;
|
|
1364
|
-
}
|
|
1365
|
-
var SpecError = class extends Error {
|
|
1366
|
-
constructor(issues) {
|
|
1367
|
-
super(`Invalid wireframe spec:
|
|
1368
|
-
- ${issues.join("\n - ")}`);
|
|
1369
|
-
this.issues = issues;
|
|
1370
|
-
}
|
|
1371
|
-
issues;
|
|
1372
|
-
};
|
|
1373
|
-
function checkSpec(spec) {
|
|
1374
|
-
const issues = [];
|
|
1375
|
-
const result = catalog.validate(spec);
|
|
1376
|
-
if (!result.success) {
|
|
1377
|
-
const err = result.error;
|
|
1378
|
-
const zodIssues = err?.issues ?? [];
|
|
1379
|
-
if (zodIssues.length) {
|
|
1380
|
-
for (const i of zodIssues) issues.push(`${(i.path ?? []).join(".")}: ${i.message}`);
|
|
1381
|
-
} else {
|
|
1382
|
-
issues.push(String(err ?? "catalog validation failed"));
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
const root = spec.elements?.[spec.root];
|
|
1386
|
-
if (!root) {
|
|
1387
|
-
issues.push(`root "${spec.root}" is not in elements`);
|
|
1388
|
-
return issues;
|
|
1389
|
-
}
|
|
1390
|
-
if (root.type !== "Board") issues.push(`root must be a Board (got ${root.type})`);
|
|
1391
|
-
for (const [id, el] of Object.entries(spec.elements)) {
|
|
1392
|
-
const def = componentDefinitions[el.type];
|
|
1393
|
-
if (!def) {
|
|
1394
|
-
issues.push(`${id}: ${unknownComponentMessage(el.type, Object.keys(componentDefinitions))}`);
|
|
1395
|
-
} else {
|
|
1396
|
-
const parsed = def.props.safeParse(el.props ?? {});
|
|
1397
|
-
if (!parsed.success) {
|
|
1398
|
-
for (const i of parsed.error.issues) {
|
|
1399
|
-
issues.push(`${id}.props${i.path.length ? "." + i.path.join(".") : ""}: ${i.message}`);
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
for (const key of Object.keys(el.props ?? {})) {
|
|
1403
|
-
if (!(key in def.props.shape)) issues.push(`${id}.props: ${el.type} has no prop "${key}"`);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
for (const child of el.children ?? []) {
|
|
1407
|
-
if (!spec.elements[child]) issues.push(`${id}: child "${child}" does not exist`);
|
|
1408
|
-
}
|
|
1409
|
-
if (el.type === "Screen" && !(root.children ?? []).includes(id)) {
|
|
1410
|
-
issues.push(`${id}: Screen must be a direct child of the Board`);
|
|
1411
|
-
}
|
|
1412
|
-
if (el.type === "Modal" || el.type === "Drawer") {
|
|
1413
|
-
const parent = Object.entries(spec.elements).find(([, p]) => p.children?.includes(id));
|
|
1414
|
-
if (parent && parent[1].type !== "Screen") {
|
|
1415
|
-
issues.push(`${id}: ${el.type} must be a direct child of a Screen (found in ${parent[1].type} "${parent[0]}")`);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
if (el.type === "Table") {
|
|
1419
|
-
const { columns, data } = el.props ?? {};
|
|
1420
|
-
if (Array.isArray(columns) && Array.isArray(data)) {
|
|
1421
|
-
const bad = data.map((row, i) => ({ row: i + 1, cells: Array.isArray(row) ? row.length : 1 })).filter((r) => r.cells !== columns.length);
|
|
1422
|
-
if (bad.length) {
|
|
1423
|
-
const cells = (n) => `${n} cell${n === 1 ? "" : "s"}`;
|
|
1424
|
-
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`;
|
|
1425
|
-
const hints = [
|
|
1426
|
-
bad.some((r) => r.cells > columns.length) && `quote cells that contain a comma, e.g. ["$1,200", Paid]`,
|
|
1427
|
-
bad.some((r) => r.cells < columns.length) && `use "" for an empty cell`
|
|
1428
|
-
].filter(Boolean);
|
|
1429
|
-
issues.push(`${id}: Table has ${columns.length} columns, but ${rows} (${hints.join("; ")})`);
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
return issues;
|
|
1435
|
-
}
|
|
1436
|
-
function boardSize(spec) {
|
|
1437
|
-
const board = spec.elements[spec.root];
|
|
1438
|
-
const p = board.props ?? {};
|
|
1439
|
-
const gap = p.gap ?? BOARD_GAP;
|
|
1440
|
-
const pad = p.padding ?? BOARD_PADDING;
|
|
1441
|
-
const kids = (board.children ?? []).map((id) => spec.elements[id]).filter(Boolean);
|
|
1442
|
-
const boxes = kids.map((el) => {
|
|
1443
|
-
const props = el.props ?? {};
|
|
1444
|
-
if (el.type === "Screen") {
|
|
1445
|
-
const s = screenSize(props);
|
|
1446
|
-
return { w: s.width, h: s.height + LABEL_H };
|
|
1447
|
-
}
|
|
1448
|
-
if (el.type === "Note") {
|
|
1449
|
-
const w = props.width ?? NOTE_WIDTH;
|
|
1450
|
-
return { w, h: LABEL_H + estimateNoteHeight(props.text ?? "", w) };
|
|
1451
|
-
}
|
|
1452
|
-
return { w: 0, h: 0 };
|
|
1453
|
-
});
|
|
1454
|
-
const perRow = p.layout === "grid" ? Math.max(1, p.columns ?? 3) : Math.max(1, boxes.length);
|
|
1455
|
-
let width = 0;
|
|
1456
|
-
let height = 0;
|
|
1457
|
-
for (let i = 0; i < boxes.length; i += perRow) {
|
|
1458
|
-
const row = boxes.slice(i, i + perRow);
|
|
1459
|
-
width = Math.max(width, row.reduce((sum, b) => sum + b.w, 0) + gap * (row.length - 1));
|
|
1460
|
-
height += Math.max(...row.map((b) => b.h)) + (i > 0 ? gap : 0);
|
|
1461
|
-
}
|
|
1462
|
-
return {
|
|
1463
|
-
width: Math.ceil(width + pad * 2),
|
|
1464
|
-
height: Math.ceil(height + pad * 2 + (p.title ? TITLE_H : 0))
|
|
1465
|
-
};
|
|
1466
|
-
}
|
|
1467
|
-
async function renderWireframeSvg(spec, opts = {}) {
|
|
1468
|
-
if (!opts.skipValidation) {
|
|
1469
|
-
const issues = checkSpec(spec);
|
|
1470
|
-
if (issues.length) throw new SpecError(issues);
|
|
1471
|
-
}
|
|
1472
|
-
const { width, height } = boardSize(spec);
|
|
1473
|
-
return renderToSvg(spec, {
|
|
1474
|
-
registry: withPalette(paletteFor(spec.elements[spec.root]?.props?.accent)),
|
|
1475
|
-
includeStandard: false,
|
|
1476
|
-
fonts: await loadFonts(),
|
|
1477
|
-
width,
|
|
1478
|
-
height
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
async function renderWireframePng(spec, opts = {}) {
|
|
1482
|
-
const svg = await renderWireframeSvg(spec, opts);
|
|
1483
|
-
const { Resvg } = await import("@resvg/resvg-js");
|
|
1484
|
-
const resvg = new Resvg(svg, { fitTo: { mode: "zoom", value: opts.scale ?? 1 } });
|
|
1485
|
-
return resvg.render().asPng();
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
1375
|
// src/text.ts
|
|
1489
1376
|
var PRIMARY_PROP = {
|
|
1490
1377
|
Board: "title",
|
|
@@ -1548,6 +1435,34 @@ for (const [name, def] of Object.entries(componentDefinitions)) {
|
|
|
1548
1435
|
for (const v of ambiguous) enumValues.delete(v);
|
|
1549
1436
|
COMPONENTS.set(name.toLowerCase(), { name, props: shape, enumValues, ambiguous, booleans: booleans2 });
|
|
1550
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
|
+
}
|
|
1551
1466
|
var normalizeType = (s) => s.toLowerCase().replace(/[-_]/g, "");
|
|
1552
1467
|
var WORD_PRIMARY = /* @__PURE__ */ new Set(["Icon", "Avatar"]);
|
|
1553
1468
|
var ANTONYMS = { off: "on", unchecked: "checked" };
|
|
@@ -1560,7 +1475,6 @@ function tokenize(src) {
|
|
|
1560
1475
|
i++;
|
|
1561
1476
|
continue;
|
|
1562
1477
|
}
|
|
1563
|
-
if (c === "#" && !(i > 0 && "=:".includes(src[i - 1]))) break;
|
|
1564
1478
|
if (c === '"' || c === "'") {
|
|
1565
1479
|
let j2 = i + 1, s = "";
|
|
1566
1480
|
while (j2 < src.length && src[j2] !== c) {
|
|
@@ -1581,8 +1495,8 @@ function tokenize(src) {
|
|
|
1581
1495
|
i++;
|
|
1582
1496
|
continue;
|
|
1583
1497
|
}
|
|
1584
|
-
let j =
|
|
1585
|
-
while (j < src.length && !` ,=[]{}
|
|
1498
|
+
let j = i;
|
|
1499
|
+
while (j < src.length && !` ,=[]{}":`.includes(src[j])) j++;
|
|
1586
1500
|
const w = src.slice(i, j);
|
|
1587
1501
|
out.push({ kind: /^-?\d+(\.\d+)?$/.test(w) ? "num" : "word", value: w, start: i, end: j });
|
|
1588
1502
|
i = j;
|
|
@@ -1677,17 +1591,29 @@ function parseWireframeText(source) {
|
|
|
1677
1591
|
const stack = [];
|
|
1678
1592
|
let root = null;
|
|
1679
1593
|
let n = 0;
|
|
1594
|
+
let lastElementLine = 0;
|
|
1680
1595
|
const srcLines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
1681
1596
|
srcLines.forEach((raw, idx) => {
|
|
1682
1597
|
const lineNo = idx + 1;
|
|
1683
1598
|
const trimmed = raw.trim();
|
|
1684
1599
|
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("```")) return;
|
|
1685
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
|
+
}
|
|
1686
1611
|
const m = trimmed.match(/^([A-Za-z][\w-]*)(.*)$/);
|
|
1687
1612
|
if (!m) {
|
|
1688
1613
|
issues.push({ line: lineNo, message: `expected a component name, got "${trimmed}"` });
|
|
1689
1614
|
return;
|
|
1690
1615
|
}
|
|
1616
|
+
lastElementLine = lineNo;
|
|
1691
1617
|
const info = COMPONENTS.get(normalizeType(m[1]));
|
|
1692
1618
|
if (!info) {
|
|
1693
1619
|
issues.push({ line: lineNo, message: unknownComponentMessage(m[1], [...COMPONENTS.keys()]) });
|
|
@@ -1703,9 +1629,10 @@ function parseWireframeText(source) {
|
|
|
1703
1629
|
if (t.kind !== "sym" && (c.isSym("=", 1) || c.isSym(":", 1))) {
|
|
1704
1630
|
c.next();
|
|
1705
1631
|
c.next();
|
|
1706
|
-
const key = t.value;
|
|
1632
|
+
const key = RENAMED_PROPS[info.name]?.[t.value] ?? t.value;
|
|
1707
1633
|
if (!(key in info.props)) {
|
|
1708
|
-
|
|
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(", ")})` });
|
|
1709
1636
|
}
|
|
1710
1637
|
props[key] = key in info.props ? textWhereExpected(info.props[key], parseValue(c)) : parseValue(c);
|
|
1711
1638
|
continue;
|
|
@@ -1726,11 +1653,15 @@ function parseWireframeText(source) {
|
|
|
1726
1653
|
props[ANTONYMS[t.value]] = false;
|
|
1727
1654
|
} else if (t.kind === "word" && WORD_PRIMARY.has(info.name) && !(PRIMARY_PROP[info.name] in props)) {
|
|
1728
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;
|
|
1729
1659
|
} else {
|
|
1730
1660
|
const options = [...info.enumValues.keys(), ...info.booleans];
|
|
1661
|
+
const elsewhere = t.kind === "word" ? belongsElsewhere(info.name, t.value, false) : "";
|
|
1731
1662
|
issues.push({
|
|
1732
1663
|
line: lineNo,
|
|
1733
|
-
message: `${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}"` : "")
|
|
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}"` : "")
|
|
1734
1665
|
});
|
|
1735
1666
|
}
|
|
1736
1667
|
}
|
|
@@ -1756,6 +1687,166 @@ function parseWireframeText(source) {
|
|
|
1756
1687
|
return { spec: root ? { root, elements } : null, issues, lines };
|
|
1757
1688
|
}
|
|
1758
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
|
+
|
|
1759
1850
|
// src/compile.ts
|
|
1760
1851
|
function compileWireframe(source) {
|
|
1761
1852
|
const parsed = parseWireframeText(source);
|
|
@@ -1774,7 +1865,7 @@ function compileWireframe(source) {
|
|
|
1774
1865
|
}
|
|
1775
1866
|
const seen = /* @__PURE__ */ new Set();
|
|
1776
1867
|
const unique = issues.filter((i) => {
|
|
1777
|
-
const key = `${i.line}|${i.message.replace(/^\w+: /, "").replace(/ \(.*$/, "")}`;
|
|
1868
|
+
const key = `${i.line}|${i.message.replace(/^\w+: /, "").replace(/(has no prop "[^"]*").*$/, "$1").replace(/ \(.*$/, "")}`;
|
|
1778
1869
|
if (seen.has(key)) return false;
|
|
1779
1870
|
seen.add(key);
|
|
1780
1871
|
return true;
|
|
@@ -1971,9 +2062,10 @@ One element per line. Indent children two spaces under their parent. The first l
|
|
|
1971
2062
|
A line is the component name in lowercase, followed by arguments separated by spaces:
|
|
1972
2063
|
- "a quoted string" sets the component's main text prop (marked "main text" below)
|
|
1973
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
|
|
1974
|
-
- a bare prop name sets
|
|
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
|
|
1975
2066
|
- key=value sets any prop. Values: "string", number, true/false, bare word, [list, of, values], {key=value key=value}
|
|
1976
|
-
|
|
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.
|
|
1977
2069
|
|
|
1978
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].
|
|
1979
2071
|
|
|
@@ -2010,14 +2102,19 @@ Reply with the corrected wireframe in a single code block.`;
|
|
|
2010
2102
|
export {
|
|
2011
2103
|
theme,
|
|
2012
2104
|
DEVICES,
|
|
2105
|
+
ACCENTS,
|
|
2106
|
+
iconNames,
|
|
2013
2107
|
componentDefinitions,
|
|
2014
2108
|
catalog,
|
|
2015
2109
|
registry,
|
|
2110
|
+
PRIMARY_PROP,
|
|
2111
|
+
bareWords,
|
|
2112
|
+
parseWireframeText,
|
|
2016
2113
|
checkSpec,
|
|
2114
|
+
boardLayout,
|
|
2017
2115
|
boardSize,
|
|
2018
2116
|
renderWireframeSvg,
|
|
2019
2117
|
renderWireframePng,
|
|
2020
|
-
parseWireframeText,
|
|
2021
2118
|
compileWireframe,
|
|
2022
2119
|
formatIssues,
|
|
2023
2120
|
WireframeError,
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
export { renderWireframe, compileWireframe, formatIssues, WireframeError, type CompileResult } from "./compile.js";
|
|
3
3
|
export { wireframePrompt, repairPrompt } from "./prompt.js";
|
|
4
4
|
export { printWireframeText as formatWireframe } from "./print.js";
|
|
5
|
-
export {
|
|
5
|
+
export { encodeWireframe, decodeWireframe, MAX_SHARED_TEXT } from "./share.js";
|
|
6
|
+
export { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, boardLayout, type BoardItem, type RenderWireframeOptions } from "./render.js";
|
|
6
7
|
export { parseWireframeText, type TextIssue, type ParseResult } from "./text.js";
|
|
7
8
|
export { catalog, componentDefinitions } from "./catalog.js";
|
|
8
9
|
export { registry } from "./components.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEVICES,
|
|
3
3
|
WireframeError,
|
|
4
|
+
boardLayout,
|
|
4
5
|
boardSize,
|
|
5
6
|
catalog,
|
|
6
7
|
checkSpec,
|
|
@@ -16,15 +17,30 @@ import {
|
|
|
16
17
|
repairPrompt,
|
|
17
18
|
theme,
|
|
18
19
|
wireframePrompt
|
|
19
|
-
} from "./chunks/chunk-
|
|
20
|
+
} from "./chunks/chunk-46MGJLGB.js";
|
|
21
|
+
|
|
22
|
+
// src/share.ts
|
|
23
|
+
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
24
|
+
var MAX_SHARED_TEXT = 64e3;
|
|
25
|
+
function encodeWireframe(text) {
|
|
26
|
+
return "z" + deflateRawSync(Buffer.from(text, "utf8"), { level: 9 }).toString("base64url");
|
|
27
|
+
}
|
|
28
|
+
function decodeWireframe(data) {
|
|
29
|
+
if (!data.startsWith("z")) throw new Error(`unknown encoding "${data.slice(0, 1)}" (expected a "z" prefix)`);
|
|
30
|
+
return inflateRawSync(Buffer.from(data.slice(1), "base64url"), { maxOutputLength: MAX_SHARED_TEXT }).toString("utf8");
|
|
31
|
+
}
|
|
20
32
|
export {
|
|
21
33
|
DEVICES,
|
|
34
|
+
MAX_SHARED_TEXT,
|
|
22
35
|
WireframeError,
|
|
36
|
+
boardLayout,
|
|
23
37
|
boardSize,
|
|
24
38
|
catalog,
|
|
25
39
|
checkSpec,
|
|
26
40
|
compileWireframe,
|
|
27
41
|
componentDefinitions,
|
|
42
|
+
decodeWireframe,
|
|
43
|
+
encodeWireframe,
|
|
28
44
|
formatIssues,
|
|
29
45
|
printWireframeText as formatWireframe,
|
|
30
46
|
parseWireframeText,
|
|
@@ -8,16 +8,27 @@
|
|
|
8
8
|
* `createPlaygroundHandler`:
|
|
9
9
|
*
|
|
10
10
|
* GET / the page
|
|
11
|
+
* GET /playground.js the page's script (CodeMirror editor and UI), bundled by the build
|
|
11
12
|
* GET /mark.png the logo mark (tab icon, header)
|
|
13
|
+
* GET /api/language what the editor needs to highlight and autocomplete
|
|
12
14
|
* GET /api/reference components, props and rendered examples
|
|
13
15
|
* GET /api/examples the example wireframes
|
|
14
16
|
* GET /api/prompt the model prompt
|
|
15
|
-
* POST /api/render wireframe text → { issues, svg? }
|
|
17
|
+
* POST /api/render wireframe text → { issues, svg?, width?, height?, items? } (items: where each screen sits)
|
|
16
18
|
* POST /api/png?scale=2 wireframe text → image/png
|
|
17
19
|
*/
|
|
18
20
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
19
21
|
/** Problems with the reference examples: a component without an example or group, or an example that doesn't compile. */
|
|
20
22
|
export declare function checkReferenceExamples(): string[];
|
|
23
|
+
/** esbuild options for the browser bundle; shared with scripts/build.ts. */
|
|
24
|
+
export declare const PLAYGROUND_BUNDLE: {
|
|
25
|
+
bundle: boolean;
|
|
26
|
+
format: "esm";
|
|
27
|
+
platform: "browser";
|
|
28
|
+
target: string;
|
|
29
|
+
minify: boolean;
|
|
30
|
+
legalComments: "eof";
|
|
31
|
+
};
|
|
21
32
|
/**
|
|
22
33
|
* Handles a playground request. Resolves to false when the path isn't a
|
|
23
34
|
* playground route, so a host server can serve its own routes around it.
|