tsquare 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +2 -2
- package/dist/chunks/{chunk-Z4RBFAOM.js → chunk-W3OU5IS6.js} +80 -33
- package/dist/cli.js +1 -1
- package/dist/escape.d.ts +5 -0
- package/dist/icons.d.ts +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +44 -25
- package/dist/mcp.d.ts +14 -1
- package/dist/playground/index.js +1 -1
- package/dist/playground/playground.js +16 -16
- package/dist/prompt.d.ts +1 -1
- package/package.json +2 -1
- package/skill/tsquare/SKILL.md +2 -1
- package/skill/tsquare/reference.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Brand logos
|
|
6
|
+
|
|
7
|
+
```tsquare
|
|
8
|
+
button secondary "Continue with Google" leadingIcon=brand-google fullWidth
|
|
9
|
+
listitem "GitHub" subtitle="Connected" leadingIcon=brand-github trailing=toggle
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- **Seven brand logos**, usable anywhere an icon is: `brand-apple`, `brand-facebook`, `brand-github`, `brand-google`, `brand-notion`, `brand-stripe`, `brand-x`. They're for sign-in buttons and integrations, and are drawn in the wireframe's gray at the same visual size as Lucide icons.
|
|
13
|
+
- **The `brand-` prefix keeps them apart from Lucide:** `apple` is still Lucide's fruit and `x` its close icon. A brand name without the prefix is an error that suggests it (`github` → `brand-github`, `twitter` → `brand-x`), and an unknown `brand-…` name lists the logos there are.
|
|
14
|
+
- The logos come from [Simple Icons](https://simpleicons.org) 16.34.0 (CC0), a new dependency pinned exactly like Lucide, since a release can change or remove logos. Simple Icons has no Slack, LinkedIn or Microsoft logo, so tsquare doesn't either.
|
|
15
|
+
- **Prompt:** rule 11 names the logos and the `apple`/`x` trap. The skill and the docs (Icons page, Icon component) are regenerated.
|
|
16
|
+
|
|
17
|
+
### Flow wording
|
|
18
|
+
|
|
19
|
+
- **"Board level" is gone.** The Flow description, prompt rule 10, the docs and the error for a flow written inside a screen now say what to do: write each flow on its own line after the screens, indented like a screen (not inside one). The error used to read "flow lines go at the board level, after the screens (indented like a screen)". A clarification only: what's valid hasn't changed.
|
|
20
|
+
|
|
21
|
+
## 0.7.1
|
|
22
|
+
|
|
23
|
+
- **`share_wireframe` returns only the links asked for.** A new `links` option takes the kinds from the playground's Copy link menu, with the same names: `share`, `svg`, `png`, `markdown`, `html`. The default is `["svg", "share"]`, an image link and the link to edit it; before, every call returned four links. Each link carries the whole wireframe (about 1,200 characters for a three-screen board), and the model has to write out every link it passes on, exactly, so fewer links means less to write and fewer chances to break one. A wireframe too long for an image link gets its share link instead, with a note saying why. New exports: `LINK_KINDS`, `DEFAULT_LINKS`, `LinkKind`.
|
|
24
|
+
|
|
25
|
+
- **Invalid values name the prop and the value.** A wrong `key=value` used to come back as Zod's bare message, which didn't say which prop on the line was wrong: `Input: Invalid option: expected one of "text"|"password"|…`. Now: `Input: type=otp isn't an option; use one of text, password, search, email, date, code`. Likewise `level=4 isn't an option; use one of 1, 2, 3`, `height=tall should be a number`, `columns=Name should be a list like [a, b]`, `range=[1] needs at least 2 items`, `columns=0 is too small (at least 1)`. This matters most in the MCP repair loop, where the model fixes what the messages say.
|
|
26
|
+
- **Fix: the HTML link escapes `&`** (and `<`, `>`) in the board title, in both the playground's Copy link → HTML and the MCP share tool, which now share one helper. A title like `Tom & Jerry` used to show as "Tom & Jerry".
|
|
27
|
+
- **One wrong value is one error.** A check that uses a prop's value is skipped when that value is already invalid: `input type=otp digits=6` no longer also says "digits only applies to type=code". Checks that only notice a prop is set still run (`leadingIcon` on an avatar item is wrong whatever the icon's name).
|
|
28
|
+
|
|
3
29
|
## 0.7.0
|
|
4
30
|
|
|
5
31
|
### MCP tools
|
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ Sticky notes beside the wireframe have their own colors, since they're annotatio
|
|
|
95
95
|
|---|---|
|
|
96
96
|
| Canvas | board, screen (`phone` 390×844, `tablet` 820×1180, `desktop` 1280×800, `custom`), note |
|
|
97
97
|
| Layout | stack, grid, card, divider, spacer |
|
|
98
|
-
| Content | heading, text (`lines=3` draws placeholder lines), image (X-box placeholder), icon (any [Lucide](https://lucide.dev/icons) name
|
|
98
|
+
| Content | heading, text (`lines=3` draws placeholder lines), image (X-box placeholder), icon (any [Lucide](https://lucide.dev/icons) name or a brand logo such as `brand-google`, see [Icons](docs/icons.md)), avatar, badge |
|
|
99
99
|
| Controls | button, input (`multiline=4` for a textarea), checkbox, radio, toggle, select |
|
|
100
100
|
| Navigation & data | navbar, tabbar, tabs, list, listitem, table |
|
|
101
101
|
| Overlays | modal, drawer (`left`, `right`, `bottom` sheet) |
|
|
@@ -158,4 +158,4 @@ How it was measured, the tasks, every model output, and the scripts: [`eval/`](e
|
|
|
158
158
|
|
|
159
159
|
## License
|
|
160
160
|
|
|
161
|
-
MIT. The Inter font comes from the `@fontsource/inter` dependency, under the SIL Open Font License 1.1. Icons are from [Lucide](https://lucide.dev), under the ISC license.
|
|
161
|
+
MIT. The Inter font comes from the `@fontsource/inter` dependency, under the SIL Open Font License 1.1. Icons are from [Lucide](https://lucide.dev), under the ISC license. Brand logos are from [Simple Icons](https://simpleicons.org) (CC0); the logos themselves are trademarks of their owners.
|
|
@@ -131,14 +131,20 @@ function paletteFor(accent) {
|
|
|
131
131
|
|
|
132
132
|
// src/icons.ts
|
|
133
133
|
import { icons } from "lucide";
|
|
134
|
+
import { siApple, siFacebook, siGithub, siGoogle, siNotion, siStripe, siX } from "simple-icons";
|
|
135
|
+
var BRAND_ICONS = Object.fromEntries(
|
|
136
|
+
[siApple, siFacebook, siGithub, siGoogle, siNotion, siStripe, siX].map((i) => [`brand-${i.slug}`, { title: i.title, path: i.path }])
|
|
137
|
+
);
|
|
138
|
+
var brandIconNames = Object.keys(BRAND_ICONS).sort();
|
|
139
|
+
var brandPath = (name) => BRAND_ICONS[name.toLowerCase()]?.path;
|
|
134
140
|
var pascal = (s) => s.replace(/(^|[-_ ])(\w)/g, (_, __, c) => c.toUpperCase());
|
|
135
141
|
var kebab = (s) => s.replace(/([a-z])([A-Z0-9])/g, "$1-$2").replace(/([0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
136
142
|
var table = icons;
|
|
137
|
-
var iconNames = Object.keys(table).map(kebab);
|
|
143
|
+
var iconNames = [...Object.keys(table).map(kebab), ...brandIconNames];
|
|
138
144
|
function iconNode(name) {
|
|
139
145
|
return table[pascal(name)];
|
|
140
146
|
}
|
|
141
|
-
var isIcon = (name) => iconNode(name) !== void 0;
|
|
147
|
+
var isIcon = (name) => iconNode(name) !== void 0 || brandPath(name) !== void 0;
|
|
142
148
|
var GUESSES = {
|
|
143
149
|
call: ["phone"],
|
|
144
150
|
compose: ["square-pen", "pencil"],
|
|
@@ -148,7 +154,8 @@ var GUESSES = {
|
|
|
148
154
|
notifications: ["bell"],
|
|
149
155
|
profile: ["user", "circle-user"],
|
|
150
156
|
envelope: ["mail"],
|
|
151
|
-
close: ["x"]
|
|
157
|
+
close: ["x"],
|
|
158
|
+
twitter: ["brand-x"]
|
|
152
159
|
};
|
|
153
160
|
var DIRECTIONS = ["right", "left", "down", "up"];
|
|
154
161
|
var directionRank = (n) => {
|
|
@@ -157,7 +164,7 @@ var directionRank = (n) => {
|
|
|
157
164
|
};
|
|
158
165
|
function suggestIcons(name, limit = 4) {
|
|
159
166
|
const q = name.toLowerCase().replace(/[_ ]/g, "-");
|
|
160
|
-
const guessed = GUESSES[q] ?? [];
|
|
167
|
+
const guessed = [...BRAND_ICONS[`brand-${q}`] ? [`brand-${q}`] : [], ...GUESSES[q] ?? []];
|
|
161
168
|
const words = q.split("-").filter((w) => w.length > 2);
|
|
162
169
|
const maxEdits = Math.max(1, Math.floor(q.length / 3));
|
|
163
170
|
const scored = [];
|
|
@@ -173,6 +180,7 @@ function suggestIcons(name, limit = 4) {
|
|
|
173
180
|
return [.../* @__PURE__ */ new Set([...guessed, ...close])].slice(0, limit);
|
|
174
181
|
}
|
|
175
182
|
function unknownIconMessage(name) {
|
|
183
|
+
if (/^brand-/i.test(name)) return `unknown icon "${name}"; the brand logos are ${brandIconNames.join(", ")}`;
|
|
176
184
|
const close = suggestIcons(name);
|
|
177
185
|
return close.length ? `unknown icon "${name}" (did you mean ${close.join(", ")}?)` : `unknown icon "${name}" (use a Lucide name in kebab-case, e.g. search, user, settings)`;
|
|
178
186
|
}
|
|
@@ -297,7 +305,7 @@ var componentDefinitions = {
|
|
|
297
305
|
)
|
|
298
306
|
}),
|
|
299
307
|
slots: [],
|
|
300
|
-
description: 'An arrow between two elements or screens, named with #id.
|
|
308
|
+
description: 'An arrow between two elements or screens, named with #id. Write each flow on its own line after the screens, indented like a screen (not inside one): flow signin -> home "Tap Sign in".',
|
|
301
309
|
example: { from: "signin", to: "home", label: "Tap Sign in" }
|
|
302
310
|
},
|
|
303
311
|
// ── Layout ────────────────────────────────────────────────────────────
|
|
@@ -427,7 +435,7 @@ var componentDefinitions = {
|
|
|
427
435
|
size: o(z.number())
|
|
428
436
|
}),
|
|
429
437
|
slots: [],
|
|
430
|
-
description: "Line icon from Lucide.",
|
|
438
|
+
description: "Line icon from Lucide, or a brand logo (brand-google, brand-apple, \u2026).",
|
|
431
439
|
example: { name: "search", size: 20 }
|
|
432
440
|
},
|
|
433
441
|
Avatar: {
|
|
@@ -735,6 +743,9 @@ function hug(dir, aligned = false) {
|
|
|
735
743
|
return dir === "row" || aligned ? {} : { alignSelf: "flex-start" };
|
|
736
744
|
}
|
|
737
745
|
function IconGlyph({ name, size = 20, color = theme.ink, strokeWidth = 2 }) {
|
|
746
|
+
const brand = name ? brandPath(name) : void 0;
|
|
747
|
+
if (brand)
|
|
748
|
+
return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "-2 -2 28 28", fill: color, style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx("path", { d: brand }) });
|
|
738
749
|
const node = name ? iconNode(name) : null;
|
|
739
750
|
return /* @__PURE__ */ jsx(
|
|
740
751
|
"svg",
|
|
@@ -2684,6 +2695,38 @@ function listItemKindHint(el, path) {
|
|
|
2684
2695
|
if (value === "icon" || !kinds.includes(value)) return void 0;
|
|
2685
2696
|
return `${value} is a ${side} kind, not an icon name: write ${side}=${value} instead of ${prop}=${value}`;
|
|
2686
2697
|
}
|
|
2698
|
+
function written(v) {
|
|
2699
|
+
if (typeof v === "string") return /^[^\s"[\]{}=,]+$/.test(v) ? v : JSON.stringify(v);
|
|
2700
|
+
if (Array.isArray(v)) return `[${v.map(written).join(", ")}]`;
|
|
2701
|
+
return JSON.stringify(v) ?? String(v);
|
|
2702
|
+
}
|
|
2703
|
+
function propIssueMessage(props, issue) {
|
|
2704
|
+
const path = issue.path ?? [];
|
|
2705
|
+
if (!path.length || issue.code === "custom") return issue.message;
|
|
2706
|
+
const key = path.map(String).join(".");
|
|
2707
|
+
const value = path.reduce((v, k) => v == null ? v : v[k], props);
|
|
2708
|
+
const given = `${key}=${written(value)}`;
|
|
2709
|
+
switch (issue.code) {
|
|
2710
|
+
case "invalid_value":
|
|
2711
|
+
return `${given} isn't an option; use one of ${(issue.values ?? []).map(written).join(", ")}`;
|
|
2712
|
+
case "invalid_union": {
|
|
2713
|
+
const options = (issue.errors ?? []).flat().flatMap((e) => e.code === "invalid_value" ? e.values : []);
|
|
2714
|
+
if (options.length) return `${given} isn't an option; use one of ${options.map(written).join(", ")}`;
|
|
2715
|
+
return `${given} isn't a valid value`;
|
|
2716
|
+
}
|
|
2717
|
+
case "invalid_type":
|
|
2718
|
+
return `${given} should be ${issue.expected === "array" ? "a list like [a, b]" : `a ${issue.expected}`}`;
|
|
2719
|
+
case "too_small":
|
|
2720
|
+
case "too_big": {
|
|
2721
|
+
const [bound, n] = issue.code === "too_small" ? ["at least", issue.minimum] : ["at most", issue.maximum];
|
|
2722
|
+
if (issue.origin === "array") return `${given} needs ${bound} ${n} item${n == 1 ? "" : "s"}`;
|
|
2723
|
+
if (issue.origin === "string") return `${given} needs ${bound} ${n} character${n == 1 ? "" : "s"}`;
|
|
2724
|
+
return `${given} is too ${issue.code === "too_small" ? "small" : "large"} (${bound} ${n})`;
|
|
2725
|
+
}
|
|
2726
|
+
default:
|
|
2727
|
+
return `${given}: ${issue.message}`;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2687
2730
|
function checkSpec(spec) {
|
|
2688
2731
|
upgradeSpec(spec);
|
|
2689
2732
|
const issues = [];
|
|
@@ -2705,13 +2748,16 @@ function checkSpec(spec) {
|
|
|
2705
2748
|
if (root.type !== "Board") issues.push(`root must be a Board (got ${root.type})`);
|
|
2706
2749
|
for (const [id, el] of Object.entries(spec.elements)) {
|
|
2707
2750
|
const def = componentDefinitions[el.type];
|
|
2751
|
+
const bad = /* @__PURE__ */ new Set();
|
|
2752
|
+
const failed = (...keys) => keys.some((k) => bad.has(k));
|
|
2708
2753
|
if (!def) {
|
|
2709
2754
|
issues.push(`${id}: ${unknownComponentMessage(el.type, Object.keys(componentDefinitions))}`);
|
|
2710
2755
|
} else {
|
|
2711
2756
|
const parsed = def.props.safeParse(el.props ?? {});
|
|
2712
2757
|
if (!parsed.success) {
|
|
2713
2758
|
for (const i of parsed.error.issues) {
|
|
2714
|
-
|
|
2759
|
+
if (i.path.length) bad.add(String(i.path[0]));
|
|
2760
|
+
issues.push(`${id}.props${i.path.length ? "." + i.path.join(".") : ""}: ${listItemKindHint(el, i.path) ?? propIssueMessage(el.props ?? {}, i)}`);
|
|
2715
2761
|
}
|
|
2716
2762
|
}
|
|
2717
2763
|
for (const key of Object.keys(el.props ?? {})) {
|
|
@@ -2750,50 +2796,50 @@ function checkSpec(spec) {
|
|
|
2750
2796
|
const p = el.props ?? {};
|
|
2751
2797
|
const { leading, trailing } = listItemEnds(p);
|
|
2752
2798
|
const conflict = (prop, needs, kind, has) => issues.push(`${id}: ${prop} only shows with ${needs} (this item has ${kind}=${has}); remove one of them`);
|
|
2753
|
-
if (p.leadingIcon && leading !== "icon") conflict("leadingIcon", "leading=icon", "leading", leading);
|
|
2754
|
-
if (p.trailingIcon && trailing !== "icon") conflict("trailingIcon", "trailing=icon", "trailing", trailing);
|
|
2755
|
-
if (p.trailingText && trailing !== "text" && trailing !== "badge") conflict("trailingText", "trailing=text or badge", "trailing", trailing);
|
|
2799
|
+
if (p.leadingIcon && leading !== "icon" && !failed("leading")) conflict("leadingIcon", "leading=icon", "leading", leading);
|
|
2800
|
+
if (p.trailingIcon && trailing !== "icon" && !failed("trailing")) conflict("trailingIcon", "trailing=icon", "trailing", trailing);
|
|
2801
|
+
if (p.trailingText && trailing !== "text" && trailing !== "badge" && !failed("trailing")) conflict("trailingText", "trailing=text or badge", "trailing", trailing);
|
|
2756
2802
|
}
|
|
2757
2803
|
{
|
|
2758
2804
|
const p = el.props ?? {};
|
|
2759
|
-
const say = (msg) => issues.push(`${id}: ${msg}`);
|
|
2805
|
+
const say = (msg, ...reads) => !failed(...reads) && issues.push(`${id}: ${msg}`);
|
|
2760
2806
|
const backwards = (r) => Array.isArray(r) && r.length === 2 && r[0] > r[1];
|
|
2761
2807
|
if (el.type === "Progress") {
|
|
2762
|
-
if (p.step != null && p.steps == null) say(`step only shows with steps (e.g. steps=4 step=${p.step})`);
|
|
2763
|
-
if (p.step != null && p.steps != null && p.step > p.steps) say(`step=${p.step} is past the last step (steps=${p.steps})
|
|
2764
|
-
if (p.steps != null && p.shape === "circle") say("a stepper (steps) can't also be a circle; remove one of them");
|
|
2808
|
+
if (p.step != null && p.steps == null) say(`step only shows with steps (e.g. steps=4 step=${typeof p.step === "number" ? p.step : 2})`);
|
|
2809
|
+
if (p.step != null && p.steps != null && p.step > p.steps) say(`step=${p.step} is past the last step (steps=${p.steps})`, "step", "steps");
|
|
2810
|
+
if (p.steps != null && p.shape === "circle") say("a stepper (steps) can't also be a circle; remove one of them", "shape");
|
|
2765
2811
|
}
|
|
2766
2812
|
if (el.type === "Pagination" && p.current != null && p.pages != null && p.current > p.pages) {
|
|
2767
|
-
say(`current=${p.current} is past the last page (pages=${p.pages})
|
|
2813
|
+
say(`current=${p.current} is past the last page (pages=${p.pages})`, "current", "pages");
|
|
2768
2814
|
}
|
|
2769
|
-
if (el.type === "Slider" && backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the smaller number first
|
|
2815
|
+
if (el.type === "Slider" && backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the smaller number first`, "range");
|
|
2770
2816
|
if (el.type === "Slider" && p.range && p.value != null) say("a slider has value (one handle) or range (two), not both");
|
|
2771
2817
|
if (el.type === "Calendar") {
|
|
2772
2818
|
const { days } = monthGrid(p.month);
|
|
2773
2819
|
const all = [p.selected, ...p.range ?? [], ...p.marked ?? []].filter((d) => typeof d === "number");
|
|
2774
2820
|
const late = all.find((d) => d > days);
|
|
2775
|
-
if (late) say(`day ${late} isn't in ${p.month ?? "the month"} (it has ${days} days)
|
|
2776
|
-
if (backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the earlier day first
|
|
2821
|
+
if (late) say(`day ${late} isn't in ${p.month ?? "the month"} (it has ${days} days)`, "month", "selected", "range", "marked");
|
|
2822
|
+
if (backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the earlier day first`, "range");
|
|
2777
2823
|
if (p.range && p.selected != null) say("a calendar has selected (one day) or range (several), not both");
|
|
2778
2824
|
}
|
|
2779
2825
|
if (el.type === "Input" && p.type === "code" && p.value != null && String(p.value).length > (p.digits ?? 6)) {
|
|
2780
|
-
say(`value has ${String(p.value).length} characters but the code has ${p.digits ?? 6} boxes (digits)
|
|
2826
|
+
say(`value has ${String(p.value).length} characters but the code has ${p.digits ?? 6} boxes (digits)`, "type", "value", "digits");
|
|
2781
2827
|
}
|
|
2782
|
-
if (el.type === "Input" && p.digits != null && p.type !== "code") say("digits only applies to type=code");
|
|
2783
|
-
if (el.type === "Input" && p.open && p.type !== "date") say("open shows a date picker, so it needs type=date");
|
|
2784
|
-
if ((el.type === "Button" || el.type === "ListItem" || el.type === "NavBar") && p.open && !(Array.isArray(p.menu) && p.menu.length)) say("open shows the menu; add menu=[\u2026] with its items");
|
|
2785
|
-
if (el.type === "Select" && p.open && !(Array.isArray(p.options) && p.options.length)) say("open shows the options list; add options=[\u2026] with the choices");
|
|
2828
|
+
if (el.type === "Input" && p.digits != null && p.type !== "code") say("digits only applies to type=code", "type");
|
|
2829
|
+
if (el.type === "Input" && p.open && p.type !== "date") say("open shows a date picker, so it needs type=date", "type");
|
|
2830
|
+
if ((el.type === "Button" || el.type === "ListItem" || el.type === "NavBar") && p.open && !(Array.isArray(p.menu) && p.menu.length)) say("open shows the menu; add menu=[\u2026] with its items", "menu");
|
|
2831
|
+
if (el.type === "Select" && p.open && !(Array.isArray(p.options) && p.options.length)) say("open shows the options list; add options=[\u2026] with the choices", "options");
|
|
2786
2832
|
}
|
|
2787
2833
|
if (el.type === "Table") {
|
|
2788
2834
|
const { columns, data } = el.props ?? {};
|
|
2789
2835
|
if (Array.isArray(columns) && Array.isArray(data)) {
|
|
2790
|
-
const
|
|
2791
|
-
if (
|
|
2836
|
+
const bad2 = data.map((row, i) => ({ row: i + 1, cells: Array.isArray(row) ? row.length : 1 })).filter((r) => r.cells !== columns.length);
|
|
2837
|
+
if (bad2.length) {
|
|
2792
2838
|
const cells = (n) => `${n} cell${n === 1 ? "" : "s"}`;
|
|
2793
|
-
const rows =
|
|
2839
|
+
const rows = bad2.length === 1 ? `data row ${bad2[0].row} has ${cells(bad2[0].cells)}` : `data rows ${bad2.map((r) => `${r.row} (${cells(r.cells)})`).join(", ")} don't match`;
|
|
2794
2840
|
const hints = [
|
|
2795
|
-
|
|
2796
|
-
|
|
2841
|
+
bad2.some((r) => r.cells > columns.length) && `quote cells that contain a comma, e.g. ["$1,200", Paid]`,
|
|
2842
|
+
bad2.some((r) => r.cells < columns.length) && `use "" for an empty cell`
|
|
2797
2843
|
].filter(Boolean);
|
|
2798
2844
|
issues.push(`${id}: Table has ${columns.length} columns, but ${rows} (${hints.join("; ")})`);
|
|
2799
2845
|
}
|
|
@@ -2808,12 +2854,12 @@ function checkSpec(spec) {
|
|
|
2808
2854
|
if (other) issues.push(`${key}: the id #${ref} is also used by ${spec.elements[other.key].type} "${other.key}"${other.id === ref ? "" : ` (as #${other.id}; ids ignore case)`}; ids must be unique`);
|
|
2809
2855
|
else owners.set(ref.toLowerCase(), { key, id: ref });
|
|
2810
2856
|
}
|
|
2811
|
-
const
|
|
2857
|
+
const written2 = [...owners.values()].map((o2) => o2.id);
|
|
2812
2858
|
const parentOf = /* @__PURE__ */ new Map();
|
|
2813
2859
|
for (const [k, el] of Object.entries(spec.elements)) for (const c of el.children ?? []) parentOf.set(c, k);
|
|
2814
2860
|
for (const [key, el] of Object.entries(spec.elements)) {
|
|
2815
2861
|
if (el.type !== "Flow") continue;
|
|
2816
|
-
if (!(root.children ?? []).includes(key)) issues.push(`${key}:
|
|
2862
|
+
if (!(root.children ?? []).includes(key)) issues.push(`${key}: write each flow on its own line after the screens, indented like a screen (not inside one)`);
|
|
2817
2863
|
const p = el.props ?? {};
|
|
2818
2864
|
for (const end of ["from", "to"]) {
|
|
2819
2865
|
if (typeof p[end] !== "string") continue;
|
|
@@ -2828,9 +2874,9 @@ function checkSpec(spec) {
|
|
|
2828
2874
|
}
|
|
2829
2875
|
continue;
|
|
2830
2876
|
}
|
|
2831
|
-
const close = closeMatches(p[end],
|
|
2877
|
+
const close = closeMatches(p[end], written2);
|
|
2832
2878
|
issues.push(
|
|
2833
|
-
`${key}: no element has the id #${p[end]}` + (close.length ? ` (did you mean ${close.map((c) => "#" + c).join(", ")}?)` : owners.size ? ` (ids on this board: ${
|
|
2879
|
+
`${key}: no element has the id #${p[end]}` + (close.length ? ` (did you mean ${close.map((c) => "#" + c).join(", ")}?)` : owners.size ? ` (ids on this board: ${written2.map((c) => "#" + c).join(", ")})` : "; name an element by writing #name after it")
|
|
2834
2880
|
);
|
|
2835
2881
|
}
|
|
2836
2882
|
if (typeof p.from === "string" && typeof p.to === "string" && p.from.toLowerCase() === p.to.toLowerCase()) issues.push(`${key}: a flow needs two different ends (both are #${p.from})`);
|
|
@@ -3194,7 +3240,8 @@ var RULES = `## Rules
|
|
|
3194
3240
|
7. Only use the components and props listed below. All props are optional unless marked required.
|
|
3195
3241
|
8. Wireframes are grayscale. The only UI colors: Board accent (one color for primary buttons, checked controls, toggles, active tabs and ghost buttons), Badge tone (success, warning, danger), Input error and Flow color. Only add an accent if the request asks for color or a brand.
|
|
3196
3242
|
9. Any element except Board, Screen, Note and Flow can take tooltip="text" to show a tooltip next to it. Select (with options), Input type=date, and Button, ListItem or NavBar (with menu) take open to show their list, calendar or menu over the screen.
|
|
3197
|
-
10. To show how screens connect, name elements or screens by writing #name after them (button primary "Sign in" #signin, screen phone "Home" #home), then
|
|
3243
|
+
10. To show how screens connect, name elements or screens by writing #name after them (button primary "Sign in" #signin, screen phone "Home" #home), then write each flow on its own line after the screens, indented like a screen (not inside one): flow signin -> home "Tap Sign in".
|
|
3244
|
+
11. Icons are Lucide names, plus brand logos named brand-\u2026: brand-google, brand-apple, brand-github, brand-facebook, brand-x, brand-notion, brand-stripe (button "Continue with Google" leadingIcon=brand-google). Plain apple is a fruit and x is a close icon.`;
|
|
3198
3245
|
var TEXT_FORMAT = `## Output format: wireframe text
|
|
3199
3246
|
One element per line. Indent children two spaces under their parent. The first line is the board.
|
|
3200
3247
|
|
package/dist/cli.js
CHANGED
package/dist/escape.d.ts
ADDED
package/dist/icons.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Brand logos, from Simple Icons (CC0; pinned exactly, like lucide). Filled shapes on a 24×24 grid,
|
|
3
|
+
* named brand-… so they can't collide with Lucide: `apple` is Lucide's fruit and `x` its close icon.
|
|
4
|
+
* Imported one by one so a browser bundle carries only these. Simple Icons has no Slack, LinkedIn or
|
|
5
|
+
* Microsoft logo (removed); don't draw our own.
|
|
6
|
+
*/
|
|
7
|
+
export declare const BRAND_ICONS: Record<string, {
|
|
8
|
+
title: string;
|
|
9
|
+
path: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare const brandIconNames: string[];
|
|
12
|
+
/** The SVG path of a brand-… logo, or undefined. */
|
|
13
|
+
export declare const brandPath: (name: string) => string | undefined;
|
|
14
|
+
/** Kebab-case names for every Lucide icon (including Lucide's aliases, e.g. home and house), then the brand logos. */
|
|
2
15
|
export declare const iconNames: string[];
|
|
3
16
|
/** The Lucide icon node for a kebab-case name, or undefined if there's no such icon. */
|
|
4
17
|
export declare function iconNode(name: string): any;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { renderWireframe, compileWireframe, formatIssues, WireframeError, type C
|
|
|
3
3
|
export { wireframePrompt, repairPrompt } from "./prompt.js";
|
|
4
4
|
export { printWireframeText as formatWireframe } from "./print.js";
|
|
5
5
|
export { encodeWireframe, decodeWireframe, MAX_SHARED_TEXT } from "./share.js";
|
|
6
|
-
export { mcpTools, MCP_INSTRUCTIONS, wireframeGuide, renderWireframeTool, shareWireframeTool, callMcpTool, type ToolResult, type ToolContent, type ToolOptions, type McpToolName } from "./mcp.js";
|
|
6
|
+
export { mcpTools, MCP_INSTRUCTIONS, LINK_KINDS, DEFAULT_LINKS, type LinkKind, wireframeGuide, renderWireframeTool, shareWireframeTool, callMcpTool, type ToolResult, type ToolContent, type ToolOptions, type McpToolName } from "./mcp.js";
|
|
7
7
|
export { upgradeWireframe, upgradeSpec, RENAMED_PROPS, type UpgradeOptions } from "./upgrade.js";
|
|
8
8
|
export { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, boardLayout, type BoardItem, type RenderWireframeOptions } from "./render.js";
|
|
9
9
|
export { parseWireframeText, type TextIssue, type ParseResult } from "./text.js";
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
upgradeSpec,
|
|
22
22
|
upgradeWireframe,
|
|
23
23
|
wireframePrompt
|
|
24
|
-
} from "./chunks/chunk-
|
|
24
|
+
} from "./chunks/chunk-W3OU5IS6.js";
|
|
25
25
|
|
|
26
26
|
// src/share.ts
|
|
27
27
|
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
@@ -45,19 +45,36 @@ function decodeWireframe(data) {
|
|
|
45
45
|
|
|
46
46
|
// src/mcp.ts
|
|
47
47
|
import { z } from "zod";
|
|
48
|
+
|
|
49
|
+
// src/escape.ts
|
|
50
|
+
var escapeAttr = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">").replace(/[\r\n]+/g, " ");
|
|
51
|
+
|
|
52
|
+
// src/mcp.ts
|
|
48
53
|
var MODEL_IMAGE_MAX = 1600;
|
|
49
54
|
var MAX_IMAGE_LINK_DATA = 16e3;
|
|
50
55
|
var DEFAULT_BASE = "https://tsquare.dev";
|
|
51
56
|
var MCP_INSTRUCTIONS = "tsquare draws low-fidelity UI wireframes from a small text language: multi-screen boards with notes and flow arrows, grayscale by default. Call wireframe_guide once before writing your first wireframe, then render_wireframe to check and see it, and share_wireframe to give the user links.";
|
|
52
57
|
var textInput = z.string().max(MAX_SHARED_TEXT).describe("The wireframe in tsquare text (a ```tsquare code fence around it is fine).");
|
|
53
58
|
var flowsInput = z.boolean().optional().describe("Draw flow arrows (default true). false leaves them out, as if there were no flow lines.");
|
|
59
|
+
var LINK_KINDS = ["share", "svg", "png", "markdown", "html"];
|
|
60
|
+
var DEFAULT_LINKS = ["svg", "share"];
|
|
61
|
+
var LINK_LABELS = {
|
|
62
|
+
share: "Share link (opens it in the playground to view and edit)",
|
|
63
|
+
svg: "Image link (SVG)",
|
|
64
|
+
png: "Image link (PNG)",
|
|
65
|
+
markdown: "Markdown",
|
|
66
|
+
html: "HTML"
|
|
67
|
+
};
|
|
68
|
+
var linksInput = z.array(z.enum(LINK_KINDS)).min(1).optional().describe(
|
|
69
|
+
"Which links to return; ask only for what the user needs, since each link is long. share: open and edit it in the playground. svg: an image link to view or embed (Notion, GitHub, docs). png: an image link for tools that don't show SVG. markdown: an image for READMEs and docs. html: an <img> tag for web pages. Default [svg, share]."
|
|
70
|
+
);
|
|
54
71
|
var GUIDE_HEADER = `# Writing tsquare wireframes
|
|
55
72
|
|
|
56
73
|
Workflow:
|
|
57
74
|
1. Write the wireframe text for the user's request, following the language below.
|
|
58
75
|
2. Call render_wireframe with it. If it reports problems, fix every line it names and render again.
|
|
59
76
|
3. Look at the image for what the checker can't see: content cut off at the bottom of a screen (make the screen taller with height=\u2026 or remove content), cramped rows, text that wraps badly. Fix and render again.
|
|
60
|
-
4. Call share_wireframe
|
|
77
|
+
4. Call share_wireframe with only the links the user needs (links=[\u2026]; by default an image link and the share link to edit it), and give them to the user exactly as returned: each link carries the whole wireframe, so one changed character breaks it. Show the wireframe text too, in a \`\`\`tsquare code block, so they can keep it.
|
|
61
78
|
`;
|
|
62
79
|
var mcpTools = {
|
|
63
80
|
wireframe_guide: {
|
|
@@ -74,8 +91,8 @@ var mcpTools = {
|
|
|
74
91
|
},
|
|
75
92
|
share_wireframe: {
|
|
76
93
|
title: "Share a wireframe",
|
|
77
|
-
description: "Links for valid tsquare wireframe text
|
|
78
|
-
inputSchema: z.object({ text: textInput, flows: flowsInput }),
|
|
94
|
+
description: "Links for valid tsquare wireframe text, the same kinds as the playground's Copy link menu: a share link to edit it in the playground, image links (SVG, PNG), Markdown and HTML. Ask only for the links the user needs (default: an SVG image link and the share link). Each link contains the whole wireframe text (compressed), so anyone with a link can read it.",
|
|
95
|
+
inputSchema: z.object({ text: textInput, flows: flowsInput, links: linksInput }),
|
|
79
96
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
80
97
|
}
|
|
81
98
|
};
|
|
@@ -113,28 +130,28 @@ function shareWireframeTool(args, opts = {}) {
|
|
|
113
130
|
return { content: [text(`This wireframe is too long to share as a link (over ${MAX_SHARED_TEXT / 1e3} KB of text). Split it into smaller boards.`)], isError: true };
|
|
114
131
|
const data = encodeWireframe(bare);
|
|
115
132
|
const query = args.flows === false ? "?flows=0" : "";
|
|
116
|
-
const title = String(spec.elements[spec.root]?.props?.title ?? "Wireframe")
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
133
|
+
const title = String(spec.elements[spec.root]?.props?.title ?? "Wireframe");
|
|
134
|
+
const kinds = [...new Set(args.links?.length ? args.links : DEFAULT_LINKS)];
|
|
135
|
+
const share = `${base}/playground#${data}`;
|
|
136
|
+
const lines = [];
|
|
137
|
+
const notes = ["Give each link exactly as written: it carries the whole wireframe, so anyone with it can read the wireframe, and one changed character breaks it."];
|
|
138
|
+
if (data.length > MAX_IMAGE_LINK_DATA && kinds.some((k) => k !== "share")) {
|
|
139
|
+
if (!kinds.includes("share")) kinds.unshift("share");
|
|
140
|
+
notes.unshift("This wireframe is too long for an image link, so only the share link is included. To get image links, split it into smaller boards.");
|
|
141
|
+
}
|
|
122
142
|
const svg = `${base}/svg/${data}${query}`;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
136
|
-
]
|
|
137
|
-
};
|
|
143
|
+
for (const kind of kinds) {
|
|
144
|
+
if (kind !== "share" && data.length > MAX_IMAGE_LINK_DATA) continue;
|
|
145
|
+
const link = {
|
|
146
|
+
share,
|
|
147
|
+
svg,
|
|
148
|
+
png: `${base}/png/${data}${query}`,
|
|
149
|
+
markdown: `![${title.replace(/[[\]\n]/g, " ")}](${svg})`,
|
|
150
|
+
html: `<img src="${svg}" alt="${escapeAttr(title)}">`
|
|
151
|
+
}[kind];
|
|
152
|
+
lines.push(`${LINK_LABELS[kind]}: ${link}`);
|
|
153
|
+
}
|
|
154
|
+
return { content: [text([...lines, "", ...notes].join("\n"))] };
|
|
138
155
|
}
|
|
139
156
|
async function callMcpTool(name, args = {}, opts = {}) {
|
|
140
157
|
if (name === "wireframe_guide") return wireframeGuide();
|
|
@@ -145,7 +162,9 @@ async function callMcpTool(name, args = {}, opts = {}) {
|
|
|
145
162
|
return name === "render_wireframe" ? renderWireframeTool(parsed.data) : shareWireframeTool(parsed.data, opts);
|
|
146
163
|
}
|
|
147
164
|
export {
|
|
165
|
+
DEFAULT_LINKS,
|
|
148
166
|
DEVICES,
|
|
167
|
+
LINK_KINDS,
|
|
149
168
|
MAX_SHARED_TEXT,
|
|
150
169
|
MCP_INSTRUCTIONS,
|
|
151
170
|
RENAMED_PROPS,
|
package/dist/mcp.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export declare const MODEL_IMAGE_MAX = 1600;
|
|
|
30
30
|
export declare const MAX_IMAGE_LINK_DATA = 16000;
|
|
31
31
|
/** Short server instructions: what the tools are for and the order to use them in. */
|
|
32
32
|
export declare const MCP_INSTRUCTIONS: string;
|
|
33
|
+
/** The link kinds share_wireframe offers: the playground's Copy link menu, with the same names. */
|
|
34
|
+
export declare const LINK_KINDS: readonly ["share", "svg", "png", "markdown", "html"];
|
|
35
|
+
export type LinkKind = (typeof LINK_KINDS)[number];
|
|
36
|
+
/** What a plain "share it" gets: one image link and the link to edit it. */
|
|
37
|
+
export declare const DEFAULT_LINKS: LinkKind[];
|
|
33
38
|
export declare const mcpTools: {
|
|
34
39
|
readonly wireframe_guide: {
|
|
35
40
|
readonly title: "tsquare language guide";
|
|
@@ -54,10 +59,17 @@ export declare const mcpTools: {
|
|
|
54
59
|
};
|
|
55
60
|
readonly share_wireframe: {
|
|
56
61
|
readonly title: "Share a wireframe";
|
|
57
|
-
readonly description: "Links for valid tsquare wireframe text
|
|
62
|
+
readonly description: "Links for valid tsquare wireframe text, the same kinds as the playground's Copy link menu: a share link to edit it in the playground, image links (SVG, PNG), Markdown and HTML. Ask only for the links the user needs (default: an SVG image link and the share link). Each link contains the whole wireframe text (compressed), so anyone with a link can read it.";
|
|
58
63
|
readonly inputSchema: z.ZodObject<{
|
|
59
64
|
text: z.ZodString;
|
|
60
65
|
flows: z.ZodOptional<z.ZodBoolean>;
|
|
66
|
+
links: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
67
|
+
html: "html";
|
|
68
|
+
markdown: "markdown";
|
|
69
|
+
png: "png";
|
|
70
|
+
share: "share";
|
|
71
|
+
svg: "svg";
|
|
72
|
+
}>>>;
|
|
61
73
|
}, z.core.$strip>;
|
|
62
74
|
readonly annotations: {
|
|
63
75
|
readonly readOnlyHint: true;
|
|
@@ -77,6 +89,7 @@ export declare function renderWireframeTool(args: {
|
|
|
77
89
|
export declare function shareWireframeTool(args: {
|
|
78
90
|
text: string;
|
|
79
91
|
flows?: boolean;
|
|
92
|
+
links?: LinkKind[];
|
|
80
93
|
}, opts?: ToolOptions): ToolResult;
|
|
81
94
|
/** Run a tool by name, for servers that dispatch by name. Unknown names come back as an error result. */
|
|
82
95
|
export declare function callMcpTool(name: string, args?: Record<string, unknown>, opts?: ToolOptions): Promise<ToolResult>;
|
package/dist/playground/index.js
CHANGED