tsquare 0.3.2 → 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 CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
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
+
3
36
  ## 0.3.2
4
37
 
5
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.
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>>;
@@ -351,6 +390,51 @@ export declare const componentDefinitions: {
351
390
  value: string;
352
391
  };
353
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
+ };
354
438
  NavBar: {
355
439
  props: z.ZodObject<{
356
440
  title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -405,6 +489,18 @@ export declare const componentDefinitions: {
405
489
  active: number;
406
490
  };
407
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
+ };
408
504
  List: {
409
505
  props: z.ZodObject<{
410
506
  dividers: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
@@ -716,6 +812,22 @@ export declare const catalog: import("@json-render/core").Catalog<{
716
812
  lines: number;
717
813
  };
718
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
+ };
719
831
  Image: {
720
832
  props: z.ZodObject<{
721
833
  height: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
@@ -730,6 +842,26 @@ export declare const catalog: import("@json-render/core").Catalog<{
730
842
  label: string;
731
843
  };
732
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
+ };
733
865
  Icon: {
734
866
  props: z.ZodObject<{
735
867
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -806,11 +938,14 @@ export declare const catalog: import("@json-render/core").Catalog<{
806
938
  placeholder: z.ZodOptional<z.ZodNullable<z.ZodString>>;
807
939
  value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
808
940
  type: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
941
+ code: "code";
942
+ date: "date";
809
943
  email: "email";
810
944
  password: "password";
811
945
  search: "search";
812
946
  text: "text";
813
947
  }>>>;
948
+ digits: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
814
949
  multiline: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
815
950
  helper: z.ZodOptional<z.ZodNullable<z.ZodString>>;
816
951
  error: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
@@ -875,6 +1010,51 @@ export declare const catalog: import("@json-render/core").Catalog<{
875
1010
  value: string;
876
1011
  };
877
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
+ };
878
1058
  NavBar: {
879
1059
  props: z.ZodObject<{
880
1060
  title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -929,6 +1109,18 @@ export declare const catalog: import("@json-render/core").Catalog<{
929
1109
  active: number;
930
1110
  };
931
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
+ };
932
1124
  List: {
933
1125
  props: z.ZodObject<{
934
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"),
@@ -440,6 +469,39 @@ var componentDefinitions = {
440
469
  description: "Dropdown field (closed state).",
441
470
  example: { label: "Country", value: "United States" }
442
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
+ },
443
505
  // ── Navigation & data ─────────────────────────────────────────────────
444
506
  NavBar: {
445
507
  props: z.object({
@@ -470,6 +532,15 @@ var componentDefinitions = {
470
532
  description: "In-page tabs with an underline on the active one.",
471
533
  example: { items: ["Overview", "Activity", "Settings"], active: 0 }
472
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
+ },
473
544
  List: {
474
545
  props: z.object({ dividers: o(z.boolean()), grow: o(z.boolean()) }),
475
546
  slots: ["default"],
@@ -876,6 +947,20 @@ function Text({ element }) {
876
947
  }
877
948
  );
878
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
+ }
879
964
  function Image({ element, dir }) {
880
965
  const p = element.props;
881
966
  const h = p.height ?? 160;
@@ -931,6 +1016,48 @@ function Icon({ element, dir, aligned }) {
931
1016
  const p = element.props;
932
1017
  return /* @__PURE__ */ jsx(Box, { style: { flexShrink: 0, ...hug(dir, aligned) }, children: /* @__PURE__ */ jsx(IconGlyph, { name: p.name, size: p.size ?? 20 }) });
933
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
+ }
934
1061
  function AvatarCircle({ initials, size = 40 }) {
935
1062
  return /* @__PURE__ */ jsx(
936
1063
  Box,
@@ -1036,8 +1163,35 @@ function Field({ children, height = 42, top = false, error = false }) {
1036
1163
  }
1037
1164
  );
1038
1165
  }
1039
- 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 }) {
1040
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
+ }
1041
1195
  const rows = p.multiline ?? 1;
1042
1196
  const isPassword = p.type === "password";
1043
1197
  const shown = p.value ?? null;
@@ -1047,7 +1201,8 @@ function Input({ element, dir }) {
1047
1201
  /* @__PURE__ */ jsxs(Field, { height: rows > 1 ? rows * 22 + 20 : 42, top: rows > 1, error: !!p.error, children: [
1048
1202
  p.type === "search" ? /* @__PURE__ */ jsx(IconGlyph, { name: "search", size: 18, color: theme.muted }) : null,
1049
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" : "") }),
1050
- 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
1051
1206
  ] }),
1052
1207
  p.helper ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 12, color: p.error ? TONES.danger : theme.muted }, children: p.helper }) : null
1053
1208
  ] });
@@ -1118,6 +1273,133 @@ function Toggle({ element, colors }) {
1118
1273
  /* @__PURE__ */ jsx(Switch, { on: p.on, c: colors })
1119
1274
  ] });
1120
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
+ }
1121
1403
  var LEADING_ICON = { menu: "menu", back: "arrow-left", close: "x" };
1122
1404
  function NavBar({ element }) {
1123
1405
  const p = element.props;
@@ -1199,6 +1481,28 @@ function Tabs({ element, colors: c = GRAY }) {
1199
1481
  i
1200
1482
  )) });
1201
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
+ }
1202
1506
  function List({ element, children, dir, stretch }) {
1203
1507
  const dividers = element.props.dividers ?? true;
1204
1508
  const items = withDir(children, "column");
@@ -1354,7 +1658,9 @@ var registry = {
1354
1658
  Spacer,
1355
1659
  Heading,
1356
1660
  Text,
1661
+ Bullets,
1357
1662
  Image,
1663
+ Chart,
1358
1664
  Icon,
1359
1665
  Avatar,
1360
1666
  Badge,
@@ -1364,9 +1670,13 @@ var registry = {
1364
1670
  Radio,
1365
1671
  Toggle,
1366
1672
  Select,
1673
+ Slider,
1674
+ Progress,
1675
+ Calendar,
1367
1676
  NavBar,
1368
1677
  TabBar,
1369
1678
  Tabs,
1679
+ Pagination,
1370
1680
  List,
1371
1681
  ListItem,
1372
1682
  Table,
@@ -1383,6 +1693,10 @@ var PRIMARY_PROP = {
1383
1693
  Heading: "text",
1384
1694
  Text: "text",
1385
1695
  Image: "label",
1696
+ Chart: "title",
1697
+ Slider: "label",
1698
+ Progress: "label",
1699
+ Calendar: "month",
1386
1700
  Icon: "name",
1387
1701
  Avatar: "initials",
1388
1702
  Badge: "label",
@@ -1455,6 +1769,18 @@ function belongsElsewhere(component, word, asProp) {
1455
1769
  }
1456
1770
  return msg;
1457
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
+ }
1458
1784
  function bareWords(component) {
1459
1785
  const info = COMPONENTS.get(component.toLowerCase());
1460
1786
  if (!info) throw new Error(`unknown component ${component}`);
@@ -1571,15 +1897,31 @@ function parseValue(c) {
1571
1897
  }
1572
1898
  if (t.value === "{") {
1573
1899
  const obj = {};
1900
+ let lastKey = null;
1574
1901
  while (!c.isSym("}")) {
1575
1902
  c.skipCommas();
1576
1903
  if (c.isSym("}")) break;
1577
1904
  if (c.done()) throw new Error("unclosed {");
1578
1905
  const k = c.next();
1579
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
+ }
1580
1920
  if (!(c.isSym("=") || c.isSym(":"))) throw new Error(`expected = after ${k.value}`);
1581
1921
  c.next();
1922
+ const unquoted = c.peek()?.kind === "word" || c.peek()?.kind === "num";
1582
1923
  obj[k.value] = parseValue(c);
1924
+ lastKey = unquoted ? k.value : null;
1583
1925
  }
1584
1926
  c.next();
1585
1927
  return obj;
@@ -1897,6 +2239,33 @@ function checkSpec(spec) {
1897
2239
  if (p.trailingIcon && trailing !== "icon") conflict("trailingIcon", "trailing=icon", "trailing", trailing);
1898
2240
  if (p.trailingText && trailing !== "text" && trailing !== "badge") conflict("trailingText", "trailing=text or badge", "trailing", trailing);
1899
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
+ }
1900
2269
  if (el.type === "Table") {
1901
2270
  const { columns, data } = el.props ?? {};
1902
2271
  if (Array.isArray(columns) && Array.isArray(data)) {
@@ -2032,7 +2401,7 @@ function fmt(v) {
2032
2401
  if (typeof v === "number" || typeof v === "boolean" || v === null) return String(v);
2033
2402
  if (Array.isArray(v)) return `[${v.map(fmt).join(", ")}]`;
2034
2403
  if (typeof v === "object") {
2035
- 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(" ")}}`;
2036
2405
  }
2037
2406
  return JSON.stringify(v);
2038
2407
  }
@@ -2146,6 +2515,8 @@ function describeType(t) {
2146
2515
  return s.def.options.map(describeType).join(" | ");
2147
2516
  case "array":
2148
2517
  return `array of ${describeType(s.def.element)}`;
2518
+ case "tuple":
2519
+ return `[${s.def.items.map(describeType).join(", ")}]`;
2149
2520
  case "object":
2150
2521
  return `{ ${Object.entries(s.shape).map(([k, v]) => `${k}: ${describeType(v)}`).join(", ")} }`;
2151
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-X46EDSCQ.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-X46EDSCQ.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-X46EDSCQ.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,7 +257,9 @@ 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
@@ -262,6 +285,24 @@ var EXAMPLES = {
262
285
  stack row gap=12
263
286
  input "City" placeholder="Monterrey" grow
264
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`,
265
306
  NavBar: `board
266
307
  screen custom width=380 height=240 padding=0 gap=0
267
308
  navbar "leading=menu" leading=menu actions=[search, bell]
@@ -277,6 +318,10 @@ var EXAMPLES = {
277
318
  screen custom width=400 height=150
278
319
  tabs items=[Overview, Activity, Settings] active=0
279
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`,
280
325
  List: `board
281
326
  screen custom width=380 height=420
282
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.2",
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",
@@ -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
@@ -145,6 +159,27 @@ Dropdown field (closed state).
145
159
  - grow: boolean — Fill the remaining space in a row
146
160
  - width: number | string — Fixed width, e.g. 320. Default fills the space.
147
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
+
148
183
  ### NavBar
149
184
  Top app bar. Place first in a Screen.
150
185
  - title: string (main text)
@@ -162,6 +197,11 @@ In-page tabs with an underline on the active one.
162
197
  - items (required): array of string
163
198
  - active: number
164
199
 
200
+ ### Pagination
201
+ Page numbers with previous and next.
202
+ - pages (required): number
203
+ - current: number — Default 1
204
+
165
205
  ### List
166
206
  Vertical list of ListItems.
167
207
  - dividers: boolean