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.
- package/README.md +20 -6
- package/dist/catalog.d.ts +24 -8
- package/dist/chunks/{chunk-L6GTIZQH.js → chunk-YCIRLKKD.js} +389 -161
- package/dist/cli.js +1 -1
- package/dist/icons.d.ts +2 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +24 -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 +50 -0
- package/dist/prompt.d.ts +1 -1
- package/dist/render.d.ts +21 -1
- package/dist/share.d.ts +8 -0
- package/dist/text.d.ts +6 -1
- package/dist/upgrade.d.ts +41 -0
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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.
|
|
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 (
|
|
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.
|
|
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 (
|
|
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 =
|
|
1585
|
-
while (j < src.length && !` ,=[]{}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|