@krizaka/ui 3.2.0 → 3.3.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.
Files changed (162) hide show
  1. package/dist/{chunk-I5RVM5CW.js → chunk-2FRWNSRO.js} +2 -6
  2. package/dist/chunk-2FRWNSRO.js.map +1 -0
  3. package/dist/chunk-BTMFZ66D.js +135 -0
  4. package/dist/chunk-BTMFZ66D.js.map +1 -0
  5. package/dist/chunk-C2BRIFJG.js +13 -0
  6. package/dist/chunk-C2BRIFJG.js.map +1 -0
  7. package/dist/chunk-KDXRYUD3.js +39 -0
  8. package/dist/chunk-KDXRYUD3.js.map +1 -0
  9. package/dist/{chunk-2PUDI7JS.js → chunk-ZKVQGXLD.js} +5 -3
  10. package/dist/{chunk-2PUDI7JS.js.map → chunk-ZKVQGXLD.js.map} +1 -1
  11. package/dist/command.js +1 -1
  12. package/dist/confirm-button.js +1 -1
  13. package/dist/data-table.js +3 -2
  14. package/dist/data-table.js.map +1 -1
  15. package/dist/date-picker.d.ts +3 -1
  16. package/dist/date-picker.js +13 -13
  17. package/dist/date-picker.js.map +1 -1
  18. package/dist/dropdown-menu.js +1 -1
  19. package/dist/filter-bar.js +3 -2
  20. package/dist/form.d.ts +23 -12
  21. package/dist/form.js +11 -47
  22. package/dist/form.js.map +1 -1
  23. package/dist/glow-card.js +1 -1
  24. package/dist/image-cropper.js +3 -1
  25. package/dist/image-cropper.js.map +1 -1
  26. package/dist/marquee.js +1 -1
  27. package/dist/native/form.d.ts +60 -15
  28. package/dist/native/form.js +103 -42
  29. package/dist/native/form.js.map +1 -1
  30. package/dist/native.d.ts +206 -2
  31. package/dist/native.js +1172 -541
  32. package/dist/native.js.map +1 -1
  33. package/dist/nav-bar.js +1 -1
  34. package/dist/popover.js +1 -1
  35. package/dist/radio-group.d.ts +66 -32
  36. package/dist/radio-group.js +54 -10
  37. package/dist/radio-group.js.map +1 -1
  38. package/dist/slider.d.ts +96 -35
  39. package/dist/slider.js +67 -13
  40. package/dist/slider.js.map +1 -1
  41. package/dist/terminal-replay.js +34 -60
  42. package/dist/terminal-replay.js.map +1 -1
  43. package/dist/toast.js +1 -1
  44. package/dist/tooltip.js +1 -1
  45. package/dist/video-player.js +3 -1
  46. package/dist/video-player.js.map +1 -1
  47. package/package.json +6 -6
  48. package/registry/combobox.json +225 -3
  49. package/registry/date-picker.json +16 -2
  50. package/registry/dialog.json +74 -3
  51. package/registry/examples/combobox/native/multiple.d.ts +4 -0
  52. package/registry/examples/combobox/native/multiple.dark.png +0 -0
  53. package/registry/examples/combobox/native/multiple.js +15 -0
  54. package/registry/examples/combobox/native/multiple.light.png +0 -0
  55. package/registry/examples/combobox/native/multiple.tsx +15 -0
  56. package/registry/examples/combobox/native/search.d.ts +4 -0
  57. package/registry/examples/combobox/native/search.dark.png +0 -0
  58. package/registry/examples/combobox/native/search.js +15 -0
  59. package/registry/examples/combobox/native/search.light.png +0 -0
  60. package/registry/examples/combobox/native/search.tsx +15 -0
  61. package/registry/examples/dialog/native/sheet.d.ts +4 -0
  62. package/registry/examples/dialog/native/sheet.dark.png +0 -0
  63. package/registry/examples/dialog/native/sheet.js +21 -0
  64. package/registry/examples/dialog/native/sheet.light.png +0 -0
  65. package/registry/examples/dialog/native/sheet.tsx +18 -0
  66. package/registry/examples/field/native/select.d.ts +4 -0
  67. package/registry/examples/field/native/select.dark.png +0 -0
  68. package/registry/examples/field/native/select.js +29 -0
  69. package/registry/examples/field/native/select.light.png +0 -0
  70. package/registry/examples/field/native/select.tsx +25 -0
  71. package/registry/examples/form/native/audience.dark.png +0 -0
  72. package/registry/examples/form/native/audience.light.png +0 -0
  73. package/registry/examples/form/native/profile.dark.png +0 -0
  74. package/registry/examples/form/native/profile.light.png +0 -0
  75. package/registry/examples/form/native/settings.dark.png +0 -0
  76. package/registry/examples/form/native/settings.light.png +0 -0
  77. package/registry/examples/form/native/sign-up.dark.png +0 -0
  78. package/registry/examples/form/native/sign-up.light.png +0 -0
  79. package/registry/examples/form/native/upload.dark.png +0 -0
  80. package/registry/examples/form/native/upload.light.png +0 -0
  81. package/registry/examples/radio-group/buttons.d.ts +4 -0
  82. package/registry/examples/radio-group/buttons.js +17 -0
  83. package/registry/examples/radio-group/buttons.tsx +21 -0
  84. package/registry/examples/radio-group/cards.js +1 -1
  85. package/registry/examples/radio-group/cards.tsx +2 -2
  86. package/registry/examples/radio-group/native/buttons.d.ts +4 -0
  87. package/registry/examples/radio-group/native/buttons.dark.png +0 -0
  88. package/registry/examples/radio-group/native/buttons.js +25 -0
  89. package/registry/examples/radio-group/native/buttons.light.png +0 -0
  90. package/registry/examples/radio-group/native/buttons.tsx +21 -0
  91. package/registry/examples/radio-group/native/cards.d.ts +4 -0
  92. package/registry/examples/radio-group/native/cards.dark.png +0 -0
  93. package/registry/examples/radio-group/native/cards.js +23 -0
  94. package/registry/examples/radio-group/native/cards.light.png +0 -0
  95. package/registry/examples/radio-group/native/cards.tsx +19 -0
  96. package/registry/examples/radio-group/native/list.d.ts +4 -0
  97. package/registry/examples/radio-group/native/list.dark.png +0 -0
  98. package/registry/examples/radio-group/native/list.js +24 -0
  99. package/registry/examples/radio-group/native/list.light.png +0 -0
  100. package/registry/examples/radio-group/native/list.tsx +20 -0
  101. package/registry/examples/radio-group/native/segmented.d.ts +4 -0
  102. package/registry/examples/radio-group/native/segmented.dark.png +0 -0
  103. package/registry/examples/radio-group/native/segmented.js +24 -0
  104. package/registry/examples/radio-group/native/segmented.light.png +0 -0
  105. package/registry/examples/radio-group/native/segmented.tsx +20 -0
  106. package/registry/examples/radio-group/segmented.d.ts +4 -0
  107. package/registry/examples/radio-group/segmented.js +15 -0
  108. package/registry/examples/radio-group/segmented.tsx +12 -0
  109. package/registry/examples/slider/floating.d.ts +4 -0
  110. package/registry/examples/slider/floating.js +11 -0
  111. package/registry/examples/slider/floating.tsx +12 -0
  112. package/registry/examples/slider/native/default.d.ts +4 -0
  113. package/registry/examples/slider/native/default.dark.png +0 -0
  114. package/registry/examples/slider/native/default.js +14 -0
  115. package/registry/examples/slider/native/default.light.png +0 -0
  116. package/registry/examples/slider/native/default.tsx +13 -0
  117. package/registry/examples/slider/native/range.d.ts +4 -0
  118. package/registry/examples/slider/native/range.dark.png +0 -0
  119. package/registry/examples/slider/native/range.js +12 -0
  120. package/registry/examples/slider/native/range.light.png +0 -0
  121. package/registry/examples/slider/native/range.tsx +11 -0
  122. package/registry/examples/slider/native/stepped.d.ts +4 -0
  123. package/registry/examples/slider/native/stepped.dark.png +0 -0
  124. package/registry/examples/slider/native/stepped.js +28 -0
  125. package/registry/examples/slider/native/stepped.light.png +0 -0
  126. package/registry/examples/slider/native/stepped.tsx +24 -0
  127. package/registry/examples/slider/native/vertical.d.ts +4 -0
  128. package/registry/examples/slider/native/vertical.dark.png +0 -0
  129. package/registry/examples/slider/native/vertical.js +16 -0
  130. package/registry/examples/slider/native/vertical.light.png +0 -0
  131. package/registry/examples/slider/native/vertical.tsx +13 -0
  132. package/registry/examples/slider/price-range.d.ts +4 -0
  133. package/registry/examples/slider/price-range.js +29 -0
  134. package/registry/examples/slider/price-range.tsx +27 -0
  135. package/registry/examples/slider/stepped.d.ts +4 -0
  136. package/registry/examples/slider/stepped.js +28 -0
  137. package/registry/examples/slider/stepped.tsx +24 -0
  138. package/registry/examples/slider/vertical.d.ts +4 -0
  139. package/registry/examples/slider/vertical.js +15 -0
  140. package/registry/examples/slider/vertical.tsx +14 -0
  141. package/registry/examples/switch/native/default.d.ts +4 -0
  142. package/registry/examples/switch/native/default.dark.png +0 -0
  143. package/registry/examples/switch/native/default.js +16 -0
  144. package/registry/examples/switch/native/default.light.png +0 -0
  145. package/registry/examples/switch/native/default.tsx +13 -0
  146. package/registry/examples/switch/native/settings.d.ts +4 -0
  147. package/registry/examples/switch/native/settings.dark.png +0 -0
  148. package/registry/examples/switch/native/settings.js +20 -0
  149. package/registry/examples/switch/native/settings.light.png +0 -0
  150. package/registry/examples/switch/native/settings.tsx +17 -0
  151. package/registry/field.json +15 -3
  152. package/registry/form.json +250 -42
  153. package/registry/index.json +24 -20
  154. package/registry/radio-group.json +227 -15
  155. package/registry/slider.json +314 -13
  156. package/registry/switch.json +115 -3
  157. package/registry/tabs.json +2 -2
  158. package/skills/choice-controls/SKILL.md +39 -0
  159. package/skills/form/SKILL.md +3 -0
  160. package/dist/chunk-I5RVM5CW.js.map +0 -1
  161. package/dist/chunk-RUPRYJ45.js +0 -82
  162. package/dist/chunk-RUPRYJ45.js.map +0 -1
@@ -31,7 +31,7 @@
31
31
  "Name refusals with keys in the schema (`z.string().regex(re, \"signUp.username.format\")`) and give the words in `messages/*.json`; the generic ones are `form.errors.*` (required, tooShort {minimum}…).",
32
32
  "Send with a mutation of @krizaka/query (`onSubmit: (v) => mutation.mutateAsync(v)`) or a Server Action (`useFormWithAction`): a thrown `KrizakaApiError` or `{ ok: false, problem }` puts its `fields` under their fields.",
33
33
  "Never disable the submit button for an invalid form: a refused submit shows every error and focuses the first.",
34
- "Pick the field for the value: `DateField entry=\"parts\"` for a memorable date (a birth date), the calendar for a date near today; `Money` stores cents; `RadioGroup` for two to five visible choices, `Select` beyond.",
34
+ "Pick the field for the value: `DateField entry=\"parts\"` (typed) or `entry=\"wheel\"` for a memorable date (a birth date, bounded by `max`), the calendar for a date near today; `Money` stores cents; `RadioGroup` for two to five visible choices (`variant` list · cards · buttons · segmented), `Select` beyond.",
35
35
  "Need a control the package does not have? `form.Field` gives its value, setter, error and ids — never import @tanstack/* in a product."
36
36
  ],
37
37
  "accessibility": {
@@ -193,7 +193,7 @@
193
193
  },
194
194
  {
195
195
  "component": "DateField",
196
- "description": "A date of the form (`YYYY-MM-DD`, `\"\"` for none): a calendar, or day · month · year typed in the locale's order.",
196
+ "description": "A date of the form (`YYYY-MM-DD`, `\"\"` for none): a calendar, day · month · year typed, or wheels.",
197
197
  "props": [
198
198
  {
199
199
  "name": "label",
@@ -213,7 +213,7 @@
213
213
  "name": "autoComplete",
214
214
  "type": "\"off\" | \"bday\"",
215
215
  "default": null,
216
- "description": "`bday` for a date of birth.",
216
+ "description": "`bday` for a date of birth: typed boxes are autofilled; the wheels rest on 1 January 2000 until turned.",
217
217
  "required": false
218
218
  },
219
219
  {
@@ -232,9 +232,9 @@
232
232
  },
233
233
  {
234
234
  "name": "entry",
235
- "type": "\"calendar\" | \"parts\"",
235
+ "type": "\"wheel\" | \"calendar\" | \"parts\"",
236
236
  "default": "calendar",
237
- "description": "`calendar` (default: a date near today — a publication, an appointment) or `parts` (day · month · year typed: a\nmemorable date, a birth date).",
237
+ "description": "`calendar` (default: a date near today — a publication, an appointment), `parts` (day · month · year typed: a\nmemorable date, a birth date) or `wheel` (three wheels bounded by `min` / `max`: a birth date on a phone).",
238
238
  "required": false
239
239
  },
240
240
  {
@@ -248,21 +248,21 @@
248
248
  "name": "labels",
249
249
  "type": "(Partial<DatePickerLabels> & Partial<Record<DatePart, string>>)",
250
250
  "default": null,
251
- "description": "The words of the calendar, or the names of the boxes (`day`, `month`, `year`), passed translated.",
251
+ "description": "The words of the calendar, or the names of the parts (`day`, `month`, `year`), passed translated.",
252
252
  "required": false
253
253
  },
254
254
  {
255
255
  "name": "max",
256
256
  "type": "string",
257
257
  "default": null,
258
- "description": "The latest date (ISO), for the calendar (`latestBirthDate(18)` for an age gate).",
258
+ "description": "The latest date (ISO), for the calendar and the wheels (`latestBirthDate(18)` for an age gate).",
259
259
  "required": false
260
260
  },
261
261
  {
262
262
  "name": "min",
263
263
  "type": "string",
264
264
  "default": null,
265
- "description": "The earliest date (ISO), for the calendar.",
265
+ "description": "The earliest date (ISO), for the calendar and the wheels.",
266
266
  "required": false
267
267
  }
268
268
  ]
@@ -548,9 +548,9 @@
548
548
  },
549
549
  {
550
550
  "name": "variant",
551
- "type": "\"list\" | \"cards\"",
551
+ "type": "\"list\" | \"buttons\" | \"cards\" | \"segmented\"",
552
552
  "default": "list",
553
- "description": "`list` (a dot and its words, default) or `cards` (a whole card is the radio: an amount, a plan, an audience).",
553
+ "description": "`list` (a dot and its words, default), `cards` (a whole card is the radio: an amount, a plan, an audience),\n`buttons` (tactile keys that press in: a size, a tip; `description` is the key's second line) or `segmented`\n(two to four short choices on one line).",
554
554
  "required": false
555
555
  }
556
556
  ]
@@ -612,7 +612,7 @@
612
612
  },
613
613
  {
614
614
  "component": "Slider",
615
- "description": "A number (or a range) of the form, chosen by feel: a share, a volume, a price range.",
615
+ "description": "A number (or a range) of the form, chosen by feel: a share, a volume, a price range — the value floats while it is touched.",
616
616
  "props": [
617
617
  {
618
618
  "name": "label",
@@ -656,6 +656,13 @@
656
656
  "description": "A help text under the control, read with it; the error takes its place when there is one.",
657
657
  "required": false
658
658
  },
659
+ {
660
+ "name": "marks",
661
+ "type": "readonly SliderMark[]",
662
+ "default": null,
663
+ "description": "Words under the track at some values.",
664
+ "required": false
665
+ },
659
666
  {
660
667
  "name": "max",
661
668
  "type": "number",
@@ -683,6 +690,13 @@
683
690
  "default": null,
684
691
  "description": "For a range (`[number, number]`): each thumb's name, passed translated.",
685
692
  "required": false
693
+ },
694
+ {
695
+ "name": "ticks",
696
+ "type": "boolean | readonly number[]",
697
+ "default": null,
698
+ "description": "A graduation per step (`true`) or at the values given.",
699
+ "required": false
686
700
  }
687
701
  ]
688
702
  },
@@ -1148,11 +1162,11 @@
1148
1162
  },
1149
1163
  "native": {
1150
1164
  "entry": "@krizaka/ui/native/form",
1151
- "import": "import { createKrizakaForm, TextField, DateField, Money, Submit, FormError } from \"@krizaka/ui/native/form\";",
1165
+ "import": "import { createKrizakaForm, TextField, DateField, Switch, RadioGroup, Select, Combobox, Slider, Money, Submit, FormError } from \"@krizaka/ui/native/form\";",
1152
1166
  "differences": [
1153
1167
  "Import `createKrizakaForm` from `@krizaka/ui/native/form`: the same definition (schema, defaults, i18n), the same `useForm`, `form.*` fields drawn with the native ones.",
1154
1168
  "`form.Form` is a View; `form.Submit` takes `label` and announces the first error of a refused submit (no focus to move).",
1155
- "`DateField` is always day · month · year; `RadioGroup` and `Select` are chips (a radio group); `Switch` is the system's; `Money` uses the decimal keypad.",
1169
+ "`DateField` is three wheels by default (`entry=\"parts\"`: typed day · month · year); `RadioGroup` has the native looks (`variant` list · cards · buttons · segmented); `Select` and `Combobox` open a `Sheet`; `Switch` is a settings row drawn by the theme; `Slider` floats its value; `Money` uses the decimal keypad. Each control plays its haptic when the app passes `haptics` to the `ThemeProvider`.",
1156
1170
  "No `Combobox`, `TagInput`, `FileUpload` or `Slider` field yet: a screen of its own (a picker) sets the value, or `form.Field` draws it."
1157
1171
  ],
1158
1172
  "props": [
@@ -1391,7 +1405,7 @@
1391
1405
  },
1392
1406
  {
1393
1407
  "component": "Switch",
1394
- "description": "An on/off setting of the form (`boolean`): its label and hint beside the system's switch.",
1408
+ "description": "An on/off setting of the form (`boolean`): a settings row — the label and its hint beside the switch, all pressable.",
1395
1409
  "props": [
1396
1410
  {
1397
1411
  "name": "label",
@@ -1432,7 +1446,7 @@
1432
1446
  },
1433
1447
  {
1434
1448
  "component": "RadioGroup",
1435
- "description": "One choice among a few, as chips (a radio group). On native, `Select` is the same control.",
1449
+ "description": "One choice among a few, all visible: the native `RadioGroup` (a haptic on each choice).",
1436
1450
  "props": [
1437
1451
  {
1438
1452
  "name": "label",
@@ -1452,7 +1466,7 @@
1452
1466
  "name": "options",
1453
1467
  "type": "readonly FormOption[]",
1454
1468
  "default": null,
1455
- "description": "The choices, as chips (one chosen).",
1469
+ "description": "The choices, all visible (two to five).",
1456
1470
  "required": true
1457
1471
  },
1458
1472
  {
@@ -1469,25 +1483,25 @@
1469
1483
  "description": "A help text under the control; the error takes its place when there is one.",
1470
1484
  "required": false
1471
1485
  },
1472
- {
1473
- "name": "scrollable",
1474
- "type": "boolean",
1475
- "default": null,
1476
- "description": "Scrolls the chips sideways instead of wrapping them (many choices).",
1477
- "required": false
1478
- },
1479
1486
  {
1480
1487
  "name": "style",
1481
1488
  "type": "StyleProp<ViewStyle>",
1482
1489
  "default": null,
1483
1490
  "description": "Styles merged last, on the field.",
1484
1491
  "required": false
1492
+ },
1493
+ {
1494
+ "name": "variant",
1495
+ "type": "\"list\" | \"cards\" | \"buttons\" | \"segmented\"",
1496
+ "default": "list",
1497
+ "description": "`list` (dots, default), `cards`, `buttons` (tactile keys) or `segmented` — the native `RadioGroup`'s looks.",
1498
+ "required": false
1485
1499
  }
1486
1500
  ]
1487
1501
  },
1488
1502
  {
1489
1503
  "component": "Select",
1490
- "description": "One choice among several (the web's `<select>`): chips on native, scrolled sideways when they are many.",
1504
+ "description": "One choice among many (the web's `<select>`): a field that opens a sheet of options.",
1491
1505
  "props": [
1492
1506
  {
1493
1507
  "name": "label",
@@ -1507,7 +1521,7 @@
1507
1521
  "name": "options",
1508
1522
  "type": "readonly FormOption[]",
1509
1523
  "default": null,
1510
- "description": "The choices, as chips (one chosen).",
1524
+ "description": "The choices, in a sheet.",
1511
1525
  "required": true
1512
1526
  },
1513
1527
  {
@@ -1525,10 +1539,86 @@
1525
1539
  "required": false
1526
1540
  },
1527
1541
  {
1528
- "name": "scrollable",
1542
+ "name": "placeholder",
1543
+ "type": "string",
1544
+ "default": null,
1545
+ "description": "Shown while nothing is chosen (\"Choose a category\"), passed translated.",
1546
+ "required": false
1547
+ },
1548
+ {
1549
+ "name": "style",
1550
+ "type": "StyleProp<ViewStyle>",
1551
+ "default": null,
1552
+ "description": "Styles merged last, on the field.",
1553
+ "required": false
1554
+ }
1555
+ ]
1556
+ },
1557
+ {
1558
+ "component": "Combobox",
1559
+ "description": "A choice among many, searched in a sheet: one value (`string`) or several (`string[]`).",
1560
+ "props": [
1561
+ {
1562
+ "name": "label",
1563
+ "type": "string",
1564
+ "default": null,
1565
+ "description": "The visible label, passed translated (also the accessible name).",
1566
+ "required": true
1567
+ },
1568
+ {
1569
+ "name": "name",
1570
+ "type": "string",
1571
+ "default": null,
1572
+ "description": "The field's name in the schema (typed by the form).",
1573
+ "required": true
1574
+ },
1575
+ {
1576
+ "name": "disabled",
1529
1577
  "type": "boolean",
1530
1578
  "default": null,
1531
- "description": "Scrolls the chips sideways instead of wrapping them (many choices).",
1579
+ "description": "Not available.",
1580
+ "required": false
1581
+ },
1582
+ {
1583
+ "name": "hint",
1584
+ "type": "string",
1585
+ "default": null,
1586
+ "description": "A help text under the control; the error takes its place when there is one.",
1587
+ "required": false
1588
+ },
1589
+ {
1590
+ "name": "labels",
1591
+ "type": "Partial<ComboboxLabels>",
1592
+ "default": null,
1593
+ "description": "The words of the control, passed translated.",
1594
+ "required": false
1595
+ },
1596
+ {
1597
+ "name": "multiple",
1598
+ "type": "boolean",
1599
+ "default": null,
1600
+ "description": "Several values (`string[]`); one (`string`) by default.",
1601
+ "required": false
1602
+ },
1603
+ {
1604
+ "name": "onSearch",
1605
+ "type": "((query: string, signal: AbortSignal) => Promise<readonly FormOption[]>)",
1606
+ "default": null,
1607
+ "description": "Fetches the options for a query (debounced, aborted by a newer one).",
1608
+ "required": false
1609
+ },
1610
+ {
1611
+ "name": "options",
1612
+ "type": "readonly FormOption[]",
1613
+ "default": null,
1614
+ "description": "The options, filtered as the person types.",
1615
+ "required": false
1616
+ },
1617
+ {
1618
+ "name": "placeholder",
1619
+ "type": "string",
1620
+ "default": null,
1621
+ "description": "Shown while nothing is chosen, passed translated.",
1532
1622
  "required": false
1533
1623
  },
1534
1624
  {
@@ -1542,7 +1632,7 @@
1542
1632
  },
1543
1633
  {
1544
1634
  "component": "DateField",
1545
- "description": "A date of the form (`YYYY-MM-DD`): day, month and year typed in the locale's order — the schema checks it.",
1635
+ "description": "A date of the form (`YYYY-MM-DD`): wheels in the locale's order (or day · month · year typed) — the schema checks it.",
1546
1636
  "props": [
1547
1637
  {
1548
1638
  "name": "label",
@@ -1562,7 +1652,7 @@
1562
1652
  "name": "autoComplete",
1563
1653
  "type": "\"off\" | \"bday\"",
1564
1654
  "default": null,
1565
- "description": "`bday` for a date of birth.",
1655
+ "description": "`bday` for a date of birth: the wheels rest on 1 January 2000 until turned; typed boxes are autofilled.",
1566
1656
  "required": false
1567
1657
  },
1568
1658
  {
@@ -1572,6 +1662,13 @@
1572
1662
  "description": "Not available.",
1573
1663
  "required": false
1574
1664
  },
1665
+ {
1666
+ "name": "entry",
1667
+ "type": "\"wheel\" | \"parts\"",
1668
+ "default": "wheel",
1669
+ "description": "`wheel` (default: day, month and year as wheels) or `parts` (typed on the numeric keypad).",
1670
+ "required": false
1671
+ },
1575
1672
  {
1576
1673
  "name": "hint",
1577
1674
  "type": "string",
@@ -1583,7 +1680,90 @@
1583
1680
  "name": "labels",
1584
1681
  "type": "Partial<DateFieldLabels>",
1585
1682
  "default": null,
1586
- "description": "The names of the boxes (`day`, `month`, `year`), passed translated.",
1683
+ "description": "The names of the wheels or boxes (`day`, `month`, `year`), passed translated.",
1684
+ "required": false
1685
+ },
1686
+ {
1687
+ "name": "max",
1688
+ "type": "string",
1689
+ "default": null,
1690
+ "description": "The latest date (ISO) of the wheels: `latestBirthDate(18)` for an age gate.",
1691
+ "required": false
1692
+ },
1693
+ {
1694
+ "name": "min",
1695
+ "type": "string",
1696
+ "default": null,
1697
+ "description": "The earliest date (ISO) of the wheels.",
1698
+ "required": false
1699
+ },
1700
+ {
1701
+ "name": "style",
1702
+ "type": "StyleProp<ViewStyle>",
1703
+ "default": null,
1704
+ "description": "Styles merged last, on the field.",
1705
+ "required": false
1706
+ }
1707
+ ]
1708
+ },
1709
+ {
1710
+ "component": "Slider",
1711
+ "description": "A number (or a range) of the form, chosen by feel: a share, a volume, a price range.",
1712
+ "props": [
1713
+ {
1714
+ "name": "label",
1715
+ "type": "string",
1716
+ "default": null,
1717
+ "description": "The visible label, passed translated (also the accessible name).",
1718
+ "required": true
1719
+ },
1720
+ {
1721
+ "name": "name",
1722
+ "type": "string",
1723
+ "default": null,
1724
+ "description": "The field's name in the schema (typed by the form).",
1725
+ "required": true
1726
+ },
1727
+ {
1728
+ "name": "disabled",
1729
+ "type": "boolean",
1730
+ "default": null,
1731
+ "description": "Not available.",
1732
+ "required": false
1733
+ },
1734
+ {
1735
+ "name": "formatValue",
1736
+ "type": "((value: number) => string)",
1737
+ "default": null,
1738
+ "description": "The value as words (\"15 %\", \"3 min\"), shown and read.",
1739
+ "required": false
1740
+ },
1741
+ {
1742
+ "name": "hint",
1743
+ "type": "string",
1744
+ "default": null,
1745
+ "description": "A help text under the control; the error takes its place when there is one.",
1746
+ "required": false
1747
+ },
1748
+ {
1749
+ "name": "max",
1750
+ "type": "number",
1751
+ "default": null,
1752
+ "description": "The highest value. Default 100.",
1753
+ "required": false
1754
+ },
1755
+ {
1756
+ "name": "min",
1757
+ "type": "number",
1758
+ "default": null,
1759
+ "description": "The lowest value. Default 0.",
1760
+ "required": false
1761
+ },
1762
+ {
1763
+ "name": "step",
1764
+ "type": "number",
1765
+ "default": null,
1766
+ "description": "The step. Default 1.",
1587
1767
  "required": false
1588
1768
  },
1589
1769
  {
@@ -1592,6 +1772,20 @@
1592
1772
  "default": null,
1593
1773
  "description": "Styles merged last, on the field.",
1594
1774
  "required": false
1775
+ },
1776
+ {
1777
+ "name": "thumbLabels",
1778
+ "type": "[string, string]",
1779
+ "default": null,
1780
+ "description": "For a range (`[number, number]`): each thumb's name, passed translated.",
1781
+ "required": false
1782
+ },
1783
+ {
1784
+ "name": "ticks",
1785
+ "type": "boolean | readonly number[]",
1786
+ "default": null,
1787
+ "description": "A graduation per step (`true`) or at the values given.",
1788
+ "required": false
1595
1789
  }
1596
1790
  ]
1597
1791
  },
@@ -1875,7 +2069,7 @@
1875
2069
  },
1876
2070
  {
1877
2071
  "path": "form/meta.ts",
1878
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Form\",\n summary:\n \"A form = a schema (zod 4) + the package's fields, on TanStack Form v1: each error shows once the field is left and goes as soon as it is fixed, in the person's language (i18n keys, never the engine's English), a value can be checked on the server as it is typed, and the server's refusals land under their fields — web and React Native, the same schema.\",\n status: \"beta\",\n category: \"forms\",\n platforms: \"both\",\n whenToUse: [\n \"Any form that sends something: sign-up, profile, upload details, an amount, an audience, a report, settings.\",\n \"When the server must re-check the same rules: the schema is shared (a product keeps it in `packages/core`) and `@krizaka/ui/form/server` parses with it in a Server Action.\",\n \"When a value depends on the server (a username's availability): `validateAsync` on the field, debounced and aborted.\",\n ],\n whenNotToUse: [\n { when: \"For a single search box that filters as you type: no submit, no validation.\", use: \"search-field\" },\n { when: \"For a filter bar over a list: its state is the address, not a form.\", use: \"filter-bar\" },\n { when: \"For a setting that applies at once on its own (a theme switch): the control alone.\", use: \"switch\" },\n ],\n bestPractices: [\n \"Define the form once (`createKrizakaForm({ schema, defaultValues })`) beside the API's contract; use it in components with `form.useForm({ onSubmit })`.\",\n \"Name refusals with keys in the schema (`z.string().regex(re, \\\"signUp.username.format\\\")`) and give the words in `messages/*.json`; the generic ones are `form.errors.*` (required, tooShort {minimum}…).\",\n \"Send with a mutation of @krizaka/query (`onSubmit: (v) => mutation.mutateAsync(v)`) or a Server Action (`useFormWithAction`): a thrown `KrizakaApiError` or `{ ok: false, problem }` puts its `fields` under their fields.\",\n \"Never disable the submit button for an invalid form: a refused submit shows every error and focuses the first.\",\n \"Pick the field for the value: `DateField entry=\\\"parts\\\"` for a memorable date (a birth date), the calendar for a date near today; `Money` stores cents; `RadioGroup` for two to five visible choices, `Select` beyond.\",\n \"Need a control the package does not have? `form.Field` gives its value, setter, error and ids — never import @tanstack/* in a product.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Enter\", action: \"Sends the form from a text field (web); the keyboard's return key moves to the next field (native).\" },\n { keys: \"Tab\", action: \"Leaves a field: from then on its error shows, and goes as soon as it is fixed.\" },\n ],\n notes: [\n \"Every control is named by its visible label; its hint and its error are in `aria-describedby`, and an invalid control has `aria-invalid`.\",\n \"Errors are announced (`role=\\\"alert\\\"`); a refused submit focuses the first invalid field on the web and announces the first error on native.\",\n \"An availability check names its wait (`checkingLabel`) and says when the value is free (`availableHint`): never by colour alone.\",\n \"A date of birth is three labelled boxes in the locale's order with numeric keypads and `bday` autofill, not a calendar to scroll through decades.\",\n ],\n },\n related: [\"field\", \"checkbox\", \"switch\", \"radio-group\", \"combobox\", \"date-picker\", \"tag-input\", \"file-upload\", \"slider\"],\n web: {\n imports: [\"createKrizakaForm\", \"useFormWithAction\", \"latestBirthDate\", \"useKrizakaField\", \"formMessage\"],\n examples: [\n { name: \"sign-up\", title: \"Sign-up\", description: \"Orochia's sign-up: a username checked as it is typed with a free one offered, an 18+ date of birth typed as day · month · year, a server refusal on its field.\" },\n { name: \"profile\", title: \"Profile\", description: \"Saved by a mutation of @krizaka/query: categories, tags, a list of links that grows (`form.FieldArray`).\" },\n { name: \"upload\", title: \"Upload\", description: \"A video and what describes it — the file, a title, a description, tags, who can watch — sent together.\" },\n { name: \"amount\", title: \"Amount\", description: \"A tip stored in cents (`Money`), typed the locale's way; presets set the field from outside.\" },\n { name: \"audience\", title: \"Audience\", description: \"Cards to choose who watches, the price only for subscribers (`form.Watch`), a publication date.\" },\n { name: \"settings\", title: \"Settings\", description: \"Sent by a Server Action (`useFormWithAction`): switches, a menu, a slider, a choice.\" },\n { name: \"report\", title: \"Report\", description: \"One reason among a few, details, and blocking the account in the same step.\" },\n ],\n },\n native: {\n entry: \"@krizaka/ui/native/form\",\n imports: [\"createKrizakaForm\", \"TextField\", \"DateField\", \"Money\", \"Submit\", \"FormError\"],\n differences: [\n \"Import `createKrizakaForm` from `@krizaka/ui/native/form`: the same definition (schema, defaults, i18n), the same `useForm`, `form.*` fields drawn with the native ones.\",\n \"`form.Form` is a View; `form.Submit` takes `label` and announces the first error of a refused submit (no focus to move).\",\n \"`DateField` is always day · month · year; `RadioGroup` and `Select` are chips (a radio group); `Switch` is the system's; `Money` uses the decimal keypad.\",\n \"No `Combobox`, `TagInput`, `FileUpload` or `Slider` field yet: a screen of its own (a picker) sets the value, or `form.Field` draws it.\",\n ],\n examples: [\n { name: \"sign-up\", title: \"Sign-up\", description: \"E-mail, a username checked as it is typed, the 18+ birth date as day · month · year, the terms.\" },\n { name: \"profile\", title: \"Profile\", description: \"Saved by a mutation of @krizaka/query; a category as chips, the list of links grows.\" },\n { name: \"upload\", title: \"Upload details\", description: \"Title, description and audience of a video picked from the library.\" },\n { name: \"amount\", title: \"Amount\", description: \"A tip in cents on the decimal keypad, typed the French way; presets set the field.\" },\n { name: \"audience\", title: \"Audience\", description: \"The price only when the video is for subscribers; comments on or off.\" },\n { name: \"settings\", title: \"Settings\", description: \"The system's switches and chips for a choice, saved in one go.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
2072
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Form\",\n summary:\n \"A form = a schema (zod 4) + the package's fields, on TanStack Form v1: each error shows once the field is left and goes as soon as it is fixed, in the person's language (i18n keys, never the engine's English), a value can be checked on the server as it is typed, and the server's refusals land under their fields — web and React Native, the same schema.\",\n status: \"beta\",\n category: \"forms\",\n platforms: \"both\",\n whenToUse: [\n \"Any form that sends something: sign-up, profile, upload details, an amount, an audience, a report, settings.\",\n \"When the server must re-check the same rules: the schema is shared (a product keeps it in `packages/core`) and `@krizaka/ui/form/server` parses with it in a Server Action.\",\n \"When a value depends on the server (a username's availability): `validateAsync` on the field, debounced and aborted.\",\n ],\n whenNotToUse: [\n { when: \"For a single search box that filters as you type: no submit, no validation.\", use: \"search-field\" },\n { when: \"For a filter bar over a list: its state is the address, not a form.\", use: \"filter-bar\" },\n { when: \"For a setting that applies at once on its own (a theme switch): the control alone.\", use: \"switch\" },\n ],\n bestPractices: [\n \"Define the form once (`createKrizakaForm({ schema, defaultValues })`) beside the API's contract; use it in components with `form.useForm({ onSubmit })`.\",\n \"Name refusals with keys in the schema (`z.string().regex(re, \\\"signUp.username.format\\\")`) and give the words in `messages/*.json`; the generic ones are `form.errors.*` (required, tooShort {minimum}…).\",\n \"Send with a mutation of @krizaka/query (`onSubmit: (v) => mutation.mutateAsync(v)`) or a Server Action (`useFormWithAction`): a thrown `KrizakaApiError` or `{ ok: false, problem }` puts its `fields` under their fields.\",\n \"Never disable the submit button for an invalid form: a refused submit shows every error and focuses the first.\",\n \"Pick the field for the value: `DateField entry=\\\"parts\\\"` (typed) or `entry=\\\"wheel\\\"` for a memorable date (a birth date, bounded by `max`), the calendar for a date near today; `Money` stores cents; `RadioGroup` for two to five visible choices (`variant` list · cards · buttons · segmented), `Select` beyond.\",\n \"Need a control the package does not have? `form.Field` gives its value, setter, error and ids — never import @tanstack/* in a product.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Enter\", action: \"Sends the form from a text field (web); the keyboard's return key moves to the next field (native).\" },\n { keys: \"Tab\", action: \"Leaves a field: from then on its error shows, and goes as soon as it is fixed.\" },\n ],\n notes: [\n \"Every control is named by its visible label; its hint and its error are in `aria-describedby`, and an invalid control has `aria-invalid`.\",\n \"Errors are announced (`role=\\\"alert\\\"`); a refused submit focuses the first invalid field on the web and announces the first error on native.\",\n \"An availability check names its wait (`checkingLabel`) and says when the value is free (`availableHint`): never by colour alone.\",\n \"A date of birth is three labelled boxes in the locale's order with numeric keypads and `bday` autofill, not a calendar to scroll through decades.\",\n ],\n },\n related: [\"field\", \"checkbox\", \"switch\", \"radio-group\", \"combobox\", \"date-picker\", \"tag-input\", \"file-upload\", \"slider\"],\n web: {\n imports: [\"createKrizakaForm\", \"useFormWithAction\", \"latestBirthDate\", \"useKrizakaField\", \"formMessage\"],\n examples: [\n { name: \"sign-up\", title: \"Sign-up\", description: \"Orochia's sign-up: a username checked as it is typed with a free one offered, an 18+ date of birth typed as day · month · year, a server refusal on its field.\" },\n { name: \"profile\", title: \"Profile\", description: \"Saved by a mutation of @krizaka/query: categories, tags, a list of links that grows (`form.FieldArray`).\" },\n { name: \"upload\", title: \"Upload\", description: \"A video and what describes it — the file, a title, a description, tags, who can watch — sent together.\" },\n { name: \"amount\", title: \"Amount\", description: \"A tip stored in cents (`Money`), typed the locale's way; presets set the field from outside.\" },\n { name: \"audience\", title: \"Audience\", description: \"Cards to choose who watches, the price only for subscribers (`form.Watch`), a publication date.\" },\n { name: \"settings\", title: \"Settings\", description: \"Sent by a Server Action (`useFormWithAction`): switches, a menu, a slider, a choice.\" },\n { name: \"report\", title: \"Report\", description: \"One reason among a few, details, and blocking the account in the same step.\" },\n ],\n },\n native: {\n entry: \"@krizaka/ui/native/form\",\n imports: [\"createKrizakaForm\", \"TextField\", \"DateField\", \"Switch\", \"RadioGroup\", \"Select\", \"Combobox\", \"Slider\", \"Money\", \"Submit\", \"FormError\"],\n differences: [\n \"Import `createKrizakaForm` from `@krizaka/ui/native/form`: the same definition (schema, defaults, i18n), the same `useForm`, `form.*` fields drawn with the native ones.\",\n \"`form.Form` is a View; `form.Submit` takes `label` and announces the first error of a refused submit (no focus to move).\",\n \"`DateField` is three wheels by default (`entry=\\\"parts\\\"`: typed day · month · year); `RadioGroup` has the native looks (`variant` list · cards · buttons · segmented); `Select` and `Combobox` open a `Sheet`; `Switch` is a settings row drawn by the theme; `Slider` floats its value; `Money` uses the decimal keypad. Each control plays its haptic when the app passes `haptics` to the `ThemeProvider`.\",\n \"No `Combobox`, `TagInput`, `FileUpload` or `Slider` field yet: a screen of its own (a picker) sets the value, or `form.Field` draws it.\",\n ],\n examples: [\n { name: \"sign-up\", title: \"Sign-up\", description: \"E-mail, a username checked as it is typed, the 18+ birth date as day · month · year, the terms.\" },\n { name: \"profile\", title: \"Profile\", description: \"Saved by a mutation of @krizaka/query; a category as chips, the list of links grows.\" },\n { name: \"upload\", title: \"Upload details\", description: \"Title, description and audience of a video picked from the library.\" },\n { name: \"amount\", title: \"Amount\", description: \"A tip in cents on the decimal keypad, typed the French way; presets set the field.\" },\n { name: \"audience\", title: \"Audience\", description: \"The price only when the video is for subscribers; comments on or off.\" },\n { name: \"settings\", title: \"Settings\", description: \"The system's switches and chips for a choice, saved in one go.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
1879
2073
  },
1880
2074
  {
1881
2075
  "path": "form/money.ts",
@@ -1887,7 +2081,7 @@
1887
2081
  },
1888
2082
  {
1889
2083
  "path": "form/web.tsx",
1890
- "content": "// Client. The form's fields for the web: thin adapters from a form field (useKrizakaField) to the platform's current\n// primitives — Field + Input, Checkbox, Switch, RadioGroup, Combobox, DatePicker, TagInput, Dropzone, Slider. A new look\n// for a primitive (a new date picker, a new radio) changes an adapter here, never a form.\nimport { Alert } from \"@krizaka/ui/alert\";\nimport { Button, type ButtonProps } from \"@krizaka/ui/button\";\nimport { Checkbox as CheckboxControl } from \"@krizaka/ui/checkbox\";\nimport { Combobox as ComboboxControl, type ComboboxLabels, type ComboboxOption } from \"@krizaka/ui/combobox\";\nimport { DatePicker, type DatePickerLabels } from \"@krizaka/ui/date-picker\";\nimport { Field, Input, Select as SelectControl, Textarea } from \"@krizaka/ui/field\";\nimport { Dropzone, formatBytes } from \"@krizaka/ui/file-upload\";\nimport { RadioGroup as RadioGroupControl } from \"@krizaka/ui/radio-group\";\nimport { Slider as SliderControl, type SliderValue } from \"@krizaka/ui/slider\";\nimport { Spinner } from \"@krizaka/ui/spinner\";\nimport { Switch as SwitchControl } from \"@krizaka/ui/switch\";\nimport { TagInput as TagInputControl, type TagInputProps } from \"@krizaka/ui/tag-input\";\nimport * as React from \"react\";\n\nimport { composeDate, DATE_PART_LENGTH, type DatePart, datePartsOrder, splitDate } from \"../date-picker/parts\";\nimport {\n type FieldCheck,\n FormContext,\n type FormContextValue,\n type KrizakaField,\n type KrizakaFieldArray,\n makeCreateKrizakaForm,\n useFormContext,\n useFormStatus,\n useFormValue,\n useKrizakaField,\n useKrizakaFieldArray,\n} from \"./core\";\nimport { formMessage } from \"./errors\";\nimport { currencySymbol, moneyInputText, parseMoney } from \"./money\";\n\ntype Base = {\n /** The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form). */\n name: string;\n /** The visible label, passed translated (also the control's accessible name). */\n label: string;\n /** A help text under the control, read with it; the error takes its place when there is one. */\n hint?: string;\n /** Not available. */\n disabled?: boolean;\n /** Classes merged on the field's root. */\n className?: string;\n};\n\nconst ids = (f: KrizakaField, hint?: string) => [hint && !f.error ? f.hintId : null, f.error ? f.errorId : null].filter(Boolean).join(\" \") || undefined;\n\nfunction Caption({ field, hint, ok }: { field: KrizakaField; hint?: string; ok?: string }) {\n const suggestion = field.errorDetail?.suggestion;\n if (field.error)\n return (\n <Field.Error id={field.errorId}>\n <span>\n {field.error}\n {suggestion ? (\n <>\n {\" \"}\n <button type=\"button\" className=\"font-semibold text-accent-text underline underline-offset-2\" onClick={() => field.setValue(suggestion.value)}>\n {suggestion.label}\n </button>\n </>\n ) : null}\n </span>\n </Field.Error>\n );\n const text = field.available && ok ? (hint ? `${ok} · ${hint}` : ok) : hint;\n return text ? <Field.Hint id={field.hintId}>{text}</Field.Hint> : null;\n}\n\nconst Check = () => (\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4 text-fg-success\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n);\n\nexport type FormProps = Omit<React.ComponentProps<\"form\">, \"onSubmit\" | \"noValidate\"> & {\n /** The fields, the error and the button. */\n children?: React.ReactNode;\n};\n\n/** The `<form>` of a form (`form.Form`): `noValidate` (the schema validates), the first error focused after a refused submit. */\nfunction FormRoot({ value, className, children, ...props }: FormProps & { value: FormContextValue }) {\n const ref = React.useRef<HTMLFormElement>(null);\n return (\n <FormContext.Provider value={value}>\n <form\n ref={ref}\n noValidate\n className={className ?? \"flex flex-col gap-4\"}\n onSubmit={async (event) => {\n event.preventDefault();\n if (!(await value.submit())) requestAnimationFrame(() => ref.current?.querySelector<HTMLElement>('[aria-invalid=\"true\"], [data-invalid] input')?.focus());\n }}\n {...props}\n >\n {children}\n </form>\n </FormContext.Provider>\n );\n}\n\nexport type SubmitProps = Omit<ButtonProps, \"type\" | \"loading\"> & {\n /** The button's words, passed translated (\"Create my account\"). */\n children: React.ReactNode;\n};\n\n/** The submit button: busy while the form is sent (not pressable twice), never disabled for an invalid form. */\nexport function Submit({ variant = \"primary\", children, ...props }: SubmitProps) {\n const { submitting } = useFormStatus();\n return (\n <Button type=\"submit\" variant={variant} loading={submitting} {...props}>\n {children}\n </Button>\n );\n}\n\nexport type FormErrorProps = {\n /** A headline above the message (\"We could not create your account\"), passed translated. */\n title?: string;\n /** Classes merged on the alert. */\n className?: string;\n};\n\n/** The whole form's error — a refusal of the server that belongs to no field (a conflict, too many tries, no network). */\nexport function FormError({ title, className }: FormErrorProps) {\n const ctx = useFormContext();\n if (!ctx.error) return null;\n return (\n <Alert tone=\"danger\" title={title} className={className}>\n {formMessage(ctx.error, ctx.i18n)}\n </Alert>\n );\n}\n\nexport type TextFieldProps = Base & {\n /** `text` (default), `email`, `password`, `url`, `tel`, `search`: the right keyboard on a phone. */\n type?: \"text\" | \"email\" | \"password\" | \"url\" | \"tel\" | \"search\";\n /** The browser's autofill (`email`, `username`, `new-password`, `name`). */\n autoComplete?: string;\n /** An example of a value, passed translated — never the label. */\n placeholder?: string;\n /** The keyboard on a phone (`numeric`, `decimal`). */\n inputMode?: React.HTMLAttributes<HTMLInputElement>[\"inputMode\"];\n /** The most characters. */\n maxLength?: number;\n /** Cleans what is typed before it is stored (`(v) => v.toLowerCase().replace(/\\s+/g, \"_\")`). */\n transform?: (raw: string) => string;\n /** Checks the value on the server as it is typed (availability): an error key, or nothing when it is fine. */\n validateAsync?: FieldCheck<string>;\n /** The wait, in ms, between the last key and `validateAsync`. Default 350. */\n asyncDebounceMs?: number;\n /** The words once `validateAsync` accepted the value (\"@eric is free\"), passed translated. */\n availableHint?: string;\n /** The name of the wait while it checks (\"Checking\"), passed translated. */\n checkingLabel?: string;\n /** Something before the text (an `@`, an icon). */\n start?: React.ReactNode;\n};\n\n/** A text field of the form: label, input, hint or error, an optional check on the server. */\nexport function TextField({ name, label, hint, disabled, className, type = \"text\", transform, validateAsync, asyncDebounceMs, availableHint, checkingLabel = \"Checking\", start, ...input }: TextFieldProps) {\n const f = useKrizakaField<string>(name, { validateAsync, asyncDebounceMs });\n const end = f.validating ? <Spinner size=\"sm\" label={checkingLabel} /> : f.available ? <Check /> : null;\n return (\n <Field.Root className={className}>\n <Field.Label htmlFor={f.id}>{label}</Field.Label>\n <span className=\"relative flex items-center\">\n {start ? <span className=\"pointer-events-none absolute left-3 text-fg-muted\">{start}</span> : null}\n <Input\n id={f.id}\n name={name}\n type={type}\n value={f.value ?? \"\"}\n disabled={disabled}\n invalid={Boolean(f.error)}\n aria-describedby={ids(f, hint ?? availableHint)}\n onChange={(event) => f.setValue(transform ? transform(event.target.value) : event.target.value)}\n onBlur={f.onBlur}\n className={[start ? \"pl-9\" : \"\", end ? \"pr-10\" : \"\"].join(\" \").trim() || undefined}\n {...input}\n />\n {end ? <span className=\"absolute right-3 flex\">{end}</span> : null}\n </span>\n <Caption field={f} hint={hint} ok={availableHint} />\n </Field.Root>\n );\n}\n\nexport type TextAreaProps = Base & {\n /** An example of a value, passed translated. */\n placeholder?: string;\n /** The visible lines. Default 4. */\n rows?: number;\n /** The most characters. */\n maxLength?: number;\n};\n\n/** A longer text of the form (a bio, a description, a report). */\nexport function TextArea({ name, label, hint, disabled, className, rows = 4, ...input }: TextAreaProps) {\n const f = useKrizakaField<string>(name);\n return (\n <Field.Root className={className}>\n <Field.Label htmlFor={f.id}>{label}</Field.Label>\n <Textarea id={f.id} name={name} rows={rows} value={f.value ?? \"\"} disabled={disabled} invalid={Boolean(f.error)} aria-describedby={ids(f, hint)} onChange={(e) => f.setValue(e.target.value)} onBlur={f.onBlur} {...input} />\n <Caption field={f} hint={hint} />\n </Field.Root>\n );\n}\n\nexport type CheckboxProps = Omit<Base, \"label\"> & {\n /** The words next to the box, clickable with it — text, or a sentence with links (terms, privacy). */\n label: React.ReactNode;\n};\n\n/** A box of the form (`boolean`): terms to accept, an option. */\nexport function Checkbox({ name, label, hint, disabled, className }: CheckboxProps) {\n const f = useKrizakaField<boolean>(name);\n return (\n <div className={className ?? \"flex flex-col gap-1.5\"}>\n <CheckboxControl id={f.id} name={name} checked={f.value === true} disabled={disabled} invalid={Boolean(f.error)} aria-describedby={ids(f, hint)} onCheckedChange={(c) => f.setValue(c === true)} onBlur={f.onBlur}>\n {label}\n </CheckboxControl>\n <Caption field={f} hint={hint} />\n </div>\n );\n}\n\n/** An on/off setting of the form (`boolean`): the label and its hint beside the switch. */\nexport function Switch({ name, label, hint, disabled, className }: Base) {\n const f = useKrizakaField<boolean>(name);\n return (\n <div className={className ?? \"flex items-start justify-between gap-4\"}>\n <div className=\"flex min-w-0 flex-col gap-0.5\">\n <Field.Label htmlFor={f.id} className=\"text-sm font-medium text-fg\">\n {label}\n </Field.Label>\n <Caption field={f} hint={hint} />\n </div>\n <SwitchControl id={f.id} name={name} checked={f.value === true} disabled={disabled} invalid={Boolean(f.error)} aria-describedby={ids(f, hint)} onCheckedChange={(c) => f.setValue(c)} onBlur={f.onBlur} />\n </div>\n );\n}\n\n/** A choice of the form: the value stored, its words (passed translated), a line under them. */\nexport type FormOption = { value: string; label: string; description?: string; disabled?: boolean };\n\nexport type RadioGroupProps = Base & {\n /** The choices, all visible (two to five). */\n options: readonly FormOption[];\n /** `list` (a dot and its words, default) or `cards` (a whole card is the radio: an amount, a plan, an audience). */\n variant?: \"list\" | \"cards\";\n /** `vertical` (default) or `horizontal`. */\n orientation?: \"vertical\" | \"horizontal\";\n};\n\n/** One choice among a few, all visible. */\nexport function RadioGroup({ name, label, hint, disabled, className, options, variant = \"list\", orientation = \"vertical\" }: RadioGroupProps) {\n const f = useKrizakaField<string>(name);\n return (\n <Field.Root className={className} role=\"group\" aria-labelledby={`${f.id}-label`}>\n <span id={`${f.id}-label`} className=\"text-xs font-semibold text-fg-secondary\">\n {label}\n </span>\n <RadioGroupControl.Root\n aria-labelledby={`${f.id}-label`}\n value={f.value ?? \"\"}\n onValueChange={(v) => f.setValue(v)}\n onBlur={f.onBlur}\n orientation={orientation}\n disabled={disabled}\n invalid={Boolean(f.error)}\n aria-describedby={ids(f, hint)}\n name={name}\n className={variant === \"cards\" ? \"grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-2\" : undefined}\n >\n {options.map((o) =>\n variant === \"cards\" ? (\n <RadioGroupControl.Card key={o.value} value={o.value} disabled={o.disabled}>\n <span className=\"font-semibold\">{o.label}</span>\n {o.description ? <span className=\"text-xs text-fg-secondary\">{o.description}</span> : null}\n </RadioGroupControl.Card>\n ) : (\n <RadioGroupControl.Item key={o.value} value={o.value} disabled={o.disabled}>\n {o.label}\n {o.description ? <span className=\"block text-xs text-fg-secondary\">{o.description}</span> : null}\n </RadioGroupControl.Item>\n ),\n )}\n </RadioGroupControl.Root>\n <Caption field={f} hint={hint} />\n </Field.Root>\n );\n}\n\nexport type SelectProps = Base & {\n /** The choices, in the platform's own menu (a country, a category). */\n options: readonly FormOption[];\n /** The first line while nothing is chosen (\"Choose a category\"), passed translated. */\n placeholder?: string;\n};\n\n/** One choice among many, in the platform's menu (`<select>`: accessible and right on phones). */\nexport function Select({ name, label, hint, disabled, className, options, placeholder }: SelectProps) {\n const f = useKrizakaField<string>(name);\n return (\n <Field.Root className={className}>\n <Field.Label htmlFor={f.id}>{label}</Field.Label>\n <SelectControl id={f.id} name={name} value={f.value ?? \"\"} disabled={disabled} invalid={Boolean(f.error)} aria-describedby={ids(f, hint)} onChange={(e) => f.setValue(e.target.value)} onBlur={f.onBlur}>\n {placeholder !== undefined ? (\n <option value=\"\" disabled>\n {placeholder}\n </option>\n ) : null}\n {options.map((o) => (\n <option key={o.value} value={o.value} disabled={o.disabled}>\n {o.label}\n </option>\n ))}\n </SelectControl>\n <Caption field={f} hint={hint} />\n </Field.Root>\n );\n}\n\nexport type ComboboxProps = Base & {\n /** The options, filtered as the person types. */\n options?: readonly ComboboxOption[];\n /** Fetches the options for a query (debounced, aborted by a newer one). */\n onSearch?: (query: string, signal: AbortSignal) => Promise<readonly ComboboxOption[]>;\n /** Several values (`string[]`), shown as chips; one (`string`) by default. */\n multiple?: boolean;\n /** Offers to create the query as a value. */\n creatable?: boolean;\n /** The input's placeholder, passed translated. */\n placeholder?: string;\n /** The words of the control, passed translated. */\n labels?: Partial<ComboboxLabels>;\n};\n\n/** A choice among many, typed to filter or fetched: one value (`string`) or several (`string[]`). */\nexport function Combobox({ name, label, hint, disabled, className, options = [], onSearch, multiple, creatable, placeholder, labels }: ComboboxProps) {\n const f = useKrizakaField<string | string[]>(name);\n // The options chosen from a search, by value: their labels once the search has moved on.\n const [seen, setSeen] = React.useState<Record<string, ComboboxOption>>({});\n const option = (value: string) => options.find((o) => o.value === value) ?? seen[value] ?? { value, label: value };\n const remember = (o: ComboboxOption) => setSeen((known) => (known[o.value] === o ? known : { ...known, [o.value]: o }));\n const shared = { id: f.id, label, hint, error: f.error, disabled, placeholder, labels, options, onSearch, creatable, name };\n return (\n <div className={className} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && f.onBlur()}>\n {multiple ? (\n <ComboboxControl\n {...shared}\n multiple\n value={((f.value as string[] | undefined) ?? []).map(option)}\n onValueChange={(next) => {\n next.forEach(remember);\n f.setValue(next.map((o) => o.value));\n }}\n />\n ) : (\n <ComboboxControl\n {...shared}\n value={f.value ? option(f.value as string) : null}\n onValueChange={(next) => {\n if (next) remember(next);\n f.setValue(next?.value ?? \"\");\n }}\n />\n )}\n </div>\n );\n}\n\nexport type DateFieldProps = Base & {\n /** `calendar` (default: a date near today — a publication, an appointment) or `parts` (day · month · year typed: a\n * memorable date, a birth date). */\n entry?: \"calendar\" | \"parts\";\n /** The earliest date (ISO), for the calendar. */\n min?: string;\n /** The latest date (ISO), for the calendar (`latestBirthDate(18)` for an age gate). */\n max?: string;\n /** The words of the calendar, or the names of the boxes (`day`, `month`, `year`), passed translated. */\n labels?: Partial<DatePickerLabels> & Partial<Record<DatePart, string>>;\n /** `bday` for a date of birth. */\n autoComplete?: \"bday\" | \"off\";\n};\n\nconst PART_NAMES: Record<DatePart, string> = { day: \"Day\", month: \"Month\", year: \"Year\" };\n\n/** A date of the form (`YYYY-MM-DD`, `\"\"` for none): a calendar, or day · month · year typed in the locale's order. */\nexport function DateField({ name, label, hint, disabled, className, entry = \"calendar\", min, max, labels, autoComplete }: DateFieldProps) {\n const f = useKrizakaField<string>(name);\n if (entry === \"calendar\")\n return (\n <div className={className} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && f.onBlur()}>\n <DatePicker label={label} hint={hint} error={f.error} disabled={disabled} name={name} locale={f.locale} min={min} max={max} labels={labels} value={f.value || null} onValueChange={(v) => f.setValue(v ?? \"\")} />\n </div>\n );\n return <DateParts field={f} label={label} hint={hint} disabled={disabled} className={className} names={{ ...PART_NAMES, ...labels }} autoComplete={autoComplete} />;\n}\n\nfunction DateParts({ field: f, label, hint, disabled, className, names, autoComplete }: { field: KrizakaField<string>; label: string; hint?: string; disabled?: boolean; className?: string; names: Record<DatePart, string>; autoComplete?: string }) {\n const order = React.useMemo(() => datePartsOrder(f.locale), [f.locale]);\n const [parts, setParts] = React.useState(() => splitDate(f.value));\n if ((f.value ?? \"\") !== composeDate(parts) && composeDate(splitDate(f.value)) === (f.value ?? \"\")) setParts(splitDate(f.value));\n const refs = { day: React.useRef<HTMLInputElement>(null), month: React.useRef<HTMLInputElement>(null), year: React.useRef<HTMLInputElement>(null) };\n return (\n <fieldset className={className ?? \"flex flex-col gap-1.5\"} aria-describedby={ids(f, hint)} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && f.onBlur()}>\n <legend className=\"mb-1.5 text-xs font-semibold text-fg-secondary\">{label}</legend>\n <div className=\"flex gap-2\">\n {order.map((part) => (\n <label key={part} className=\"flex flex-col gap-1 text-xs text-fg-secondary\">\n {names[part]}\n <Input\n ref={refs[part]}\n inputMode=\"numeric\"\n autoComplete={autoComplete === \"bday\" ? `bday-${part}` : \"off\"}\n maxLength={DATE_PART_LENGTH[part]}\n value={parts[part]}\n disabled={disabled}\n invalid={Boolean(f.error)}\n className={part === \"year\" ? \"w-20 text-center tabular-nums\" : \"w-14 text-center tabular-nums\"}\n onChange={(e) => {\n const digits = e.target.value.replace(/\\D/g, \"\").slice(0, DATE_PART_LENGTH[part]);\n const next = { ...parts, [part]: digits };\n setParts(next);\n f.setValue(composeDate(next));\n const following = order[order.indexOf(part) + 1];\n if (digits.length === DATE_PART_LENGTH[part] && following) refs[following].current?.focus();\n }}\n />\n </label>\n ))}\n </div>\n <Caption field={f} hint={hint} />\n </fieldset>\n );\n}\n\nexport type TagsProps = Base & Pick<TagInputProps, \"suggestions\" | \"normalize\" | \"max\" | \"separators\" | \"placeholder\" | \"labels\">;\n\n/** Free words of the form as chips (`string[]`): tags, keywords. */\nexport function TagInput({ name, label, hint, disabled, className, ...props }: TagsProps) {\n const f = useKrizakaField<string[]>(name);\n return (\n <div className={className} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && f.onBlur()}>\n <TagInputControl label={label} hint={hint} error={f.error} disabled={disabled} name={name} value={f.value ?? []} onValueChange={(tags) => f.setValue(tags)} {...props} />\n </div>\n );\n}\n\nexport type FileUploadProps = Omit<Base, \"hint\"> & {\n /** The title in the zone (\"Drop your video here\"), passed translated. */\n title: string;\n /** The button that opens the file picker, passed translated. */\n browseLabel: string;\n /** What is accepted, in words (\"MP4 or MOV, up to 4 GB\"). */\n hint?: string;\n /** The types accepted (`video/*`, `image/png,.pdf`). */\n accept?: string;\n /** The largest file, in bytes. */\n maxSize?: number;\n /** Several files (`File[]`, up to `maxFiles`); one by default (still a `File[]` of one). */\n multiple?: boolean;\n /** The most files. */\n maxFiles?: number;\n /** The name of a file's remove button (`(name) => \"Remove \" + name`), passed translated. */\n removeLabel: (fileName: string) => string;\n /** The words of a refused file (`(r) => r.file.name + \" is too large\"`). */\n refusalLabel?: React.ComponentProps<typeof Dropzone>[\"refusalLabel\"];\n};\n\n/** Files of the form (`File[]`): a drop zone, then the chosen files, each removable. */\nexport function FileUpload({ name, label, hint, disabled, className, title, browseLabel, accept, maxSize, multiple, maxFiles, removeLabel, refusalLabel }: FileUploadProps) {\n const f = useKrizakaField<File[]>(name);\n const files = f.value ?? [];\n return (\n <div className={className ?? \"flex flex-col gap-2\"} role=\"group\" aria-label={label} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && f.onBlur()}>\n <span className=\"text-xs font-semibold text-fg-secondary\">{label}</span>\n <Dropzone\n title={title}\n browseLabel={browseLabel}\n hint={hint}\n accept={accept}\n maxSize={maxSize}\n multiple={multiple}\n maxFiles={maxFiles}\n disabled={disabled}\n refusalLabel={refusalLabel}\n onFiles={(chosen) => f.setValue(multiple ? [...files, ...chosen].slice(0, maxFiles ?? Infinity) : chosen.slice(0, 1))}\n />\n {files.length > 0 ? (\n <ul className=\"flex flex-col gap-1.5\">\n {files.map((file, i) => (\n <li key={`${file.name}-${i}`} className=\"flex items-center justify-between gap-3 rounded-lg border border-border-default bg-surface-1 px-3 py-2 text-sm\">\n <span className=\"min-w-0 truncate text-fg\">{file.name}</span>\n <span className=\"flex shrink-0 items-center gap-2\">\n <span className=\"font-mono text-xs text-fg-secondary\">{formatBytes(file.size, f.locale)}</span>\n <Button type=\"button\" size=\"sm\" variant=\"ghost\" onClick={() => f.setValue(files.filter((_, j) => j !== i))}>\n {removeLabel(file.name)}\n </Button>\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n {f.error ? <Field.Error id={f.errorId}>{f.error}</Field.Error> : null}\n </div>\n );\n}\n\nexport type MoneyProps = Base & {\n /** The currency (ISO 4217: `USD`, `EUR`, `JPY`): its sign before the amount, its decimals. */\n currency: string;\n /** An example of an amount, passed translated. */\n placeholder?: string;\n};\n\n/** An amount of the form, stored as integer minor units (cents: `1250` = 12.50), typed the locale's way. */\nexport function Money({ name, label, hint, disabled, className, currency, placeholder }: MoneyProps) {\n const f = useKrizakaField<number | null>(name);\n const [text, setText] = React.useState(() => moneyInputText(f.value, currency, f.locale));\n const [editing, setEditing] = React.useState(false);\n const shown = editing ? text : moneyInputText(f.value, currency, f.locale);\n return (\n <Field.Root className={className}>\n <Field.Label htmlFor={f.id}>{label}</Field.Label>\n <span className=\"relative flex items-center\">\n <span className=\"pointer-events-none absolute left-3 text-sm text-fg-secondary\" aria-hidden>\n {currencySymbol(currency, f.locale)}\n </span>\n <Input\n id={f.id}\n name={name}\n inputMode=\"decimal\"\n value={shown}\n placeholder={placeholder}\n disabled={disabled}\n invalid={Boolean(f.error)}\n aria-describedby={ids(f, hint)}\n className=\"pl-9 tabular-nums\"\n onFocus={() => {\n setText(moneyInputText(f.value, currency, f.locale));\n setEditing(true);\n }}\n onChange={(e) => {\n setText(e.target.value);\n const units = parseMoney(e.target.value, currency, f.locale);\n f.setValue(units === undefined ? null : units === null ? Number.NaN : units);\n }}\n onBlur={() => {\n setEditing(false);\n f.onBlur();\n }}\n />\n </span>\n <Caption field={f} hint={hint} />\n </Field.Root>\n );\n}\n\nexport type SliderProps = Base & {\n /** The lowest value. Default 0. */\n min?: number;\n /** The highest value. Default 100. */\n max?: number;\n /** The step. Default 1. */\n step?: number;\n /** The value as words (\"15 %\", \"3 min\"), shown and read. */\n formatValue?: (value: number) => string;\n /** For a range (`[number, number]`): each thumb's name, passed translated. */\n thumbLabels?: [string, string];\n};\n\n/** A number (or a range) of the form, chosen by feel: a share, a volume, a price range. */\nexport function Slider({ name, label, hint, disabled, className, min, max, step, formatValue, thumbLabels }: SliderProps) {\n const f = useKrizakaField<SliderValue>(name);\n return (\n <div className={className ?? \"flex flex-col gap-1.5\"}>\n <SliderControl label={label} showLabel value={f.value} min={min} max={max} step={step} disabled={disabled} formatValue={formatValue} thumbLabels={thumbLabels} aria-describedby={ids(f, hint)} onValueChange={(v) => f.setValue(v)} onValueCommit={() => f.onBlur()} />\n <Caption field={f} hint={hint} />\n </div>\n );\n}\n\nexport type FormFieldProps<T = unknown> = {\n /** The field's name in the schema (typed by the form). */\n name: string;\n /** Checks the value on the server as it changes. */\n validateAsync?: FieldCheck<T>;\n /** The wait, in ms, before `validateAsync`. */\n asyncDebounceMs?: number;\n /** Draws the field: its value, setter, error and ids (`useKrizakaField`). */\n children: (field: KrizakaField<T>) => React.ReactNode;\n};\n\n/** A field drawn by the app (a control the package does not have): the form's value, errors and ids, any markup. */\nexport function FormField({ name, validateAsync, asyncDebounceMs, children }: FormFieldProps) {\n return <>{children(useKrizakaField(name, { validateAsync, asyncDebounceMs }))}</>;\n}\n\nexport type FieldArrayProps<T = unknown> = {\n /** The array's name in the schema (typed by the form). */\n name: string;\n /** Draws the items: `items`, `push`, `remove`, `move`, `fieldName(i, \"url\")` for the fields of an item. */\n children: (array: KrizakaFieldArray<T>) => React.ReactNode;\n};\n\n/** A list of fields that grows and shrinks (links, tiers, guests): each item's fields named `links[0].url`. */\nexport function FieldArray({ name, children }: FieldArrayProps) {\n return <>{children(useKrizakaFieldArray(name))}</>;\n}\n\nexport type WatchProps<T = unknown> = {\n /** The field to follow (typed by the form). */\n name: string;\n /** Draws from its current value: show a part of the form only when a box is ticked. */\n children: (value: T) => React.ReactNode;\n};\n\n/** Renders from a field's current value (a part shown only when a box is ticked, a total). */\nexport function Watch({ name, children }: WatchProps) {\n return <>{children(useFormValue(name))}</>;\n}\n\nconst fields = { TextField, TextArea, Checkbox, Switch, RadioGroup, Select, Combobox, DateField, TagInput, FileUpload, Money, Slider, Field: FormField, FieldArray, Watch, Submit, FormError };\n\n/**\n * A form, defined once: its schema (zod 4) and default values — the same schema the server parses with. In a component,\n * `signUp.useForm({ onSubmit })` gives the form and its fields, typed by the schema: `<form.Form>`, `<form.TextField\n * name=\"email\" …/>`, `<form.Submit>`. Errors are i18n keys (`form.errors.tooShort`), never the engine's English.\n */\nexport const createKrizakaForm = makeCreateKrizakaForm(fields, FormRoot);\n\n/** The form sent by a Server Action: `useFormWithAction(signUp, createAccount)` — `signUp.useFormWithAction(createAccount)`. */\nexport function useFormWithAction<F extends { useFormWithAction: (...args: never[]) => unknown }>(form: F, ...args: Parameters<F[\"useFormWithAction\"]>): ReturnType<F[\"useFormWithAction\"]> {\n return form.useFormWithAction(...args) as ReturnType<F[\"useFormWithAction\"]>;\n}\n"
2084
+ "content": "// Client. The form's fields for the web: thin adapters from a form field (useKrizakaField) to the platform's current\n// primitives — Field + Input, Checkbox, Switch, RadioGroup (dots, cards, tactile keys, segments), Combobox, the date\n// pickers (calendar, typed, wheels), TagInput, Dropzone, Slider. A new look for a primitive changes an adapter here,\n// never a form.\nimport { Alert } from \"@krizaka/ui/alert\";\nimport { Button, type ButtonProps } from \"@krizaka/ui/button\";\nimport { Checkbox as CheckboxControl } from \"@krizaka/ui/checkbox\";\nimport { Combobox as ComboboxControl, type ComboboxLabels, type ComboboxOption } from \"@krizaka/ui/combobox\";\nimport { DateField as DatePartsControl, DatePicker, type DatePickerLabels, DateWheel } from \"@krizaka/ui/date-picker\";\nimport { Field, Input, Select as SelectControl, Textarea } from \"@krizaka/ui/field\";\nimport { Dropzone, formatBytes } from \"@krizaka/ui/file-upload\";\nimport { RadioGroup as RadioGroupControl } from \"@krizaka/ui/radio-group\";\nimport { Slider as SliderControl, type SliderMark, type SliderValue } from \"@krizaka/ui/slider\";\nimport { Spinner } from \"@krizaka/ui/spinner\";\nimport { Switch as SwitchControl } from \"@krizaka/ui/switch\";\nimport { TagInput as TagInputControl, type TagInputProps } from \"@krizaka/ui/tag-input\";\nimport * as React from \"react\";\n\nimport type { DatePart } from \"../date-picker/parts\";\nimport {\n type FieldCheck,\n FormContext,\n type FormContextValue,\n type KrizakaField,\n type KrizakaFieldArray,\n makeCreateKrizakaForm,\n useFormContext,\n useFormStatus,\n useFormValue,\n useKrizakaField,\n useKrizakaFieldArray,\n} from \"./core\";\nimport { formMessage } from \"./errors\";\nimport { currencySymbol, moneyInputText, parseMoney } from \"./money\";\n\ntype Base = {\n /** The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form). */\n name: string;\n /** The visible label, passed translated (also the control's accessible name). */\n label: string;\n /** A help text under the control, read with it; the error takes its place when there is one. */\n hint?: string;\n /** Not available. */\n disabled?: boolean;\n /** Classes merged on the field's root. */\n className?: string;\n};\n\nconst ids = (f: KrizakaField, hint?: string) => [hint && !f.error ? f.hintId : null, f.error ? f.errorId : null].filter(Boolean).join(\" \") || undefined;\n\nfunction Caption({ field, hint, ok }: { field: KrizakaField; hint?: string; ok?: string }) {\n const suggestion = field.errorDetail?.suggestion;\n if (field.error)\n return (\n <Field.Error id={field.errorId}>\n <span>\n {field.error}\n {suggestion ? (\n <>\n {\" \"}\n <button type=\"button\" className=\"font-semibold text-accent-text underline underline-offset-2\" onClick={() => field.setValue(suggestion.value)}>\n {suggestion.label}\n </button>\n </>\n ) : null}\n </span>\n </Field.Error>\n );\n const text = field.available && ok ? (hint ? `${ok} · ${hint}` : ok) : hint;\n return text ? <Field.Hint id={field.hintId}>{text}</Field.Hint> : null;\n}\n\nconst Check = () => (\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4 text-fg-success\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n);\n\nexport type FormProps = Omit<React.ComponentProps<\"form\">, \"onSubmit\" | \"noValidate\"> & {\n /** The fields, the error and the button. */\n children?: React.ReactNode;\n};\n\n/** The `<form>` of a form (`form.Form`): `noValidate` (the schema validates), the first error focused after a refused submit. */\nfunction FormRoot({ value, className, children, ...props }: FormProps & { value: FormContextValue }) {\n const ref = React.useRef<HTMLFormElement>(null);\n return (\n <FormContext.Provider value={value}>\n <form\n ref={ref}\n noValidate\n className={className ?? \"flex flex-col gap-4\"}\n onSubmit={async (event) => {\n event.preventDefault();\n if (!(await value.submit())) requestAnimationFrame(() => ref.current?.querySelector<HTMLElement>('[aria-invalid=\"true\"], [data-invalid] input')?.focus());\n }}\n {...props}\n >\n {children}\n </form>\n </FormContext.Provider>\n );\n}\n\nexport type SubmitProps = Omit<ButtonProps, \"type\" | \"loading\"> & {\n /** The button's words, passed translated (\"Create my account\"). */\n children: React.ReactNode;\n};\n\n/** The submit button: busy while the form is sent (not pressable twice), never disabled for an invalid form. */\nexport function Submit({ variant = \"primary\", children, ...props }: SubmitProps) {\n const { submitting } = useFormStatus();\n return (\n <Button type=\"submit\" variant={variant} loading={submitting} {...props}>\n {children}\n </Button>\n );\n}\n\nexport type FormErrorProps = {\n /** A headline above the message (\"We could not create your account\"), passed translated. */\n title?: string;\n /** Classes merged on the alert. */\n className?: string;\n};\n\n/** The whole form's error — a refusal of the server that belongs to no field (a conflict, too many tries, no network). */\nexport function FormError({ title, className }: FormErrorProps) {\n const ctx = useFormContext();\n if (!ctx.error) return null;\n return (\n <Alert tone=\"danger\" title={title} className={className}>\n {formMessage(ctx.error, ctx.i18n)}\n </Alert>\n );\n}\n\nexport type TextFieldProps = Base & {\n /** `text` (default), `email`, `password`, `url`, `tel`, `search`: the right keyboard on a phone. */\n type?: \"text\" | \"email\" | \"password\" | \"url\" | \"tel\" | \"search\";\n /** The browser's autofill (`email`, `username`, `new-password`, `name`). */\n autoComplete?: string;\n /** An example of a value, passed translated — never the label. */\n placeholder?: string;\n /** The keyboard on a phone (`numeric`, `decimal`). */\n inputMode?: React.HTMLAttributes<HTMLInputElement>[\"inputMode\"];\n /** The most characters. */\n maxLength?: number;\n /** Cleans what is typed before it is stored (`(v) => v.toLowerCase().replace(/\\s+/g, \"_\")`). */\n transform?: (raw: string) => string;\n /** Checks the value on the server as it is typed (availability): an error key, or nothing when it is fine. */\n validateAsync?: FieldCheck<string>;\n /** The wait, in ms, between the last key and `validateAsync`. Default 350. */\n asyncDebounceMs?: number;\n /** The words once `validateAsync` accepted the value (\"@eric is free\"), passed translated. */\n availableHint?: string;\n /** The name of the wait while it checks (\"Checking\"), passed translated. */\n checkingLabel?: string;\n /** Something before the text (an `@`, an icon). */\n start?: React.ReactNode;\n};\n\n/** A text field of the form: label, input, hint or error, an optional check on the server. */\nexport function TextField({ name, label, hint, disabled, className, type = \"text\", transform, validateAsync, asyncDebounceMs, availableHint, checkingLabel = \"Checking\", start, ...input }: TextFieldProps) {\n const f = useKrizakaField<string>(name, { validateAsync, asyncDebounceMs });\n const end = f.validating ? <Spinner size=\"sm\" label={checkingLabel} /> : f.available ? <Check /> : null;\n return (\n <Field.Root className={className}>\n <Field.Label htmlFor={f.id}>{label}</Field.Label>\n <span className=\"relative flex items-center\">\n {start ? <span className=\"pointer-events-none absolute left-3 text-fg-muted\">{start}</span> : null}\n <Input\n id={f.id}\n name={name}\n type={type}\n value={f.value ?? \"\"}\n disabled={disabled}\n invalid={Boolean(f.error)}\n aria-describedby={ids(f, hint ?? availableHint)}\n onChange={(event) => f.setValue(transform ? transform(event.target.value) : event.target.value)}\n onBlur={f.onBlur}\n className={[start ? \"pl-9\" : \"\", end ? \"pr-10\" : \"\"].join(\" \").trim() || undefined}\n {...input}\n />\n {end ? <span className=\"absolute right-3 flex\">{end}</span> : null}\n </span>\n <Caption field={f} hint={hint} ok={availableHint} />\n </Field.Root>\n );\n}\n\nexport type TextAreaProps = Base & {\n /** An example of a value, passed translated. */\n placeholder?: string;\n /** The visible lines. Default 4. */\n rows?: number;\n /** The most characters. */\n maxLength?: number;\n};\n\n/** A longer text of the form (a bio, a description, a report). */\nexport function TextArea({ name, label, hint, disabled, className, rows = 4, ...input }: TextAreaProps) {\n const f = useKrizakaField<string>(name);\n return (\n <Field.Root className={className}>\n <Field.Label htmlFor={f.id}>{label}</Field.Label>\n <Textarea id={f.id} name={name} rows={rows} value={f.value ?? \"\"} disabled={disabled} invalid={Boolean(f.error)} aria-describedby={ids(f, hint)} onChange={(e) => f.setValue(e.target.value)} onBlur={f.onBlur} {...input} />\n <Caption field={f} hint={hint} />\n </Field.Root>\n );\n}\n\nexport type CheckboxProps = Omit<Base, \"label\"> & {\n /** The words next to the box, clickable with it — text, or a sentence with links (terms, privacy). */\n label: React.ReactNode;\n};\n\n/** A box of the form (`boolean`): terms to accept, an option. */\nexport function Checkbox({ name, label, hint, disabled, className }: CheckboxProps) {\n const f = useKrizakaField<boolean>(name);\n return (\n <div className={className ?? \"flex flex-col gap-1.5\"}>\n <CheckboxControl id={f.id} name={name} checked={f.value === true} disabled={disabled} invalid={Boolean(f.error)} aria-describedby={ids(f, hint)} onCheckedChange={(c) => f.setValue(c === true)} onBlur={f.onBlur}>\n {label}\n </CheckboxControl>\n <Caption field={f} hint={hint} />\n </div>\n );\n}\n\n/** An on/off setting of the form (`boolean`): the label and its hint beside the switch. */\nexport function Switch({ name, label, hint, disabled, className }: Base) {\n const f = useKrizakaField<boolean>(name);\n return (\n <div className={className ?? \"flex items-start justify-between gap-4\"}>\n <div className=\"flex min-w-0 flex-col gap-0.5\">\n <Field.Label htmlFor={f.id} className=\"text-sm font-medium text-fg\">\n {label}\n </Field.Label>\n <Caption field={f} hint={hint} />\n </div>\n <SwitchControl id={f.id} name={name} checked={f.value === true} disabled={disabled} invalid={Boolean(f.error)} aria-describedby={ids(f, hint)} onCheckedChange={(c) => f.setValue(c)} onBlur={f.onBlur} />\n </div>\n );\n}\n\n/** A choice of the form: the value stored, its words (passed translated), a line under them. */\nexport type FormOption = { value: string; label: string; description?: string; disabled?: boolean };\n\nexport type RadioGroupProps = Base & {\n /** The choices, all visible (two to five). */\n options: readonly FormOption[];\n /**\n * `list` (a dot and its words, default), `cards` (a whole card is the radio: an amount, a plan, an audience),\n * `buttons` (tactile keys that press in: a size, a tip; `description` is the key's second line) or `segmented`\n * (two to four short choices on one line).\n */\n variant?: \"list\" | \"cards\" | \"buttons\" | \"segmented\";\n /** `vertical` (default) or `horizontal`. */\n orientation?: \"vertical\" | \"horizontal\";\n};\n\n/** One choice among a few, all visible. */\nexport function RadioGroup({ name, label, hint, disabled, className, options, variant = \"list\", orientation = \"vertical\" }: RadioGroupProps) {\n const f = useKrizakaField<string>(name);\n return (\n <Field.Root className={className} role=\"group\" aria-labelledby={`${f.id}-label`}>\n <span id={`${f.id}-label`} className=\"text-xs font-semibold text-fg-secondary\">\n {label}\n </span>\n <RadioGroupControl.Root\n aria-labelledby={`${f.id}-label`}\n value={f.value ?? \"\"}\n onValueChange={(v) => f.setValue(v)}\n onBlur={f.onBlur}\n orientation={orientation}\n disabled={disabled}\n invalid={Boolean(f.error)}\n aria-describedby={ids(f, hint)}\n name={name}\n variant={variant}\n className={variant === \"segmented\" ? \"self-start\" : undefined}\n >\n {options.map((o) =>\n variant === \"cards\" ? (\n <RadioGroupControl.Card key={o.value} value={o.value} disabled={o.disabled}>\n <span className=\"font-semibold\">{o.label}</span>\n {o.description ? <span className=\"text-xs text-fg-secondary\">{o.description}</span> : null}\n </RadioGroupControl.Card>\n ) : variant === \"buttons\" ? (\n <RadioGroupControl.Button key={o.value} value={o.value} disabled={o.disabled} hint={o.description}>\n {o.label}\n </RadioGroupControl.Button>\n ) : variant === \"segmented\" ? (\n <RadioGroupControl.Segment key={o.value} value={o.value} disabled={o.disabled}>\n {o.label}\n </RadioGroupControl.Segment>\n ) : (\n <RadioGroupControl.Item key={o.value} value={o.value} disabled={o.disabled}>\n {o.label}\n {o.description ? <span className=\"block text-xs text-fg-secondary\">{o.description}</span> : null}\n </RadioGroupControl.Item>\n ),\n )}\n </RadioGroupControl.Root>\n <Caption field={f} hint={hint} />\n </Field.Root>\n );\n}\n\nexport type SelectProps = Base & {\n /** The choices, in the platform's own menu (a country, a category). */\n options: readonly FormOption[];\n /** The first line while nothing is chosen (\"Choose a category\"), passed translated. */\n placeholder?: string;\n};\n\n/** One choice among many, in the platform's menu (`<select>`: accessible and right on phones). */\nexport function Select({ name, label, hint, disabled, className, options, placeholder }: SelectProps) {\n const f = useKrizakaField<string>(name);\n return (\n <Field.Root className={className}>\n <Field.Label htmlFor={f.id}>{label}</Field.Label>\n <SelectControl id={f.id} name={name} value={f.value ?? \"\"} disabled={disabled} invalid={Boolean(f.error)} aria-describedby={ids(f, hint)} onChange={(e) => f.setValue(e.target.value)} onBlur={f.onBlur}>\n {placeholder !== undefined ? (\n <option value=\"\" disabled>\n {placeholder}\n </option>\n ) : null}\n {options.map((o) => (\n <option key={o.value} value={o.value} disabled={o.disabled}>\n {o.label}\n </option>\n ))}\n </SelectControl>\n <Caption field={f} hint={hint} />\n </Field.Root>\n );\n}\n\nexport type ComboboxProps = Base & {\n /** The options, filtered as the person types. */\n options?: readonly ComboboxOption[];\n /** Fetches the options for a query (debounced, aborted by a newer one). */\n onSearch?: (query: string, signal: AbortSignal) => Promise<readonly ComboboxOption[]>;\n /** Several values (`string[]`), shown as chips; one (`string`) by default. */\n multiple?: boolean;\n /** Offers to create the query as a value. */\n creatable?: boolean;\n /** The input's placeholder, passed translated. */\n placeholder?: string;\n /** The words of the control, passed translated. */\n labels?: Partial<ComboboxLabels>;\n};\n\n/** A choice among many, typed to filter or fetched: one value (`string`) or several (`string[]`). */\nexport function Combobox({ name, label, hint, disabled, className, options = [], onSearch, multiple, creatable, placeholder, labels }: ComboboxProps) {\n const f = useKrizakaField<string | string[]>(name);\n // The options chosen from a search, by value: their labels once the search has moved on.\n const [seen, setSeen] = React.useState<Record<string, ComboboxOption>>({});\n const option = (value: string) => options.find((o) => o.value === value) ?? seen[value] ?? { value, label: value };\n const remember = (o: ComboboxOption) => setSeen((known) => (known[o.value] === o ? known : { ...known, [o.value]: o }));\n const shared = { id: f.id, label, hint, error: f.error, disabled, placeholder, labels, options, onSearch, creatable, name };\n return (\n <div className={className} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && f.onBlur()}>\n {multiple ? (\n <ComboboxControl\n {...shared}\n multiple\n value={((f.value as string[] | undefined) ?? []).map(option)}\n onValueChange={(next) => {\n next.forEach(remember);\n f.setValue(next.map((o) => o.value));\n }}\n />\n ) : (\n <ComboboxControl\n {...shared}\n value={f.value ? option(f.value as string) : null}\n onValueChange={(next) => {\n if (next) remember(next);\n f.setValue(next?.value ?? \"\");\n }}\n />\n )}\n </div>\n );\n}\n\nexport type DateFieldProps = Base & {\n /**\n * `calendar` (default: a date near today — a publication, an appointment), `parts` (day · month · year typed: a\n * memorable date, a birth date) or `wheel` (three wheels bounded by `min` / `max`: a birth date on a phone).\n */\n entry?: \"calendar\" | \"parts\" | \"wheel\";\n /** The earliest date (ISO), for the calendar and the wheels. */\n min?: string;\n /** The latest date (ISO), for the calendar and the wheels (`latestBirthDate(18)` for an age gate). */\n max?: string;\n /** The words of the calendar, or the names of the parts (`day`, `month`, `year`), passed translated. */\n labels?: Partial<DatePickerLabels> & Partial<Record<DatePart, string>>;\n /** `bday` for a date of birth: typed boxes are autofilled; the wheels rest on 1 January 2000 until turned. */\n autoComplete?: \"bday\" | \"off\";\n};\n\n/** A date of the form (`YYYY-MM-DD`, `\"\"` for none): a calendar, day · month · year typed, or wheels. */\nexport function DateField({ name, label, hint, disabled, className, entry = \"calendar\", min, max, labels, autoComplete }: DateFieldProps) {\n const f = useKrizakaField<string>(name);\n const onBlur = (e: React.FocusEvent<HTMLElement>) => !e.currentTarget.contains(e.relatedTarget as Node | null) && f.onBlur();\n if (entry === \"calendar\")\n return (\n <div className={className} onBlur={onBlur}>\n <DatePicker label={label} hint={hint} error={f.error} disabled={disabled} name={name} locale={f.locale} min={min} max={max} labels={labels} value={f.value || null} onValueChange={(v) => f.setValue(v ?? \"\")} />\n </div>\n );\n if (entry === \"wheel\")\n return (\n <div className={className} onBlur={onBlur}>\n <DateWheel label={label} hint={hint} error={f.error} disabled={disabled} name={name} locale={f.locale} min={min} max={max} labels={labels} defaultView={autoComplete === \"bday\" ? \"2000-01-01\" : undefined} value={f.value || null} onValueChange={(v) => f.setValue(v)} />\n </div>\n );\n return <DatePartsControl label={label} hint={hint} error={f.error} disabled={disabled} name={name} locale={f.locale} labels={labels} autoComplete={autoComplete} className={className} value={f.value ?? \"\"} onValueChange={(v) => f.setValue(v)} onBlur={f.onBlur} />;\n}\n\nexport type TagsProps = Base & Pick<TagInputProps, \"suggestions\" | \"normalize\" | \"max\" | \"separators\" | \"placeholder\" | \"labels\">;\n\n/** Free words of the form as chips (`string[]`): tags, keywords. */\nexport function TagInput({ name, label, hint, disabled, className, ...props }: TagsProps) {\n const f = useKrizakaField<string[]>(name);\n return (\n <div className={className} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && f.onBlur()}>\n <TagInputControl label={label} hint={hint} error={f.error} disabled={disabled} name={name} value={f.value ?? []} onValueChange={(tags) => f.setValue(tags)} {...props} />\n </div>\n );\n}\n\nexport type FileUploadProps = Omit<Base, \"hint\"> & {\n /** The title in the zone (\"Drop your video here\"), passed translated. */\n title: string;\n /** The button that opens the file picker, passed translated. */\n browseLabel: string;\n /** What is accepted, in words (\"MP4 or MOV, up to 4 GB\"). */\n hint?: string;\n /** The types accepted (`video/*`, `image/png,.pdf`). */\n accept?: string;\n /** The largest file, in bytes. */\n maxSize?: number;\n /** Several files (`File[]`, up to `maxFiles`); one by default (still a `File[]` of one). */\n multiple?: boolean;\n /** The most files. */\n maxFiles?: number;\n /** The name of a file's remove button (`(name) => \"Remove \" + name`), passed translated. */\n removeLabel: (fileName: string) => string;\n /** The words of a refused file (`(r) => r.file.name + \" is too large\"`). */\n refusalLabel?: React.ComponentProps<typeof Dropzone>[\"refusalLabel\"];\n};\n\n/** Files of the form (`File[]`): a drop zone, then the chosen files, each removable. */\nexport function FileUpload({ name, label, hint, disabled, className, title, browseLabel, accept, maxSize, multiple, maxFiles, removeLabel, refusalLabel }: FileUploadProps) {\n const f = useKrizakaField<File[]>(name);\n const files = f.value ?? [];\n return (\n <div className={className ?? \"flex flex-col gap-2\"} role=\"group\" aria-label={label} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && f.onBlur()}>\n <span className=\"text-xs font-semibold text-fg-secondary\">{label}</span>\n <Dropzone\n title={title}\n browseLabel={browseLabel}\n hint={hint}\n accept={accept}\n maxSize={maxSize}\n multiple={multiple}\n maxFiles={maxFiles}\n disabled={disabled}\n refusalLabel={refusalLabel}\n onFiles={(chosen) => f.setValue(multiple ? [...files, ...chosen].slice(0, maxFiles ?? Infinity) : chosen.slice(0, 1))}\n />\n {files.length > 0 ? (\n <ul className=\"flex flex-col gap-1.5\">\n {files.map((file, i) => (\n <li key={`${file.name}-${i}`} className=\"flex items-center justify-between gap-3 rounded-lg border border-border-default bg-surface-1 px-3 py-2 text-sm\">\n <span className=\"min-w-0 truncate text-fg\">{file.name}</span>\n <span className=\"flex shrink-0 items-center gap-2\">\n <span className=\"font-mono text-xs text-fg-secondary\">{formatBytes(file.size, f.locale)}</span>\n <Button type=\"button\" size=\"sm\" variant=\"ghost\" onClick={() => f.setValue(files.filter((_, j) => j !== i))}>\n {removeLabel(file.name)}\n </Button>\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n {f.error ? <Field.Error id={f.errorId}>{f.error}</Field.Error> : null}\n </div>\n );\n}\n\nexport type MoneyProps = Base & {\n /** The currency (ISO 4217: `USD`, `EUR`, `JPY`): its sign before the amount, its decimals. */\n currency: string;\n /** An example of an amount, passed translated. */\n placeholder?: string;\n};\n\n/** An amount of the form, stored as integer minor units (cents: `1250` = 12.50), typed the locale's way. */\nexport function Money({ name, label, hint, disabled, className, currency, placeholder }: MoneyProps) {\n const f = useKrizakaField<number | null>(name);\n const [text, setText] = React.useState(() => moneyInputText(f.value, currency, f.locale));\n const [editing, setEditing] = React.useState(false);\n const shown = editing ? text : moneyInputText(f.value, currency, f.locale);\n return (\n <Field.Root className={className}>\n <Field.Label htmlFor={f.id}>{label}</Field.Label>\n <span className=\"relative flex items-center\">\n <span className=\"pointer-events-none absolute left-3 text-sm text-fg-secondary\" aria-hidden>\n {currencySymbol(currency, f.locale)}\n </span>\n <Input\n id={f.id}\n name={name}\n inputMode=\"decimal\"\n value={shown}\n placeholder={placeholder}\n disabled={disabled}\n invalid={Boolean(f.error)}\n aria-describedby={ids(f, hint)}\n className=\"pl-9 tabular-nums\"\n onFocus={() => {\n setText(moneyInputText(f.value, currency, f.locale));\n setEditing(true);\n }}\n onChange={(e) => {\n setText(e.target.value);\n const units = parseMoney(e.target.value, currency, f.locale);\n f.setValue(units === undefined ? null : units === null ? Number.NaN : units);\n }}\n onBlur={() => {\n setEditing(false);\n f.onBlur();\n }}\n />\n </span>\n <Caption field={f} hint={hint} />\n </Field.Root>\n );\n}\n\nexport type SliderProps = Base & {\n /** The lowest value. Default 0. */\n min?: number;\n /** The highest value. Default 100. */\n max?: number;\n /** The step. Default 1. */\n step?: number;\n /** The value as words (\"15 %\", \"3 min\"), shown and read. */\n formatValue?: (value: number) => string;\n /** For a range (`[number, number]`): each thumb's name, passed translated. */\n thumbLabels?: [string, string];\n /** A graduation per step (`true`) or at the values given. */\n ticks?: boolean | readonly number[];\n /** Words under the track at some values. */\n marks?: readonly SliderMark[];\n};\n\n/** A number (or a range) of the form, chosen by feel: a share, a volume, a price range — the value floats while it is touched. */\nexport function Slider({ name, label, hint, disabled, className, min, max, step, formatValue, thumbLabels, ticks, marks }: SliderProps) {\n const f = useKrizakaField<SliderValue>(name);\n return (\n <div className={className ?? \"flex flex-col gap-1.5\"}>\n <SliderControl label={label} showLabel floatingValue value={f.value} min={min} max={max} step={step} ticks={ticks} marks={marks} disabled={disabled} formatValue={formatValue} thumbLabels={thumbLabels} aria-describedby={ids(f, hint)} onValueChange={(v) => f.setValue(v)} onValueCommit={() => f.onBlur()} />\n <Caption field={f} hint={hint} />\n </div>\n );\n}\n\nexport type FormFieldProps<T = unknown> = {\n /** The field's name in the schema (typed by the form). */\n name: string;\n /** Checks the value on the server as it changes. */\n validateAsync?: FieldCheck<T>;\n /** The wait, in ms, before `validateAsync`. */\n asyncDebounceMs?: number;\n /** Draws the field: its value, setter, error and ids (`useKrizakaField`). */\n children: (field: KrizakaField<T>) => React.ReactNode;\n};\n\n/** A field drawn by the app (a control the package does not have): the form's value, errors and ids, any markup. */\nexport function FormField({ name, validateAsync, asyncDebounceMs, children }: FormFieldProps) {\n return <>{children(useKrizakaField(name, { validateAsync, asyncDebounceMs }))}</>;\n}\n\nexport type FieldArrayProps<T = unknown> = {\n /** The array's name in the schema (typed by the form). */\n name: string;\n /** Draws the items: `items`, `push`, `remove`, `move`, `fieldName(i, \"url\")` for the fields of an item. */\n children: (array: KrizakaFieldArray<T>) => React.ReactNode;\n};\n\n/** A list of fields that grows and shrinks (links, tiers, guests): each item's fields named `links[0].url`. */\nexport function FieldArray({ name, children }: FieldArrayProps) {\n return <>{children(useKrizakaFieldArray(name))}</>;\n}\n\nexport type WatchProps<T = unknown> = {\n /** The field to follow (typed by the form). */\n name: string;\n /** Draws from its current value: show a part of the form only when a box is ticked. */\n children: (value: T) => React.ReactNode;\n};\n\n/** Renders from a field's current value (a part shown only when a box is ticked, a total). */\nexport function Watch({ name, children }: WatchProps) {\n return <>{children(useFormValue(name))}</>;\n}\n\nconst fields = { TextField, TextArea, Checkbox, Switch, RadioGroup, Select, Combobox, DateField, TagInput, FileUpload, Money, Slider, Field: FormField, FieldArray, Watch, Submit, FormError };\n\n/**\n * A form, defined once: its schema (zod 4) and default values — the same schema the server parses with. In a component,\n * `signUp.useForm({ onSubmit })` gives the form and its fields, typed by the schema: `<form.Form>`, `<form.TextField\n * name=\"email\" …/>`, `<form.Submit>`. Errors are i18n keys (`form.errors.tooShort`), never the engine's English.\n */\nexport const createKrizakaForm = makeCreateKrizakaForm(fields, FormRoot);\n\n/** The form sent by a Server Action: `useFormWithAction(signUp, createAccount)` — `signUp.useFormWithAction(createAccount)`. */\nexport function useFormWithAction<F extends { useFormWithAction: (...args: never[]) => unknown }>(form: F, ...args: Parameters<F[\"useFormWithAction\"]>): ReturnType<F[\"useFormWithAction\"]> {\n return form.useFormWithAction(...args) as ReturnType<F[\"useFormWithAction\"]>;\n}\n"
1891
2085
  },
1892
2086
  {
1893
2087
  "path": "meta.ts",
@@ -2043,7 +2237,7 @@
2043
2237
  },
2044
2238
  {
2045
2239
  "component": "DateField",
2046
- "description": "A date of the form (`YYYY-MM-DD`, `\"\"` for none): a calendar, or day · month · year typed in the locale's order.",
2240
+ "description": "A date of the form (`YYYY-MM-DD`, `\"\"` for none): a calendar, day · month · year typed, or wheels.",
2047
2241
  "props": [
2048
2242
  {
2049
2243
  "name": "label",
@@ -2063,7 +2257,7 @@
2063
2257
  "name": "autoComplete",
2064
2258
  "type": "\"off\" | \"bday\"",
2065
2259
  "default": null,
2066
- "description": "`bday` for a date of birth.",
2260
+ "description": "`bday` for a date of birth: typed boxes are autofilled; the wheels rest on 1 January 2000 until turned.",
2067
2261
  "required": false
2068
2262
  },
2069
2263
  {
@@ -2082,9 +2276,9 @@
2082
2276
  },
2083
2277
  {
2084
2278
  "name": "entry",
2085
- "type": "\"calendar\" | \"parts\"",
2279
+ "type": "\"wheel\" | \"calendar\" | \"parts\"",
2086
2280
  "default": "calendar",
2087
- "description": "`calendar` (default: a date near today — a publication, an appointment) or `parts` (day · month · year typed: a\nmemorable date, a birth date).",
2281
+ "description": "`calendar` (default: a date near today — a publication, an appointment), `parts` (day · month · year typed: a\nmemorable date, a birth date) or `wheel` (three wheels bounded by `min` / `max`: a birth date on a phone).",
2088
2282
  "required": false
2089
2283
  },
2090
2284
  {
@@ -2098,21 +2292,21 @@
2098
2292
  "name": "labels",
2099
2293
  "type": "(Partial<DatePickerLabels> & Partial<Record<DatePart, string>>)",
2100
2294
  "default": null,
2101
- "description": "The words of the calendar, or the names of the boxes (`day`, `month`, `year`), passed translated.",
2295
+ "description": "The words of the calendar, or the names of the parts (`day`, `month`, `year`), passed translated.",
2102
2296
  "required": false
2103
2297
  },
2104
2298
  {
2105
2299
  "name": "max",
2106
2300
  "type": "string",
2107
2301
  "default": null,
2108
- "description": "The latest date (ISO), for the calendar (`latestBirthDate(18)` for an age gate).",
2302
+ "description": "The latest date (ISO), for the calendar and the wheels (`latestBirthDate(18)` for an age gate).",
2109
2303
  "required": false
2110
2304
  },
2111
2305
  {
2112
2306
  "name": "min",
2113
2307
  "type": "string",
2114
2308
  "default": null,
2115
- "description": "The earliest date (ISO), for the calendar.",
2309
+ "description": "The earliest date (ISO), for the calendar and the wheels.",
2116
2310
  "required": false
2117
2311
  }
2118
2312
  ]
@@ -2398,9 +2592,9 @@
2398
2592
  },
2399
2593
  {
2400
2594
  "name": "variant",
2401
- "type": "\"list\" | \"cards\"",
2595
+ "type": "\"list\" | \"buttons\" | \"cards\" | \"segmented\"",
2402
2596
  "default": "list",
2403
- "description": "`list` (a dot and its words, default) or `cards` (a whole card is the radio: an amount, a plan, an audience).",
2597
+ "description": "`list` (a dot and its words, default), `cards` (a whole card is the radio: an amount, a plan, an audience),\n`buttons` (tactile keys that press in: a size, a tip; `description` is the key's second line) or `segmented`\n(two to four short choices on one line).",
2404
2598
  "required": false
2405
2599
  }
2406
2600
  ]
@@ -2462,7 +2656,7 @@
2462
2656
  },
2463
2657
  {
2464
2658
  "component": "Slider",
2465
- "description": "A number (or a range) of the form, chosen by feel: a share, a volume, a price range.",
2659
+ "description": "A number (or a range) of the form, chosen by feel: a share, a volume, a price range — the value floats while it is touched.",
2466
2660
  "props": [
2467
2661
  {
2468
2662
  "name": "label",
@@ -2506,6 +2700,13 @@
2506
2700
  "description": "A help text under the control, read with it; the error takes its place when there is one.",
2507
2701
  "required": false
2508
2702
  },
2703
+ {
2704
+ "name": "marks",
2705
+ "type": "readonly SliderMark[]",
2706
+ "default": null,
2707
+ "description": "Words under the track at some values.",
2708
+ "required": false
2709
+ },
2509
2710
  {
2510
2711
  "name": "max",
2511
2712
  "type": "number",
@@ -2533,6 +2734,13 @@
2533
2734
  "default": null,
2534
2735
  "description": "For a range (`[number, number]`): each thumb's name, passed translated.",
2535
2736
  "required": false
2737
+ },
2738
+ {
2739
+ "name": "ticks",
2740
+ "type": "boolean | readonly number[]",
2741
+ "default": null,
2742
+ "description": "A graduation per step (`true`) or at the values given.",
2743
+ "required": false
2536
2744
  }
2537
2745
  ]
2538
2746
  },