tsquare 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -207,7 +207,7 @@ var wireframeSchema = defineSchema(
207
207
  "The root element MUST be a Board. A Board's children are Screens (and optional Notes placed beside them).",
208
208
  "Every Screen MUST be a direct child of the Board. Each Screen is one view of the product; use several Screens to show several views or states.",
209
209
  "Screens lay out their children top to bottom. Use Stack (direction row/column) and Grid inside a Screen to arrange content.",
210
- "Modal and Drawer are overlays. They MUST be direct children of a Screen, and SHOULD be listed last in the Screen's children so they draw on top.",
210
+ "Modal, Drawer and Toast are overlays. They MUST be direct children of a Screen, and SHOULD be listed last in the Screen's children so they draw on top.",
211
211
  "NavBar goes first in a Screen's children and TabBar goes last (before any overlay).",
212
212
  "This is a low-fidelity wireframe: prefer placeholders (Image boxes, Text with lines) over invented copy, unless the copy matters to the design.",
213
213
  'Every element MUST include a "children" array. Leaf elements use an empty array: "children": [].',
@@ -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"],
@@ -303,6 +310,15 @@ var componentDefinitions = {
303
310
  description: "Bordered container with optional title. Children stack vertically.",
304
311
  example: { title: "Revenue", variant: "outline" }
305
312
  },
313
+ Accordion: {
314
+ props: z.object({
315
+ title: o(z.string()),
316
+ open: o(z.boolean()).describe("Show the children. Default closed: only the title row")
317
+ }),
318
+ slots: ["default"],
319
+ description: "Collapsible section: a title row with a chevron, children below when open. Stack several for an FAQ.",
320
+ example: { title: "Shipping", open: true }
321
+ },
306
322
  Divider: {
307
323
  props: z.object({ vertical: o(z.boolean()) }),
308
324
  slots: [],
@@ -341,17 +357,39 @@ var componentDefinitions = {
341
357
  description: "Body text, or placeholder lines when `lines` is set and `text` is not.",
342
358
  example: { lines: 3 }
343
359
  },
360
+ Bullets: {
361
+ props: z.object({
362
+ items: z.array(bulletItem).describe("Text, or {label icon muted} to change one line, e.g. {label=SSO icon=x muted}"),
363
+ numbered: o(z.boolean()).describe("1. 2. 3. instead of dots"),
364
+ icon: o(iconName).describe("A Lucide icon instead of dots, e.g. check for a feature list")
365
+ }),
366
+ slots: [],
367
+ description: "Bulleted, numbered or icon list of short lines.",
368
+ example: { items: ["Fast setup", "No credit card"] }
369
+ },
344
370
  Image: {
345
371
  props: z.object({
346
372
  height: o(z.number()),
347
373
  width: o(z.union([z.number(), z.string()])).describe("Default fills the container width"),
348
374
  label: o(z.string()),
349
- rounded: o(z.boolean())
375
+ rounded: o(z.boolean()),
376
+ kind: o(z.enum(["photo", "map"])).describe("map draws roads and a pin instead of the X")
350
377
  }),
351
378
  slots: [],
352
- description: "Image placeholder: a box with an X through it.",
379
+ description: "Image placeholder: a box with an X through it, or a map.",
353
380
  example: { height: 180, label: "Hero photo" }
354
381
  },
382
+ Chart: {
383
+ props: z.object({
384
+ kind: o(z.enum(["line", "bar", "area", "pie", "donut"])).describe("Default line"),
385
+ title: o(z.string()),
386
+ height: o(z.number()).describe("Default 180"),
387
+ width: o(z.union([z.number(), z.string()])).describe("Default fills the container width")
388
+ }),
389
+ slots: [],
390
+ description: "Chart placeholder: a generic line, bar, area, pie or donut shape, no data.",
391
+ example: { kind: "bar", title: "Signups" }
392
+ },
355
393
  Icon: {
356
394
  props: z.object({
357
395
  name: o(iconName),
@@ -399,7 +437,8 @@ var componentDefinitions = {
399
437
  label: o(z.string()),
400
438
  placeholder: o(z.string()),
401
439
  value: o(z.string()),
402
- type: o(z.enum(["text", "password", "search", "email"])),
440
+ type: o(z.enum(["text", "password", "search", "email", "date", "code"])).describe("date shows a calendar icon; code draws one box per digit"),
441
+ digits: o(z.number().int().min(2).max(10)).describe("Boxes for type=code, default 6"),
403
442
  multiline: o(z.number().int().min(1)).describe("Number of rows; 2 or more makes a textarea"),
404
443
  helper: o(z.string()),
405
444
  error: o(z.boolean()).describe("Validation error: red border and red helper text"),
@@ -440,6 +479,39 @@ var componentDefinitions = {
440
479
  description: "Dropdown field (closed state).",
441
480
  example: { label: "Country", value: "United States" }
442
481
  },
482
+ Slider: {
483
+ props: z.object({
484
+ label: o(z.string()),
485
+ value: o(z.number().min(0)).describe("Handle position: 0 to 100, or any amount (the track scales to fit)"),
486
+ range: o(z.tuple([z.number().min(0), z.number().min(0)])).describe("Two handles instead, e.g. [20, 80] or [50, 400]")
487
+ }),
488
+ slots: [],
489
+ description: "Slider with one handle, or two with `range`.",
490
+ example: { label: "Volume", value: 30 }
491
+ },
492
+ Progress: {
493
+ props: z.object({
494
+ label: o(z.string()),
495
+ value: o(percent).describe("Percent done"),
496
+ shape: o(z.enum(["bar", "circle"])).describe("Default bar; circle is a ring with the percent inside"),
497
+ steps: o(z.number().int().min(2).max(10)).describe("Draw a stepper with this many steps instead"),
498
+ step: o(z.number().int().min(1)).describe("The current step, with steps")
499
+ }),
500
+ slots: [],
501
+ description: "Progress bar, ring (circle), or stepper (steps).",
502
+ example: { label: "Uploading", value: 40 }
503
+ },
504
+ Calendar: {
505
+ props: z.object({
506
+ month: o(z.string()).describe('Month and year, e.g. "October 2026"; the days match that month'),
507
+ selected: o(day),
508
+ range: o(z.tuple([day, day])).describe("Selected days, e.g. [12, 18]"),
509
+ marked: o(z.array(day)).describe("Days with a dot, e.g. events")
510
+ }),
511
+ slots: [],
512
+ description: "Month calendar.",
513
+ example: { month: "October 2026", selected: 14 }
514
+ },
443
515
  // ── Navigation & data ─────────────────────────────────────────────────
444
516
  NavBar: {
445
517
  props: z.object({
@@ -470,6 +542,15 @@ var componentDefinitions = {
470
542
  description: "In-page tabs with an underline on the active one.",
471
543
  example: { items: ["Overview", "Activity", "Settings"], active: 0 }
472
544
  },
545
+ Pagination: {
546
+ props: z.object({
547
+ pages: z.number().int().min(1),
548
+ current: o(z.number().int().min(1)).describe("Default 1")
549
+ }),
550
+ slots: [],
551
+ description: "Page numbers with previous and next.",
552
+ example: { pages: 12, current: 3 }
553
+ },
473
554
  List: {
474
555
  props: z.object({ dividers: o(z.boolean()), grow: o(z.boolean()) }),
475
556
  slots: ["default"],
@@ -522,6 +603,17 @@ var componentDefinitions = {
522
603
  slots: ["default"],
523
604
  description: "Panel sliding in from an edge: side nav (left), filters/details (right), or bottom sheet. Must be a direct child of a Screen, listed last.",
524
605
  example: { side: "left", title: "Menu", size: 300 }
606
+ },
607
+ Toast: {
608
+ props: z.object({
609
+ text: o(z.string()),
610
+ action: o(z.string()).describe('A text button, e.g. "Undo"'),
611
+ icon: o(iconName),
612
+ position: o(z.enum(["bottom", "top"])).describe("Default bottom, above a TabBar")
613
+ }),
614
+ slots: [],
615
+ description: "Short message over the screen, e.g. after saving. Must be a direct child of a Screen, listed last.",
616
+ example: { text: "Message sent", action: "Undo" }
525
617
  }
526
618
  };
527
619
  var catalog = defineCatalog(wireframeSchema, {
@@ -672,7 +764,7 @@ function Screen({ element, children }) {
672
764
  const type = isValidElement(c) ? c.props.element?.type : null;
673
765
  if (type === "NavBar") top.push(c);
674
766
  else if (type === "TabBar") bottom.push(c);
675
- else if (type === "Modal" || type === "Drawer") overlays.push(c);
767
+ else if (type === "Modal" || type === "Drawer" || type === "Toast") overlays.push(c);
676
768
  else body.push(c);
677
769
  }
678
770
  return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", flexShrink: 0 }, children: [
@@ -711,7 +803,7 @@ function Screen({ element, children }) {
711
803
  }
712
804
  ),
713
805
  bottom,
714
- overlays
806
+ overlays.map((o2) => isValidElement(o2) ? cloneElement(o2, { aboveTabBar: bottom.length > 0 }) : o2)
715
807
  ] })
716
808
  ]
717
809
  }
@@ -814,6 +906,16 @@ function Card({ element, children, dir, stretch }) {
814
906
  }
815
907
  );
816
908
  }
909
+ function Accordion({ element, children }) {
910
+ const p = element.props;
911
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", borderBottom: `1px solid ${theme.line}` }, children: [
912
+ /* @__PURE__ */ jsxs(Box, { style: { alignItems: "center", justifyContent: "space-between", gap: 12, padding: "12px 0" }, children: [
913
+ /* @__PURE__ */ jsx(Box, { style: { fontSize: 15, fontWeight: 600, color: theme.ink }, children: p.title ?? "" }),
914
+ /* @__PURE__ */ jsx(IconGlyph, { name: p.open ? "chevron-up" : "chevron-down", size: 18, color: theme.muted })
915
+ ] }),
916
+ p.open ? /* @__PURE__ */ jsx(Box, { style: { flexDirection: "column", gap: 10, paddingBottom: 14 }, children: withDir(children, "column") }) : null
917
+ ] });
918
+ }
817
919
  function Divider({ element, dir }) {
818
920
  const vertical = element.props.vertical ?? dir === "row";
819
921
  return vertical ? /* @__PURE__ */ jsx(Box, { style: { width: 1, alignSelf: "stretch", backgroundColor: theme.line, flexShrink: 0 } }) : /* @__PURE__ */ jsx(Box, { style: { height: 1, alignSelf: "stretch", backgroundColor: theme.line, flexShrink: 0 } });
@@ -876,6 +978,21 @@ function Text({ element }) {
876
978
  }
877
979
  );
878
980
  }
981
+ function Bullets({ element }) {
982
+ const p = element.props;
983
+ const items = p.items ?? [];
984
+ return /* @__PURE__ */ jsx(Box, { style: { flexDirection: "column", gap: 8 }, children: items.map((item, i) => {
985
+ const { label, icon, muted } = typeof item === "string" ? { label: item, icon: null, muted: false } : item;
986
+ const color = muted ? theme.muted : theme.text;
987
+ const glyph = icon ?? p.icon;
988
+ 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 } }) });
989
+ return /* @__PURE__ */ jsxs(Box, { style: { gap: 10, alignItems: glyph ? "center" : "flex-start", fontSize: 15, lineHeight: 1.45, color }, children: [
990
+ marker,
991
+ /* @__PURE__ */ jsx(Box, { style: { flexShrink: 1 }, children: label })
992
+ ] }, i);
993
+ }) });
994
+ }
995
+ var MAP_ROADS = ["M0 42 C30 38 60 50 100 44", "M38 0 C42 30 34 60 44 100", "M0 78 L100 70", "M72 0 L80 100", "M0 16 L40 22", "M44 60 L100 92"];
879
996
  function Image({ element, dir }) {
880
997
  const p = element.props;
881
998
  const h = p.height ?? 160;
@@ -896,7 +1013,12 @@ function Image({ element, dir }) {
896
1013
  justifyContent: "center"
897
1014
  },
898
1015
  children: [
899
- /* @__PURE__ */ jsxs(
1016
+ p.kind === "map" ? /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", preserveAspectRatio: "none", style: { position: "absolute", top: 0, left: 0 }, children: [
1017
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: "100", height: "100", fill: theme.fill2 }),
1018
+ /* @__PURE__ */ jsx("rect", { x: "62", y: "8", width: "26", height: "22", fill: theme.fill }),
1019
+ /* @__PURE__ */ jsx("rect", { x: "8", y: "64", width: "20", height: "26", fill: theme.fill }),
1020
+ MAP_ROADS.map((d, i) => /* @__PURE__ */ jsx("path", { d, fill: "none", stroke: theme.paper, strokeWidth: i < 2 ? 7 : 4, vectorEffect: "non-scaling-stroke" }, i))
1021
+ ] }) : /* @__PURE__ */ jsxs(
900
1022
  "svg",
901
1023
  {
902
1024
  width: "100%",
@@ -910,6 +1032,10 @@ function Image({ element, dir }) {
910
1032
  ]
911
1033
  }
912
1034
  ),
1035
+ p.kind === "map" ? /* @__PURE__ */ jsx(Box, { style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, alignItems: "center", justifyContent: "center", paddingBottom: 28 }, children: /* @__PURE__ */ jsxs("svg", { width: "28", height: "36", viewBox: "0 0 24 31", children: [
1036
+ /* @__PURE__ */ jsx("path", { d: "M12 1C6.5 1 2 5.3 2 10.7 2 18 12 30 12 30s10-12 10-19.3C22 5.3 17.5 1 12 1z", fill: theme.ink }),
1037
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "10.5", r: "3.6", fill: theme.paper })
1038
+ ] }) }) : null,
913
1039
  p.label ? /* @__PURE__ */ jsx(
914
1040
  Box,
915
1041
  {
@@ -918,7 +1044,9 @@ function Image({ element, dir }) {
918
1044
  fontSize: 12,
919
1045
  color: theme.muted,
920
1046
  backgroundColor: theme.fill,
921
- borderRadius: 4
1047
+ borderRadius: 4,
1048
+ // a map keeps its pin in the middle, so the label goes in the corner
1049
+ ...p.kind === "map" ? { position: "absolute", left: 8, bottom: 8, backgroundColor: theme.paper } : {}
922
1050
  },
923
1051
  children: p.label
924
1052
  }
@@ -931,6 +1059,48 @@ function Icon({ element, dir, aligned }) {
931
1059
  const p = element.props;
932
1060
  return /* @__PURE__ */ jsx(Box, { style: { flexShrink: 0, ...hug(dir, aligned) }, children: /* @__PURE__ */ jsx(IconGlyph, { name: p.name, size: p.size ?? 20 }) });
933
1061
  }
1062
+ var CHART_LINE = [62, 48, 55, 30, 38, 22, 34, 12];
1063
+ var CHART_BARS = [55, 72, 40, 86, 64, 48, 78];
1064
+ function Chart({ element, dir }) {
1065
+ const p = element.props;
1066
+ const kind = p.kind ?? "line";
1067
+ const h = p.height ?? 180;
1068
+ const w = p.width ?? (dir === "row" ? void 0 : "100%");
1069
+ const round = kind === "pie" || kind === "donut";
1070
+ const plot = (() => {
1071
+ if (round) {
1072
+ const slice = (from, to, fill, k) => {
1073
+ const a0 = from / 100 * 2 * Math.PI - Math.PI / 2, a1 = to / 100 * 2 * Math.PI - Math.PI / 2;
1074
+ 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);
1075
+ 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);
1076
+ };
1077
+ return /* @__PURE__ */ jsxs("svg", { width: h - 32, height: h - 32, viewBox: "0 0 100 100", children: [
1078
+ slice(0, 45, theme.lineStrong, 0),
1079
+ slice(45, 75, theme.line, 1),
1080
+ slice(75, 100, theme.fill2, 2),
1081
+ kind === "donut" ? /* @__PURE__ */ jsx("circle", { cx: "50", cy: "50", r: "26", fill: theme.paper }) : null
1082
+ ] });
1083
+ }
1084
+ 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));
1085
+ if (kind === "bar") {
1086
+ const bw = 300 / CHART_BARS.length;
1087
+ return /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 300 100", preserveAspectRatio: "none", children: [
1088
+ grid,
1089
+ 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))
1090
+ ] });
1091
+ }
1092
+ const pts = CHART_LINE.map((y, i) => `${i / (CHART_LINE.length - 1) * 300},${y}`).join(" ");
1093
+ return /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 300 100", preserveAspectRatio: "none", children: [
1094
+ grid,
1095
+ kind === "area" ? /* @__PURE__ */ jsx("polygon", { points: `0,100 ${pts} 300,100`, fill: theme.fill2 }) : null,
1096
+ /* @__PURE__ */ jsx("polyline", { points: pts, fill: "none", stroke: theme.lineStrong, strokeWidth: "2.5", strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" })
1097
+ ] });
1098
+ })();
1099
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 10, width: w, height: h, flexShrink: 0, ...dir === "row" ? { flexGrow: 1, flexBasis: "0px" } : {} }, children: [
1100
+ p.title ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 13, fontWeight: 600, color: theme.text }, children: p.title }) : null,
1101
+ /* @__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 })
1102
+ ] });
1103
+ }
934
1104
  function AvatarCircle({ initials, size = 40 }) {
935
1105
  return /* @__PURE__ */ jsx(
936
1106
  Box,
@@ -1036,8 +1206,35 @@ function Field({ children, height = 42, top = false, error = false }) {
1036
1206
  }
1037
1207
  );
1038
1208
  }
1039
- function Input({ element, dir }) {
1209
+ function CodeBoxes({ digits, value, c = GRAY }) {
1210
+ return /* @__PURE__ */ jsx(Box, { style: { gap: 8 }, children: Array.from({ length: digits }, (_, i) => /* @__PURE__ */ jsx(
1211
+ Box,
1212
+ {
1213
+ style: {
1214
+ width: 44,
1215
+ height: 52,
1216
+ borderRadius: 8,
1217
+ border: `1.5px solid ${i === value.length ? c.accent : theme.lineStrong}`,
1218
+ alignItems: "center",
1219
+ justifyContent: "center",
1220
+ fontSize: 22,
1221
+ fontWeight: 600,
1222
+ color: theme.ink
1223
+ },
1224
+ children: value[i] ?? ""
1225
+ },
1226
+ i
1227
+ )) });
1228
+ }
1229
+ function Input({ element, dir, colors }) {
1040
1230
  const p = element.props;
1231
+ if (p.type === "code") {
1232
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 6, ...hug(dir) }, children: [
1233
+ /* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
1234
+ /* @__PURE__ */ jsx(CodeBoxes, { digits: p.digits ?? 6, value: String(p.value ?? "").slice(0, p.digits ?? 6), c: colors }),
1235
+ p.helper ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 12, color: p.error ? TONES.danger : theme.muted }, children: p.helper }) : null
1236
+ ] });
1237
+ }
1041
1238
  const rows = p.multiline ?? 1;
1042
1239
  const isPassword = p.type === "password";
1043
1240
  const shown = p.value ?? null;
@@ -1047,7 +1244,8 @@ function Input({ element, dir }) {
1047
1244
  /* @__PURE__ */ jsxs(Field, { height: rows > 1 ? rows * 22 + 20 : 42, top: rows > 1, error: !!p.error, children: [
1048
1245
  p.type === "search" ? /* @__PURE__ */ jsx(IconGlyph, { name: "search", size: 18, color: theme.muted }) : null,
1049
1246
  /* @__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
1247
+ isPassword ? /* @__PURE__ */ jsx(IconGlyph, { name: "eye-off", size: 18, color: theme.muted }) : null,
1248
+ p.type === "date" ? /* @__PURE__ */ jsx(IconGlyph, { name: "calendar", size: 18, color: theme.muted }) : null
1051
1249
  ] }),
1052
1250
  p.helper ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 12, color: p.error ? TONES.danger : theme.muted }, children: p.helper }) : null
1053
1251
  ] });
@@ -1118,6 +1316,133 @@ function Toggle({ element, colors }) {
1118
1316
  /* @__PURE__ */ jsx(Switch, { on: p.on, c: colors })
1119
1317
  ] });
1120
1318
  }
1319
+ function Slider({ element, colors: c = GRAY }) {
1320
+ const p = element.props;
1321
+ const [a, b] = p.range ?? [0, p.value ?? 50];
1322
+ const top = Math.max(a, b);
1323
+ 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))));
1324
+ const from = a / max * 100, to = b / max * 100;
1325
+ 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);
1326
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 10 }, children: [
1327
+ /* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
1328
+ /* @__PURE__ */ jsxs(Box, { style: { position: "relative", height: 20, marginLeft: 10, marginRight: 10 }, children: [
1329
+ /* @__PURE__ */ jsx(Box, { style: { position: "absolute", left: 0, right: 0, top: 8, height: 4, borderRadius: 2, backgroundColor: theme.fill2 } }),
1330
+ /* @__PURE__ */ jsx(Box, { style: { position: "absolute", left: `${from}%`, width: `${to - from}%`, top: 8, height: 4, borderRadius: 2, backgroundColor: c.accent } }),
1331
+ p.range ? thumb(from, 0) : null,
1332
+ thumb(to, 1)
1333
+ ] })
1334
+ ] });
1335
+ }
1336
+ function Progress({ element, dir, aligned, colors: c = GRAY }) {
1337
+ const p = element.props;
1338
+ const value = Math.max(0, Math.min(100, p.value ?? 40));
1339
+ if (p.steps) {
1340
+ const current = Math.min(p.step ?? 1, p.steps);
1341
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 10 }, children: [
1342
+ /* @__PURE__ */ jsx(FieldLabel, { text: p.label }),
1343
+ /* @__PURE__ */ jsx(Box, { style: { alignItems: "center" }, children: Array.from({ length: p.steps }, (_, i) => {
1344
+ const n = i + 1;
1345
+ const done = n < current, now = n === current;
1346
+ const dot = /* @__PURE__ */ jsx(
1347
+ Box,
1348
+ {
1349
+ style: {
1350
+ width: 28,
1351
+ height: 28,
1352
+ flexShrink: 0,
1353
+ borderRadius: 14,
1354
+ alignItems: "center",
1355
+ justifyContent: "center",
1356
+ fontSize: 13,
1357
+ fontWeight: 600,
1358
+ backgroundColor: done ? c.accent : theme.paper,
1359
+ border: `2px solid ${done || now ? c.accent : theme.lineStrong}`,
1360
+ color: now ? c.accentText : theme.muted
1361
+ },
1362
+ children: done ? /* @__PURE__ */ jsx(IconGlyph, { name: "check", size: 14, color: c.onAccent, strokeWidth: 3 }) : String(n)
1363
+ },
1364
+ `d${i}`
1365
+ );
1366
+ return n === 1 ? dot : [/* @__PURE__ */ jsx(Box, { style: { flexGrow: 1, height: 2, backgroundColor: n <= current ? c.accent : theme.line } }, `l${i}`), dot];
1367
+ }) })
1368
+ ] });
1369
+ }
1370
+ if (p.shape === "circle") {
1371
+ const r = 26, circumference = 2 * Math.PI * r;
1372
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 8, alignItems: "center", ...hug(dir, aligned), ...dir === "row" ? { alignSelf: "flex-start" } : {} }, children: [
1373
+ /* @__PURE__ */ jsxs(Box, { style: { position: "relative", width: 64, height: 64, alignItems: "center", justifyContent: "center" }, children: [
1374
+ /* @__PURE__ */ jsxs("svg", { width: "64", height: "64", viewBox: "0 0 64 64", style: { position: "absolute", top: 0, left: 0 }, children: [
1375
+ /* @__PURE__ */ jsx("circle", { cx: "32", cy: "32", r, fill: "none", stroke: theme.fill2, strokeWidth: "6" }),
1376
+ /* @__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)" })
1377
+ ] }),
1378
+ /* @__PURE__ */ jsx(Box, { style: { fontSize: 14, fontWeight: 600, color: theme.ink }, children: `${Math.round(value)}%` })
1379
+ ] }),
1380
+ p.label ? /* @__PURE__ */ jsx(Box, { style: { fontSize: 13, color: theme.muted }, children: p.label }) : null
1381
+ ] });
1382
+ }
1383
+ return /* @__PURE__ */ jsxs(Box, { style: { flexDirection: "column", gap: 8 }, children: [
1384
+ p.label ? /* @__PURE__ */ jsxs(Box, { style: { justifyContent: "space-between", fontSize: 13 }, children: [
1385
+ /* @__PURE__ */ jsx(Box, { style: { fontWeight: 600, color: theme.text }, children: p.label }),
1386
+ /* @__PURE__ */ jsx(Box, { style: { color: theme.muted }, children: `${Math.round(value)}%` })
1387
+ ] }) : null,
1388
+ /* @__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 } }) })
1389
+ ] });
1390
+ }
1391
+ var MONTHS = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"];
1392
+ function monthGrid(month) {
1393
+ const m = String(month ?? "").toLowerCase().match(/([a-z]+)\s+(\d{4})/);
1394
+ const index = m ? MONTHS.findIndex((name) => name.startsWith(m[1].slice(0, 3))) : -1;
1395
+ if (!m || index < 0) return { start: 3, days: 31 };
1396
+ const year = Number(m[2]);
1397
+ return { start: new Date(Date.UTC(year, index, 1)).getUTCDay(), days: new Date(Date.UTC(year, index + 1, 0)).getUTCDate() };
1398
+ }
1399
+ function Calendar({ element, dir, aligned, colors: c = GRAY }) {
1400
+ const p = element.props;
1401
+ const { start, days } = monthGrid(p.month);
1402
+ const [from, to] = p.range ?? [p.selected ?? 0, p.selected ?? 0];
1403
+ const marked = new Set(p.marked ?? []);
1404
+ const cells = [...Array(start).fill(null), ...Array.from({ length: days }, (_, i) => i + 1)];
1405
+ while (cells.length % 7) cells.push(null);
1406
+ const weeks = Array.from({ length: cells.length / 7 }, (_, w) => cells.slice(w * 7, w * 7 + 7));
1407
+ const CELL = 38;
1408
+ 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: [
1409
+ /* @__PURE__ */ jsxs(Box, { style: { alignItems: "center", justifyContent: "space-between", padding: "2px 4px 8px" }, children: [
1410
+ /* @__PURE__ */ jsx(IconGlyph, { name: "chevron-left", size: 18, color: theme.muted }),
1411
+ /* @__PURE__ */ jsx(Box, { style: { fontSize: 15, fontWeight: 600, color: theme.ink }, children: p.month ?? "Month" }),
1412
+ /* @__PURE__ */ jsx(IconGlyph, { name: "chevron-right", size: 18, color: theme.muted })
1413
+ ] }),
1414
+ /* @__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)) }),
1415
+ weeks.map((week, w) => /* @__PURE__ */ jsx(Box, { children: week.map((d, i) => {
1416
+ const end = d != null && (d === from || d === to) && from > 0;
1417
+ const inside = d != null && from > 0 && d > from && d < to;
1418
+ const band = d != null && from > 0 && to > from && d >= from && d <= to;
1419
+ return /* @__PURE__ */ jsxs(Box, { style: { width: CELL, height: CELL, alignItems: "center", justifyContent: "center", position: "relative" }, children: [
1420
+ 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,
1421
+ d != null ? /* @__PURE__ */ jsxs(
1422
+ Box,
1423
+ {
1424
+ style: {
1425
+ width: 32,
1426
+ height: 32,
1427
+ borderRadius: 16,
1428
+ flexDirection: "column",
1429
+ alignItems: "center",
1430
+ justifyContent: "center",
1431
+ fontSize: 14,
1432
+ backgroundColor: end ? c.accent : void 0,
1433
+ color: end ? c.onAccent : inside ? theme.ink : theme.text,
1434
+ fontWeight: end ? 600 : 400
1435
+ },
1436
+ children: [
1437
+ String(d),
1438
+ marked.has(d) ? /* @__PURE__ */ jsx(Box, { style: { position: "absolute", bottom: 3, width: 4, height: 4, borderRadius: 2, backgroundColor: end ? c.onAccent : theme.muted } }) : null
1439
+ ]
1440
+ }
1441
+ ) : null
1442
+ ] }, i);
1443
+ }) }, w))
1444
+ ] });
1445
+ }
1121
1446
  var LEADING_ICON = { menu: "menu", back: "arrow-left", close: "x" };
1122
1447
  function NavBar({ element }) {
1123
1448
  const p = element.props;
@@ -1199,6 +1524,28 @@ function Tabs({ element, colors: c = GRAY }) {
1199
1524
  i
1200
1525
  )) });
1201
1526
  }
1527
+ function pageList(pages, current) {
1528
+ if (pages <= 7) return Array.from({ length: pages }, (_, i) => i + 1);
1529
+ const near = [current - 1, current, current + 1].filter((n) => n > 1 && n < pages);
1530
+ const shown = [1, ...near, pages];
1531
+ const out = [];
1532
+ shown.forEach((n, i) => {
1533
+ if (i && n - shown[i - 1] > 1) out.push("\u2026");
1534
+ out.push(n);
1535
+ });
1536
+ return out;
1537
+ }
1538
+ function Pagination({ element, dir, aligned, colors: c = GRAY }) {
1539
+ const p = element.props;
1540
+ const pages = Math.max(1, p.pages ?? 1);
1541
+ const current = Math.min(Math.max(1, p.current ?? 1), pages);
1542
+ 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);
1543
+ return /* @__PURE__ */ jsxs(Box, { style: { gap: 4, alignItems: "center", ...hug(dir, aligned) }, children: [
1544
+ cell(/* @__PURE__ */ jsx(IconGlyph, { name: "chevron-left", size: 18, color: current > 1 ? theme.text : theme.line }), "prev"),
1545
+ 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)),
1546
+ cell(/* @__PURE__ */ jsx(IconGlyph, { name: "chevron-right", size: 18, color: current < pages ? theme.text : theme.line }), "next")
1547
+ ] });
1548
+ }
1202
1549
  function List({ element, children, dir, stretch }) {
1203
1550
  const dividers = element.props.dividers ?? true;
1204
1551
  const items = withDir(children, "column");
@@ -1343,6 +1690,15 @@ function Drawer({ element, children }) {
1343
1690
  }
1344
1691
  ) });
1345
1692
  }
1693
+ function Toast({ element, aboveTabBar }) {
1694
+ const p = element.props;
1695
+ const top = p.position === "top";
1696
+ return /* @__PURE__ */ jsx(Box, { style: { position: "absolute", left: 16, right: 16, justifyContent: "center", ...top ? { top: 16 } : { bottom: aboveTabBar ? 92 : 24 } }, children: /* @__PURE__ */ jsxs(Box, { style: { alignItems: "center", gap: 12, padding: "12px 16px", borderRadius: 12, backgroundColor: theme.ink, color: theme.onPrimary, fontSize: 14, boxShadow: "0 6px 20px rgba(0,0,0,0.22)", maxWidth: 420 }, children: [
1697
+ p.icon ? /* @__PURE__ */ jsx(IconGlyph, { name: p.icon, size: 18, color: theme.onPrimary }) : null,
1698
+ /* @__PURE__ */ jsx(Box, { style: { flexShrink: 1 }, children: p.text ?? "" }),
1699
+ p.action ? /* @__PURE__ */ jsx(Box, { style: { fontWeight: 700, marginLeft: 8, flexShrink: 0 }, children: p.action }) : null
1700
+ ] }) });
1701
+ }
1346
1702
  var registry = {
1347
1703
  Board,
1348
1704
  Screen,
@@ -1350,11 +1706,14 @@ var registry = {
1350
1706
  Stack,
1351
1707
  Grid,
1352
1708
  Card,
1709
+ Accordion,
1353
1710
  Divider,
1354
1711
  Spacer,
1355
1712
  Heading,
1356
1713
  Text,
1714
+ Bullets,
1357
1715
  Image,
1716
+ Chart,
1358
1717
  Icon,
1359
1718
  Avatar,
1360
1719
  Badge,
@@ -1364,14 +1723,19 @@ var registry = {
1364
1723
  Radio,
1365
1724
  Toggle,
1366
1725
  Select,
1726
+ Slider,
1727
+ Progress,
1728
+ Calendar,
1367
1729
  NavBar,
1368
1730
  TabBar,
1369
1731
  Tabs,
1732
+ Pagination,
1370
1733
  List,
1371
1734
  ListItem,
1372
1735
  Table,
1373
1736
  Modal,
1374
- Drawer
1737
+ Drawer,
1738
+ Toast
1375
1739
  };
1376
1740
 
1377
1741
  // src/text.ts
@@ -1383,6 +1747,10 @@ var PRIMARY_PROP = {
1383
1747
  Heading: "text",
1384
1748
  Text: "text",
1385
1749
  Image: "label",
1750
+ Chart: "title",
1751
+ Slider: "label",
1752
+ Progress: "label",
1753
+ Calendar: "month",
1386
1754
  Icon: "name",
1387
1755
  Avatar: "initials",
1388
1756
  Badge: "label",
@@ -1395,7 +1763,9 @@ var PRIMARY_PROP = {
1395
1763
  NavBar: "title",
1396
1764
  ListItem: "title",
1397
1765
  Modal: "title",
1398
- Drawer: "title"
1766
+ Drawer: "title",
1767
+ Accordion: "title",
1768
+ Toast: "text"
1399
1769
  };
1400
1770
  function unwrap(t) {
1401
1771
  let cur = t;
@@ -1455,6 +1825,18 @@ function belongsElsewhere(component, word, asProp) {
1455
1825
  }
1456
1826
  return msg;
1457
1827
  }
1828
+ var OBJECT_SWITCHES = /* @__PURE__ */ new Set();
1829
+ for (const info of COMPONENTS.values()) {
1830
+ for (const schema of Object.values(info.props)) {
1831
+ const s = unwrap(schema);
1832
+ if (s?.def?.type !== "array") continue;
1833
+ const el = unwrap(s.def.element);
1834
+ for (const option of el?.def?.type === "union" ? el.def.options.map(unwrap) : [el]) {
1835
+ if (option?.def?.type !== "object") continue;
1836
+ for (const [key, value] of Object.entries(option.shape)) if (unwrap(value)?.def?.type === "boolean") OBJECT_SWITCHES.add(key);
1837
+ }
1838
+ }
1839
+ }
1458
1840
  function bareWords(component) {
1459
1841
  const info = COMPONENTS.get(component.toLowerCase());
1460
1842
  if (!info) throw new Error(`unknown component ${component}`);
@@ -1571,15 +1953,31 @@ function parseValue(c) {
1571
1953
  }
1572
1954
  if (t.value === "{") {
1573
1955
  const obj = {};
1956
+ let lastKey = null;
1574
1957
  while (!c.isSym("}")) {
1575
1958
  c.skipCommas();
1576
1959
  if (c.isSym("}")) break;
1577
1960
  if (c.done()) throw new Error("unclosed {");
1578
1961
  const k = c.next();
1579
1962
  if (!k || k.kind === "sym") throw new Error("expected key in {\u2026}");
1963
+ if ((k.kind === "word" || k.kind === "num") && !(c.isSym("=") || c.isSym(":"))) {
1964
+ if (k.kind === "word" && OBJECT_SWITCHES.has(k.value)) {
1965
+ obj[k.value] = true;
1966
+ lastKey = null;
1967
+ continue;
1968
+ }
1969
+ if (lastKey) {
1970
+ obj[lastKey] = `${obj[lastKey]} ${k.value}`;
1971
+ continue;
1972
+ }
1973
+ obj[k.value] = true;
1974
+ continue;
1975
+ }
1580
1976
  if (!(c.isSym("=") || c.isSym(":"))) throw new Error(`expected = after ${k.value}`);
1581
1977
  c.next();
1978
+ const unquoted = c.peek()?.kind === "word" || c.peek()?.kind === "num";
1582
1979
  obj[k.value] = parseValue(c);
1980
+ lastKey = unquoted ? k.value : null;
1583
1981
  }
1584
1982
  c.next();
1585
1983
  return obj;
@@ -1883,7 +2281,7 @@ function checkSpec(spec) {
1883
2281
  if (el.type === "Screen" && !(root.children ?? []).includes(id)) {
1884
2282
  issues.push(`${id}: Screen must be a direct child of the Board`);
1885
2283
  }
1886
- if (el.type === "Modal" || el.type === "Drawer") {
2284
+ if (el.type === "Modal" || el.type === "Drawer" || el.type === "Toast") {
1887
2285
  const parent = Object.entries(spec.elements).find(([, p]) => p.children?.includes(id));
1888
2286
  if (parent && parent[1].type !== "Screen") {
1889
2287
  issues.push(`${id}: ${el.type} must be a direct child of a Screen (found in ${parent[1].type} "${parent[0]}")`);
@@ -1897,6 +2295,33 @@ function checkSpec(spec) {
1897
2295
  if (p.trailingIcon && trailing !== "icon") conflict("trailingIcon", "trailing=icon", "trailing", trailing);
1898
2296
  if (p.trailingText && trailing !== "text" && trailing !== "badge") conflict("trailingText", "trailing=text or badge", "trailing", trailing);
1899
2297
  }
2298
+ {
2299
+ const p = el.props ?? {};
2300
+ const say = (msg) => issues.push(`${id}: ${msg}`);
2301
+ const backwards = (r) => Array.isArray(r) && r.length === 2 && r[0] > r[1];
2302
+ if (el.type === "Progress") {
2303
+ if (p.step != null && p.steps == null) say(`step only shows with steps (e.g. steps=4 step=${p.step})`);
2304
+ if (p.step != null && p.steps != null && p.step > p.steps) say(`step=${p.step} is past the last step (steps=${p.steps})`);
2305
+ if (p.steps != null && p.shape === "circle") say("a stepper (steps) can't also be a circle; remove one of them");
2306
+ }
2307
+ if (el.type === "Pagination" && p.current != null && p.pages != null && p.current > p.pages) {
2308
+ say(`current=${p.current} is past the last page (pages=${p.pages})`);
2309
+ }
2310
+ if (el.type === "Slider" && backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the smaller number first`);
2311
+ if (el.type === "Slider" && p.range && p.value != null) say("a slider has value (one handle) or range (two), not both");
2312
+ if (el.type === "Calendar") {
2313
+ const { days } = monthGrid(p.month);
2314
+ const all = [p.selected, ...p.range ?? [], ...p.marked ?? []].filter((d) => typeof d === "number");
2315
+ const late = all.find((d) => d > days);
2316
+ if (late) say(`day ${late} isn't in ${p.month ?? "the month"} (it has ${days} days)`);
2317
+ if (backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the earlier day first`);
2318
+ if (p.range && p.selected != null) say("a calendar has selected (one day) or range (several), not both");
2319
+ }
2320
+ if (el.type === "Input" && p.type === "code" && p.value != null && String(p.value).length > (p.digits ?? 6)) {
2321
+ say(`value has ${String(p.value).length} characters but the code has ${p.digits ?? 6} boxes (digits)`);
2322
+ }
2323
+ if (el.type === "Input" && p.digits != null && p.type !== "code") say("digits only applies to type=code");
2324
+ }
1900
2325
  if (el.type === "Table") {
1901
2326
  const { columns, data } = el.props ?? {};
1902
2327
  if (Array.isArray(columns) && Array.isArray(data)) {
@@ -2032,7 +2457,7 @@ function fmt(v) {
2032
2457
  if (typeof v === "number" || typeof v === "boolean" || v === null) return String(v);
2033
2458
  if (Array.isArray(v)) return `[${v.map(fmt).join(", ")}]`;
2034
2459
  if (typeof v === "object") {
2035
- return `{${Object.entries(v).map(([k, x]) => `${k}=${fmt(x)}`).join(" ")}}`;
2460
+ return `{${Object.entries(v).map(([k, x]) => x === true ? k : `${k}=${fmt(x)}`).join(" ")}}`;
2036
2461
  }
2037
2462
  return JSON.stringify(v);
2038
2463
  }
@@ -2146,6 +2571,8 @@ function describeType(t) {
2146
2571
  return s.def.options.map(describeType).join(" | ");
2147
2572
  case "array":
2148
2573
  return `array of ${describeType(s.def.element)}`;
2574
+ case "tuple":
2575
+ return `[${s.def.items.map(describeType).join(", ")}]`;
2149
2576
  case "object":
2150
2577
  return `{ ${Object.entries(s.shape).map(([k, v]) => `${k}: ${describeType(v)}`).join(", ")} }`;
2151
2578
  default:
@@ -2181,7 +2608,7 @@ var RULES = `## Rules
2181
2608
  2. Each Screen is one view of the product. Use several Screens to show several views or states.
2182
2609
  3. A Screen lays out its children top to bottom. Use Stack (direction row or column) and Grid to arrange content.
2183
2610
  4. NavBar is pinned to the top of its Screen and TabBar to the bottom.
2184
- 5. Modal and Drawer are overlays. They must be direct children of a Screen.
2611
+ 5. Modal, Drawer and Toast are overlays. They must be direct children of a Screen.
2185
2612
  6. This is a low-fidelity wireframe. Prefer placeholders (Image boxes, Text with lines) over invented copy unless the copy matters.
2186
2613
  7. Only use the components and props listed below. All props are optional unless marked required.
2187
2614
  8. Wireframes are grayscale. The only UI colors: Board accent (one color for primary buttons, checked controls, toggles, active tabs and ghost buttons), Badge tone (success, warning, danger) and Input error. Only add an accent if the request asks for color or a brand.`;