@krizaka/ui 2.10.0 → 3.1.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 (264) hide show
  1. package/README.md +4 -2
  2. package/dist/audio-player.js +1 -1
  3. package/dist/{chunk-EUPTXKH7.js → chunk-BRKMQ6JV.js} +2 -1
  4. package/dist/chunk-FDCMBWUU.js +168 -0
  5. package/dist/chunk-FDCMBWUU.js.map +1 -0
  6. package/dist/chunk-HCNOOPZE.js +210 -0
  7. package/dist/chunk-HCNOOPZE.js.map +1 -0
  8. package/dist/{chunk-X6TW3X6Y.js → chunk-LCZXS6AK.js} +2 -2
  9. package/dist/chunk-OGJI5X4W.js +157 -0
  10. package/dist/chunk-OGJI5X4W.js.map +1 -0
  11. package/dist/combobox.js +2 -2
  12. package/dist/data-table.d.ts +284 -24
  13. package/dist/data-table.js +914 -121
  14. package/dist/data-table.js.map +1 -1
  15. package/dist/date-picker.js +1 -1
  16. package/dist/filter-bar.d.ts +12 -12
  17. package/dist/filter-bar.js +9 -145
  18. package/dist/filter-bar.js.map +1 -1
  19. package/dist/form/server.d.ts +96 -0
  20. package/dist/form/server.js +82 -0
  21. package/dist/form/server.js.map +1 -0
  22. package/dist/form.d.ts +503 -0
  23. package/dist/form.js +760 -0
  24. package/dist/form.js.map +1 -0
  25. package/dist/image-cropper.js +1 -1
  26. package/dist/media-capture.js +1 -1
  27. package/dist/native/form.d.ts +376 -0
  28. package/dist/native/form.js +650 -0
  29. package/dist/native/form.js.map +1 -0
  30. package/dist/native/list.d.ts +95 -0
  31. package/dist/native/list.js +135 -0
  32. package/dist/native/list.js.map +1 -0
  33. package/dist/native.d.ts +113 -2
  34. package/dist/native.js +618 -388
  35. package/dist/native.js.map +1 -1
  36. package/dist/search-field.js +6 -160
  37. package/dist/search-field.js.map +1 -1
  38. package/dist/tag-input.js +2 -2
  39. package/dist/video-player.js +1 -1
  40. package/dist/video-trimmer.js +1 -1
  41. package/dist/virtual.d.ts +140 -0
  42. package/dist/virtual.js +19 -0
  43. package/dist/virtual.js.map +1 -0
  44. package/package.json +111 -8
  45. package/registry/achievement.json +1 -1
  46. package/registry/action-bar.json +1 -1
  47. package/registry/alert.json +1 -1
  48. package/registry/app-shell.json +1 -1
  49. package/registry/argument-card.json +1 -1
  50. package/registry/audio-player.json +1 -1
  51. package/registry/auth-layout.json +1 -1
  52. package/registry/avatar.json +1 -1
  53. package/registry/badge.json +1 -1
  54. package/registry/button.json +1 -1
  55. package/registry/card-grid.json +1 -1
  56. package/registry/card.json +1 -1
  57. package/registry/catalogue-page.json +1 -1
  58. package/registry/changelog-page.json +1 -1
  59. package/registry/checkbox.json +89 -4
  60. package/registry/chip-link.json +1 -1
  61. package/registry/chip.json +1 -1
  62. package/registry/choice-grid.json +1 -1
  63. package/registry/code-block.json +1 -1
  64. package/registry/combobox.json +1 -1
  65. package/registry/command.json +1 -1
  66. package/registry/component-page.json +1 -1
  67. package/registry/confirm-button.json +1 -1
  68. package/registry/contact-page.json +1 -1
  69. package/registry/continue-prompt.json +1 -1
  70. package/registry/countdown.json +1 -1
  71. package/registry/cta-panel.json +1 -1
  72. package/registry/dashboard-layout.json +1 -1
  73. package/registry/data-table.json +643 -49
  74. package/registry/date-picker.json +142 -4
  75. package/registry/decision-list.json +1 -1
  76. package/registry/detail-layout.json +1 -1
  77. package/registry/dialog.json +1 -1
  78. package/registry/docs-article-page.json +1 -1
  79. package/registry/docs-hub-page.json +1 -1
  80. package/registry/docs-layout.json +1 -1
  81. package/registry/dropdown-menu.json +1 -1
  82. package/registry/empty-state.json +1 -1
  83. package/registry/error-page.json +1 -1
  84. package/registry/examples/checkbox/native/terms.d.ts +4 -0
  85. package/registry/examples/checkbox/native/terms.dark.png +0 -0
  86. package/registry/examples/checkbox/native/terms.js +26 -0
  87. package/registry/examples/checkbox/native/terms.light.png +0 -0
  88. package/registry/examples/checkbox/native/terms.tsx +28 -0
  89. package/registry/examples/data-table/details.d.ts +4 -0
  90. package/registry/examples/data-table/details.js +33 -0
  91. package/registry/examples/data-table/details.tsx +31 -0
  92. package/registry/examples/data-table/filters.d.ts +4 -0
  93. package/registry/examples/data-table/filters.js +40 -0
  94. package/registry/examples/data-table/filters.tsx +40 -0
  95. package/registry/examples/data-table/grouped.d.ts +4 -0
  96. package/registry/examples/data-table/grouped.js +24 -0
  97. package/registry/examples/data-table/grouped.tsx +23 -0
  98. package/registry/examples/data-table/paginated.d.ts +4 -0
  99. package/registry/examples/data-table/paginated.js +23 -0
  100. package/registry/examples/data-table/paginated.tsx +22 -0
  101. package/registry/examples/data-table/pinned.d.ts +4 -0
  102. package/registry/examples/data-table/pinned.js +26 -0
  103. package/registry/examples/data-table/pinned.tsx +25 -0
  104. package/registry/examples/data-table/server.d.ts +4 -0
  105. package/registry/examples/data-table/server.js +40 -0
  106. package/registry/examples/data-table/server.tsx +40 -0
  107. package/registry/examples/data-table/virtualized.js +2 -2
  108. package/registry/examples/data-table/virtualized.tsx +3 -3
  109. package/registry/examples/date-picker/native/birth-date.d.ts +4 -0
  110. package/registry/examples/date-picker/native/birth-date.dark.png +0 -0
  111. package/registry/examples/date-picker/native/birth-date.js +30 -0
  112. package/registry/examples/date-picker/native/birth-date.light.png +0 -0
  113. package/registry/examples/date-picker/native/birth-date.tsx +27 -0
  114. package/registry/examples/field/native/default.d.ts +4 -0
  115. package/registry/examples/field/native/default.dark.png +0 -0
  116. package/registry/examples/field/native/default.js +29 -0
  117. package/registry/examples/field/native/default.light.png +0 -0
  118. package/registry/examples/field/native/default.tsx +23 -0
  119. package/registry/examples/form/amount.d.ts +4 -0
  120. package/registry/examples/form/amount.js +30 -0
  121. package/registry/examples/form/amount.tsx +36 -0
  122. package/registry/examples/form/audience.d.ts +4 -0
  123. package/registry/examples/form/audience.js +29 -0
  124. package/registry/examples/form/audience.tsx +31 -0
  125. package/registry/examples/form/native/amount.d.ts +4 -0
  126. package/registry/examples/form/native/amount.dark.png +0 -0
  127. package/registry/examples/form/native/amount.js +26 -0
  128. package/registry/examples/form/native/amount.light.png +0 -0
  129. package/registry/examples/form/native/amount.tsx +30 -0
  130. package/registry/examples/form/native/audience.d.ts +4 -0
  131. package/registry/examples/form/native/audience.dark.png +0 -0
  132. package/registry/examples/form/native/audience.js +24 -0
  133. package/registry/examples/form/native/audience.light.png +0 -0
  134. package/registry/examples/form/native/audience.tsx +24 -0
  135. package/registry/examples/form/native/profile.d.ts +4 -0
  136. package/registry/examples/form/native/profile.dark.png +0 -0
  137. package/registry/examples/form/native/profile.js +33 -0
  138. package/registry/examples/form/native/profile.light.png +0 -0
  139. package/registry/examples/form/native/profile.tsx +36 -0
  140. package/registry/examples/form/native/settings.d.ts +4 -0
  141. package/registry/examples/form/native/settings.dark.png +0 -0
  142. package/registry/examples/form/native/settings.js +25 -0
  143. package/registry/examples/form/native/settings.light.png +0 -0
  144. package/registry/examples/form/native/settings.tsx +23 -0
  145. package/registry/examples/form/native/sign-up.d.ts +4 -0
  146. package/registry/examples/form/native/sign-up.dark.png +0 -0
  147. package/registry/examples/form/native/sign-up.js +38 -0
  148. package/registry/examples/form/native/sign-up.light.png +0 -0
  149. package/registry/examples/form/native/sign-up.tsx +34 -0
  150. package/registry/examples/form/native/upload.d.ts +4 -0
  151. package/registry/examples/form/native/upload.dark.png +0 -0
  152. package/registry/examples/form/native/upload.js +23 -0
  153. package/registry/examples/form/native/upload.light.png +0 -0
  154. package/registry/examples/form/native/upload.tsx +22 -0
  155. package/registry/examples/form/profile.d.ts +4 -0
  156. package/registry/examples/form/profile.js +35 -0
  157. package/registry/examples/form/profile.tsx +40 -0
  158. package/registry/examples/form/report.d.ts +4 -0
  159. package/registry/examples/form/report.js +30 -0
  160. package/registry/examples/form/report.tsx +30 -0
  161. package/registry/examples/form/settings.d.ts +4 -0
  162. package/registry/examples/form/settings.js +30 -0
  163. package/registry/examples/form/settings.tsx +31 -0
  164. package/registry/examples/form/sign-up.d.ts +4 -0
  165. package/registry/examples/form/sign-up.js +38 -0
  166. package/registry/examples/form/sign-up.tsx +37 -0
  167. package/registry/examples/form/upload.d.ts +4 -0
  168. package/registry/examples/form/upload.js +32 -0
  169. package/registry/examples/form/upload.tsx +32 -0
  170. package/registry/examples/list/native/default.d.ts +4 -0
  171. package/registry/examples/list/native/default.dark.png +0 -0
  172. package/registry/examples/list/native/default.js +33 -0
  173. package/registry/examples/list/native/default.light.png +0 -0
  174. package/registry/examples/list/native/default.tsx +31 -0
  175. package/registry/examples/list/native/infinite.d.ts +4 -0
  176. package/registry/examples/list/native/infinite.dark.png +0 -0
  177. package/registry/examples/list/native/infinite.js +30 -0
  178. package/registry/examples/list/native/infinite.light.png +0 -0
  179. package/registry/examples/list/native/infinite.tsx +35 -0
  180. package/registry/examples/list/native/states.d.ts +4 -0
  181. package/registry/examples/list/native/states.dark.png +0 -0
  182. package/registry/examples/list/native/states.js +26 -0
  183. package/registry/examples/list/native/states.light.png +0 -0
  184. package/registry/examples/list/native/states.tsx +21 -0
  185. package/registry/examples/virtual/feed.d.ts +4 -0
  186. package/registry/examples/virtual/feed.js +26 -0
  187. package/registry/examples/virtual/feed.tsx +24 -0
  188. package/registry/examples/virtual/gallery.d.ts +4 -0
  189. package/registry/examples/virtual/gallery.js +31 -0
  190. package/registry/examples/virtual/gallery.tsx +28 -0
  191. package/registry/examples/virtual/messages.d.ts +4 -0
  192. package/registry/examples/virtual/messages.js +28 -0
  193. package/registry/examples/virtual/messages.tsx +28 -0
  194. package/registry/field.json +104 -4
  195. package/registry/file-upload.json +1 -1
  196. package/registry/filter-bar.json +1 -1
  197. package/registry/form.json +2940 -0
  198. package/registry/gallery-page.json +1 -1
  199. package/registry/glow-card.json +1 -1
  200. package/registry/goal-gauge.json +1 -1
  201. package/registry/how-it-works-page.json +1 -1
  202. package/registry/image-cropper.json +1 -1
  203. package/registry/index.json +86 -11
  204. package/registry/iso.json +1 -1
  205. package/registry/kbd.json +1 -1
  206. package/registry/landing-page.json +1 -1
  207. package/registry/landscape.json +1 -1
  208. package/registry/legal-page.json +1 -1
  209. package/registry/link-card.json +1 -1
  210. package/registry/list.json +249 -0
  211. package/registry/loader.json +1 -1
  212. package/registry/longform-page.json +1 -1
  213. package/registry/marketing-layout.json +1 -1
  214. package/registry/marquee.json +1 -1
  215. package/registry/mascot-quote.json +1 -1
  216. package/registry/mascot-state.json +2 -2
  217. package/registry/mascots.json +2 -2
  218. package/registry/media-capture.json +1 -1
  219. package/registry/meter.json +1 -1
  220. package/registry/nav-bar.json +1 -1
  221. package/registry/number-ticker.json +1 -1
  222. package/registry/page-header.json +1 -1
  223. package/registry/page-hero.json +1 -1
  224. package/registry/page.json +1 -1
  225. package/registry/pagination.json +1 -1
  226. package/registry/pointer-light.json +1 -1
  227. package/registry/popover.json +1 -1
  228. package/registry/pricing-page.json +1 -1
  229. package/registry/product-page.json +1 -1
  230. package/registry/profile-card.json +1 -1
  231. package/registry/progress.json +1 -1
  232. package/registry/promise-card.json +1 -1
  233. package/registry/radio-group.json +1 -1
  234. package/registry/scroll-stage.json +1 -1
  235. package/registry/search-field.json +1 -1
  236. package/registry/section-backdrop.json +1 -1
  237. package/registry/separator.json +1 -1
  238. package/registry/settings-layout.json +1 -1
  239. package/registry/skeleton.json +1 -1
  240. package/registry/slider.json +1 -1
  241. package/registry/slot.json +1 -1
  242. package/registry/spinner.json +1 -1
  243. package/registry/split-view.json +1 -1
  244. package/registry/stage-banner.json +1 -1
  245. package/registry/stat.json +1 -1
  246. package/registry/status-page.json +1 -1
  247. package/registry/stepper.json +1 -1
  248. package/registry/switch.json +1 -1
  249. package/registry/tabs.json +1 -1
  250. package/registry/tag-input.json +1 -1
  251. package/registry/terminal-replay.json +1 -1
  252. package/registry/theme.json +1 -1
  253. package/registry/toast.json +1 -1
  254. package/registry/toc.json +1 -1
  255. package/registry/tooltip.json +1 -1
  256. package/registry/video-player.json +1 -1
  257. package/registry/video-trimmer.json +1 -1
  258. package/registry/virtual.json +569 -0
  259. package/skills/data-table/SKILL.md +56 -0
  260. package/skills/form/SKILL.md +44 -0
  261. package/skills/native-list/SKILL.md +39 -0
  262. package/skills/virtual/SKILL.md +35 -0
  263. /package/dist/{chunk-EUPTXKH7.js.map → chunk-BRKMQ6JV.js.map} +0 -0
  264. /package/dist/{chunk-X6TW3X6Y.js.map → chunk-LCZXS6AK.js.map} +0 -0
@@ -0,0 +1,2940 @@
1
+ {
2
+ "name": "form",
3
+ "type": "primitive",
4
+ "title": "Form",
5
+ "summary": "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.",
6
+ "description": "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.",
7
+ "status": "beta",
8
+ "category": "forms",
9
+ "platforms": "both",
10
+ "whenToUse": [
11
+ "Any form that sends something: sign-up, profile, upload details, an amount, an audience, a report, settings.",
12
+ "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.",
13
+ "When a value depends on the server (a username's availability): `validateAsync` on the field, debounced and aborted."
14
+ ],
15
+ "whenNotToUse": [
16
+ {
17
+ "when": "For a single search box that filters as you type: no submit, no validation.",
18
+ "use": "search-field"
19
+ },
20
+ {
21
+ "when": "For a filter bar over a list: its state is the address, not a form.",
22
+ "use": "filter-bar"
23
+ },
24
+ {
25
+ "when": "For a setting that applies at once on its own (a theme switch): the control alone.",
26
+ "use": "switch"
27
+ }
28
+ ],
29
+ "bestPractices": [
30
+ "Define the form once (`createKrizakaForm({ schema, defaultValues })`) beside the API's contract; use it in components with `form.useForm({ onSubmit })`.",
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
+ "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
+ "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.",
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
+ ],
37
+ "accessibility": {
38
+ "keyboard": [
39
+ {
40
+ "keys": "Enter",
41
+ "action": "Sends the form from a text field (web); the keyboard's return key moves to the next field (native)."
42
+ },
43
+ {
44
+ "keys": "Tab",
45
+ "action": "Leaves a field: from then on its error shows, and goes as soon as it is fixed."
46
+ }
47
+ ],
48
+ "notes": [
49
+ "Every control is named by its visible label; its hint and its error are in `aria-describedby`, and an invalid control has `aria-invalid`.",
50
+ "Errors are announced (`role=\"alert\"`); a refused submit focuses the first invalid field on the web and announces the first error on native.",
51
+ "An availability check names its wait (`checkingLabel`) and says when the value is free (`availableHint`): never by colour alone.",
52
+ "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."
53
+ ]
54
+ },
55
+ "related": [
56
+ "field",
57
+ "checkbox",
58
+ "switch",
59
+ "radio-group",
60
+ "combobox",
61
+ "date-picker",
62
+ "tag-input",
63
+ "file-upload",
64
+ "slider"
65
+ ],
66
+ "web": {
67
+ "entry": "@krizaka/ui/form",
68
+ "import": "import { createKrizakaForm, useFormWithAction, latestBirthDate, useKrizakaField, formMessage } from \"@krizaka/ui/form\";",
69
+ "props": [
70
+ {
71
+ "component": "Checkbox",
72
+ "description": "A box of the form (`boolean`): terms to accept, an option.",
73
+ "props": [
74
+ {
75
+ "name": "label",
76
+ "type": "ReactNode",
77
+ "default": null,
78
+ "description": "The words next to the box, clickable with it — text, or a sentence with links (terms, privacy).",
79
+ "required": true
80
+ },
81
+ {
82
+ "name": "name",
83
+ "type": "string",
84
+ "default": null,
85
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
86
+ "required": true
87
+ },
88
+ {
89
+ "name": "className",
90
+ "type": "string",
91
+ "default": null,
92
+ "description": "Classes merged on the field's root.",
93
+ "required": false
94
+ },
95
+ {
96
+ "name": "disabled",
97
+ "type": "boolean",
98
+ "default": null,
99
+ "description": "Not available.",
100
+ "required": false
101
+ },
102
+ {
103
+ "name": "hint",
104
+ "type": "string",
105
+ "default": null,
106
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
107
+ "required": false
108
+ }
109
+ ]
110
+ },
111
+ {
112
+ "component": "Combobox",
113
+ "description": "A choice among many, typed to filter or fetched: one value (`string`) or several (`string[]`).",
114
+ "props": [
115
+ {
116
+ "name": "label",
117
+ "type": "string",
118
+ "default": null,
119
+ "description": "The visible label, passed translated (also the control's accessible name).",
120
+ "required": true
121
+ },
122
+ {
123
+ "name": "name",
124
+ "type": "string",
125
+ "default": null,
126
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
127
+ "required": true
128
+ },
129
+ {
130
+ "name": "className",
131
+ "type": "string",
132
+ "default": null,
133
+ "description": "Classes merged on the field's root.",
134
+ "required": false
135
+ },
136
+ {
137
+ "name": "creatable",
138
+ "type": "boolean",
139
+ "default": null,
140
+ "description": "Offers to create the query as a value.",
141
+ "required": false
142
+ },
143
+ {
144
+ "name": "disabled",
145
+ "type": "boolean",
146
+ "default": null,
147
+ "description": "Not available.",
148
+ "required": false
149
+ },
150
+ {
151
+ "name": "hint",
152
+ "type": "string",
153
+ "default": null,
154
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
155
+ "required": false
156
+ },
157
+ {
158
+ "name": "labels",
159
+ "type": "Partial<ComboboxLabels>",
160
+ "default": null,
161
+ "description": "The words of the control, passed translated.",
162
+ "required": false
163
+ },
164
+ {
165
+ "name": "multiple",
166
+ "type": "boolean",
167
+ "default": null,
168
+ "description": "Several values (`string[]`), shown as chips; one (`string`) by default.",
169
+ "required": false
170
+ },
171
+ {
172
+ "name": "onSearch",
173
+ "type": "((query: string, signal: AbortSignal) => Promise<readonly ComboboxOption[]>)",
174
+ "default": null,
175
+ "description": "Fetches the options for a query (debounced, aborted by a newer one).",
176
+ "required": false
177
+ },
178
+ {
179
+ "name": "options",
180
+ "type": "readonly ComboboxOption[]",
181
+ "default": "[]",
182
+ "description": "The options, filtered as the person types.",
183
+ "required": false
184
+ },
185
+ {
186
+ "name": "placeholder",
187
+ "type": "string",
188
+ "default": null,
189
+ "description": "The input's placeholder, passed translated.",
190
+ "required": false
191
+ }
192
+ ]
193
+ },
194
+ {
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.",
197
+ "props": [
198
+ {
199
+ "name": "label",
200
+ "type": "string",
201
+ "default": null,
202
+ "description": "The visible label, passed translated (also the control's accessible name).",
203
+ "required": true
204
+ },
205
+ {
206
+ "name": "name",
207
+ "type": "string",
208
+ "default": null,
209
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
210
+ "required": true
211
+ },
212
+ {
213
+ "name": "autoComplete",
214
+ "type": "\"off\" | \"bday\"",
215
+ "default": null,
216
+ "description": "`bday` for a date of birth.",
217
+ "required": false
218
+ },
219
+ {
220
+ "name": "className",
221
+ "type": "string",
222
+ "default": null,
223
+ "description": "Classes merged on the field's root.",
224
+ "required": false
225
+ },
226
+ {
227
+ "name": "disabled",
228
+ "type": "boolean",
229
+ "default": null,
230
+ "description": "Not available.",
231
+ "required": false
232
+ },
233
+ {
234
+ "name": "entry",
235
+ "type": "\"calendar\" | \"parts\"",
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).",
238
+ "required": false
239
+ },
240
+ {
241
+ "name": "hint",
242
+ "type": "string",
243
+ "default": null,
244
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
245
+ "required": false
246
+ },
247
+ {
248
+ "name": "labels",
249
+ "type": "(Partial<DatePickerLabels> & Partial<Record<DatePart, string>>)",
250
+ "default": null,
251
+ "description": "The words of the calendar, or the names of the boxes (`day`, `month`, `year`), passed translated.",
252
+ "required": false
253
+ },
254
+ {
255
+ "name": "max",
256
+ "type": "string",
257
+ "default": null,
258
+ "description": "The latest date (ISO), for the calendar (`latestBirthDate(18)` for an age gate).",
259
+ "required": false
260
+ },
261
+ {
262
+ "name": "min",
263
+ "type": "string",
264
+ "default": null,
265
+ "description": "The earliest date (ISO), for the calendar.",
266
+ "required": false
267
+ }
268
+ ]
269
+ },
270
+ {
271
+ "component": "FieldArray",
272
+ "description": "A list of fields that grows and shrinks (links, tiers, guests): each item's fields named `links[0].url`.",
273
+ "props": [
274
+ {
275
+ "name": "children",
276
+ "type": "(array: KrizakaFieldArray<unknown>) => ReactNode",
277
+ "default": null,
278
+ "description": "Draws the items: `items`, `push`, `remove`, `move`, `fieldName(i, \"url\")` for the fields of an item.",
279
+ "required": true
280
+ },
281
+ {
282
+ "name": "name",
283
+ "type": "string",
284
+ "default": null,
285
+ "description": "The array's name in the schema (typed by the form).",
286
+ "required": true
287
+ }
288
+ ]
289
+ },
290
+ {
291
+ "component": "FileUpload",
292
+ "description": "Files of the form (`File[]`): a drop zone, then the chosen files, each removable.",
293
+ "props": [
294
+ {
295
+ "name": "browseLabel",
296
+ "type": "string",
297
+ "default": null,
298
+ "description": "The button that opens the file picker, passed translated.",
299
+ "required": true
300
+ },
301
+ {
302
+ "name": "label",
303
+ "type": "string",
304
+ "default": null,
305
+ "description": "The visible label, passed translated (also the control's accessible name).",
306
+ "required": true
307
+ },
308
+ {
309
+ "name": "name",
310
+ "type": "string",
311
+ "default": null,
312
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
313
+ "required": true
314
+ },
315
+ {
316
+ "name": "removeLabel",
317
+ "type": "(fileName: string) => string",
318
+ "default": null,
319
+ "description": "The name of a file's remove button (`(name) => \"Remove \" + name`), passed translated.",
320
+ "required": true
321
+ },
322
+ {
323
+ "name": "title",
324
+ "type": "string",
325
+ "default": null,
326
+ "description": "The title in the zone (\"Drop your video here\"), passed translated.",
327
+ "required": true
328
+ },
329
+ {
330
+ "name": "accept",
331
+ "type": "string",
332
+ "default": null,
333
+ "description": "The types accepted (`video/*`, `image/png,.pdf`).",
334
+ "required": false
335
+ },
336
+ {
337
+ "name": "className",
338
+ "type": "string",
339
+ "default": null,
340
+ "description": "Classes merged on the field's root.",
341
+ "required": false
342
+ },
343
+ {
344
+ "name": "disabled",
345
+ "type": "boolean",
346
+ "default": null,
347
+ "description": "Not available.",
348
+ "required": false
349
+ },
350
+ {
351
+ "name": "hint",
352
+ "type": "string",
353
+ "default": null,
354
+ "description": "What is accepted, in words (\"MP4 or MOV, up to 4 GB\").",
355
+ "required": false
356
+ },
357
+ {
358
+ "name": "maxFiles",
359
+ "type": "number",
360
+ "default": null,
361
+ "description": "The most files.",
362
+ "required": false
363
+ },
364
+ {
365
+ "name": "maxSize",
366
+ "type": "number",
367
+ "default": null,
368
+ "description": "The largest file, in bytes.",
369
+ "required": false
370
+ },
371
+ {
372
+ "name": "multiple",
373
+ "type": "boolean",
374
+ "default": null,
375
+ "description": "Several files (`File[]`, up to `maxFiles`); one by default (still a `File[]` of one).",
376
+ "required": false
377
+ },
378
+ {
379
+ "name": "refusalLabel",
380
+ "type": "((rejection: FileRejection) => string)",
381
+ "default": null,
382
+ "description": "The words of a refused file (`(r) => r.file.name + \" is too large\"`).",
383
+ "required": false
384
+ }
385
+ ]
386
+ },
387
+ {
388
+ "component": "FormError",
389
+ "description": "The whole form's error — a refusal of the server that belongs to no field (a conflict, too many tries, no network).",
390
+ "props": [
391
+ {
392
+ "name": "className",
393
+ "type": "string",
394
+ "default": null,
395
+ "description": "Classes merged on the alert.",
396
+ "required": false
397
+ },
398
+ {
399
+ "name": "title",
400
+ "type": "string",
401
+ "default": null,
402
+ "description": "A headline above the message (\"We could not create your account\"), passed translated.",
403
+ "required": false
404
+ }
405
+ ]
406
+ },
407
+ {
408
+ "component": "FormField",
409
+ "description": "A field drawn by the app (a control the package does not have): the form's value, errors and ids, any markup.",
410
+ "props": [
411
+ {
412
+ "name": "children",
413
+ "type": "(field: KrizakaField<unknown>) => ReactNode",
414
+ "default": null,
415
+ "description": "Draws the field: its value, setter, error and ids (`useKrizakaField`).",
416
+ "required": true
417
+ },
418
+ {
419
+ "name": "name",
420
+ "type": "string",
421
+ "default": null,
422
+ "description": "The field's name in the schema (typed by the form).",
423
+ "required": true
424
+ },
425
+ {
426
+ "name": "asyncDebounceMs",
427
+ "type": "number",
428
+ "default": null,
429
+ "description": "The wait, in ms, before `validateAsync`.",
430
+ "required": false
431
+ },
432
+ {
433
+ "name": "validateAsync",
434
+ "type": "FieldCheck<unknown>",
435
+ "default": null,
436
+ "description": "Checks the value on the server as it changes.",
437
+ "required": false
438
+ }
439
+ ]
440
+ },
441
+ {
442
+ "component": "Money",
443
+ "description": "An amount of the form, stored as integer minor units (cents: `1250` = 12.50), typed the locale's way.",
444
+ "props": [
445
+ {
446
+ "name": "currency",
447
+ "type": "string",
448
+ "default": null,
449
+ "description": "The currency (ISO 4217: `USD`, `EUR`, `JPY`): its sign before the amount, its decimals.",
450
+ "required": true
451
+ },
452
+ {
453
+ "name": "label",
454
+ "type": "string",
455
+ "default": null,
456
+ "description": "The visible label, passed translated (also the control's accessible name).",
457
+ "required": true
458
+ },
459
+ {
460
+ "name": "name",
461
+ "type": "string",
462
+ "default": null,
463
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
464
+ "required": true
465
+ },
466
+ {
467
+ "name": "className",
468
+ "type": "string",
469
+ "default": null,
470
+ "description": "Classes merged on the field's root.",
471
+ "required": false
472
+ },
473
+ {
474
+ "name": "disabled",
475
+ "type": "boolean",
476
+ "default": null,
477
+ "description": "Not available.",
478
+ "required": false
479
+ },
480
+ {
481
+ "name": "hint",
482
+ "type": "string",
483
+ "default": null,
484
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
485
+ "required": false
486
+ },
487
+ {
488
+ "name": "placeholder",
489
+ "type": "string",
490
+ "default": null,
491
+ "description": "An example of an amount, passed translated.",
492
+ "required": false
493
+ }
494
+ ]
495
+ },
496
+ {
497
+ "component": "RadioGroup",
498
+ "description": "One choice among a few, all visible.",
499
+ "props": [
500
+ {
501
+ "name": "label",
502
+ "type": "string",
503
+ "default": null,
504
+ "description": "The visible label, passed translated (also the control's accessible name).",
505
+ "required": true
506
+ },
507
+ {
508
+ "name": "name",
509
+ "type": "string",
510
+ "default": null,
511
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
512
+ "required": true
513
+ },
514
+ {
515
+ "name": "options",
516
+ "type": "readonly FormOption[]",
517
+ "default": null,
518
+ "description": "The choices, all visible (two to five).",
519
+ "required": true
520
+ },
521
+ {
522
+ "name": "className",
523
+ "type": "string",
524
+ "default": null,
525
+ "description": "Classes merged on the field's root.",
526
+ "required": false
527
+ },
528
+ {
529
+ "name": "disabled",
530
+ "type": "boolean",
531
+ "default": null,
532
+ "description": "Not available.",
533
+ "required": false
534
+ },
535
+ {
536
+ "name": "hint",
537
+ "type": "string",
538
+ "default": null,
539
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
540
+ "required": false
541
+ },
542
+ {
543
+ "name": "orientation",
544
+ "type": "\"horizontal\" | \"vertical\"",
545
+ "default": "vertical",
546
+ "description": "`vertical` (default) or `horizontal`.",
547
+ "required": false
548
+ },
549
+ {
550
+ "name": "variant",
551
+ "type": "\"list\" | \"cards\"",
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).",
554
+ "required": false
555
+ }
556
+ ]
557
+ },
558
+ {
559
+ "component": "Select",
560
+ "description": "One choice among many, in the platform's menu (`<select>`: accessible and right on phones).",
561
+ "props": [
562
+ {
563
+ "name": "label",
564
+ "type": "string",
565
+ "default": null,
566
+ "description": "The visible label, passed translated (also the control's accessible name).",
567
+ "required": true
568
+ },
569
+ {
570
+ "name": "name",
571
+ "type": "string",
572
+ "default": null,
573
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
574
+ "required": true
575
+ },
576
+ {
577
+ "name": "options",
578
+ "type": "readonly FormOption[]",
579
+ "default": null,
580
+ "description": "The choices, in the platform's own menu (a country, a category).",
581
+ "required": true
582
+ },
583
+ {
584
+ "name": "className",
585
+ "type": "string",
586
+ "default": null,
587
+ "description": "Classes merged on the field's root.",
588
+ "required": false
589
+ },
590
+ {
591
+ "name": "disabled",
592
+ "type": "boolean",
593
+ "default": null,
594
+ "description": "Not available.",
595
+ "required": false
596
+ },
597
+ {
598
+ "name": "hint",
599
+ "type": "string",
600
+ "default": null,
601
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
602
+ "required": false
603
+ },
604
+ {
605
+ "name": "placeholder",
606
+ "type": "string",
607
+ "default": null,
608
+ "description": "The first line while nothing is chosen (\"Choose a category\"), passed translated.",
609
+ "required": false
610
+ }
611
+ ]
612
+ },
613
+ {
614
+ "component": "Slider",
615
+ "description": "A number (or a range) of the form, chosen by feel: a share, a volume, a price range.",
616
+ "props": [
617
+ {
618
+ "name": "label",
619
+ "type": "string",
620
+ "default": null,
621
+ "description": "The visible label, passed translated (also the control's accessible name).",
622
+ "required": true
623
+ },
624
+ {
625
+ "name": "name",
626
+ "type": "string",
627
+ "default": null,
628
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
629
+ "required": true
630
+ },
631
+ {
632
+ "name": "className",
633
+ "type": "string",
634
+ "default": null,
635
+ "description": "Classes merged on the field's root.",
636
+ "required": false
637
+ },
638
+ {
639
+ "name": "disabled",
640
+ "type": "boolean",
641
+ "default": null,
642
+ "description": "Not available.",
643
+ "required": false
644
+ },
645
+ {
646
+ "name": "formatValue",
647
+ "type": "((value: number) => string)",
648
+ "default": null,
649
+ "description": "The value as words (\"15 %\", \"3 min\"), shown and read.",
650
+ "required": false
651
+ },
652
+ {
653
+ "name": "hint",
654
+ "type": "string",
655
+ "default": null,
656
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
657
+ "required": false
658
+ },
659
+ {
660
+ "name": "max",
661
+ "type": "number",
662
+ "default": null,
663
+ "description": "The highest value. Default 100.",
664
+ "required": false
665
+ },
666
+ {
667
+ "name": "min",
668
+ "type": "number",
669
+ "default": null,
670
+ "description": "The lowest value. Default 0.",
671
+ "required": false
672
+ },
673
+ {
674
+ "name": "step",
675
+ "type": "number",
676
+ "default": null,
677
+ "description": "The step. Default 1.",
678
+ "required": false
679
+ },
680
+ {
681
+ "name": "thumbLabels",
682
+ "type": "[string, string]",
683
+ "default": null,
684
+ "description": "For a range (`[number, number]`): each thumb's name, passed translated.",
685
+ "required": false
686
+ }
687
+ ]
688
+ },
689
+ {
690
+ "component": "Submit",
691
+ "description": "The submit button: busy while the form is sent (not pressable twice), never disabled for an invalid form.",
692
+ "props": [
693
+ {
694
+ "name": "announce",
695
+ "type": "string",
696
+ "default": null,
697
+ "description": "A message announced politely by assistive technology when it changes (“Saved”, “Upload failed”) — passed translated.",
698
+ "required": false
699
+ },
700
+ {
701
+ "name": "asChild",
702
+ "type": "boolean",
703
+ "default": null,
704
+ "description": "Renders the child (e.g. a <Link>) instead of the <button>, with props, classes and ref merged.",
705
+ "required": false
706
+ },
707
+ {
708
+ "name": "indicator",
709
+ "type": "boolean",
710
+ "default": "true",
711
+ "description": "Shows the glyph while `loading` / with a `status`. `false`: you draw your own.",
712
+ "required": false
713
+ },
714
+ {
715
+ "name": "magnetic",
716
+ "type": "boolean",
717
+ "default": null,
718
+ "description": "The button leans a few pixels toward a fine pointer (driven by `MotionObserver`; never under reduced motion or on touch).",
719
+ "required": false
720
+ },
721
+ {
722
+ "name": "progress",
723
+ "type": "number | null",
724
+ "default": null,
725
+ "description": "Between 0 and 1, while `loading`: the glyph's trace and a light fill across the button grow to the value (an\nupload, an export).",
726
+ "required": false
727
+ },
728
+ {
729
+ "name": "shape",
730
+ "type": "\"rounded\" | \"pill\" | \"cut\"",
731
+ "default": "rounded",
732
+ "description": "rounded · pill · cut (45° corners, the Krizaka chamfer).",
733
+ "required": false
734
+ },
735
+ {
736
+ "name": "shimmer",
737
+ "type": "boolean",
738
+ "default": null,
739
+ "description": "A sheen of light crosses the button on hover — for the one action a page is about.",
740
+ "required": false
741
+ },
742
+ {
743
+ "name": "size",
744
+ "type": "\"icon\" | \"sm\" | \"md\" | \"lg\"",
745
+ "default": "md",
746
+ "description": "sm · md · lg, or `icon` (a square).",
747
+ "required": false
748
+ },
749
+ {
750
+ "name": "status",
751
+ "type": "\"success\" | \"error\" | null",
752
+ "default": null,
753
+ "description": "How the action ended: `success` draws a check, `error` a cross (and the button shakes its head once); a ring of\nthe status colour opens around it. Clear it to return to rest.",
754
+ "required": false
755
+ },
756
+ {
757
+ "name": "variant",
758
+ "type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
759
+ "default": "primary",
760
+ "description": "The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n`ghost`, `danger`.",
761
+ "required": false
762
+ }
763
+ ]
764
+ },
765
+ {
766
+ "component": "Switch",
767
+ "description": "An on/off setting of the form (`boolean`): the label and its hint beside the switch.",
768
+ "props": [
769
+ {
770
+ "name": "label",
771
+ "type": "string",
772
+ "default": null,
773
+ "description": "The visible label, passed translated (also the control's accessible name).",
774
+ "required": true
775
+ },
776
+ {
777
+ "name": "name",
778
+ "type": "string",
779
+ "default": null,
780
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
781
+ "required": true
782
+ },
783
+ {
784
+ "name": "className",
785
+ "type": "string",
786
+ "default": null,
787
+ "description": "Classes merged on the field's root.",
788
+ "required": false
789
+ },
790
+ {
791
+ "name": "disabled",
792
+ "type": "boolean",
793
+ "default": null,
794
+ "description": "Not available.",
795
+ "required": false
796
+ },
797
+ {
798
+ "name": "hint",
799
+ "type": "string",
800
+ "default": null,
801
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
802
+ "required": false
803
+ }
804
+ ]
805
+ },
806
+ {
807
+ "component": "TagInput",
808
+ "description": "Free words of the form as chips (`string[]`): tags, keywords.",
809
+ "props": [
810
+ {
811
+ "name": "label",
812
+ "type": "string",
813
+ "default": null,
814
+ "description": "The visible label, passed translated (also the control's accessible name).",
815
+ "required": true
816
+ },
817
+ {
818
+ "name": "name",
819
+ "type": "string",
820
+ "default": null,
821
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
822
+ "required": true
823
+ },
824
+ {
825
+ "name": "className",
826
+ "type": "string",
827
+ "default": null,
828
+ "description": "Classes merged on the field's root.",
829
+ "required": false
830
+ },
831
+ {
832
+ "name": "disabled",
833
+ "type": "boolean",
834
+ "default": null,
835
+ "description": "Not available.",
836
+ "required": false
837
+ },
838
+ {
839
+ "name": "hint",
840
+ "type": "string",
841
+ "default": null,
842
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
843
+ "required": false
844
+ },
845
+ {
846
+ "name": "labels",
847
+ "type": "Partial<Pick<ComboboxLabels, \"results\" | \"toggle\" | \"remove\" | \"empty\" | \"create\">>",
848
+ "default": null,
849
+ "description": "The words of the field, passed translated; English by default.",
850
+ "required": false
851
+ },
852
+ {
853
+ "name": "max",
854
+ "type": "number",
855
+ "default": null,
856
+ "description": "Most tags; the field stops offering more past it.",
857
+ "required": false
858
+ },
859
+ {
860
+ "name": "normalize",
861
+ "type": "((tag: string) => string)",
862
+ "default": null,
863
+ "description": "Cleans a word before it is added (`(t) => t.toLowerCase().replace(/^#/, \"\")`). An empty result is ignored.",
864
+ "required": false
865
+ },
866
+ {
867
+ "name": "placeholder",
868
+ "type": "string",
869
+ "default": null,
870
+ "description": "The input's placeholder, passed translated.",
871
+ "required": false
872
+ },
873
+ {
874
+ "name": "separators",
875
+ "type": "readonly string[]",
876
+ "default": null,
877
+ "description": "Keys that add the word, besides Enter. Default `[\",\"]`.",
878
+ "required": false
879
+ },
880
+ {
881
+ "name": "suggestions",
882
+ "type": "readonly string[]",
883
+ "default": null,
884
+ "description": "Words offered as the person types (their own history, a curated list).",
885
+ "required": false
886
+ }
887
+ ]
888
+ },
889
+ {
890
+ "component": "TextArea",
891
+ "description": "A longer text of the form (a bio, a description, a report).",
892
+ "props": [
893
+ {
894
+ "name": "label",
895
+ "type": "string",
896
+ "default": null,
897
+ "description": "The visible label, passed translated (also the control's accessible name).",
898
+ "required": true
899
+ },
900
+ {
901
+ "name": "name",
902
+ "type": "string",
903
+ "default": null,
904
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
905
+ "required": true
906
+ },
907
+ {
908
+ "name": "className",
909
+ "type": "string",
910
+ "default": null,
911
+ "description": "Classes merged on the field's root.",
912
+ "required": false
913
+ },
914
+ {
915
+ "name": "disabled",
916
+ "type": "boolean",
917
+ "default": null,
918
+ "description": "Not available.",
919
+ "required": false
920
+ },
921
+ {
922
+ "name": "hint",
923
+ "type": "string",
924
+ "default": null,
925
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
926
+ "required": false
927
+ },
928
+ {
929
+ "name": "maxLength",
930
+ "type": "number",
931
+ "default": null,
932
+ "description": "The most characters.",
933
+ "required": false
934
+ },
935
+ {
936
+ "name": "placeholder",
937
+ "type": "string",
938
+ "default": null,
939
+ "description": "An example of a value, passed translated.",
940
+ "required": false
941
+ },
942
+ {
943
+ "name": "rows",
944
+ "type": "number",
945
+ "default": "4",
946
+ "description": "The visible lines. Default 4.",
947
+ "required": false
948
+ }
949
+ ]
950
+ },
951
+ {
952
+ "component": "TextField",
953
+ "description": "A text field of the form: label, input, hint or error, an optional check on the server.",
954
+ "props": [
955
+ {
956
+ "name": "label",
957
+ "type": "string",
958
+ "default": null,
959
+ "description": "The visible label, passed translated (also the control's accessible name).",
960
+ "required": true
961
+ },
962
+ {
963
+ "name": "name",
964
+ "type": "string",
965
+ "default": null,
966
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
967
+ "required": true
968
+ },
969
+ {
970
+ "name": "asyncDebounceMs",
971
+ "type": "number",
972
+ "default": null,
973
+ "description": "The wait, in ms, between the last key and `validateAsync`. Default 350.",
974
+ "required": false
975
+ },
976
+ {
977
+ "name": "autoComplete",
978
+ "type": "string",
979
+ "default": null,
980
+ "description": "The browser's autofill (`email`, `username`, `new-password`, `name`).",
981
+ "required": false
982
+ },
983
+ {
984
+ "name": "availableHint",
985
+ "type": "string",
986
+ "default": null,
987
+ "description": "The words once `validateAsync` accepted the value (\"@eric is free\"), passed translated.",
988
+ "required": false
989
+ },
990
+ {
991
+ "name": "checkingLabel",
992
+ "type": "string",
993
+ "default": "Checking",
994
+ "description": "The name of the wait while it checks (\"Checking\"), passed translated.",
995
+ "required": false
996
+ },
997
+ {
998
+ "name": "className",
999
+ "type": "string",
1000
+ "default": null,
1001
+ "description": "Classes merged on the field's root.",
1002
+ "required": false
1003
+ },
1004
+ {
1005
+ "name": "disabled",
1006
+ "type": "boolean",
1007
+ "default": null,
1008
+ "description": "Not available.",
1009
+ "required": false
1010
+ },
1011
+ {
1012
+ "name": "hint",
1013
+ "type": "string",
1014
+ "default": null,
1015
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
1016
+ "required": false
1017
+ },
1018
+ {
1019
+ "name": "inputMode",
1020
+ "type": "\"none\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"email\" | \"numeric\" | \"decimal\"",
1021
+ "default": null,
1022
+ "description": "The keyboard on a phone (`numeric`, `decimal`).",
1023
+ "required": false
1024
+ },
1025
+ {
1026
+ "name": "maxLength",
1027
+ "type": "number",
1028
+ "default": null,
1029
+ "description": "The most characters.",
1030
+ "required": false
1031
+ },
1032
+ {
1033
+ "name": "placeholder",
1034
+ "type": "string",
1035
+ "default": null,
1036
+ "description": "An example of a value, passed translated — never the label.",
1037
+ "required": false
1038
+ },
1039
+ {
1040
+ "name": "start",
1041
+ "type": "ReactNode",
1042
+ "default": null,
1043
+ "description": "Something before the text (an `@`, an icon).",
1044
+ "required": false
1045
+ },
1046
+ {
1047
+ "name": "transform",
1048
+ "type": "((raw: string) => string)",
1049
+ "default": null,
1050
+ "description": "Cleans what is typed before it is stored (`(v) => v.toLowerCase().replace(/\\s+/g, \"_\")`).",
1051
+ "required": false
1052
+ },
1053
+ {
1054
+ "name": "type",
1055
+ "type": "\"search\" | \"text\" | \"tel\" | \"url\" | \"email\" | \"password\"",
1056
+ "default": "text",
1057
+ "description": "`text` (default), `email`, `password`, `url`, `tel`, `search`: the right keyboard on a phone.",
1058
+ "required": false
1059
+ },
1060
+ {
1061
+ "name": "validateAsync",
1062
+ "type": "FieldCheck<string>",
1063
+ "default": null,
1064
+ "description": "Checks the value on the server as it is typed (availability): an error key, or nothing when it is fine.",
1065
+ "required": false
1066
+ }
1067
+ ]
1068
+ },
1069
+ {
1070
+ "component": "Watch",
1071
+ "description": "Renders from a field's current value (a part shown only when a box is ticked, a total).",
1072
+ "props": [
1073
+ {
1074
+ "name": "children",
1075
+ "type": "(value: unknown) => ReactNode",
1076
+ "default": null,
1077
+ "description": "Draws from its current value: show a part of the form only when a box is ticked.",
1078
+ "required": true
1079
+ },
1080
+ {
1081
+ "name": "name",
1082
+ "type": "string",
1083
+ "default": null,
1084
+ "description": "The field to follow (typed by the form).",
1085
+ "required": true
1086
+ }
1087
+ ]
1088
+ }
1089
+ ],
1090
+ "examples": [
1091
+ {
1092
+ "name": "sign-up",
1093
+ "title": "Sign-up",
1094
+ "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.",
1095
+ "path": "examples/form/sign-up.tsx",
1096
+ "module": "@krizaka/ui/registry/examples/form/sign-up",
1097
+ "code": "\"use client\";\n\nimport { KrizakaApiError } from \"@krizaka/query\";\nimport { createKrizakaForm, latestBirthDate } from \"@krizaka/ui/form\";\nimport { z } from \"zod\";\n\n// The schema the sign-up API parses with: the same rules, the same keys. `signUp.*` are the app's own words.\nconst signUp = createKrizakaForm({\n schema: z.object({\n email: z.email(),\n username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/, \"signUp.username.format\"),\n password: z.string().min(8),\n dateOfBirth: z.iso.date().refine((d) => d <= latestBirthDate(18), \"form.errors.UNDERAGE\"),\n acceptTerms: z.boolean().refine(Boolean, \"signUp.terms.required\"),\n }),\n defaultValues: { email: \"\", username: \"\", password: \"\", dateOfBirth: \"\", acceptTerms: false },\n});\nconst WORDS: Record<string, string> = { \"signUp.username.format\": \"Lowercase letters, digits and _ only.\", \"signUp.username.taken\": \"This username is taken.\", \"signUp.terms.required\": \"Accept the terms to continue.\", \"form.errors.UNDERAGE\": \"You must be 18 or older.\", \"form.errors.BIRTH_DATE_INVALID\": \"Enter your real date of birth.\", \"form.errors.USERNAME_RESERVED\": \"This username is reserved.\", \"apiErrors.ACCOUNT_EXISTS\": \"This e-mail already has an account. Sign in instead.\" };\nconst taken = (u: string) => new Promise<boolean>((done) => setTimeout(() => done([\"eric\", \"elena\"].includes(u)), 300));\n// The server's codes (Orochia's register API): the ones that belong to a field land under it, the others above the button.\nconst register = async (v: { email: string; username: string }) => { if (v.username === \"admin\") throw new KrizakaApiError({ status: 400, code: \"USERNAME_RESERVED\" }); if (v.email.endsWith(\"@orochia.com\")) throw new KrizakaApiError({ status: 409, code: \"ACCOUNT_EXISTS\" }); };\n\n// Orochia's sign-up: the username checked as it is typed (a free one offered), an 18+ birth date, the server's refusals in place.\nexport default function FormSignUp() {\n const form = signUp.useForm({ i18n: { t: (key) => WORDS[key] }, codeFields: { UNDERAGE: \"dateOfBirth\", BIRTH_DATE_INVALID: \"dateOfBirth\", USERNAME_RESERVED: \"username\" }, onSubmit: register });\n return (\n <form.Form aria-label=\"Create your account\" className=\"flex w-80 flex-col gap-4\">\n <form.TextField name=\"email\" label=\"E-mail\" type=\"email\" autoComplete=\"email\" hint=\"We send a link to confirm it.\" />\n <form.TextField name=\"username\" label=\"Username\" start=\"@\" autoComplete=\"username\" transform={(v) => v.toLowerCase().replace(/\\s+/g, \"_\")} availableHint=\"Free\" validateAsync={async (u) => ((await taken(u)) ? { key: \"signUp.username.taken\", suggestion: { label: `Use @${u}_${u.length}`, value: `${u}_${u.length}` } } : null)} />\n <form.TextField name=\"password\" label=\"Password\" type=\"password\" autoComplete=\"new-password\" hint=\"At least 8 characters.\" />\n <form.DateField name=\"dateOfBirth\" label=\"Date of birth\" entry=\"parts\" autoComplete=\"bday\" hint=\"Private, never shown.\" />\n <form.Checkbox name=\"acceptTerms\" label=\"I accept the terms and the privacy policy\" />\n <form.FormError />\n <form.Submit>Create my account</form.Submit>\n </form.Form>\n );\n}\n"
1098
+ },
1099
+ {
1100
+ "name": "profile",
1101
+ "title": "Profile",
1102
+ "description": "Saved by a mutation of @krizaka/query: categories, tags, a list of links that grows (`form.FieldArray`).",
1103
+ "path": "examples/form/profile.tsx",
1104
+ "module": "@krizaka/ui/registry/examples/form/profile",
1105
+ "code": "\"use client\";\n\nimport { KrizakaQueryProvider, useMutation } from \"@krizaka/query\";\nimport { Button } from \"@krizaka/ui/button\";\nimport { createKrizakaForm } from \"@krizaka/ui/form\";\nimport { z } from \"zod\";\n\nconst profile = createKrizakaForm({\n schema: z.object({ displayName: z.string().min(2).max(40), bio: z.string().max(160), categories: z.array(z.string()).min(1).max(3), tags: z.array(z.string()).max(10), links: z.array(z.object({ url: z.url() })).max(3) }),\n defaultValues: { displayName: \"Elena Varga\", bio: \"Night skies over the Tatras.\", categories: [\"travel\"], tags: [\"astro\", \"mountains\"], links: [{ url: \"https://elena.example\" }] },\n});\nconst CATEGORIES = [{ value: \"travel\", label: \"Travel\" }, { value: \"music\", label: \"Music\" }, { value: \"science\", label: \"Science\" }, { value: \"sport\", label: \"Sport\" }];\n\nfunction ProfileForm() {\n const mutation = useMutation({ mutationFn: (values: unknown) => new Promise((done) => setTimeout(() => done(values), 400)) });\n const form = profile.useForm({ onSubmit: (values) => mutation.mutateAsync(values) });\n return (\n <form.Form aria-label=\"Your profile\" className=\"flex w-96 flex-col gap-4\">\n <form.TextField name=\"displayName\" label=\"Display name\" autoComplete=\"name\" />\n <form.TextArea name=\"bio\" label=\"Bio\" rows={3} maxLength={160} hint=\"Up to 160 characters.\" />\n <form.Combobox name=\"categories\" label=\"Categories\" multiple options={CATEGORIES} hint=\"Up to three.\" />\n <form.TagInput name=\"tags\" label=\"Tags\" max={10} normalize={(t) => t.toLowerCase().replace(/^#/, \"\")} />\n <form.FieldArray name=\"links\">\n {(links) => (\n <fieldset className=\"flex flex-col gap-2\">\n <legend className=\"text-xs font-semibold text-fg-secondary\">Links</legend>\n {links.items.map((_, i) => <form.TextField key={i} name={`links[${i}].url`} label={`Link ${i + 1}`} type=\"url\" />)}\n {links.items.length < 3 ? <Button type=\"button\" size=\"sm\" variant=\"ghost\" onClick={() => links.push({ url: \"\" })}>Add a link</Button> : null}\n </fieldset>\n )}\n </form.FieldArray>\n <form.Submit>Save the profile</form.Submit>\n </form.Form>\n );\n}\n\n// A profile saved by a mutation of @krizaka/query: categories, tags, a list of links that grows.\nexport default function FormProfile() {\n return <KrizakaQueryProvider><ProfileForm /></KrizakaQueryProvider>;\n}\n"
1106
+ },
1107
+ {
1108
+ "name": "upload",
1109
+ "title": "Upload",
1110
+ "description": "A video and what describes it — the file, a title, a description, tags, who can watch — sent together.",
1111
+ "path": "examples/form/upload.tsx",
1112
+ "module": "@krizaka/ui/registry/examples/form/upload",
1113
+ "code": "\"use client\";\n\nimport { createKrizakaForm } from \"@krizaka/ui/form\";\nimport { z } from \"zod\";\n\nconst upload = createKrizakaForm({\n schema: z.object({\n video: z.array(z.instanceof(Blob)).length(1, \"upload.video.required\"),\n title: z.string().min(3).max(100),\n description: z.string().max(2000),\n tags: z.array(z.string()).max(10),\n visibility: z.enum([\"public\", \"followers\", \"private\"]),\n }),\n defaultValues: { video: [] as Blob[], title: \"\", description: \"\", tags: [] as string[], visibility: \"public\" as const },\n i18n: { t: (key) => (key === \"upload.video.required\" ? \"Choose the video to publish.\" : undefined) },\n});\nconst VISIBILITY = [{ value: \"public\", label: \"Everyone\" }, { value: \"followers\", label: \"My followers\" }, { value: \"private\", label: \"Only me\" }];\n\n// A video and what describes it: the file, a title, a description, tags and who sees it — sent once, together.\nexport default function FormUpload() {\n const form = upload.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 600)) });\n return (\n <form.Form aria-label=\"Publish a video\" className=\"flex w-96 flex-col gap-4\">\n <form.FileUpload name=\"video\" label=\"Video\" title=\"Drop your video here\" browseLabel=\"Choose a file\" hint=\"MP4 or MOV, up to 4 GB.\" accept=\"video/*\" maxSize={4 * 1024 ** 3} removeLabel={(file) => `Remove ${file}`} />\n <form.TextField name=\"title\" label=\"Title\" maxLength={100} />\n <form.TextArea name=\"description\" label=\"Description\" rows={3} />\n <form.TagInput name=\"tags\" label=\"Tags\" max={10} hint=\"Up to 10, separated by commas.\" />\n <form.Select name=\"visibility\" label=\"Who can watch\" options={VISIBILITY} />\n <form.Submit>Publish</form.Submit>\n </form.Form>\n );\n}\n"
1114
+ },
1115
+ {
1116
+ "name": "amount",
1117
+ "title": "Amount",
1118
+ "description": "A tip stored in cents (`Money`), typed the locale's way; presets set the field from outside.",
1119
+ "path": "examples/form/amount.tsx",
1120
+ "module": "@krizaka/ui/registry/examples/form/amount",
1121
+ "code": "\"use client\";\n\nimport { Chip } from \"@krizaka/ui/chip\";\nimport { createKrizakaForm } from \"@krizaka/ui/form\";\nimport { z } from \"zod\";\n\nconst tip = createKrizakaForm({\n schema: z.object({\n amount: z.number({ message: \"tip.amount.invalid\" }).int().min(100, \"tip.amount.min\").max(50_000, \"tip.amount.max\"),\n message: z.string().max(140),\n anonymous: z.boolean(),\n }),\n defaultValues: { amount: 500, message: \"\", anonymous: false },\n i18n: { t: (key) => ({ \"tip.amount.invalid\": \"Enter an amount.\", \"tip.amount.min\": \"At least $1.\", \"tip.amount.max\": \"At most $500.\" })[key] },\n});\nconst PRESETS = [200, 500, 1000];\n\n// A tip in cents: typed the locale's way (`12.50`), never a float; presets set the field from outside.\nexport default function FormAmount() {\n const form = tip.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });\n return (\n <form.Form aria-label=\"Send a tip\" className=\"flex w-80 flex-col gap-4\">\n <form.Money name=\"amount\" label=\"Amount\" currency=\"USD\" hint=\"Between $1 and $500.\" />\n <form.Watch name=\"amount\">\n {(amount) => (\n <div className=\"flex gap-2\" role=\"group\" aria-label=\"Suggested amounts\">\n {PRESETS.map((cents) => <Chip key={cents} selected={amount === cents} onSelectedChange={() => form.setValue(\"amount\", cents)}>{`$${cents / 100}`}</Chip>)}\n </div>\n )}\n </form.Watch>\n <form.TextArea name=\"message\" label=\"A word for the creator\" rows={2} maxLength={140} />\n <form.Checkbox name=\"anonymous\" label=\"Send it anonymously\" />\n <form.Submit>Send the tip</form.Submit>\n </form.Form>\n );\n}\n"
1122
+ },
1123
+ {
1124
+ "name": "audience",
1125
+ "title": "Audience",
1126
+ "description": "Cards to choose who watches, the price only for subscribers (`form.Watch`), a publication date.",
1127
+ "path": "examples/form/audience.tsx",
1128
+ "module": "@krizaka/ui/registry/examples/form/audience",
1129
+ "code": "\"use client\";\n\nimport { createKrizakaForm } from \"@krizaka/ui/form\";\nimport { z } from \"zod\";\n\nconst audience = createKrizakaForm({\n schema: z\n .object({ audience: z.enum([\"everyone\", \"followers\", \"subscribers\"]), price: z.number().int().nullable(), publishOn: z.iso.date().or(z.literal(\"\")), comments: z.boolean() })\n .refine((v) => v.audience !== \"subscribers\" || (v.price ?? 0) >= 100, { path: [\"price\"], message: \"audience.price.min\" }),\n defaultValues: { audience: \"subscribers\" as \"everyone\" | \"followers\" | \"subscribers\", price: 499 as number | null, publishOn: \"\", comments: true },\n i18n: { t: (key) => (key === \"audience.price.min\" ? \"Subscribers pay at least $1.\" : undefined) },\n});\nconst AUDIENCES = [\n { value: \"everyone\", label: \"Everyone\", description: \"Free, in the feed\" },\n { value: \"followers\", label: \"Followers\", description: \"Free, for them\" },\n { value: \"subscribers\", label: \"Subscribers\", description: \"Paid, once\" },\n];\n\n// Who sees a video: cards to choose from, the price only when it is for subscribers, a publication date.\nexport default function FormAudience() {\n const form = audience.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });\n return (\n <form.Form aria-label=\"Audience\" className=\"flex w-[28rem] max-w-full flex-col gap-4\">\n <form.RadioGroup name=\"audience\" label=\"Who can watch\" variant=\"cards\" options={AUDIENCES} />\n <form.Watch name=\"audience\">{(value) => (value === \"subscribers\" ? <form.Money name=\"price\" label=\"Price\" currency=\"USD\" /> : null)}</form.Watch>\n <form.DateField name=\"publishOn\" label=\"Publish on\" hint=\"Empty: at once.\" />\n <form.Switch name=\"comments\" label=\"Comments\" hint=\"Viewers can comment under the video.\" />\n <form.Submit>Save</form.Submit>\n </form.Form>\n );\n}\n"
1130
+ },
1131
+ {
1132
+ "name": "settings",
1133
+ "title": "Settings",
1134
+ "description": "Sent by a Server Action (`useFormWithAction`): switches, a menu, a slider, a choice.",
1135
+ "path": "examples/form/settings.tsx",
1136
+ "module": "@krizaka/ui/registry/examples/form/settings",
1137
+ "code": "\"use client\";\n\nimport { createKrizakaForm, type FormActionResult } from \"@krizaka/ui/form\";\nimport { z } from \"zod\";\n\nconst settings = createKrizakaForm({\n schema: z.object({ language: z.enum([\"en\", \"fr\", \"es\"]), emailDigest: z.boolean(), pushMessages: z.boolean(), autoplayVolume: z.number().min(0).max(100), quality: z.enum([\"auto\", \"data-saver\", \"best\"]) }),\n defaultValues: { language: \"en\" as \"en\" | \"fr\" | \"es\", emailDigest: true, pushMessages: false, autoplayVolume: 40, quality: \"auto\" as \"auto\" | \"data-saver\" | \"best\" },\n});\nconst LANGUAGES = [{ value: \"en\", label: \"English\" }, { value: \"fr\", label: \"Français\" }, { value: \"es\", label: \"Español\" }];\nconst QUALITY = [{ value: \"auto\", label: \"Automatic\" }, { value: \"data-saver\", label: \"Data saver\" }, { value: \"best\", label: \"Best\" }];\n// What a Server Action answers (`\"use server\"` in the app): done, or a refusal the form shows.\nasync function saveSettings(): Promise<FormActionResult> {\n await new Promise((done) => setTimeout(done, 400));\n return { ok: true };\n}\n\n// Settings sent by a Server Action (`useFormWithAction`): switches, a menu, a slider, a choice.\nexport default function FormSettings() {\n const form = settings.useFormWithAction(saveSettings);\n return (\n <form.Form aria-label=\"Settings\" className=\"flex w-96 flex-col gap-5\">\n <form.Select name=\"language\" label=\"Language\" options={LANGUAGES} />\n <form.Switch name=\"emailDigest\" label=\"Weekly e-mail\" hint=\"What happened on your channel.\" />\n <form.Switch name=\"pushMessages\" label=\"Message notifications\" />\n <form.Slider name=\"autoplayVolume\" label=\"Autoplay volume\" step={10} formatValue={(v) => `${v} %`} />\n <form.RadioGroup name=\"quality\" label=\"Video quality\" options={QUALITY} orientation=\"horizontal\" />\n <form.Submit>Save the settings</form.Submit>\n </form.Form>\n );\n}\n"
1138
+ },
1139
+ {
1140
+ "name": "report",
1141
+ "title": "Report",
1142
+ "description": "One reason among a few, details, and blocking the account in the same step.",
1143
+ "path": "examples/form/report.tsx",
1144
+ "module": "@krizaka/ui/registry/examples/form/report",
1145
+ "code": "\"use client\";\n\nimport { createKrizakaForm } from \"@krizaka/ui/form\";\nimport { z } from \"zod\";\n\nconst report = createKrizakaForm({\n schema: z.object({ reason: z.enum([\"spam\", \"harassment\", \"minor\", \"copyright\", \"other\"], { message: \"report.reason.required\" }), details: z.string().max(500), block: z.boolean() }),\n defaultValues: { reason: \"\" as unknown as \"spam\", details: \"\", block: false },\n i18n: { t: (key) => (key === \"report.reason.required\" ? \"Choose what is wrong.\" : undefined) },\n});\nconst REASONS = [\n { value: \"spam\", label: \"Spam or a scam\" },\n { value: \"harassment\", label: \"Harassment\" },\n { value: \"minor\", label: \"It may involve a minor\" },\n { value: \"copyright\", label: \"It uses my work\" },\n { value: \"other\", label: \"Something else\" },\n];\n\n// A report: one reason among a few, details, and blocking the account in the same step.\nexport default function FormReport() {\n const form = report.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });\n return (\n <form.Form aria-label=\"Report this video\" className=\"flex w-80 flex-col gap-4\">\n <form.RadioGroup name=\"reason\" label=\"What is wrong?\" options={REASONS} />\n <form.TextArea name=\"details\" label=\"Details\" hint=\"Optional. What should our team look at?\" rows={3} maxLength={500} />\n <form.Checkbox name=\"block\" label=\"Also block this account\" />\n <form.Submit variant=\"danger\">Send the report</form.Submit>\n </form.Form>\n );\n}\n"
1146
+ }
1147
+ ]
1148
+ },
1149
+ "native": {
1150
+ "entry": "@krizaka/ui/native/form",
1151
+ "import": "import { createKrizakaForm, TextField, DateField, Money, Submit, FormError } from \"@krizaka/ui/native/form\";",
1152
+ "differences": [
1153
+ "Import `createKrizakaForm` from `@krizaka/ui/native/form`: the same definition (schema, defaults, i18n), the same `useForm`, `form.*` fields drawn with the native ones.",
1154
+ "`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.",
1156
+ "No `Combobox`, `TagInput`, `FileUpload` or `Slider` field yet: a screen of its own (a picker) sets the value, or `form.Field` draws it."
1157
+ ],
1158
+ "props": [
1159
+ {
1160
+ "component": "Submit",
1161
+ "description": "Sends the form: busy while it is sent (the product's mark turning, or a spinner); a refused submit announces its first error.",
1162
+ "props": [
1163
+ {
1164
+ "name": "label",
1165
+ "type": "string",
1166
+ "default": null,
1167
+ "description": "The button's words, passed translated (\"Create my account\").",
1168
+ "required": true
1169
+ },
1170
+ {
1171
+ "name": "brand",
1172
+ "type": "BrandId",
1173
+ "default": null,
1174
+ "description": "The product whose animated mark turns in the button while the form is sent (the brand's loader); the system's spinner otherwise.",
1175
+ "required": false
1176
+ },
1177
+ {
1178
+ "name": "style",
1179
+ "type": "StyleProp<ViewStyle>",
1180
+ "default": null,
1181
+ "description": "Styles merged last, on the button.",
1182
+ "required": false
1183
+ },
1184
+ {
1185
+ "name": "variant",
1186
+ "type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
1187
+ "default": "primary",
1188
+ "description": "The look. Default `primary`.",
1189
+ "required": false
1190
+ }
1191
+ ]
1192
+ },
1193
+ {
1194
+ "component": "FormError",
1195
+ "description": "The whole form's error — a refusal that belongs to no field (a conflict, too many tries, no network).",
1196
+ "props": [
1197
+ {
1198
+ "name": "style",
1199
+ "type": "StyleProp<ViewStyle>",
1200
+ "default": null,
1201
+ "description": "Styles merged last.",
1202
+ "required": false
1203
+ },
1204
+ {
1205
+ "name": "title",
1206
+ "type": "string",
1207
+ "default": null,
1208
+ "description": "A headline above the message, passed translated.",
1209
+ "required": false
1210
+ }
1211
+ ]
1212
+ },
1213
+ {
1214
+ "component": "TextField",
1215
+ "description": "A text field of the form: its label, the input, the hint or the error, an optional check on the server.",
1216
+ "props": [
1217
+ {
1218
+ "name": "label",
1219
+ "type": "string",
1220
+ "default": null,
1221
+ "description": "The visible label, passed translated (also the accessible name).",
1222
+ "required": true
1223
+ },
1224
+ {
1225
+ "name": "name",
1226
+ "type": "string",
1227
+ "default": null,
1228
+ "description": "The field's name in the schema (typed by the form).",
1229
+ "required": true
1230
+ },
1231
+ {
1232
+ "name": "asyncDebounceMs",
1233
+ "type": "number",
1234
+ "default": null,
1235
+ "description": "The wait, in ms, before `validateAsync`. Default 350.",
1236
+ "required": false
1237
+ },
1238
+ {
1239
+ "name": "availableHint",
1240
+ "type": "string",
1241
+ "default": null,
1242
+ "description": "The words once `validateAsync` accepted the value (\"@eric is free\"), passed translated.",
1243
+ "required": false
1244
+ },
1245
+ {
1246
+ "name": "checkingLabel",
1247
+ "type": "string",
1248
+ "default": "Checking",
1249
+ "description": "The name of the wait while it checks (\"Checking\"), passed translated.",
1250
+ "required": false
1251
+ },
1252
+ {
1253
+ "name": "disabled",
1254
+ "type": "boolean",
1255
+ "default": null,
1256
+ "description": "Not available.",
1257
+ "required": false
1258
+ },
1259
+ {
1260
+ "name": "hint",
1261
+ "type": "string",
1262
+ "default": null,
1263
+ "description": "A help text under the control; the error takes its place when there is one.",
1264
+ "required": false
1265
+ },
1266
+ {
1267
+ "name": "inputRef",
1268
+ "type": "Ref<TextInput>",
1269
+ "default": null,
1270
+ "description": "The input itself, to focus it (`ref.current?.focus()`) or move to the next field.",
1271
+ "required": false
1272
+ },
1273
+ {
1274
+ "name": "style",
1275
+ "type": "StyleProp<ViewStyle>",
1276
+ "default": null,
1277
+ "description": "Styles merged last, on the field.",
1278
+ "required": false
1279
+ },
1280
+ {
1281
+ "name": "transform",
1282
+ "type": "((raw: string) => string)",
1283
+ "default": null,
1284
+ "description": "Cleans what is typed before it is stored (`(v) => v.toLowerCase()`).",
1285
+ "required": false
1286
+ },
1287
+ {
1288
+ "name": "validateAsync",
1289
+ "type": "FieldCheck<string>",
1290
+ "default": null,
1291
+ "description": "Checks the value on the server as it is typed (availability): an error key, or nothing when it is fine.",
1292
+ "required": false
1293
+ }
1294
+ ]
1295
+ },
1296
+ {
1297
+ "component": "TextArea",
1298
+ "description": "A longer text of the form (a bio, a description, a report).",
1299
+ "props": [
1300
+ {
1301
+ "name": "label",
1302
+ "type": "string",
1303
+ "default": null,
1304
+ "description": "The visible label, passed translated (also the accessible name).",
1305
+ "required": true
1306
+ },
1307
+ {
1308
+ "name": "name",
1309
+ "type": "string",
1310
+ "default": null,
1311
+ "description": "The field's name in the schema (typed by the form).",
1312
+ "required": true
1313
+ },
1314
+ {
1315
+ "name": "disabled",
1316
+ "type": "boolean",
1317
+ "default": null,
1318
+ "description": "Not available.",
1319
+ "required": false
1320
+ },
1321
+ {
1322
+ "name": "hint",
1323
+ "type": "string",
1324
+ "default": null,
1325
+ "description": "A help text under the control; the error takes its place when there is one.",
1326
+ "required": false
1327
+ },
1328
+ {
1329
+ "name": "maxLength",
1330
+ "type": "number",
1331
+ "default": null,
1332
+ "description": "The most characters.",
1333
+ "required": false
1334
+ },
1335
+ {
1336
+ "name": "placeholder",
1337
+ "type": "string",
1338
+ "default": null,
1339
+ "description": "An example, passed translated.",
1340
+ "required": false
1341
+ },
1342
+ {
1343
+ "name": "style",
1344
+ "type": "StyleProp<ViewStyle>",
1345
+ "default": null,
1346
+ "description": "Styles merged last, on the field.",
1347
+ "required": false
1348
+ }
1349
+ ]
1350
+ },
1351
+ {
1352
+ "component": "Checkbox",
1353
+ "description": "A box of the form (`boolean`): terms to accept, an option.",
1354
+ "props": [
1355
+ {
1356
+ "name": "label",
1357
+ "type": "ReactNode",
1358
+ "default": null,
1359
+ "description": "The words next to the box: text, or a `Txt` with links inside (then name it with `accessibleLabel`).",
1360
+ "required": true
1361
+ },
1362
+ {
1363
+ "name": "name",
1364
+ "type": "string",
1365
+ "default": null,
1366
+ "description": "The field's name in the schema (typed by the form).",
1367
+ "required": true
1368
+ },
1369
+ {
1370
+ "name": "accessibleLabel",
1371
+ "type": "string",
1372
+ "default": null,
1373
+ "description": "The accessible name when `label` is not plain text, passed translated.",
1374
+ "required": false
1375
+ },
1376
+ {
1377
+ "name": "disabled",
1378
+ "type": "boolean",
1379
+ "default": null,
1380
+ "description": "Not available.",
1381
+ "required": false
1382
+ },
1383
+ {
1384
+ "name": "style",
1385
+ "type": "StyleProp<ViewStyle>",
1386
+ "default": null,
1387
+ "description": "Styles merged last, on the field.",
1388
+ "required": false
1389
+ }
1390
+ ]
1391
+ },
1392
+ {
1393
+ "component": "Switch",
1394
+ "description": "An on/off setting of the form (`boolean`): its label and hint beside the system's switch.",
1395
+ "props": [
1396
+ {
1397
+ "name": "label",
1398
+ "type": "string",
1399
+ "default": null,
1400
+ "description": "The visible label, passed translated (also the accessible name).",
1401
+ "required": true
1402
+ },
1403
+ {
1404
+ "name": "name",
1405
+ "type": "string",
1406
+ "default": null,
1407
+ "description": "The field's name in the schema (typed by the form).",
1408
+ "required": true
1409
+ },
1410
+ {
1411
+ "name": "disabled",
1412
+ "type": "boolean",
1413
+ "default": null,
1414
+ "description": "Not available.",
1415
+ "required": false
1416
+ },
1417
+ {
1418
+ "name": "hint",
1419
+ "type": "string",
1420
+ "default": null,
1421
+ "description": "A help text under the control; the error takes its place when there is one.",
1422
+ "required": false
1423
+ },
1424
+ {
1425
+ "name": "style",
1426
+ "type": "StyleProp<ViewStyle>",
1427
+ "default": null,
1428
+ "description": "Styles merged last, on the field.",
1429
+ "required": false
1430
+ }
1431
+ ]
1432
+ },
1433
+ {
1434
+ "component": "RadioGroup",
1435
+ "description": "One choice among a few, as chips (a radio group). On native, `Select` is the same control.",
1436
+ "props": [
1437
+ {
1438
+ "name": "label",
1439
+ "type": "string",
1440
+ "default": null,
1441
+ "description": "The visible label, passed translated (also the accessible name).",
1442
+ "required": true
1443
+ },
1444
+ {
1445
+ "name": "name",
1446
+ "type": "string",
1447
+ "default": null,
1448
+ "description": "The field's name in the schema (typed by the form).",
1449
+ "required": true
1450
+ },
1451
+ {
1452
+ "name": "options",
1453
+ "type": "readonly FormOption[]",
1454
+ "default": null,
1455
+ "description": "The choices, as chips (one chosen).",
1456
+ "required": true
1457
+ },
1458
+ {
1459
+ "name": "disabled",
1460
+ "type": "boolean",
1461
+ "default": null,
1462
+ "description": "Not available.",
1463
+ "required": false
1464
+ },
1465
+ {
1466
+ "name": "hint",
1467
+ "type": "string",
1468
+ "default": null,
1469
+ "description": "A help text under the control; the error takes its place when there is one.",
1470
+ "required": false
1471
+ },
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
+ {
1480
+ "name": "style",
1481
+ "type": "StyleProp<ViewStyle>",
1482
+ "default": null,
1483
+ "description": "Styles merged last, on the field.",
1484
+ "required": false
1485
+ }
1486
+ ]
1487
+ },
1488
+ {
1489
+ "component": "Select",
1490
+ "description": "One choice among several (the web's `<select>`): chips on native, scrolled sideways when they are many.",
1491
+ "props": [
1492
+ {
1493
+ "name": "label",
1494
+ "type": "string",
1495
+ "default": null,
1496
+ "description": "The visible label, passed translated (also the accessible name).",
1497
+ "required": true
1498
+ },
1499
+ {
1500
+ "name": "name",
1501
+ "type": "string",
1502
+ "default": null,
1503
+ "description": "The field's name in the schema (typed by the form).",
1504
+ "required": true
1505
+ },
1506
+ {
1507
+ "name": "options",
1508
+ "type": "readonly FormOption[]",
1509
+ "default": null,
1510
+ "description": "The choices, as chips (one chosen).",
1511
+ "required": true
1512
+ },
1513
+ {
1514
+ "name": "disabled",
1515
+ "type": "boolean",
1516
+ "default": null,
1517
+ "description": "Not available.",
1518
+ "required": false
1519
+ },
1520
+ {
1521
+ "name": "hint",
1522
+ "type": "string",
1523
+ "default": null,
1524
+ "description": "A help text under the control; the error takes its place when there is one.",
1525
+ "required": false
1526
+ },
1527
+ {
1528
+ "name": "scrollable",
1529
+ "type": "boolean",
1530
+ "default": null,
1531
+ "description": "Scrolls the chips sideways instead of wrapping them (many choices).",
1532
+ "required": false
1533
+ },
1534
+ {
1535
+ "name": "style",
1536
+ "type": "StyleProp<ViewStyle>",
1537
+ "default": null,
1538
+ "description": "Styles merged last, on the field.",
1539
+ "required": false
1540
+ }
1541
+ ]
1542
+ },
1543
+ {
1544
+ "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.",
1546
+ "props": [
1547
+ {
1548
+ "name": "label",
1549
+ "type": "string",
1550
+ "default": null,
1551
+ "description": "The visible label, passed translated (also the accessible name).",
1552
+ "required": true
1553
+ },
1554
+ {
1555
+ "name": "name",
1556
+ "type": "string",
1557
+ "default": null,
1558
+ "description": "The field's name in the schema (typed by the form).",
1559
+ "required": true
1560
+ },
1561
+ {
1562
+ "name": "autoComplete",
1563
+ "type": "\"off\" | \"bday\"",
1564
+ "default": null,
1565
+ "description": "`bday` for a date of birth.",
1566
+ "required": false
1567
+ },
1568
+ {
1569
+ "name": "disabled",
1570
+ "type": "boolean",
1571
+ "default": null,
1572
+ "description": "Not available.",
1573
+ "required": false
1574
+ },
1575
+ {
1576
+ "name": "hint",
1577
+ "type": "string",
1578
+ "default": null,
1579
+ "description": "A help text under the control; the error takes its place when there is one.",
1580
+ "required": false
1581
+ },
1582
+ {
1583
+ "name": "labels",
1584
+ "type": "Partial<DateFieldLabels>",
1585
+ "default": null,
1586
+ "description": "The names of the boxes (`day`, `month`, `year`), passed translated.",
1587
+ "required": false
1588
+ },
1589
+ {
1590
+ "name": "style",
1591
+ "type": "StyleProp<ViewStyle>",
1592
+ "default": null,
1593
+ "description": "Styles merged last, on the field.",
1594
+ "required": false
1595
+ }
1596
+ ]
1597
+ },
1598
+ {
1599
+ "component": "Money",
1600
+ "description": "An amount of the form, stored as integer minor units (cents), typed on the decimal keypad the locale's way.",
1601
+ "props": [
1602
+ {
1603
+ "name": "currency",
1604
+ "type": "string",
1605
+ "default": null,
1606
+ "description": "The currency (ISO 4217): its sign beside the amount, its decimals.",
1607
+ "required": true
1608
+ },
1609
+ {
1610
+ "name": "label",
1611
+ "type": "string",
1612
+ "default": null,
1613
+ "description": "The visible label, passed translated (also the accessible name).",
1614
+ "required": true
1615
+ },
1616
+ {
1617
+ "name": "name",
1618
+ "type": "string",
1619
+ "default": null,
1620
+ "description": "The field's name in the schema (typed by the form).",
1621
+ "required": true
1622
+ },
1623
+ {
1624
+ "name": "disabled",
1625
+ "type": "boolean",
1626
+ "default": null,
1627
+ "description": "Not available.",
1628
+ "required": false
1629
+ },
1630
+ {
1631
+ "name": "hint",
1632
+ "type": "string",
1633
+ "default": null,
1634
+ "description": "A help text under the control; the error takes its place when there is one.",
1635
+ "required": false
1636
+ },
1637
+ {
1638
+ "name": "placeholder",
1639
+ "type": "string",
1640
+ "default": null,
1641
+ "description": "An example of an amount, passed translated.",
1642
+ "required": false
1643
+ },
1644
+ {
1645
+ "name": "style",
1646
+ "type": "StyleProp<ViewStyle>",
1647
+ "default": null,
1648
+ "description": "Styles merged last, on the field.",
1649
+ "required": false
1650
+ }
1651
+ ]
1652
+ },
1653
+ {
1654
+ "component": "FormField",
1655
+ "description": "A field drawn by the app (a control the package does not have).",
1656
+ "props": [
1657
+ {
1658
+ "name": "children",
1659
+ "type": "(field: KrizakaField<unknown>) => ReactNode",
1660
+ "default": null,
1661
+ "description": "Draws the field: its value, setter, error (`useKrizakaField`).",
1662
+ "required": true
1663
+ },
1664
+ {
1665
+ "name": "name",
1666
+ "type": "string",
1667
+ "default": null,
1668
+ "description": "The field's name in the schema (typed by the form).",
1669
+ "required": true
1670
+ },
1671
+ {
1672
+ "name": "asyncDebounceMs",
1673
+ "type": "number",
1674
+ "default": null,
1675
+ "description": "The wait, in ms, before `validateAsync`.",
1676
+ "required": false
1677
+ },
1678
+ {
1679
+ "name": "validateAsync",
1680
+ "type": "FieldCheck<unknown>",
1681
+ "default": null,
1682
+ "description": "Checks the value on the server as it changes.",
1683
+ "required": false
1684
+ }
1685
+ ]
1686
+ },
1687
+ {
1688
+ "component": "FieldArray",
1689
+ "description": "A list of fields that grows and shrinks: each item's fields named `links[0].url`.",
1690
+ "props": [
1691
+ {
1692
+ "name": "children",
1693
+ "type": "(array: KrizakaFieldArray<unknown>) => ReactNode",
1694
+ "default": null,
1695
+ "description": "Draws the items: `items`, `push`, `remove`, `move`, `fieldName(i, \"url\")`.",
1696
+ "required": true
1697
+ },
1698
+ {
1699
+ "name": "name",
1700
+ "type": "string",
1701
+ "default": null,
1702
+ "description": "The array's name in the schema (typed by the form).",
1703
+ "required": true
1704
+ }
1705
+ ]
1706
+ },
1707
+ {
1708
+ "component": "Watch",
1709
+ "description": "Renders from a field's current value (a part shown only when a choice is made).",
1710
+ "props": [
1711
+ {
1712
+ "name": "children",
1713
+ "type": "(value: unknown) => ReactNode",
1714
+ "default": null,
1715
+ "description": "Draws from its current value.",
1716
+ "required": true
1717
+ },
1718
+ {
1719
+ "name": "name",
1720
+ "type": "string",
1721
+ "default": null,
1722
+ "description": "The field to follow (typed by the form).",
1723
+ "required": true
1724
+ }
1725
+ ]
1726
+ },
1727
+ {
1728
+ "component": "FieldCheck",
1729
+ "description": "A value checked on the server as it is typed: an error (or its key), or nothing when the value is fine.",
1730
+ "props": []
1731
+ },
1732
+ {
1733
+ "component": "KrizakaField",
1734
+ "description": "A field as its component draws it: the value, its setter, its error in words, its state.",
1735
+ "props": []
1736
+ },
1737
+ {
1738
+ "component": "KrizakaFieldArray",
1739
+ "description": "The array of a field array, with its operations (`form.FieldArray`).",
1740
+ "props": []
1741
+ },
1742
+ {
1743
+ "component": "FormActionResult",
1744
+ "description": "What a Server Action (or any submit) answers: done, or a refusal the form shows.",
1745
+ "props": []
1746
+ },
1747
+ {
1748
+ "component": "FormOutput",
1749
+ "description": "The values a form sends: the schema's output (zod transforms applied).",
1750
+ "props": []
1751
+ },
1752
+ {
1753
+ "component": "FormSchema",
1754
+ "description": "A schema the form validates with: any Standard Schema — zod 4 (`z.object`), valibot, arktype.",
1755
+ "props": []
1756
+ },
1757
+ {
1758
+ "component": "FormValues",
1759
+ "description": "The values a form holds: the schema's input.",
1760
+ "props": []
1761
+ },
1762
+ {
1763
+ "component": "KrizakaFormFactory",
1764
+ "description": "A form defined once (schema + defaults), used in components: `signUp.useForm({ onSubmit })`.",
1765
+ "props": []
1766
+ },
1767
+ {
1768
+ "component": "KrizakaFormInstance",
1769
+ "description": "A form in a component: its fields (typed by the schema), its submit and its state.",
1770
+ "props": []
1771
+ },
1772
+ {
1773
+ "component": "FormFieldError",
1774
+ "description": "An error of a field or of the whole form: the key of its words in the app's catalogue, and what fills them in.",
1775
+ "props": []
1776
+ },
1777
+ {
1778
+ "component": "FormI18n",
1779
+ "description": "How a form finds its words: the app's translate function, and the prefixes of its keys.",
1780
+ "props": []
1781
+ },
1782
+ {
1783
+ "component": "FormProblem",
1784
+ "description": "A refusal the form shows, serialisable (a Server Action returns it): the shape of `KrizakaApiError` of",
1785
+ "props": []
1786
+ }
1787
+ ],
1788
+ "examples": [
1789
+ {
1790
+ "name": "sign-up",
1791
+ "title": "Sign-up",
1792
+ "description": "E-mail, a username checked as it is typed, the 18+ birth date as day · month · year, the terms.",
1793
+ "path": "examples/form/native/sign-up.tsx",
1794
+ "module": "@krizaka/ui/registry/examples/form/native/sign-up",
1795
+ "code": "import { KrizakaApiError } from \"@krizaka/query\";\nimport { createKrizakaForm, latestBirthDate } from \"@krizaka/ui/native/form\";\nimport { z } from \"zod\";\n\n// The same schema as the web form (a product keeps it in packages/core): the same rules, the same keys.\nconst signUp = createKrizakaForm({\n schema: z.object({\n email: z.email(),\n username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/, \"signUp.username.format\"),\n password: z.string().min(8),\n dateOfBirth: z.iso.date().refine((d) => d <= latestBirthDate(18), \"form.errors.UNDERAGE\"),\n acceptTerms: z.boolean().refine(Boolean, \"signUp.terms.required\"),\n }),\n defaultValues: { email: \"\", username: \"\", password: \"\", dateOfBirth: \"\", acceptTerms: false },\n});\nconst WORDS: Record<string, string> = { \"signUp.username.format\": \"Lowercase letters, digits and _ only.\", \"signUp.username.taken\": \"This username is taken.\", \"signUp.terms.required\": \"Accept the terms to continue.\", \"form.errors.UNDERAGE\": \"You must be 18 or older.\", \"form.errors.BIRTH_DATE_INVALID\": \"Enter your real date of birth.\", \"form.errors.USERNAME_RESERVED\": \"This username is reserved.\", \"apiErrors.ACCOUNT_EXISTS\": \"This e-mail already has an account. Sign in instead.\" };\nconst taken = (u: string) => new Promise<boolean>((done) => setTimeout(() => done([\"eric\", \"elena\"].includes(u)), 300));\nconst register = async (v: { email: string; username: string }) => { if (v.username === \"admin\") throw new KrizakaApiError({ status: 400, code: \"USERNAME_RESERVED\" }); if (v.email.endsWith(\"@orochia.com\")) throw new KrizakaApiError({ status: 409, code: \"ACCOUNT_EXISTS\" }); };\n\n// Sign-up in the app: the return key walks the fields, the username is checked as typed, the 18+ birth date is day · month · year.\nexport default function NativeFormSignUp() {\n const form = signUp.useForm({ i18n: { t: (key) => WORDS[key], locale: \"en-GB\" }, codeFields: { UNDERAGE: \"dateOfBirth\", BIRTH_DATE_INVALID: \"dateOfBirth\", USERNAME_RESERVED: \"username\" }, onSubmit: register });\n return (\n <form.Form aria-label=\"Create your account\" style={{ width: 312 }}>\n <form.TextField name=\"email\" label=\"E-mail\" keyboardType=\"email-address\" autoComplete=\"email\" autoCapitalize=\"none\" textContentType=\"emailAddress\" />\n <form.TextField name=\"username\" label=\"Username\" autoCapitalize=\"none\" autoComplete=\"username\" transform={(v) => v.toLowerCase().replace(/\\s+/g, \"_\")} availableHint=\"Free\" validateAsync={async (u) => ((await taken(u)) ? { key: \"signUp.username.taken\", suggestion: { label: `Use @${u}_${u.length}`, value: `${u}_${u.length}` } } : null)} />\n <form.TextField name=\"password\" label=\"Password\" secureTextEntry autoComplete=\"new-password\" textContentType=\"newPassword\" hint=\"At least 8 characters.\" />\n <form.DateField name=\"dateOfBirth\" label=\"Date of birth\" autoComplete=\"bday\" hint=\"Private, never shown.\" />\n <form.Checkbox name=\"acceptTerms\" label=\"I accept the terms and the privacy policy\" />\n <form.FormError />\n <form.Submit label=\"Create my account\" brand=\"orochia\" />\n </form.Form>\n );\n}\n",
1796
+ "screenshots": {
1797
+ "dark": "examples/form/native/sign-up.dark.png",
1798
+ "light": "examples/form/native/sign-up.light.png"
1799
+ }
1800
+ },
1801
+ {
1802
+ "name": "profile",
1803
+ "title": "Profile",
1804
+ "description": "Saved by a mutation of @krizaka/query; a category as chips, the list of links grows.",
1805
+ "path": "examples/form/native/profile.tsx",
1806
+ "module": "@krizaka/ui/registry/examples/form/native/profile",
1807
+ "code": "import { KrizakaQueryProvider, useMutation } from \"@krizaka/query\";\nimport { Button } from \"@krizaka/ui/native\";\nimport { createKrizakaForm } from \"@krizaka/ui/native/form\";\nimport { z } from \"zod\";\n\nconst profile = createKrizakaForm({\n schema: z.object({ displayName: z.string().min(2).max(40), bio: z.string().max(160), category: z.enum([\"travel\", \"music\", \"science\", \"sport\"]), links: z.array(z.object({ url: z.url() })).max(3) }),\n defaultValues: { displayName: \"Elena Varga\", bio: \"Night skies over the Tatras.\", category: \"travel\" as \"travel\" | \"music\" | \"science\" | \"sport\", links: [{ url: \"https://elena.example\" }] },\n});\nconst CATEGORIES = [{ value: \"travel\", label: \"Travel\" }, { value: \"music\", label: \"Music\" }, { value: \"science\", label: \"Science\" }, { value: \"sport\", label: \"Sport\" }];\n\nfunction ProfileForm() {\n const mutation = useMutation({ mutationFn: (values: unknown) => new Promise((done) => setTimeout(() => done(values), 400)) });\n const form = profile.useForm({ onSubmit: (values) => mutation.mutateAsync(values) });\n return (\n <form.Form aria-label=\"Your profile\" style={{ width: 312 }}>\n <form.TextField name=\"displayName\" label=\"Display name\" autoComplete=\"name\" />\n <form.TextArea name=\"bio\" label=\"Bio\" maxLength={160} />\n <form.RadioGroup name=\"category\" label=\"Category\" options={CATEGORIES} />\n <form.FieldArray name=\"links\">\n {(links) => (\n <>\n {links.items.map((_, i) => <form.TextField key={i} name={`links[${i}].url`} label={`Link ${i + 1}`} keyboardType=\"url\" autoCapitalize=\"none\" />)}\n {links.items.length < 3 ? <Button label=\"Add a link\" variant=\"ghost\" size=\"sm\" onPress={() => links.push({ url: \"\" })} /> : null}\n </>\n )}\n </form.FieldArray>\n <form.Submit label=\"Save the profile\" />\n </form.Form>\n );\n}\n\n// A profile saved by a mutation of @krizaka/query; the list of links grows.\nexport default function NativeFormProfile() {\n return <KrizakaQueryProvider><ProfileForm /></KrizakaQueryProvider>;\n}\n",
1808
+ "screenshots": {
1809
+ "dark": "examples/form/native/profile.dark.png",
1810
+ "light": "examples/form/native/profile.light.png"
1811
+ }
1812
+ },
1813
+ {
1814
+ "name": "upload",
1815
+ "title": "Upload details",
1816
+ "description": "Title, description and audience of a video picked from the library.",
1817
+ "path": "examples/form/native/upload.tsx",
1818
+ "module": "@krizaka/ui/registry/examples/form/native/upload",
1819
+ "code": "import { createKrizakaForm } from \"@krizaka/ui/native/form\";\nimport { z } from \"zod\";\n\n// What describes a video picked from the library (the picker is the app's: expo-image-picker): title, description, audience.\nconst upload = createKrizakaForm({\n schema: z.object({ title: z.string().min(3).max(100), description: z.string().max(2000), visibility: z.enum([\"public\", \"followers\", \"private\"]) }),\n defaultValues: { title: \"Sunrise over Rysy\", description: \"\", visibility: \"public\" as \"public\" | \"followers\" | \"private\" },\n});\nconst VISIBILITY = [{ value: \"public\", label: \"Everyone\" }, { value: \"followers\", label: \"Followers\" }, { value: \"private\", label: \"Only me\" }];\n\n// The details of an upload, then the button that publishes them with the file.\nexport default function NativeFormUpload() {\n const form = upload.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 600)) });\n return (\n <form.Form aria-label=\"Publish a video\" style={{ width: 312 }}>\n <form.TextField name=\"title\" label=\"Title\" maxLength={100} />\n <form.TextArea name=\"description\" label=\"Description\" placeholder=\"What happens in it?\" />\n <form.RadioGroup name=\"visibility\" label=\"Who can watch\" options={VISIBILITY} />\n <form.Submit label=\"Publish\" />\n </form.Form>\n );\n}\n",
1820
+ "screenshots": {
1821
+ "dark": "examples/form/native/upload.dark.png",
1822
+ "light": "examples/form/native/upload.light.png"
1823
+ }
1824
+ },
1825
+ {
1826
+ "name": "amount",
1827
+ "title": "Amount",
1828
+ "description": "A tip in cents on the decimal keypad, typed the French way; presets set the field.",
1829
+ "path": "examples/form/native/amount.tsx",
1830
+ "module": "@krizaka/ui/registry/examples/form/native/amount",
1831
+ "code": "import { Chip } from \"@krizaka/ui/native\";\nimport { createKrizakaForm } from \"@krizaka/ui/native/form\";\nimport { z } from \"zod\";\n\nconst tip = createKrizakaForm({\n schema: z.object({ amount: z.number({ message: \"tip.amount.invalid\" }).int().min(100, \"tip.amount.min\").max(50_000, \"tip.amount.max\"), message: z.string().max(140), anonymous: z.boolean() }),\n defaultValues: { amount: 500, message: \"\", anonymous: false },\n i18n: { t: (key) => ({ \"tip.amount.invalid\": \"Enter an amount.\", \"tip.amount.min\": \"At least €1.\", \"tip.amount.max\": \"At most €500.\" })[key], locale: \"fr-FR\" },\n});\nconst PRESETS = [200, 500, 1000];\n\n// A tip in cents on the decimal keypad, typed the locale's way (`12,50` in French); presets set the field from outside.\nexport default function NativeFormAmount() {\n const form = tip.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });\n return (\n <form.Form aria-label=\"Send a tip\" style={{ width: 312 }}>\n <form.Money name=\"amount\" label=\"Amount\" currency=\"EUR\" hint=\"Between €1 and €500.\" />\n <form.Watch name=\"amount\">\n {(amount) => (\n <Chip.Group type=\"single\" aria-label=\"Suggested amounts\" value={String(amount)} onValueChange={(v) => form.setValue(\"amount\", Number(v))}>\n {PRESETS.map((cents) => <Chip key={cents} value={String(cents)}>{`${cents / 100} €`}</Chip>)}\n </Chip.Group>\n )}\n </form.Watch>\n <form.TextArea name=\"message\" label=\"A word for the creator\" maxLength={140} />\n <form.Checkbox name=\"anonymous\" label=\"Send it anonymously\" />\n <form.Submit label=\"Send the tip\" />\n </form.Form>\n );\n}\n",
1832
+ "screenshots": {
1833
+ "dark": "examples/form/native/amount.dark.png",
1834
+ "light": "examples/form/native/amount.light.png"
1835
+ }
1836
+ },
1837
+ {
1838
+ "name": "audience",
1839
+ "title": "Audience",
1840
+ "description": "The price only when the video is for subscribers; comments on or off.",
1841
+ "path": "examples/form/native/audience.tsx",
1842
+ "module": "@krizaka/ui/registry/examples/form/native/audience",
1843
+ "code": "import { createKrizakaForm } from \"@krizaka/ui/native/form\";\nimport { z } from \"zod\";\n\nconst audience = createKrizakaForm({\n schema: z\n .object({ audience: z.enum([\"everyone\", \"followers\", \"subscribers\"]), price: z.number().int().nullable(), comments: z.boolean() })\n .refine((v) => v.audience !== \"subscribers\" || (v.price ?? 0) >= 100, { path: [\"price\"], message: \"audience.price.min\" }),\n defaultValues: { audience: \"subscribers\" as \"everyone\" | \"followers\" | \"subscribers\", price: 499 as number | null, comments: true },\n i18n: { t: (key) => (key === \"audience.price.min\" ? \"Subscribers pay at least $1.\" : undefined) },\n});\nconst AUDIENCES = [{ value: \"everyone\", label: \"Everyone\" }, { value: \"followers\", label: \"Followers\" }, { value: \"subscribers\", label: \"Subscribers\" }];\n\n// Who sees a video: the price only when it is for subscribers (`Watch`), comments on or off.\nexport default function NativeFormAudience() {\n const form = audience.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });\n return (\n <form.Form aria-label=\"Audience\" style={{ width: 312 }}>\n <form.RadioGroup name=\"audience\" label=\"Who can watch\" options={AUDIENCES} />\n <form.Watch name=\"audience\">{(value) => (value === \"subscribers\" ? <form.Money name=\"price\" label=\"Price\" currency=\"USD\" /> : null)}</form.Watch>\n <form.Switch name=\"comments\" label=\"Comments\" hint=\"Viewers can comment under the video.\" />\n <form.Submit label=\"Save\" />\n </form.Form>\n );\n}\n",
1844
+ "screenshots": {
1845
+ "dark": "examples/form/native/audience.dark.png",
1846
+ "light": "examples/form/native/audience.light.png"
1847
+ }
1848
+ },
1849
+ {
1850
+ "name": "settings",
1851
+ "title": "Settings",
1852
+ "description": "The system's switches and chips for a choice, saved in one go.",
1853
+ "path": "examples/form/native/settings.tsx",
1854
+ "module": "@krizaka/ui/registry/examples/form/native/settings",
1855
+ "code": "import { createKrizakaForm } from \"@krizaka/ui/native/form\";\nimport { z } from \"zod\";\n\nconst settings = createKrizakaForm({\n schema: z.object({ language: z.enum([\"en\", \"fr\", \"es\"]), emailDigest: z.boolean(), pushMessages: z.boolean(), quality: z.enum([\"auto\", \"data-saver\", \"best\"]) }),\n defaultValues: { language: \"en\" as \"en\" | \"fr\" | \"es\", emailDigest: true, pushMessages: false, quality: \"auto\" as \"auto\" | \"data-saver\" | \"best\" },\n});\nconst LANGUAGES = [{ value: \"en\", label: \"English\" }, { value: \"fr\", label: \"Français\" }, { value: \"es\", label: \"Español\" }];\nconst QUALITY = [{ value: \"auto\", label: \"Automatic\" }, { value: \"data-saver\", label: \"Data saver\" }, { value: \"best\", label: \"Best\" }];\n\n// The app's settings: the system's switches, chips for a choice — saved in one go.\nexport default function NativeFormSettings() {\n const form = settings.useForm({ onSubmit: (values) => new Promise((done) => setTimeout(() => done(values), 400)) });\n return (\n <form.Form aria-label=\"Settings\" style={{ width: 312 }}>\n <form.Select name=\"language\" label=\"Language\" options={LANGUAGES} />\n <form.Switch name=\"emailDigest\" label=\"Weekly e-mail\" hint=\"What happened on your channel.\" />\n <form.Switch name=\"pushMessages\" label=\"Message notifications\" />\n <form.RadioGroup name=\"quality\" label=\"Video quality\" options={QUALITY} />\n <form.Submit label=\"Save the settings\" />\n </form.Form>\n );\n}\n",
1856
+ "screenshots": {
1857
+ "dark": "examples/form/native/settings.dark.png",
1858
+ "light": "examples/form/native/settings.light.png"
1859
+ }
1860
+ }
1861
+ ]
1862
+ },
1863
+ "files": [
1864
+ {
1865
+ "path": "form/core.tsx",
1866
+ "content": "// Platform-neutral React (web and React Native): the engine of a Krizaka form, TanStack Form v1 behind the facade\n// (ADR-001: a product never imports @tanstack/*). A form = a Standard Schema (zod 4) + its default values; the schema\n// validates every change, an error shows once the person leaves the field or tries to send (and goes as soon as it is\n// fixed), a field may check a value on the server (debounced, aborted when the value changes), and a refusal of the\n// server (Problem Details `fields`) lands under its fields. The fields themselves are the platform's (web.tsx, native).\nimport { type AnyFieldApi, type AnyFormApi, type DeepKeys, type StandardSchemaV1, useField, useForm, useStore } from \"@tanstack/react-form\";\nimport * as React from \"react\";\n\nimport { type FormFieldError, type FormI18n, formMessage, type FormProblem, isFormProblem, issuesToFields, problemToErrors, type SchemaIssue } from \"./errors\";\n\n/** A schema the form validates with: any Standard Schema — zod 4 (`z.object`), valibot, arktype. */\nexport type FormSchema<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output>;\n/** The values a form holds: the schema's input. */\nexport type FormValues<S> = S extends { readonly \"~standard\": { readonly types?: { readonly input: infer I } } } ? I : never;\n/** The values a form sends: the schema's output (zod transforms applied). */\nexport type FormOutput<S> = S extends { readonly \"~standard\": { readonly types?: { readonly output: infer O } } } ? O : never;\n/** A field's name: `email`, `address.city`, `links[0].url`. */\nexport type FieldName<V> = DeepKeys<V> & string;\n\n/** What a Server Action (or any submit) answers: done, or a refusal the form shows. */\nexport type FormActionResult<T = unknown> = { ok: true; data?: T } | { ok: false; problem: FormProblem };\n\n/** A value checked on the server as it is typed: an error (or its key), or nothing when the value is fine. */\nexport type FieldCheck<T = unknown> = (value: T, signal: AbortSignal) => Promise<FormFieldError | string | null | undefined> | FormFieldError | string | null | undefined;\n\nexport interface KrizakaFormDefinition<S extends FormSchema> {\n /** The schema of the values (zod 4): the same one the server parses with. */\n schema: S;\n /** The values at first: every field of the schema. */\n defaultValues: FormValues<S>;\n /** The words: the app's translate function, the key prefixes, the locale. */\n i18n?: FormI18n;\n}\n\nexport interface UseKrizakaFormOptions<S extends FormSchema, R = unknown> {\n /** Sends the values (the schema's output): a mutation of @krizaka/query, a Server Action. Throw a `KrizakaApiError` or\n * return `{ ok: false, problem }` to refuse: its `fields` go under their fields, the rest above the button. */\n onSubmit?: (values: FormOutput<S>) => R | Promise<R>;\n /** Called after a successful submit with what `onSubmit` returned (`data` of a `{ ok: true, data }`). */\n onSuccess?: (result: R extends FormActionResult<infer T> ? T | undefined : R) => void;\n /** Values that replace the definition's (a profile loaded from the server). */\n defaultValues?: FormValues<S>;\n /** Overrides the definition's words (the translate function of a hook). */\n i18n?: FormI18n;\n /** The server's codes that belong to a field (`{ UNDERAGE: \"dateOfBirth\", USERNAME_RESERVED: \"username\" }`): a refusal\n * with that code shows under the field, as `form.errors.<CODE>`, instead of above the button. */\n codeFields?: Readonly<Record<string, string>>;\n}\n\n/** The state of a form, for its fields and its button. */\nexport interface FormContextValue {\n /** TanStack's form, through the facade: never import @tanstack/* in a product. */\n readonly api: AnyFormApi;\n readonly i18n: FormI18n;\n /** The whole form's error (a refusal without a field), or null. */\n readonly error: FormFieldError | null;\n /** Sends the form: validates, focuses (web) or announces (native) the first error, else calls `onSubmit`. */\n readonly submit: () => Promise<boolean>;\n}\n\nexport const FormContext = React.createContext<FormContextValue | null>(null);\n\n/** The form around a field: inside `form.Form`. */\nexport function useFormContext(): FormContextValue {\n const ctx = React.useContext(FormContext);\n if (!ctx) throw new Error(\"A Krizaka form field must be inside its form's `form.Form`.\");\n return ctx;\n}\n\ntype StandardResult = { readonly value?: unknown; readonly issues?: readonly SchemaIssue[] };\n\nconst run = (schema: FormSchema, value: unknown) => schema[\"~standard\"].validate(value) as StandardResult | Promise<StandardResult>;\n\n/** The engine of a form: TanStack's form wired to the schema, the server's refusals and the submit. */\nexport function useKrizakaFormCore<S extends FormSchema, R>(definition: KrizakaFormDefinition<S>, options: UseKrizakaFormOptions<S, R> = {}): FormContextValue {\n const i18n = { ...definition.i18n, ...options.i18n };\n const latest = React.useRef({ options, i18n });\n // The latest options for the engine's callbacks (a submit, a change), which outlive the render that made them.\n React.useLayoutEffect(() => {\n latest.current = { options, i18n };\n });\n const [error, setError] = React.useState<FormFieldError | null>(null);\n const refused = React.useRef(false);\n\n const api = useForm({\n defaultValues: options.defaultValues ?? definition.defaultValues,\n validators: {\n // Every change: the schema, synchronously (an async refinement waits for the submit). Errors are computed for\n // every field; each field shows its own once the person left it or tried to send.\n onChange: ({ value }: { value: unknown }) => {\n const result = run(definition.schema, value);\n if (result instanceof Promise || !result.issues?.length) return undefined;\n const fields = issuesToFields(result.issues, latest.current.i18n.prefix);\n delete fields[\"\"];\n return { fields };\n },\n },\n onSubmit: async ({ value }: { value: unknown }) => {\n const parsed = await run(definition.schema, value);\n if (parsed.issues?.length) {\n apply({ fields: issuesToFields(parsed.issues, latest.current.i18n.prefix) as FormProblem[\"fields\"] });\n return;\n }\n let result: unknown;\n try {\n result = await latest.current.options.onSubmit?.(parsed.value as FormOutput<S>);\n } catch (thrown) {\n apply(isFormProblem(thrown) ? thrown : { code: \"server\" });\n return;\n }\n if (result && typeof result === \"object\" && \"ok\" in result && (result as FormActionResult).ok === false) {\n apply((result as unknown as { problem: FormProblem }).problem);\n return;\n }\n const data = result && typeof result === \"object\" && \"ok\" in result ? (result as { data?: unknown }).data : result;\n latest.current.options.onSuccess?.(data as never);\n },\n });\n\n const known = (name: string) => name in api.fieldInfo;\n function apply(problem: FormProblem) {\n refused.current = true;\n const { fields, form } = problemToErrors(problem, latest.current.i18n, known, latest.current.options.codeFields);\n for (const [name, fieldError] of Object.entries(fields)) {\n api.setFieldMeta(name as never, (meta) => ({ ...meta, isBlurred: true, errorMap: { ...meta.errorMap, onServer: fieldError } }));\n }\n setError(form);\n }\n\n const submit = React.useCallback(async () => {\n setError(null);\n refused.current = false;\n for (const name of Object.keys(api.fieldInfo)) {\n if ((api.getFieldMeta(name as never) as { errorMap?: Record<string, unknown> } | undefined)?.errorMap?.onServer) api.setFieldMeta(name as never, (meta) => ({ ...meta, errorMap: { ...meta.errorMap, onServer: undefined } }));\n }\n await api.handleSubmit();\n const state = api.state;\n return !refused.current && state.isSubmitSuccessful && state.isValid && !(Object.values(state.fieldMeta) as { errorMap?: Record<string, unknown> }[]).some((meta) => meta?.errorMap?.onServer);\n }, [api]);\n\n return React.useMemo(() => ({ api, i18n, error, submit }), [api, error, submit, i18n.t, i18n.prefix, i18n.apiPrefix, i18n.locale]); // eslint-disable-line react-hooks/exhaustive-deps\n}\n\nconst UNCHECKED = Symbol(\"unchecked\");\n\nexport interface FieldOptions<T> {\n /** Checks the value on the server as it is typed (a username's availability): debounced, aborted when it changes. */\n validateAsync?: FieldCheck<T>;\n /** The wait, in ms, between the last change and `validateAsync`. Default 350. */\n asyncDebounceMs?: number;\n}\n\n/** A field as its component draws it: the value, its setter, its error in words, its state. */\nexport interface KrizakaField<T = unknown> {\n /** The id of the control (`htmlFor` of its label). */\n readonly id: string;\n readonly name: string;\n readonly value: T;\n /** Sets the value (clears a refusal of the server on it). */\n setValue(value: T): void;\n /** Call when the control loses focus: from then on, its errors show. */\n readonly onBlur: () => void;\n /** The error to show, in words — only once it should show. */\n readonly error: string | undefined;\n /** The error itself, with its suggestion. */\n readonly errorDetail: FormFieldError | undefined;\n /** A check (`validateAsync`) is running. */\n readonly validating: boolean;\n /** The check passed for the current value. */\n readonly available: boolean;\n /** The ids of the hint and the error (`aria-describedby`). */\n readonly hintId: string;\n readonly errorId: string;\n /** The form's locale (numbers, money, dates). */\n readonly locale: string;\n /** TanStack's field, through the facade. */\n readonly api: AnyFieldApi;\n}\n\n/** Binds a field of the form around it (inside `form.Form`): what the platform's field components are made of. */\nexport function useKrizakaField<T = unknown>(name: string, options: FieldOptions<T> = {}): KrizakaField<T> {\n const ctx = useFormContext();\n const id = React.useId();\n const [checked, setChecked] = React.useState<unknown>(UNCHECKED);\n const check = options.validateAsync;\n const field = useField({\n form: ctx.api,\n name: name as never,\n ...(check\n ? {\n asyncDebounceMs: options.asyncDebounceMs ?? 350,\n validators: {\n onChangeAsyncDebounceMs: options.asyncDebounceMs ?? 350,\n onChangeAsync: async ({ value, signal }: { value: T; signal: AbortSignal }) => {\n const answer = await check(value, signal);\n if (signal.aborted) return undefined;\n if (!answer) {\n setChecked(value);\n return undefined;\n }\n setChecked(UNCHECKED);\n const error = typeof answer === \"string\" ? { key: answer } : answer;\n return { ...error, immediate: true, source: \"check\" } satisfies FormFieldError;\n },\n },\n }\n : {}),\n } as never) as unknown as AnyFieldApi;\n const attempts = useStore(ctx.api.store, (state: { submissionAttempts: number }) => state.submissionAttempts);\n const meta = field.state.meta;\n const map = meta.errorMap as Record<string, unknown>;\n const all = [map.onServer, map.onChange, map.onBlur, map.onSubmit, map.onMount].filter(Boolean) as (FormFieldError | string)[];\n const first = all[0];\n const detail = typeof first === \"string\" ? { key: first } : first;\n const visible = detail && (detail.immediate || detail.source === \"server\" || meta.isBlurred || attempts > 0) ? detail : undefined;\n const value = field.state.value as T;\n\n return {\n id,\n name,\n value,\n setValue: (next: T) => {\n if (map.onServer) ctx.api.setFieldMeta(name as never, (m: { errorMap: Record<string, unknown> }) => ({ ...m, errorMap: { ...m.errorMap, onServer: undefined } }) as never);\n field.handleChange(next as never);\n },\n onBlur: () => field.handleBlur(),\n error: visible ? formMessage(visible, ctx.i18n) : undefined,\n errorDetail: visible,\n validating: Boolean(meta.isValidating),\n available: Boolean(check) && checked !== UNCHECKED && Object.is(checked, value) && !meta.isValidating && all.length === 0,\n hintId: `${id}-hint`,\n errorId: `${id}-error`,\n locale: ctx.i18n.locale ?? \"en\",\n api: field,\n };\n}\n\n/** The array of a field array, with its operations (`form.FieldArray`). */\nexport interface KrizakaFieldArray<T = unknown> {\n readonly name: string;\n readonly items: readonly T[];\n /** Adds an item at the end. */\n readonly push: (item: T) => void;\n /** Removes the item at `index`. */\n readonly remove: (index: number) => void;\n /** Moves an item. */\n readonly move: (from: number, to: number) => void;\n /** The name of a sub-field of an item: `name(0, \"url\")` → `links[0].url`. */\n readonly fieldName: (index: number, key?: string) => string;\n /** The error of the array itself (too few, too many), in words, once it should show. */\n readonly error: string | undefined;\n}\n\n/** Binds an array of the form (a list of links, of tiers): its items and their operations. */\nexport function useKrizakaFieldArray<T = unknown>(name: string): KrizakaFieldArray<T> {\n const ctx = useFormContext();\n const field = useField({ form: ctx.api, name: name as never, mode: \"array\" } as never) as unknown as AnyFieldApi;\n const attempts = useStore(ctx.api.store, (state: { submissionAttempts: number }) => state.submissionAttempts);\n const map = field.state.meta.errorMap as Record<string, FormFieldError | undefined>;\n const first = map.onServer ?? map.onChange ?? map.onSubmit;\n return {\n name,\n items: (field.state.value as T[] | undefined) ?? [],\n push: (item) => field.pushValue(item as never),\n remove: (index) => void field.removeValue(index),\n move: (from, to) => field.moveValue(from, to),\n fieldName: (index, key) => `${name}[${index}]${key ? `.${key}` : \"\"}`,\n error: first && (attempts > 0 || first.source === \"server\" || field.state.meta.isDirty) ? formMessage(first, ctx.i18n) : undefined,\n };\n}\n\n/** The value of a field, kept up to date (`form.Watch`): show a part of the form only when a box is ticked. */\nexport function useFormValue<T = unknown>(name: string): T {\n const ctx = useFormContext();\n return useStore(ctx.api.store, (state: { values: unknown }) => getPath(state.values, name)) as T;\n}\n\n/** The state of the submit, for a button: sending, and whether the last try succeeded. */\nexport function useFormStatus(): { submitting: boolean; succeeded: boolean; attempts: number } {\n const ctx = useFormContext();\n const submitting = useStore(ctx.api.store, (s: { isSubmitting: boolean }) => s.isSubmitting);\n const succeeded = useStore(ctx.api.store, (s: { isSubmitSuccessful: boolean }) => s.isSubmitSuccessful);\n const attempts = useStore(ctx.api.store, (s: { submissionAttempts: number }) => s.submissionAttempts);\n return { submitting, succeeded: succeeded && !ctx.error, attempts };\n}\n\nfunction getPath(source: unknown, path: string): unknown {\n let current = source;\n for (const part of path.replace(/\\[(\\d+)\\]/g, \".$1\").split(\".\")) {\n if (current === null || typeof current !== \"object\") return undefined;\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\n/** The first visible error of a form, in words (native: announced after a refused submit). */\nexport function firstVisibleError(ctx: FormContextValue): string | undefined {\n if (ctx.error) return formMessage(ctx.error, ctx.i18n);\n for (const meta of Object.values(ctx.api.state.fieldMeta) as { errorMap?: Record<string, FormFieldError | string | undefined> }[]) {\n const error = meta?.errorMap && Object.values(meta.errorMap).find(Boolean);\n if (error) return formMessage(error, ctx.i18n);\n }\n return undefined;\n}\n\n/** The platform's components, typed by the form's field names: `name` completes and is checked against the schema. */\nexport type BoundFields<C, N extends string> = {\n [K in keyof C]: C[K] extends (props: infer P) => infer R ? (P extends { name: string } ? (props: Omit<P, \"name\"> & { name: N }) => R : C[K]) : C[K];\n};\n\n/** A form in a component: its fields (typed by the schema), its submit and its state. */\nexport type KrizakaFormInstance<S extends FormSchema, C, RootProps> = BoundFields<C, FieldName<FormValues<S>>> & {\n /** The form element (web) or view (native): renders its fields and provides them the form. */\n Form: (props: RootProps) => React.ReactElement;\n /** Sends the form (what `Submit` does): `true` when it went through. */\n submit: () => Promise<boolean>;\n /** Back to the default values (or the given ones), errors cleared. */\n reset: (values?: FormValues<S>) => void;\n /** Sets a field's value from outside (a suggestion, a preset). */\n setValue: (name: FieldName<FormValues<S>>, value: unknown) => void;\n /** The values now. */\n getValues: () => FormValues<S>;\n /** The whole form's error (a refusal without a field), or null. */\n error: FormFieldError | null;\n /** TanStack's form, the escape hatch — never import @tanstack/* in a product. */\n api: AnyFormApi;\n};\n\n/** A form defined once (schema + defaults), used in components: `signUp.useForm({ onSubmit })`. */\nexport interface KrizakaFormFactory<S extends FormSchema, C, RootProps> {\n readonly schema: S;\n readonly defaultValues: FormValues<S>;\n /** The form in a component: `onSubmit` sends the values (a mutation), a thrown `KrizakaApiError` lands on the fields. */\n useForm<R = unknown>(options?: UseKrizakaFormOptions<S, R>): KrizakaFormInstance<S, C, RootProps>;\n /** The form sent by a Server Action: it receives the values and answers `{ ok: true, data }` or `{ ok: false, problem }`. */\n useFormWithAction<T = unknown>(action: (values: FormOutput<S>) => Promise<FormActionResult<T>>, options?: Omit<UseKrizakaFormOptions<S, FormActionResult<T>>, \"onSubmit\">): KrizakaFormInstance<S, C, RootProps>;\n}\n\n/** Builds a platform's `createKrizakaForm` from its field components and its root (web `<form>`, native `View`). */\nexport function makeCreateKrizakaForm<C extends object, RootProps extends object>(components: C, Root: (props: RootProps & { value: FormContextValue }) => React.ReactElement) {\n return function createKrizakaForm<S extends FormSchema>(definition: KrizakaFormDefinition<S>): KrizakaFormFactory<S, C, RootProps> {\n function useForm<R>(options?: UseKrizakaFormOptions<S, R>): KrizakaFormInstance<S, C, RootProps> {\n const ctx = useKrizakaFormCore(definition, options);\n // One `Form` for the life of the form (never remounted, so focus and fields stay): it reads the latest state from a\n // small store the hook updates.\n const [store] = React.useState(() => createStore(ctx));\n React.useLayoutEffect(() => store.set(ctx), [store, ctx]);\n const Form = React.useCallback((props: RootProps) => <Bound store={store} Root={Root} props={props} />, [store]);\n return React.useMemo(\n () =>\n ({\n ...components,\n Form,\n submit: ctx.submit,\n reset: (values?: FormValues<S>) => ctx.api.reset(values),\n setValue: (name: string, value: unknown) => ctx.api.setFieldValue(name as never, value as never),\n getValues: () => ctx.api.state.values as FormValues<S>,\n error: ctx.error,\n api: ctx.api,\n }) as unknown as KrizakaFormInstance<S, C, RootProps>,\n [ctx, Form],\n );\n }\n return {\n schema: definition.schema,\n defaultValues: definition.defaultValues,\n useForm,\n useFormWithAction: (action, options) => useForm({ ...options, onSubmit: action }),\n };\n };\n}\n\ntype Store<T> = { get: () => T; set: (value: T) => void; subscribe: (listener: () => void) => () => void };\n\nfunction createStore<T>(initial: T): Store<T> {\n let value = initial;\n const listeners = new Set<() => void>();\n return {\n get: () => value,\n set: (next) => {\n if (Object.is(next, value)) return;\n value = next;\n listeners.forEach((listener) => listener());\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n}\n\nfunction Bound<RootProps extends object>({ store, Root, props }: { store: Store<FormContextValue>; Root: (props: RootProps & { value: FormContextValue }) => React.ReactElement; props: RootProps }) {\n const value = React.useSyncExternalStore(store.subscribe, store.get, store.get);\n return <Root {...props} value={value} />;\n}\n"
1867
+ },
1868
+ {
1869
+ "path": "form/errors.ts",
1870
+ "content": "// Platform-neutral and server-safe: no React, no DOM, no React Native. The errors of a Krizaka form — a schema's issues,\n// a server's Problem Details, an availability check — as one shape, `FormFieldError`, whose words come from a key of the\n// app's catalogue (`form.errors.tooShort`), never from the validation engine's English.\n\n/** A value a message placeholder takes (`{minimum}`, `{suggestion}`). */\nexport type FormMessageValue = string | number;\n\n/** An error of a field or of the whole form: the key of its words in the app's catalogue, and what fills them in. */\nexport interface FormFieldError {\n /** The i18n key: `form.errors.required`, or the key a schema names (`z.string().min(3, \"signUp.username.short\")`). */\n key: string;\n /** The placeholders of the message (`{ minimum: 3 }`). */\n values?: Readonly<Record<string, FormMessageValue>>;\n /** The server's own words, in English: shown only when the catalogue has no message for the key. */\n message?: string;\n /** A value to offer instead (a free username): a button under the error sets it. Its label is passed translated. */\n suggestion?: { readonly label: string; readonly value: unknown };\n /** Shown at once, while the person types (an availability check), not only once they leave the field. */\n immediate?: boolean;\n /** Where it comes from: the schema (default), a field's check, or the server's answer (kept until the field changes). */\n source?: \"schema\" | \"check\" | \"server\";\n}\n\n/** The English defaults of the generic errors, by code — the words a form shows when the app's catalogue has none. */\nexport const formMessages: Readonly<Record<string, string>> = {\n required: \"This field is required.\",\n tooShort: \"Use at least {minimum} characters.\",\n tooLong: \"Use at most {maximum} characters.\",\n tooSmall: \"Enter {minimum} or more.\",\n tooBig: \"Enter {maximum} or less.\",\n tooFew: \"Choose at least {minimum}.\",\n tooMany: \"Choose at most {maximum}.\",\n invalidEmail: \"Enter an e-mail address, like name@example.com.\",\n invalidUrl: \"Enter a web address, like https://example.com.\",\n invalidDate: \"Enter a real date.\",\n invalidNumber: \"Enter a number.\",\n invalidFormat: \"Check the format of this field.\",\n invalidChoice: \"Choose one of the options.\",\n invalid: \"Check this field.\",\n // The whole form (a server's answer without fields): the codes of @krizaka/query's KrizakaApiError.\n network: \"No connection. Check your network and try again.\",\n \"rate-limited\": \"Too many tries. Wait a moment and try again.\",\n unauthorized: \"Your session has ended. Sign in again.\",\n forbidden: \"You are not allowed to do this.\",\n conflict: \"This was changed meanwhile. Reload and try again.\",\n \"validation-failed\": \"Some fields need your attention.\",\n unavailable: \"The service is unavailable for a moment. Try again shortly.\",\n server: \"Something went wrong on our side. Try again.\",\n};\n\n/** How a form finds its words: the app's translate function, and the prefixes of its keys. */\nexport interface FormI18n {\n /** The app's translate function (`(key, values) => t(key, values)`): a string, or `undefined` when the key is missing. */\n t?: (key: string, values?: Readonly<Record<string, FormMessageValue>>) => string | undefined;\n /** The prefix of the generic field errors. Default `form.errors` (→ `form.errors.tooShort`). */\n prefix?: string;\n /** The prefix of the server's codes for the whole form, as `KrizakaApiError.messageKey()`. Default `apiErrors`. */\n apiPrefix?: string;\n /** The locale of numbers, money and dates. Default `en`. */\n locale?: string;\n}\n\nconst KEY = /^[\\w-]+(\\.[\\w-]+)+$/;\n\nconst fill = (text: string, values?: Readonly<Record<string, FormMessageValue>>) =>\n values ? text.replace(/\\{(\\w+)\\}/g, (all, name: string) => (values[name] === undefined ? all : String(values[name]))) : text;\n\n/** The words of an error: the app's catalogue, else the English default of its code, else the server's words, else the key. */\nexport function formMessage(error: FormFieldError | string, i18n: FormI18n = {}): string {\n const e = typeof error === \"string\" ? { key: error } : error;\n const translated = i18n.t?.(e.key, e.values);\n if (translated && translated !== e.key) return translated;\n const code = e.key.slice(e.key.lastIndexOf(\".\") + 1);\n const fallback = formMessages[code];\n // The server's words beat a generic default (a refusal without a code), never a default for its own code.\n if (e.message && (code === \"invalid\" || !fallback)) return e.message;\n if (fallback && (e.key.startsWith(`${i18n.prefix ?? \"form.errors\"}.`) || e.key.startsWith(`${i18n.apiPrefix ?? \"apiErrors\"}.`))) return fill(fallback, e.values);\n return e.message ?? (KEY.test(e.key) ? fill(fallback ?? formMessages.invalid!, e.values) : e.key);\n}\n\n/** One issue of a Standard Schema (zod 4, valibot, arktype): zod adds its `code` and limits, read when present. */\nexport interface SchemaIssue {\n readonly message: string;\n readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>;\n readonly code?: string;\n readonly minimum?: number | bigint;\n readonly maximum?: number | bigint;\n readonly origin?: string;\n readonly format?: string;\n readonly input?: unknown;\n readonly expected?: string;\n}\n\n/** A field's name from an issue's path, as the form writes it: `links[0].url`. */\nexport function pathName(path: SchemaIssue[\"path\"]): string {\n let name = \"\";\n for (const part of path ?? []) {\n const key = typeof part === \"object\" && part !== null ? part.key : part;\n if (typeof key === \"number\") name += `[${key}]`;\n else name += name ? `.${String(key)}` : String(key);\n }\n return name;\n}\n\nconst num = (value: number | bigint | undefined) => (value === undefined ? undefined : Number(value));\n\n/** The key and values of an issue: the schema's own key when it names one, else the generic code of what failed. */\nexport function issueError(issue: SchemaIssue, prefix = \"form.errors\"): FormFieldError {\n if (KEY.test(issue.message)) return { key: issue.message };\n const key = (code: string, values?: Record<string, FormMessageValue>) => ({ key: `${prefix}.${code}`, ...(values ? { values } : {}) });\n const collection = issue.origin === \"array\" || issue.origin === \"set\";\n switch (issue.code) {\n case \"invalid_type\":\n return issue.input === undefined || issue.input === null || issue.input === \"\" ? key(\"required\") : issue.expected === \"number\" ? key(\"invalidNumber\") : issue.expected === \"date\" ? key(\"invalidDate\") : key(\"invalid\");\n case \"too_small\": {\n const minimum = num(issue.minimum)!;\n if (issue.origin === \"string\") return minimum <= 1 ? key(\"required\") : key(\"tooShort\", { minimum });\n if (collection) return minimum <= 1 ? key(\"required\") : key(\"tooFew\", { minimum });\n return key(\"tooSmall\", { minimum });\n }\n case \"too_big\": {\n const maximum = num(issue.maximum)!;\n return issue.origin === \"string\" ? key(\"tooLong\", { maximum }) : collection ? key(\"tooMany\", { maximum }) : key(\"tooBig\", { maximum });\n }\n case \"invalid_format\":\n if (issue.format === \"email\") return key(\"invalidEmail\");\n if (issue.format === \"url\") return key(\"invalidUrl\");\n if (issue.format === \"date\" || issue.format === \"datetime\") return key(\"invalidDate\");\n return key(\"invalidFormat\");\n case \"invalid_value\":\n // `z.literal(true)`: a box that must be ticked.\n return issue.input === false || issue.input === undefined ? key(\"required\") : key(\"invalidChoice\");\n case \"invalid_union\":\n case \"invalid_key\":\n case \"invalid_element\":\n return key(\"invalidChoice\");\n default:\n return key(\"invalid\");\n }\n}\n\n/** The first error of each field of a schema's issues (an issue without a path is the whole form's: `\"\"`). */\nexport function issuesToFields(issues: readonly SchemaIssue[], prefix?: string): Record<string, FormFieldError> {\n const fields: Record<string, FormFieldError> = {};\n for (const issue of issues) {\n const name = pathName(issue.path);\n if (!(name in fields)) fields[name] = { ...issueError(issue, prefix), source: \"schema\" };\n }\n return fields;\n}\n\n/**\n * A refusal the form shows, serialisable (a Server Action returns it): the shape of `KrizakaApiError` of @krizaka/query —\n * a `KrizakaApiError` *is* a `FormProblem`. `fields` go under their fields, the rest above the submit button.\n */\nexport interface FormProblem {\n /** The HTTP status (0 when no response arrived). */\n status?: number;\n /** The stable code of the refusal (`rate-limited`, `conflict`, `ACCOUNT_EXISTS`): the key of its words. */\n code?: string;\n /** The server's sentence, in English: shown only when the catalogue has no message for the code. */\n detail?: string;\n /** The refused fields, by name: a server's `{ code, message }`, or an error the form already knows (`key`). */\n fields?: Readonly<Record<string, { code?: string; message: string } | FormFieldError | string>>;\n /** Epoch ms before which the same call is refused (a 429). */\n retryAt?: number;\n}\n\n/** True for what a form can read as a problem: a `KrizakaApiError`, a `FormProblem`, an `{ ok: false, problem }` result. */\nexport function isFormProblem(value: unknown): value is FormProblem {\n return typeof value === \"object\" && value !== null && (\"fields\" in value || \"code\" in value || \"status\" in value);\n}\n\n/** A problem as field errors (by name) and the whole form's error (or `null` when every refusal has its field). */\nexport function problemToErrors(\n problem: FormProblem,\n i18n: FormI18n = {},\n known?: (name: string) => boolean,\n codeFields: Readonly<Record<string, string>> = {},\n): { fields: Record<string, FormFieldError>; form: FormFieldError | null } {\n const prefix = i18n.prefix ?? \"form.errors\";\n const fields: Record<string, FormFieldError> = {};\n let unplaced = false;\n // A refusal named by a code that belongs to a field (`UNDERAGE` → `dateOfBirth`): under that field, in its words.\n const owner = problem.code ? codeFields[problem.code] : undefined;\n if (owner && (!known || known(owner)) && !problem.fields?.[owner]) {\n fields[owner] = { key: `${prefix}.${problem.code}`, ...(problem.detail ? { message: problem.detail } : {}), source: \"server\" };\n if (!problem.fields || Object.keys(problem.fields).length === 0) return { fields, form: null };\n }\n for (const [name, refusal] of Object.entries(problem.fields ?? {})) {\n if (known && !known(name)) {\n unplaced = true;\n continue;\n }\n const error: FormFieldError =\n typeof refusal === \"string\"\n ? { key: refusal }\n : \"key\" in refusal\n ? refusal\n : { key: refusal.code ? (KEY.test(refusal.code) ? refusal.code : `${prefix}.${refusal.code}`) : `${prefix}.invalid`, message: refusal.message };\n fields[name] = { ...error, source: \"server\" };\n }\n const placed = Object.keys(fields).length > 0;\n if (placed && !unplaced) return { fields, form: null };\n const code = problem.code ?? (problem.status === 0 ? \"network\" : problem.status && problem.status >= 500 ? \"server\" : \"invalid\");\n const retryIn = problem.retryAt ? Math.max(1, Math.ceil((problem.retryAt - Date.now()) / 1000)) : undefined;\n return {\n fields,\n form: { key: KEY.test(code) ? code : `${i18n.apiPrefix ?? \"apiErrors\"}.${code}`, ...(problem.detail ? { message: problem.detail } : {}), ...(retryIn ? { values: { seconds: retryIn } } : {}), source: \"server\" },\n };\n}\n"
1871
+ },
1872
+ {
1873
+ "path": "form/index.ts",
1874
+ "content": "/**\n * @krizaka/ui/form — a form = a schema (zod 4) + the package's fields, on TanStack Form v1 behind the facade:\n * `createKrizakaForm`, `useFormWithAction`, the fields (`TextField`, `TextArea`, `Checkbox`, `Switch`, `RadioGroup`,\n * `Select`, `Combobox`, `DateField`, `TagInput`, `FileUpload`, `Money`, `Slider`), `Submit`, `FormError`, `FormField`,\n * `FieldArray`, `Watch`, the hooks to draw a field of your own, and the errors' words. Client (\"use client\"). The server\n * side (a Server Action re-validating with the same schema) is `@krizaka/ui/form/server`.\n */\nexport { composeDate, datePartsOrder, isRealDate, latestBirthDate } from \"../date-picker/parts\";\nexport {\n type FieldCheck,\n type FieldName,\n type FieldOptions,\n type FormActionResult,\n type FormContextValue,\n type FormOutput,\n type FormSchema,\n type FormValues,\n type KrizakaField,\n type KrizakaFieldArray,\n type KrizakaFormDefinition,\n type KrizakaFormFactory,\n type KrizakaFormInstance,\n useFormContext,\n useFormStatus,\n useFormValue,\n useKrizakaField,\n useKrizakaFieldArray,\n type UseKrizakaFormOptions,\n} from \"./core\";\nexport { type FormFieldError, type FormI18n, formMessage, formMessages, type FormMessageValue, type FormProblem, issueError, issuesToFields, problemToErrors } from \"./errors\";\nexport { currencySymbol, moneyInputText, parseMoney } from \"./money\";\nexport {\n Checkbox,\n type CheckboxProps,\n Combobox,\n type ComboboxProps,\n createKrizakaForm,\n DateField,\n type DateFieldProps,\n FieldArray,\n type FieldArrayProps,\n FileUpload,\n type FileUploadProps,\n FormError,\n type FormErrorProps,\n FormField,\n type FormFieldProps,\n type FormOption,\n type FormProps,\n Money,\n type MoneyProps,\n RadioGroup,\n type RadioGroupProps,\n Select,\n type SelectProps,\n Slider,\n type SliderProps,\n Submit,\n type SubmitProps,\n Switch,\n TagInput,\n type TagsProps,\n TextArea,\n type TextAreaProps,\n TextField,\n type TextFieldProps,\n useFormWithAction,\n Watch,\n type WatchProps,\n} from \"./web\";\n"
1875
+ },
1876
+ {
1877
+ "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"
1879
+ },
1880
+ {
1881
+ "path": "form/money.ts",
1882
+ "content": "// Platform-neutral: an amount typed in a field ↔ integer minor units (cents), never a float on the way.\nimport { currencyDigits } from \"@krizaka/intl\";\n\n/** The decimal separator of a locale (`en` → `.`, `fr` → `,`). */\nexport function decimalSeparator(locale: string): string {\n return new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === \"decimal\")?.value ?? \".\";\n}\n\n/** The currency's sign in a locale (`USD`, `en-US` → `$`; `EUR`, `fr` → `€`). */\nexport function currencySymbol(currency: string, locale: string): string {\n return new Intl.NumberFormat(locale, { style: \"currency\", currency }).formatToParts(0).find((p) => p.type === \"currency\")?.value ?? currency;\n}\n\n/**\n * An amount as typed (`12.5`, `12,50`, `1,234.56`, `1 234,56`) → integer minor units (`1250`). `undefined` when empty,\n * `null` when it is not an amount (letters, more decimals than the currency has).\n */\nexport function parseMoney(text: string, currency: string, locale = \"en\"): number | null | undefined {\n const digits = currencyDigits(currency);\n const t = text.replace(/[\\s\\u00a0\\u202f']/g, \"\");\n if (!t) return undefined;\n const negative = t.startsWith(\"-\");\n const body = negative ? t.slice(1) : t;\n if (!/^[\\d.,]+$/.test(body)) return null;\n let int = body;\n let frac = \"\";\n const last = Math.max(body.lastIndexOf(\".\"), body.lastIndexOf(\",\"));\n if (last >= 0) {\n const tail = body.slice(last + 1);\n // The last separator is the decimal one when it is the locale's, or when it is not followed by a group of three.\n if (body[last] === decimalSeparator(locale) || tail.length !== 3) {\n if (tail.length > digits || !/^\\d*$/.test(tail)) return null;\n int = body.slice(0, last);\n frac = tail;\n }\n }\n // What is left before the decimals: digits, grouped by three when separated (`1,234,567`).\n if (!/^(\\d*|\\d{1,3}([.,]\\d{3})+)$/.test(int)) return null;\n int = int.replace(/[.,]/g, \"\");\n if (!int && !frac) return null;\n const units = BigInt(int || \"0\") * 10n ** BigInt(digits) + BigInt((frac || \"\").padEnd(digits, \"0\") || \"0\");\n const value = Number(negative ? -units : units);\n return Number.isSafeInteger(value) ? value : null;\n}\n\n/** Minor units as the field writes them while editing (`1250`, `USD`, `fr` → `12,50`): no symbol, no grouping. */\nexport function moneyInputText(minorUnits: number | null | undefined, currency: string, locale = \"en\"): string {\n if (minorUnits === null || minorUnits === undefined || !Number.isFinite(minorUnits)) return \"\";\n const digits = currencyDigits(currency);\n const sign = minorUnits < 0 ? \"-\" : \"\";\n const abs = String(Math.abs(Math.trunc(minorUnits))).padStart(digits + 1, \"0\");\n return digits === 0 ? sign + abs : `${sign}${abs.slice(0, -digits)}${decimalSeparator(locale)}${abs.slice(-digits)}`;\n}\n"
1883
+ },
1884
+ {
1885
+ "path": "form/server.ts",
1886
+ "content": "/**\n * @krizaka/ui/form/server — the server side of a Krizaka form, for a Server Action or a route handler (no \"use client\",\n * no React): `validateForm` parses with the form's own schema and answers the errors with the same keys the form shows,\n * `formProblem` turns a thrown error (a `KrizakaApiError` of @krizaka/query) into the serialisable refusal a form reads.\n */\nimport { type FormFieldError, type FormProblem, issuesToFields, type SchemaIssue } from \"./errors\";\n\nexport type { FormFieldError, FormProblem } from \"./errors\";\n\n/** What a Server Action answers a form: done (with what it made), or a refusal (`fields` under their fields). */\nexport type FormActionResult<T = unknown> = { ok: true; data?: T } | { ok: false; problem: FormProblem };\n\ntype Schema<O> = { readonly \"~standard\": { validate: (value: unknown) => { value?: O; issues?: readonly SchemaIssue[] } | Promise<{ value?: O; issues?: readonly SchemaIssue[] }> } };\n\n/**\n * Parses what a form sent with its schema (never trust the client): the value, or a `validation-failed` problem whose\n * fields carry the keys the form would show (`form.errors.tooShort`, the schema's own keys).\n */\nexport async function validateForm<O>(schema: Schema<O>, input: unknown, options: { prefix?: string } = {}): Promise<{ ok: true; value: O } | { ok: false; problem: FormProblem }> {\n const result = await schema[\"~standard\"].validate(input);\n if (!result.issues?.length) return { ok: true, value: result.value as O };\n const fields: Record<string, FormFieldError> = issuesToFields(result.issues, options.prefix);\n const general = fields[\"\"];\n delete fields[\"\"];\n return { ok: false, problem: { status: 422, code: general ? general.key : \"validation-failed\", fields } };\n}\n\n/** A thrown error as a refusal a form reads — a `KrizakaApiError` keeps its status, code, fields and retry time. */\nexport function formProblem(error: unknown): FormProblem {\n if (error && typeof error === \"object\") {\n const e = error as { status?: unknown; code?: unknown; detail?: unknown; fields?: unknown; retryAt?: unknown };\n if (typeof e.status === \"number\" || typeof e.code === \"string\") {\n return {\n ...(typeof e.status === \"number\" ? { status: e.status } : {}),\n ...(typeof e.code === \"string\" ? { code: e.code } : {}),\n ...(typeof e.detail === \"string\" ? { detail: e.detail } : {}),\n ...(e.fields && typeof e.fields === \"object\" ? { fields: JSON.parse(JSON.stringify(e.fields)) as FormProblem[\"fields\"] } : {}),\n ...(typeof e.retryAt === \"number\" ? { retryAt: e.retryAt } : {}),\n };\n }\n }\n return { status: 500, code: \"server\" };\n}\n"
1887
+ },
1888
+ {
1889
+ "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"
1891
+ },
1892
+ {
1893
+ "path": "meta.ts",
1894
+ "content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which krizaka.com renders as\n// /docs/ui/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"iso\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"media-capture\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"radio-group\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"tag-input\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"txt\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\nexport interface Example {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its phone\n * version thought out), copied as a starting point — at most 160 lines, listed on krizaka.com/docs/ui/templates.\n */\n readonly template?: boolean;\n}\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
1895
+ }
1896
+ ],
1897
+ "dependencies": [
1898
+ "@krizaka/intl@^0.1.0",
1899
+ "@tanstack/react-form@1.33.5"
1900
+ ],
1901
+ "registryDependencies": [
1902
+ "alert",
1903
+ "button",
1904
+ "checkbox",
1905
+ "combobox",
1906
+ "date-picker",
1907
+ "field",
1908
+ "file-upload",
1909
+ "radio-group",
1910
+ "slider",
1911
+ "spinner",
1912
+ "switch",
1913
+ "tag-input"
1914
+ ],
1915
+ "demo": {
1916
+ "path": "examples/form/sign-up.tsx",
1917
+ "content": "\"use client\";\n\nimport { KrizakaApiError } from \"@krizaka/query\";\nimport { createKrizakaForm, latestBirthDate } from \"@krizaka/ui/form\";\nimport { z } from \"zod\";\n\n// The schema the sign-up API parses with: the same rules, the same keys. `signUp.*` are the app's own words.\nconst signUp = createKrizakaForm({\n schema: z.object({\n email: z.email(),\n username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/, \"signUp.username.format\"),\n password: z.string().min(8),\n dateOfBirth: z.iso.date().refine((d) => d <= latestBirthDate(18), \"form.errors.UNDERAGE\"),\n acceptTerms: z.boolean().refine(Boolean, \"signUp.terms.required\"),\n }),\n defaultValues: { email: \"\", username: \"\", password: \"\", dateOfBirth: \"\", acceptTerms: false },\n});\nconst WORDS: Record<string, string> = { \"signUp.username.format\": \"Lowercase letters, digits and _ only.\", \"signUp.username.taken\": \"This username is taken.\", \"signUp.terms.required\": \"Accept the terms to continue.\", \"form.errors.UNDERAGE\": \"You must be 18 or older.\", \"form.errors.BIRTH_DATE_INVALID\": \"Enter your real date of birth.\", \"form.errors.USERNAME_RESERVED\": \"This username is reserved.\", \"apiErrors.ACCOUNT_EXISTS\": \"This e-mail already has an account. Sign in instead.\" };\nconst taken = (u: string) => new Promise<boolean>((done) => setTimeout(() => done([\"eric\", \"elena\"].includes(u)), 300));\n// The server's codes (Orochia's register API): the ones that belong to a field land under it, the others above the button.\nconst register = async (v: { email: string; username: string }) => { if (v.username === \"admin\") throw new KrizakaApiError({ status: 400, code: \"USERNAME_RESERVED\" }); if (v.email.endsWith(\"@orochia.com\")) throw new KrizakaApiError({ status: 409, code: \"ACCOUNT_EXISTS\" }); };\n\n// Orochia's sign-up: the username checked as it is typed (a free one offered), an 18+ birth date, the server's refusals in place.\nexport default function FormSignUp() {\n const form = signUp.useForm({ i18n: { t: (key) => WORDS[key] }, codeFields: { UNDERAGE: \"dateOfBirth\", BIRTH_DATE_INVALID: \"dateOfBirth\", USERNAME_RESERVED: \"username\" }, onSubmit: register });\n return (\n <form.Form aria-label=\"Create your account\" className=\"flex w-80 flex-col gap-4\">\n <form.TextField name=\"email\" label=\"E-mail\" type=\"email\" autoComplete=\"email\" hint=\"We send a link to confirm it.\" />\n <form.TextField name=\"username\" label=\"Username\" start=\"@\" autoComplete=\"username\" transform={(v) => v.toLowerCase().replace(/\\s+/g, \"_\")} availableHint=\"Free\" validateAsync={async (u) => ((await taken(u)) ? { key: \"signUp.username.taken\", suggestion: { label: `Use @${u}_${u.length}`, value: `${u}_${u.length}` } } : null)} />\n <form.TextField name=\"password\" label=\"Password\" type=\"password\" autoComplete=\"new-password\" hint=\"At least 8 characters.\" />\n <form.DateField name=\"dateOfBirth\" label=\"Date of birth\" entry=\"parts\" autoComplete=\"bday\" hint=\"Private, never shown.\" />\n <form.Checkbox name=\"acceptTerms\" label=\"I accept the terms and the privacy policy\" />\n <form.FormError />\n <form.Submit>Create my account</form.Submit>\n </form.Form>\n );\n}\n"
1918
+ },
1919
+ "props": [
1920
+ {
1921
+ "component": "Checkbox",
1922
+ "description": "A box of the form (`boolean`): terms to accept, an option.",
1923
+ "props": [
1924
+ {
1925
+ "name": "label",
1926
+ "type": "ReactNode",
1927
+ "default": null,
1928
+ "description": "The words next to the box, clickable with it — text, or a sentence with links (terms, privacy).",
1929
+ "required": true
1930
+ },
1931
+ {
1932
+ "name": "name",
1933
+ "type": "string",
1934
+ "default": null,
1935
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
1936
+ "required": true
1937
+ },
1938
+ {
1939
+ "name": "className",
1940
+ "type": "string",
1941
+ "default": null,
1942
+ "description": "Classes merged on the field's root.",
1943
+ "required": false
1944
+ },
1945
+ {
1946
+ "name": "disabled",
1947
+ "type": "boolean",
1948
+ "default": null,
1949
+ "description": "Not available.",
1950
+ "required": false
1951
+ },
1952
+ {
1953
+ "name": "hint",
1954
+ "type": "string",
1955
+ "default": null,
1956
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
1957
+ "required": false
1958
+ }
1959
+ ]
1960
+ },
1961
+ {
1962
+ "component": "Combobox",
1963
+ "description": "A choice among many, typed to filter or fetched: one value (`string`) or several (`string[]`).",
1964
+ "props": [
1965
+ {
1966
+ "name": "label",
1967
+ "type": "string",
1968
+ "default": null,
1969
+ "description": "The visible label, passed translated (also the control's accessible name).",
1970
+ "required": true
1971
+ },
1972
+ {
1973
+ "name": "name",
1974
+ "type": "string",
1975
+ "default": null,
1976
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
1977
+ "required": true
1978
+ },
1979
+ {
1980
+ "name": "className",
1981
+ "type": "string",
1982
+ "default": null,
1983
+ "description": "Classes merged on the field's root.",
1984
+ "required": false
1985
+ },
1986
+ {
1987
+ "name": "creatable",
1988
+ "type": "boolean",
1989
+ "default": null,
1990
+ "description": "Offers to create the query as a value.",
1991
+ "required": false
1992
+ },
1993
+ {
1994
+ "name": "disabled",
1995
+ "type": "boolean",
1996
+ "default": null,
1997
+ "description": "Not available.",
1998
+ "required": false
1999
+ },
2000
+ {
2001
+ "name": "hint",
2002
+ "type": "string",
2003
+ "default": null,
2004
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2005
+ "required": false
2006
+ },
2007
+ {
2008
+ "name": "labels",
2009
+ "type": "Partial<ComboboxLabels>",
2010
+ "default": null,
2011
+ "description": "The words of the control, passed translated.",
2012
+ "required": false
2013
+ },
2014
+ {
2015
+ "name": "multiple",
2016
+ "type": "boolean",
2017
+ "default": null,
2018
+ "description": "Several values (`string[]`), shown as chips; one (`string`) by default.",
2019
+ "required": false
2020
+ },
2021
+ {
2022
+ "name": "onSearch",
2023
+ "type": "((query: string, signal: AbortSignal) => Promise<readonly ComboboxOption[]>)",
2024
+ "default": null,
2025
+ "description": "Fetches the options for a query (debounced, aborted by a newer one).",
2026
+ "required": false
2027
+ },
2028
+ {
2029
+ "name": "options",
2030
+ "type": "readonly ComboboxOption[]",
2031
+ "default": "[]",
2032
+ "description": "The options, filtered as the person types.",
2033
+ "required": false
2034
+ },
2035
+ {
2036
+ "name": "placeholder",
2037
+ "type": "string",
2038
+ "default": null,
2039
+ "description": "The input's placeholder, passed translated.",
2040
+ "required": false
2041
+ }
2042
+ ]
2043
+ },
2044
+ {
2045
+ "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.",
2047
+ "props": [
2048
+ {
2049
+ "name": "label",
2050
+ "type": "string",
2051
+ "default": null,
2052
+ "description": "The visible label, passed translated (also the control's accessible name).",
2053
+ "required": true
2054
+ },
2055
+ {
2056
+ "name": "name",
2057
+ "type": "string",
2058
+ "default": null,
2059
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2060
+ "required": true
2061
+ },
2062
+ {
2063
+ "name": "autoComplete",
2064
+ "type": "\"off\" | \"bday\"",
2065
+ "default": null,
2066
+ "description": "`bday` for a date of birth.",
2067
+ "required": false
2068
+ },
2069
+ {
2070
+ "name": "className",
2071
+ "type": "string",
2072
+ "default": null,
2073
+ "description": "Classes merged on the field's root.",
2074
+ "required": false
2075
+ },
2076
+ {
2077
+ "name": "disabled",
2078
+ "type": "boolean",
2079
+ "default": null,
2080
+ "description": "Not available.",
2081
+ "required": false
2082
+ },
2083
+ {
2084
+ "name": "entry",
2085
+ "type": "\"calendar\" | \"parts\"",
2086
+ "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).",
2088
+ "required": false
2089
+ },
2090
+ {
2091
+ "name": "hint",
2092
+ "type": "string",
2093
+ "default": null,
2094
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2095
+ "required": false
2096
+ },
2097
+ {
2098
+ "name": "labels",
2099
+ "type": "(Partial<DatePickerLabels> & Partial<Record<DatePart, string>>)",
2100
+ "default": null,
2101
+ "description": "The words of the calendar, or the names of the boxes (`day`, `month`, `year`), passed translated.",
2102
+ "required": false
2103
+ },
2104
+ {
2105
+ "name": "max",
2106
+ "type": "string",
2107
+ "default": null,
2108
+ "description": "The latest date (ISO), for the calendar (`latestBirthDate(18)` for an age gate).",
2109
+ "required": false
2110
+ },
2111
+ {
2112
+ "name": "min",
2113
+ "type": "string",
2114
+ "default": null,
2115
+ "description": "The earliest date (ISO), for the calendar.",
2116
+ "required": false
2117
+ }
2118
+ ]
2119
+ },
2120
+ {
2121
+ "component": "FieldArray",
2122
+ "description": "A list of fields that grows and shrinks (links, tiers, guests): each item's fields named `links[0].url`.",
2123
+ "props": [
2124
+ {
2125
+ "name": "children",
2126
+ "type": "(array: KrizakaFieldArray<unknown>) => ReactNode",
2127
+ "default": null,
2128
+ "description": "Draws the items: `items`, `push`, `remove`, `move`, `fieldName(i, \"url\")` for the fields of an item.",
2129
+ "required": true
2130
+ },
2131
+ {
2132
+ "name": "name",
2133
+ "type": "string",
2134
+ "default": null,
2135
+ "description": "The array's name in the schema (typed by the form).",
2136
+ "required": true
2137
+ }
2138
+ ]
2139
+ },
2140
+ {
2141
+ "component": "FileUpload",
2142
+ "description": "Files of the form (`File[]`): a drop zone, then the chosen files, each removable.",
2143
+ "props": [
2144
+ {
2145
+ "name": "browseLabel",
2146
+ "type": "string",
2147
+ "default": null,
2148
+ "description": "The button that opens the file picker, passed translated.",
2149
+ "required": true
2150
+ },
2151
+ {
2152
+ "name": "label",
2153
+ "type": "string",
2154
+ "default": null,
2155
+ "description": "The visible label, passed translated (also the control's accessible name).",
2156
+ "required": true
2157
+ },
2158
+ {
2159
+ "name": "name",
2160
+ "type": "string",
2161
+ "default": null,
2162
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2163
+ "required": true
2164
+ },
2165
+ {
2166
+ "name": "removeLabel",
2167
+ "type": "(fileName: string) => string",
2168
+ "default": null,
2169
+ "description": "The name of a file's remove button (`(name) => \"Remove \" + name`), passed translated.",
2170
+ "required": true
2171
+ },
2172
+ {
2173
+ "name": "title",
2174
+ "type": "string",
2175
+ "default": null,
2176
+ "description": "The title in the zone (\"Drop your video here\"), passed translated.",
2177
+ "required": true
2178
+ },
2179
+ {
2180
+ "name": "accept",
2181
+ "type": "string",
2182
+ "default": null,
2183
+ "description": "The types accepted (`video/*`, `image/png,.pdf`).",
2184
+ "required": false
2185
+ },
2186
+ {
2187
+ "name": "className",
2188
+ "type": "string",
2189
+ "default": null,
2190
+ "description": "Classes merged on the field's root.",
2191
+ "required": false
2192
+ },
2193
+ {
2194
+ "name": "disabled",
2195
+ "type": "boolean",
2196
+ "default": null,
2197
+ "description": "Not available.",
2198
+ "required": false
2199
+ },
2200
+ {
2201
+ "name": "hint",
2202
+ "type": "string",
2203
+ "default": null,
2204
+ "description": "What is accepted, in words (\"MP4 or MOV, up to 4 GB\").",
2205
+ "required": false
2206
+ },
2207
+ {
2208
+ "name": "maxFiles",
2209
+ "type": "number",
2210
+ "default": null,
2211
+ "description": "The most files.",
2212
+ "required": false
2213
+ },
2214
+ {
2215
+ "name": "maxSize",
2216
+ "type": "number",
2217
+ "default": null,
2218
+ "description": "The largest file, in bytes.",
2219
+ "required": false
2220
+ },
2221
+ {
2222
+ "name": "multiple",
2223
+ "type": "boolean",
2224
+ "default": null,
2225
+ "description": "Several files (`File[]`, up to `maxFiles`); one by default (still a `File[]` of one).",
2226
+ "required": false
2227
+ },
2228
+ {
2229
+ "name": "refusalLabel",
2230
+ "type": "((rejection: FileRejection) => string)",
2231
+ "default": null,
2232
+ "description": "The words of a refused file (`(r) => r.file.name + \" is too large\"`).",
2233
+ "required": false
2234
+ }
2235
+ ]
2236
+ },
2237
+ {
2238
+ "component": "FormError",
2239
+ "description": "The whole form's error — a refusal of the server that belongs to no field (a conflict, too many tries, no network).",
2240
+ "props": [
2241
+ {
2242
+ "name": "className",
2243
+ "type": "string",
2244
+ "default": null,
2245
+ "description": "Classes merged on the alert.",
2246
+ "required": false
2247
+ },
2248
+ {
2249
+ "name": "title",
2250
+ "type": "string",
2251
+ "default": null,
2252
+ "description": "A headline above the message (\"We could not create your account\"), passed translated.",
2253
+ "required": false
2254
+ }
2255
+ ]
2256
+ },
2257
+ {
2258
+ "component": "FormField",
2259
+ "description": "A field drawn by the app (a control the package does not have): the form's value, errors and ids, any markup.",
2260
+ "props": [
2261
+ {
2262
+ "name": "children",
2263
+ "type": "(field: KrizakaField<unknown>) => ReactNode",
2264
+ "default": null,
2265
+ "description": "Draws the field: its value, setter, error and ids (`useKrizakaField`).",
2266
+ "required": true
2267
+ },
2268
+ {
2269
+ "name": "name",
2270
+ "type": "string",
2271
+ "default": null,
2272
+ "description": "The field's name in the schema (typed by the form).",
2273
+ "required": true
2274
+ },
2275
+ {
2276
+ "name": "asyncDebounceMs",
2277
+ "type": "number",
2278
+ "default": null,
2279
+ "description": "The wait, in ms, before `validateAsync`.",
2280
+ "required": false
2281
+ },
2282
+ {
2283
+ "name": "validateAsync",
2284
+ "type": "FieldCheck<unknown>",
2285
+ "default": null,
2286
+ "description": "Checks the value on the server as it changes.",
2287
+ "required": false
2288
+ }
2289
+ ]
2290
+ },
2291
+ {
2292
+ "component": "Money",
2293
+ "description": "An amount of the form, stored as integer minor units (cents: `1250` = 12.50), typed the locale's way.",
2294
+ "props": [
2295
+ {
2296
+ "name": "currency",
2297
+ "type": "string",
2298
+ "default": null,
2299
+ "description": "The currency (ISO 4217: `USD`, `EUR`, `JPY`): its sign before the amount, its decimals.",
2300
+ "required": true
2301
+ },
2302
+ {
2303
+ "name": "label",
2304
+ "type": "string",
2305
+ "default": null,
2306
+ "description": "The visible label, passed translated (also the control's accessible name).",
2307
+ "required": true
2308
+ },
2309
+ {
2310
+ "name": "name",
2311
+ "type": "string",
2312
+ "default": null,
2313
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2314
+ "required": true
2315
+ },
2316
+ {
2317
+ "name": "className",
2318
+ "type": "string",
2319
+ "default": null,
2320
+ "description": "Classes merged on the field's root.",
2321
+ "required": false
2322
+ },
2323
+ {
2324
+ "name": "disabled",
2325
+ "type": "boolean",
2326
+ "default": null,
2327
+ "description": "Not available.",
2328
+ "required": false
2329
+ },
2330
+ {
2331
+ "name": "hint",
2332
+ "type": "string",
2333
+ "default": null,
2334
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2335
+ "required": false
2336
+ },
2337
+ {
2338
+ "name": "placeholder",
2339
+ "type": "string",
2340
+ "default": null,
2341
+ "description": "An example of an amount, passed translated.",
2342
+ "required": false
2343
+ }
2344
+ ]
2345
+ },
2346
+ {
2347
+ "component": "RadioGroup",
2348
+ "description": "One choice among a few, all visible.",
2349
+ "props": [
2350
+ {
2351
+ "name": "label",
2352
+ "type": "string",
2353
+ "default": null,
2354
+ "description": "The visible label, passed translated (also the control's accessible name).",
2355
+ "required": true
2356
+ },
2357
+ {
2358
+ "name": "name",
2359
+ "type": "string",
2360
+ "default": null,
2361
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2362
+ "required": true
2363
+ },
2364
+ {
2365
+ "name": "options",
2366
+ "type": "readonly FormOption[]",
2367
+ "default": null,
2368
+ "description": "The choices, all visible (two to five).",
2369
+ "required": true
2370
+ },
2371
+ {
2372
+ "name": "className",
2373
+ "type": "string",
2374
+ "default": null,
2375
+ "description": "Classes merged on the field's root.",
2376
+ "required": false
2377
+ },
2378
+ {
2379
+ "name": "disabled",
2380
+ "type": "boolean",
2381
+ "default": null,
2382
+ "description": "Not available.",
2383
+ "required": false
2384
+ },
2385
+ {
2386
+ "name": "hint",
2387
+ "type": "string",
2388
+ "default": null,
2389
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2390
+ "required": false
2391
+ },
2392
+ {
2393
+ "name": "orientation",
2394
+ "type": "\"horizontal\" | \"vertical\"",
2395
+ "default": "vertical",
2396
+ "description": "`vertical` (default) or `horizontal`.",
2397
+ "required": false
2398
+ },
2399
+ {
2400
+ "name": "variant",
2401
+ "type": "\"list\" | \"cards\"",
2402
+ "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).",
2404
+ "required": false
2405
+ }
2406
+ ]
2407
+ },
2408
+ {
2409
+ "component": "Select",
2410
+ "description": "One choice among many, in the platform's menu (`<select>`: accessible and right on phones).",
2411
+ "props": [
2412
+ {
2413
+ "name": "label",
2414
+ "type": "string",
2415
+ "default": null,
2416
+ "description": "The visible label, passed translated (also the control's accessible name).",
2417
+ "required": true
2418
+ },
2419
+ {
2420
+ "name": "name",
2421
+ "type": "string",
2422
+ "default": null,
2423
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2424
+ "required": true
2425
+ },
2426
+ {
2427
+ "name": "options",
2428
+ "type": "readonly FormOption[]",
2429
+ "default": null,
2430
+ "description": "The choices, in the platform's own menu (a country, a category).",
2431
+ "required": true
2432
+ },
2433
+ {
2434
+ "name": "className",
2435
+ "type": "string",
2436
+ "default": null,
2437
+ "description": "Classes merged on the field's root.",
2438
+ "required": false
2439
+ },
2440
+ {
2441
+ "name": "disabled",
2442
+ "type": "boolean",
2443
+ "default": null,
2444
+ "description": "Not available.",
2445
+ "required": false
2446
+ },
2447
+ {
2448
+ "name": "hint",
2449
+ "type": "string",
2450
+ "default": null,
2451
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2452
+ "required": false
2453
+ },
2454
+ {
2455
+ "name": "placeholder",
2456
+ "type": "string",
2457
+ "default": null,
2458
+ "description": "The first line while nothing is chosen (\"Choose a category\"), passed translated.",
2459
+ "required": false
2460
+ }
2461
+ ]
2462
+ },
2463
+ {
2464
+ "component": "Slider",
2465
+ "description": "A number (or a range) of the form, chosen by feel: a share, a volume, a price range.",
2466
+ "props": [
2467
+ {
2468
+ "name": "label",
2469
+ "type": "string",
2470
+ "default": null,
2471
+ "description": "The visible label, passed translated (also the control's accessible name).",
2472
+ "required": true
2473
+ },
2474
+ {
2475
+ "name": "name",
2476
+ "type": "string",
2477
+ "default": null,
2478
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2479
+ "required": true
2480
+ },
2481
+ {
2482
+ "name": "className",
2483
+ "type": "string",
2484
+ "default": null,
2485
+ "description": "Classes merged on the field's root.",
2486
+ "required": false
2487
+ },
2488
+ {
2489
+ "name": "disabled",
2490
+ "type": "boolean",
2491
+ "default": null,
2492
+ "description": "Not available.",
2493
+ "required": false
2494
+ },
2495
+ {
2496
+ "name": "formatValue",
2497
+ "type": "((value: number) => string)",
2498
+ "default": null,
2499
+ "description": "The value as words (\"15 %\", \"3 min\"), shown and read.",
2500
+ "required": false
2501
+ },
2502
+ {
2503
+ "name": "hint",
2504
+ "type": "string",
2505
+ "default": null,
2506
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2507
+ "required": false
2508
+ },
2509
+ {
2510
+ "name": "max",
2511
+ "type": "number",
2512
+ "default": null,
2513
+ "description": "The highest value. Default 100.",
2514
+ "required": false
2515
+ },
2516
+ {
2517
+ "name": "min",
2518
+ "type": "number",
2519
+ "default": null,
2520
+ "description": "The lowest value. Default 0.",
2521
+ "required": false
2522
+ },
2523
+ {
2524
+ "name": "step",
2525
+ "type": "number",
2526
+ "default": null,
2527
+ "description": "The step. Default 1.",
2528
+ "required": false
2529
+ },
2530
+ {
2531
+ "name": "thumbLabels",
2532
+ "type": "[string, string]",
2533
+ "default": null,
2534
+ "description": "For a range (`[number, number]`): each thumb's name, passed translated.",
2535
+ "required": false
2536
+ }
2537
+ ]
2538
+ },
2539
+ {
2540
+ "component": "Submit",
2541
+ "description": "The submit button: busy while the form is sent (not pressable twice), never disabled for an invalid form.",
2542
+ "props": [
2543
+ {
2544
+ "name": "announce",
2545
+ "type": "string",
2546
+ "default": null,
2547
+ "description": "A message announced politely by assistive technology when it changes (“Saved”, “Upload failed”) — passed translated.",
2548
+ "required": false
2549
+ },
2550
+ {
2551
+ "name": "asChild",
2552
+ "type": "boolean",
2553
+ "default": null,
2554
+ "description": "Renders the child (e.g. a <Link>) instead of the <button>, with props, classes and ref merged.",
2555
+ "required": false
2556
+ },
2557
+ {
2558
+ "name": "indicator",
2559
+ "type": "boolean",
2560
+ "default": "true",
2561
+ "description": "Shows the glyph while `loading` / with a `status`. `false`: you draw your own.",
2562
+ "required": false
2563
+ },
2564
+ {
2565
+ "name": "magnetic",
2566
+ "type": "boolean",
2567
+ "default": null,
2568
+ "description": "The button leans a few pixels toward a fine pointer (driven by `MotionObserver`; never under reduced motion or on touch).",
2569
+ "required": false
2570
+ },
2571
+ {
2572
+ "name": "progress",
2573
+ "type": "number | null",
2574
+ "default": null,
2575
+ "description": "Between 0 and 1, while `loading`: the glyph's trace and a light fill across the button grow to the value (an\nupload, an export).",
2576
+ "required": false
2577
+ },
2578
+ {
2579
+ "name": "shape",
2580
+ "type": "\"rounded\" | \"pill\" | \"cut\"",
2581
+ "default": "rounded",
2582
+ "description": "rounded · pill · cut (45° corners, the Krizaka chamfer).",
2583
+ "required": false
2584
+ },
2585
+ {
2586
+ "name": "shimmer",
2587
+ "type": "boolean",
2588
+ "default": null,
2589
+ "description": "A sheen of light crosses the button on hover — for the one action a page is about.",
2590
+ "required": false
2591
+ },
2592
+ {
2593
+ "name": "size",
2594
+ "type": "\"icon\" | \"sm\" | \"md\" | \"lg\"",
2595
+ "default": "md",
2596
+ "description": "sm · md · lg, or `icon` (a square).",
2597
+ "required": false
2598
+ },
2599
+ {
2600
+ "name": "status",
2601
+ "type": "\"success\" | \"error\" | null",
2602
+ "default": null,
2603
+ "description": "How the action ended: `success` draws a check, `error` a cross (and the button shakes its head once); a ring of\nthe status colour opens around it. Clear it to return to rest.",
2604
+ "required": false
2605
+ },
2606
+ {
2607
+ "name": "variant",
2608
+ "type": "\"danger\" | \"primary\" | \"gradient\" | \"secondary\" | \"outline\" | \"ghost\"",
2609
+ "default": "primary",
2610
+ "description": "The look: `primary` (the accent), `gradient` (accent → accent-2), `secondary` (a raised surface), `outline`,\n`ghost`, `danger`.",
2611
+ "required": false
2612
+ }
2613
+ ]
2614
+ },
2615
+ {
2616
+ "component": "Switch",
2617
+ "description": "An on/off setting of the form (`boolean`): the label and its hint beside the switch.",
2618
+ "props": [
2619
+ {
2620
+ "name": "label",
2621
+ "type": "string",
2622
+ "default": null,
2623
+ "description": "The visible label, passed translated (also the control's accessible name).",
2624
+ "required": true
2625
+ },
2626
+ {
2627
+ "name": "name",
2628
+ "type": "string",
2629
+ "default": null,
2630
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2631
+ "required": true
2632
+ },
2633
+ {
2634
+ "name": "className",
2635
+ "type": "string",
2636
+ "default": null,
2637
+ "description": "Classes merged on the field's root.",
2638
+ "required": false
2639
+ },
2640
+ {
2641
+ "name": "disabled",
2642
+ "type": "boolean",
2643
+ "default": null,
2644
+ "description": "Not available.",
2645
+ "required": false
2646
+ },
2647
+ {
2648
+ "name": "hint",
2649
+ "type": "string",
2650
+ "default": null,
2651
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2652
+ "required": false
2653
+ }
2654
+ ]
2655
+ },
2656
+ {
2657
+ "component": "TagInput",
2658
+ "description": "Free words of the form as chips (`string[]`): tags, keywords.",
2659
+ "props": [
2660
+ {
2661
+ "name": "label",
2662
+ "type": "string",
2663
+ "default": null,
2664
+ "description": "The visible label, passed translated (also the control's accessible name).",
2665
+ "required": true
2666
+ },
2667
+ {
2668
+ "name": "name",
2669
+ "type": "string",
2670
+ "default": null,
2671
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2672
+ "required": true
2673
+ },
2674
+ {
2675
+ "name": "className",
2676
+ "type": "string",
2677
+ "default": null,
2678
+ "description": "Classes merged on the field's root.",
2679
+ "required": false
2680
+ },
2681
+ {
2682
+ "name": "disabled",
2683
+ "type": "boolean",
2684
+ "default": null,
2685
+ "description": "Not available.",
2686
+ "required": false
2687
+ },
2688
+ {
2689
+ "name": "hint",
2690
+ "type": "string",
2691
+ "default": null,
2692
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2693
+ "required": false
2694
+ },
2695
+ {
2696
+ "name": "labels",
2697
+ "type": "Partial<Pick<ComboboxLabels, \"results\" | \"toggle\" | \"remove\" | \"empty\" | \"create\">>",
2698
+ "default": null,
2699
+ "description": "The words of the field, passed translated; English by default.",
2700
+ "required": false
2701
+ },
2702
+ {
2703
+ "name": "max",
2704
+ "type": "number",
2705
+ "default": null,
2706
+ "description": "Most tags; the field stops offering more past it.",
2707
+ "required": false
2708
+ },
2709
+ {
2710
+ "name": "normalize",
2711
+ "type": "((tag: string) => string)",
2712
+ "default": null,
2713
+ "description": "Cleans a word before it is added (`(t) => t.toLowerCase().replace(/^#/, \"\")`). An empty result is ignored.",
2714
+ "required": false
2715
+ },
2716
+ {
2717
+ "name": "placeholder",
2718
+ "type": "string",
2719
+ "default": null,
2720
+ "description": "The input's placeholder, passed translated.",
2721
+ "required": false
2722
+ },
2723
+ {
2724
+ "name": "separators",
2725
+ "type": "readonly string[]",
2726
+ "default": null,
2727
+ "description": "Keys that add the word, besides Enter. Default `[\",\"]`.",
2728
+ "required": false
2729
+ },
2730
+ {
2731
+ "name": "suggestions",
2732
+ "type": "readonly string[]",
2733
+ "default": null,
2734
+ "description": "Words offered as the person types (their own history, a curated list).",
2735
+ "required": false
2736
+ }
2737
+ ]
2738
+ },
2739
+ {
2740
+ "component": "TextArea",
2741
+ "description": "A longer text of the form (a bio, a description, a report).",
2742
+ "props": [
2743
+ {
2744
+ "name": "label",
2745
+ "type": "string",
2746
+ "default": null,
2747
+ "description": "The visible label, passed translated (also the control's accessible name).",
2748
+ "required": true
2749
+ },
2750
+ {
2751
+ "name": "name",
2752
+ "type": "string",
2753
+ "default": null,
2754
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2755
+ "required": true
2756
+ },
2757
+ {
2758
+ "name": "className",
2759
+ "type": "string",
2760
+ "default": null,
2761
+ "description": "Classes merged on the field's root.",
2762
+ "required": false
2763
+ },
2764
+ {
2765
+ "name": "disabled",
2766
+ "type": "boolean",
2767
+ "default": null,
2768
+ "description": "Not available.",
2769
+ "required": false
2770
+ },
2771
+ {
2772
+ "name": "hint",
2773
+ "type": "string",
2774
+ "default": null,
2775
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2776
+ "required": false
2777
+ },
2778
+ {
2779
+ "name": "maxLength",
2780
+ "type": "number",
2781
+ "default": null,
2782
+ "description": "The most characters.",
2783
+ "required": false
2784
+ },
2785
+ {
2786
+ "name": "placeholder",
2787
+ "type": "string",
2788
+ "default": null,
2789
+ "description": "An example of a value, passed translated.",
2790
+ "required": false
2791
+ },
2792
+ {
2793
+ "name": "rows",
2794
+ "type": "number",
2795
+ "default": "4",
2796
+ "description": "The visible lines. Default 4.",
2797
+ "required": false
2798
+ }
2799
+ ]
2800
+ },
2801
+ {
2802
+ "component": "TextField",
2803
+ "description": "A text field of the form: label, input, hint or error, an optional check on the server.",
2804
+ "props": [
2805
+ {
2806
+ "name": "label",
2807
+ "type": "string",
2808
+ "default": null,
2809
+ "description": "The visible label, passed translated (also the control's accessible name).",
2810
+ "required": true
2811
+ },
2812
+ {
2813
+ "name": "name",
2814
+ "type": "string",
2815
+ "default": null,
2816
+ "description": "The field's name in the schema: `email`, `address.city`, `links[0].url` (typed by the form).",
2817
+ "required": true
2818
+ },
2819
+ {
2820
+ "name": "asyncDebounceMs",
2821
+ "type": "number",
2822
+ "default": null,
2823
+ "description": "The wait, in ms, between the last key and `validateAsync`. Default 350.",
2824
+ "required": false
2825
+ },
2826
+ {
2827
+ "name": "autoComplete",
2828
+ "type": "string",
2829
+ "default": null,
2830
+ "description": "The browser's autofill (`email`, `username`, `new-password`, `name`).",
2831
+ "required": false
2832
+ },
2833
+ {
2834
+ "name": "availableHint",
2835
+ "type": "string",
2836
+ "default": null,
2837
+ "description": "The words once `validateAsync` accepted the value (\"@eric is free\"), passed translated.",
2838
+ "required": false
2839
+ },
2840
+ {
2841
+ "name": "checkingLabel",
2842
+ "type": "string",
2843
+ "default": "Checking",
2844
+ "description": "The name of the wait while it checks (\"Checking\"), passed translated.",
2845
+ "required": false
2846
+ },
2847
+ {
2848
+ "name": "className",
2849
+ "type": "string",
2850
+ "default": null,
2851
+ "description": "Classes merged on the field's root.",
2852
+ "required": false
2853
+ },
2854
+ {
2855
+ "name": "disabled",
2856
+ "type": "boolean",
2857
+ "default": null,
2858
+ "description": "Not available.",
2859
+ "required": false
2860
+ },
2861
+ {
2862
+ "name": "hint",
2863
+ "type": "string",
2864
+ "default": null,
2865
+ "description": "A help text under the control, read with it; the error takes its place when there is one.",
2866
+ "required": false
2867
+ },
2868
+ {
2869
+ "name": "inputMode",
2870
+ "type": "\"none\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"email\" | \"numeric\" | \"decimal\"",
2871
+ "default": null,
2872
+ "description": "The keyboard on a phone (`numeric`, `decimal`).",
2873
+ "required": false
2874
+ },
2875
+ {
2876
+ "name": "maxLength",
2877
+ "type": "number",
2878
+ "default": null,
2879
+ "description": "The most characters.",
2880
+ "required": false
2881
+ },
2882
+ {
2883
+ "name": "placeholder",
2884
+ "type": "string",
2885
+ "default": null,
2886
+ "description": "An example of a value, passed translated — never the label.",
2887
+ "required": false
2888
+ },
2889
+ {
2890
+ "name": "start",
2891
+ "type": "ReactNode",
2892
+ "default": null,
2893
+ "description": "Something before the text (an `@`, an icon).",
2894
+ "required": false
2895
+ },
2896
+ {
2897
+ "name": "transform",
2898
+ "type": "((raw: string) => string)",
2899
+ "default": null,
2900
+ "description": "Cleans what is typed before it is stored (`(v) => v.toLowerCase().replace(/\\s+/g, \"_\")`).",
2901
+ "required": false
2902
+ },
2903
+ {
2904
+ "name": "type",
2905
+ "type": "\"search\" | \"text\" | \"tel\" | \"url\" | \"email\" | \"password\"",
2906
+ "default": "text",
2907
+ "description": "`text` (default), `email`, `password`, `url`, `tel`, `search`: the right keyboard on a phone.",
2908
+ "required": false
2909
+ },
2910
+ {
2911
+ "name": "validateAsync",
2912
+ "type": "FieldCheck<string>",
2913
+ "default": null,
2914
+ "description": "Checks the value on the server as it is typed (availability): an error key, or nothing when it is fine.",
2915
+ "required": false
2916
+ }
2917
+ ]
2918
+ },
2919
+ {
2920
+ "component": "Watch",
2921
+ "description": "Renders from a field's current value (a part shown only when a box is ticked, a total).",
2922
+ "props": [
2923
+ {
2924
+ "name": "children",
2925
+ "type": "(value: unknown) => ReactNode",
2926
+ "default": null,
2927
+ "description": "Draws from its current value: show a part of the form only when a box is ticked.",
2928
+ "required": true
2929
+ },
2930
+ {
2931
+ "name": "name",
2932
+ "type": "string",
2933
+ "default": null,
2934
+ "description": "The field to follow (typed by the form).",
2935
+ "required": true
2936
+ }
2937
+ ]
2938
+ }
2939
+ ]
2940
+ }