tsquare 0.4.2 → 0.5.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/CHANGELOG.md +26 -0
- package/dist/anchors.d.ts +75 -0
- package/dist/catalog.d.ts +47 -0
- package/dist/chunks/{chunk-NREXWHAE.js → chunk-B6KIVOSH.js} +257 -21
- package/dist/cli.js +1 -1
- package/dist/components.d.ts +9 -1
- package/dist/index.js +1 -1
- package/dist/playground/index.js +36 -6
- package/dist/prompt.d.ts +1 -1
- package/dist/render.d.ts +13 -0
- package/package.json +2 -2
- package/skill/tsquare/SKILL.md +1 -0
- package/skill/tsquare/reference.md +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
Docs only: tooltips are easier to find.
|
|
6
|
+
- **Language guide:** the "Tooltips and open states" section starts with a rendered picture of a tooltip, an open select, an open date picker and a row menu. It's generated by `npm run docs`, so it always matches the renderer. The section also now says notes can't take tooltips.
|
|
7
|
+
- **Button page and the playground's Components tab:** a rendered tooltip example.
|
|
8
|
+
- **Components index:** a note that any element inside a screen can take `tooltip="…"`.
|
|
9
|
+
|
|
10
|
+
## 0.5.0
|
|
11
|
+
|
|
12
|
+
### Open states and tooltips
|
|
13
|
+
|
|
14
|
+
```tsquare
|
|
15
|
+
select "Country" value="Mexico" open options=[Canada, Mexico, United States]
|
|
16
|
+
input "Check-in" type=date value="Oct 14, 2026" open
|
|
17
|
+
button ghost leadingIcon=more-horizontal open menu=[{label=Rename icon=pencil}, Duplicate, Delete]
|
|
18
|
+
button secondary "Draft" tooltip="Saves without publishing"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **Open states:** a select's options list, a date input's calendar and a button's menu now draw over the screen, covering what's below, as they would in the app.
|
|
22
|
+
- Each opens below its element, or above when there isn't room, and stays inside its screen.
|
|
23
|
+
- The date picker opens on the value's month with that day selected.
|
|
24
|
+
- **Menus on list rows and the navbar too:** `listitem "Budget.xlsx" open menu=[Rename, Move, Delete]` opens right-aligned under the row; a row with nothing at its end shows … for it. The navbar's menu opens under a ⋮, added after its actions unless the last one already is a ⋯/⋮ icon.
|
|
25
|
+
- **Tooltips:** `tooltip="…"` works on any element except a board or screen. It sits above the element (below it near the top of the screen), with its arrow pointing at the element.
|
|
26
|
+
- **Errors** when an open state has nothing to show: `open` without `options` or `menu`, or `open` on an input that isn't `type=date`.
|
|
27
|
+
- **How it works:** a board with any of these renders twice. The first pass measures where elements landed, and the second draws the overlays on top. Boards without them render once, exactly as before.
|
|
28
|
+
|
|
3
29
|
## 0.4.2
|
|
4
30
|
|
|
5
31
|
- **Toast corners:** `position` also takes `topLeft`, `topRight`, `bottomLeft` and `bottomRight`, besides `top` and `bottom` (centered, the default). Desktop apps often show toasts in a corner.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchored overlays: things drawn over the board at an element's position (an open
|
|
3
|
+
* select's options, an open date picker, a button's menu, tooltips).
|
|
4
|
+
*
|
|
5
|
+
* Satori lays out and draws in one go without reporting positions, so a board that
|
|
6
|
+
* has any of these renders twice:
|
|
7
|
+
* 1. Measuring pass: each anchored element (and its screen) gets a marker, a box in a
|
|
8
|
+
* unique color filling it. Satori's SVG uses absolute coordinates, so reading the
|
|
9
|
+
* markers' rects back gives every box on the board.
|
|
10
|
+
* 2. Final pass: the board as usual, plus a top layer on the Board with each overlay
|
|
11
|
+
* placed from those boxes, clipped to its screen, so it covers the content below.
|
|
12
|
+
* Boards without anchored elements render once, exactly as before.
|
|
13
|
+
*
|
|
14
|
+
* Precision: a marker fills its box inside the box's border, so bordered elements measure up to
|
|
15
|
+
* 1.5px in from their outer edge. That's invisible in the output, so overlays don't correct for it.
|
|
16
|
+
*/
|
|
17
|
+
import { type ReactNode } from "react";
|
|
18
|
+
import type { Spec } from "@json-render/core";
|
|
19
|
+
import type { Palette } from "./colors.js";
|
|
20
|
+
export interface Rect {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
w: number;
|
|
24
|
+
h: number;
|
|
25
|
+
}
|
|
26
|
+
export type AnchorKind = "tooltip" | "options" | "calendar" | "menu";
|
|
27
|
+
/** `end`: the menu opens from the element's right end (a list row's …, a navbar's ⋮). */
|
|
28
|
+
export interface Anchor {
|
|
29
|
+
id: string;
|
|
30
|
+
kind: AnchorKind;
|
|
31
|
+
screen: string;
|
|
32
|
+
field: boolean;
|
|
33
|
+
end?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Every anchored overlay on the board, in drawing order (document order; tooltips last). */
|
|
36
|
+
export declare function findAnchors(spec: Spec): Anchor[];
|
|
37
|
+
/**
|
|
38
|
+
* A copy of the spec whose anchored elements and screens carry marker colors in hidden props:
|
|
39
|
+
* `__mark` on the outer box (menus, tooltips, screens), `__markField` on a field (options, calendar).
|
|
40
|
+
* Box keys are the element id, or `<id>#field`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function tagForMeasuring(spec: Spec, anchors: Anchor[]): {
|
|
43
|
+
tagged: Spec;
|
|
44
|
+
colors: Map<string, string>;
|
|
45
|
+
};
|
|
46
|
+
/** The boxes of the markers in a measuring-pass SVG, by the ids they were tagged with. */
|
|
47
|
+
export declare function readMarkers(svg: string, colors: Map<string, string>): Record<string, Rect>;
|
|
48
|
+
/**
|
|
49
|
+
* Where an overlay of `size` goes next to `anchor`, inside `screen`: below (or above, for
|
|
50
|
+
* tooltips) when it fits, otherwise the side with more room; horizontally aligned as asked,
|
|
51
|
+
* then kept inside the screen. Coordinates are relative to the screen.
|
|
52
|
+
*/
|
|
53
|
+
export declare function place(anchor: Rect, size: {
|
|
54
|
+
w: number;
|
|
55
|
+
h: number;
|
|
56
|
+
}, screen: Rect, opts: {
|
|
57
|
+
prefer: "below" | "above";
|
|
58
|
+
align: "start" | "center" | "end";
|
|
59
|
+
gap: number;
|
|
60
|
+
}): {
|
|
61
|
+
x: number;
|
|
62
|
+
y: number;
|
|
63
|
+
bottom: number;
|
|
64
|
+
side: "above" | "below";
|
|
65
|
+
};
|
|
66
|
+
/** "Oct 14, 2026", "October 14 2026", "14 Oct 2026", "2026-10-14" → the month for the calendar and the day. */
|
|
67
|
+
export declare function parseDate(value: unknown): {
|
|
68
|
+
month: string;
|
|
69
|
+
day?: number;
|
|
70
|
+
} | null;
|
|
71
|
+
/**
|
|
72
|
+
* The top layer: each overlay inside a box covering its screen (clipped to it), placed from the
|
|
73
|
+
* measured boxes. `Calendar` is the board's own calendar component, reused for date pickers.
|
|
74
|
+
*/
|
|
75
|
+
export declare function overlayLayer(spec: Spec, anchors: Anchor[], boxes: Record<string, Rect>, c: Palette, Calendar: (props: any) => ReactNode): ReactNode;
|
package/dist/catalog.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Props every element except Board and Screen accepts, on top of its own. Kept out of each
|
|
4
|
+
* component's schema so the prompt describes them once.
|
|
5
|
+
*/
|
|
6
|
+
export declare const UNIVERSAL_PROPS: {
|
|
7
|
+
readonly tooltip: z.ZodString;
|
|
8
|
+
};
|
|
9
|
+
/** Not the canvas (Board, Screen) or its annotations (Note): a tooltip there would be meaningless. */
|
|
10
|
+
export declare const takesUniversalProps: (type: string) => boolean;
|
|
11
|
+
/** Why a universal prop isn't allowed here, and what to do instead. */
|
|
12
|
+
export declare function universalPropMessage(type: string, key: string): string;
|
|
2
13
|
export declare const componentDefinitions: {
|
|
3
14
|
Board: {
|
|
4
15
|
props: z.ZodObject<{
|
|
@@ -319,6 +330,11 @@ export declare const componentDefinitions: {
|
|
|
319
330
|
leadingIcon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
320
331
|
trailingIcon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
321
332
|
fullWidth: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
333
|
+
menu: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
334
|
+
label: z.ZodString;
|
|
335
|
+
icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
336
|
+
}, z.core.$strict>]>>>>;
|
|
337
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
322
338
|
}, z.core.$strip>;
|
|
323
339
|
slots: never[];
|
|
324
340
|
description: string;
|
|
@@ -342,6 +358,7 @@ export declare const componentDefinitions: {
|
|
|
342
358
|
text: "text";
|
|
343
359
|
}>>>;
|
|
344
360
|
digits: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
361
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
345
362
|
multiline: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
346
363
|
helper: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
347
364
|
error: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
@@ -396,6 +413,8 @@ export declare const componentDefinitions: {
|
|
|
396
413
|
label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
397
414
|
value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
398
415
|
placeholder: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
416
|
+
options: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
417
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
399
418
|
grow: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
400
419
|
width: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
|
|
401
420
|
}, z.core.$strip>;
|
|
@@ -466,6 +485,11 @@ export declare const componentDefinitions: {
|
|
|
466
485
|
center: "center";
|
|
467
486
|
left: "left";
|
|
468
487
|
}>>>;
|
|
488
|
+
menu: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
489
|
+
label: z.ZodString;
|
|
490
|
+
icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
491
|
+
}, z.core.$strict>]>>>>;
|
|
492
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
469
493
|
}, z.core.$strip>;
|
|
470
494
|
slots: never[];
|
|
471
495
|
description: string;
|
|
@@ -550,6 +574,11 @@ export declare const componentDefinitions: {
|
|
|
550
574
|
}>>>;
|
|
551
575
|
trailingText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
552
576
|
trailingIcon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
577
|
+
menu: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
578
|
+
label: z.ZodString;
|
|
579
|
+
icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
580
|
+
}, z.core.$strict>]>>>>;
|
|
581
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
553
582
|
}, z.core.$strip>;
|
|
554
583
|
slots: never[];
|
|
555
584
|
description: string;
|
|
@@ -976,6 +1005,11 @@ export declare const catalog: import("@json-render/core").Catalog<{
|
|
|
976
1005
|
leadingIcon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
977
1006
|
trailingIcon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
978
1007
|
fullWidth: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
1008
|
+
menu: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
1009
|
+
label: z.ZodString;
|
|
1010
|
+
icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1011
|
+
}, z.core.$strict>]>>>>;
|
|
1012
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
979
1013
|
}, z.core.$strip>;
|
|
980
1014
|
slots: never[];
|
|
981
1015
|
description: string;
|
|
@@ -999,6 +1033,7 @@ export declare const catalog: import("@json-render/core").Catalog<{
|
|
|
999
1033
|
text: "text";
|
|
1000
1034
|
}>>>;
|
|
1001
1035
|
digits: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
1036
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
1002
1037
|
multiline: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
1003
1038
|
helper: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1004
1039
|
error: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
@@ -1053,6 +1088,8 @@ export declare const catalog: import("@json-render/core").Catalog<{
|
|
|
1053
1088
|
label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1054
1089
|
value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1055
1090
|
placeholder: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1091
|
+
options: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
1092
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
1056
1093
|
grow: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
1057
1094
|
width: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
|
|
1058
1095
|
}, z.core.$strip>;
|
|
@@ -1123,6 +1160,11 @@ export declare const catalog: import("@json-render/core").Catalog<{
|
|
|
1123
1160
|
center: "center";
|
|
1124
1161
|
left: "left";
|
|
1125
1162
|
}>>>;
|
|
1163
|
+
menu: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
1164
|
+
label: z.ZodString;
|
|
1165
|
+
icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1166
|
+
}, z.core.$strict>]>>>>;
|
|
1167
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
1126
1168
|
}, z.core.$strip>;
|
|
1127
1169
|
slots: never[];
|
|
1128
1170
|
description: string;
|
|
@@ -1207,6 +1249,11 @@ export declare const catalog: import("@json-render/core").Catalog<{
|
|
|
1207
1249
|
}>>>;
|
|
1208
1250
|
trailingText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1209
1251
|
trailingIcon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1252
|
+
menu: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
1253
|
+
label: z.ZodString;
|
|
1254
|
+
icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1255
|
+
}, z.core.$strict>]>>>>;
|
|
1256
|
+
open: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
1210
1257
|
}, z.core.$strip>;
|
|
1211
1258
|
slots: never[];
|
|
1212
1259
|
description: string;
|
|
@@ -209,6 +209,7 @@ var wireframeSchema = defineSchema(
|
|
|
209
209
|
"Screens lay out their children top to bottom. Use Stack (direction row/column) and Grid inside a Screen to arrange content.",
|
|
210
210
|
"Modal, Drawer and Toast are overlays. They MUST be direct children of a Screen, and SHOULD be listed last in the Screen's children so they draw on top.",
|
|
211
211
|
"NavBar goes first in a Screen's children and TabBar goes last (before any overlay).",
|
|
212
|
+
"Any element except Board, Screen and Note can take a tooltip prop (text) to show a tooltip next to it. Select (with options), Input with type date, and Button, ListItem or NavBar (with menu) take open to show their list, calendar or menu over the screen.",
|
|
212
213
|
"This is a low-fidelity wireframe: prefer placeholders (Image boxes, Text with lines) over invented copy, unless the copy matters to the design.",
|
|
213
214
|
'Every element MUST include a "children" array. Leaf elements use an empty array: "children": [].',
|
|
214
215
|
"Every id listed in a children array MUST exist as its own element."
|
|
@@ -223,6 +224,15 @@ var justify = z.enum(["start", "center", "end", "between", "around"]);
|
|
|
223
224
|
var iconName = z.string().refine(isIcon, { error: (iss) => unknownIconMessage(String(iss.input)) }).describe("Lucide icon name in kebab-case, e.g. menu, search, arrow-left, settings, bell, user");
|
|
224
225
|
var day = z.number().int().min(1).max(31);
|
|
225
226
|
var percent = z.number().min(0).max(100);
|
|
227
|
+
var menuItem = z.union([z.string(), z.strictObject({ label: z.string(), icon: o(iconName) })]);
|
|
228
|
+
var UNIVERSAL_PROPS = {
|
|
229
|
+
tooltip: z.string().describe("Show a tooltip with this text next to the element")
|
|
230
|
+
};
|
|
231
|
+
var takesUniversalProps = (type) => type !== "Board" && type !== "Screen" && type !== "Note";
|
|
232
|
+
function universalPropMessage(type, key) {
|
|
233
|
+
const base = `${key}s go on elements inside a screen`;
|
|
234
|
+
return type === "Note" ? `${base}; a note is already an annotation, so put the text in the note` : `${base}, not on the ${type.toLowerCase()} itself`;
|
|
235
|
+
}
|
|
226
236
|
var bulletItem = z.union([
|
|
227
237
|
z.string(),
|
|
228
238
|
// strict: a misspelled key is an error, not silently dropped
|
|
@@ -426,10 +436,12 @@ var componentDefinitions = {
|
|
|
426
436
|
size: o(z.enum(["sm", "md", "lg"])),
|
|
427
437
|
leadingIcon: o(iconName).describe("Lucide icon before the label, e.g. chevron-left for Back"),
|
|
428
438
|
trailingIcon: o(iconName).describe("Lucide icon after the label, e.g. chevron-right for Next"),
|
|
429
|
-
fullWidth: o(z.boolean())
|
|
439
|
+
fullWidth: o(z.boolean()),
|
|
440
|
+
menu: o(z.array(menuItem)).describe("Dropdown menu items, shown below the button when open"),
|
|
441
|
+
open: o(z.boolean()).describe("Show the menu")
|
|
430
442
|
}),
|
|
431
443
|
slots: [],
|
|
432
|
-
description: "Button. Primary is filled, secondary is outlined, ghost is text only.",
|
|
444
|
+
description: "Button. Primary is filled, secondary is outlined, ghost is text only. With menu and open, a dropdown menu.",
|
|
433
445
|
example: { label: "Continue", variant: "primary", fullWidth: true }
|
|
434
446
|
},
|
|
435
447
|
Input: {
|
|
@@ -439,6 +451,7 @@ var componentDefinitions = {
|
|
|
439
451
|
value: o(z.string()),
|
|
440
452
|
type: o(z.enum(["text", "password", "search", "email", "date", "code"])).describe("date shows a calendar icon; code draws one box per digit"),
|
|
441
453
|
digits: o(z.number().int().min(2).max(10)).describe("Boxes for type=code, default 6"),
|
|
454
|
+
open: o(z.boolean()).describe("type=date: show the calendar below the field"),
|
|
442
455
|
multiline: o(z.number().int().min(1)).describe("Number of rows; 2 or more makes a textarea"),
|
|
443
456
|
helper: o(z.string()),
|
|
444
457
|
error: o(z.boolean()).describe("Validation error: red border and red helper text"),
|
|
@@ -472,11 +485,13 @@ var componentDefinitions = {
|
|
|
472
485
|
label: o(z.string()),
|
|
473
486
|
value: o(z.string()),
|
|
474
487
|
placeholder: o(z.string()),
|
|
488
|
+
options: o(z.array(z.string())).describe("The choices, shown below the field when open"),
|
|
489
|
+
open: o(z.boolean()).describe("Show the options list"),
|
|
475
490
|
grow: o(z.boolean()).describe("Fill the remaining space in a row"),
|
|
476
491
|
width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 320. Default fills the space.")
|
|
477
492
|
}),
|
|
478
493
|
slots: [],
|
|
479
|
-
description: "Dropdown field
|
|
494
|
+
description: "Dropdown field. With options and open, the list shows below it.",
|
|
480
495
|
example: { label: "Country", value: "United States" }
|
|
481
496
|
},
|
|
482
497
|
Slider: {
|
|
@@ -518,7 +533,9 @@ var componentDefinitions = {
|
|
|
518
533
|
title: o(z.string()),
|
|
519
534
|
leading: o(z.enum(["none", "menu", "back", "close", "logo"])),
|
|
520
535
|
actions: o(z.array(iconName)).describe("Icon names shown on the right"),
|
|
521
|
-
align: o(z.enum(["left", "center"]))
|
|
536
|
+
align: o(z.enum(["left", "center"])),
|
|
537
|
+
menu: o(z.array(menuItem)).describe("A menu under the last action (adds \u22EE if needed), shown when open"),
|
|
538
|
+
open: o(z.boolean()).describe("Show the menu")
|
|
522
539
|
}),
|
|
523
540
|
slots: [],
|
|
524
541
|
description: "Top app bar. Place first in a Screen.",
|
|
@@ -565,7 +582,9 @@ var componentDefinitions = {
|
|
|
565
582
|
leadingIcon: o(iconName).describe("Lucide icon on the left; setting it implies leading=icon"),
|
|
566
583
|
trailing: o(z.enum(["none", "chevron", "toggle", "text", "badge", "icon"])),
|
|
567
584
|
trailingText: o(z.string()).describe("Text for trailing=text or badge; setting it implies trailing=text"),
|
|
568
|
-
trailingIcon: o(iconName).describe("Lucide icon on the right; setting it implies trailing=icon")
|
|
585
|
+
trailingIcon: o(iconName).describe("Lucide icon on the right; setting it implies trailing=icon"),
|
|
586
|
+
menu: o(z.array(menuItem)).describe("A menu for the row (shows \u2026 on the right if nothing else is there), shown when open"),
|
|
587
|
+
open: o(z.boolean()).describe("Show the menu")
|
|
569
588
|
}),
|
|
570
589
|
slots: [],
|
|
571
590
|
description: "Row in a List. Omit title for a placeholder bar.",
|
|
@@ -648,6 +667,38 @@ function Box({ style, children }) {
|
|
|
648
667
|
for (const [k, v] of Object.entries(style ?? {})) if (v !== void 0 && v !== null) clean[k] = v;
|
|
649
668
|
return /* @__PURE__ */ jsx("div", { style: clean, children });
|
|
650
669
|
}
|
|
670
|
+
function Marker({ color }) {
|
|
671
|
+
return /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", backgroundColor: color } });
|
|
672
|
+
}
|
|
673
|
+
function markRoot(root, color) {
|
|
674
|
+
let node = root;
|
|
675
|
+
while (isValidElement(node) && typeof node.type === "function" && node.type !== Box) node = node.type(node.props);
|
|
676
|
+
if (!isValidElement(node)) return root;
|
|
677
|
+
const kids = Children.toArray(node.props.children);
|
|
678
|
+
return cloneElement(node, {}, ...kids, /* @__PURE__ */ jsx(Marker, { color }, "__marker"));
|
|
679
|
+
}
|
|
680
|
+
function withMarkers(reg) {
|
|
681
|
+
return Object.fromEntries(
|
|
682
|
+
Object.entries(reg).map(([name, C]) => [
|
|
683
|
+
name,
|
|
684
|
+
(props) => {
|
|
685
|
+
const root = C(props);
|
|
686
|
+
const color = props.element?.props?.__mark;
|
|
687
|
+
return color && name !== "Screen" ? markRoot(root, color) : root;
|
|
688
|
+
}
|
|
689
|
+
])
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
function withTopLayer(reg, layer) {
|
|
693
|
+
return {
|
|
694
|
+
...reg,
|
|
695
|
+
Board: (props) => {
|
|
696
|
+
const root = reg.Board(props);
|
|
697
|
+
if (!isValidElement(root)) return root;
|
|
698
|
+
return cloneElement(root, {}, ...Children.toArray(root.props.children), layer);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
651
702
|
function withDir(children, dir, stretch = false, aligned = false) {
|
|
652
703
|
return Children.toArray(children).map((c) => isValidElement(c) ? cloneElement(c, { dir, stretch, aligned }) : c);
|
|
653
704
|
}
|
|
@@ -803,7 +854,8 @@ function Screen({ element, children }) {
|
|
|
803
854
|
}
|
|
804
855
|
),
|
|
805
856
|
bottom,
|
|
806
|
-
overlays.map((o2) => isValidElement(o2) ? cloneElement(o2, { aboveTabBar: bottom.length > 0 }) : o2)
|
|
857
|
+
overlays.map((o2) => isValidElement(o2) ? cloneElement(o2, { aboveTabBar: bottom.length > 0 }) : o2),
|
|
858
|
+
p.__mark ? /* @__PURE__ */ jsx(Marker, { color: p.__mark }) : null
|
|
807
859
|
] })
|
|
808
860
|
]
|
|
809
861
|
}
|
|
@@ -1188,8 +1240,8 @@ function fieldWidth(width, dir) {
|
|
|
1188
1240
|
function FieldLabel({ text }) {
|
|
1189
1241
|
return text ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 13, fontWeight: 600, color: theme.text }, children: text }) : null;
|
|
1190
1242
|
}
|
|
1191
|
-
function Field({ children, height = 42, top = false, error = false }) {
|
|
1192
|
-
return /* @__PURE__ */
|
|
1243
|
+
function Field({ children, height = 42, top = false, error = false, mark }) {
|
|
1244
|
+
return /* @__PURE__ */ jsxs(
|
|
1193
1245
|
Box,
|
|
1194
1246
|
{
|
|
1195
1247
|
style: {
|
|
@@ -1202,7 +1254,10 @@ function Field({ children, height = 42, top = false, error = false }) {
|
|
|
1202
1254
|
backgroundColor: theme.paper,
|
|
1203
1255
|
fontSize: 15
|
|
1204
1256
|
},
|
|
1205
|
-
children
|
|
1257
|
+
children: [
|
|
1258
|
+
children,
|
|
1259
|
+
mark ? /* @__PURE__ */ jsx(Marker, { color: mark }) : null
|
|
1260
|
+
]
|
|
1206
1261
|
}
|
|
1207
1262
|
);
|
|
1208
1263
|
}
|
|
@@ -1241,7 +1296,7 @@ function Input({ element, dir, colors }) {
|
|
|
1241
1296
|
const content = isPassword && shown ? "\u2022".repeat(Math.min(shown.length, 12)) : shown;
|
|
1242
1297
|
return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 6, ...fieldWidth(p.width, dir), ...p.grow ? { flexGrow: 1 } : {} }, children: [
|
|
1243
1298
|
/* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
|
|
1244
|
-
/* @__PURE__ */ jsxs(Field, { height: rows > 1 ? rows * 22 + 20 : 42, top: rows > 1, error: !!p.error, children: [
|
|
1299
|
+
/* @__PURE__ */ jsxs(Field, { height: rows > 1 ? rows * 22 + 20 : 42, top: rows > 1, error: !!p.error, mark: p.__markField, children: [
|
|
1245
1300
|
p.type === "search" ? /* @__PURE__ */ jsx(IconGlyph, { name: "search", size: 18, color: theme.muted }) : null,
|
|
1246
1301
|
/* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, color: content ? theme.ink : theme.muted }, children: content ?? p.placeholder ?? (isPassword ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : "") }),
|
|
1247
1302
|
isPassword ? /* @__PURE__ */ jsx(IconGlyph, { name: "eye-off", size: 18, color: theme.muted }) : null,
|
|
@@ -1254,7 +1309,7 @@ function Select({ element, dir }) {
|
|
|
1254
1309
|
const p = element.props;
|
|
1255
1310
|
return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 6, ...fieldWidth(p.width, dir), ...p.grow ? { flexGrow: 1 } : {} }, children: [
|
|
1256
1311
|
/* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
|
|
1257
|
-
/* @__PURE__ */ jsxs(Field, { children: [
|
|
1312
|
+
/* @__PURE__ */ jsxs(Field, { mark: p.__markField, children: [
|
|
1258
1313
|
/* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, color: p.value ? theme.ink : theme.muted }, children: p.value ?? p.placeholder ?? "Select\u2026" }),
|
|
1259
1314
|
/* @__PURE__ */ jsx(IconGlyph, { name: "chevron-down", size: 18, color: theme.text })
|
|
1260
1315
|
] })
|
|
@@ -1447,7 +1502,9 @@ var LEADING_ICON = { menu: "menu", back: "arrow-left", close: "x" };
|
|
|
1447
1502
|
function NavBar({ element }) {
|
|
1448
1503
|
const p = element.props;
|
|
1449
1504
|
const leading = p.leading === "logo" ? /* @__PURE__ */ jsx(Box, { style: { width: 30, height: 30, borderRadius: 8, backgroundColor: theme.fill2 } }) : p.leading && p.leading !== "none" ? /* @__PURE__ */ jsx(IconGlyph, { name: LEADING_ICON[p.leading], size: 22 }) : null;
|
|
1450
|
-
const
|
|
1505
|
+
const icons2 = [...p.actions ?? []];
|
|
1506
|
+
if (p.menu?.length && !/^(more-|ellipsis)/.test(icons2[icons2.length - 1] ?? "")) icons2.push("more-vertical");
|
|
1507
|
+
const actions = /* @__PURE__ */ jsx(Box, { style: { gap: 18, alignItems: "center", justifyContent: "flex-end" }, children: icons2.map((a, i) => /* @__PURE__ */ jsx(IconGlyph, { name: a, size: 22 }, i)) });
|
|
1451
1508
|
const title = p.title ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 17, fontWeight: 600 }, children: p.title }) : null;
|
|
1452
1509
|
const centered = p.align === "center";
|
|
1453
1510
|
return /* @__PURE__ */ jsx(
|
|
@@ -1584,7 +1641,7 @@ function ListItem({ element, colors }) {
|
|
|
1584
1641
|
case "icon":
|
|
1585
1642
|
return /* @__PURE__ */ jsx(IconGlyph, { name: p.trailingIcon ?? "more-horizontal", size: 20, color: theme.muted });
|
|
1586
1643
|
default:
|
|
1587
|
-
return null;
|
|
1644
|
+
return p.menu?.length ? /* @__PURE__ */ jsx(IconGlyph, { name: "more-horizontal", size: 20, color: theme.muted }) : null;
|
|
1588
1645
|
}
|
|
1589
1646
|
})();
|
|
1590
1647
|
return /* @__PURE__ */ jsxs(Box, { style: { minHeight: 60, padding: "10px 0", gap: 14, alignItems: "center" }, children: [
|
|
@@ -2032,6 +2089,14 @@ function parseWireframeText(source) {
|
|
|
2032
2089
|
c.next();
|
|
2033
2090
|
c.next();
|
|
2034
2091
|
const key = t.value;
|
|
2092
|
+
if (key in UNIVERSAL_PROPS) {
|
|
2093
|
+
if (takesUniversalProps(info.name)) props[key] = textWhereExpected(UNIVERSAL_PROPS[key], parseValue(c));
|
|
2094
|
+
else {
|
|
2095
|
+
parseValue(c);
|
|
2096
|
+
issues.push({ line: lineNo, message: `${info.name}: ${universalPropMessage(info.name, key)}` });
|
|
2097
|
+
}
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2035
2100
|
if (!(key in info.props)) {
|
|
2036
2101
|
const elsewhere = belongsElsewhere(info.name, key, true);
|
|
2037
2102
|
issues.push({ line: lineNo, message: `${info.name} has no prop "${key}"` + (elsewhere ? `: ${elsewhere}` : "") + ` (its props: ${Object.keys(info.props).join(", ")})` });
|
|
@@ -2209,6 +2274,155 @@ function scanLine(line, from) {
|
|
|
2209
2274
|
import { readFile } from "node:fs/promises";
|
|
2210
2275
|
import { createRequire } from "node:module";
|
|
2211
2276
|
import { renderToSvg } from "@json-render/image/render";
|
|
2277
|
+
|
|
2278
|
+
// src/anchors.tsx
|
|
2279
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2280
|
+
var propsOf = (els, id) => els[id].props ?? {};
|
|
2281
|
+
function findAnchors(spec) {
|
|
2282
|
+
const out = [];
|
|
2283
|
+
const visit = (id, screen) => {
|
|
2284
|
+
const el = spec.elements[id];
|
|
2285
|
+
if (!el) return;
|
|
2286
|
+
const p = propsOf(spec.elements, id);
|
|
2287
|
+
if (el.type === "Screen") screen = id;
|
|
2288
|
+
if (screen) {
|
|
2289
|
+
if (el.type === "Select" && p.open) out.push({ id, kind: "options", screen, field: true });
|
|
2290
|
+
if (el.type === "Input" && p.open && p.type === "date") out.push({ id, kind: "calendar", screen, field: true });
|
|
2291
|
+
if (el.type === "Button" && p.open && Array.isArray(p.menu) && p.menu.length) out.push({ id, kind: "menu", screen, field: false });
|
|
2292
|
+
if ((el.type === "ListItem" || el.type === "NavBar") && p.open && Array.isArray(p.menu) && p.menu.length) out.push({ id, kind: "menu", screen, field: false, end: true });
|
|
2293
|
+
}
|
|
2294
|
+
for (const child of el.children ?? []) visit(child, screen);
|
|
2295
|
+
};
|
|
2296
|
+
visit(spec.root, "");
|
|
2297
|
+
const tips = [];
|
|
2298
|
+
const visitTips = (id, screen) => {
|
|
2299
|
+
const el = spec.elements[id];
|
|
2300
|
+
if (!el) return;
|
|
2301
|
+
if (el.type === "Screen") screen = id;
|
|
2302
|
+
if (screen && el.type !== "Screen" && typeof propsOf(spec.elements, id).tooltip === "string") tips.push({ id, kind: "tooltip", screen, field: false });
|
|
2303
|
+
for (const child of el.children ?? []) visitTips(child, screen);
|
|
2304
|
+
};
|
|
2305
|
+
visitTips(spec.root, "");
|
|
2306
|
+
return [...out, ...tips];
|
|
2307
|
+
}
|
|
2308
|
+
var MARK = (i) => `#fe${(i >> 8).toString(16).padStart(2, "0")}${(i & 255).toString(16).padStart(2, "0")}`;
|
|
2309
|
+
function tagForMeasuring(spec, anchors) {
|
|
2310
|
+
const tagged = structuredClone(spec);
|
|
2311
|
+
const colors = /* @__PURE__ */ new Map();
|
|
2312
|
+
let n = 1;
|
|
2313
|
+
const tag = (id, field) => {
|
|
2314
|
+
const key = field ? `${id}#field` : id;
|
|
2315
|
+
if (colors.has(key)) return;
|
|
2316
|
+
const color = MARK(n++);
|
|
2317
|
+
colors.set(key, color);
|
|
2318
|
+
const el = tagged.elements[id];
|
|
2319
|
+
el.props = { ...el.props ?? {}, [field ? "__markField" : "__mark"]: color };
|
|
2320
|
+
};
|
|
2321
|
+
for (const a of anchors) {
|
|
2322
|
+
tag(a.screen, false);
|
|
2323
|
+
tag(a.id, a.field);
|
|
2324
|
+
}
|
|
2325
|
+
return { tagged, colors };
|
|
2326
|
+
}
|
|
2327
|
+
function readMarkers(svg, colors) {
|
|
2328
|
+
const rects = [...svg.matchAll(/<rect ([^>]*?)\/?>/g)].map(
|
|
2329
|
+
(m) => Object.fromEntries([...m[1].matchAll(/([\w-]+)="([^"]*)"/g)].map((a) => [a[1], a[2]]))
|
|
2330
|
+
);
|
|
2331
|
+
const boxes = {};
|
|
2332
|
+
for (const [id, color] of colors) {
|
|
2333
|
+
const r = rects.find((a) => a.fill === color);
|
|
2334
|
+
if (r) boxes[id] = { x: +r.x, y: +r.y, w: +r.width, h: +r.height };
|
|
2335
|
+
}
|
|
2336
|
+
return boxes;
|
|
2337
|
+
}
|
|
2338
|
+
function place(anchor, size, screen, opts) {
|
|
2339
|
+
const room = { above: anchor.y - screen.y - opts.gap, below: screen.y + screen.h - (anchor.y + anchor.h) - opts.gap };
|
|
2340
|
+
const other = opts.prefer === "below" ? "above" : "below";
|
|
2341
|
+
const side = room[opts.prefer] >= size.h || room[opts.prefer] >= room[other] ? opts.prefer : other;
|
|
2342
|
+
const y = side === "below" ? anchor.y + anchor.h + opts.gap : anchor.y - opts.gap - size.h;
|
|
2343
|
+
const x0 = opts.align === "start" ? anchor.x : opts.align === "end" ? anchor.x + anchor.w - size.w : anchor.x + anchor.w / 2 - size.w / 2;
|
|
2344
|
+
const x = Math.max(screen.x + 8, Math.min(x0, screen.x + screen.w - 8 - size.w));
|
|
2345
|
+
return { x: x - screen.x, y: y - screen.y, bottom: screen.y + screen.h - (anchor.y - opts.gap), side };
|
|
2346
|
+
}
|
|
2347
|
+
var ROW = 36;
|
|
2348
|
+
function ListCard({ items, width, selected, c }) {
|
|
2349
|
+
return /* @__PURE__ */ jsx2("div", { style: { display: "flex", flexDirection: "column", width, padding: 4, backgroundColor: theme.paper, border: `1.5px solid ${theme.lineStrong}`, borderRadius: 8, boxShadow: "0 8px 24px rgba(0,0,0,0.18)" }, children: items.map((item, i) => {
|
|
2350
|
+
const on = selected != null && item.label === selected;
|
|
2351
|
+
return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 10, height: ROW - 4, padding: "0 10px", borderRadius: 6, fontSize: 15, color: theme.ink, backgroundColor: on ? theme.fill : "transparent", fontWeight: on ? 600 : 400 }, children: [
|
|
2352
|
+
item.icon ? /* @__PURE__ */ jsx2(IconGlyph, { name: item.icon, size: 16, color: theme.text }) : null,
|
|
2353
|
+
/* @__PURE__ */ jsx2("div", { style: { display: "flex", flexGrow: 1 }, children: item.label }),
|
|
2354
|
+
on ? /* @__PURE__ */ jsx2(IconGlyph, { name: "check", size: 16, color: c.accentText }) : null
|
|
2355
|
+
] }, i);
|
|
2356
|
+
}) });
|
|
2357
|
+
}
|
|
2358
|
+
function parseDate(value) {
|
|
2359
|
+
const s = String(value ?? "").trim();
|
|
2360
|
+
const MONTHS2 = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
2361
|
+
const iso = s.match(/^(\d{4})-(\d{1,2})(?:-(\d{1,2}))?/);
|
|
2362
|
+
if (iso) return { month: `${MONTHS2[+iso[2] - 1]} ${iso[1]}`, day: iso[3] ? +iso[3] : void 0 };
|
|
2363
|
+
const name = s.match(/([A-Za-z]{3,})/)?.[1];
|
|
2364
|
+
const index = name ? MONTHS2.findIndex((m) => m.toLowerCase().startsWith(name.toLowerCase().slice(0, 3))) : -1;
|
|
2365
|
+
const year = s.match(/\b(\d{4})\b/)?.[1];
|
|
2366
|
+
if (index < 0 || !year) return null;
|
|
2367
|
+
const day2 = s.replace(year, "").match(/\b(\d{1,2})\b/)?.[1];
|
|
2368
|
+
return { month: `${MONTHS2[index]} ${year}`, day: day2 ? +day2 : void 0 };
|
|
2369
|
+
}
|
|
2370
|
+
function Tooltip({ text, side, width, arrowX }) {
|
|
2371
|
+
const arrow = /* @__PURE__ */ jsx2("svg", { width: "12", height: "6", viewBox: "0 0 12 6", style: { flexShrink: 0, marginLeft: Math.max(6, Math.min(arrowX, width - 6)) - 6 }, children: /* @__PURE__ */ jsx2("path", { d: side === "above" ? "M0 0 L6 6 L12 0 Z" : "M0 6 L6 0 L12 6 Z", fill: theme.ink }) });
|
|
2372
|
+
return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-start", width }, children: [
|
|
2373
|
+
side === "below" ? arrow : null,
|
|
2374
|
+
/* @__PURE__ */ jsx2("div", { style: { display: "flex", justifyContent: "center", width, padding: "6px 10px", borderRadius: 6, backgroundColor: theme.ink, color: theme.onPrimary, fontSize: 13, lineHeight: 1.35, textAlign: "center" }, children: text }),
|
|
2375
|
+
side === "above" ? arrow : null
|
|
2376
|
+
] });
|
|
2377
|
+
}
|
|
2378
|
+
function tooltipSize(text) {
|
|
2379
|
+
const width = Math.min(240, Math.ceil(text.length * 7.4) + 24);
|
|
2380
|
+
const lines = Math.max(1, Math.ceil(text.length * 7.4 / 216));
|
|
2381
|
+
return { w: width, h: lines * 18 + 12 + 6 };
|
|
2382
|
+
}
|
|
2383
|
+
function overlayLayer(spec, anchors, boxes, c, Calendar2) {
|
|
2384
|
+
const byScreen = /* @__PURE__ */ new Map();
|
|
2385
|
+
for (const a of anchors) {
|
|
2386
|
+
const screen = boxes[a.screen];
|
|
2387
|
+
const anchor = boxes[a.field ? `${a.id}#field` : a.id];
|
|
2388
|
+
if (!screen || !anchor) continue;
|
|
2389
|
+
const p = propsOf(spec.elements, a.id);
|
|
2390
|
+
let node = null;
|
|
2391
|
+
let pos = null;
|
|
2392
|
+
if (a.kind === "options") {
|
|
2393
|
+
const options = p.options?.length ? p.options : [p.value ?? "Option 1", "Option 2", "Option 3"];
|
|
2394
|
+
const size = { w: anchor.w, h: options.length * ROW + 8 };
|
|
2395
|
+
pos = place(anchor, size, screen, { prefer: "below", align: "start", gap: 4 });
|
|
2396
|
+
node = /* @__PURE__ */ jsx2(ListCard, { items: options.map((label) => ({ label })), width: size.w, selected: p.value, c });
|
|
2397
|
+
} else if (a.kind === "menu") {
|
|
2398
|
+
const items = p.menu.map((i) => typeof i === "string" ? { label: i } : i);
|
|
2399
|
+
const width = a.end ? 200 : Math.max(180, anchor.w);
|
|
2400
|
+
const inRightHalf = anchor.x + anchor.w / 2 > screen.x + screen.w / 2;
|
|
2401
|
+
pos = place(anchor, { w: width, h: items.length * ROW + 8 }, screen, { prefer: "below", align: a.end || inRightHalf ? "end" : "start", gap: a.end ? -6 : 4 });
|
|
2402
|
+
node = /* @__PURE__ */ jsx2(ListCard, { items, width, c });
|
|
2403
|
+
} else if (a.kind === "calendar") {
|
|
2404
|
+
const date = parseDate(p.value);
|
|
2405
|
+
const { start, days } = monthGrid(date?.month);
|
|
2406
|
+
const weeks = Math.ceil((start + days) / 7);
|
|
2407
|
+
pos = place(anchor, { w: 296, h: 92 + weeks * 38 }, screen, { prefer: "below", align: "start", gap: 4 });
|
|
2408
|
+
node = /* @__PURE__ */ jsx2("div", { style: { display: "flex", boxShadow: "0 8px 24px rgba(0,0,0,0.18)", borderRadius: 12 }, children: Calendar2({ element: { type: "Calendar", props: { month: date?.month ?? "Month", selected: date?.day } }, colors: c }) });
|
|
2409
|
+
} else {
|
|
2410
|
+
const size = tooltipSize(p.tooltip);
|
|
2411
|
+
pos = place(anchor, size, screen, { prefer: "above", align: "center", gap: 4 });
|
|
2412
|
+
node = /* @__PURE__ */ jsx2(Tooltip, { text: p.tooltip, side: pos.side, width: size.w, arrowX: anchor.x + anchor.w / 2 - (screen.x + pos.x) });
|
|
2413
|
+
}
|
|
2414
|
+
const list = byScreen.get(a.screen) ?? [];
|
|
2415
|
+
const vertical = pos.side === "above" ? { bottom: pos.bottom } : { top: pos.y };
|
|
2416
|
+
list.push(/* @__PURE__ */ jsx2("div", { style: { position: "absolute", left: pos.x, ...vertical, display: "flex" }, children: node }, `${a.id}-${a.kind}`));
|
|
2417
|
+
byScreen.set(a.screen, list);
|
|
2418
|
+
}
|
|
2419
|
+
return /* @__PURE__ */ jsx2("div", { style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", display: "flex" }, children: [...byScreen].map(([screenId, nodes]) => {
|
|
2420
|
+
const s = boxes[screenId];
|
|
2421
|
+
return /* @__PURE__ */ jsx2("div", { style: { position: "absolute", left: s.x, top: s.y, width: s.w, height: s.h, overflow: "hidden", display: "flex" }, children: nodes }, screenId);
|
|
2422
|
+
}) }, "__overlays");
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// src/render.ts
|
|
2212
2426
|
var fontFile = (name) => createRequire(import.meta.url).resolve(`@fontsource/inter/files/${name}`);
|
|
2213
2427
|
var fontCache = null;
|
|
2214
2428
|
function loadFonts() {
|
|
@@ -2271,6 +2485,14 @@ function checkSpec(spec) {
|
|
|
2271
2485
|
}
|
|
2272
2486
|
}
|
|
2273
2487
|
for (const key of Object.keys(el.props ?? {})) {
|
|
2488
|
+
if (key in UNIVERSAL_PROPS) {
|
|
2489
|
+
if (!takesUniversalProps(el.type)) issues.push(`${id}: ${universalPropMessage(el.type, key)}`);
|
|
2490
|
+
else {
|
|
2491
|
+
const r = UNIVERSAL_PROPS[key].safeParse(el.props[key]);
|
|
2492
|
+
if (!r.success) issues.push(`${id}.props.${key}: ${r.error.issues[0].message}`);
|
|
2493
|
+
}
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2274
2496
|
if (!(key in def.props.shape)) {
|
|
2275
2497
|
const elsewhere = belongsElsewhere(el.type, key, true);
|
|
2276
2498
|
issues.push(`${id}.props: ${el.type} has no prop "${key}"${elsewhere ? `: ${elsewhere}` : ""}`);
|
|
@@ -2323,6 +2545,9 @@ function checkSpec(spec) {
|
|
|
2323
2545
|
say(`value has ${String(p.value).length} characters but the code has ${p.digits ?? 6} boxes (digits)`);
|
|
2324
2546
|
}
|
|
2325
2547
|
if (el.type === "Input" && p.digits != null && p.type !== "code") say("digits only applies to type=code");
|
|
2548
|
+
if (el.type === "Input" && p.open && p.type !== "date") say("open shows a date picker, so it needs type=date");
|
|
2549
|
+
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");
|
|
2550
|
+
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");
|
|
2326
2551
|
}
|
|
2327
2552
|
if (el.type === "Table") {
|
|
2328
2553
|
const { columns, data } = el.props ?? {};
|
|
@@ -2388,13 +2613,21 @@ async function renderWireframeSvg(spec, opts = {}) {
|
|
|
2388
2613
|
if (issues.length) throw new SpecError(issues);
|
|
2389
2614
|
}
|
|
2390
2615
|
const { width, height } = boardSize(spec);
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2616
|
+
const palette = paletteFor(spec.elements[spec.root]?.props?.accent);
|
|
2617
|
+
const registry2 = withPalette(palette);
|
|
2618
|
+
const draw = async (s, reg) => renderToSvg(s, { registry: reg, includeStandard: false, fonts: await loadFonts(), width, height });
|
|
2619
|
+
const { anchors, boxes } = await measureAnchors(spec);
|
|
2620
|
+
if (!anchors.length) return draw(spec, registry2);
|
|
2621
|
+
return draw(spec, withTopLayer(registry2, overlayLayer(spec, anchors, boxes, palette, registry2.Calendar)));
|
|
2622
|
+
}
|
|
2623
|
+
async function measureAnchors(spec) {
|
|
2624
|
+
const anchors = findAnchors(spec);
|
|
2625
|
+
if (!anchors.length) return { anchors, boxes: {} };
|
|
2626
|
+
const { width, height } = boardSize(spec);
|
|
2627
|
+
const registry2 = withPalette(paletteFor());
|
|
2628
|
+
const { tagged, colors } = tagForMeasuring(spec, anchors);
|
|
2629
|
+
const svg = await renderToSvg(tagged, { registry: withMarkers(registry2), includeStandard: false, fonts: await loadFonts(), width, height });
|
|
2630
|
+
return { anchors, boxes: readMarkers(svg, colors) };
|
|
2398
2631
|
}
|
|
2399
2632
|
async function renderWireframePng(spec, opts = {}) {
|
|
2400
2633
|
const svg = await renderWireframeSvg(spec, opts);
|
|
@@ -2613,7 +2846,8 @@ var RULES = `## Rules
|
|
|
2613
2846
|
5. Modal, Drawer and Toast are overlays. They must be direct children of a Screen.
|
|
2614
2847
|
6. This is a low-fidelity wireframe. Prefer placeholders (Image boxes, Text with lines) over invented copy unless the copy matters.
|
|
2615
2848
|
7. Only use the components and props listed below. All props are optional unless marked required.
|
|
2616
|
-
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) and Input error. Only add an accent if the request asks for color or a brand
|
|
2849
|
+
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) and Input error. Only add an accent if the request asks for color or a brand.
|
|
2850
|
+
9. Any element except Board, Screen and Note 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.`;
|
|
2617
2851
|
var TEXT_FORMAT = `## Output format: wireframe text
|
|
2618
2852
|
One element per line. Indent children two spaces under their parent. The first line is the board.
|
|
2619
2853
|
|
|
@@ -2662,6 +2896,8 @@ export {
|
|
|
2662
2896
|
DEVICES,
|
|
2663
2897
|
ACCENTS,
|
|
2664
2898
|
iconNames,
|
|
2899
|
+
UNIVERSAL_PROPS,
|
|
2900
|
+
takesUniversalProps,
|
|
2665
2901
|
componentDefinitions,
|
|
2666
2902
|
catalog,
|
|
2667
2903
|
registry,
|
package/dist/cli.js
CHANGED
package/dist/components.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
2
|
import { type Palette } from "./colors.js";
|
|
3
3
|
import type { ComponentRenderProps } from "@json-render/image";
|
|
4
4
|
/**
|
|
@@ -16,6 +16,14 @@ type Props<P = Record<string, any>> = ComponentRenderProps<P> & {
|
|
|
16
16
|
};
|
|
17
17
|
/** A registry whose components all receive this board's palette as `colors`. */
|
|
18
18
|
export declare function withPalette(colors: Palette): typeof registry;
|
|
19
|
+
/**
|
|
20
|
+
* A registry for the measuring pass: an element whose props carry `__mark` (a color) gets a marker on
|
|
21
|
+
* its outer box. Fields and screens place their own (`__markField`, Screen's frame), since an overlay
|
|
22
|
+
* hangs from the field, not the label, and stays inside the device frame.
|
|
23
|
+
*/
|
|
24
|
+
export declare function withMarkers(reg: typeof registry): typeof registry;
|
|
25
|
+
/** A registry whose Board also draws `layer` on top of everything (the anchored overlays). */
|
|
26
|
+
export declare function withTopLayer(reg: typeof registry, layer: ReactNode): typeof registry;
|
|
19
27
|
export declare function IconGlyph({ name, size, color, strokeWidth }: {
|
|
20
28
|
name?: string | null;
|
|
21
29
|
size?: number;
|
package/dist/index.js
CHANGED
package/dist/playground/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACCENTS,
|
|
3
3
|
PRIMARY_PROP,
|
|
4
|
+
UNIVERSAL_PROPS,
|
|
4
5
|
bareWords,
|
|
5
6
|
boardLayout,
|
|
6
7
|
compileWireframe,
|
|
@@ -10,8 +11,9 @@ import {
|
|
|
10
11
|
iconNames,
|
|
11
12
|
renderWireframePng,
|
|
12
13
|
renderWireframeSvg,
|
|
14
|
+
takesUniversalProps,
|
|
13
15
|
wireframePrompt
|
|
14
|
-
} from "../chunks/chunk-
|
|
16
|
+
} from "../chunks/chunk-B6KIVOSH.js";
|
|
15
17
|
|
|
16
18
|
// src/playground/index.ts
|
|
17
19
|
import { createServer } from "node:http";
|
|
@@ -54,6 +56,7 @@ function languageData() {
|
|
|
54
56
|
const components = Object.entries(componentDefinitions).map(([type, def]) => {
|
|
55
57
|
const shape = def.props.shape;
|
|
56
58
|
const props = Object.entries(shape).map(([name, schema]) => describe(name, schema));
|
|
59
|
+
if (takesUniversalProps(type)) for (const [name, schema] of Object.entries(UNIVERSAL_PROPS)) props.push(describe(name, schema));
|
|
57
60
|
if (type === "Icon") props.find((p) => p.name === "name").icon = true;
|
|
58
61
|
const words = bareWords(type);
|
|
59
62
|
const off = words.booleans.flatMap((b) => b === "on" ? ["off"] : b === "checked" ? ["unchecked"] : [`no-${b}`]);
|
|
@@ -258,7 +261,19 @@ var EXAMPLES = {
|
|
|
258
261
|
button secondary "Back" leadingIcon=chevron-left
|
|
259
262
|
button primary "Next" trailingIcon=chevron-right
|
|
260
263
|
button primary "Full width" fullWidth
|
|
261
|
-
text "With a board accent, primary buttons fill with it and ghost buttons use it for text." sm muted
|
|
264
|
+
text "With a board accent, primary buttons fill with it and ghost buttons use it for text." sm muted
|
|
265
|
+
screen custom "tooltip" width=380 height=170
|
|
266
|
+
spacer size=34
|
|
267
|
+
stack row gap=8
|
|
268
|
+
button secondary "Draft" tooltip="Saves without publishing"
|
|
269
|
+
button primary "Publish"
|
|
270
|
+
text "Any element inside a screen can take tooltip=." sm muted
|
|
271
|
+
screen custom "menu and open" width=380 height=300
|
|
272
|
+
stack row justify=between align=center
|
|
273
|
+
heading "Notes" level=3
|
|
274
|
+
button ghost leadingIcon=more-horizontal open menu=[{label=Rename icon=pencil}, {label=Duplicate icon=copy}, Share, {label=Delete icon=trash-2}]
|
|
275
|
+
text lines=3
|
|
276
|
+
text "menu lists the items; open shows them over the screen." sm muted`,
|
|
262
277
|
Input: `board
|
|
263
278
|
screen custom width=420 height=760
|
|
264
279
|
input "Email" placeholder="you@example.com" helper="We never share it"
|
|
@@ -270,7 +285,10 @@ var EXAMPLES = {
|
|
|
270
285
|
input placeholder="grow fills the row" grow
|
|
271
286
|
button primary "Send"
|
|
272
287
|
input "Check-in" type=date value="Oct 14, 2026"
|
|
273
|
-
input "Verification code" type=code value="4821" helper="Sent to (555) 010-2400"
|
|
288
|
+
input "Verification code" type=code value="4821" helper="Sent to (555) 010-2400"
|
|
289
|
+
screen custom "type=date open" width=420 height=440
|
|
290
|
+
input "Check-in" type=date value="Oct 14, 2026" open
|
|
291
|
+
input "Check-out" type=date value="Oct 18, 2026"`,
|
|
274
292
|
Checkbox: `board
|
|
275
293
|
screen custom width=300 height=100
|
|
276
294
|
checkbox "Checked" checked
|
|
@@ -295,7 +313,10 @@ var EXAMPLES = {
|
|
|
295
313
|
select "Size" placeholder="Choose one" width=160
|
|
296
314
|
stack row gap=12
|
|
297
315
|
input "City" placeholder="Monterrey" grow
|
|
298
|
-
select "State" value="Nuevo Le\xF3n" grow
|
|
316
|
+
select "State" value="Nuevo Le\xF3n" grow
|
|
317
|
+
screen custom "options and open" width=380 height=260
|
|
318
|
+
select "Country" value="Mexico" open options=[Canada, Mexico, United States, Spain]
|
|
319
|
+
input "City" placeholder="Monterrey"`,
|
|
299
320
|
Slider: `board
|
|
300
321
|
screen custom width=380 height=200
|
|
301
322
|
slider "Volume" value=30
|
|
@@ -319,7 +340,10 @@ var EXAMPLES = {
|
|
|
319
340
|
navbar "leading=menu" leading=menu actions=[search, bell]
|
|
320
341
|
navbar "leading=back, centered" leading=back align=center actions=[share]
|
|
321
342
|
navbar "leading=close" leading=close
|
|
322
|
-
navbar "leading=logo" leading=logo actions=[user]
|
|
343
|
+
navbar "leading=logo" leading=logo actions=[user]
|
|
344
|
+
screen custom "menu and open" width=380 height=240 padding=0 gap=0
|
|
345
|
+
navbar "Inbox" leading=menu actions=[search] open menu=[Mark all as read, {label=Settings icon=settings}, {label=Sign out icon=log-out}]
|
|
346
|
+
text lines=4`,
|
|
323
347
|
TabBar: `board
|
|
324
348
|
screen phone "Tab bar sits at the bottom"
|
|
325
349
|
heading "Home"
|
|
@@ -352,7 +376,13 @@ var EXAMPLES = {
|
|
|
352
376
|
listitem "leading=image" subtitle="trailing=badge" leading=image trailing=badge trailingText="3"
|
|
353
377
|
listitem "leading=checkbox" leading=checkbox trailing=toggle
|
|
354
378
|
listitem "trailingIcon=trash-2" trailingIcon=trash-2
|
|
355
|
-
listitem leading=image trailing=chevron
|
|
379
|
+
listitem leading=image trailing=chevron
|
|
380
|
+
screen custom "menu and open" width=400 height=330
|
|
381
|
+
list
|
|
382
|
+
listitem "Report.pdf" subtitle="2.4 MB" leadingIcon=file-text
|
|
383
|
+
listitem "Budget.xlsx" subtitle="380 KB" leadingIcon=sheet open menu=[{label=Rename icon=pencil}, {label=Move icon=folder}, {label=Delete icon=trash-2}]
|
|
384
|
+
listitem "Notes.txt" subtitle="12 KB" leadingIcon=file
|
|
385
|
+
listitem "Photo.jpg" subtitle="1.1 MB" leadingIcon=image`,
|
|
356
386
|
Table: `board
|
|
357
387
|
screen custom width=520 height=400
|
|
358
388
|
table columns=[Name, Role, Status] data=[["Ana Ruiz", Admin, Active], ["Ben Cho", Editor, Invited]]
|
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.\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) and Input error. Only add an accent if the request asks for color or a brand.";
|
|
16
|
+
export declare const RULES = "## Rules\n1. The top element is a Board. The Board's children are Screens, plus optional Notes beside them.\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) and Input error. Only add an accent if the request asks for color or a brand.\n9. Any element except Board, Screen and Note 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.";
|
|
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/dist/render.d.ts
CHANGED
|
@@ -35,6 +35,19 @@ export interface RenderWireframeOptions {
|
|
|
35
35
|
skipValidation?: boolean;
|
|
36
36
|
}
|
|
37
37
|
export declare function renderWireframeSvg(spec: Spec, opts?: RenderWireframeOptions): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* The measuring pass: the board's anchored elements (and their screens) and their boxes on the
|
|
40
|
+
* canvas, keyed by element id (`<id>#field` for a field). No render at all when there are none.
|
|
41
|
+
*/
|
|
42
|
+
export declare function measureAnchors(spec: Spec): Promise<{
|
|
43
|
+
anchors: import("./anchors.js").Anchor[];
|
|
44
|
+
boxes: Record<string, {
|
|
45
|
+
x: number;
|
|
46
|
+
y: number;
|
|
47
|
+
w: number;
|
|
48
|
+
h: number;
|
|
49
|
+
}>;
|
|
50
|
+
}>;
|
|
38
51
|
export declare function renderWireframePng(spec: Spec, opts?: RenderWireframeOptions & {
|
|
39
52
|
scale?: number;
|
|
40
53
|
}): Promise<Buffer<ArrayBufferLike>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsquare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@fontsource/inter": "5.3.0",
|
|
71
71
|
"@json-render/core": "^0.21.0",
|
|
72
|
-
"@json-render/image": "
|
|
72
|
+
"@json-render/image": "0.21.0",
|
|
73
73
|
"@resvg/resvg-js": "^2.6.2",
|
|
74
74
|
"@types/node": ">=20",
|
|
75
75
|
"@types/react": "^19.3.0",
|
package/skill/tsquare/SKILL.md
CHANGED
|
@@ -51,6 +51,7 @@ If a bare word could mean more than one prop, write it as key=value.
|
|
|
51
51
|
6. This is a low-fidelity wireframe. Prefer placeholders (Image boxes, Text with lines) over invented copy unless the copy matters.
|
|
52
52
|
7. Only use the components and props listed below. All props are optional unless marked required.
|
|
53
53
|
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) and Input error. Only add an accent if the request asks for color or a brand.
|
|
54
|
+
9. Any element except Board, Screen and Note 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.
|
|
54
55
|
|
|
55
56
|
## Example
|
|
56
57
|
|
|
@@ -121,13 +121,15 @@ Small pill label for counts or statuses.
|
|
|
121
121
|
- tone: "neutral" | "success" | "warning" | "danger" — Status color. Default neutral (the accent for solid badges)
|
|
122
122
|
|
|
123
123
|
### Button
|
|
124
|
-
Button. Primary is filled, secondary is outlined, ghost is text only.
|
|
124
|
+
Button. Primary is filled, secondary is outlined, ghost is text only. With menu and open, a dropdown menu.
|
|
125
125
|
- label: string (main text)
|
|
126
126
|
- variant: "primary" | "secondary" | "ghost"
|
|
127
127
|
- size: "sm" | "md" | "lg"
|
|
128
128
|
- leadingIcon: string — Lucide icon before the label, e.g. chevron-left for Back
|
|
129
129
|
- trailingIcon: string — Lucide icon after the label, e.g. chevron-right for Next
|
|
130
130
|
- fullWidth: boolean
|
|
131
|
+
- menu: array of string | { label: string, icon: string } — Dropdown menu items, shown below the button when open
|
|
132
|
+
- open: boolean — Show the menu
|
|
131
133
|
|
|
132
134
|
### Input
|
|
133
135
|
Text field with optional label. Set multiline for a textarea.
|
|
@@ -136,6 +138,7 @@ Text field with optional label. Set multiline for a textarea.
|
|
|
136
138
|
- value: string
|
|
137
139
|
- type: "text" | "password" | "search" | "email" | "date" | "code" — date shows a calendar icon; code draws one box per digit
|
|
138
140
|
- digits: number — Boxes for type=code, default 6
|
|
141
|
+
- open: boolean — type=date: show the calendar below the field
|
|
139
142
|
- multiline: number — Number of rows; 2 or more makes a textarea
|
|
140
143
|
- helper: string
|
|
141
144
|
- error: boolean — Validation error: red border and red helper text
|
|
@@ -158,10 +161,12 @@ On/off switch with label on the left.
|
|
|
158
161
|
- on: boolean
|
|
159
162
|
|
|
160
163
|
### Select
|
|
161
|
-
Dropdown field
|
|
164
|
+
Dropdown field. With options and open, the list shows below it.
|
|
162
165
|
- label: string (main text)
|
|
163
166
|
- value: string
|
|
164
167
|
- placeholder: string
|
|
168
|
+
- options: array of string — The choices, shown below the field when open
|
|
169
|
+
- open: boolean — Show the options list
|
|
165
170
|
- grow: boolean — Fill the remaining space in a row
|
|
166
171
|
- width: number | string — Fixed width, e.g. 320. Default fills the space.
|
|
167
172
|
|
|
@@ -192,6 +197,8 @@ Top app bar. Place first in a Screen.
|
|
|
192
197
|
- leading: "none" | "menu" | "back" | "close" | "logo"
|
|
193
198
|
- actions: array of string — Icon names shown on the right
|
|
194
199
|
- align: "left" | "center"
|
|
200
|
+
- menu: array of string | { label: string, icon: string } — A menu under the last action (adds ⋮ if needed), shown when open
|
|
201
|
+
- open: boolean — Show the menu
|
|
195
202
|
|
|
196
203
|
### TabBar
|
|
197
204
|
Bottom tab bar for mobile. Place last in a Screen (before overlays).
|
|
@@ -222,6 +229,8 @@ Row in a List. Omit title for a placeholder bar.
|
|
|
222
229
|
- trailing: "none" | "chevron" | "toggle" | "text" | "badge" | "icon"
|
|
223
230
|
- trailingText: string — Text for trailing=text or badge; setting it implies trailing=text
|
|
224
231
|
- trailingIcon: string — Lucide icon on the right; setting it implies trailing=icon
|
|
232
|
+
- menu: array of string | { label: string, icon: string } — A menu for the row (shows … on the right if nothing else is there), shown when open
|
|
233
|
+
- open: boolean — Show the menu
|
|
225
234
|
|
|
226
235
|
### Table
|
|
227
236
|
Table with headers. Rows are placeholder bars unless `data` is given.
|