tsquare 0.7.1 → 0.9.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 +33 -0
- package/README.md +3 -3
- package/dist/chunks/chunk-GTWUMF6W.js +65 -0
- package/dist/chunks/{chunk-ZLVSCEWO.js → chunk-W3OU5IS6.js} +20 -8
- package/dist/cli.js +1 -1
- package/dist/icons.d.ts +14 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -1
- package/dist/playground/index.js +4 -60
- package/dist/playground/playground.js +18 -18
- package/dist/playground/viewer.d.ts +44 -0
- package/dist/playground/viewer.js +172 -0
- package/dist/prompt.d.ts +1 -1
- package/package.json +15 -4
- package/skill/tsquare/SKILL.md +4 -1
- package/skill/tsquare/reference.md +2 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The zoomable board: zoom, pan, pinch, and zoom to a screen. Browser code,
|
|
3
|
+
* published as `tsquare/viewer`: the playground page uses it, and so does the
|
|
4
|
+
* VS Code extension's preview. Each brings its own markup and controls and
|
|
5
|
+
* passes the elements in; the board comes from renderWireframeSvg + boardLayout.
|
|
6
|
+
*
|
|
7
|
+
* The viewer sets the styles its math depends on (see structuralStyles), so
|
|
8
|
+
* the host only needs to size the stage and optionally style the
|
|
9
|
+
* "panning", "animate" and "stale" classes it toggles on the stage.
|
|
10
|
+
*/
|
|
11
|
+
import type { BoardItem } from "../render.js";
|
|
12
|
+
export type { BoardItem };
|
|
13
|
+
export interface ViewerElements {
|
|
14
|
+
/**
|
|
15
|
+
* The visible area; give it a size. It gets these classes, for optional styling:
|
|
16
|
+
* "panning" while dragging (cursor: grabbing), "animate" while zooming to a
|
|
17
|
+
* screen or fitting (transition the canvas's transform), and "stale" from
|
|
18
|
+
* setStale (dim the image).
|
|
19
|
+
*/
|
|
20
|
+
stage: HTMLElement;
|
|
21
|
+
/** Inside the stage; holds the board image and is transformed. */
|
|
22
|
+
canvas: HTMLElement;
|
|
23
|
+
/** Where the screen chips go ("All", then one per screen). */
|
|
24
|
+
screens: HTMLElement;
|
|
25
|
+
/** Removed when the first board shows. */
|
|
26
|
+
empty?: HTMLElement | null;
|
|
27
|
+
zoomIn?: HTMLElement | null;
|
|
28
|
+
zoomOut?: HTMLElement | null;
|
|
29
|
+
/** Shows the zoom level; clicking it fits the board. */
|
|
30
|
+
zoomFit?: HTMLElement | null;
|
|
31
|
+
}
|
|
32
|
+
export interface Viewer {
|
|
33
|
+
show(svg: string, width: number, height: number, items: BoardItem[]): void;
|
|
34
|
+
refresh(animate?: boolean): void;
|
|
35
|
+
/** Dims the board, for when the text has problems and this is the last valid render. */
|
|
36
|
+
setStale(stale: boolean): void;
|
|
37
|
+
fit(): void;
|
|
38
|
+
zoomIn(): void;
|
|
39
|
+
zoomOut(): void;
|
|
40
|
+
}
|
|
41
|
+
export declare function createViewer(el: ViewerElements, opts?: {
|
|
42
|
+
onScreenZoom?: () => void;
|
|
43
|
+
onZoom?: (scale: number) => void;
|
|
44
|
+
}): Viewer;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/playground/viewer.ts
|
|
2
|
+
function structuralStyles(stage, canvas) {
|
|
3
|
+
if (getComputedStyle(stage).position === "static") stage.style.position = "relative";
|
|
4
|
+
stage.style.overflow = "hidden";
|
|
5
|
+
stage.style.touchAction = "none";
|
|
6
|
+
Object.assign(canvas.style, { position: "absolute", left: "0px", top: "0px", transformOrigin: "0 0" });
|
|
7
|
+
}
|
|
8
|
+
function createViewer(el, opts = {}) {
|
|
9
|
+
const { stage, canvas } = el;
|
|
10
|
+
structuralStyles(stage, canvas);
|
|
11
|
+
let img = null;
|
|
12
|
+
let size = { width: 0, height: 0 };
|
|
13
|
+
let items = [];
|
|
14
|
+
let scale = 1, x = 0, y = 0;
|
|
15
|
+
let mode = "fit";
|
|
16
|
+
const PAD = 16;
|
|
17
|
+
const label = () => {
|
|
18
|
+
if (el.zoomFit) el.zoomFit.textContent = `${Math.round(scale * 100)}%`;
|
|
19
|
+
opts.onZoom?.(scale);
|
|
20
|
+
};
|
|
21
|
+
const apply = (animate = false) => {
|
|
22
|
+
stage.classList.toggle("animate", animate);
|
|
23
|
+
canvas.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
|
|
24
|
+
label();
|
|
25
|
+
};
|
|
26
|
+
const fitRect = (r, animate) => {
|
|
27
|
+
const w = stage.clientWidth - PAD * 2, h = stage.clientHeight - PAD * 2;
|
|
28
|
+
if (w <= 0 || h <= 0 || !r.width) return;
|
|
29
|
+
scale = Math.min(w / r.width, h / r.height, 4);
|
|
30
|
+
x = PAD + (w - r.width * scale) / 2 - r.x * scale;
|
|
31
|
+
y = PAD + (h - r.height * scale) / 2 - r.y * scale;
|
|
32
|
+
apply(animate);
|
|
33
|
+
};
|
|
34
|
+
const markScreens = () => el.screens.querySelectorAll("button").forEach((b, i) => b.setAttribute("aria-pressed", String(i === 0 ? mode === "fit" : mode === i - 1)));
|
|
35
|
+
function refresh(animate = false) {
|
|
36
|
+
if (!size.width) return;
|
|
37
|
+
if (mode === "fit") fitRect({ x: 0, y: 0, ...size }, animate);
|
|
38
|
+
else if (typeof mode === "number" && items[mode]) fitRect(items[mode], animate);
|
|
39
|
+
else apply();
|
|
40
|
+
markScreens();
|
|
41
|
+
}
|
|
42
|
+
function zoomAt(factor, cx = stage.clientWidth / 2, cy = stage.clientHeight / 2) {
|
|
43
|
+
const next = Math.min(8, Math.max(0.05, scale * factor));
|
|
44
|
+
x = cx - (cx - x) * (next / scale);
|
|
45
|
+
y = cy - (cy - y) * (next / scale);
|
|
46
|
+
scale = next;
|
|
47
|
+
mode = "manual";
|
|
48
|
+
apply();
|
|
49
|
+
markScreens();
|
|
50
|
+
}
|
|
51
|
+
const fit = () => {
|
|
52
|
+
mode = "fit";
|
|
53
|
+
refresh(true);
|
|
54
|
+
};
|
|
55
|
+
function show(svg, width, height, list) {
|
|
56
|
+
const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" }));
|
|
57
|
+
if (!img) {
|
|
58
|
+
img = document.createElement("img");
|
|
59
|
+
img.alt = "Rendered wireframe";
|
|
60
|
+
img.draggable = false;
|
|
61
|
+
canvas.replaceChildren(img);
|
|
62
|
+
el.empty?.remove();
|
|
63
|
+
}
|
|
64
|
+
const old = img.src;
|
|
65
|
+
img.src = url;
|
|
66
|
+
if (old.startsWith("blob:")) setTimeout(() => URL.revokeObjectURL(old), 1e3);
|
|
67
|
+
const resized = width !== size.width || height !== size.height;
|
|
68
|
+
size = { width, height };
|
|
69
|
+
img.width = width;
|
|
70
|
+
img.height = height;
|
|
71
|
+
const screens = list.filter((i) => i.type === "Screen");
|
|
72
|
+
if (screens.map((s) => s.name).join("\n") !== items.filter((i) => i.type === "Screen").map((s) => s.name).join("\n")) {
|
|
73
|
+
if (typeof mode === "number") mode = "fit";
|
|
74
|
+
buildScreenChips(screens);
|
|
75
|
+
}
|
|
76
|
+
items = screens;
|
|
77
|
+
if (resized && mode === "manual") mode = "fit";
|
|
78
|
+
refresh();
|
|
79
|
+
}
|
|
80
|
+
function buildScreenChips(screens) {
|
|
81
|
+
const bar = el.screens;
|
|
82
|
+
bar.replaceChildren();
|
|
83
|
+
if (screens.length < 2) return;
|
|
84
|
+
const chip = (text, onClick) => {
|
|
85
|
+
const b = document.createElement("button");
|
|
86
|
+
b.type = "button";
|
|
87
|
+
b.textContent = text;
|
|
88
|
+
b.addEventListener("click", onClick);
|
|
89
|
+
bar.appendChild(b);
|
|
90
|
+
};
|
|
91
|
+
chip("All", fit);
|
|
92
|
+
screens.forEach((s, i) => chip(s.name || `Screen ${i + 1}`, () => {
|
|
93
|
+
mode = i;
|
|
94
|
+
refresh(true);
|
|
95
|
+
opts.onScreenZoom?.();
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
stage.addEventListener("wheel", (e) => {
|
|
99
|
+
if (!size.width) return;
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
const r = stage.getBoundingClientRect();
|
|
102
|
+
if (e.ctrlKey || e.metaKey) zoomAt(Math.exp(-e.deltaY * 0.01), e.clientX - r.left, e.clientY - r.top);
|
|
103
|
+
else {
|
|
104
|
+
x -= e.deltaX;
|
|
105
|
+
y -= e.deltaY;
|
|
106
|
+
mode = "manual";
|
|
107
|
+
apply();
|
|
108
|
+
markScreens();
|
|
109
|
+
}
|
|
110
|
+
}, { passive: false });
|
|
111
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
112
|
+
let pinch = null;
|
|
113
|
+
const twoFingers = () => {
|
|
114
|
+
const [a, b] = [...pointers.values()];
|
|
115
|
+
return { mid: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, dist: Math.hypot(a.x - b.x, a.y - b.y) };
|
|
116
|
+
};
|
|
117
|
+
stage.addEventListener("pointerdown", (e) => {
|
|
118
|
+
stage.setPointerCapture(e.pointerId);
|
|
119
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
120
|
+
stage.classList.add("panning");
|
|
121
|
+
pinch = pointers.size === 2 ? twoFingers() : null;
|
|
122
|
+
});
|
|
123
|
+
stage.addEventListener("pointermove", (e) => {
|
|
124
|
+
const prev = pointers.get(e.pointerId);
|
|
125
|
+
if (!prev) return;
|
|
126
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
127
|
+
if (pointers.size === 2 && pinch) {
|
|
128
|
+
const now = twoFingers();
|
|
129
|
+
const r = stage.getBoundingClientRect();
|
|
130
|
+
x += now.mid.x - pinch.mid.x;
|
|
131
|
+
y += now.mid.y - pinch.mid.y;
|
|
132
|
+
if (pinch.dist && now.dist) zoomAt(now.dist / pinch.dist, now.mid.x - r.left, now.mid.y - r.top);
|
|
133
|
+
else {
|
|
134
|
+
mode = "manual";
|
|
135
|
+
apply();
|
|
136
|
+
markScreens();
|
|
137
|
+
}
|
|
138
|
+
pinch = now;
|
|
139
|
+
} else {
|
|
140
|
+
x += e.clientX - prev.x;
|
|
141
|
+
y += e.clientY - prev.y;
|
|
142
|
+
mode = "manual";
|
|
143
|
+
apply();
|
|
144
|
+
markScreens();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const release = (e) => {
|
|
148
|
+
pointers.delete(e.pointerId);
|
|
149
|
+
pinch = pointers.size === 2 ? twoFingers() : null;
|
|
150
|
+
if (!pointers.size) stage.classList.remove("panning");
|
|
151
|
+
};
|
|
152
|
+
stage.addEventListener("pointerup", release);
|
|
153
|
+
stage.addEventListener("pointercancel", release);
|
|
154
|
+
stage.addEventListener("dblclick", fit);
|
|
155
|
+
new ResizeObserver(() => refresh()).observe(stage);
|
|
156
|
+
el.zoomIn?.addEventListener("click", () => zoomAt(1.25));
|
|
157
|
+
el.zoomOut?.addEventListener("click", () => zoomAt(0.8));
|
|
158
|
+
el.zoomFit?.addEventListener("click", fit);
|
|
159
|
+
return {
|
|
160
|
+
show,
|
|
161
|
+
refresh,
|
|
162
|
+
setStale: (stale) => {
|
|
163
|
+
stage.classList.toggle("stale", stale);
|
|
164
|
+
},
|
|
165
|
+
fit,
|
|
166
|
+
zoomIn: () => zoomAt(1.25),
|
|
167
|
+
zoomOut: () => zoomAt(0.8)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
createViewer
|
|
172
|
+
};
|
package/dist/prompt.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare function componentDocs(): {
|
|
|
13
13
|
props: PropDoc[];
|
|
14
14
|
}[];
|
|
15
15
|
export declare function componentReference(): string;
|
|
16
|
-
export declare const RULES = "## Rules\n1. The top element is a Board. The Board's children are Screens, plus optional Notes beside them and Flow lines after them (rule 10).\n2. Each Screen is one view of the product. Use several Screens to show several views or states.\n3. A Screen lays out its children top to bottom. Use Stack (direction row or column) and Grid to arrange content.\n4. NavBar is pinned to the top of its Screen and TabBar to the bottom.\n5. Modal, Drawer and Toast are overlays. They must be direct children of a Screen.\n6. This is a low-fidelity wireframe. Prefer placeholders (Image boxes, Text with lines) over invented copy unless the copy matters.\n7. Only use the components and props listed below. All props are optional unless marked required.\n8. 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.\n9. 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.\n10. To show how screens connect, name elements or screens by writing #name after them (button primary \"Sign in\" #signin, screen phone \"Home\" #home), then
|
|
16
|
+
export declare const RULES = "## Rules\n1. The top element is a Board. The Board's children are Screens, plus optional Notes beside them and Flow lines after them (rule 10).\n2. Each Screen is one view of the product. Use several Screens to show several views or states.\n3. A Screen lays out its children top to bottom. Use Stack (direction row or column) and Grid to arrange content.\n4. NavBar is pinned to the top of its Screen and TabBar to the bottom.\n5. Modal, Drawer and Toast are overlays. They must be direct children of a Screen.\n6. This is a low-fidelity wireframe. Prefer placeholders (Image boxes, Text with lines) over invented copy unless the copy matters.\n7. Only use the components and props listed below. All props are optional unless marked required.\n8. 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.\n9. 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.\n10. 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\".\n11. 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.";
|
|
17
17
|
export declare const TEXT_FORMAT = "## Output format: wireframe text\nOne element per line. Indent children two spaces under their parent. The first line is the board.\n\nA line is the component name in lowercase, followed by arguments separated by spaces:\n- \"a quoted string\" sets the component's main text prop (marked \"main text\" below)\n- 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\n- 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\n- key=value sets any prop. Values: \"string\", number, true/false, bare word, [list, of, values], {key=value key=value}\n\nComments: 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.\n\nLists: 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].\n\nIf a bare word could mean more than one prop, write it as key=value.";
|
|
18
18
|
export declare const EXAMPLE_REQUEST = "Two phone screens for a recipe app: a browse screen with search, category tabs, a grid of recipe cards and a tab bar; and a recipe detail screen with its options sheet open.";
|
|
19
19
|
/** The system prompt for writing wireframe text. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsquare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Wireframes in plain text: easy for LLMs to write, fast to render as SVG or PNG.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wireframe",
|
|
@@ -26,11 +26,18 @@
|
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
|
-
"import": "./dist/index.js"
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"default": "./dist/index.js"
|
|
30
31
|
},
|
|
31
32
|
"./playground": {
|
|
32
33
|
"types": "./dist/playground/index.d.ts",
|
|
33
|
-
"import": "./dist/playground/index.js"
|
|
34
|
+
"import": "./dist/playground/index.js",
|
|
35
|
+
"default": "./dist/playground/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./viewer": {
|
|
38
|
+
"types": "./dist/playground/viewer.d.ts",
|
|
39
|
+
"import": "./dist/playground/viewer.js",
|
|
40
|
+
"default": "./dist/playground/viewer.js"
|
|
34
41
|
},
|
|
35
42
|
"./package.json": "./package.json"
|
|
36
43
|
},
|
|
@@ -75,6 +82,7 @@
|
|
|
75
82
|
"@types/react": "^19.3.0",
|
|
76
83
|
"lucide": "1.48.0",
|
|
77
84
|
"react": "^19.3.0",
|
|
85
|
+
"simple-icons": "16.34.0",
|
|
78
86
|
"zod": "^4.6.5"
|
|
79
87
|
},
|
|
80
88
|
"devDependencies": {
|
|
@@ -86,9 +94,12 @@
|
|
|
86
94
|
"@codemirror/state": "^6.7.6",
|
|
87
95
|
"@codemirror/view": "^6.43.13",
|
|
88
96
|
"@lezer/highlight": "^1.2.5",
|
|
97
|
+
"@types/jsdom": "^30.0.0",
|
|
89
98
|
"esbuild": "^0.28.2",
|
|
90
99
|
"gpt-tokenizer": "^4.0.0",
|
|
100
|
+
"jsdom": "^29.1.1",
|
|
91
101
|
"tsx": "^4.23.15",
|
|
92
102
|
"typescript": "^7.0.2"
|
|
93
|
-
}
|
|
103
|
+
},
|
|
104
|
+
"gitHead": "c248211e0c39260cb4cbd59008fb1d64b54b3f08"
|
|
94
105
|
}
|
package/skill/tsquare/SKILL.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tsquare
|
|
3
3
|
description: Create low-fidelity UI wireframes and mockups (app screens, several devices side by side, boards for specs and docs) as tsquare text, then render them to PNG or SVG with the tsquare CLI. Use when asked to wireframe, mock up or sketch a UI screen, page or flow, or to add a UI mockup to a document.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Needs Node.js 20 or newer to run the tsquare CLI (installed, or through npx).
|
|
4
6
|
---
|
|
5
7
|
|
|
6
8
|
# tsquare wireframes
|
|
@@ -52,7 +54,8 @@ If a bare word could mean more than one prop, write it as key=value.
|
|
|
52
54
|
7. Only use the components and props listed below. All props are optional unless marked required.
|
|
53
55
|
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.
|
|
54
56
|
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.
|
|
55
|
-
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
|
|
57
|
+
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".
|
|
58
|
+
11. Icons are Lucide names, plus brand logos named brand-…: 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.
|
|
56
59
|
|
|
57
60
|
## Example
|
|
58
61
|
|
|
@@ -28,7 +28,7 @@ Sticky-note annotation. Put it on the Board beside screens, or inside a Screen n
|
|
|
28
28
|
- width: number
|
|
29
29
|
|
|
30
30
|
### Flow
|
|
31
|
-
An arrow between two elements or screens, named with #id.
|
|
31
|
+
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".
|
|
32
32
|
- from (required): string — The id where the arrow starts (written before ->)
|
|
33
33
|
- to (required): string — The id it points at (written after ->)
|
|
34
34
|
- label: string (main text)
|
|
@@ -116,7 +116,7 @@ Chart placeholder: a generic line, bar, area, pie or donut shape, no data.
|
|
|
116
116
|
- width: number | string — Default fills the container width
|
|
117
117
|
|
|
118
118
|
### Icon
|
|
119
|
-
Line icon from Lucide.
|
|
119
|
+
Line icon from Lucide, or a brand logo (brand-google, brand-apple, …).
|
|
120
120
|
- name: string (main text) — Lucide icon name in kebab-case, e.g. menu, search, arrow-left, settings, bell, user
|
|
121
121
|
- size: number
|
|
122
122
|
|