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.
@@ -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
- icon: o(iconName),
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
- icon: o(iconName),
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", leading: "icon", icon: "user", trailing: "chevron" }
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.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
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 (p.leading) {
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.icon, size: 20 }) });
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 (p.trailing) {
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 = c === "#" ? i + 1 : i;
1585
- while (j < src.length && !` ,=[]{}#":`.includes(src[j])) j++;
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
- issues.push({ line: lineNo, message: `${info.name} has no prop "${key}" (its props: ${Object.keys(info.props).join(", ")})` });
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 a boolean prop to true: checked, fullWidth, grow, muted. \`off\` and \`unchecked\` set on/checked to false
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
- - \`#\` after a space starts a comment that runs to the end of the line
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
@@ -6,7 +6,7 @@ import {
6
6
  printWireframeText,
7
7
  renderWireframe,
8
8
  wireframePrompt
9
- } from "./chunks/chunk-L6GTIZQH.js";
9
+ } from "./chunks/chunk-46MGJLGB.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { readFile, writeFile } from "node:fs/promises";
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 { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, type RenderWireframeOptions } from "./render.js";
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-L6GTIZQH.js";
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.