@eifi1/ui-kit 0.20.1 → 0.22.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 (286) hide show
  1. package/dist/chart.d.ts +10 -8
  2. package/dist/components/account-chips.d.ts +10 -3
  3. package/dist/components/account-chips.js +5 -2
  4. package/dist/components/account-chips.js.map +1 -1
  5. package/dist/components/account-settings.d.ts +33 -2
  6. package/dist/components/account-settings.js +43 -21
  7. package/dist/components/account-settings.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +27 -8
  9. package/dist/components/amount-input.js +15 -28
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.d.ts +8 -0
  12. package/dist/components/autocomplete.js +9 -4
  13. package/dist/components/autocomplete.js.map +1 -1
  14. package/dist/components/button-group.d.ts +10 -8
  15. package/dist/components/calculator.d.ts +10 -8
  16. package/dist/components/checkbox-group.d.ts +104 -0
  17. package/dist/components/checkbox-group.js +111 -0
  18. package/dist/components/checkbox-group.js.map +1 -0
  19. package/dist/components/checkbox.d.ts +19 -0
  20. package/dist/components/checkbox.js +30 -7
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +18 -0
  23. package/dist/components/chip.js +47 -10
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/combobox.d.ts +13 -2
  26. package/dist/components/combobox.js +17 -8
  27. package/dist/components/combobox.js.map +1 -1
  28. package/dist/components/confirm-dialog.d.ts +66 -161
  29. package/dist/components/copy-button.d.ts +10 -8
  30. package/dist/components/country-select.d.ts +124 -0
  31. package/dist/components/country-select.js +237 -0
  32. package/dist/components/country-select.js.map +1 -0
  33. package/dist/components/currency-select.d.ts +17 -1
  34. package/dist/components/currency-select.js +56 -23
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +66 -166
  37. package/dist/components/danger-confirm.js +133 -49
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-cells.d.ts +1 -2
  40. package/dist/components/data-table-filter-popover.d.ts +1 -2
  41. package/dist/components/data-table-filters.d.ts +1 -2
  42. package/dist/components/data-table-labels.d.ts +4 -97
  43. package/dist/components/data-table-labels.js +9 -1
  44. package/dist/components/data-table-labels.js.map +1 -1
  45. package/dist/components/data-table-pagination.d.ts +3 -1
  46. package/dist/components/data-table.d.ts +1 -2
  47. package/dist/components/data-table.js +98 -3
  48. package/dist/components/data-table.js.map +1 -1
  49. package/dist/components/date-picker.d.ts +25 -5
  50. package/dist/components/date-picker.js +110 -59
  51. package/dist/components/date-picker.js.map +1 -1
  52. package/dist/components/entity-combobox.d.ts +8 -1
  53. package/dist/components/entity-combobox.js +9 -4
  54. package/dist/components/entity-combobox.js.map +1 -1
  55. package/dist/components/facing-pair.d.ts +10 -8
  56. package/dist/components/field-anatomy.d.ts +95 -0
  57. package/dist/components/field-anatomy.js +84 -0
  58. package/dist/components/field-anatomy.js.map +1 -0
  59. package/dist/components/field-parts.d.ts +114 -0
  60. package/dist/components/field-parts.js +72 -0
  61. package/dist/components/field-parts.js.map +1 -0
  62. package/dist/components/file-button.d.ts +10 -8
  63. package/dist/components/file-button.js +13 -3
  64. package/dist/components/file-button.js.map +1 -1
  65. package/dist/components/file-dropzone.d.ts +32 -9
  66. package/dist/components/file-dropzone.js +57 -34
  67. package/dist/components/file-dropzone.js.map +1 -1
  68. package/dist/components/form-actions.d.ts +10 -8
  69. package/dist/components/form-actions.js +61 -3
  70. package/dist/components/form-actions.js.map +1 -1
  71. package/dist/components/hover-menu.d.ts +3 -0
  72. package/dist/components/hover-menu.js +2 -2
  73. package/dist/components/hover-menu.js.map +1 -1
  74. package/dist/components/iban-input.d.ts +66 -0
  75. package/dist/components/iban-input.js +125 -0
  76. package/dist/components/iban-input.js.map +1 -0
  77. package/dist/components/icon-picker.d.ts +12 -2
  78. package/dist/components/icon-picker.js +9 -2
  79. package/dist/components/icon-picker.js.map +1 -1
  80. package/dist/components/inline-edit-field.d.ts +167 -0
  81. package/dist/components/inline-edit-field.js +293 -0
  82. package/dist/components/inline-edit-field.js.map +1 -0
  83. package/dist/components/language-select.d.ts +127 -0
  84. package/dist/components/language-select.js +18 -0
  85. package/dist/components/language-select.js.map +1 -0
  86. package/dist/components/line-items.d.ts +25 -3
  87. package/dist/components/line-items.js +55 -46
  88. package/dist/components/line-items.js.map +1 -1
  89. package/dist/components/list.d.ts +25 -0
  90. package/dist/components/list.js +16 -5
  91. package/dist/components/list.js.map +1 -1
  92. package/dist/components/money-field.d.ts +20 -8
  93. package/dist/components/money-field.js.map +1 -1
  94. package/dist/components/month-picker.d.ts +52 -4
  95. package/dist/components/month-picker.js +290 -86
  96. package/dist/components/month-picker.js.map +1 -1
  97. package/dist/components/multi-select.d.ts +8 -1
  98. package/dist/components/multi-select.js +9 -4
  99. package/dist/components/multi-select.js.map +1 -1
  100. package/dist/components/number-field.d.ts +25 -13
  101. package/dist/components/number-field.js +6 -20
  102. package/dist/components/number-field.js.map +1 -1
  103. package/dist/components/number-input.d.ts +70 -13
  104. package/dist/components/number-input.js +90 -38
  105. package/dist/components/number-input.js.map +1 -1
  106. package/dist/components/numpad-sheet.d.ts +10 -8
  107. package/dist/components/one-time-code-input.d.ts +100 -0
  108. package/dist/components/one-time-code-input.js +137 -0
  109. package/dist/components/one-time-code-input.js.map +1 -0
  110. package/dist/components/phone-input.d.ts +66 -0
  111. package/dist/components/phone-input.js +171 -0
  112. package/dist/components/phone-input.js.map +1 -0
  113. package/dist/components/series-chart.d.ts +10 -8
  114. package/dist/components/settings-fields.d.ts +10 -8
  115. package/dist/components/share-card.d.ts +8 -6
  116. package/dist/components/sign-chip.d.ts +66 -0
  117. package/dist/components/sign-chip.js +39 -0
  118. package/dist/components/sign-chip.js.map +1 -0
  119. package/dist/components/swatch-picker.d.ts +27 -2
  120. package/dist/components/swatch-picker.js +73 -31
  121. package/dist/components/swatch-picker.js.map +1 -1
  122. package/dist/components/switch.d.ts +19 -0
  123. package/dist/components/switch.js +24 -6
  124. package/dist/components/switch.js.map +1 -1
  125. package/dist/components/text-link.d.ts +10 -8
  126. package/dist/components/tile-radio.d.ts +68 -13
  127. package/dist/components/tile-radio.js +3 -3
  128. package/dist/components/tile-radio.js.map +1 -1
  129. package/dist/components/time-input.d.ts +15 -9
  130. package/dist/components/time-input.js.map +1 -1
  131. package/dist/components/toggle-group.d.ts +47 -1
  132. package/dist/components/toggle-group.js +110 -69
  133. package/dist/components/toggle-group.js.map +1 -1
  134. package/dist/components/ui.d.ts +10 -8
  135. package/dist/components/ui.js +280 -78
  136. package/dist/components/ui.js.map +1 -1
  137. package/dist/components/use-table-state.d.ts +1 -2
  138. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  139. package/dist/data-table.d.ts +1 -2
  140. package/dist/data-table.js +2 -1
  141. package/dist/data-table.js.map +1 -1
  142. package/dist/feedback/feedback-attachment.d.ts +1 -1
  143. package/dist/feedback/feedback-attachment.js +210 -3
  144. package/dist/feedback/feedback-attachment.js.map +1 -1
  145. package/dist/feedback/feedback-dialog.d.ts +1 -1
  146. package/dist/feedback/feedback-dialog.js.map +1 -1
  147. package/dist/feedback/feedback-inbox.d.ts +1 -1
  148. package/dist/feedback/feedback-thread.d.ts +37 -3
  149. package/dist/feedback/feedback-thread.js +38 -9
  150. package/dist/feedback/feedback-thread.js.map +1 -1
  151. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
  152. package/dist/feedback.d.ts +2 -2
  153. package/dist/hooks/use-file-drop.d.ts +10 -8
  154. package/dist/i18n/defaults.d.ts +10 -8
  155. package/dist/i18n/defaults.js +17 -2
  156. package/dist/i18n/defaults.js.map +1 -1
  157. package/dist/i18n/german.d.ts +10 -8
  158. package/dist/i18n/german.js +50 -5
  159. package/dist/i18n/german.js.map +1 -1
  160. package/dist/i18n/kit-labels.d.ts +10 -8
  161. package/dist/i18n/kit-labels.js +8 -2
  162. package/dist/i18n/kit-labels.js.map +1 -1
  163. package/dist/i18n/languages.d.ts +10 -8
  164. package/dist/i18n/locales/de-CH.d.ts +10 -8
  165. package/dist/i18n/locales/en.d.ts +10 -8
  166. package/dist/i18n/locales/en.js +9 -0
  167. package/dist/i18n/locales/en.js.map +1 -1
  168. package/dist/i18n/locales/es.d.ts +10 -8
  169. package/dist/i18n/locales/es.js +48 -5
  170. package/dist/i18n/locales/es.js.map +1 -1
  171. package/dist/i18n/locales/fr.d.ts +10 -8
  172. package/dist/i18n/locales/fr.js +48 -5
  173. package/dist/i18n/locales/fr.js.map +1 -1
  174. package/dist/i18n/locales/hu.d.ts +10 -8
  175. package/dist/i18n/locales/hu.js +49 -5
  176. package/dist/i18n/locales/hu.js.map +1 -1
  177. package/dist/i18n/locales/it.d.ts +10 -8
  178. package/dist/i18n/locales/it.js +49 -5
  179. package/dist/i18n/locales/it.js.map +1 -1
  180. package/dist/i18n/locales/zh.d.ts +10 -8
  181. package/dist/i18n/locales/zh.js +48 -5
  182. package/dist/i18n/locales/zh.js.map +1 -1
  183. package/dist/i18n/review.d.ts +10 -8
  184. package/dist/i18n/review.js +8 -1
  185. package/dist/i18n/review.js.map +1 -1
  186. package/dist/index.d.ts +17 -10
  187. package/dist/index.js +57 -2
  188. package/dist/index.js.map +1 -1
  189. package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
  190. package/dist/lib/countries.d.ts +43 -0
  191. package/dist/lib/countries.js +39 -0
  192. package/dist/lib/countries.js.map +1 -0
  193. package/dist/lib/decimal-marks.d.ts +46 -0
  194. package/dist/lib/decimal-marks.js +40 -0
  195. package/dist/lib/decimal-marks.js.map +1 -0
  196. package/dist/lib/iban.d.ts +98 -0
  197. package/dist/lib/iban.js +154 -0
  198. package/dist/lib/iban.js.map +1 -0
  199. package/dist/lib/isin.d.ts +30 -0
  200. package/dist/lib/isin.js +36 -0
  201. package/dist/lib/isin.js.map +1 -0
  202. package/dist/lib/phone.d.ts +69 -0
  203. package/dist/lib/phone.js +121 -0
  204. package/dist/lib/phone.js.map +1 -0
  205. package/dist/rhf/fields.d.ts +148 -11
  206. package/dist/rhf/fields.js +332 -3
  207. package/dist/rhf/fields.js.map +1 -1
  208. package/dist/rhf/form.d.ts +10 -8
  209. package/dist/rhf.d.ts +13 -9
  210. package/dist/rhf.js.map +1 -1
  211. package/dist/shell/app-shell.d.ts +10 -8
  212. package/dist/shell/top-bar-brand.d.ts +10 -8
  213. package/dist/shell/topbar-action-menu.d.ts +13 -1
  214. package/dist/shell/topbar-action-menu.js +31 -28
  215. package/dist/shell/topbar-action-menu.js.map +1 -1
  216. package/dist/shell.d.ts +10 -8
  217. package/dist/tour/tour.js +20 -24
  218. package/dist/tour/tour.js.map +1 -1
  219. package/dist/wizard/stepper-nav.d.ts +10 -8
  220. package/dist/wizard.d.ts +10 -8
  221. package/package.json +1 -1
  222. package/src/components/account-chips.tsx +17 -3
  223. package/src/components/account-settings.tsx +92 -26
  224. package/src/components/amount-input.tsx +49 -56
  225. package/src/components/autocomplete.tsx +133 -110
  226. package/src/components/checkbox-group.tsx +229 -0
  227. package/src/components/checkbox.tsx +72 -8
  228. package/src/components/chip.tsx +80 -10
  229. package/src/components/combobox.tsx +292 -253
  230. package/src/components/country-select.tsx +443 -0
  231. package/src/components/currency-select.tsx +109 -33
  232. package/src/components/danger-confirm.tsx +345 -77
  233. package/src/components/data-table-labels.ts +16 -1
  234. package/src/components/data-table.tsx +175 -6
  235. package/src/components/date-picker.tsx +173 -62
  236. package/src/components/entity-combobox.tsx +107 -87
  237. package/src/components/field-anatomy.tsx +190 -0
  238. package/src/components/field-parts.tsx +184 -0
  239. package/src/components/file-button.tsx +36 -3
  240. package/src/components/file-dropzone.tsx +106 -38
  241. package/src/components/form-actions.tsx +115 -2
  242. package/src/components/hover-menu.tsx +7 -4
  243. package/src/components/iban-input.tsx +231 -0
  244. package/src/components/icon-picker.tsx +26 -4
  245. package/src/components/inline-edit-field.tsx +533 -0
  246. package/src/components/language-select.tsx +78 -0
  247. package/src/components/line-items.tsx +35 -2
  248. package/src/components/list.tsx +55 -6
  249. package/src/components/money-field.tsx +10 -0
  250. package/src/components/month-picker.tsx +399 -63
  251. package/src/components/multi-select.tsx +80 -58
  252. package/src/components/number-field.tsx +28 -46
  253. package/src/components/number-input.tsx +210 -43
  254. package/src/components/one-time-code-input.tsx +288 -0
  255. package/src/components/phone-input.tsx +299 -0
  256. package/src/components/sign-chip.tsx +103 -0
  257. package/src/components/swatch-picker.tsx +118 -30
  258. package/src/components/switch.tsx +58 -7
  259. package/src/components/tile-radio.tsx +71 -16
  260. package/src/components/time-input.tsx +8 -1
  261. package/src/components/toggle-group.tsx +206 -73
  262. package/src/components/ui.tsx +472 -105
  263. package/src/data-table.ts +2 -1
  264. package/src/feedback/feedback-attachment.tsx +313 -9
  265. package/src/feedback/feedback-dialog.tsx +2 -0
  266. package/src/feedback/feedback-thread.tsx +66 -5
  267. package/src/i18n/defaults.ts +16 -1
  268. package/src/i18n/german.ts +47 -0
  269. package/src/i18n/kit-labels.tsx +78 -2
  270. package/src/i18n/locales/en.ts +10 -0
  271. package/src/i18n/locales/es.ts +46 -0
  272. package/src/i18n/locales/fr.ts +45 -0
  273. package/src/i18n/locales/hu.ts +46 -0
  274. package/src/i18n/locales/it.ts +47 -0
  275. package/src/i18n/locales/zh.ts +44 -0
  276. package/src/i18n/review.ts +7 -0
  277. package/src/index.ts +53 -3
  278. package/src/lib/countries.ts +91 -0
  279. package/src/lib/decimal-marks.ts +99 -0
  280. package/src/lib/iban.ts +171 -0
  281. package/src/lib/isin.ts +62 -0
  282. package/src/lib/phone.ts +211 -0
  283. package/src/rhf/fields.tsx +606 -9
  284. package/src/rhf.ts +2 -1
  285. package/src/shell/topbar-action-menu.tsx +53 -29
  286. package/src/tour/tour.tsx +66 -28
@@ -10,6 +10,9 @@
10
10
  * <RhfSelect name="type" label="Type" options={TYPES} />
11
11
  * <RhfCombobox name="tenantId" label="Tenant" options={tenants} clearable />
12
12
  * <RhfCheckbox name="isDefault" label="Default account" />
13
+ * <RhfTimeInput name="meetingTime" label="Time" />
14
+ * <RhfDateRangePicker fromName="periodFrom" toName="periodTo" label="Period" />
15
+ * <RhfToggleGroup name="interval" label="Interval" options={INTERVALS} />
13
16
  * </Form>
14
17
  * ```
15
18
  *
@@ -41,19 +44,34 @@
41
44
  * {@link RhfField} is the shell they are all built on, for a control the kit does not
42
45
  * ship (an address autocomplete): it takes a `render` and wires the rest.
43
46
  */
44
- import { useLayoutEffect, useRef, useState, type ReactElement, type ReactNode } from "react";
45
- import type {
46
- ControllerFieldState,
47
- ControllerProps,
48
- ControllerRenderProps,
49
- FieldPath,
50
- FieldValues,
47
+ import { useLayoutEffect, useRef, useState, type FocusEvent, type ReactElement, type ReactNode } from "react";
48
+ import {
49
+ useController,
50
+ type ControllerFieldState,
51
+ type ControllerProps,
52
+ type ControllerRenderProps,
53
+ type FieldPath,
54
+ type FieldValues,
51
55
  } from "react-hook-form";
52
56
  import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField } from "./form";
53
57
  import { Input, Select, Textarea, type InputProps, type SelectProps, type TextareaProps } from "../components/ui";
54
58
  import { NumberField, type NumberFieldProps } from "../components/number-field";
55
59
  import { AmountInput } from "../components/amount-input";
56
- import { DatePicker, type DatePickerProps } from "../components/date-picker";
60
+ import {
61
+ DatePicker,
62
+ DateRangePicker,
63
+ type DatePickerProps,
64
+ type DateRangePickerProps,
65
+ } from "../components/date-picker";
66
+ import { TimeInput, type TimeInputProps } from "../components/time-input";
67
+ import { IbanInput, type IbanInputProps } from "../components/iban-input";
68
+ import { PhoneInput, type PhoneInputProps } from "../components/phone-input";
69
+ import {
70
+ ToggleGroup,
71
+ type ToggleGroupBaseProps,
72
+ type ToggleGroupClearableProps,
73
+ type ToggleGroupRequiredProps,
74
+ } from "../components/toggle-group";
57
75
  import { Checkbox, type CheckboxProps } from "../components/checkbox";
58
76
  import { EntityCombobox, type EntityComboboxProps } from "../components/entity-combobox";
59
77
  import { Combobox, type ComboboxProps } from "../components/combobox";
@@ -679,7 +697,9 @@ export type RhfDateFieldProps<
679
697
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
680
698
  TTransformed = TFieldValues,
681
699
  > = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
682
- Omit<DatePickerProps, "value" | "onChange" | "label" | "invalid" | "disabled" | "className" | "id"> & {
700
+ // `error`: the form's message is shown by the binding (FormMessage); the picker's own
701
+ // `error` (0.22) would show it a second time.
702
+ Omit<DatePickerProps, "value" | "onChange" | "label" | "invalid" | "disabled" | "className" | "id" | "error"> & {
683
703
  /** Classes for the picker. Default `w-full`. */
684
704
  inputClassName?: string;
685
705
  /** What a cleared date stores. Default `""`, the empty ISO string; `null` for a
@@ -766,6 +786,417 @@ export function RhfDateField<
766
786
  );
767
787
  }
768
788
 
789
+ // ── date range ───────────────────────────────────────────────────────────────
790
+
791
+ type OwnDateRangeProps = Omit<
792
+ DateRangePickerProps,
793
+ "from" | "to" | "onChange" | "label" | "invalid" | "disabled" | "className" | "id" | "error"
794
+ >;
795
+
796
+ export type RhfDateRangePickerProps<
797
+ TFieldValues extends FieldValues = FieldValues,
798
+ TFromName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
799
+ TToName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
800
+ TTransformed = TFieldValues,
801
+ > = Omit<RhfFieldBaseProps<TFieldValues, TFromName, TTransformed>, "name" | "rules"> &
802
+ OwnDateRangeProps & {
803
+ /** The form field holding the range's first day, an ISO `"YYYY-MM-DD"`. */
804
+ fromName: TFromName;
805
+ /** The form field holding the range's last day. */
806
+ toName: TToName;
807
+ /** react-hook-form's `rules` for the start (`required`, a `validate` …). */
808
+ fromRules?: ControllerProps<TFieldValues, TFromName, TTransformed>["rules"];
809
+ /** react-hook-form's `rules` for the end — the place for "not before the start",
810
+ * which can read the start with `getValues`: the start is always written first. */
811
+ toRules?: ControllerProps<TFieldValues, TToName, TTransformed>["rules"];
812
+ /** Classes for the picker. Default `w-full`. */
813
+ inputClassName?: string;
814
+ /** What an empty end stores — a clear, or the half-made range between the first
815
+ * and second click. Default `""`, the empty ISO string; `null` for a nullable
816
+ * schema. */
817
+ emptyValue?: "" | null;
818
+ };
819
+
820
+ /**
821
+ * The kit's {@link DateRangePicker} over TWO form fields — `fromName` and `toName` —
822
+ * the way a period is stored: as two columns, each its own schema key, not one
823
+ * `{ from, to }` object (kastlan 4).
824
+ *
825
+ * ```tsx
826
+ * <RhfDateRangePicker fromName="periodFrom" toName="periodTo" label="Billing period"
827
+ * presets={presets} clearable />
828
+ * ```
829
+ *
830
+ * Why it is not {@link RhfField}: a `Controller` binds one name, and the range is
831
+ * two. Built by hand — an `RhfField` on the start and a second `useController` for
832
+ * the end, which is what kastlan's period fields needed — it works until the END is
833
+ * the one in error: the shell's label, message and `aria-invalid` all read the
834
+ * start's state alone, so "End before start" (a `refine` with `path: ["periodTo"]`,
835
+ * where such a rule goes) never shows. Here both states are read:
836
+ *
837
+ * - **Either field's error is shown and paints.** The label turns, the trigger is
838
+ * `aria-invalid` and wears the danger border when EITHER field has an error, and
839
+ * both messages are listed under the field (once, if the two say the same thing),
840
+ * each pointed at by the trigger's `aria-describedby`.
841
+ * - **One change writes both.** The picker reports the range whole, so the start and
842
+ * then the end are written on every change — the end too when only the start
843
+ * moved, so a cross-field rule on the end re-runs and an "End before start" clears
844
+ * the moment the start is fixed. A clear (`clearable`'s ×) empties both.
845
+ * - **Focus on error** lands on the trigger whichever of the two is in error.
846
+ * - **Touched** once a range is complete or cleared — not on the first click of a
847
+ * pick, which would mark (and, with `mode: "onTouched"`, validate) an end the user
848
+ * is still choosing.
849
+ *
850
+ * In `commit="immediate"` mode (the picker's default) the first calendar click is
851
+ * reported on its own, so the form holds `(from, emptyValue)` until the second; with
852
+ * `commit="apply"` nothing reaches the form until Apply.
853
+ *
854
+ * The common props (`label`, `hint`, `required`, `disabled`, `excludeWhenDisabled`,
855
+ * `control`, `className`) apply to the pair; `rules` are per field.
856
+ */
857
+ export function RhfDateRangePicker<
858
+ TFieldValues extends FieldValues = FieldValues,
859
+ TFromName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
860
+ TToName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
861
+ TTransformed = TFieldValues,
862
+ >({
863
+ fromName,
864
+ toName,
865
+ control,
866
+ fromRules,
867
+ toRules,
868
+ label,
869
+ hint,
870
+ required,
871
+ disabled,
872
+ excludeWhenDisabled,
873
+ className,
874
+ inputClassName,
875
+ emptyValue = "",
876
+ ...pickerProps
877
+ }: RhfDateRangePickerProps<TFieldValues, TFromName, TToName, TTransformed>) {
878
+ const rhfDisabled = excludeWhenDisabled ? disabled : undefined;
879
+ // The end, read here; the start is the FormField below, which gives the label, hint
880
+ // and ids their context. This component re-renders on the end's state, and with it
881
+ // the FormField's render.
882
+ const to = useController<TFieldValues, TToName, TTransformed>({
883
+ name: toName,
884
+ control,
885
+ rules: toRules,
886
+ disabled: rhfDisabled,
887
+ });
888
+ return (
889
+ <FormField
890
+ control={control}
891
+ name={fromName}
892
+ rules={fromRules}
893
+ disabled={rhfDisabled}
894
+ render={({ field, fieldState }) => (
895
+ <FormItem className={className}>
896
+ <RangeBody
897
+ from={field as unknown as ControllerRenderProps}
898
+ fromState={fieldState}
899
+ to={to.field as unknown as ControllerRenderProps}
900
+ toState={to.fieldState}
901
+ label={label}
902
+ hint={hint}
903
+ required={required}
904
+ disabled={Boolean(disabled)}
905
+ emptyValue={emptyValue}
906
+ inputClassName={inputClassName}
907
+ pickerProps={pickerProps}
908
+ />
909
+ </FormItem>
910
+ )}
911
+ />
912
+ );
913
+ }
914
+
915
+ function RangeBody({
916
+ from,
917
+ fromState,
918
+ to,
919
+ toState,
920
+ label,
921
+ hint,
922
+ required,
923
+ disabled,
924
+ emptyValue,
925
+ inputClassName,
926
+ pickerProps,
927
+ }: {
928
+ from: ControllerRenderProps;
929
+ fromState: ControllerFieldState;
930
+ to: ControllerRenderProps;
931
+ toState: ControllerFieldState;
932
+ label: ReactNode;
933
+ hint: ReactNode;
934
+ required?: boolean;
935
+ disabled: boolean;
936
+ emptyValue: "" | null;
937
+ inputClassName?: string;
938
+ pickerProps: OwnDateRangeProps;
939
+ }) {
940
+ const { id, formItemId, formMessageId, describedBy } = useFormField();
941
+ // The trigger forwards no ref; both fields focus it, so `shouldFocusError` lands
942
+ // there whichever of the two is first in error.
943
+ useFocusHandle(from.ref, () => document.getElementById(formItemId));
944
+ useFocusHandle(to.ref, () => document.getElementById(formItemId));
945
+ const invalid = fromState.invalid || toState.invalid;
946
+ const messages = [fromState.error?.message, toState.error?.message].filter(
947
+ (m, i, all): m is string => typeof m === "string" && m !== "" && all.indexOf(m) === i,
948
+ );
949
+ const messageIds = messages.map((_, i) => `${formMessageId}-${i}`);
950
+ const fromValue: unknown = from.value;
951
+ const toValue: unknown = to.value;
952
+ return (
953
+ <>
954
+ {hasContent(label) && (
955
+ // FormLabel colours from the START's error alone (its context is that field);
956
+ // the pair's state says it here.
957
+ <FormLabel id={`${id}-label`} required={required} data-error={invalid || undefined}>
958
+ {label}
959
+ </FormLabel>
960
+ )}
961
+ <DateRangePicker
962
+ {...pickerProps}
963
+ id={formItemId}
964
+ // Merged, never replaced — a caller's own reference first, as FormControl does.
965
+ aria-describedby={
966
+ [pickerProps["aria-describedby"], describedBy, ...messageIds].filter(Boolean).join(" ") || undefined
967
+ }
968
+ aria-invalid={invalid || undefined}
969
+ aria-required={required || undefined}
970
+ from={typeof fromValue === "string" ? fromValue : ""}
971
+ to={typeof toValue === "string" ? toValue : ""}
972
+ onChange={(f, t) => {
973
+ from.onChange(f === "" ? emptyValue : f);
974
+ to.onChange(t === "" ? emptyValue : t);
975
+ if (t !== "" || f === "") {
976
+ from.onBlur();
977
+ to.onBlur();
978
+ }
979
+ }}
980
+ disabled={disabled || from.disabled || to.disabled}
981
+ invalid={invalid}
982
+ className={inputClassName ?? "w-full"}
983
+ />
984
+ {hasContent(hint) && <FormDescription>{hint}</FormDescription>}
985
+ {messages.map((message, i) => (
986
+ // FormMessage's look and its rule: no `role="alert"` — read when focus reaches
987
+ // the trigger, which names these in its description.
988
+ <p
989
+ key={message}
990
+ id={messageIds[i]}
991
+ data-slot="form-message"
992
+ className="text-[11px] leading-tight text-[var(--danger)]"
993
+ >
994
+ {message}
995
+ </p>
996
+ ))}
997
+ </>
998
+ );
999
+ }
1000
+
1001
+ // ── time ─────────────────────────────────────────────────────────────────────
1002
+
1003
+ export type RhfTimeInputProps<
1004
+ TFieldValues extends FieldValues = FieldValues,
1005
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1006
+ TTransformed = TFieldValues,
1007
+ > = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
1008
+ Omit<
1009
+ TimeInputProps,
1010
+ | "name"
1011
+ | "value"
1012
+ | "defaultValue"
1013
+ | "onChange"
1014
+ | "onValueChange"
1015
+ | "onBlur"
1016
+ | "label"
1017
+ | "error"
1018
+ | "invalid"
1019
+ | "className"
1020
+ | "disabled"
1021
+ | "required"
1022
+ > & {
1023
+ /** What a cleared time stores. Default `""`; `null` for a nullable schema. */
1024
+ emptyValue?: "" | null;
1025
+ };
1026
+
1027
+ /**
1028
+ * The kit's {@link TimeInput}: a time of day as `"HH:mm"` (`"HH:mm:ss"` when `step`
1029
+ * asks for seconds), normalised before it reaches the form, so the stored value
1030
+ * compares and sorts as a string. kastlan 4: the owner meeting's time was an
1031
+ * {@link RhfField} with a hand-wired `TimeInput` in its render — `ref`, `name`, the
1032
+ * `?? ""`, `onValueChange`, `onBlur`, `invalid` — the seven lines every bound field
1033
+ * folds into one.
1034
+ *
1035
+ * `min` / `max` paint an out-of-window time (see TimeInput) but do not fail the form:
1036
+ * validation stays the schema's or `rules`'. `rules={{ validate: (v) =>
1037
+ * isTimeInRange(v, min, max) || "…" }}` says it with the same window.
1038
+ */
1039
+ export function RhfTimeInput<
1040
+ TFieldValues extends FieldValues = FieldValues,
1041
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1042
+ TTransformed = TFieldValues,
1043
+ >({
1044
+ name,
1045
+ control,
1046
+ rules,
1047
+ label,
1048
+ hint,
1049
+ required,
1050
+ disabled,
1051
+ excludeWhenDisabled,
1052
+ className,
1053
+ emptyValue = "",
1054
+ ...timeProps
1055
+ }: RhfTimeInputProps<TFieldValues, TName, TTransformed>) {
1056
+ return (
1057
+ <RhfField
1058
+ {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1059
+ render={({ field, invalid }) => {
1060
+ const value: unknown = field.value;
1061
+ return (
1062
+ <TimeInput
1063
+ aria-required={required || undefined}
1064
+ {...timeProps}
1065
+ name={field.name}
1066
+ ref={field.ref}
1067
+ value={typeof value === "string" ? value : ""}
1068
+ onValueChange={(v) => field.onChange(v === "" ? emptyValue : v)}
1069
+ onBlur={field.onBlur}
1070
+ disabled={field.disabled}
1071
+ invalid={invalid}
1072
+ />
1073
+ );
1074
+ }}
1075
+ />
1076
+ );
1077
+ }
1078
+
1079
+ // ── IBAN and phone (0.22) ────────────────────────────────────────────────────
1080
+
1081
+ type OwnTextishProps<P> = Omit<
1082
+ P,
1083
+ | "name"
1084
+ | "value"
1085
+ | "defaultValue"
1086
+ | "onChange"
1087
+ | "onValueChange"
1088
+ | "onBlur"
1089
+ | "label"
1090
+ | "error"
1091
+ | "invalid"
1092
+ | "className"
1093
+ | "disabled"
1094
+ | "required"
1095
+ >;
1096
+
1097
+ export type RhfIbanInputProps<
1098
+ TFieldValues extends FieldValues = FieldValues,
1099
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1100
+ TTransformed = TFieldValues,
1101
+ > = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnTextishProps<IbanInputProps>;
1102
+
1103
+ /**
1104
+ * {@link IbanInput} bound to one field, which stores the COMPACT upper-case IBAN (what
1105
+ * kastlan's server checks and its QR bill prints). The field's own checksum and
1106
+ * `kind` messages show while the form has nothing to say; a form error (the schema's
1107
+ * or `rules`') replaces them, as a caller's `error` does on the bare field. kastlan
1108
+ * asked for the field; it binds everything through react-hook-form.
1109
+ */
1110
+ export function RhfIbanInput<
1111
+ TFieldValues extends FieldValues = FieldValues,
1112
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1113
+ TTransformed = TFieldValues,
1114
+ >({
1115
+ name,
1116
+ control,
1117
+ rules,
1118
+ label,
1119
+ hint,
1120
+ required,
1121
+ disabled,
1122
+ excludeWhenDisabled,
1123
+ className,
1124
+ ...ibanProps
1125
+ }: RhfIbanInputProps<TFieldValues, TName, TTransformed>) {
1126
+ return (
1127
+ <RhfField
1128
+ {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1129
+ render={({ field, invalid }) => {
1130
+ const value: unknown = field.value;
1131
+ return (
1132
+ <IbanInput
1133
+ aria-required={required || undefined}
1134
+ {...ibanProps}
1135
+ name={field.name}
1136
+ ref={field.ref}
1137
+ value={typeof value === "string" ? value : ""}
1138
+ onValueChange={field.onChange}
1139
+ onBlur={field.onBlur}
1140
+ disabled={field.disabled}
1141
+ invalid={invalid}
1142
+ />
1143
+ );
1144
+ }}
1145
+ />
1146
+ );
1147
+ }
1148
+
1149
+ export type RhfPhoneInputProps<
1150
+ TFieldValues extends FieldValues = FieldValues,
1151
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1152
+ TTransformed = TFieldValues,
1153
+ > = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnTextishProps<PhoneInputProps>;
1154
+
1155
+ /**
1156
+ * {@link PhoneInput} bound to one field, which stores E.164 when the number reads as
1157
+ * one and the typed text unchanged otherwise — kastlan's contacts keep their old free
1158
+ * text until someone edits it. Validation (required, a pattern for E.164 only) is the
1159
+ * schema's or `rules`'.
1160
+ */
1161
+ export function RhfPhoneInput<
1162
+ TFieldValues extends FieldValues = FieldValues,
1163
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1164
+ TTransformed = TFieldValues,
1165
+ >({
1166
+ name,
1167
+ control,
1168
+ rules,
1169
+ label,
1170
+ hint,
1171
+ required,
1172
+ disabled,
1173
+ excludeWhenDisabled,
1174
+ className,
1175
+ ...phoneProps
1176
+ }: RhfPhoneInputProps<TFieldValues, TName, TTransformed>) {
1177
+ return (
1178
+ <RhfField
1179
+ {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1180
+ render={({ field, invalid }) => {
1181
+ const value: unknown = field.value;
1182
+ return (
1183
+ <PhoneInput
1184
+ aria-required={required || undefined}
1185
+ {...phoneProps}
1186
+ name={field.name}
1187
+ ref={field.ref}
1188
+ value={typeof value === "string" ? value : ""}
1189
+ onValueChange={field.onChange}
1190
+ onBlur={field.onBlur}
1191
+ disabled={field.disabled}
1192
+ invalid={invalid}
1193
+ />
1194
+ );
1195
+ }}
1196
+ />
1197
+ );
1198
+ }
1199
+
769
1200
  // ── select ───────────────────────────────────────────────────────────────────
770
1201
 
771
1202
  export interface RhfSelectOption {
@@ -900,6 +1331,172 @@ export function RhfCheckbox<
900
1331
  );
901
1332
  }
902
1333
 
1334
+ // ── toggle group ─────────────────────────────────────────────────────────────
1335
+
1336
+ /** The two shapes a {@link ToggleGroup} comes in, carried over: `allowEmpty` picks
1337
+ * whether a press on the chosen option clears it, and types the caption to match. */
1338
+ type RhfToggleGroupMode<T extends string> =
1339
+ | {
1340
+ /** One option is always the answer (the default): a press on the chosen
1341
+ * option re-sends it. */
1342
+ allowEmpty?: false;
1343
+ semantics?: ToggleGroupRequiredProps<T>["semantics"];
1344
+ caption?: ToggleGroupRequiredProps<T>["caption"];
1345
+ emptyValue?: undefined;
1346
+ }
1347
+ | {
1348
+ /** A press on the chosen option clears it, and the form stores `emptyValue`. */
1349
+ allowEmpty: true;
1350
+ semantics?: ToggleGroupClearableProps<T>["semantics"];
1351
+ caption?: ToggleGroupClearableProps<T>["caption"];
1352
+ /** What a cleared group stores. Default `null`; `""` for a schema that spells
1353
+ * "no choice" as an empty string. */
1354
+ emptyValue?: null | "";
1355
+ };
1356
+
1357
+ export type RhfToggleGroupProps<
1358
+ TFieldValues extends FieldValues = FieldValues,
1359
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1360
+ T extends string = string,
1361
+ TTransformed = TFieldValues,
1362
+ > = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &
1363
+ Pick<
1364
+ ToggleGroupBaseProps<T>,
1365
+ "options" | "optionClassName" | "overflow" | "size" | "aria-label" | "commit" | "disabledReason"
1366
+ > & {
1367
+ /** Classes for the group itself — `className` is the item's box. */
1368
+ groupClassName?: string;
1369
+ } & RhfToggleGroupMode<T>;
1370
+
1371
+ /**
1372
+ * The kit's {@link ToggleGroup} bound to one field: a choice between a few answers
1373
+ * ("Monthly / Quarterly / Yearly"), stored as the chosen option's `value`. kastlan 4
1374
+ * asked for it with {@link RhfTimeInput} and {@link RhfDateRangePicker}: the controls
1375
+ * its forms still had to wire by hand through {@link RhfField}'s render.
1376
+ *
1377
+ * The label is the form's {@link FormLabel} above the bare group, as on every other
1378
+ * bound field — the `labelPlacement="above"` shape, not the group's own field chrome —
1379
+ * and it names the group (`aria-labelledby`); the hint and the message describe it,
1380
+ * and an error paints the group's border. Focus on error goes to the option a
1381
+ * keyboard user would land on (the chosen one, else the first); touched is set when
1382
+ * focus leaves the group, not when it moves between options.
1383
+ *
1384
+ * Required by default, as the group is: a press on the chosen option keeps it. With
1385
+ * `allowEmpty`, a second press clears the choice and the form stores `emptyValue`
1386
+ * (`null` by default) — the options then become toggle buttons, see ToggleGroup. A
1387
+ * value that matches no option (an empty default) leaves nothing pressed; that the
1388
+ * field still needs answering is the schema's or `rules`' to say.
1389
+ */
1390
+ export function RhfToggleGroup<
1391
+ TFieldValues extends FieldValues = FieldValues,
1392
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
1393
+ T extends string = string,
1394
+ TTransformed = TFieldValues,
1395
+ >({
1396
+ name,
1397
+ control,
1398
+ rules,
1399
+ label,
1400
+ hint,
1401
+ required,
1402
+ disabled,
1403
+ excludeWhenDisabled,
1404
+ className,
1405
+ ...groupProps
1406
+ }: RhfToggleGroupProps<TFieldValues, TName, T, TTransformed>) {
1407
+ const labelled = hasContent(label);
1408
+ return (
1409
+ <RhfField
1410
+ {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
1411
+ asControl={false}
1412
+ render={({ field, invalid, id, labelId, describedBy }) => (
1413
+ <ToggleControl<T>
1414
+ field={field as unknown as ControllerRenderProps}
1415
+ invalid={invalid}
1416
+ required={required}
1417
+ id={id}
1418
+ labelledBy={labelled ? labelId : undefined}
1419
+ describedBy={describedBy}
1420
+ groupProps={groupProps}
1421
+ />
1422
+ )}
1423
+ />
1424
+ );
1425
+ }
1426
+
1427
+ function ToggleControl<T extends string>({
1428
+ field,
1429
+ invalid,
1430
+ required,
1431
+ id,
1432
+ labelledBy,
1433
+ describedBy,
1434
+ groupProps,
1435
+ }: {
1436
+ field: ControllerRenderProps;
1437
+ invalid: boolean;
1438
+ required?: boolean;
1439
+ id: string;
1440
+ labelledBy: string | undefined;
1441
+ describedBy: string | undefined;
1442
+ groupProps: Omit<RhfToggleGroupProps<FieldValues, string, T>, keyof RhfFieldBaseProps>;
1443
+ }) {
1444
+ const { groupClassName, emptyValue, allowEmpty, semantics, caption, ...rest } = groupProps;
1445
+ // ToggleGroup forwards no ref. Its options are buttons: the radio shape's one tab
1446
+ // stop (`tabindex="0"`), or in the pressed shape the pressed one — else the first.
1447
+ useFocusHandle(field.ref, () => {
1448
+ const group = document.getElementById(id);
1449
+ return (
1450
+ group?.querySelector<HTMLElement>('button[tabindex="0"], button[aria-pressed="true"]') ??
1451
+ group?.querySelector<HTMLElement>("button")
1452
+ );
1453
+ });
1454
+ // React's onBlur is focusout: it fires on every move between options as well, and
1455
+ // only leaving the group is a blur of the FIELD.
1456
+ const onBlur = (e: FocusEvent<HTMLDivElement>) => {
1457
+ if (!e.currentTarget.contains(e.relatedTarget as Node | null)) field.onBlur();
1458
+ };
1459
+ const raw: unknown = field.value;
1460
+ const value = raw === null || raw === undefined || raw === "" ? null : (String(raw) as T);
1461
+ const common = {
1462
+ ...rest,
1463
+ id,
1464
+ className: groupClassName,
1465
+ "aria-labelledby": labelledBy,
1466
+ "aria-describedby": describedBy,
1467
+ "aria-invalid": invalid || undefined,
1468
+ disabled: field.disabled,
1469
+ onBlur,
1470
+ };
1471
+ // The props' union pairs `allowEmpty` with its caption's type; taken apart above it no
1472
+ // longer narrows, so each branch says which half it is.
1473
+ if (allowEmpty) {
1474
+ return (
1475
+ <ToggleGroup<T>
1476
+ {...common}
1477
+ allowEmpty
1478
+ caption={caption as ToggleGroupClearableProps<T>["caption"]}
1479
+ value={value}
1480
+ onChange={(v) => field.onChange(v ?? emptyValue ?? null)}
1481
+ />
1482
+ );
1483
+ }
1484
+ return (
1485
+ <ToggleGroup<T>
1486
+ {...common}
1487
+ allowEmpty={false}
1488
+ semantics={semantics}
1489
+ caption={caption as ToggleGroupRequiredProps<T>["caption"]}
1490
+ // A radiogroup takes `aria-required`; a group of toggle buttons does not.
1491
+ aria-required={required && semantics !== "pressed" ? true : undefined}
1492
+ // An empty default matches no option: nothing pressed, the first option the tab
1493
+ // stop.
1494
+ value={(value ?? "") as T}
1495
+ onChange={(v) => field.onChange(v)}
1496
+ />
1497
+ );
1498
+ }
1499
+
903
1500
  // ── comboboxes ───────────────────────────────────────────────────────────────
904
1501
 
905
1502
  export type RhfComboboxProps<
package/src/rhf.ts CHANGED
@@ -10,7 +10,8 @@ export * from "./rhf/form";
10
10
  // The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).
11
11
  export * from "./rhf/use-rhf-wizard-step";
12
12
  // Bound fields — one line per field (RhfTextField, RhfNumberField, RhfIntegerField, RhfMoneyField,
13
- // RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox)
13
+ // RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox; 0.22: RhfTimeInput,
14
+ // RhfDateRangePicker, RhfToggleGroup, RhfIbanInput, RhfPhoneInput)
14
15
  // and RhfField, the shell they are built on.
15
16
  export * from "./rhf/fields";
16
17
  // LineItems over useFieldArray.