tsquare 0.3.1 → 0.4.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 ADDED
@@ -0,0 +1,124 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0
4
+
5
+ ### New components
6
+
7
+ ```tsquare
8
+ chart line "Revenue" # line | bar | area | pie | donut, a placeholder shape
9
+ calendar "October 2026" range=[12, 18] marked=[3, 9]
10
+ input "Check-in" type=date value="Oct 14, 2026" # calendar icon
11
+ input "Verification code" type=code value="4821" # one box per digit (digits=6)
12
+ slider "Price" range=[50, 400] # any amounts; the track scales
13
+ progress "Uploading" value=40 # also: progress circle, progress steps=4 step=2
14
+ pagination pages=12 current=3
15
+ bullets items=[Fast setup, No credit card] # also: numbered, icon=check
16
+ bullets icon=check items=[Boards, {label="SSO" icon=x muted}]
17
+ ```
18
+
19
+ - **Charts are placeholders:** they show the kind of chart, not data.
20
+ - **The calendar draws the real month** from its title ("October 2026" starts on a Thursday).
21
+ - **Bullets** take any Lucide icon. One item can be an object that changes its icon or greys it out (`muted`).
22
+ - **Inside `{…}`:**
23
+ - a bare word turns an option on (`{label="SSO" muted}`);
24
+ - an unquoted value can contain spaces (`{label=Custom domain icon=x}`).
25
+ - **Clear errors for values that contradict each other:** a step past the last step, a page past the last page, a day that isn't in the month, a range written backwards.
26
+ - **The accent color** also fills sliders, progress, completed steps, and the selected day and page. Charts stay gray.
27
+ - **The prompt** grew by about 480 tokens (2,670 → 3,150).
28
+
29
+ ### Measured
30
+
31
+ - **The new components:** 7 requests that describe a screen without naming components, two runs each.
32
+ - Sonnet: valid 14 of 14, and picked the right components every time (32 of 32 checks).
33
+ - Haiku: valid 12 of 14, 30 of 32 checks.
34
+ - **The original 20 requests, with the new prompt:** Sonnet 100%, Haiku 65%, within its usual range. None of Haiku's failures involve the new components.
35
+
36
+ ## 0.3.2
37
+
38
+ - **`grow` on Select**, like Input: `select "State" grow` fills the rest of a row. Sonnet wrote it in 5 of 9 eval runs, its only remaining mistake; with it, every stored Sonnet text output is valid.
39
+ - **Docs:** a new [Embedding](docs/embedding.md) page for Notion, GitHub and your own site, and the Radio page shows radios side by side (`stack row` + `radio`).
40
+ - **Tests and CI:** a test suite (`npm test`), run on Node 20, 22 and 24 for every pull request, plus a check that eval scores, docs and the skill match what's committed.
41
+ - `CHANGELOG.md` is now included in the npm package.
42
+
43
+ ## 0.3.1
44
+
45
+ ### Older wireframes are upgraded in one step
46
+
47
+ - All support for older syntax now lives in one module (`upgradeWireframe`, `upgradeSpec`), run before parsing. The parser, catalog and renderer only know the current language.
48
+ - **Links made before 0.3.0 render again** when a comment follows an element on the same line (`screen phone "Home" # main`): the comment moves onto its own line. Text you write or paste still gets the error, so new comments go on their own line.
49
+ - `decodeWireframe` returns the upgraded text, so a decoded link always means what it did when it was made.
50
+
51
+ ### Better suggestions for unknown icons
52
+
53
+ - Common guesses that aren't Lucide names now suggest what was meant: `compose` → `square-pen`, `call` → `phone`, `person` → `user`, `bag` → `shopping-bag`, `notification` → `bell`, `close` → `x`.
54
+ - Among equally close matches, the usual directions come first: `chevron` suggests `chevron-right, chevron-left, chevron-down, chevron-up`.
55
+ - On a list item, `trailingIcon=chevron` (or `toggle`, `avatar`…) says to write `trailing=chevron`, since that's a kind, not an icon.
56
+
57
+ ## 0.3.0
58
+
59
+ ### Renamed: `icon` is now `leadingIcon` on Button and ListItem
60
+
61
+ Buttons and list items take an icon on either side, so the prop names now say which side:
62
+
63
+ | Before | Now |
64
+ |---|---|
65
+ | `button "Download" icon=download` | `button "Download" leadingIcon=download` |
66
+ | (not possible) | `button "Next" trailingIcon=chevron-right` |
67
+ | `listitem "Inbox" leading=icon icon=inbox` | `listitem "Inbox" leadingIcon=inbox` |
68
+ | `listitem "Delete" trailing=icon trailingIcon=trash-2` | `listitem "Delete" trailingIcon=trash-2` |
69
+
70
+ - **Nothing breaks.** The old `icon=` spelling still works on both components, so existing files and shared render links render the same. `tsquare fmt` rewrites it to `leadingIcon=`. The prompt, the skill and the docs only teach the new names.
71
+ - **Buttons can have a trailing icon**, for Next, external links or dropdowns. Both sides work together.
72
+ - `leading` and `trailing` on ListItem stay as before. They pick what fills each side (`avatar`, `image`, `checkbox`, `chevron`, `badge`, `toggle`, …). An icon or text value implies its kind, so `leading=icon` and `trailing=icon` are optional.
73
+ - `icon` stays as it is on TabBar items (`{label=Home icon=house}`) and on the Icon component.
74
+
75
+ ### Changed: comments go on their own line
76
+
77
+ A line that starts with `#` is a comment. Anywhere else, `#` is ordinary text, so order numbers, tags and hex colors need no quotes:
78
+
79
+ ```tsquare
80
+ # the orders page
81
+ screen desktop "Orders"
82
+ table columns=[Order, Customer] data=[[#1001, Ana Torres], [#1002, Ben Lee]]
83
+ ```
84
+
85
+ - **Why:** comments used to start at any `#` after a space. That cut off `[#1001, Ana Torres]` and broke the table, which happened to Sonnet in 3 of 4 eval runs. Across 160 eval outputs, models never wrote a comment.
86
+ - **Upgrading:** a comment after an element on the same line (`screen phone "Home" # main`) is now an error that tells you to move it to its own line. Nothing is dropped silently.
87
+
88
+ ### Clearer errors for props on the wrong component
89
+
90
+ When a word or prop belongs to a different component, the error now says which one:
91
+
92
+ ```
93
+ Input: fullWidth is a Button prop, not an Input one; an Input already fills its width (width=… sets a fixed one), so remove it
94
+ Card has no prop "border": border is a Stack prop, not a Card one
95
+ ```
96
+
97
+ A prop or list wrapped onto its own line says where it belongs, instead of "unknown component":
98
+
99
+ ```
100
+ trailing=…: props go on the same line as their component; move it to the end of line 15
101
+ a list must stay on one line; join this to line 6
102
+ ```
103
+
104
+ ### Playground
105
+
106
+ - A CodeMirror editor: highlighting, autocomplete for components, options, props, Lucide icons and accent colors, and problems underlined on their line.
107
+ - Light and dark themes.
108
+ - Zoom and pan, zoom to a single screen, full screen and a resizable split.
109
+ - Copy link: share link, SVG/PNG image links, Markdown and HTML snippets.
110
+ - An examples gallery with thumbnails.
111
+
112
+ ### Library
113
+
114
+ - `encodeWireframe` / `decodeWireframe` for share and render links.
115
+ - `boardLayout()` returns the board size and each screen's position.
116
+
117
+ ### List items: a value implies its kind
118
+
119
+ - `trailingIcon=trash-2` without `trailing=icon` used to render nothing, silently. Now the value implies the kind.
120
+ - A value that contradicts an explicit kind is an error that names the fix, for example: `leadingIcon only shows with leading=icon (this item has leading=avatar); remove one of them`.
121
+
122
+ ### Examples
123
+
124
+ - New: `sign-in`, `dashboard`, `checkout` and `states`.
package/README.md CHANGED
@@ -50,7 +50,7 @@ A wireframe can be embedded anywhere an image works (Notion, GitHub, docs sites)
50
50
  ![Login](https://tsquare.dev/svg/zNYwxDsIwEAT7vGJ…)
51
51
  ```
52
52
 
53
- The playground's **Copy link** menu makes these. The text is compressed into the URL, not encrypted, so anyone with the link can read it; see [privacy](https://tsquare.dev/privacy). `encodeWireframe` and `decodeWireframe` in the library produce and read the same links.
53
+ The playground's **Copy link** menu makes these; [Embedding](docs/embedding.md) has the steps for Notion, GitHub and your own site. The text is compressed into the URL, not encrypted, so anyone with the link can read it; see [privacy](https://tsquare.dev/privacy). `encodeWireframe` and `decodeWireframe` in the library produce and read the same links.
54
54
 
55
55
  ## The language
56
56
 
package/dist/catalog.d.ts CHANGED
@@ -192,6 +192,22 @@ export declare const componentDefinitions: {
192
192
  lines: number;
193
193
  };
194
194
  };
195
+ Bullets: {
196
+ props: z.ZodObject<{
197
+ items: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
198
+ label: z.ZodString;
199
+ icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
200
+ muted: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
201
+ }, z.core.$strict>]>>;
202
+ numbered: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
203
+ icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
204
+ }, z.core.$strip>;
205
+ slots: never[];
206
+ description: string;
207
+ example: {
208
+ items: string[];
209
+ };
210
+ };
195
211
  Image: {
196
212
  props: z.ZodObject<{
197
213
  height: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
@@ -206,6 +222,26 @@ export declare const componentDefinitions: {
206
222
  label: string;
207
223
  };
208
224
  };
225
+ Chart: {
226
+ props: z.ZodObject<{
227
+ kind: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
228
+ area: "area";
229
+ bar: "bar";
230
+ donut: "donut";
231
+ line: "line";
232
+ pie: "pie";
233
+ }>>>;
234
+ title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
235
+ height: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
236
+ width: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
237
+ }, z.core.$strip>;
238
+ slots: never[];
239
+ description: string;
240
+ example: {
241
+ kind: string;
242
+ title: string;
243
+ };
244
+ };
209
245
  Icon: {
210
246
  props: z.ZodObject<{
211
247
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -282,11 +318,14 @@ export declare const componentDefinitions: {
282
318
  placeholder: z.ZodOptional<z.ZodNullable<z.ZodString>>;
283
319
  value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
284
320
  type: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
321
+ code: "code";
322
+ date: "date";
285
323
  email: "email";
286
324
  password: "password";
287
325
  search: "search";
288
326
  text: "text";
289
327
  }>>>;
328
+ digits: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
290
329
  multiline: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
291
330
  helper: z.ZodOptional<z.ZodNullable<z.ZodString>>;
292
331
  error: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
@@ -341,6 +380,7 @@ export declare const componentDefinitions: {
341
380
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
342
381
  value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
343
382
  placeholder: z.ZodOptional<z.ZodNullable<z.ZodString>>;
383
+ grow: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
344
384
  width: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
345
385
  }, z.core.$strip>;
346
386
  slots: never[];
@@ -350,6 +390,51 @@ export declare const componentDefinitions: {
350
390
  value: string;
351
391
  };
352
392
  };
393
+ Slider: {
394
+ props: z.ZodObject<{
395
+ label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
396
+ value: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
397
+ range: z.ZodOptional<z.ZodNullable<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
398
+ }, z.core.$strip>;
399
+ slots: never[];
400
+ description: string;
401
+ example: {
402
+ label: string;
403
+ value: number;
404
+ };
405
+ };
406
+ Progress: {
407
+ props: z.ZodObject<{
408
+ label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
409
+ value: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
410
+ shape: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
411
+ bar: "bar";
412
+ circle: "circle";
413
+ }>>>;
414
+ steps: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
415
+ step: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
416
+ }, z.core.$strip>;
417
+ slots: never[];
418
+ description: string;
419
+ example: {
420
+ label: string;
421
+ value: number;
422
+ };
423
+ };
424
+ Calendar: {
425
+ props: z.ZodObject<{
426
+ month: z.ZodOptional<z.ZodNullable<z.ZodString>>;
427
+ selected: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
428
+ range: z.ZodOptional<z.ZodNullable<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
429
+ marked: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodNumber>>>;
430
+ }, z.core.$strip>;
431
+ slots: never[];
432
+ description: string;
433
+ example: {
434
+ month: string;
435
+ selected: number;
436
+ };
437
+ };
353
438
  NavBar: {
354
439
  props: z.ZodObject<{
355
440
  title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -404,6 +489,18 @@ export declare const componentDefinitions: {
404
489
  active: number;
405
490
  };
406
491
  };
492
+ Pagination: {
493
+ props: z.ZodObject<{
494
+ pages: z.ZodNumber;
495
+ current: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
496
+ }, z.core.$strip>;
497
+ slots: never[];
498
+ description: string;
499
+ example: {
500
+ pages: number;
501
+ current: number;
502
+ };
503
+ };
407
504
  List: {
408
505
  props: z.ZodObject<{
409
506
  dividers: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
@@ -715,6 +812,22 @@ export declare const catalog: import("@json-render/core").Catalog<{
715
812
  lines: number;
716
813
  };
717
814
  };
815
+ Bullets: {
816
+ props: z.ZodObject<{
817
+ items: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
818
+ label: z.ZodString;
819
+ icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
820
+ muted: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
821
+ }, z.core.$strict>]>>;
822
+ numbered: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
823
+ icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
824
+ }, z.core.$strip>;
825
+ slots: never[];
826
+ description: string;
827
+ example: {
828
+ items: string[];
829
+ };
830
+ };
718
831
  Image: {
719
832
  props: z.ZodObject<{
720
833
  height: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
@@ -729,6 +842,26 @@ export declare const catalog: import("@json-render/core").Catalog<{
729
842
  label: string;
730
843
  };
731
844
  };
845
+ Chart: {
846
+ props: z.ZodObject<{
847
+ kind: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
848
+ area: "area";
849
+ bar: "bar";
850
+ donut: "donut";
851
+ line: "line";
852
+ pie: "pie";
853
+ }>>>;
854
+ title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
855
+ height: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
856
+ width: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
857
+ }, z.core.$strip>;
858
+ slots: never[];
859
+ description: string;
860
+ example: {
861
+ kind: string;
862
+ title: string;
863
+ };
864
+ };
732
865
  Icon: {
733
866
  props: z.ZodObject<{
734
867
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -805,11 +938,14 @@ export declare const catalog: import("@json-render/core").Catalog<{
805
938
  placeholder: z.ZodOptional<z.ZodNullable<z.ZodString>>;
806
939
  value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
807
940
  type: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
941
+ code: "code";
942
+ date: "date";
808
943
  email: "email";
809
944
  password: "password";
810
945
  search: "search";
811
946
  text: "text";
812
947
  }>>>;
948
+ digits: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
813
949
  multiline: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
814
950
  helper: z.ZodOptional<z.ZodNullable<z.ZodString>>;
815
951
  error: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
@@ -864,6 +1000,7 @@ export declare const catalog: import("@json-render/core").Catalog<{
864
1000
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
865
1001
  value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
866
1002
  placeholder: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1003
+ grow: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
867
1004
  width: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
868
1005
  }, z.core.$strip>;
869
1006
  slots: never[];
@@ -873,6 +1010,51 @@ export declare const catalog: import("@json-render/core").Catalog<{
873
1010
  value: string;
874
1011
  };
875
1012
  };
1013
+ Slider: {
1014
+ props: z.ZodObject<{
1015
+ label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1016
+ value: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1017
+ range: z.ZodOptional<z.ZodNullable<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
1018
+ }, z.core.$strip>;
1019
+ slots: never[];
1020
+ description: string;
1021
+ example: {
1022
+ label: string;
1023
+ value: number;
1024
+ };
1025
+ };
1026
+ Progress: {
1027
+ props: z.ZodObject<{
1028
+ label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1029
+ value: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1030
+ shape: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
1031
+ bar: "bar";
1032
+ circle: "circle";
1033
+ }>>>;
1034
+ steps: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1035
+ step: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1036
+ }, z.core.$strip>;
1037
+ slots: never[];
1038
+ description: string;
1039
+ example: {
1040
+ label: string;
1041
+ value: number;
1042
+ };
1043
+ };
1044
+ Calendar: {
1045
+ props: z.ZodObject<{
1046
+ month: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1047
+ selected: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1048
+ range: z.ZodOptional<z.ZodNullable<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>;
1049
+ marked: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodNumber>>>;
1050
+ }, z.core.$strip>;
1051
+ slots: never[];
1052
+ description: string;
1053
+ example: {
1054
+ month: string;
1055
+ selected: number;
1056
+ };
1057
+ };
876
1058
  NavBar: {
877
1059
  props: z.ZodObject<{
878
1060
  title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -927,6 +1109,18 @@ export declare const catalog: import("@json-render/core").Catalog<{
927
1109
  active: number;
928
1110
  };
929
1111
  };
1112
+ Pagination: {
1113
+ props: z.ZodObject<{
1114
+ pages: z.ZodNumber;
1115
+ current: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1116
+ }, z.core.$strip>;
1117
+ slots: never[];
1118
+ description: string;
1119
+ example: {
1120
+ pages: number;
1121
+ current: number;
1122
+ };
1123
+ };
930
1124
  List: {
931
1125
  props: z.ZodObject<{
932
1126
  dividers: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
@@ -221,6 +221,13 @@ var o = (t) => t.nullish();
221
221
  var align = z.enum(["start", "center", "end", "stretch"]);
222
222
  var justify = z.enum(["start", "center", "end", "between", "around"]);
223
223
  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
+ var day = z.number().int().min(1).max(31);
225
+ var percent = z.number().min(0).max(100);
226
+ var bulletItem = z.union([
227
+ z.string(),
228
+ // strict: a misspelled key is an error, not silently dropped
229
+ z.strictObject({ label: z.string(), icon: o(iconName), muted: o(z.boolean()) })
230
+ ]);
224
231
  var componentDefinitions = {
225
232
  // ── Canvas ────────────────────────────────────────────────────────────
226
233
  Board: {
@@ -231,7 +238,7 @@ var componentDefinitions = {
231
238
  gap: o(z.number()),
232
239
  padding: o(z.number()),
233
240
  accent: o(z.string().refine(isAccent, { error: (iss) => accentMessage(String(iss.input)) })).describe(
234
- `The one UI color: primary buttons, solid badges, checked controls, toggles, active tabs, ghost buttons. ${Object.keys(ACCENTS).join(", ")}, or a hex color like #1a73e8. Omit for grayscale.`
241
+ `The one UI color: primary buttons, solid badges, checked controls, toggles, sliders, progress, selected days and pages, active tabs, ghost buttons. ${Object.keys(ACCENTS).join(", ")}, or a hex color like #1a73e8. Omit for grayscale.`
235
242
  )
236
243
  }),
237
244
  slots: ["default"],
@@ -341,6 +348,16 @@ var componentDefinitions = {
341
348
  description: "Body text, or placeholder lines when `lines` is set and `text` is not.",
342
349
  example: { lines: 3 }
343
350
  },
351
+ Bullets: {
352
+ props: z.object({
353
+ items: z.array(bulletItem).describe("Text, or {label icon muted} to change one line, e.g. {label=SSO icon=x muted}"),
354
+ numbered: o(z.boolean()).describe("1. 2. 3. instead of dots"),
355
+ icon: o(iconName).describe("A Lucide icon instead of dots, e.g. check for a feature list")
356
+ }),
357
+ slots: [],
358
+ description: "Bulleted, numbered or icon list of short lines.",
359
+ example: { items: ["Fast setup", "No credit card"] }
360
+ },
344
361
  Image: {
345
362
  props: z.object({
346
363
  height: o(z.number()),
@@ -352,6 +369,17 @@ var componentDefinitions = {
352
369
  description: "Image placeholder: a box with an X through it.",
353
370
  example: { height: 180, label: "Hero photo" }
354
371
  },
372
+ Chart: {
373
+ props: z.object({
374
+ kind: o(z.enum(["line", "bar", "area", "pie", "donut"])).describe("Default line"),
375
+ title: o(z.string()),
376
+ height: o(z.number()).describe("Default 180"),
377
+ width: o(z.union([z.number(), z.string()])).describe("Default fills the container width")
378
+ }),
379
+ slots: [],
380
+ description: "Chart placeholder: a generic line, bar, area, pie or donut shape, no data.",
381
+ example: { kind: "bar", title: "Signups" }
382
+ },
355
383
  Icon: {
356
384
  props: z.object({
357
385
  name: o(iconName),
@@ -399,7 +427,8 @@ var componentDefinitions = {
399
427
  label: o(z.string()),
400
428
  placeholder: o(z.string()),
401
429
  value: o(z.string()),
402
- type: o(z.enum(["text", "password", "search", "email"])),
430
+ type: o(z.enum(["text", "password", "search", "email", "date", "code"])).describe("date shows a calendar icon; code draws one box per digit"),
431
+ digits: o(z.number().int().min(2).max(10)).describe("Boxes for type=code, default 6"),
403
432
  multiline: o(z.number().int().min(1)).describe("Number of rows; 2 or more makes a textarea"),
404
433
  helper: o(z.string()),
405
434
  error: o(z.boolean()).describe("Validation error: red border and red helper text"),
@@ -433,12 +462,46 @@ var componentDefinitions = {
433
462
  label: o(z.string()),
434
463
  value: o(z.string()),
435
464
  placeholder: o(z.string()),
465
+ grow: o(z.boolean()).describe("Fill the remaining space in a row"),
436
466
  width: o(z.union([z.number(), z.string()])).describe("Fixed width, e.g. 320. Default fills the space.")
437
467
  }),
438
468
  slots: [],
439
469
  description: "Dropdown field (closed state).",
440
470
  example: { label: "Country", value: "United States" }
441
471
  },
472
+ Slider: {
473
+ props: z.object({
474
+ label: o(z.string()),
475
+ value: o(z.number().min(0)).describe("Handle position: 0 to 100, or any amount (the track scales to fit)"),
476
+ range: o(z.tuple([z.number().min(0), z.number().min(0)])).describe("Two handles instead, e.g. [20, 80] or [50, 400]")
477
+ }),
478
+ slots: [],
479
+ description: "Slider with one handle, or two with `range`.",
480
+ example: { label: "Volume", value: 30 }
481
+ },
482
+ Progress: {
483
+ props: z.object({
484
+ label: o(z.string()),
485
+ value: o(percent).describe("Percent done"),
486
+ shape: o(z.enum(["bar", "circle"])).describe("Default bar; circle is a ring with the percent inside"),
487
+ steps: o(z.number().int().min(2).max(10)).describe("Draw a stepper with this many steps instead"),
488
+ step: o(z.number().int().min(1)).describe("The current step, with steps")
489
+ }),
490
+ slots: [],
491
+ description: "Progress bar, ring (circle), or stepper (steps).",
492
+ example: { label: "Uploading", value: 40 }
493
+ },
494
+ Calendar: {
495
+ props: z.object({
496
+ month: o(z.string()).describe('Month and year, e.g. "October 2026"; the days match that month'),
497
+ selected: o(day),
498
+ range: o(z.tuple([day, day])).describe("Selected days, e.g. [12, 18]"),
499
+ marked: o(z.array(day)).describe("Days with a dot, e.g. events")
500
+ }),
501
+ slots: [],
502
+ description: "Month calendar.",
503
+ example: { month: "October 2026", selected: 14 }
504
+ },
442
505
  // ── Navigation & data ─────────────────────────────────────────────────
443
506
  NavBar: {
444
507
  props: z.object({
@@ -469,6 +532,15 @@ var componentDefinitions = {
469
532
  description: "In-page tabs with an underline on the active one.",
470
533
  example: { items: ["Overview", "Activity", "Settings"], active: 0 }
471
534
  },
535
+ Pagination: {
536
+ props: z.object({
537
+ pages: z.number().int().min(1),
538
+ current: o(z.number().int().min(1)).describe("Default 1")
539
+ }),
540
+ slots: [],
541
+ description: "Page numbers with previous and next.",
542
+ example: { pages: 12, current: 3 }
543
+ },
472
544
  List: {
473
545
  props: z.object({ dividers: o(z.boolean()), grow: o(z.boolean()) }),
474
546
  slots: ["default"],
@@ -875,6 +947,20 @@ function Text({ element }) {
875
947
  }
876
948
  );
877
949
  }
950
+ function Bullets({ element }) {
951
+ const p = element.props;
952
+ const items = p.items ?? [];
953
+ return /* @__PURE__ */ jsx(Box, { style: { flexDirection: "column", gap: 8 }, children: items.map((item, i) => {
954
+ const { label, icon, muted } = typeof item === "string" ? { label: item, icon: null, muted: false } : item;
955
+ const color = muted ? theme.muted : theme.text;
956
+ const glyph = icon ?? p.icon;
957
+ const marker = glyph ? /* @__PURE__ */ jsx(IconGlyph, { name: glyph, size: 18, color }) : p.numbered ? /* @__PURE__ */ jsx(Box, { style: { minWidth: 18, color, fontWeight: 600 }, children: `${i + 1}.` }) : /* @__PURE__ */ jsx(Box, { style: { width: 18, height: 22, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx(Box, { style: { width: 6, height: 6, borderRadius: 3, backgroundColor: color } }) });
958
+ return /* @__PURE__ */ jsxs(Box, { style: { gap: 10, alignItems: glyph ? "center" : "flex-start", fontSize: 15, lineHeight: 1.45, color }, children: [
959
+ marker,
960
+ /* @__PURE__ */ jsx(Box, { style: { flexShrink: 1 }, children: label })
961
+ ] }, i);
962
+ }) });
963
+ }
878
964
  function Image({ element, dir }) {
879
965
  const p = element.props;
880
966
  const h = p.height ?? 160;
@@ -930,6 +1016,48 @@ function Icon({ element, dir, aligned }) {
930
1016
  const p = element.props;
931
1017
  return /* @__PURE__ */ jsx(Box, { style: { flexShrink: 0, ...hug(dir, aligned) }, children: /* @__PURE__ */ jsx(IconGlyph, { name: p.name, size: p.size ?? 20 }) });
932
1018
  }
1019
+ var CHART_LINE = [62, 48, 55, 30, 38, 22, 34, 12];
1020
+ var CHART_BARS = [55, 72, 40, 86, 64, 48, 78];
1021
+ function Chart({ element, dir }) {
1022
+ const p = element.props;
1023
+ const kind = p.kind ?? "line";
1024
+ const h = p.height ?? 180;
1025
+ const w = p.width ?? (dir === "row" ? void 0 : "100%");
1026
+ const round = kind === "pie" || kind === "donut";
1027
+ const plot = (() => {
1028
+ if (round) {
1029
+ const slice = (from, to, fill, k) => {
1030
+ const a0 = from / 100 * 2 * Math.PI - Math.PI / 2, a1 = to / 100 * 2 * Math.PI - Math.PI / 2;
1031
+ const x0 = 50 + 46 * Math.cos(a0), y0 = 50 + 46 * Math.sin(a0), x1 = 50 + 46 * Math.cos(a1), y1 = 50 + 46 * Math.sin(a1);
1032
+ return /* @__PURE__ */ jsx("path", { d: `M50 50 L${x0} ${y0} A46 46 0 ${to - from > 50 ? 1 : 0} 1 ${x1} ${y1} Z`, fill, stroke: theme.paper, strokeWidth: "1.5" }, k);
1033
+ };
1034
+ return /* @__PURE__ */ jsxs("svg", { width: h - 32, height: h - 32, viewBox: "0 0 100 100", children: [
1035
+ slice(0, 45, theme.lineStrong, 0),
1036
+ slice(45, 75, theme.line, 1),
1037
+ slice(75, 100, theme.fill2, 2),
1038
+ kind === "donut" ? /* @__PURE__ */ jsx("circle", { cx: "50", cy: "50", r: "26", fill: theme.paper }) : null
1039
+ ] });
1040
+ }
1041
+ const grid = [25, 50, 75].map((y) => /* @__PURE__ */ jsx("line", { x1: "0", y1: y, x2: "300", y2: y, stroke: theme.fill2, strokeWidth: "1", vectorEffect: "non-scaling-stroke" }, y));
1042
+ if (kind === "bar") {
1043
+ const bw = 300 / CHART_BARS.length;
1044
+ return /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 300 100", preserveAspectRatio: "none", children: [
1045
+ grid,
1046
+ CHART_BARS.map((v, i) => /* @__PURE__ */ jsx("rect", { x: i * bw + bw * 0.2, y: 100 - v, width: bw * 0.6, height: v, rx: "2", fill: theme.line }, i))
1047
+ ] });
1048
+ }
1049
+ const pts = CHART_LINE.map((y, i) => `${i / (CHART_LINE.length - 1) * 300},${y}`).join(" ");
1050
+ return /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 300 100", preserveAspectRatio: "none", children: [
1051
+ grid,
1052
+ kind === "area" ? /* @__PURE__ */ jsx("polygon", { points: `0,100 ${pts} 300,100`, fill: theme.fill2 }) : null,
1053
+ /* @__PURE__ */ jsx("polyline", { points: pts, fill: "none", stroke: theme.lineStrong, strokeWidth: "2.5", strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" })
1054
+ ] });
1055
+ })();
1056
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 10, width: w, height: h, flexShrink: 0, ...dir === "row" ? { flexGrow: 1, flexBasis: "0px" } : {} }, children: [
1057
+ p.title ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 13, fontWeight: 600, color: theme.text }, children: p.title }) : null,
1058
+ /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, borderBottom: round ? void 0 : `1.5px solid ${theme.line}`, justifyContent: round ? "center" : void 0, alignItems: round ? "center" : void 0 }, children: plot })
1059
+ ] });
1060
+ }
933
1061
  function AvatarCircle({ initials, size = 40 }) {
934
1062
  return /* @__PURE__ */ jsx(
935
1063
  Box,
@@ -1035,8 +1163,35 @@ function Field({ children, height = 42, top = false, error = false }) {
1035
1163
  }
1036
1164
  );
1037
1165
  }
1038
- function Input({ element, dir }) {
1166
+ function CodeBoxes({ digits, value, c = GRAY }) {
1167
+ return /* @__PURE__ */ jsx(Box, { style: { gap: 8 }, children: Array.from({ length: digits }, (_, i) => /* @__PURE__ */ jsx(
1168
+ Box,
1169
+ {
1170
+ style: {
1171
+ width: 44,
1172
+ height: 52,
1173
+ borderRadius: 8,
1174
+ border: `1.5px solid ${i === value.length ? c.accent : theme.lineStrong}`,
1175
+ alignItems: "center",
1176
+ justifyContent: "center",
1177
+ fontSize: 22,
1178
+ fontWeight: 600,
1179
+ color: theme.ink
1180
+ },
1181
+ children: value[i] ?? ""
1182
+ },
1183
+ i
1184
+ )) });
1185
+ }
1186
+ function Input({ element, dir, colors }) {
1039
1187
  const p = element.props;
1188
+ if (p.type === "code") {
1189
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 6, ...hug(dir) }, children: [
1190
+ /* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
1191
+ /* @__PURE__ */ jsx(CodeBoxes, { digits: p.digits ?? 6, value: String(p.value ?? "").slice(0, p.digits ?? 6), c: colors }),
1192
+ p.helper ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 12, color: p.error ? TONES.danger : theme.muted }, children: p.helper }) : null
1193
+ ] });
1194
+ }
1040
1195
  const rows = p.multiline ?? 1;
1041
1196
  const isPassword = p.type === "password";
1042
1197
  const shown = p.value ?? null;
@@ -1046,14 +1201,15 @@ function Input({ element, dir }) {
1046
1201
  /* @__PURE__ */ jsxs(Field, { height: rows > 1 ? rows * 22 + 20 : 42, top: rows > 1, error: !!p.error, children: [
1047
1202
  p.type === "search" ? /* @__PURE__ */ jsx(IconGlyph, { name: "search", size: 18, color: theme.muted }) : null,
1048
1203
  /* @__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" : "") }),
1049
- isPassword ? /* @__PURE__ */ jsx(IconGlyph, { name: "eye-off", size: 18, color: theme.muted }) : null
1204
+ isPassword ? /* @__PURE__ */ jsx(IconGlyph, { name: "eye-off", size: 18, color: theme.muted }) : null,
1205
+ p.type === "date" ? /* @__PURE__ */ jsx(IconGlyph, { name: "calendar", size: 18, color: theme.muted }) : null
1050
1206
  ] }),
1051
1207
  p.helper ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 12, color: p.error ? TONES.danger : theme.muted }, children: p.helper }) : null
1052
1208
  ] });
1053
1209
  }
1054
1210
  function Select({ element, dir }) {
1055
1211
  const p = element.props;
1056
- return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 6, ...fieldWidth(p.width, dir) }, children: [
1212
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 6, ...fieldWidth(p.width, dir), ...p.grow ? { flexGrow: 1 } : {} }, children: [
1057
1213
  /* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
1058
1214
  /* @__PURE__ */ jsxs(Field, { children: [
1059
1215
  /* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, color: p.value ? theme.ink : theme.muted }, children: p.value ?? p.placeholder ?? "Select\u2026" }),
@@ -1117,6 +1273,133 @@ function Toggle({ element, colors }) {
1117
1273
  /* @__PURE__ */ jsx(Switch, { on: p.on, c: colors })
1118
1274
  ] });
1119
1275
  }
1276
+ function Slider({ element, colors: c = GRAY }) {
1277
+ const p = element.props;
1278
+ const [a, b] = p.range ?? [0, p.value ?? 50];
1279
+ const top = Math.max(a, b);
1280
+ const max = top <= 100 ? 100 : Math.pow(10, Math.floor(Math.log10(top * 1.25))) * Math.ceil(top * 1.25 / Math.pow(10, Math.floor(Math.log10(top * 1.25))));
1281
+ const from = a / max * 100, to = b / max * 100;
1282
+ const thumb = (at, k) => /* @__PURE__ */ jsx(Box, { style: { position: "absolute", left: `${at}%`, top: 0, marginLeft: -10, width: 20, height: 20, borderRadius: 10, backgroundColor: theme.paper, border: `2px solid ${c.accent}` } }, k);
1283
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 10 }, children: [
1284
+ /* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
1285
+ /* @__PURE__ */ jsxs(Box, { style: { position: "relative", height: 20, marginLeft: 10, marginRight: 10 }, children: [
1286
+ /* @__PURE__ */ jsx(Box, { style: { position: "absolute", left: 0, right: 0, top: 8, height: 4, borderRadius: 2, backgroundColor: theme.fill2 } }),
1287
+ /* @__PURE__ */ jsx(Box, { style: { position: "absolute", left: `${from}%`, width: `${to - from}%`, top: 8, height: 4, borderRadius: 2, backgroundColor: c.accent } }),
1288
+ p.range ? thumb(from, 0) : null,
1289
+ thumb(to, 1)
1290
+ ] })
1291
+ ] });
1292
+ }
1293
+ function Progress({ element, dir, aligned, colors: c = GRAY }) {
1294
+ const p = element.props;
1295
+ const value = Math.max(0, Math.min(100, p.value ?? 40));
1296
+ if (p.steps) {
1297
+ const current = Math.min(p.step ?? 1, p.steps);
1298
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 10 }, children: [
1299
+ /* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
1300
+ /* @__PURE__ */ jsx(Box, { style: { alignItems: "center" }, children: Array.from({ length: p.steps }, (_, i) => {
1301
+ const n = i + 1;
1302
+ const done = n < current, now = n === current;
1303
+ const dot = /* @__PURE__ */ jsx(
1304
+ Box,
1305
+ {
1306
+ style: {
1307
+ width: 28,
1308
+ height: 28,
1309
+ flexShrink: 0,
1310
+ borderRadius: 14,
1311
+ alignItems: "center",
1312
+ justifyContent: "center",
1313
+ fontSize: 13,
1314
+ fontWeight: 600,
1315
+ backgroundColor: done ? c.accent : theme.paper,
1316
+ border: `2px solid ${done || now ? c.accent : theme.lineStrong}`,
1317
+ color: now ? c.accentText : theme.muted
1318
+ },
1319
+ children: done ? /* @__PURE__ */ jsx(IconGlyph, { name: "check", size: 14, color: c.onAccent, strokeWidth: 3 }) : String(n)
1320
+ },
1321
+ `d${i}`
1322
+ );
1323
+ return n === 1 ? dot : [/* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, height: 2, backgroundColor: n <= current ? c.accent : theme.line } }, `l${i}`), dot];
1324
+ }) })
1325
+ ] });
1326
+ }
1327
+ if (p.shape === "circle") {
1328
+ const r = 26, circumference = 2 * Math.PI * r;
1329
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 8, alignItems: "center", ...hug(dir, aligned), ...dir === "row" ? { alignSelf: "flex-start" } : {} }, children: [
1330
+ /* @__PURE__ */ jsxs(Box, { style: { position: "relative", width: 64, height: 64, alignItems: "center", justifyContent: "center" }, children: [
1331
+ /* @__PURE__ */ jsxs("svg", { width: "64", height: "64", viewBox: "0 0 64 64", style: { position: "absolute", top: 0, left: 0 }, children: [
1332
+ /* @__PURE__ */ jsx("circle", { cx: "32", cy: "32", r, fill: "none", stroke: theme.fill2, strokeWidth: "6" }),
1333
+ /* @__PURE__ */ jsx("circle", { cx: "32", cy: "32", r, fill: "none", stroke: c.accent, strokeWidth: "6", strokeLinecap: "round", strokeDasharray: `${circumference * value / 100} ${circumference}`, transform: "rotate(-90 32 32)" })
1334
+ ] }),
1335
+ /* @__PURE__ */ jsx(Box, { style: { fontSize: 14, fontWeight: 600, color: theme.ink }, children: `${Math.round(value)}%` })
1336
+ ] }),
1337
+ p.label ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 13, color: theme.muted }, children: p.label }) : null
1338
+ ] });
1339
+ }
1340
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 8 }, children: [
1341
+ p.label ? /* @__PURE__ */ jsxs(Box, { style: { justifyContent: "space-between", fontSize: 13 }, children: [
1342
+ /* @__PURE__ */ jsx(Box, { style: { fontWeight: 600, color: theme.text }, children: p.label }),
1343
+ /* @__PURE__ */ jsx(Box, { style: { color: theme.muted }, children: `${Math.round(value)}%` })
1344
+ ] }) : null,
1345
+ /* @__PURE__ */ jsx(Box, { style: { height: 8, borderRadius: 4, backgroundColor: theme.fill2 }, children: /* @__PURE__ */ jsx(Box, { style: { width: `${value}%`, height: 8, borderRadius: 4, backgroundColor: c.accent } }) })
1346
+ ] });
1347
+ }
1348
+ var MONTHS = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"];
1349
+ function monthGrid(month) {
1350
+ const m = String(month ?? "").toLowerCase().match(/([a-z]+)\s+(\d{4})/);
1351
+ const index = m ? MONTHS.findIndex((name) => name.startsWith(m[1].slice(0, 3))) : -1;
1352
+ if (!m || index < 0) return { start: 3, days: 31 };
1353
+ const year = Number(m[2]);
1354
+ return { start: new Date(Date.UTC(year, index, 1)).getUTCDay(), days: new Date(Date.UTC(year, index + 1, 0)).getUTCDate() };
1355
+ }
1356
+ function Calendar({ element, dir, aligned, colors: c = GRAY }) {
1357
+ const p = element.props;
1358
+ const { start, days } = monthGrid(p.month);
1359
+ const [from, to] = p.range ?? [p.selected ?? 0, p.selected ?? 0];
1360
+ const marked = new Set(p.marked ?? []);
1361
+ const cells = [...Array(start).fill(null), ...Array.from({ length: days }, (_, i) => i + 1)];
1362
+ while (cells.length % 7) cells.push(null);
1363
+ const weeks = Array.from({ length: cells.length / 7 }, (_, w) => cells.slice(w * 7, w * 7 + 7));
1364
+ const CELL = 38;
1365
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 4, padding: 12, border: `1.5px solid ${theme.line}`, borderRadius: 12, backgroundColor: theme.paper, ...hug(dir, aligned) }, children: [
1366
+ /* @__PURE__ */ jsxs(Box, { style: { alignItems: "center", justifyContent: "space-between", padding: "2px 4px 8px" }, children: [
1367
+ /* @__PURE__ */ jsx(IconGlyph, { name: "chevron-left", size: 18, color: theme.muted }),
1368
+ /* @__PURE__ */ jsx(Box, { style: { fontSize: 15, fontWeight: 600, color: theme.ink }, children: p.month ?? "Month" }),
1369
+ /* @__PURE__ */ jsx(IconGlyph, { name: "chevron-right", size: 18, color: theme.muted })
1370
+ ] }),
1371
+ /* @__PURE__ */ jsx(Box, { children: ["S", "M", "T", "W", "T", "F", "S"].map((d, i) => /* @__PURE__ */ jsx(Box, { style: { width: CELL, justifyContent: "center", fontSize: 12, color: theme.muted }, children: d }, i)) }),
1372
+ weeks.map((week, w) => /* @__PURE__ */ jsx(Box, { children: week.map((d, i) => {
1373
+ const end = d != null && (d === from || d === to) && from > 0;
1374
+ const inside = d != null && from > 0 && d > from && d < to;
1375
+ const band = d != null && from > 0 && to > from && d >= from && d <= to;
1376
+ return /* @__PURE__ */ jsxs(Box, { style: { width: CELL, height: CELL, alignItems: "center", justifyContent: "center", position: "relative" }, children: [
1377
+ band ? /* @__PURE__ */ jsx(Box, { style: { position: "absolute", top: 4, bottom: 4, left: d === from ? CELL / 2 : 0, right: d === to ? CELL / 2 : 0, backgroundColor: theme.fill } }) : null,
1378
+ d != null ? /* @__PURE__ */ jsxs(
1379
+ Box,
1380
+ {
1381
+ style: {
1382
+ width: 32,
1383
+ height: 32,
1384
+ borderRadius: 16,
1385
+ flexDirection: "column",
1386
+ alignItems: "center",
1387
+ justifyContent: "center",
1388
+ fontSize: 14,
1389
+ backgroundColor: end ? c.accent : void 0,
1390
+ color: end ? c.onAccent : inside ? theme.ink : theme.text,
1391
+ fontWeight: end ? 600 : 400
1392
+ },
1393
+ children: [
1394
+ String(d),
1395
+ marked.has(d) ? /* @__PURE__ */ jsx(Box, { style: { position: "absolute", bottom: 3, width: 4, height: 4, borderRadius: 2, backgroundColor: end ? c.onAccent : theme.muted } }) : null
1396
+ ]
1397
+ }
1398
+ ) : null
1399
+ ] }, i);
1400
+ }) }, w))
1401
+ ] });
1402
+ }
1120
1403
  var LEADING_ICON = { menu: "menu", back: "arrow-left", close: "x" };
1121
1404
  function NavBar({ element }) {
1122
1405
  const p = element.props;
@@ -1198,6 +1481,28 @@ function Tabs({ element, colors: c = GRAY }) {
1198
1481
  i
1199
1482
  )) });
1200
1483
  }
1484
+ function pageList(pages, current) {
1485
+ if (pages <= 7) return Array.from({ length: pages }, (_, i) => i + 1);
1486
+ const near = [current - 1, current, current + 1].filter((n) => n > 1 && n < pages);
1487
+ const shown = [1, ...near, pages];
1488
+ const out = [];
1489
+ shown.forEach((n, i) => {
1490
+ if (i && n - shown[i - 1] > 1) out.push("\u2026");
1491
+ out.push(n);
1492
+ });
1493
+ return out;
1494
+ }
1495
+ function Pagination({ element, dir, aligned, colors: c = GRAY }) {
1496
+ const p = element.props;
1497
+ const pages = Math.max(1, p.pages ?? 1);
1498
+ const current = Math.min(Math.max(1, p.current ?? 1), pages);
1499
+ const cell = (content, key, on = false) => /* @__PURE__ */ jsx(Box, { style: { minWidth: 34, height: 34, padding: "0 6px", borderRadius: 8, alignItems: "center", justifyContent: "center", fontSize: 14, backgroundColor: on ? c.accent : void 0, color: on ? c.onAccent : theme.text, fontWeight: on ? 600 : 400 }, children: content }, key);
1500
+ return /* @__PURE__ */ jsxs(Box, { style: { gap: 4, alignItems: "center", ...hug(dir, aligned) }, children: [
1501
+ cell(/* @__PURE__ */ jsx(IconGlyph, { name: "chevron-left", size: 18, color: current > 1 ? theme.text : theme.line }), "prev"),
1502
+ pageList(pages, current).map((n, i) => n === "\u2026" ? cell(/* @__PURE__ */ jsx(Box, { style: { color: theme.muted }, children: "\u2026" }), `e${i}`) : cell(String(n), n, n === current)),
1503
+ cell(/* @__PURE__ */ jsx(IconGlyph, { name: "chevron-right", size: 18, color: current < pages ? theme.text : theme.line }), "next")
1504
+ ] });
1505
+ }
1201
1506
  function List({ element, children, dir, stretch }) {
1202
1507
  const dividers = element.props.dividers ?? true;
1203
1508
  const items = withDir(children, "column");
@@ -1353,7 +1658,9 @@ var registry = {
1353
1658
  Spacer,
1354
1659
  Heading,
1355
1660
  Text,
1661
+ Bullets,
1356
1662
  Image,
1663
+ Chart,
1357
1664
  Icon,
1358
1665
  Avatar,
1359
1666
  Badge,
@@ -1363,9 +1670,13 @@ var registry = {
1363
1670
  Radio,
1364
1671
  Toggle,
1365
1672
  Select,
1673
+ Slider,
1674
+ Progress,
1675
+ Calendar,
1366
1676
  NavBar,
1367
1677
  TabBar,
1368
1678
  Tabs,
1679
+ Pagination,
1369
1680
  List,
1370
1681
  ListItem,
1371
1682
  Table,
@@ -1382,6 +1693,10 @@ var PRIMARY_PROP = {
1382
1693
  Heading: "text",
1383
1694
  Text: "text",
1384
1695
  Image: "label",
1696
+ Chart: "title",
1697
+ Slider: "label",
1698
+ Progress: "label",
1699
+ Calendar: "month",
1385
1700
  Icon: "name",
1386
1701
  Avatar: "initials",
1387
1702
  Badge: "label",
@@ -1454,6 +1769,18 @@ function belongsElsewhere(component, word, asProp) {
1454
1769
  }
1455
1770
  return msg;
1456
1771
  }
1772
+ var OBJECT_SWITCHES = /* @__PURE__ */ new Set();
1773
+ for (const info of COMPONENTS.values()) {
1774
+ for (const schema of Object.values(info.props)) {
1775
+ const s = unwrap(schema);
1776
+ if (s?.def?.type !== "array") continue;
1777
+ const el = unwrap(s.def.element);
1778
+ for (const option of el?.def?.type === "union" ? el.def.options.map(unwrap) : [el]) {
1779
+ if (option?.def?.type !== "object") continue;
1780
+ for (const [key, value] of Object.entries(option.shape)) if (unwrap(value)?.def?.type === "boolean") OBJECT_SWITCHES.add(key);
1781
+ }
1782
+ }
1783
+ }
1457
1784
  function bareWords(component) {
1458
1785
  const info = COMPONENTS.get(component.toLowerCase());
1459
1786
  if (!info) throw new Error(`unknown component ${component}`);
@@ -1570,15 +1897,31 @@ function parseValue(c) {
1570
1897
  }
1571
1898
  if (t.value === "{") {
1572
1899
  const obj = {};
1900
+ let lastKey = null;
1573
1901
  while (!c.isSym("}")) {
1574
1902
  c.skipCommas();
1575
1903
  if (c.isSym("}")) break;
1576
1904
  if (c.done()) throw new Error("unclosed {");
1577
1905
  const k = c.next();
1578
1906
  if (!k || k.kind === "sym") throw new Error("expected key in {\u2026}");
1907
+ if ((k.kind === "word" || k.kind === "num") && !(c.isSym("=") || c.isSym(":"))) {
1908
+ if (k.kind === "word" && OBJECT_SWITCHES.has(k.value)) {
1909
+ obj[k.value] = true;
1910
+ lastKey = null;
1911
+ continue;
1912
+ }
1913
+ if (lastKey) {
1914
+ obj[lastKey] = `${obj[lastKey]} ${k.value}`;
1915
+ continue;
1916
+ }
1917
+ obj[k.value] = true;
1918
+ continue;
1919
+ }
1579
1920
  if (!(c.isSym("=") || c.isSym(":"))) throw new Error(`expected = after ${k.value}`);
1580
1921
  c.next();
1922
+ const unquoted = c.peek()?.kind === "word" || c.peek()?.kind === "num";
1581
1923
  obj[k.value] = parseValue(c);
1924
+ lastKey = unquoted ? k.value : null;
1582
1925
  }
1583
1926
  c.next();
1584
1927
  return obj;
@@ -1896,6 +2239,33 @@ function checkSpec(spec) {
1896
2239
  if (p.trailingIcon && trailing !== "icon") conflict("trailingIcon", "trailing=icon", "trailing", trailing);
1897
2240
  if (p.trailingText && trailing !== "text" && trailing !== "badge") conflict("trailingText", "trailing=text or badge", "trailing", trailing);
1898
2241
  }
2242
+ {
2243
+ const p = el.props ?? {};
2244
+ const say = (msg) => issues.push(`${id}: ${msg}`);
2245
+ const backwards = (r) => Array.isArray(r) && r.length === 2 && r[0] > r[1];
2246
+ if (el.type === "Progress") {
2247
+ if (p.step != null && p.steps == null) say(`step only shows with steps (e.g. steps=4 step=${p.step})`);
2248
+ if (p.step != null && p.steps != null && p.step > p.steps) say(`step=${p.step} is past the last step (steps=${p.steps})`);
2249
+ if (p.steps != null && p.shape === "circle") say("a stepper (steps) can't also be a circle; remove one of them");
2250
+ }
2251
+ if (el.type === "Pagination" && p.current != null && p.pages != null && p.current > p.pages) {
2252
+ say(`current=${p.current} is past the last page (pages=${p.pages})`);
2253
+ }
2254
+ if (el.type === "Slider" && backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the smaller number first`);
2255
+ if (el.type === "Slider" && p.range && p.value != null) say("a slider has value (one handle) or range (two), not both");
2256
+ if (el.type === "Calendar") {
2257
+ const { days } = monthGrid(p.month);
2258
+ const all = [p.selected, ...p.range ?? [], ...p.marked ?? []].filter((d) => typeof d === "number");
2259
+ const late = all.find((d) => d > days);
2260
+ if (late) say(`day ${late} isn't in ${p.month ?? "the month"} (it has ${days} days)`);
2261
+ if (backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the earlier day first`);
2262
+ if (p.range && p.selected != null) say("a calendar has selected (one day) or range (several), not both");
2263
+ }
2264
+ if (el.type === "Input" && p.type === "code" && p.value != null && String(p.value).length > (p.digits ?? 6)) {
2265
+ say(`value has ${String(p.value).length} characters but the code has ${p.digits ?? 6} boxes (digits)`);
2266
+ }
2267
+ if (el.type === "Input" && p.digits != null && p.type !== "code") say("digits only applies to type=code");
2268
+ }
1899
2269
  if (el.type === "Table") {
1900
2270
  const { columns, data } = el.props ?? {};
1901
2271
  if (Array.isArray(columns) && Array.isArray(data)) {
@@ -2031,7 +2401,7 @@ function fmt(v) {
2031
2401
  if (typeof v === "number" || typeof v === "boolean" || v === null) return String(v);
2032
2402
  if (Array.isArray(v)) return `[${v.map(fmt).join(", ")}]`;
2033
2403
  if (typeof v === "object") {
2034
- return `{${Object.entries(v).map(([k, x]) => `${k}=${fmt(x)}`).join(" ")}}`;
2404
+ return `{${Object.entries(v).map(([k, x]) => x === true ? k : `${k}=${fmt(x)}`).join(" ")}}`;
2035
2405
  }
2036
2406
  return JSON.stringify(v);
2037
2407
  }
@@ -2145,6 +2515,8 @@ function describeType(t) {
2145
2515
  return s.def.options.map(describeType).join(" | ");
2146
2516
  case "array":
2147
2517
  return `array of ${describeType(s.def.element)}`;
2518
+ case "tuple":
2519
+ return `[${s.def.items.map(describeType).join(", ")}]`;
2148
2520
  case "object":
2149
2521
  return `{ ${Object.entries(s.shape).map(([k, v]) => `${k}: ${describeType(v)}`).join(", ")} }`;
2150
2522
  default:
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  printWireframeText,
7
7
  renderWireframe,
8
8
  wireframePrompt
9
- } from "./chunks/chunk-YCIRLKKD.js";
9
+ } from "./chunks/chunk-2IDCJVKK.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { readFile, writeFile } from "node:fs/promises";
@@ -32,19 +32,32 @@ declare function Divider({ element, dir }: Props): React.JSX.Element;
32
32
  declare function Spacer({ element }: Props): React.JSX.Element;
33
33
  declare function Heading({ element }: Props): React.JSX.Element;
34
34
  declare function Text({ element }: Props): React.JSX.Element;
35
+ declare function Bullets({ element }: Props): React.JSX.Element;
35
36
  declare function Image({ element, dir }: Props): React.JSX.Element;
36
37
  declare function Icon({ element, dir, aligned }: Props): React.JSX.Element;
38
+ declare function Chart({ element, dir }: Props): React.JSX.Element;
37
39
  declare function Avatar({ element, dir, aligned }: Props): React.JSX.Element;
38
40
  declare function Badge({ element, dir, aligned, colors }: Props): React.JSX.Element;
39
41
  declare function Button({ element, dir, aligned, colors: c }: Props): React.JSX.Element;
40
- declare function Input({ element, dir }: Props): React.JSX.Element;
42
+ declare function Input({ element, dir, colors }: Props): React.JSX.Element;
41
43
  declare function Select({ element, dir }: Props): React.JSX.Element;
42
44
  declare function Checkbox({ element, dir, aligned, colors }: Props): React.JSX.Element;
43
45
  declare function Radio({ element, dir, aligned, colors }: Props): React.JSX.Element;
44
46
  declare function Toggle({ element, colors }: Props): React.JSX.Element;
47
+ declare function Slider({ element, colors: c }: Props): React.JSX.Element;
48
+ declare function Progress({ element, dir, aligned, colors: c }: Props): React.JSX.Element;
49
+ /** The weekday (0 = Sunday) the month starts on and its length, from text like "October 2026". Generic if it can't tell. */
50
+ export declare function monthGrid(month?: string | null): {
51
+ start: number;
52
+ days: number;
53
+ };
54
+ declare function Calendar({ element, dir, aligned, colors: c }: Props): React.JSX.Element;
45
55
  declare function NavBar({ element }: Props): React.JSX.Element;
46
56
  declare function TabBar({ element, colors: c }: Props): React.JSX.Element;
47
57
  declare function Tabs({ element, colors: c }: Props): React.JSX.Element;
58
+ /** The page numbers to show: all of them up to 7, otherwise the ends and the current page's neighbours. */
59
+ export declare function pageList(pages: number, current: number): (number | "…")[];
60
+ declare function Pagination({ element, dir, aligned, colors: c }: Props): React.JSX.Element;
48
61
  declare function List({ element, children, dir, stretch }: Props): React.JSX.Element;
49
62
  declare function ListItem({ element, colors }: Props): React.JSX.Element;
50
63
  declare function Table({ element }: Props): React.JSX.Element;
@@ -61,7 +74,9 @@ export declare const registry: {
61
74
  Spacer: typeof Spacer;
62
75
  Heading: typeof Heading;
63
76
  Text: typeof Text;
77
+ Bullets: typeof Bullets;
64
78
  Image: typeof Image;
79
+ Chart: typeof Chart;
65
80
  Icon: typeof Icon;
66
81
  Avatar: typeof Avatar;
67
82
  Badge: typeof Badge;
@@ -71,9 +86,13 @@ export declare const registry: {
71
86
  Radio: typeof Radio;
72
87
  Toggle: typeof Toggle;
73
88
  Select: typeof Select;
89
+ Slider: typeof Slider;
90
+ Progress: typeof Progress;
91
+ Calendar: typeof Calendar;
74
92
  NavBar: typeof NavBar;
75
93
  TabBar: typeof TabBar;
76
94
  Tabs: typeof Tabs;
95
+ Pagination: typeof Pagination;
77
96
  List: typeof List;
78
97
  ListItem: typeof ListItem;
79
98
  Table: typeof Table;
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  upgradeSpec,
21
21
  upgradeWireframe,
22
22
  wireframePrompt
23
- } from "./chunks/chunk-YCIRLKKD.js";
23
+ } from "./chunks/chunk-2IDCJVKK.js";
24
24
 
25
25
  // src/share.ts
26
26
  import { deflateRawSync, inflateRawSync } from "node:zlib";
@@ -11,7 +11,7 @@ import {
11
11
  renderWireframePng,
12
12
  renderWireframeSvg,
13
13
  wireframePrompt
14
- } from "../chunks/chunk-YCIRLKKD.js";
14
+ } from "../chunks/chunk-2IDCJVKK.js";
15
15
 
16
16
  // src/playground/index.ts
17
17
  import { createServer } from "node:http";
@@ -38,7 +38,8 @@ function describe(name, schema) {
38
38
  return { name, kind: "number", description };
39
39
  case "array": {
40
40
  const el = unwrap(s.def.element);
41
- const hasIcon = el?.def?.type === "object" && "icon" in el.shape;
41
+ const objects = el?.def?.type === "union" ? el.def.options.map(unwrap) : [el];
42
+ const hasIcon = objects.some((x) => x?.def?.type === "object" && "icon" in x.shape);
42
43
  return { name, kind: "list", icon: iconish || hasIcon, description };
43
44
  }
44
45
  case "object":
@@ -73,9 +74,9 @@ function languageData() {
73
74
  var GROUPS = [
74
75
  { name: "Canvas", components: ["Board", "Screen", "Note"] },
75
76
  { name: "Layout", components: ["Stack", "Grid", "Card", "Divider", "Spacer"] },
76
- { name: "Content", components: ["Heading", "Text", "Image", "Icon", "Avatar", "Badge"] },
77
- { name: "Controls", components: ["Button", "Input", "Checkbox", "Radio", "Toggle", "Select"] },
78
- { name: "Navigation & data", components: ["NavBar", "TabBar", "Tabs", "List", "ListItem", "Table"] },
77
+ { name: "Content", components: ["Heading", "Text", "Bullets", "Image", "Chart", "Icon", "Avatar", "Badge"] },
78
+ { name: "Controls", components: ["Button", "Input", "Checkbox", "Radio", "Toggle", "Select", "Slider", "Progress", "Calendar"] },
79
+ { name: "Navigation & data", components: ["NavBar", "TabBar", "Tabs", "Pagination", "List", "ListItem", "Table"] },
79
80
  { name: "Overlays", components: ["Modal", "Drawer"] }
80
81
  ];
81
82
  var EXAMPLES = {
@@ -178,6 +179,16 @@ var EXAMPLES = {
178
179
  text "Bold" bold
179
180
  text "Placeholder lines when lines is set and there's no text:" sm muted
180
181
  text lines=3`,
182
+ Bullets: `board
183
+ screen custom width=420 height=600
184
+ text "Bullets" bold
185
+ bullets items=[Fast setup, No credit card, Cancel anytime]
186
+ text "numbered" bold
187
+ bullets numbered items=[Create an account, Verify your email, Invite your team]
188
+ text "icon= any Lucide icon" bold
189
+ bullets icon=check items=[Unlimited boards, Share links, Version history]
190
+ text "One item can change its icon, or be muted" bold
191
+ bullets icon=check items=[Unlimited boards, Share links, {label="SSO" icon=x muted}]`,
181
192
  Image: `board
182
193
  screen custom width=480 height=210
183
194
  stack row gap=12
@@ -185,6 +196,16 @@ var EXAMPLES = {
185
196
  image height=120 width=120 rounded
186
197
  image height=120
187
198
  text "In a row, an image without a width is square." sm muted`,
199
+ Chart: `board
200
+ screen custom width=640 height=430
201
+ stack row gap=16
202
+ chart line "line (default)"
203
+ chart bar "bar"
204
+ stack row gap=16
205
+ chart area "area" height=150
206
+ chart pie "pie" height=150
207
+ chart donut "donut" height=150
208
+ text "Charts are placeholders: they show the kind of chart, not data." sm muted`,
188
209
  Icon: `board
189
210
  screen custom width=360 height=120
190
211
  stack row gap=16 align=center
@@ -228,7 +249,7 @@ var EXAMPLES = {
228
249
  button primary "Full width" fullWidth
229
250
  text "With a board accent, primary buttons fill with it and ghost buttons use it for text." sm muted`,
230
251
  Input: `board
231
- screen custom width=420 height=540
252
+ screen custom width=420 height=760
232
253
  input "Email" placeholder="you@example.com" helper="We never share it"
233
254
  input "Username" value="dana moore" error helper="No spaces allowed"
234
255
  input password "Password" value=secret
@@ -236,24 +257,52 @@ var EXAMPLES = {
236
257
  input "Message" multiline=3
237
258
  stack row gap=8
238
259
  input placeholder="grow fills the row" grow
239
- button primary "Send"`,
260
+ button primary "Send"
261
+ input "Check-in" type=date value="Oct 14, 2026"
262
+ input "Verification code" type=code value="4821" helper="Sent to (555) 010-2400"`,
240
263
  Checkbox: `board
241
264
  screen custom width=300 height=100
242
265
  checkbox "Checked" checked
243
266
  checkbox "Unchecked"`,
244
267
  Radio: `board
245
- screen custom width=300 height=130
268
+ screen custom width=380 height=230
246
269
  radio "Selected" checked
247
270
  radio "Not selected"
248
- radio "Not selected"`,
271
+ radio "Not selected"
272
+ text "Side by side: put them in a row" sm muted
273
+ stack row gap=20
274
+ radio "Standard" checked
275
+ radio "Express"
276
+ radio "Pickup"`,
249
277
  Toggle: `board
250
278
  screen custom width=300 height=100
251
279
  toggle "On" on
252
280
  toggle "Off" off`,
253
281
  Select: `board
254
- screen custom width=320 height=170
282
+ screen custom width=380 height=260
255
283
  select "Country" value="Mexico"
256
- select "Size" placeholder="Choose one" width=160`,
284
+ select "Size" placeholder="Choose one" width=160
285
+ stack row gap=12
286
+ input "City" placeholder="Monterrey" grow
287
+ select "State" value="Nuevo Le\xF3n" grow`,
288
+ Slider: `board
289
+ screen custom width=380 height=200
290
+ slider "Volume" value=30
291
+ slider "Price range" range=[20, 80]`,
292
+ Progress: `board
293
+ screen custom width=420 height=330
294
+ progress "Uploading" value=40
295
+ progress value=75
296
+ stack row gap=24
297
+ progress circle "Storage" value=72
298
+ progress circle value=15
299
+ progress "Setup" steps=4 step=2`,
300
+ Calendar: `board
301
+ screen custom width=660 height=400
302
+ stack row gap=24 align=start
303
+ calendar "October 2026" selected=14
304
+ calendar "October 2026" range=[12, 18] marked=[3, 9, 22]
305
+ text "range selects several days; marked adds a dot, e.g. for events." sm muted`,
257
306
  NavBar: `board
258
307
  screen custom width=380 height=240 padding=0 gap=0
259
308
  navbar "leading=menu" leading=menu actions=[search, bell]
@@ -269,6 +318,10 @@ var EXAMPLES = {
269
318
  screen custom width=400 height=150
270
319
  tabs items=[Overview, Activity, Settings] active=0
271
320
  text lines=2`,
321
+ Pagination: `board
322
+ screen custom width=420 height=160
323
+ pagination pages=5 current=2
324
+ pagination pages=12 current=6`,
272
325
  List: `board
273
326
  screen custom width=380 height=420
274
327
  list
@@ -1,5 +1,5 @@
1
1
  board "Analytics" accent=blue
2
- screen desktop "Dashboard" padding=0 height=640
2
+ screen desktop "Dashboard" padding=0 height=760
3
3
  stack row grow
4
4
  stack width=220 border=right fill padding=16 gap=6
5
5
  text "Acme" bold
@@ -24,4 +24,9 @@ board "Analytics" accent=blue
24
24
  heading "932" level=2
25
25
  card "Refunds"
26
26
  heading "12" level=2
27
+ stack row gap=12
28
+ card "Revenue" grow
29
+ chart area height=170
30
+ card "Traffic sources" width=300
31
+ chart donut height=170
27
32
  table columns=[Order, Customer, Status, Total] data=[["#1042", Ana Torres, Paid, "$120"], ["#1041", Ben Cho, Pending, "$80"], ["#1040", Priya Nair, Paid, "$45"]]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsquare",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",
@@ -44,7 +44,8 @@
44
44
  "examples/*.tsq",
45
45
  "assets/mark.png",
46
46
  "LICENSE",
47
- "README.md"
47
+ "README.md",
48
+ "CHANGELOG.md"
48
49
  ],
49
50
  "engines": {
50
51
  "node": ">=20"
@@ -62,7 +63,8 @@
62
63
  "eval:prompts": "tsx eval/prompts.ts",
63
64
  "eval:score": "tsx eval/score.ts",
64
65
  "build": "tsx scripts/build.ts",
65
- "prepare": "npm run build"
66
+ "prepare": "npm run build",
67
+ "test": "tsx --test test/*.test.ts"
66
68
  },
67
69
  "dependencies": {
68
70
  "@fontsource/inter": "5.3.0",
@@ -9,7 +9,7 @@ Root canvas (artboard). Holds Screens side by side, plus optional Notes. Must be
9
9
  - columns: number
10
10
  - gap: number
11
11
  - padding: number
12
- - accent: string — The one UI color: primary buttons, solid badges, checked controls, toggles, active tabs, ghost buttons. blue, indigo, violet, pink, red, orange, green, teal, or a hex color like #1a73e8. Omit for grayscale.
12
+ - accent: string — The one UI color: primary buttons, solid badges, checked controls, toggles, sliders, progress, selected days and pages, active tabs, ghost buttons. blue, indigo, violet, pink, red, orange, green, teal, or a hex color like #1a73e8. Omit for grayscale.
13
13
 
14
14
  ### Screen
15
15
  One view of the product in a device frame. Children stack vertically.
@@ -78,6 +78,12 @@ Body text, or placeholder lines when `lines` is set and `text` is not.
78
78
  - bold: boolean
79
79
  - align: "left" | "center" | "right"
80
80
 
81
+ ### Bullets
82
+ Bulleted, numbered or icon list of short lines.
83
+ - items (required): array of string | { label: string, icon: string, muted: boolean } — Text, or {label icon muted} to change one line, e.g. {label=SSO icon=x muted}
84
+ - numbered: boolean — 1. 2. 3. instead of dots
85
+ - icon: string — A Lucide icon instead of dots, e.g. check for a feature list
86
+
81
87
  ### Image
82
88
  Image placeholder: a box with an X through it.
83
89
  - height: number
@@ -85,6 +91,13 @@ Image placeholder: a box with an X through it.
85
91
  - label: string (main text)
86
92
  - rounded: boolean
87
93
 
94
+ ### Chart
95
+ Chart placeholder: a generic line, bar, area, pie or donut shape, no data.
96
+ - kind: "line" | "bar" | "area" | "pie" | "donut" — Default line
97
+ - title: string (main text)
98
+ - height: number — Default 180
99
+ - width: number | string — Default fills the container width
100
+
88
101
  ### Icon
89
102
  Line icon from Lucide.
90
103
  - name: string (main text) — Lucide icon name in kebab-case, e.g. menu, search, arrow-left, settings, bell, user
@@ -115,7 +128,8 @@ Text field with optional label. Set multiline for a textarea.
115
128
  - label: string (main text)
116
129
  - placeholder: string
117
130
  - value: string
118
- - type: "text" | "password" | "search" | "email"
131
+ - type: "text" | "password" | "search" | "email" | "date" | "code" — date shows a calendar icon; code draws one box per digit
132
+ - digits: number — Boxes for type=code, default 6
119
133
  - multiline: number — Number of rows; 2 or more makes a textarea
120
134
  - helper: string
121
135
  - error: boolean — Validation error: red border and red helper text
@@ -142,8 +156,30 @@ Dropdown field (closed state).
142
156
  - label: string (main text)
143
157
  - value: string
144
158
  - placeholder: string
159
+ - grow: boolean — Fill the remaining space in a row
145
160
  - width: number | string — Fixed width, e.g. 320. Default fills the space.
146
161
 
162
+ ### Slider
163
+ Slider with one handle, or two with `range`.
164
+ - label: string (main text)
165
+ - value: number — Handle position: 0 to 100, or any amount (the track scales to fit)
166
+ - range: [number, number] — Two handles instead, e.g. [20, 80] or [50, 400]
167
+
168
+ ### Progress
169
+ Progress bar, ring (circle), or stepper (steps).
170
+ - label: string (main text)
171
+ - value: number — Percent done
172
+ - shape: "bar" | "circle" — Default bar; circle is a ring with the percent inside
173
+ - steps: number — Draw a stepper with this many steps instead
174
+ - step: number — The current step, with steps
175
+
176
+ ### Calendar
177
+ Month calendar.
178
+ - month: string (main text) — Month and year, e.g. "October 2026"; the days match that month
179
+ - selected: number
180
+ - range: [number, number] — Selected days, e.g. [12, 18]
181
+ - marked: array of number — Days with a dot, e.g. events
182
+
147
183
  ### NavBar
148
184
  Top app bar. Place first in a Screen.
149
185
  - title: string (main text)
@@ -161,6 +197,11 @@ In-page tabs with an underline on the active one.
161
197
  - items (required): array of string
162
198
  - active: number
163
199
 
200
+ ### Pagination
201
+ Page numbers with previous and next.
202
+ - pages (required): number
203
+ - current: number — Default 1
204
+
164
205
  ### List
165
206
  Vertical list of ListItems.
166
207
  - dividers: boolean