@mihcm/ui 0.14.1 → 0.15.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 (300) hide show
  1. package/dist/CheckboxGrid.native.d.ts.map +1 -1
  2. package/dist/CheckboxGrid.native.js +2 -1
  3. package/dist/CheckboxGrid.native.js.map +1 -1
  4. package/dist/Combobox.native.d.ts.map +1 -1
  5. package/dist/Combobox.native.js +2 -1
  6. package/dist/Combobox.native.js.map +1 -1
  7. package/dist/DataTable/column-filter.d.ts +8 -0
  8. package/dist/DataTable/column-filter.d.ts.map +1 -0
  9. package/dist/DataTable/column-filter.js +67 -0
  10. package/dist/DataTable/column-filter.js.map +1 -0
  11. package/dist/DataTable/column-header.d.ts +16 -0
  12. package/dist/DataTable/column-header.d.ts.map +1 -0
  13. package/dist/DataTable/column-header.js +11 -0
  14. package/dist/DataTable/column-header.js.map +1 -0
  15. package/dist/DataTable/column-visibility.d.ts +7 -0
  16. package/dist/DataTable/column-visibility.d.ts.map +1 -0
  17. package/dist/DataTable/column-visibility.js +35 -0
  18. package/dist/DataTable/column-visibility.js.map +1 -0
  19. package/dist/DataTable/index.d.ts +5 -0
  20. package/dist/DataTable/index.d.ts.map +1 -0
  21. package/dist/DataTable/index.js +5 -0
  22. package/dist/DataTable/index.js.map +1 -0
  23. package/dist/DataTable/pinning.d.ts +13 -0
  24. package/dist/DataTable/pinning.d.ts.map +1 -0
  25. package/dist/DataTable/pinning.js +29 -0
  26. package/dist/DataTable/pinning.js.map +1 -0
  27. package/dist/DataTable.d.ts +3 -7
  28. package/dist/DataTable.d.ts.map +1 -1
  29. package/dist/DataTable.js +7 -126
  30. package/dist/DataTable.js.map +1 -1
  31. package/dist/Dialog.native.d.ts +3 -1
  32. package/dist/Dialog.native.d.ts.map +1 -1
  33. package/dist/Dialog.native.js +2 -2
  34. package/dist/Dialog.native.js.map +1 -1
  35. package/dist/Form/building-blocks.d.ts +26 -0
  36. package/dist/Form/building-blocks.d.ts.map +1 -0
  37. package/dist/Form/building-blocks.js +29 -0
  38. package/dist/Form/building-blocks.js.map +1 -0
  39. package/dist/Form/fields-choice.d.ts +72 -0
  40. package/dist/Form/fields-choice.d.ts.map +1 -0
  41. package/dist/Form/fields-choice.js +69 -0
  42. package/dist/Form/fields-choice.js.map +1 -0
  43. package/dist/Form/fields-complex.d.ts +28 -0
  44. package/dist/Form/fields-complex.d.ts.map +1 -0
  45. package/dist/Form/fields-complex.js +38 -0
  46. package/dist/Form/fields-complex.js.map +1 -0
  47. package/dist/Form/fields-date.d.ts +46 -0
  48. package/dist/Form/fields-date.d.ts.map +1 -0
  49. package/dist/Form/fields-date.js +41 -0
  50. package/dist/Form/fields-date.js.map +1 -0
  51. package/dist/Form/fields-text.d.ts +47 -0
  52. package/dist/Form/fields-text.d.ts.map +1 -0
  53. package/dist/Form/fields-text.js +46 -0
  54. package/dist/Form/fields-text.js.map +1 -0
  55. package/dist/Form/fields-toggle.d.ts +24 -0
  56. package/dist/Form/fields-toggle.d.ts.map +1 -0
  57. package/dist/Form/fields-toggle.js +32 -0
  58. package/dist/Form/fields-toggle.js.map +1 -0
  59. package/dist/Form/helpers.d.ts +66 -0
  60. package/dist/Form/helpers.d.ts.map +1 -0
  61. package/dist/Form/helpers.js +44 -0
  62. package/dist/Form/helpers.js.map +1 -0
  63. package/dist/Form/types.d.ts +25 -0
  64. package/dist/Form/types.d.ts.map +1 -0
  65. package/dist/Form/types.js +8 -0
  66. package/dist/Form/types.js.map +1 -0
  67. package/dist/Form.d.ts +24 -298
  68. package/dist/Form.d.ts.map +1 -1
  69. package/dist/Form.js +30 -246
  70. package/dist/Form.js.map +1 -1
  71. package/dist/IconSidebar.d.ts +6 -46
  72. package/dist/IconSidebar.d.ts.map +1 -1
  73. package/dist/IconSidebar.js +6 -116
  74. package/dist/IconSidebar.js.map +1 -1
  75. package/dist/MainSidebar/back-button.d.ts +14 -0
  76. package/dist/MainSidebar/back-button.d.ts.map +1 -0
  77. package/dist/MainSidebar/back-button.js +14 -0
  78. package/dist/MainSidebar/back-button.js.map +1 -0
  79. package/dist/MainSidebar/breadcrumb.d.ts +10 -0
  80. package/dist/MainSidebar/breadcrumb.d.ts.map +1 -0
  81. package/dist/MainSidebar/breadcrumb.js +24 -0
  82. package/dist/MainSidebar/breadcrumb.js.map +1 -0
  83. package/dist/MainSidebar/columns.d.ts +3 -0
  84. package/dist/MainSidebar/columns.d.ts.map +1 -0
  85. package/dist/MainSidebar/columns.js +198 -0
  86. package/dist/MainSidebar/columns.js.map +1 -0
  87. package/dist/MainSidebar/command.d.ts +3 -0
  88. package/dist/MainSidebar/command.d.ts.map +1 -0
  89. package/dist/MainSidebar/command.js +193 -0
  90. package/dist/MainSidebar/command.js.map +1 -0
  91. package/dist/MainSidebar/drilldown.d.ts +3 -0
  92. package/dist/MainSidebar/drilldown.d.ts.map +1 -0
  93. package/dist/MainSidebar/drilldown.js +154 -0
  94. package/dist/MainSidebar/drilldown.js.map +1 -0
  95. package/dist/MainSidebar/expanded.d.ts +7 -0
  96. package/dist/MainSidebar/expanded.d.ts.map +1 -0
  97. package/dist/MainSidebar/expanded.js +102 -0
  98. package/dist/MainSidebar/expanded.js.map +1 -0
  99. package/dist/MainSidebar/floating.d.ts +3 -0
  100. package/dist/MainSidebar/floating.d.ts.map +1 -0
  101. package/dist/MainSidebar/floating.js +116 -0
  102. package/dist/MainSidebar/floating.js.map +1 -0
  103. package/dist/MainSidebar/helpers.d.ts +50 -0
  104. package/dist/MainSidebar/helpers.d.ts.map +1 -0
  105. package/dist/MainSidebar/helpers.js +148 -0
  106. package/dist/MainSidebar/helpers.js.map +1 -0
  107. package/dist/MainSidebar/hover.d.ts +3 -0
  108. package/dist/MainSidebar/hover.d.ts.map +1 -0
  109. package/dist/MainSidebar/hover.js +177 -0
  110. package/dist/MainSidebar/hover.js.map +1 -0
  111. package/dist/MainSidebar/index.d.ts +6 -0
  112. package/dist/MainSidebar/index.d.ts.map +1 -0
  113. package/dist/MainSidebar/index.js +108 -0
  114. package/dist/MainSidebar/index.js.map +1 -0
  115. package/dist/MainSidebar/mobile.d.ts +29 -0
  116. package/dist/MainSidebar/mobile.d.ts.map +1 -0
  117. package/dist/MainSidebar/mobile.js +38 -0
  118. package/dist/MainSidebar/mobile.js.map +1 -0
  119. package/dist/MainSidebar/motion.d.ts +23 -0
  120. package/dist/MainSidebar/motion.d.ts.map +1 -0
  121. package/dist/MainSidebar/motion.js +40 -0
  122. package/dist/MainSidebar/motion.js.map +1 -0
  123. package/dist/MainSidebar/rail.d.ts +24 -0
  124. package/dist/MainSidebar/rail.d.ts.map +1 -0
  125. package/dist/MainSidebar/rail.js +29 -0
  126. package/dist/MainSidebar/rail.js.map +1 -0
  127. package/dist/MainSidebar/search.d.ts +19 -0
  128. package/dist/MainSidebar/search.d.ts.map +1 -0
  129. package/dist/MainSidebar/search.js +33 -0
  130. package/dist/MainSidebar/search.js.map +1 -0
  131. package/dist/MainSidebar/types.d.ts +161 -0
  132. package/dist/MainSidebar/types.d.ts.map +1 -0
  133. package/dist/MainSidebar/types.js +2 -0
  134. package/dist/MainSidebar/types.js.map +1 -0
  135. package/dist/MainSidebar.d.ts +6 -1
  136. package/dist/MainSidebar.d.ts.map +1 -1
  137. package/dist/MainSidebar.js +6 -1
  138. package/dist/MainSidebar.js.map +1 -1
  139. package/dist/NavigationMenu.js +1 -1
  140. package/dist/NavigationMenu.js.map +1 -1
  141. package/dist/RichTextEditor/theme.d.ts +44 -0
  142. package/dist/RichTextEditor/theme.d.ts.map +1 -0
  143. package/dist/RichTextEditor/theme.js +41 -0
  144. package/dist/RichTextEditor/theme.js.map +1 -0
  145. package/dist/RichTextEditor/toolbar-icons.d.ts +21 -0
  146. package/dist/RichTextEditor/toolbar-icons.d.ts.map +1 -0
  147. package/dist/RichTextEditor/toolbar-icons.js +21 -0
  148. package/dist/RichTextEditor/toolbar-icons.js.map +1 -0
  149. package/dist/RichTextEditor/toolbar.d.ts +5 -0
  150. package/dist/RichTextEditor/toolbar.d.ts.map +1 -0
  151. package/dist/RichTextEditor/toolbar.js +116 -0
  152. package/dist/RichTextEditor/toolbar.js.map +1 -0
  153. package/dist/RichTextEditor.d.ts +16 -9
  154. package/dist/RichTextEditor.d.ts.map +1 -1
  155. package/dist/RichTextEditor.js +18 -164
  156. package/dist/RichTextEditor.js.map +1 -1
  157. package/dist/Select/content.d.ts +9 -0
  158. package/dist/Select/content.d.ts.map +1 -0
  159. package/dist/Select/content.js +80 -0
  160. package/dist/Select/content.js.map +1 -0
  161. package/dist/Select/context.d.ts +27 -0
  162. package/dist/Select/context.d.ts.map +1 -0
  163. package/dist/Select/context.js +35 -0
  164. package/dist/Select/context.js.map +1 -0
  165. package/dist/Select/item.d.ts +13 -0
  166. package/dist/Select/item.d.ts.map +1 -0
  167. package/dist/Select/item.js +39 -0
  168. package/dist/Select/item.js.map +1 -0
  169. package/dist/Select/parts.d.ts +14 -0
  170. package/dist/Select/parts.d.ts.map +1 -0
  171. package/dist/Select/parts.js +17 -0
  172. package/dist/Select/parts.js.map +1 -0
  173. package/dist/Select/react-select.d.ts +25 -0
  174. package/dist/Select/react-select.d.ts.map +1 -0
  175. package/dist/Select/react-select.js +66 -0
  176. package/dist/Select/react-select.js.map +1 -0
  177. package/dist/Select/root.d.ts +15 -0
  178. package/dist/Select/root.d.ts.map +1 -0
  179. package/dist/Select/root.js +41 -0
  180. package/dist/Select/root.js.map +1 -0
  181. package/dist/Select/trigger.d.ts +15 -0
  182. package/dist/Select/trigger.d.ts.map +1 -0
  183. package/dist/Select/trigger.js +61 -0
  184. package/dist/Select/trigger.js.map +1 -0
  185. package/dist/Select.d.ts +14 -62
  186. package/dist/Select.d.ts.map +1 -1
  187. package/dist/Select.js +14 -293
  188. package/dist/Select.js.map +1 -1
  189. package/dist/Sidebar/context.d.ts +28 -0
  190. package/dist/Sidebar/context.d.ts.map +1 -0
  191. package/dist/Sidebar/context.js +37 -0
  192. package/dist/Sidebar/context.js.map +1 -0
  193. package/dist/Sidebar/group.d.ts +13 -0
  194. package/dist/Sidebar/group.d.ts.map +1 -0
  195. package/dist/Sidebar/group.js +20 -0
  196. package/dist/Sidebar/group.js.map +1 -0
  197. package/dist/Sidebar/icons.d.ts +7 -0
  198. package/dist/Sidebar/icons.d.ts.map +1 -0
  199. package/dist/Sidebar/icons.js +12 -0
  200. package/dist/Sidebar/icons.js.map +1 -0
  201. package/dist/Sidebar/layout.d.ts +9 -0
  202. package/dist/Sidebar/layout.d.ts.map +1 -0
  203. package/dist/Sidebar/layout.js +21 -0
  204. package/dist/Sidebar/layout.js.map +1 -0
  205. package/dist/Sidebar/menu.d.ts +29 -0
  206. package/dist/Sidebar/menu.d.ts.map +1 -0
  207. package/dist/Sidebar/menu.js +55 -0
  208. package/dist/Sidebar/menu.js.map +1 -0
  209. package/dist/Sidebar/provider.d.ts +33 -0
  210. package/dist/Sidebar/provider.d.ts.map +1 -0
  211. package/dist/Sidebar/provider.js +110 -0
  212. package/dist/Sidebar/provider.js.map +1 -0
  213. package/dist/Sidebar/sidebar.d.ts +17 -0
  214. package/dist/Sidebar/sidebar.d.ts.map +1 -0
  215. package/dist/Sidebar/sidebar.js +51 -0
  216. package/dist/Sidebar/sidebar.js.map +1 -0
  217. package/dist/Sidebar/submenu.d.ts +13 -0
  218. package/dist/Sidebar/submenu.d.ts.map +1 -0
  219. package/dist/Sidebar/submenu.js +17 -0
  220. package/dist/Sidebar/submenu.js.map +1 -0
  221. package/dist/Sidebar/trigger.d.ts +9 -0
  222. package/dist/Sidebar/trigger.d.ts.map +1 -0
  223. package/dist/Sidebar/trigger.js +33 -0
  224. package/dist/Sidebar/trigger.js.map +1 -0
  225. package/dist/Sidebar.d.ts +14 -104
  226. package/dist/Sidebar.d.ts.map +1 -1
  227. package/dist/Sidebar.js +14 -300
  228. package/dist/Sidebar.js.map +1 -1
  229. package/dist/StatCard.d.ts +67 -9
  230. package/dist/StatCard.d.ts.map +1 -1
  231. package/dist/StatCard.js +111 -9
  232. package/dist/StatCard.js.map +1 -1
  233. package/dist/TransferList.native.d.ts.map +1 -1
  234. package/dist/TransferList.native.js +2 -1
  235. package/dist/TransferList.native.js.map +1 -1
  236. package/package.json +2 -2
  237. package/src/CheckboxGrid.native.tsx +2 -1
  238. package/src/Combobox.native.tsx +2 -1
  239. package/src/DataTable/column-filter.tsx +134 -0
  240. package/src/DataTable/column-header.tsx +67 -0
  241. package/src/DataTable/column-visibility.tsx +87 -0
  242. package/src/DataTable/index.ts +4 -0
  243. package/src/DataTable/pinning.ts +40 -0
  244. package/src/DataTable.tsx +14 -297
  245. package/src/Dialog.native.tsx +4 -2
  246. package/src/Form/building-blocks.tsx +97 -0
  247. package/src/Form/fields-choice.tsx +312 -0
  248. package/src/Form/fields-complex.tsx +195 -0
  249. package/src/Form/fields-date.tsx +195 -0
  250. package/src/Form/fields-text.tsx +218 -0
  251. package/src/Form/fields-toggle.tsx +123 -0
  252. package/src/Form/helpers.tsx +189 -0
  253. package/src/Form/types.ts +26 -0
  254. package/src/Form.tsx +91 -1308
  255. package/src/IconSidebar.tsx +20 -442
  256. package/src/MainSidebar/back-button.tsx +58 -0
  257. package/src/MainSidebar/breadcrumb.tsx +53 -0
  258. package/src/MainSidebar/columns.tsx +350 -0
  259. package/src/MainSidebar/command.tsx +404 -0
  260. package/src/MainSidebar/drilldown.tsx +373 -0
  261. package/src/MainSidebar/expanded.tsx +414 -0
  262. package/src/MainSidebar/floating.tsx +268 -0
  263. package/src/MainSidebar/helpers.ts +164 -0
  264. package/src/MainSidebar/hover.tsx +334 -0
  265. package/src/MainSidebar/index.tsx +191 -0
  266. package/src/MainSidebar/mobile.tsx +117 -0
  267. package/src/MainSidebar/motion.ts +64 -0
  268. package/src/MainSidebar/rail.tsx +137 -0
  269. package/src/MainSidebar/search.tsx +99 -0
  270. package/src/MainSidebar/types.ts +208 -0
  271. package/src/MainSidebar.tsx +15 -4
  272. package/src/NavigationMenu.tsx +1 -1
  273. package/src/RichTextEditor/theme.ts +43 -0
  274. package/src/RichTextEditor/toolbar-icons.tsx +40 -0
  275. package/src/RichTextEditor/toolbar.tsx +271 -0
  276. package/src/RichTextEditor.tsx +23 -371
  277. package/src/Select/content.tsx +111 -0
  278. package/src/Select/context.tsx +66 -0
  279. package/src/Select/item.tsx +97 -0
  280. package/src/Select/parts.tsx +43 -0
  281. package/src/Select/react-select.tsx +216 -0
  282. package/src/Select/root.tsx +75 -0
  283. package/src/Select/trigger.tsx +122 -0
  284. package/src/Select.tsx +34 -692
  285. package/src/Sidebar/context.tsx +72 -0
  286. package/src/Sidebar/group.tsx +69 -0
  287. package/src/Sidebar/icons.tsx +42 -0
  288. package/src/Sidebar/layout.tsx +64 -0
  289. package/src/Sidebar/menu.tsx +171 -0
  290. package/src/Sidebar/provider.tsx +224 -0
  291. package/src/Sidebar/sidebar.tsx +178 -0
  292. package/src/Sidebar/submenu.tsx +58 -0
  293. package/src/Sidebar/trigger.tsx +104 -0
  294. package/src/Sidebar.tsx +44 -927
  295. package/src/StatCard.tsx +365 -20
  296. package/src/TransferList.native.tsx +2 -1
  297. package/dist/TiptapEditor.d.ts +0 -24
  298. package/dist/TiptapEditor.d.ts.map +0 -1
  299. package/dist/TiptapEditor.js +0 -84
  300. package/dist/TiptapEditor.js.map +0 -1
package/src/Form.tsx CHANGED
@@ -3,1322 +3,105 @@
3
3
  /**
4
4
  * Form field building blocks (web variant — React DOM).
5
5
  *
6
- * Composable primitives that integrate TanStack React Form v1 with
7
- * the existing MiHCM UI primitives (Input, Label, Textarea, Select,
8
- * Checkbox, RadioGroup, Switch). Two layers:
6
+ * Public barrel for the `@mihcm/ui/Form` subpath. The implementation is
7
+ * split into focused modules under `./Form/*` so each file stays under the
8
+ * 400-line rule (CLAUDE.md §6). The split is internal — every consumer-
9
+ * facing export below is unchanged.
9
10
  *
10
- * 1. **Building blocks** — FormItem, FormLabel, FormDescription,
11
- * FormMessage. Layout and accessibility scaffolding you compose
11
+ * Two layers:
12
+ *
13
+ * 1. **Building blocks** — `FormItem`, `FormLabel`, `FormDescription`,
14
+ * `FormMessage`. Layout and accessibility scaffolding you compose
12
15
  * freely around any control.
13
16
  *
14
- * 2. **Integrated fields** — FormInput, FormTextarea, FormCheckbox,
15
- * FormSwitch, FormSelect, FormRadioGroup. Each one wires a
16
- * TanStack `form.Field` to the matching UI primitive with label,
17
+ * 2. **Integrated fields** — `FormInput`, `FormSearchField`, `FormTextarea`,
18
+ * `FormCheckbox`, `FormSwitch`, `FormSelect`, `FormCombobox`,
19
+ * `FormRadioGroup`, `FormRadioCardGroup`, `FormDatePicker`,
20
+ * `FormDateRangePicker`, `FormCheckboxGrid`, `FormDropzone`. Each wires
21
+ * a TanStack `form.Field` to the matching UI primitive with label,
17
22
  * description, and error display included.
18
23
  *
24
+ * 3. **Composition helpers** — `FormFieldArray`, `FormSubscribe`,
25
+ * `FormListenEffect`, `FormActions`.
26
+ *
19
27
  * Wiki: docs/components/Form.md
20
28
  */
21
- import { useId, type HTMLAttributes, type ReactNode } from 'react';
22
- import { cn } from './internal/cn.js';
23
- import { Label } from './Label.js';
24
- import { Input } from './Input.js';
25
- import { SearchField, type SearchFieldProps } from './SearchField.js';
26
- import { Textarea } from './Textarea.js';
27
- import { Checkbox } from './Checkbox.js';
28
- import {
29
- CheckboxGrid,
30
- type CheckboxGridProps,
31
- type CheckboxGridSelection,
32
- } from './CheckboxGrid.js';
33
- import { Switch } from './Switch.js';
34
- import { Button } from './Button.js';
35
- import { DatePicker, type DatePickerProps } from './DatePicker.js';
36
- import { Dropzone, type DropzoneProps } from './Dropzone.js';
37
- import { Combobox, type ComboboxOption } from './Combobox.js';
38
- import { RadioCardGroup, type RadioCardOption } from './RadioCardGroup.js';
39
- import { Select, SelectTrigger, SelectContent, SelectItem } from './Select.js';
40
- import { RadioGroup, RadioGroupItem } from './RadioGroup.js';
41
29
 
42
30
  /* Re-export useForm for consumer convenience */
43
31
  export { useForm } from '@tanstack/react-form';
44
32
 
45
- /* ------------------------------------------------------------------ */
46
- /* Validator type kept loose so consumers can pass onChange, onBlur */
47
- /* etc. without fighting 12-param generics. */
48
- /* ------------------------------------------------------------------ */
49
-
50
- /** Validator config passed through to TanStack Form's `form.Field`. */
51
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
- export type FormFieldValidators = Record<string, any>;
53
-
54
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
- type AnyFormApi = any;
56
-
57
- /* Field render-prop shape — kept minimal to avoid coupling to
58
- TanStack internals that change across minor versions. */
59
- interface FieldRenderProps<T> {
60
- state: {
61
- value: T;
62
- meta: { errors: string[]; isTouched: boolean };
63
- };
64
- handleChange: (v: T) => void;
65
- handleBlur: () => void;
66
- }
67
-
68
- /* ------------------------------------------------------------------ */
69
- /* Building blocks */
70
- /* ------------------------------------------------------------------ */
71
-
72
- /* ── FormItem ─────────────────────────────────────────────────────── */
73
-
74
- export interface FormItemProps extends HTMLAttributes<HTMLDivElement> {}
75
-
76
- /** Vertical stack container for label + control + description + error. */
77
- export function FormItem({ className, children, ...props }: FormItemProps) {
78
- return (
79
- <div className={cn('flex flex-col gap-1.5', className)} {...props}>
80
- {children}
81
- </div>
82
- );
83
- }
84
-
85
- /* ── FormLabel ────────────────────────────────────────────────────── */
86
-
87
- export interface FormLabelProps extends HTMLAttributes<HTMLLabelElement> {
88
- htmlFor?: string;
89
- required?: boolean;
90
- }
91
-
92
- /** Styled label with optional required asterisk. Delegates to the existing Label primitive. */
93
- export function FormLabel({ htmlFor, required, className, children, ...props }: FormLabelProps) {
94
- return (
95
- <Label
96
- {...(htmlFor !== undefined && { htmlFor })}
97
- {...(required !== undefined && { required })}
98
- {...(className !== undefined && { className })}
99
- {...props}
100
- >
101
- {children}
102
- </Label>
103
- );
104
- }
105
-
106
- /* ── FormDescription ──────────────────────────────────────────────── */
107
-
108
- export interface FormDescriptionProps extends HTMLAttributes<HTMLParagraphElement> {}
109
-
110
- /** Muted helper text rendered below the control. */
111
- export function FormDescription({ className, children, ...props }: FormDescriptionProps) {
112
- return (
113
- <p className={cn('text-xs text-muted-foreground', className)} {...props}>
114
- {children}
115
- </p>
116
- );
117
- }
118
-
119
- /* ── FormMessage ──────────────────────────────────────────────────── */
120
-
121
- export interface FormMessageProps extends HTMLAttributes<HTMLParagraphElement> {
122
- /** Array of error strings — only the first is displayed. */
123
- errors?: string[];
124
- }
125
-
126
- /** Error message display with warning icon. Transitions in/out smoothly. */
127
- export function FormMessage({ errors, className, ...props }: FormMessageProps) {
128
- const firstError = errors?.filter(Boolean)[0];
129
- if (!firstError) return null;
130
-
131
- return (
132
- <p
133
- role="alert"
134
- className={cn(
135
- 'flex items-center gap-1 text-xs text-destructive',
136
- 'transition-all duration-150',
137
- className,
138
- )}
139
- {...props}
140
- >
141
- {/* Warning triangle icon */}
142
- <svg
143
- aria-hidden
144
- viewBox="0 0 16 16"
145
- fill="none"
146
- stroke="currentColor"
147
- strokeWidth={1.5}
148
- strokeLinecap="round"
149
- strokeLinejoin="round"
150
- className="h-3.5 w-3.5 shrink-0"
151
- >
152
- <path d="M8 2L1.5 13h13L8 2z" />
153
- <path d="M8 6v3" />
154
- <circle cx="8" cy="11" r="0.5" fill="currentColor" stroke="none" />
155
- </svg>
156
- {firstError}
157
- </p>
158
- );
159
- }
160
-
161
- /* ------------------------------------------------------------------ */
162
- /* FormInput */
163
- /* ------------------------------------------------------------------ */
164
-
165
- export interface FormInputProps {
166
- form: AnyFormApi;
167
- name: string;
168
- label: string;
169
- description?: string;
170
- required?: boolean;
171
- placeholder?: string;
172
- type?: string;
173
- disabled?: boolean;
174
- className?: string;
175
- validators?: FormFieldValidators;
176
- }
177
-
178
- /** Input field integrated with TanStack Form. */
179
- export function FormInput({
180
- form,
181
- name,
182
- label,
183
- description,
184
- required,
185
- placeholder,
186
- type,
187
- disabled,
188
- className,
189
- validators,
190
- }: FormInputProps) {
191
- const id = useId();
192
- const descId = `${id}-desc`;
193
- const msgId = `${id}-msg`;
194
-
195
- return (
196
- <form.Field name={name} {...(validators !== undefined && { validators })}>
197
- {(field: FieldRenderProps<string>) => {
198
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
199
- const hasError = errors.length > 0;
200
- return (
201
- <FormItem {...(className !== undefined && { className })}>
202
- <FormLabel htmlFor={id} {...(required !== undefined && { required })}>
203
- {label}
204
- </FormLabel>
205
- <Input
206
- id={id}
207
- {...(type !== undefined && { type })}
208
- {...(placeholder !== undefined && { placeholder })}
209
- {...(disabled !== undefined && { disabled })}
210
- {...(required !== undefined && { required })}
211
- invalid={hasError}
212
- value={field.state.value ?? ''}
213
- onChange={(e) => field.handleChange(e.target.value)}
214
- onBlur={field.handleBlur}
215
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
216
- />
217
- {description !== undefined ? (
218
- <FormDescription id={descId}>{description}</FormDescription>
219
- ) : null}
220
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
221
- </FormItem>
222
- );
223
- }}
224
- </form.Field>
225
- );
226
- }
227
-
228
- /* ------------------------------------------------------------------ */
229
- /* FormSearchField */
230
- /* ------------------------------------------------------------------ */
231
-
232
- export interface FormSearchFieldProps {
233
- form: AnyFormApi;
234
- name: string;
235
- label: string;
236
- description?: string;
237
- required?: boolean;
238
- placeholder?: string;
239
- disabled?: boolean;
240
- size?: SearchFieldProps['size'];
241
- noClear?: boolean;
242
- className?: string;
243
- inputClassName?: string;
244
- validators?: FormFieldValidators;
245
- }
246
-
247
- /** Search field integrated with TanStack Form. */
248
- export function FormSearchField({
249
- form,
250
- name,
251
- label,
252
- description,
253
- required,
254
- placeholder,
255
- disabled,
256
- size,
257
- noClear,
258
- className,
259
- inputClassName,
260
- validators,
261
- }: FormSearchFieldProps) {
262
- const id = useId();
263
- const descId = `${id}-desc`;
264
- const msgId = `${id}-msg`;
265
-
266
- return (
267
- <form.Field name={name} {...(validators !== undefined && { validators })}>
268
- {(field: FieldRenderProps<string>) => {
269
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
270
- const hasError = errors.length > 0;
271
- return (
272
- <FormItem {...(className !== undefined && { className })}>
273
- <FormLabel htmlFor={id} {...(required !== undefined && { required })}>
274
- {label}
275
- </FormLabel>
276
- <SearchField
277
- id={id}
278
- value={field.state.value ?? ''}
279
- onValueChange={field.handleChange}
280
- onBlur={field.handleBlur}
281
- {...(placeholder !== undefined && { placeholder })}
282
- {...(disabled !== undefined && { disabled })}
283
- {...(size !== undefined && { size })}
284
- {...(noClear !== undefined && { noClear })}
285
- {...(inputClassName !== undefined && { inputClassName })}
286
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
287
- aria-invalid={hasError}
288
- />
289
- {description !== undefined ? (
290
- <FormDescription id={descId}>{description}</FormDescription>
291
- ) : null}
292
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
293
- </FormItem>
294
- );
295
- }}
296
- </form.Field>
297
- );
298
- }
299
-
300
- /* ------------------------------------------------------------------ */
301
- /* FormTextarea */
302
- /* ------------------------------------------------------------------ */
303
-
304
- export interface FormTextareaProps {
305
- form: AnyFormApi;
306
- name: string;
307
- label: string;
308
- description?: string;
309
- required?: boolean;
310
- placeholder?: string;
311
- rows?: number;
312
- disabled?: boolean;
313
- className?: string;
314
- validators?: FormFieldValidators;
315
- }
316
-
317
- /** Textarea field integrated with TanStack Form. */
318
- export function FormTextarea({
319
- form,
320
- name,
321
- label,
322
- description,
323
- required,
324
- placeholder,
325
- rows,
326
- disabled,
327
- className,
328
- validators,
329
- }: FormTextareaProps) {
330
- const id = useId();
331
- const descId = `${id}-desc`;
332
- const msgId = `${id}-msg`;
333
-
334
- return (
335
- <form.Field name={name} {...(validators !== undefined && { validators })}>
336
- {(field: FieldRenderProps<string>) => {
337
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
338
- const hasError = errors.length > 0;
339
- return (
340
- <FormItem {...(className !== undefined && { className })}>
341
- <FormLabel htmlFor={id} {...(required !== undefined && { required })}>
342
- {label}
343
- </FormLabel>
344
- <Textarea
345
- id={id}
346
- {...(placeholder !== undefined && { placeholder })}
347
- {...(disabled !== undefined && { disabled })}
348
- {...(required !== undefined && { required })}
349
- {...(rows !== undefined && { rows })}
350
- invalid={hasError}
351
- value={field.state.value ?? ''}
352
- onChange={(e) => field.handleChange(e.target.value)}
353
- onBlur={field.handleBlur}
354
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
355
- />
356
- {description !== undefined ? (
357
- <FormDescription id={descId}>{description}</FormDescription>
358
- ) : null}
359
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
360
- </FormItem>
361
- );
362
- }}
363
- </form.Field>
364
- );
365
- }
366
-
367
- /* ------------------------------------------------------------------ */
368
- /* FormCheckbox */
369
- /* ------------------------------------------------------------------ */
370
-
371
- export interface FormCheckboxProps {
372
- form: AnyFormApi;
373
- name: string;
374
- label: string;
375
- description?: string;
376
- disabled?: boolean;
377
- className?: string;
378
- validators?: FormFieldValidators;
379
- }
380
-
381
- /** Checkbox field integrated with TanStack Form. */
382
- export function FormCheckbox({
383
- form,
384
- name,
385
- label,
386
- description,
387
- disabled,
388
- className,
389
- validators,
390
- }: FormCheckboxProps) {
391
- const id = useId();
392
- const descId = `${id}-desc`;
393
- const msgId = `${id}-msg`;
394
-
395
- return (
396
- <form.Field name={name} {...(validators !== undefined && { validators })}>
397
- {(field: FieldRenderProps<boolean>) => {
398
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
399
- const hasError = errors.length > 0;
400
- return (
401
- <FormItem {...(className !== undefined && { className })}>
402
- <div className="flex items-center gap-2">
403
- <Checkbox
404
- id={id}
405
- checked={!!field.state.value}
406
- onCheckedChange={(checked) => field.handleChange(checked === true)}
407
- {...(disabled !== undefined && { disabled })}
408
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
409
- />
410
- <FormLabel htmlFor={id}>{label}</FormLabel>
411
- </div>
412
- {description !== undefined ? (
413
- <FormDescription id={descId}>{description}</FormDescription>
414
- ) : null}
415
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
416
- </FormItem>
417
- );
418
- }}
419
- </form.Field>
420
- );
421
- }
422
-
423
- /* ------------------------------------------------------------------ */
424
- /* FormSwitch */
425
- /* ------------------------------------------------------------------ */
426
-
427
- export interface FormSwitchProps {
428
- form: AnyFormApi;
429
- name: string;
430
- label: string;
431
- description?: string;
432
- disabled?: boolean;
433
- className?: string;
434
- validators?: FormFieldValidators;
435
- }
436
-
437
- /** Switch field integrated with TanStack Form. */
438
- export function FormSwitch({
439
- form,
440
- name,
441
- label,
442
- description,
443
- disabled,
444
- className,
445
- validators,
446
- }: FormSwitchProps) {
447
- const id = useId();
448
- const descId = `${id}-desc`;
449
- const msgId = `${id}-msg`;
450
-
451
- return (
452
- <form.Field name={name} {...(validators !== undefined && { validators })}>
453
- {(field: FieldRenderProps<boolean>) => {
454
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
455
- const hasError = errors.length > 0;
456
- return (
457
- <FormItem {...(className !== undefined && { className })}>
458
- <div className="flex items-center gap-3">
459
- <Switch
460
- id={id}
461
- checked={!!field.state.value}
462
- onCheckedChange={(checked) => field.handleChange(checked)}
463
- {...(disabled !== undefined && { disabled })}
464
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
465
- />
466
- <FormLabel htmlFor={id}>{label}</FormLabel>
467
- </div>
468
- {description !== undefined ? (
469
- <FormDescription id={descId}>{description}</FormDescription>
470
- ) : null}
471
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
472
- </FormItem>
473
- );
474
- }}
475
- </form.Field>
476
- );
477
- }
478
-
479
- /* ------------------------------------------------------------------ */
480
- /* FormSelect */
481
- /* ------------------------------------------------------------------ */
482
-
483
- export interface FormSelectOption {
484
- value: string;
485
- label: string;
486
- }
487
-
488
- export interface FormSelectProps {
489
- form: AnyFormApi;
490
- name: string;
491
- label: string;
492
- options: FormSelectOption[];
493
- description?: string;
494
- required?: boolean;
495
- placeholder?: string;
496
- disabled?: boolean;
497
- className?: string;
498
- validators?: FormFieldValidators;
499
- }
500
-
501
- /** Select dropdown field integrated with TanStack Form. */
502
- export function FormSelect({
503
- form,
504
- name,
505
- label,
506
- options,
507
- description,
508
- required,
509
- placeholder,
510
- disabled,
511
- className,
512
- validators,
513
- }: FormSelectProps) {
514
- const id = useId();
515
- const descId = `${id}-desc`;
516
- const msgId = `${id}-msg`;
517
-
518
- return (
519
- <form.Field name={name} {...(validators !== undefined && { validators })}>
520
- {(field: FieldRenderProps<string>) => {
521
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
522
- const hasError = errors.length > 0;
523
- return (
524
- <FormItem {...(className !== undefined && { className })}>
525
- <FormLabel htmlFor={id} {...(required !== undefined && { required })}>
526
- {label}
527
- </FormLabel>
528
- <Select
529
- value={field.state.value ?? ''}
530
- onValueChange={(v) => {
531
- field.handleChange(v);
532
- field.handleBlur();
533
- }}
534
- >
535
- <SelectTrigger
536
- id={id}
537
- {...(placeholder !== undefined && { placeholder })}
538
- {...(disabled !== undefined && { disabled })}
539
- variant={hasError ? 'destructive' : 'default'}
540
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
541
- />
542
- <SelectContent>
543
- {options.map((opt) => (
544
- <SelectItem key={opt.value} value={opt.value}>
545
- {opt.label}
546
- </SelectItem>
547
- ))}
548
- </SelectContent>
549
- </Select>
550
- {description !== undefined ? (
551
- <FormDescription id={descId}>{description}</FormDescription>
552
- ) : null}
553
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
554
- </FormItem>
555
- );
556
- }}
557
- </form.Field>
558
- );
559
- }
560
-
561
- /* ------------------------------------------------------------------ */
562
- /* FormRadioGroup */
563
- /* ------------------------------------------------------------------ */
564
-
565
- export interface FormRadioGroupOption {
566
- value: string;
567
- label: string;
568
- }
569
-
570
- export interface FormRadioGroupProps {
571
- form: AnyFormApi;
572
- name: string;
573
- label: string;
574
- options: FormRadioGroupOption[];
575
- description?: string;
576
- required?: boolean;
577
- orientation?: 'horizontal' | 'vertical';
578
- disabled?: boolean;
579
- className?: string;
580
- validators?: FormFieldValidators;
581
- }
582
-
583
- /** RadioGroup field integrated with TanStack Form. */
584
- export function FormRadioGroup({
585
- form,
586
- name,
587
- label,
588
- options,
589
- description,
590
- required,
591
- orientation,
592
- disabled,
593
- className,
594
- validators,
595
- }: FormRadioGroupProps) {
596
- const id = useId();
597
- const descId = `${id}-desc`;
598
- const msgId = `${id}-msg`;
599
-
600
- return (
601
- <form.Field name={name} {...(validators !== undefined && { validators })}>
602
- {(field: FieldRenderProps<string>) => {
603
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
604
- const hasError = errors.length > 0;
605
- return (
606
- <FormItem {...(className !== undefined && { className })}>
607
- <FormLabel {...(required !== undefined && { required })}>{label}</FormLabel>
608
- <RadioGroup
609
- value={field.state.value ?? ''}
610
- onValueChange={(v) => {
611
- field.handleChange(v);
612
- field.handleBlur();
613
- }}
614
- {...(orientation !== undefined && { orientation })}
615
- {...(disabled !== undefined && { disabled })}
616
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
617
- >
618
- {options.map((opt) => (
619
- <RadioGroupItem key={opt.value} value={opt.value}>
620
- {opt.label}
621
- </RadioGroupItem>
622
- ))}
623
- </RadioGroup>
624
- {description !== undefined ? (
625
- <FormDescription id={descId}>{description}</FormDescription>
626
- ) : null}
627
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
628
- </FormItem>
629
- );
630
- }}
631
- </form.Field>
632
- );
633
- }
634
-
635
- /* ------------------------------------------------------------------ */
636
- /* FormRadioCardGroup */
637
- /* ------------------------------------------------------------------ */
638
-
639
- export interface FormRadioCardGroupProps {
640
- form: AnyFormApi;
641
- name: string;
642
- label: string;
643
- options: RadioCardOption[];
644
- description?: string;
645
- required?: boolean;
646
- orientation?: 'horizontal' | 'vertical';
647
- columns?: 1 | 2 | 3 | 4;
648
- tone?: 'primary' | 'accent';
649
- disabled?: boolean;
650
- className?: string;
651
- validators?: FormFieldValidators;
652
- }
653
-
654
- /** Card-style radio group integrated with TanStack Form. */
655
- export function FormRadioCardGroup({
656
- form,
657
- name,
658
- label,
659
- options,
660
- description,
661
- required,
662
- orientation,
663
- columns,
664
- tone,
665
- disabled,
666
- className,
667
- validators,
668
- }: FormRadioCardGroupProps) {
669
- const id = useId();
670
- const descId = `${id}-desc`;
671
- const msgId = `${id}-msg`;
672
-
673
- return (
674
- <form.Field name={name} {...(validators !== undefined && { validators })}>
675
- {(field: FieldRenderProps<string>) => {
676
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
677
- const hasError = errors.length > 0;
678
- return (
679
- <FormItem {...(className !== undefined && { className })}>
680
- <FormLabel {...(required !== undefined && { required })}>{label}</FormLabel>
681
- <RadioCardGroup
682
- options={options}
683
- value={field.state.value ?? ''}
684
- onValueChange={(value) => {
685
- field.handleChange(value);
686
- field.handleBlur();
687
- }}
688
- {...(orientation !== undefined && { orientation })}
689
- {...(columns !== undefined && { columns })}
690
- {...(tone !== undefined && { tone })}
691
- {...(disabled !== undefined && { disabled })}
692
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
693
- aria-invalid={hasError}
694
- />
695
- {description !== undefined ? (
696
- <FormDescription id={descId}>{description}</FormDescription>
697
- ) : null}
698
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
699
- </FormItem>
700
- );
701
- }}
702
- </form.Field>
703
- );
704
- }
705
-
706
- /* ------------------------------------------------------------------ */
707
- /* FormCombobox */
708
- /* ------------------------------------------------------------------ */
709
-
710
- export interface FormComboboxProps {
711
- form: AnyFormApi;
712
- name: string;
713
- label: string;
714
- options: ComboboxOption[];
715
- description?: string;
716
- required?: boolean;
717
- placeholder?: string;
718
- searchPlaceholder?: string;
719
- emptyText?: string;
720
- disabled?: boolean;
721
- className?: string;
722
- validators?: FormFieldValidators;
723
- }
724
-
725
- /** Combobox field integrated with TanStack Form. */
726
- export function FormCombobox({
727
- form,
728
- name,
729
- label,
730
- options,
731
- description,
732
- required,
733
- placeholder,
734
- searchPlaceholder,
735
- emptyText,
736
- disabled,
737
- className,
738
- validators,
739
- }: FormComboboxProps) {
740
- const id = useId();
741
- const descId = `${id}-desc`;
742
- const msgId = `${id}-msg`;
743
-
744
- return (
745
- <form.Field name={name} {...(validators !== undefined && { validators })}>
746
- {(field: FieldRenderProps<string>) => {
747
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
748
- const hasError = errors.length > 0;
749
- return (
750
- <FormItem {...(className !== undefined && { className })}>
751
- <FormLabel htmlFor={id} {...(required !== undefined && { required })}>
752
- {label}
753
- </FormLabel>
754
- <Combobox
755
- id={id}
756
- aria-label={label}
757
- options={options}
758
- value={field.state.value ?? ''}
759
- onValueChange={(value) => {
760
- field.handleChange(value);
761
- field.handleBlur();
762
- }}
763
- {...(placeholder !== undefined && { placeholder })}
764
- {...(searchPlaceholder !== undefined && { searchPlaceholder })}
765
- {...(emptyText !== undefined && { emptyText })}
766
- {...(disabled !== undefined && { disabled })}
767
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
768
- aria-invalid={hasError}
769
- />
770
- {description !== undefined ? (
771
- <FormDescription id={descId}>{description}</FormDescription>
772
- ) : null}
773
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
774
- </FormItem>
775
- );
776
- }}
777
- </form.Field>
778
- );
779
- }
780
-
781
- /* ------------------------------------------------------------------ */
782
- /* FormDatePicker */
783
- /* ------------------------------------------------------------------ */
784
-
785
- export interface FormDatePickerProps {
786
- form: AnyFormApi;
787
- name: string;
788
- label: string;
789
- description?: string;
790
- required?: boolean;
791
- placeholder?: string;
792
- disabled?: boolean;
793
- /** Show time selection alongside date. */
794
- showTimeSelect?: boolean;
795
- /** Date format string (default: "MM/dd/yyyy"). */
796
- dateFormat?: string;
797
- className?: string;
798
- validators?: FormFieldValidators;
799
- }
800
-
801
- /** DatePicker field integrated with TanStack Form. Uses the mihcm DatePicker primitive. */
802
- export function FormDatePicker({
803
- form,
804
- name,
805
- label,
806
- description,
807
- required,
808
- placeholder,
809
- disabled,
810
- showTimeSelect,
811
- dateFormat,
812
- className,
813
- validators,
814
- }: FormDatePickerProps) {
815
- const id = useId();
816
- const descId = `${id}-desc`;
817
- const msgId = `${id}-msg`;
818
-
819
- return (
820
- <form.Field name={name} {...(validators !== undefined && { validators })}>
821
- {(field: FieldRenderProps<Date | null>) => {
822
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
823
- const hasError = errors.length > 0;
824
- return (
825
- <FormItem {...(className !== undefined && { className })}>
826
- <FormLabel htmlFor={id} {...(required !== undefined && { required })}>
827
- {label}
828
- </FormLabel>
829
- <DatePicker
830
- id={id}
831
- selected={field.state.value}
832
- onChange={(date: Date | null) => field.handleChange(date)}
833
- onBlur={field.handleBlur}
834
- {...(placeholder !== undefined && { placeholderText: placeholder })}
835
- {...(disabled !== undefined && { disabled })}
836
- {...(showTimeSelect !== undefined && { showTimeSelect })}
837
- {...(dateFormat !== undefined && { dateFormat })}
838
- className={cn(hasError && 'border-destructive focus:ring-destructive')}
839
- {...(hasError
840
- ? { 'aria-describedby': msgId }
841
- : description !== undefined
842
- ? { 'aria-describedby': descId }
843
- : {})}
844
- />
845
- {description !== undefined ? (
846
- <FormDescription id={descId}>{description}</FormDescription>
847
- ) : null}
848
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
849
- </FormItem>
850
- );
851
- }}
852
- </form.Field>
853
- );
854
- }
855
-
856
- /* ------------------------------------------------------------------ */
857
- /* FormDateRangePicker */
858
- /* ------------------------------------------------------------------ */
859
-
860
- export interface FormDateRangePickerProps {
861
- form: AnyFormApi;
862
- name: string;
863
- label: string;
864
- description?: string;
865
- required?: boolean;
866
- placeholder?: string;
867
- disabled?: boolean;
868
- isClearable?: boolean;
869
- align?: DatePickerProps['align'];
870
- showTimeSelect?: boolean;
871
- showWeekNumbers?: boolean;
872
- monthsShown?: number;
873
- minDate?: Date;
874
- maxDate?: Date;
875
- disabledDays?: DatePickerProps['disabledDays'];
876
- dateFormat?: string;
877
- calendarProps?: DatePickerProps['calendarProps'];
878
- triggerWidth?: 'auto' | 'full';
879
- triggerClassName?: string;
880
- closeOnRangeComplete?: boolean;
881
- className?: string;
882
- validators?: FormFieldValidators;
883
- }
884
-
885
- /** Date range picker integrated with TanStack Form. Field value is `[start, end]`. */
886
- export function FormDateRangePicker({
887
- form,
888
- name,
889
- label,
890
- description,
891
- required,
892
- placeholder,
893
- disabled,
894
- isClearable,
895
- align,
896
- showTimeSelect,
897
- showWeekNumbers,
898
- monthsShown,
899
- minDate,
900
- maxDate,
901
- disabledDays,
902
- dateFormat,
903
- calendarProps,
904
- triggerWidth,
905
- triggerClassName,
906
- closeOnRangeComplete,
907
- className,
908
- validators,
909
- }: FormDateRangePickerProps) {
910
- const id = useId();
911
- const descId = `${id}-desc`;
912
- const msgId = `${id}-msg`;
913
-
914
- return (
915
- <form.Field name={name} {...(validators !== undefined && { validators })}>
916
- {(field: FieldRenderProps<[Date | null, Date | null]>) => {
917
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
918
- const hasError = errors.length > 0;
919
- const [startDate, endDate] = field.state.value ?? [null, null];
920
- return (
921
- <FormItem {...(className !== undefined && { className })}>
922
- <FormLabel htmlFor={id} {...(required !== undefined && { required })}>
923
- {label}
924
- </FormLabel>
925
- <DatePicker
926
- id={id}
927
- selectsRange
928
- startDate={startDate}
929
- endDate={endDate}
930
- onChange={(range) => field.handleChange(range)}
931
- onBlur={field.handleBlur}
932
- {...(placeholder !== undefined && { placeholderText: placeholder })}
933
- {...(disabled !== undefined && { disabled })}
934
- {...(isClearable !== undefined && { isClearable })}
935
- {...(align !== undefined && { align })}
936
- {...(showTimeSelect !== undefined && { showTimeSelect })}
937
- {...(showWeekNumbers !== undefined && { showWeekNumbers })}
938
- {...(monthsShown !== undefined && { monthsShown })}
939
- {...(minDate !== undefined && { minDate })}
940
- {...(maxDate !== undefined && { maxDate })}
941
- {...(disabledDays !== undefined && { disabledDays })}
942
- {...(dateFormat !== undefined && { dateFormat })}
943
- {...(calendarProps !== undefined && { calendarProps })}
944
- {...(triggerWidth !== undefined && { triggerWidth })}
945
- {...(triggerClassName !== undefined && { triggerClassName })}
946
- {...(closeOnRangeComplete !== undefined && { closeOnRangeComplete })}
947
- className={cn(hasError && 'border-destructive focus:ring-destructive')}
948
- {...(hasError
949
- ? { 'aria-describedby': msgId }
950
- : description !== undefined
951
- ? { 'aria-describedby': descId }
952
- : {})}
953
- />
954
- {description !== undefined ? (
955
- <FormDescription id={descId}>{description}</FormDescription>
956
- ) : null}
957
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
958
- </FormItem>
959
- );
960
- }}
961
- </form.Field>
962
- );
963
- }
964
-
965
- /* ------------------------------------------------------------------ */
966
- /* FormCheckboxGrid */
967
- /* ------------------------------------------------------------------ */
968
-
969
- export interface FormCheckboxGridProps extends Pick<
970
- CheckboxGridProps,
971
- | 'items'
972
- | 'columns'
973
- | 'layout'
974
- | 'density'
975
- | 'showToolbar'
976
- | 'searchPlaceholder'
977
- | 'maxHeight'
978
- | 'emptyState'
979
- | 'getItemClassName'
980
- > {
981
- form: AnyFormApi;
982
- name: string;
983
- label: string;
984
- description?: string;
985
- required?: boolean;
986
- disabled?: boolean;
987
- className?: string;
988
- validators?: FormFieldValidators;
989
- }
990
-
991
- /** Multi-select CheckboxGrid integrated with TanStack Form. */
992
- export function FormCheckboxGrid({
993
- form,
994
- name,
995
- label,
996
- description,
997
- required,
998
- disabled,
999
- className,
1000
- validators,
1001
- items,
1002
- columns,
1003
- layout,
1004
- density,
1005
- showToolbar,
1006
- searchPlaceholder,
1007
- maxHeight,
1008
- emptyState,
1009
- getItemClassName,
1010
- }: FormCheckboxGridProps) {
1011
- const descId = useId();
1012
- const msgId = `${descId}-msg`;
1013
-
1014
- return (
1015
- <form.Field name={name} {...(validators !== undefined && { validators })}>
1016
- {(field: FieldRenderProps<CheckboxGridSelection>) => {
1017
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
1018
- const hasError = errors.length > 0;
1019
- return (
1020
- <FormItem {...(className !== undefined && { className })}>
1021
- <FormLabel {...(required !== undefined && { required })}>{label}</FormLabel>
1022
- <CheckboxGrid
1023
- items={items}
1024
- selected={field.state.value ?? {}}
1025
- onSelectedChange={(next) => {
1026
- if (!disabled) field.handleChange(next);
1027
- field.handleBlur();
1028
- }}
1029
- {...(columns !== undefined && { columns })}
1030
- {...(layout !== undefined && { layout })}
1031
- {...(density !== undefined && { density })}
1032
- {...(showToolbar !== undefined && { showToolbar })}
1033
- {...(searchPlaceholder !== undefined && { searchPlaceholder })}
1034
- {...(maxHeight !== undefined && { maxHeight })}
1035
- {...(emptyState !== undefined && { emptyState })}
1036
- {...(getItemClassName !== undefined && { getItemClassName })}
1037
- aria-disabled={disabled}
1038
- aria-describedby={hasError ? msgId : description !== undefined ? descId : undefined}
1039
- aria-invalid={hasError}
1040
- />
1041
- {description !== undefined ? (
1042
- <FormDescription id={descId}>{description}</FormDescription>
1043
- ) : null}
1044
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
1045
- </FormItem>
1046
- );
1047
- }}
1048
- </form.Field>
1049
- );
1050
- }
1051
-
1052
- /* ------------------------------------------------------------------ */
1053
- /* FormDropzone */
1054
- /* ------------------------------------------------------------------ */
1055
-
1056
- export interface FormDropzoneProps extends Pick<
1057
- DropzoneProps,
1058
- | 'accept'
1059
- | 'maxFiles'
1060
- | 'maxSize'
1061
- | 'minSize'
1062
- | 'multiple'
1063
- | 'validator'
1064
- | 'heading'
1065
- | 'description'
1066
- | 'browseLabel'
1067
- | 'children'
1068
- > {
1069
- form: AnyFormApi;
1070
- name: string;
1071
- label: string;
1072
- fieldDescription?: string;
1073
- required?: boolean;
1074
- disabled?: boolean;
1075
- className?: string;
1076
- validators?: FormFieldValidators;
1077
- }
1078
-
1079
- /** File-list Dropzone integrated with TanStack Form. Field value is `File[]`. */
1080
- export function FormDropzone({
1081
- form,
1082
- name,
1083
- label,
1084
- fieldDescription,
1085
- required,
1086
- disabled,
1087
- className,
1088
- validators,
1089
- accept,
1090
- maxFiles,
1091
- maxSize,
1092
- minSize,
1093
- multiple,
1094
- validator,
1095
- heading,
1096
- description,
1097
- browseLabel,
1098
- children,
1099
- }: FormDropzoneProps) {
1100
- const descId = useId();
1101
- const msgId = `${descId}-msg`;
1102
-
1103
- return (
1104
- <form.Field name={name} {...(validators !== undefined && { validators })}>
1105
- {(field: FieldRenderProps<File[]>) => {
1106
- const errors = field.state.meta.isTouched ? field.state.meta.errors : [];
1107
- const hasError = errors.length > 0;
1108
- return (
1109
- <FormItem {...(className !== undefined && { className })}>
1110
- <FormLabel {...(required !== undefined && { required })}>{label}</FormLabel>
1111
- <Dropzone
1112
- {...(accept !== undefined && { accept })}
1113
- {...(maxFiles !== undefined && { maxFiles })}
1114
- {...(maxSize !== undefined && { maxSize })}
1115
- {...(minSize !== undefined && { minSize })}
1116
- {...(multiple !== undefined && { multiple })}
1117
- {...(validator !== undefined && { validator })}
1118
- {...(heading !== undefined && { heading })}
1119
- {...(description !== undefined && { description })}
1120
- {...(browseLabel !== undefined && { browseLabel })}
1121
- {...(children !== undefined && { children })}
1122
- {...(disabled !== undefined && { disabled })}
1123
- onFilesAccepted={(files) => {
1124
- field.handleChange(files);
1125
- field.handleBlur();
1126
- }}
1127
- onFilesRejected={() => field.handleBlur()}
1128
- aria-describedby={
1129
- hasError ? msgId : fieldDescription !== undefined ? descId : undefined
1130
- }
1131
- aria-invalid={hasError}
1132
- />
1133
- {fieldDescription !== undefined ? (
1134
- <FormDescription id={descId}>{fieldDescription}</FormDescription>
1135
- ) : null}
1136
- {hasError ? <FormMessage id={msgId} errors={errors} /> : null}
1137
- </FormItem>
1138
- );
1139
- }}
1140
- </form.Field>
1141
- );
1142
- }
1143
-
1144
- /* ------------------------------------------------------------------ */
1145
- /* FormFieldArray */
1146
- /* ------------------------------------------------------------------ */
1147
-
1148
- /** Render props supplied to each array item's `renderField` callback. */
1149
- export interface FieldArrayItemApi {
1150
- /** Index of this item within the array. */
1151
- index: number;
1152
- /** Total number of items currently in the array. */
1153
- total: number;
1154
- /** Remove this item from the array. */
1155
- remove: () => void;
1156
- }
1157
-
1158
- export interface FormFieldArrayProps {
1159
- form: AnyFormApi;
1160
- name: string;
1161
- label: string;
1162
- description?: string;
1163
- /** Render function called for each array item. */
1164
- renderField: (item: FieldArrayItemApi) => ReactNode;
1165
- /** Label text for the add button. Default: "Add item". */
1166
- addLabel?: string;
1167
- className?: string;
1168
- validators?: FormFieldValidators;
1169
- }
1170
-
1171
- /** Field array integrated with TanStack Form. Provides add/remove buttons. */
1172
- export function FormFieldArray({
1173
- form,
1174
- name,
1175
- label,
1176
- description,
1177
- renderField,
1178
- addLabel = 'Add item',
1179
- className,
1180
- validators,
1181
- }: FormFieldArrayProps) {
1182
- const id = useId();
1183
- const descId = `${id}-desc`;
1184
-
1185
- return (
1186
- <form.Field name={name} mode="array" {...(validators !== undefined && { validators })}>
1187
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
1188
- {(field: any) => {
1189
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1190
- const items = (field.state.value ?? []) as any[];
1191
- return (
1192
- <FormItem {...(className !== undefined && { className })}>
1193
- <div className="flex items-center justify-between">
1194
- <FormLabel>{label}</FormLabel>
1195
- <Button type="button" variant="outline" size="sm" onClick={() => field.pushValue({})}>
1196
- + {addLabel}
1197
- </Button>
1198
- </div>
1199
- {description !== undefined ? (
1200
- <FormDescription id={descId}>{description}</FormDescription>
1201
- ) : null}
1202
- <div className="space-y-3">
1203
- {items.map((_: unknown, i: number) => (
1204
- <div
1205
- key={i}
1206
- className="rounded-lg border border-border p-3 transition-all duration-150"
1207
- >
1208
- {renderField({
1209
- index: i,
1210
- total: items.length,
1211
- remove: () => field.removeValue(i),
1212
- })}
1213
- </div>
1214
- ))}
1215
- </div>
1216
- </FormItem>
1217
- );
1218
- }}
1219
- </form.Field>
1220
- );
1221
- }
1222
-
1223
- /* ------------------------------------------------------------------ */
1224
- /* FormSubscribe */
1225
- /* ------------------------------------------------------------------ */
1226
-
1227
- export interface FormSubscribeProps<TSelected> {
1228
- form: AnyFormApi;
1229
- /** Selector function that picks values from form state. */
1230
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1231
- selector: (state: any) => TSelected;
1232
- /** Render function receiving the selected state. */
1233
- children: (selected: TSelected) => ReactNode;
1234
- }
1235
-
1236
- /** Convenience wrapper around `form.Subscribe` for reactive state display. */
1237
- export function FormSubscribe<TSelected>({
1238
- form,
1239
- selector,
1240
- children,
1241
- }: FormSubscribeProps<TSelected>) {
1242
- return <form.Subscribe selector={selector}>{children}</form.Subscribe>;
1243
- }
1244
-
1245
- /* ------------------------------------------------------------------ */
1246
- /* FormListenEffect */
1247
- /* ------------------------------------------------------------------ */
1248
-
1249
- export interface FormListenEffectProps {
1250
- form: AnyFormApi;
1251
- /** Field name to listen to. */
1252
- name: string;
1253
- /** Listener callbacks (onChange, onBlur, etc.) for side effects. */
1254
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1255
- listeners: Record<string, any>;
1256
- }
1257
-
1258
- /**
1259
- * Invisible field that fires side effects when another field changes.
1260
- * Useful for dependent field updates (e.g. country changes → reset city).
1261
- * Renders nothing visually.
1262
- */
1263
- export function FormListenEffect({ form, name, listeners }: FormListenEffectProps) {
1264
- return (
1265
- <form.Field name={name} listeners={listeners}>
1266
- {() => null}
1267
- </form.Field>
1268
- );
1269
- }
1270
-
1271
- /* ------------------------------------------------------------------ */
1272
- /* FormActions */
1273
- /* ------------------------------------------------------------------ */
1274
-
1275
- export interface FormActionsProps {
1276
- form: AnyFormApi;
1277
- /** Label for the submit button. Default: "Submit". */
1278
- submitLabel?: string;
1279
- /** Label for the reset button. Default: "Reset". */
1280
- resetLabel?: string;
1281
- /** Whether to show the reset button. Default: true. */
1282
- showReset?: boolean;
1283
- /** Disable both buttons externally. */
1284
- disabled?: boolean;
1285
- className?: string;
1286
- }
1287
-
1288
- /** Footer with submit and reset buttons. Reacts to form submitting state. */
1289
- export function FormActions({
1290
- form,
1291
- submitLabel = 'Submit',
1292
- resetLabel = 'Reset',
1293
- showReset = true,
1294
- disabled,
1295
- className,
1296
- }: FormActionsProps) {
1297
- return (
1298
- <form.Subscribe
1299
- selector={(s: { canSubmit: boolean; isSubmitting: boolean }) => ({
1300
- canSubmit: s.canSubmit,
1301
- isSubmitting: s.isSubmitting,
1302
- })}
1303
- >
1304
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
1305
- {(state: any) => (
1306
- <div className={cn('flex items-center gap-3 pt-2', className)}>
1307
- <Button type="submit" disabled={disabled || !state.canSubmit}>
1308
- {state.isSubmitting ? 'Submitting...' : submitLabel}
1309
- </Button>
1310
- {showReset ? (
1311
- <Button
1312
- type="button"
1313
- variant="outline"
1314
- disabled={disabled || state.isSubmitting}
1315
- onClick={() => form.reset()}
1316
- >
1317
- {resetLabel}
1318
- </Button>
1319
- ) : null}
1320
- </div>
1321
- )}
1322
- </form.Subscribe>
1323
- );
1324
- }
33
+ /* Shared types */
34
+ export type { FormFieldValidators } from './Form/types.js';
35
+
36
+ /* Building blocks */
37
+ export {
38
+ FormDescription,
39
+ FormItem,
40
+ FormLabel,
41
+ FormMessage,
42
+ type FormDescriptionProps,
43
+ type FormItemProps,
44
+ type FormLabelProps,
45
+ type FormMessageProps,
46
+ } from './Form/building-blocks.js';
47
+
48
+ /* Text fields */
49
+ export {
50
+ FormInput,
51
+ FormSearchField,
52
+ FormTextarea,
53
+ type FormInputProps,
54
+ type FormSearchFieldProps,
55
+ type FormTextareaProps,
56
+ } from './Form/fields-text.js';
57
+
58
+ /* Toggle fields */
59
+ export {
60
+ FormCheckbox,
61
+ FormSwitch,
62
+ type FormCheckboxProps,
63
+ type FormSwitchProps,
64
+ } from './Form/fields-toggle.js';
65
+
66
+ /* Choice fields */
67
+ export {
68
+ FormCombobox,
69
+ FormRadioCardGroup,
70
+ FormRadioGroup,
71
+ FormSelect,
72
+ type FormComboboxProps,
73
+ type FormRadioCardGroupProps,
74
+ type FormRadioGroupOption,
75
+ type FormRadioGroupProps,
76
+ type FormSelectOption,
77
+ type FormSelectProps,
78
+ } from './Form/fields-choice.js';
79
+
80
+ /* Date fields */
81
+ export {
82
+ FormDatePicker,
83
+ FormDateRangePicker,
84
+ type FormDatePickerProps,
85
+ type FormDateRangePickerProps,
86
+ } from './Form/fields-date.js';
87
+
88
+ /* Complex fields */
89
+ export {
90
+ FormCheckboxGrid,
91
+ FormDropzone,
92
+ type FormCheckboxGridProps,
93
+ type FormDropzoneProps,
94
+ } from './Form/fields-complex.js';
95
+
96
+ /* Composition helpers */
97
+ export {
98
+ FormActions,
99
+ FormFieldArray,
100
+ FormListenEffect,
101
+ FormSubscribe,
102
+ type FieldArrayItemApi,
103
+ type FormActionsProps,
104
+ type FormFieldArrayProps,
105
+ type FormListenEffectProps,
106
+ type FormSubscribeProps,
107
+ } from './Form/helpers.js';