tsquare 0.2.0 → 0.3.1

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.
@@ -139,8 +139,25 @@ function iconNode(name) {
139
139
  return table[pascal(name)];
140
140
  }
141
141
  var isIcon = (name) => iconNode(name) !== void 0;
142
- function suggestIcons(name, limit = 3) {
142
+ var GUESSES = {
143
+ call: ["phone"],
144
+ compose: ["square-pen", "pencil"],
145
+ person: ["user"],
146
+ bag: ["shopping-bag"],
147
+ notification: ["bell"],
148
+ notifications: ["bell"],
149
+ profile: ["user", "circle-user"],
150
+ envelope: ["mail"],
151
+ close: ["x"]
152
+ };
153
+ var DIRECTIONS = ["right", "left", "down", "up"];
154
+ var directionRank = (n) => {
155
+ const i = DIRECTIONS.indexOf(n.split("-").pop());
156
+ return i < 0 ? DIRECTIONS.length : i;
157
+ };
158
+ function suggestIcons(name, limit = 4) {
143
159
  const q = name.toLowerCase().replace(/[_ ]/g, "-");
160
+ const guessed = GUESSES[q] ?? [];
144
161
  const words = q.split("-").filter((w) => w.length > 2);
145
162
  const maxEdits = Math.max(1, Math.floor(q.length / 3));
146
163
  const scored = [];
@@ -152,7 +169,8 @@ function suggestIcons(name, limit = 3) {
152
169
  const score = Math.min(wordScore, d <= maxEdits ? 3 * d / q.length : Infinity);
153
170
  if (score < Infinity) scored.push([n, score]);
154
171
  }
155
- return scored.sort((a, b) => a[1] - b[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([n]) => n);
172
+ const close = scored.sort((a, b) => a[1] - b[1] || directionRank(a[0]) - directionRank(b[0]) || a[0].localeCompare(b[0])).map(([n]) => n);
173
+ return [.../* @__PURE__ */ new Set([...guessed, ...close])].slice(0, limit);
156
174
  }
157
175
  function unknownIconMessage(name) {
158
176
  const close = suggestIcons(name);
@@ -368,7 +386,8 @@ var componentDefinitions = {
368
386
  label: o(z.string()),
369
387
  variant: o(z.enum(["primary", "secondary", "ghost"])),
370
388
  size: o(z.enum(["sm", "md", "lg"])),
371
- icon: o(iconName),
389
+ leadingIcon: o(iconName).describe("Lucide icon before the label, e.g. chevron-left for Back"),
390
+ trailingIcon: o(iconName).describe("Lucide icon after the label, e.g. chevron-right for Next"),
372
391
  fullWidth: o(z.boolean())
373
392
  }),
374
393
  slots: [],
@@ -461,14 +480,14 @@ var componentDefinitions = {
461
480
  title: o(z.string()),
462
481
  subtitle: o(z.string()),
463
482
  leading: o(z.enum(["none", "icon", "avatar", "image", "checkbox"])),
464
- icon: o(iconName),
483
+ leadingIcon: o(iconName).describe("Lucide icon on the left; setting it implies leading=icon"),
465
484
  trailing: o(z.enum(["none", "chevron", "toggle", "text", "badge", "icon"])),
466
- trailingText: o(z.string()),
467
- trailingIcon: o(iconName)
485
+ trailingText: o(z.string()).describe("Text for trailing=text or badge; setting it implies trailing=text"),
486
+ trailingIcon: o(iconName).describe("Lucide icon on the right; setting it implies trailing=icon")
468
487
  }),
469
488
  slots: [],
470
489
  description: "Row in a List. Omit title for a placeholder bar.",
471
- example: { title: "Account", subtitle: "Email, password", leading: "icon", icon: "user", trailing: "chevron" }
490
+ example: { title: "Account", subtitle: "Email, password", leadingIcon: "user", trailing: "chevron" }
472
491
  },
473
492
  Table: {
474
493
  props: z.object({
@@ -507,6 +526,12 @@ var componentDefinitions = {
507
526
  var catalog = defineCatalog(wireframeSchema, {
508
527
  components: componentDefinitions
509
528
  });
529
+ function listItemEnds(p) {
530
+ return {
531
+ leading: p.leading ?? (p.leadingIcon ? "icon" : void 0),
532
+ trailing: p.trailing ?? (p.trailingIcon ? "icon" : p.trailingText ? "text" : void 0)
533
+ };
534
+ }
510
535
 
511
536
  // src/components.tsx
512
537
  import React, { Children, cloneElement, isValidElement } from "react";
@@ -978,8 +1003,9 @@ function Button({ element, dir, aligned, colors: c = GRAY }) {
978
1003
  ...p.fullWidth ? { alignSelf: "stretch" } : hug(dir, aligned)
979
1004
  },
980
1005
  children: [
981
- p.icon ? /* @__PURE__ */ jsx(IconGlyph, { name: p.icon, size: h < 36 ? 16 : 18, color: fg }) : null,
982
- p.label ? /* @__PURE__ */ jsx(Box, { children: p.label }) : null
1006
+ p.leadingIcon ? /* @__PURE__ */ jsx(IconGlyph, { name: p.leadingIcon, size: h < 36 ? 16 : 18, color: fg }) : null,
1007
+ p.label ? /* @__PURE__ */ jsx(Box, { children: p.label }) : null,
1008
+ p.trailingIcon ? /* @__PURE__ */ jsx(IconGlyph, { name: p.trailingIcon, size: h < 36 ? 16 : 18, color: fg }) : null
983
1009
  ]
984
1010
  }
985
1011
  );
@@ -1182,10 +1208,11 @@ function List({ element, children, dir, stretch }) {
1182
1208
  }
1183
1209
  function ListItem({ element, colors }) {
1184
1210
  const p = element.props;
1211
+ const ends = listItemEnds(p);
1185
1212
  const leading = (() => {
1186
- switch (p.leading) {
1213
+ switch (ends.leading) {
1187
1214
  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.icon, size: 20 }) });
1215
+ 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
1216
  case "avatar":
1190
1217
  return /* @__PURE__ */ jsx(AvatarCircle, { size: 40 });
1191
1218
  case "image":
@@ -1197,7 +1224,7 @@ function ListItem({ element, colors }) {
1197
1224
  }
1198
1225
  })();
1199
1226
  const trailing = (() => {
1200
- switch (p.trailing) {
1227
+ switch (ends.trailing) {
1201
1228
  case "chevron":
1202
1229
  return /* @__PURE__ */ jsx(IconGlyph, { name: "chevron-right", size: 20, color: theme.muted });
1203
1230
  case "toggle":
@@ -1346,145 +1373,6 @@ var registry = {
1346
1373
  Drawer
1347
1374
  };
1348
1375
 
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
1376
  // src/text.ts
1489
1377
  var PRIMARY_PROP = {
1490
1378
  Board: "title",
@@ -1548,6 +1436,34 @@ for (const [name, def] of Object.entries(componentDefinitions)) {
1548
1436
  for (const v of ambiguous) enumValues.delete(v);
1549
1437
  COMPONENTS.set(name.toLowerCase(), { name, props: shape, enumValues, ambiguous, booleans: booleans2 });
1550
1438
  }
1439
+ function belongsElsewhere(component, word, asProp) {
1440
+ const info = COMPONENTS.get(component.toLowerCase());
1441
+ if (!info) return "";
1442
+ const props = [], options = [];
1443
+ for (const other of COMPONENTS.values()) {
1444
+ if (other === info) continue;
1445
+ if (word in other.props && (asProp || other.booleans.has(word))) props.push(other.name);
1446
+ else if (!asProp && (other.enumValues.has(word) || other.ambiguous.has(word))) options.push(other.name);
1447
+ }
1448
+ const names = (xs) => xs.length > 3 ? `${xs.slice(0, 3).join(", ")}\u2026` : xs.join(", ").replace(/, ([^,]*)$/, " and $1");
1449
+ const a = (name) => /^[AEIOU]/.test(name) ? `an ${name}` : `a ${name}`;
1450
+ 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` : "";
1451
+ const width = info.props.width;
1452
+ if (msg && word === "fullWidth" && /fills the space/i.test(width?.description ?? unwrap(width)?.description ?? "")) {
1453
+ msg += `; ${a(info.name)} already fills its width (width=\u2026 sets a fixed one), so remove it`;
1454
+ }
1455
+ return msg;
1456
+ }
1457
+ function bareWords(component) {
1458
+ const info = COMPONENTS.get(component.toLowerCase());
1459
+ if (!info) throw new Error(`unknown component ${component}`);
1460
+ return {
1461
+ options: [...info.enumValues].map(([value, prop]) => ({ value, prop })),
1462
+ ambiguous: [...info.ambiguous],
1463
+ booleans: [...info.booleans],
1464
+ wordPrimary: WORD_PRIMARY.has(info.name)
1465
+ };
1466
+ }
1551
1467
  var normalizeType = (s) => s.toLowerCase().replace(/[-_]/g, "");
1552
1468
  var WORD_PRIMARY = /* @__PURE__ */ new Set(["Icon", "Avatar"]);
1553
1469
  var ANTONYMS = { off: "on", unchecked: "checked" };
@@ -1560,7 +1476,6 @@ function tokenize(src) {
1560
1476
  i++;
1561
1477
  continue;
1562
1478
  }
1563
- if (c === "#" && !(i > 0 && "=:".includes(src[i - 1]))) break;
1564
1479
  if (c === '"' || c === "'") {
1565
1480
  let j2 = i + 1, s = "";
1566
1481
  while (j2 < src.length && src[j2] !== c) {
@@ -1581,8 +1496,8 @@ function tokenize(src) {
1581
1496
  i++;
1582
1497
  continue;
1583
1498
  }
1584
- let j = c === "#" ? i + 1 : i;
1585
- while (j < src.length && !` ,=[]{}#":`.includes(src[j])) j++;
1499
+ let j = i;
1500
+ while (j < src.length && !` ,=[]{}":`.includes(src[j])) j++;
1586
1501
  const w = src.slice(i, j);
1587
1502
  out.push({ kind: /^-?\d+(\.\d+)?$/.test(w) ? "num" : "word", value: w, start: i, end: j });
1588
1503
  i = j;
@@ -1677,17 +1592,29 @@ function parseWireframeText(source) {
1677
1592
  const stack = [];
1678
1593
  let root = null;
1679
1594
  let n = 0;
1595
+ let lastElementLine = 0;
1680
1596
  const srcLines = source.replace(/\r\n?/g, "\n").split("\n");
1681
1597
  srcLines.forEach((raw, idx) => {
1682
1598
  const lineNo = idx + 1;
1683
1599
  const trimmed = raw.trim();
1684
1600
  if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("```")) return;
1685
1601
  const indent = raw.length - raw.trimStart().length;
1602
+ const target = lastElementLine ? `line ${lastElementLine}` : "its component's line";
1603
+ const wrappedProp = trimmed.match(/^([A-Za-z][\w-]*)\s*[=:]/);
1604
+ if (wrappedProp) {
1605
+ issues.push({ line: lineNo, message: `${wrappedProp[1]}=\u2026: props go on the same line as their component; move it to the end of ${target}` });
1606
+ return;
1607
+ }
1608
+ if (/^[\[\]{}]/.test(trimmed)) {
1609
+ issues.push({ line: lineNo, message: `a list must stay on one line; join this to ${target}` });
1610
+ return;
1611
+ }
1686
1612
  const m = trimmed.match(/^([A-Za-z][\w-]*)(.*)$/);
1687
1613
  if (!m) {
1688
1614
  issues.push({ line: lineNo, message: `expected a component name, got "${trimmed}"` });
1689
1615
  return;
1690
1616
  }
1617
+ lastElementLine = lineNo;
1691
1618
  const info = COMPONENTS.get(normalizeType(m[1]));
1692
1619
  if (!info) {
1693
1620
  issues.push({ line: lineNo, message: unknownComponentMessage(m[1], [...COMPONENTS.keys()]) });
@@ -1705,7 +1632,8 @@ function parseWireframeText(source) {
1705
1632
  c.next();
1706
1633
  const key = t.value;
1707
1634
  if (!(key in info.props)) {
1708
- issues.push({ line: lineNo, message: `${info.name} has no prop "${key}" (its props: ${Object.keys(info.props).join(", ")})` });
1635
+ const elsewhere = belongsElsewhere(info.name, key, true);
1636
+ issues.push({ line: lineNo, message: `${info.name} has no prop "${key}"` + (elsewhere ? `: ${elsewhere}` : "") + ` (its props: ${Object.keys(info.props).join(", ")})` });
1709
1637
  }
1710
1638
  props[key] = key in info.props ? textWhereExpected(info.props[key], parseValue(c)) : parseValue(c);
1711
1639
  continue;
@@ -1726,11 +1654,15 @@ function parseWireframeText(source) {
1726
1654
  props[ANTONYMS[t.value]] = false;
1727
1655
  } else if (t.kind === "word" && WORD_PRIMARY.has(info.name) && !(PRIMARY_PROP[info.name] in props)) {
1728
1656
  props[PRIMARY_PROP[info.name]] = t.value;
1657
+ } else if (t.kind === "word" && t.value.startsWith("#")) {
1658
+ issues.push({ line: lineNo, message: `"${t.value}": a comment must be on its own line, starting with #; to show # as text, quote it` });
1659
+ break;
1729
1660
  } else {
1730
1661
  const options = [...info.enumValues.keys(), ...info.booleans];
1662
+ const elsewhere = t.kind === "word" ? belongsElsewhere(info.name, t.value, false) : "";
1731
1663
  issues.push({
1732
1664
  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}"` : "")
1665
+ 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
1666
  });
1735
1667
  }
1736
1668
  }
@@ -1756,9 +1688,296 @@ function parseWireframeText(source) {
1756
1688
  return { spec: root ? { root, elements } : null, issues, lines };
1757
1689
  }
1758
1690
 
1691
+ // src/upgrade.ts
1692
+ var RENAMED_PROPS = {
1693
+ Button: { icon: "leadingIcon" },
1694
+ ListItem: { icon: "leadingIcon" }
1695
+ };
1696
+ var RENAMED_BY_WORD = new Map(Object.entries(RENAMED_PROPS).map(([type, renames]) => [type.toLowerCase(), renames]));
1697
+ function upgradeWireframe(text, options = {}) {
1698
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
1699
+ const out = [];
1700
+ for (const line of lines) {
1701
+ const trimmed = line.trim();
1702
+ const component = trimmed.match(/^([A-Za-z][\w-]*)/)?.[1];
1703
+ if (!component || trimmed.startsWith("```")) {
1704
+ out.push(line);
1705
+ continue;
1706
+ }
1707
+ const tokens = scanLine(line, component.length + (line.length - line.trimStart().length));
1708
+ let upgraded = line;
1709
+ let comment = "";
1710
+ if (options.fromLink) {
1711
+ const hash = tokens.find((t) => t.kind === "word" && t.depth === 0 && upgraded[t.start] === "#" && !t.afterEquals);
1712
+ if (hash && !fillsMainText(component, tokens, hash)) {
1713
+ comment = upgraded.slice(hash.start).trimEnd();
1714
+ upgraded = upgraded.slice(0, hash.start).trimEnd();
1715
+ }
1716
+ }
1717
+ const renames = RENAMED_BY_WORD.get(component.toLowerCase().replace(/[-_]/g, ""));
1718
+ if (renames) {
1719
+ for (const t of [...tokens].reverse()) {
1720
+ if (t.kind === "key" && t.depth === 0 && t.start < upgraded.length && renames[t.text]) {
1721
+ upgraded = upgraded.slice(0, t.start) + renames[t.text] + upgraded.slice(t.start + t.text.length);
1722
+ }
1723
+ }
1724
+ }
1725
+ if (comment) out.push(line.slice(0, line.length - line.trimStart().length) + comment);
1726
+ out.push(upgraded);
1727
+ }
1728
+ return out.join("\n");
1729
+ }
1730
+ function upgradeSpec(spec) {
1731
+ for (const el of Object.values(spec.elements)) {
1732
+ const renames = RENAMED_PROPS[el.type];
1733
+ if (!renames || !el.props) continue;
1734
+ for (const [from, to] of Object.entries(renames)) {
1735
+ if (from in el.props) {
1736
+ if (!(to in el.props)) el.props[to] = el.props[from];
1737
+ delete el.props[from];
1738
+ }
1739
+ }
1740
+ }
1741
+ return spec;
1742
+ }
1743
+ var PLAIN_WORD_MAIN_TEXT = ["avatar", "icon"];
1744
+ function fillsMainText(component, tokens, hash) {
1745
+ if (!PLAIN_WORD_MAIN_TEXT.includes(component.toLowerCase())) return false;
1746
+ for (const t of tokens) {
1747
+ if (t === hash) return true;
1748
+ if (t.depth > 0 || t.afterEquals || t.kind === "key") continue;
1749
+ return false;
1750
+ }
1751
+ return false;
1752
+ }
1753
+ function scanLine(line, from) {
1754
+ const tokens = [];
1755
+ let depth = 0;
1756
+ let afterEquals = false;
1757
+ let i = from;
1758
+ while (i < line.length) {
1759
+ const c = line[i];
1760
+ if (c === " " || c === " ") {
1761
+ i++;
1762
+ continue;
1763
+ }
1764
+ if (c === '"' || c === "'") {
1765
+ let j2 = i + 1;
1766
+ while (j2 < line.length && line[j2] !== c) j2 += line[j2] === "\\" ? 2 : 1;
1767
+ tokens.push({ kind: "string", text: line.slice(i, j2 + 1), start: i, depth, afterEquals });
1768
+ afterEquals = false;
1769
+ i = j2 + 1;
1770
+ continue;
1771
+ }
1772
+ if (c === "[" || c === "{") {
1773
+ depth++;
1774
+ afterEquals = false;
1775
+ i++;
1776
+ continue;
1777
+ }
1778
+ if (c === "]" || c === "}") {
1779
+ depth = Math.max(0, depth - 1);
1780
+ afterEquals = false;
1781
+ i++;
1782
+ continue;
1783
+ }
1784
+ if (c === ",") {
1785
+ afterEquals = false;
1786
+ i++;
1787
+ continue;
1788
+ }
1789
+ if (c === "=" || c === ":") {
1790
+ afterEquals = true;
1791
+ i++;
1792
+ continue;
1793
+ }
1794
+ let j = i;
1795
+ while (j < line.length && !` ,=[]{}":`.includes(line[j])) j++;
1796
+ const text = line.slice(i, j);
1797
+ let k = j;
1798
+ while (line[k] === " " || line[k] === " ") k++;
1799
+ const isKey = !afterEquals && (line[k] === "=" || line[k] === ":");
1800
+ tokens.push({ kind: isKey ? "key" : "word", text, start: i, depth, afterEquals });
1801
+ afterEquals = false;
1802
+ i = j;
1803
+ }
1804
+ return tokens;
1805
+ }
1806
+
1807
+ // src/render.ts
1808
+ import { readFile } from "node:fs/promises";
1809
+ import { createRequire } from "node:module";
1810
+ import { renderToSvg } from "@json-render/image/render";
1811
+ var fontFile = (name) => createRequire(import.meta.url).resolve(`@fontsource/inter/files/${name}`);
1812
+ var fontCache = null;
1813
+ function loadFonts() {
1814
+ fontCache ??= Promise.all([
1815
+ readFile(fontFile("inter-latin-400-normal.woff")),
1816
+ readFile(fontFile("inter-latin-600-normal.woff"))
1817
+ ]).then(([regular, semibold]) => [
1818
+ { name: "Inter", data: regular, weight: 400, style: "normal" },
1819
+ { name: "Inter", data: semibold, weight: 600, style: "normal" }
1820
+ ]);
1821
+ return fontCache;
1822
+ }
1823
+ var SpecError = class extends Error {
1824
+ constructor(issues) {
1825
+ super(`Invalid wireframe spec:
1826
+ - ${issues.join("\n - ")}`);
1827
+ this.issues = issues;
1828
+ }
1829
+ issues;
1830
+ };
1831
+ function listItemKindHint(el, path) {
1832
+ const prop = path[0];
1833
+ if (el.type !== "ListItem" || path.length !== 1 || prop !== "leadingIcon" && prop !== "trailingIcon") return void 0;
1834
+ const side = prop === "leadingIcon" ? "leading" : "trailing";
1835
+ const value = String(el.props?.[prop]);
1836
+ let schema = componentDefinitions.ListItem.props.shape[side];
1837
+ while (schema && ["optional", "nullable", "default"].includes(schema.def?.type)) schema = schema.def.innerType;
1838
+ const kinds = schema?.options ?? [];
1839
+ if (value === "icon" || !kinds.includes(value)) return void 0;
1840
+ return `${value} is a ${side} kind, not an icon name: write ${side}=${value} instead of ${prop}=${value}`;
1841
+ }
1842
+ function checkSpec(spec) {
1843
+ upgradeSpec(spec);
1844
+ const issues = [];
1845
+ const result = catalog.validate(spec);
1846
+ if (!result.success) {
1847
+ const err = result.error;
1848
+ const zodIssues = err?.issues ?? [];
1849
+ if (zodIssues.length) {
1850
+ for (const i of zodIssues) issues.push(`${(i.path ?? []).join(".")}: ${i.message}`);
1851
+ } else {
1852
+ issues.push(String(err ?? "catalog validation failed"));
1853
+ }
1854
+ }
1855
+ const root = spec.elements?.[spec.root];
1856
+ if (!root) {
1857
+ issues.push(`root "${spec.root}" is not in elements`);
1858
+ return issues;
1859
+ }
1860
+ if (root.type !== "Board") issues.push(`root must be a Board (got ${root.type})`);
1861
+ for (const [id, el] of Object.entries(spec.elements)) {
1862
+ const def = componentDefinitions[el.type];
1863
+ if (!def) {
1864
+ issues.push(`${id}: ${unknownComponentMessage(el.type, Object.keys(componentDefinitions))}`);
1865
+ } else {
1866
+ const parsed = def.props.safeParse(el.props ?? {});
1867
+ if (!parsed.success) {
1868
+ for (const i of parsed.error.issues) {
1869
+ issues.push(`${id}.props${i.path.length ? "." + i.path.join(".") : ""}: ${listItemKindHint(el, i.path) ?? i.message}`);
1870
+ }
1871
+ }
1872
+ for (const key of Object.keys(el.props ?? {})) {
1873
+ if (!(key in def.props.shape)) {
1874
+ const elsewhere = belongsElsewhere(el.type, key, true);
1875
+ issues.push(`${id}.props: ${el.type} has no prop "${key}"${elsewhere ? `: ${elsewhere}` : ""}`);
1876
+ }
1877
+ }
1878
+ }
1879
+ for (const child of el.children ?? []) {
1880
+ if (!spec.elements[child]) issues.push(`${id}: child "${child}" does not exist`);
1881
+ }
1882
+ if (el.type === "Screen" && !(root.children ?? []).includes(id)) {
1883
+ issues.push(`${id}: Screen must be a direct child of the Board`);
1884
+ }
1885
+ if (el.type === "Modal" || el.type === "Drawer") {
1886
+ const parent = Object.entries(spec.elements).find(([, p]) => p.children?.includes(id));
1887
+ if (parent && parent[1].type !== "Screen") {
1888
+ issues.push(`${id}: ${el.type} must be a direct child of a Screen (found in ${parent[1].type} "${parent[0]}")`);
1889
+ }
1890
+ }
1891
+ if (el.type === "ListItem") {
1892
+ const p = el.props ?? {};
1893
+ const { leading, trailing } = listItemEnds(p);
1894
+ const conflict = (prop, needs, kind, has) => issues.push(`${id}: ${prop} only shows with ${needs} (this item has ${kind}=${has}); remove one of them`);
1895
+ if (p.leadingIcon && leading !== "icon") conflict("leadingIcon", "leading=icon", "leading", leading);
1896
+ if (p.trailingIcon && trailing !== "icon") conflict("trailingIcon", "trailing=icon", "trailing", trailing);
1897
+ if (p.trailingText && trailing !== "text" && trailing !== "badge") conflict("trailingText", "trailing=text or badge", "trailing", trailing);
1898
+ }
1899
+ if (el.type === "Table") {
1900
+ const { columns, data } = el.props ?? {};
1901
+ if (Array.isArray(columns) && Array.isArray(data)) {
1902
+ const bad = data.map((row, i) => ({ row: i + 1, cells: Array.isArray(row) ? row.length : 1 })).filter((r) => r.cells !== columns.length);
1903
+ if (bad.length) {
1904
+ const cells = (n) => `${n} cell${n === 1 ? "" : "s"}`;
1905
+ 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`;
1906
+ const hints = [
1907
+ bad.some((r) => r.cells > columns.length) && `quote cells that contain a comma, e.g. ["$1,200", Paid]`,
1908
+ bad.some((r) => r.cells < columns.length) && `use "" for an empty cell`
1909
+ ].filter(Boolean);
1910
+ issues.push(`${id}: Table has ${columns.length} columns, but ${rows} (${hints.join("; ")})`);
1911
+ }
1912
+ }
1913
+ }
1914
+ }
1915
+ return issues;
1916
+ }
1917
+ function boardLayout(spec) {
1918
+ const board = spec.elements[spec.root];
1919
+ const p = board.props ?? {};
1920
+ const gap = p.gap ?? BOARD_GAP;
1921
+ const pad = p.padding ?? BOARD_PADDING;
1922
+ const kids = (board.children ?? []).map((id) => spec.elements[id]).filter(Boolean);
1923
+ const boxes = kids.map((el) => {
1924
+ const props = el.props ?? {};
1925
+ if (el.type === "Screen") {
1926
+ const s = screenSize(props);
1927
+ return { type: "Screen", name: String(props.name ?? ""), w: s.width, h: s.height + LABEL_H };
1928
+ }
1929
+ if (el.type === "Note") {
1930
+ const w = props.width ?? NOTE_WIDTH;
1931
+ return { type: "Note", name: String(props.text ?? ""), w, h: LABEL_H + estimateNoteHeight(props.text ?? "", w) };
1932
+ }
1933
+ return null;
1934
+ });
1935
+ const perRow = p.layout === "grid" ? Math.max(1, p.columns ?? 3) : Math.max(1, boxes.length);
1936
+ const items = [];
1937
+ let width = 0;
1938
+ let y = pad + (p.title ? TITLE_H : 0);
1939
+ for (let i = 0; i < boxes.length; i += perRow) {
1940
+ const row = boxes.slice(i, i + perRow);
1941
+ if (i > 0) y += gap;
1942
+ let x = pad;
1943
+ for (const b of row) {
1944
+ if (b) items.push({ type: b.type, name: b.name, x, y, width: b.w, height: b.h });
1945
+ x += (b?.w ?? 0) + gap;
1946
+ }
1947
+ width = Math.max(width, row.reduce((sum, b) => sum + (b?.w ?? 0), 0) + gap * (row.length - 1));
1948
+ y += Math.max(...row.map((b) => b?.h ?? 0));
1949
+ }
1950
+ return { width: Math.ceil(width + pad * 2), height: Math.ceil(y + pad), items };
1951
+ }
1952
+ function boardSize(spec) {
1953
+ const { width, height } = boardLayout(spec);
1954
+ return { width, height };
1955
+ }
1956
+ async function renderWireframeSvg(spec, opts = {}) {
1957
+ upgradeSpec(spec);
1958
+ if (!opts.skipValidation) {
1959
+ const issues = checkSpec(spec);
1960
+ if (issues.length) throw new SpecError(issues);
1961
+ }
1962
+ const { width, height } = boardSize(spec);
1963
+ return renderToSvg(spec, {
1964
+ registry: withPalette(paletteFor(spec.elements[spec.root]?.props?.accent)),
1965
+ includeStandard: false,
1966
+ fonts: await loadFonts(),
1967
+ width,
1968
+ height
1969
+ });
1970
+ }
1971
+ async function renderWireframePng(spec, opts = {}) {
1972
+ const svg = await renderWireframeSvg(spec, opts);
1973
+ const { Resvg } = await import("@resvg/resvg-js");
1974
+ const resvg = new Resvg(svg, { fitTo: { mode: "zoom", value: opts.scale ?? 1 } });
1975
+ return resvg.render().asPng();
1976
+ }
1977
+
1759
1978
  // src/compile.ts
1760
1979
  function compileWireframe(source) {
1761
- const parsed = parseWireframeText(source);
1980
+ const parsed = parseWireframeText(upgradeWireframe(source));
1762
1981
  const issues = [...parsed.issues];
1763
1982
  if (parsed.spec) {
1764
1983
  for (const msg of checkSpec(parsed.spec)) {
@@ -1774,7 +1993,7 @@ function compileWireframe(source) {
1774
1993
  }
1775
1994
  const seen = /* @__PURE__ */ new Set();
1776
1995
  const unique = issues.filter((i) => {
1777
- const key = `${i.line}|${i.message.replace(/^\w+: /, "").replace(/ \(.*$/, "")}`;
1996
+ const key = `${i.line}|${i.message.replace(/^\w+: /, "").replace(/(has no prop "[^"]*").*$/, "$1").replace(/ \(.*$/, "")}`;
1778
1997
  if (seen.has(key)) return false;
1779
1998
  seen.add(key);
1780
1999
  return true;
@@ -1971,9 +2190,10 @@ One element per line. Indent children two spaces under their parent. The first l
1971
2190
  A line is the component name in lowercase, followed by arguments separated by spaces:
1972
2191
  - "a quoted string" sets the component's main text prop (marked "main text" below)
1973
2192
  - 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 a boolean prop to true: checked, fullWidth, grow, muted. \`off\` and \`unchecked\` set on/checked to false
2193
+ - 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
2194
  - key=value sets any prop. Values: "string", number, true/false, bare word, [list, of, values], {key=value key=value}
1976
- - \`#\` after a space starts a comment that runs to the end of the line
2195
+
2196
+ 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
2197
 
1978
2198
  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
2199
 
@@ -2010,14 +2230,22 @@ Reply with the corrected wireframe in a single code block.`;
2010
2230
  export {
2011
2231
  theme,
2012
2232
  DEVICES,
2233
+ ACCENTS,
2234
+ iconNames,
2013
2235
  componentDefinitions,
2014
2236
  catalog,
2015
2237
  registry,
2238
+ PRIMARY_PROP,
2239
+ bareWords,
2240
+ parseWireframeText,
2241
+ RENAMED_PROPS,
2242
+ upgradeWireframe,
2243
+ upgradeSpec,
2016
2244
  checkSpec,
2245
+ boardLayout,
2017
2246
  boardSize,
2018
2247
  renderWireframeSvg,
2019
2248
  renderWireframePng,
2020
- parseWireframeText,
2021
2249
  compileWireframe,
2022
2250
  formatIssues,
2023
2251
  WireframeError,