@eifi1/ui-kit 0.11.0 → 0.12.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 (342) hide show
  1. package/README.md +57 -32
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/amount-input.d.ts +24 -4
  12. package/dist/components/amount-input.js +5 -2
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/button-group.d.ts +51 -2
  21. package/dist/components/button-group.js +5 -1
  22. package/dist/components/button-group.js.map +1 -1
  23. package/dist/components/calculator.d.ts +15 -4
  24. package/dist/components/calendar-heatmap.d.ts +41 -3
  25. package/dist/components/calendar-heatmap.js +48 -7
  26. package/dist/components/calendar-heatmap.js.map +1 -1
  27. package/dist/components/chip.d.ts +2 -1
  28. package/dist/components/chip.js +6 -3
  29. package/dist/components/chip.js.map +1 -1
  30. package/dist/components/choice-card.d.ts +35 -3
  31. package/dist/components/choice-card.js +83 -25
  32. package/dist/components/choice-card.js.map +1 -1
  33. package/dist/components/combobox-core.js +2 -0
  34. package/dist/components/combobox-core.js.map +1 -1
  35. package/dist/components/combobox.d.ts +2 -2
  36. package/dist/components/combobox.js +31 -9
  37. package/dist/components/combobox.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +51 -70
  39. package/dist/components/copy-button.js +1 -1
  40. package/dist/components/copy-button.js.map +1 -1
  41. package/dist/components/data-table-cells.d.ts +63 -0
  42. package/dist/components/data-table-cells.js +64 -0
  43. package/dist/components/data-table-cells.js.map +1 -0
  44. package/dist/components/data-table-filter-popover.d.ts +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +31 -1
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +13 -0
  49. package/dist/components/data-table-labels.js +8 -1
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table.d.ts +1 -1
  52. package/dist/components/data-table.js +102 -12
  53. package/dist/components/data-table.js.map +1 -1
  54. package/dist/components/date-picker.d.ts +3 -1
  55. package/dist/components/date-picker.js +15 -2
  56. package/dist/components/date-picker.js.map +1 -1
  57. package/dist/components/description-list.d.ts +23 -3
  58. package/dist/components/description-list.js +23 -3
  59. package/dist/components/description-list.js.map +1 -1
  60. package/dist/components/dialog-frame.d.ts +1 -0
  61. package/dist/components/entity-combobox.d.ts +1 -1
  62. package/dist/components/entity-combobox.js +18 -3
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/error-boundary.d.ts +92 -0
  65. package/dist/components/error-boundary.js +111 -0
  66. package/dist/components/error-boundary.js.map +1 -0
  67. package/dist/components/field.d.ts +30 -4
  68. package/dist/components/field.js +40 -17
  69. package/dist/components/field.js.map +1 -1
  70. package/dist/components/file-button.d.ts +50 -208
  71. package/dist/components/file-dropzone.d.ts +49 -2
  72. package/dist/components/floating-panel.d.ts +13 -4
  73. package/dist/components/floating-panel.js +7 -3
  74. package/dist/components/floating-panel.js.map +1 -1
  75. package/dist/components/form-actions.d.ts +53 -0
  76. package/dist/components/form-actions.js +98 -0
  77. package/dist/components/form-actions.js.map +1 -0
  78. package/dist/components/image-grid.d.ts +67 -0
  79. package/dist/components/image-grid.js +112 -0
  80. package/dist/components/image-grid.js.map +1 -0
  81. package/dist/components/lightbox.d.ts +104 -0
  82. package/dist/components/lightbox.js +229 -0
  83. package/dist/components/lightbox.js.map +1 -0
  84. package/dist/components/line-items.d.ts +114 -0
  85. package/dist/components/line-items.js +238 -0
  86. package/dist/components/line-items.js.map +1 -0
  87. package/dist/components/list.d.ts +23 -2
  88. package/dist/components/list.js +32 -13
  89. package/dist/components/list.js.map +1 -1
  90. package/dist/components/loading-state.d.ts +24 -0
  91. package/dist/components/loading-state.js +32 -0
  92. package/dist/components/loading-state.js.map +1 -0
  93. package/dist/components/menu-item.d.ts +2 -1
  94. package/dist/components/menu-item.js +5 -1
  95. package/dist/components/menu-item.js.map +1 -1
  96. package/dist/components/mini-calendar.d.ts +13 -1
  97. package/dist/components/mini-calendar.js +46 -2
  98. package/dist/components/mini-calendar.js.map +1 -1
  99. package/dist/components/modal.d.ts +20 -1
  100. package/dist/components/modal.js +26 -2
  101. package/dist/components/modal.js.map +1 -1
  102. package/dist/components/month-picker.d.ts +21 -1
  103. package/dist/components/month-picker.js +139 -26
  104. package/dist/components/month-picker.js.map +1 -1
  105. package/dist/components/multi-entity-combobox.d.ts +1 -1
  106. package/dist/components/multi-entity-combobox.js +19 -3
  107. package/dist/components/multi-entity-combobox.js.map +1 -1
  108. package/dist/components/nav-pills.d.ts +2 -1
  109. package/dist/components/nav-pills.js +5 -1
  110. package/dist/components/nav-pills.js.map +1 -1
  111. package/dist/components/number-field.d.ts +24 -5
  112. package/dist/components/number-field.js +5 -7
  113. package/dist/components/number-field.js.map +1 -1
  114. package/dist/components/number-input.d.ts +25 -5
  115. package/dist/components/number-input.js +7 -1
  116. package/dist/components/number-input.js.map +1 -1
  117. package/dist/components/numpad-sheet.d.ts +15 -4
  118. package/dist/components/passkeys-setting.d.ts +67 -0
  119. package/dist/components/passkeys-setting.js +214 -0
  120. package/dist/components/passkeys-setting.js.map +1 -0
  121. package/dist/components/picker-sheet.js +2 -1
  122. package/dist/components/picker-sheet.js.map +1 -1
  123. package/dist/components/pie-chart-labels.d.ts +20 -0
  124. package/dist/components/pie-chart-labels.js +12 -0
  125. package/dist/components/pie-chart-labels.js.map +1 -0
  126. package/dist/components/pie-chart.d.ts +101 -0
  127. package/dist/components/pie-chart.js +283 -0
  128. package/dist/components/pie-chart.js.map +1 -0
  129. package/dist/components/progress-bar.d.ts +44 -3
  130. package/dist/components/progress-bar.js +58 -22
  131. package/dist/components/progress-bar.js.map +1 -1
  132. package/dist/components/qr-code.d.ts +34 -0
  133. package/dist/components/qr-code.js +57 -0
  134. package/dist/components/qr-code.js.map +1 -0
  135. package/dist/components/search-field.d.ts +15 -3
  136. package/dist/components/search-field.js +5 -1
  137. package/dist/components/search-field.js.map +1 -1
  138. package/dist/components/settings-fields.d.ts +49 -1
  139. package/dist/components/signed-amount.d.ts +121 -0
  140. package/dist/components/signed-amount.js +152 -0
  141. package/dist/components/signed-amount.js.map +1 -0
  142. package/dist/components/skeleton.d.ts +10 -3
  143. package/dist/components/skeleton.js +12 -2
  144. package/dist/components/skeleton.js.map +1 -1
  145. package/dist/components/stat-tile.d.ts +2 -1
  146. package/dist/components/stat-tile.js +5 -2
  147. package/dist/components/stat-tile.js.map +1 -1
  148. package/dist/components/table.d.ts +39 -5
  149. package/dist/components/table.js +45 -11
  150. package/dist/components/table.js.map +1 -1
  151. package/dist/components/text-link.d.ts +176 -0
  152. package/dist/components/text-link.js +109 -0
  153. package/dist/components/text-link.js.map +1 -0
  154. package/dist/components/time-input.d.ts +49 -1
  155. package/dist/components/toggle-group.d.ts +13 -0
  156. package/dist/components/toggle-group.js +35 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +36 -3
  159. package/dist/components/toggle-legend.js +49 -0
  160. package/dist/components/toggle-legend.js.map +1 -1
  161. package/dist/components/ui.d.ts +51 -779
  162. package/dist/components/ui.js +215 -23
  163. package/dist/components/ui.js.map +1 -1
  164. package/dist/components/use-table-state.d.ts +3 -52
  165. package/dist/components/use-table-state.js +128 -30
  166. package/dist/components/use-table-state.js.map +1 -1
  167. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  168. package/dist/data-table.d.ts +2 -1
  169. package/dist/data-table.js +5 -1
  170. package/dist/data-table.js.map +1 -1
  171. package/dist/feedback/feedback-dialog.d.ts +26 -3
  172. package/dist/feedback/feedback-dialog.js +16 -2
  173. package/dist/feedback/feedback-dialog.js.map +1 -1
  174. package/dist/feedback/feedback-thread.d.ts +133 -0
  175. package/dist/feedback/feedback-thread.js +237 -0
  176. package/dist/feedback/feedback-thread.js.map +1 -0
  177. package/dist/feedback.d.ts +2 -1
  178. package/dist/feedback.js +1 -0
  179. package/dist/feedback.js.map +1 -1
  180. package/dist/hooks/use-authed-src.d.ts +71 -0
  181. package/dist/hooks/use-authed-src.js +63 -0
  182. package/dist/hooks/use-authed-src.js.map +1 -0
  183. package/dist/hooks/use-file-drop.d.ts +49 -2
  184. package/dist/hooks/use-hotkey.d.ts +59 -0
  185. package/dist/hooks/use-hotkey.js +105 -0
  186. package/dist/hooks/use-hotkey.js.map +1 -0
  187. package/dist/hooks/use-search-param-state.d.ts +83 -0
  188. package/dist/hooks/use-search-param-state.js +94 -0
  189. package/dist/hooks/use-search-param-state.js.map +1 -0
  190. package/dist/i18n/defaults.d.ts +15 -4
  191. package/dist/i18n/defaults.js +31 -1
  192. package/dist/i18n/defaults.js.map +1 -1
  193. package/dist/i18n/kit-labels.d.ts +46 -312
  194. package/dist/i18n/kit-labels.js +20 -5
  195. package/dist/i18n/kit-labels.js.map +1 -1
  196. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  197. package/dist/i18n/locales/de-CH.d.ts +15 -4
  198. package/dist/i18n/locales/de-informal.d.ts +15 -4
  199. package/dist/i18n/locales/de.d.ts +15 -4
  200. package/dist/i18n/locales/de.js +156 -3
  201. package/dist/i18n/locales/de.js.map +1 -1
  202. package/dist/i18n/locales/es.d.ts +15 -4
  203. package/dist/i18n/locales/es.js +153 -3
  204. package/dist/i18n/locales/es.js.map +1 -1
  205. package/dist/i18n/locales/fr.d.ts +15 -4
  206. package/dist/i18n/locales/fr.js +153 -3
  207. package/dist/i18n/locales/fr.js.map +1 -1
  208. package/dist/i18n/locales/hu.d.ts +15 -4
  209. package/dist/i18n/locales/hu.js +153 -3
  210. package/dist/i18n/locales/hu.js.map +1 -1
  211. package/dist/i18n/locales/it.d.ts +15 -4
  212. package/dist/i18n/locales/it.js +153 -3
  213. package/dist/i18n/locales/it.js.map +1 -1
  214. package/dist/i18n/locales/zh.d.ts +15 -4
  215. package/dist/i18n/locales/zh.js +153 -3
  216. package/dist/i18n/locales/zh.js.map +1 -1
  217. package/dist/index.d.ts +37 -17
  218. package/dist/index.js +40 -1
  219. package/dist/index.js.map +1 -1
  220. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  221. package/dist/lib/format.d.ts +144 -0
  222. package/dist/lib/format.js +149 -0
  223. package/dist/lib/format.js.map +1 -0
  224. package/dist/lib/qr-encode.d.ts +44 -0
  225. package/dist/lib/qr-encode.js +338 -0
  226. package/dist/lib/qr-encode.js.map +1 -0
  227. package/dist/rhf/fields.d.ts +241 -0
  228. package/dist/rhf/fields.js +601 -0
  229. package/dist/rhf/fields.js.map +1 -0
  230. package/dist/rhf/form.d.ts +49 -1
  231. package/dist/rhf/line-items.d.ts +31 -0
  232. package/dist/rhf/line-items.js +59 -0
  233. package/dist/rhf/line-items.js.map +1 -0
  234. package/dist/rhf.d.ts +61 -1
  235. package/dist/rhf.js +2 -0
  236. package/dist/rhf.js.map +1 -1
  237. package/dist/shell/app-shell.d.ts +99 -2
  238. package/dist/shell/app-shell.js +58 -3
  239. package/dist/shell/app-shell.js.map +1 -1
  240. package/dist/shell/auth-layout.d.ts +74 -0
  241. package/dist/shell/auth-layout.js +97 -0
  242. package/dist/shell/auth-layout.js.map +1 -0
  243. package/dist/shell/top-bar-brand.d.ts +81 -0
  244. package/dist/shell/top-bar-brand.js +47 -0
  245. package/dist/shell/top-bar-brand.js.map +1 -0
  246. package/dist/shell/topbar-action-menu.d.ts +12 -1
  247. package/dist/shell/topbar-action-menu.js +16 -1
  248. package/dist/shell/topbar-action-menu.js.map +1 -1
  249. package/dist/shell.d.ts +56 -1
  250. package/dist/shell.js +2 -0
  251. package/dist/shell.js.map +1 -1
  252. package/dist/wizard/stepper-nav.d.ts +48 -1
  253. package/dist/wizard/wizard-context.d.ts +24 -1
  254. package/dist/wizard/wizard-context.js +19 -2
  255. package/dist/wizard/wizard-context.js.map +1 -1
  256. package/dist/wizard/wizard-step.d.ts +20 -5
  257. package/dist/wizard/wizard-step.js +16 -3
  258. package/dist/wizard/wizard-step.js.map +1 -1
  259. package/dist/wizard.d.ts +50 -3
  260. package/package.json +1 -1
  261. package/src/chart.ts +4 -0
  262. package/src/components/account-settings-labels.ts +190 -0
  263. package/src/components/account-settings.tsx +110 -63
  264. package/src/components/amount-input.tsx +13 -1
  265. package/src/components/authed-image.tsx +190 -0
  266. package/src/components/breadcrumbs.tsx +8 -2
  267. package/src/components/button-group.tsx +7 -2
  268. package/src/components/calendar-heatmap.tsx +105 -10
  269. package/src/components/chip.tsx +8 -4
  270. package/src/components/choice-card.tsx +136 -25
  271. package/src/components/combobox-core.tsx +5 -0
  272. package/src/components/combobox.tsx +31 -9
  273. package/src/components/copy-button.tsx +3 -2
  274. package/src/components/data-table-cells.tsx +119 -0
  275. package/src/components/data-table-filters.ts +77 -0
  276. package/src/components/data-table-labels.ts +23 -0
  277. package/src/components/data-table.tsx +224 -13
  278. package/src/components/date-picker.tsx +19 -3
  279. package/src/components/description-list.tsx +52 -3
  280. package/src/components/entity-combobox.tsx +18 -3
  281. package/src/components/error-boundary.tsx +216 -0
  282. package/src/components/field.tsx +56 -5
  283. package/src/components/floating-panel.tsx +20 -7
  284. package/src/components/form-actions.tsx +191 -0
  285. package/src/components/image-grid.tsx +181 -0
  286. package/src/components/lightbox.tsx +374 -0
  287. package/src/components/line-items.tsx +385 -0
  288. package/src/components/list.tsx +65 -16
  289. package/src/components/loading-state.tsx +47 -0
  290. package/src/components/menu-item.tsx +7 -2
  291. package/src/components/mini-calendar.tsx +72 -3
  292. package/src/components/modal.tsx +61 -3
  293. package/src/components/month-picker.tsx +181 -26
  294. package/src/components/multi-entity-combobox.tsx +19 -3
  295. package/src/components/nav-pills.tsx +8 -1
  296. package/src/components/number-field.tsx +15 -10
  297. package/src/components/number-input.tsx +16 -1
  298. package/src/components/passkeys-setting.tsx +331 -0
  299. package/src/components/picker-sheet.tsx +7 -1
  300. package/src/components/pie-chart-labels.ts +32 -0
  301. package/src/components/pie-chart.tsx +479 -0
  302. package/src/components/progress-bar.tsx +129 -19
  303. package/src/components/qr-code.tsx +83 -0
  304. package/src/components/search-field.tsx +20 -4
  305. package/src/components/signed-amount.tsx +286 -0
  306. package/src/components/skeleton.tsx +21 -3
  307. package/src/components/stat-tile.tsx +8 -2
  308. package/src/components/table.tsx +102 -13
  309. package/src/components/text-link.tsx +248 -0
  310. package/src/components/toggle-group.tsx +58 -10
  311. package/src/components/toggle-legend.tsx +87 -2
  312. package/src/components/ui.tsx +426 -36
  313. package/src/components/use-table-state.ts +265 -33
  314. package/src/data-table.ts +10 -0
  315. package/src/feedback/feedback-dialog.tsx +42 -3
  316. package/src/feedback/feedback-thread.tsx +434 -0
  317. package/src/feedback.ts +1 -0
  318. package/src/hooks/use-authed-src.ts +161 -0
  319. package/src/hooks/use-hotkey.ts +172 -0
  320. package/src/hooks/use-search-param-state.ts +187 -0
  321. package/src/i18n/defaults.ts +30 -0
  322. package/src/i18n/kit-labels.tsx +102 -8
  323. package/src/i18n/locales/de.ts +153 -0
  324. package/src/i18n/locales/es.ts +150 -0
  325. package/src/i18n/locales/fr.ts +153 -0
  326. package/src/i18n/locales/hu.ts +151 -0
  327. package/src/i18n/locales/it.ts +151 -0
  328. package/src/i18n/locales/zh.ts +150 -0
  329. package/src/index.ts +59 -0
  330. package/src/lib/format.ts +325 -0
  331. package/src/lib/qr-encode.ts +420 -0
  332. package/src/rhf/fields.tsx +1054 -0
  333. package/src/rhf/line-items.tsx +130 -0
  334. package/src/rhf.ts +6 -0
  335. package/src/shell/app-shell.tsx +147 -2
  336. package/src/shell/auth-layout.tsx +190 -0
  337. package/src/shell/top-bar-brand.tsx +76 -0
  338. package/src/shell/topbar-action-menu.tsx +21 -0
  339. package/src/shell.ts +2 -0
  340. package/src/wizard/wizard-context.tsx +41 -1
  341. package/src/wizard/wizard-step.tsx +43 -6
  342. package/tokens.css +12 -0
@@ -68,6 +68,6 @@ interface EntityComboboxProps<V extends string | number, C extends ComboClearVal
68
68
  * the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking
69
69
  * several entities use {@link MultiEntityCombobox}.
70
70
  */
71
- declare function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({ value, onChange, clearValue, options, loadOptions, loading, label, placeholder, searchPlaceholder, emptyLabel, clearable, clearLabel, closeLabel, disabled, onCreate, createLabel, className, invalid, error, filter, minChars, debounceMs, loadErrorLabel, "aria-label": ariaLabel, ...rest }: EntityComboboxProps<V, C>): react.JSX.Element;
71
+ declare function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({ value, onChange, clearValue, options, loadOptions, loading, label, placeholder, searchPlaceholder, emptyLabel, clearable, clearLabel, closeLabel, disabled, onCreate, createLabel, className, invalid, error, filter, minChars, debounceMs, loadErrorLabel, "aria-label": ariaLabel, id, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, ...rest }: EntityComboboxProps<V, C>): react.JSX.Element;
72
72
 
73
73
  export { ComboClearValue, ComboOption, EntityCombobox, type EntityComboboxProps };
@@ -35,6 +35,14 @@ function EntityCombobox({
35
35
  debounceMs,
36
36
  loadErrorLabel,
37
37
  "aria-label": ariaLabel,
38
+ // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
39
+ // the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,
40
+ // aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper
41
+ // div the label's `htmlFor` named nothing and the hint described nothing.
42
+ id,
43
+ "aria-describedby": ariaDescribedBy,
44
+ "aria-invalid": ariaInvalid,
45
+ "aria-required": ariaRequired,
38
46
  ...rest
39
47
  }) {
40
48
  const core = useComboboxCore({
@@ -45,7 +53,11 @@ function EntityCombobox({
45
53
  minChars,
46
54
  debounceMs
47
55
  });
48
- const field = useComboboxFieldError(error, invalid);
56
+ const field = useComboboxFieldError(
57
+ error,
58
+ invalid || ariaInvalid === true || ariaInvalid === "true",
59
+ ariaDescribedBy
60
+ );
49
61
  const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
50
62
  search: searchPlaceholder,
51
63
  noResults: emptyLabel,
@@ -67,8 +79,9 @@ function EntityCombobox({
67
79
  core.closeToTrigger();
68
80
  };
69
81
  return (
70
- // `rest` dresses the wrapper, which has no role; the accessible NAME goes on
71
- // the trigger, which has one. Spread FIRST so the trigger's ARIA and the
82
+ // `rest` dresses the wrapper, which has no role; the accessible NAME — and the
83
+ // id, description, invalid and required state above — go on the trigger, which
84
+ // has one. Spread FIRST so the trigger's ARIA and the
72
85
  // handlers that open the panel cannot be clobbered from outside.
73
86
  /* @__PURE__ */ jsxs("div", { ...rest, className: cn("relative", className), children: [
74
87
  label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
@@ -76,6 +89,7 @@ function EntityCombobox({
76
89
  "button",
77
90
  {
78
91
  ref: core.triggerRef,
92
+ id,
79
93
  type: "button",
80
94
  role: "combobox",
81
95
  "aria-expanded": open,
@@ -85,6 +99,7 @@ function EntityCombobox({
85
99
  disabled,
86
100
  "aria-invalid": field.isInvalid || void 0,
87
101
  "aria-describedby": field.describedBy,
102
+ "aria-required": ariaRequired,
88
103
  onClick: () => !disabled && setOpen((o) => !o),
89
104
  onKeyDown: (e) => {
90
105
  if (disabled) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/entity-combobox.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./ui\";\nimport {\n ComboboxPanel,\n useComboboxCore,\n useComboboxFieldError,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\nexport type { ComboClearValue, ComboOption } from \"./combobox-core\";\n\n/** `onChange` is the kit's — \"a selection was made\", carrying values — rather\n * than the div's form event, so the DOM's spelling of it is omitted. */\nexport interface EntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** Selected id; `null`, `undefined` or `clearValue` when nothing is selected. */\n value: V | C | null | undefined;\n /** Selecting an option emits its value; the clear button emits `clearValue`. */\n onChange: (value: V | C) => void;\n /**\n * What the clear button emits. Default `null`. Pass `\"\"` for a form whose schema\n * wants an empty string for \"no choice\" (a zod `z.string()` field), rather than\n * mapping `null` in every `onChange` — the type of `onChange` follows it, so the\n * mapping cannot be forgotten on one field and not another. The picker also reads\n * the value back as empty, so `value=\"\"` shows the placeholder and no \"×\".\n * See {@link ComboClearValue} for why the choice is closed.\n */\n clearValue?: C;\n /** Already-loaded options (client-side filtered). Also used to resolve the\n * trigger label for the current `value`. */\n options?: ComboOption<V>[];\n /** Async option source, debounced and called on open + as the query changes\n * (at `minChars` and up). Stale responses are ignored; a rejection empties the\n * list and says `loadErrorLabel` instead of leaving the last query's rows up. When set, `options` is used only for label\n * resolution, not as the result list. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** External loading flag (OR-ed with the internal async state). */\n loading?: boolean;\n label?: ReactNode;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyLabel?: string;\n clearable?: boolean;\n clearLabel?: string;\n /** The phone sheet's close button. Its own prop rather than a reuse of\n * `clearLabel`: \"clear the selection\" and \"close this screen\" are different\n * actions, and on a full-screen sheet the close button is the only way out —\n * so it is the one control here that MUST be in the reader's language. */\n closeLabel?: string;\n disabled?: boolean;\n /** When set, a \"create\" row appears for a non-empty query with no exact match. */\n onCreate?: (query: string) => void;\n createLabel?: (query: string) => string;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the trigger's `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** Narrow `options` client-side by the query. Default `true`; `false` shows them\n * as given (a server-ranked list). */\n filter?: boolean;\n /** Offer nothing, and call no `loadOptions`, below this many characters. Default\n * `0`, i.e. the list loads as the panel opens. */\n minChars?: number;\n /** `loadOptions` debounce. Default 150 ms. */\n debounceMs?: number;\n /** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n}\n\n/**\n * An id-keyed, searchable entity picker: a field-styled trigger showing the\n * selected item's label, and a portalled dropdown of `{label, sublabel, icon}`\n * options — loaded up front via `options` or lazily via `loadOptions`. Built on\n * the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking\n * several entities use {@link MultiEntityCombobox}.\n */\nexport function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({\n value,\n onChange,\n clearValue = null as C,\n options,\n loadOptions,\n loading,\n label,\n placeholder,\n searchPlaceholder,\n emptyLabel,\n clearable,\n clearLabel,\n closeLabel,\n disabled,\n onCreate,\n createLabel,\n className,\n invalid,\n error,\n filter,\n minChars,\n debounceMs,\n loadErrorLabel,\n \"aria-label\": ariaLabel,\n ...rest\n}: EntityComboboxProps<V, C>) {\n const core = useComboboxCore<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n });\n const field = useComboboxFieldError(error, invalid);\n // The props are the per-instance overrides, the provider the app-wide ones; a\n // prop left `undefined` falls through to the provider rather than masking it.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { open, results, resolve, setOpen } = core;\n // One id per instance, generated here rather than in the core: `aria-controls` on\n // the trigger has to name the list while the list is still closed, so the id\n // belongs to whoever renders both ends of it.\n const listboxId = useId();\n\n // The clear value is \"nothing selected\" too, whichever one the caller picked.\n const chosen: V | null = value == null || value === clearValue ? null : (value as V);\n const selectedOption = chosen == null ? null : resolve(chosen);\n const q = core.query.trim();\n const showCreate =\n Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());\n const showClear = Boolean(clearable && chosen != null && !disabled);\n /** What the closed control is showing — the second half of its accessible name. */\n const triggerText = selectedOption?.label ?? placeholder ?? \"\";\n\n const choose = (o: ComboOption<V>) => {\n core.cacheRef.current.set(o.value, o);\n onChange(o.value);\n // Back to the trigger, not to <body>: the panel that held focus is about to\n // unmount, and a keyboard user who just answered this field should be standing\n // on it, ready to Tab to the next one.\n core.closeToTrigger();\n };\n\n return (\n // `rest` dresses the wrapper, which has no role; the accessible NAME goes on\n // the trigger, which has one. Spread FIRST so the trigger's ARIA and the\n // handlers that open the panel cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n <button\n ref={core.triggerRef}\n type=\"button\"\n // A combobox, not a button. The distinction is not pedantry: this control\n // carried `aria-invalid`, which `button` does not support, so a required\n // field left empty painted a rose border and told a reader nothing at all —\n // and ESLint flagged it as exactly that (`role-supports-aria-props`). The\n // fix the audit asked for is the role that describes what this IS: a closed\n // choice that expands into the list named below. `combobox` supports\n // `aria-invalid`, so the border and the announcement finally agree.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger's accessible name is whatever value happens to be selected —\n // \"Checking\" with nothing saying it is the account. The label AND the\n // value, because `aria-label` replaces the content rather than adding to\n // it, and a control that announces only its name has lost the answer.\n // A caller's own name wins over the composition: two fields labelled\n // \"Account\" on a transfer form are the from and the to, and only the\n // caller knows which is which.\n aria-label={\n ariaLabel ??\n (typeof label === \"string\" ? common.fieldValue(label, triggerText) : undefined)\n }\n disabled={disabled}\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={field.describedBy}\n onClick={() => !disabled && setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG. Enter and\n // Space already do it through the button's own click.\n onKeyDown={(e) => {\n if (disabled) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n {selectedOption?.icon && <span className=\"shrink-0\">{selectedOption.icon}</span>}\n <span\n className={cn(\n \"truncate\",\n // A chosen value is the field's VALUE, so it is set in the same ink an\n // <input>'s value is — FIELD_BASE's own text colour, inherited rather\n // than restated (Keksdose dev#477). It used to be one notch lighter\n // than the typeahead fields beside it, which is visible when a picker\n // and a text field share a form row. Nothing selected keeps the\n // placeholder tone (`--text-placeholder`), which every field here\n // agrees on.\n !selectedOption && \"text-[var(--text-placeholder)]\",\n )}\n >\n {selectedOption?.label ?? placeholder ?? \"\"}\n </span>\n </span>\n {showClear ? (\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n onClick={(e) => {\n e.stopPropagation();\n onChange(clearValue);\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>\n <ComboboxPanel\n core={core}\n listboxId={listboxId}\n // On a phone the panel becomes a full-screen sheet, which needs the field's\n // own label to say what it is asking for (live #200).\n sheetTitle={label ?? placeholder}\n searchPlaceholder={labels.search}\n emptyLabel={labels.noResults}\n closeLabel={closeLabel}\n loadErrorLabel={loadErrorLabel}\n isSelected={(v) => v === value}\n onChoose={choose}\n showCreate={showCreate}\n onCreate={() => {\n onCreate?.(q);\n core.closeToTrigger();\n }}\n createContent={labels.create(q)}\n />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/entity-combobox.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./ui\";\nimport {\n ComboboxPanel,\n useComboboxCore,\n useComboboxFieldError,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\nexport type { ComboClearValue, ComboOption } from \"./combobox-core\";\n\n/** `onChange` is the kit's — \"a selection was made\", carrying values — rather\n * than the div's form event, so the DOM's spelling of it is omitted. */\nexport interface EntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** Selected id; `null`, `undefined` or `clearValue` when nothing is selected. */\n value: V | C | null | undefined;\n /** Selecting an option emits its value; the clear button emits `clearValue`. */\n onChange: (value: V | C) => void;\n /**\n * What the clear button emits. Default `null`. Pass `\"\"` for a form whose schema\n * wants an empty string for \"no choice\" (a zod `z.string()` field), rather than\n * mapping `null` in every `onChange` — the type of `onChange` follows it, so the\n * mapping cannot be forgotten on one field and not another. The picker also reads\n * the value back as empty, so `value=\"\"` shows the placeholder and no \"×\".\n * See {@link ComboClearValue} for why the choice is closed.\n */\n clearValue?: C;\n /** Already-loaded options (client-side filtered). Also used to resolve the\n * trigger label for the current `value`. */\n options?: ComboOption<V>[];\n /** Async option source, debounced and called on open + as the query changes\n * (at `minChars` and up). Stale responses are ignored; a rejection empties the\n * list and says `loadErrorLabel` instead of leaving the last query's rows up. When set, `options` is used only for label\n * resolution, not as the result list. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** External loading flag (OR-ed with the internal async state). */\n loading?: boolean;\n label?: ReactNode;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyLabel?: string;\n clearable?: boolean;\n clearLabel?: string;\n /** The phone sheet's close button. Its own prop rather than a reuse of\n * `clearLabel`: \"clear the selection\" and \"close this screen\" are different\n * actions, and on a full-screen sheet the close button is the only way out —\n * so it is the one control here that MUST be in the reader's language. */\n closeLabel?: string;\n disabled?: boolean;\n /** When set, a \"create\" row appears for a non-empty query with no exact match. */\n onCreate?: (query: string) => void;\n createLabel?: (query: string) => string;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the trigger's `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** Narrow `options` client-side by the query. Default `true`; `false` shows them\n * as given (a server-ranked list). */\n filter?: boolean;\n /** Offer nothing, and call no `loadOptions`, below this many characters. Default\n * `0`, i.e. the list loads as the panel opens. */\n minChars?: number;\n /** `loadOptions` debounce. Default 150 ms. */\n debounceMs?: number;\n /** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n}\n\n/**\n * An id-keyed, searchable entity picker: a field-styled trigger showing the\n * selected item's label, and a portalled dropdown of `{label, sublabel, icon}`\n * options — loaded up front via `options` or lazily via `loadOptions`. Built on\n * the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking\n * several entities use {@link MultiEntityCombobox}.\n */\nexport function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({\n value,\n onChange,\n clearValue = null as C,\n options,\n loadOptions,\n loading,\n label,\n placeholder,\n searchPlaceholder,\n emptyLabel,\n clearable,\n clearLabel,\n closeLabel,\n disabled,\n onCreate,\n createLabel,\n className,\n invalid,\n error,\n filter,\n minChars,\n debounceMs,\n loadErrorLabel,\n \"aria-label\": ariaLabel,\n // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than\n // the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,\n // aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper\n // div the label's `htmlFor` named nothing and the hint described nothing.\n id,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n}: EntityComboboxProps<V, C>) {\n const core = useComboboxCore<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n });\n const field = useComboboxFieldError(\n error,\n invalid || ariaInvalid === true || ariaInvalid === \"true\",\n ariaDescribedBy,\n );\n // The props are the per-instance overrides, the provider the app-wide ones; a\n // prop left `undefined` falls through to the provider rather than masking it.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { open, results, resolve, setOpen } = core;\n // One id per instance, generated here rather than in the core: `aria-controls` on\n // the trigger has to name the list while the list is still closed, so the id\n // belongs to whoever renders both ends of it.\n const listboxId = useId();\n\n // The clear value is \"nothing selected\" too, whichever one the caller picked.\n const chosen: V | null = value == null || value === clearValue ? null : (value as V);\n const selectedOption = chosen == null ? null : resolve(chosen);\n const q = core.query.trim();\n const showCreate =\n Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());\n const showClear = Boolean(clearable && chosen != null && !disabled);\n /** What the closed control is showing — the second half of its accessible name. */\n const triggerText = selectedOption?.label ?? placeholder ?? \"\";\n\n const choose = (o: ComboOption<V>) => {\n core.cacheRef.current.set(o.value, o);\n onChange(o.value);\n // Back to the trigger, not to <body>: the panel that held focus is about to\n // unmount, and a keyboard user who just answered this field should be standing\n // on it, ready to Tab to the next one.\n core.closeToTrigger();\n };\n\n return (\n // `rest` dresses the wrapper, which has no role; the accessible NAME — and the\n // id, description, invalid and required state above — go on the trigger, which\n // has one. Spread FIRST so the trigger's ARIA and the\n // handlers that open the panel cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n <button\n ref={core.triggerRef}\n id={id}\n type=\"button\"\n // A combobox, not a button. The distinction is not pedantry: this control\n // carried `aria-invalid`, which `button` does not support, so a required\n // field left empty painted a rose border and told a reader nothing at all —\n // and ESLint flagged it as exactly that (`role-supports-aria-props`). The\n // fix the audit asked for is the role that describes what this IS: a closed\n // choice that expands into the list named below. `combobox` supports\n // `aria-invalid`, so the border and the announcement finally agree.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger's accessible name is whatever value happens to be selected —\n // \"Checking\" with nothing saying it is the account. The label AND the\n // value, because `aria-label` replaces the content rather than adding to\n // it, and a control that announces only its name has lost the answer.\n // A caller's own name wins over the composition: two fields labelled\n // \"Account\" on a transfer form are the from and the to, and only the\n // caller knows which is which.\n aria-label={\n ariaLabel ??\n (typeof label === \"string\" ? common.fieldValue(label, triggerText) : undefined)\n }\n disabled={disabled}\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={field.describedBy}\n aria-required={ariaRequired}\n onClick={() => !disabled && setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG. Enter and\n // Space already do it through the button's own click.\n onKeyDown={(e) => {\n if (disabled) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n {selectedOption?.icon && <span className=\"shrink-0\">{selectedOption.icon}</span>}\n <span\n className={cn(\n \"truncate\",\n // A chosen value is the field's VALUE, so it is set in the same ink an\n // <input>'s value is — FIELD_BASE's own text colour, inherited rather\n // than restated (Keksdose dev#477). It used to be one notch lighter\n // than the typeahead fields beside it, which is visible when a picker\n // and a text field share a form row. Nothing selected keeps the\n // placeholder tone (`--text-placeholder`), which every field here\n // agrees on.\n !selectedOption && \"text-[var(--text-placeholder)]\",\n )}\n >\n {selectedOption?.label ?? placeholder ?? \"\"}\n </span>\n </span>\n {showClear ? (\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n onClick={(e) => {\n e.stopPropagation();\n onChange(clearValue);\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>\n <ComboboxPanel\n core={core}\n listboxId={listboxId}\n // On a phone the panel becomes a full-screen sheet, which needs the field's\n // own label to say what it is asking for (live #200).\n sheetTitle={label ?? placeholder}\n searchPlaceholder={labels.search}\n emptyLabel={labels.noResults}\n closeLabel={closeLabel}\n loadErrorLabel={loadErrorLabel}\n isSelected={(v) => v === value}\n onChoose={choose}\n showCreate={showCreate}\n onCreate={() => {\n onCreate?.(q);\n core.closeToTrigger();\n }}\n createContent={labels.create(q)}\n />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,92 @@
1
+ import { Component, ReactNode, ErrorInfo } from 'react';
2
+
3
+ /** The words of the default fallback. */
4
+ interface ErrorBoundaryLabels {
5
+ /** The fallback's heading. */
6
+ title: string;
7
+ /** The line under it. */
8
+ message: string;
9
+ /** The button that clears the error and renders the children again. */
10
+ retry: string;
11
+ /** The disclosure that opens the error's own text (`showDetails`). */
12
+ details: string;
13
+ }
14
+ declare const DEFAULT_ERROR_BOUNDARY_LABELS: ErrorBoundaryLabels;
15
+ /**
16
+ * What was thrown, as flat strings, read ONCE when it arrives.
17
+ *
18
+ * React stores whatever was thrown, hostile or not — `throw { get message() { throw … } }`,
19
+ * `throw { name: Symbol() }`, `throw undefined` — and reading one of those during the
20
+ * fallback's own render would throw INSIDE the boundary, escalating to the next one up
21
+ * (keksdose's error-boundary.tsx learned this as feedback #160: at the app level there is
22
+ * none, and the result is the white screen the boundary exists to prevent).
23
+ */
24
+ interface ErrorBoundaryDetails {
25
+ name: string;
26
+ message: string;
27
+ stack?: string;
28
+ }
29
+ /** {@link ErrorBoundaryDetails} of any thrown value, without ever throwing. */
30
+ declare function describeThrown(error: unknown): ErrorBoundaryDetails;
31
+ /** What a render-prop `fallback` is handed. */
32
+ interface ErrorBoundaryFallbackProps {
33
+ /** The thrown value, as it was thrown. Read it with care — see {@link describeThrown}. */
34
+ error: unknown;
35
+ details: ErrorBoundaryDetails;
36
+ /** Clear the error and render the children again. */
37
+ reset: () => void;
38
+ }
39
+ interface ErrorBoundaryProps {
40
+ children?: ReactNode;
41
+ /** Replaces the default fallback: a node, or a render function handed the error and
42
+ * a `reset`. */
43
+ fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);
44
+ /** Called once per caught error — log it, file a crash report. */
45
+ onError?: (error: unknown, info: ErrorInfo) => void;
46
+ /** Called when the error is cleared, by Retry, `reset` or a `resetKeys` change —
47
+ * drop a cache, refetch. */
48
+ onReset?: () => void;
49
+ /**
50
+ * When any of these changes (compared with `Object.is`) AFTER an error was caught, the
51
+ * error is cleared and the children render again — a change in the same update that
52
+ * threw does not count (it is what the error was shown under) — pass the route path so leaving a broken page does not
53
+ * carry its fallback to the next one (keksdose's `resetKey`).
54
+ */
55
+ resetKeys?: ReadonlyArray<unknown>;
56
+ /** A disclosure under the message with the error's name, message and stack — for an
57
+ * internal tool, or while developing. Off by default: a stack means nothing to a user. */
58
+ showDetails?: boolean;
59
+ /** The default fallback's heading level. Default `h2`: the fallback often IS the page. */
60
+ headingAs?: "h2" | "h3" | "h4" | "h5" | "h6";
61
+ /** Classes for the default fallback's box (`m-8 min-h-[400px]`, kastlan). */
62
+ className?: string;
63
+ labels?: Partial<ErrorBoundaryLabels>;
64
+ }
65
+ interface ErrorBoundaryState {
66
+ /** A flag of its own, so `throw undefined` is still an error shown. */
67
+ failed: boolean;
68
+ error: unknown;
69
+ details: ErrorBoundaryDetails | null;
70
+ }
71
+ /**
72
+ * Catch a render error below it and show a fallback instead of an empty page.
73
+ *
74
+ * kastlan (shared/components/error-boundary.tsx) and keksdose (app/error-boundary.tsx)
75
+ * each built one on `EmptyState tone="danger"`: a warning glyph, a title, a line of
76
+ * explanation and a Retry. This is that fallback, translated through the provider
77
+ * (`errorBoundary.*`), with the hooks an app hangs its own behaviour on — `onError`
78
+ * for the crash report, `resetKeys` for the route change, `fallback` for anything else.
79
+ *
80
+ * The fallback is `role="alert"`: a page replaced by an error is news, and a reader
81
+ * that was on it is told.
82
+ */
83
+ declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
84
+ state: ErrorBoundaryState;
85
+ static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState>;
86
+ componentDidCatch(error: unknown, info: ErrorInfo): void;
87
+ componentDidUpdate(prev: ErrorBoundaryProps, prevState: ErrorBoundaryState): void;
88
+ reset: () => void;
89
+ render(): ReactNode;
90
+ }
91
+
92
+ export { DEFAULT_ERROR_BOUNDARY_LABELS, ErrorBoundary, type ErrorBoundaryDetails, type ErrorBoundaryFallbackProps, type ErrorBoundaryLabels, type ErrorBoundaryProps, describeThrown };
@@ -0,0 +1,111 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Component } from "react";
4
+ import { AlertTriangle } from "lucide-react";
5
+ import { useKitLabels } from "../i18n/kit-labels.js";
6
+ import { Button, EmptyState } from "./ui.js";
7
+ import { Disclosure } from "./disclosure.js";
8
+ const DEFAULT_ERROR_BOUNDARY_LABELS = {
9
+ title: "Something went wrong",
10
+ message: "This part of the page could not be shown. Try again, or reload the page.",
11
+ retry: "Try again",
12
+ details: "Error details"
13
+ };
14
+ function read(get) {
15
+ try {
16
+ const v = get();
17
+ return typeof v === "string" ? v : v === void 0 || v === null ? void 0 : String(v);
18
+ } catch {
19
+ return void 0;
20
+ }
21
+ }
22
+ function describeThrown(error) {
23
+ if (typeof error === "object" && error !== null) {
24
+ const e = error;
25
+ return {
26
+ name: read(() => e.name) ?? "Error",
27
+ message: read(() => e.message) ?? "",
28
+ stack: read(() => e.stack)
29
+ };
30
+ }
31
+ return { name: "Error", message: read(() => error) ?? "" };
32
+ }
33
+ const CLEAR = { failed: false, error: null, details: null };
34
+ function keysChanged(a, b) {
35
+ if (a === b) return false;
36
+ if (!a || !b || a.length !== b.length) return true;
37
+ return a.some((v, i) => !Object.is(v, b[i]));
38
+ }
39
+ class ErrorBoundary extends Component {
40
+ state = CLEAR;
41
+ static getDerivedStateFromError(error) {
42
+ return { failed: true, error, details: describeThrown(error) };
43
+ }
44
+ componentDidCatch(error, info) {
45
+ this.props.onError?.(error, info);
46
+ }
47
+ componentDidUpdate(prev, prevState) {
48
+ if (this.state.failed && prevState.failed && keysChanged(prev.resetKeys, this.props.resetKeys)) {
49
+ this.reset();
50
+ }
51
+ }
52
+ reset = () => {
53
+ this.props.onReset?.();
54
+ this.setState(CLEAR);
55
+ };
56
+ render() {
57
+ const { failed, error, details } = this.state;
58
+ if (!failed || !details) return this.props.children;
59
+ const { fallback } = this.props;
60
+ if (typeof fallback === "function") return fallback({ error, details, reset: this.reset });
61
+ if (fallback !== void 0) return fallback;
62
+ return /* @__PURE__ */ jsx(
63
+ ErrorFallback,
64
+ {
65
+ details,
66
+ reset: this.reset,
67
+ showDetails: this.props.showDetails ?? false,
68
+ headingAs: this.props.headingAs ?? "h2",
69
+ className: this.props.className,
70
+ labels: this.props.labels
71
+ }
72
+ );
73
+ }
74
+ }
75
+ function ErrorFallback({
76
+ details,
77
+ reset,
78
+ showDetails,
79
+ headingAs,
80
+ className,
81
+ labels
82
+ }) {
83
+ const text = useKitLabels("errorBoundary", DEFAULT_ERROR_BOUNDARY_LABELS, labels);
84
+ return /* @__PURE__ */ jsx(
85
+ EmptyState,
86
+ {
87
+ role: "alert",
88
+ tone: "danger",
89
+ headingAs,
90
+ className,
91
+ icon: /* @__PURE__ */ jsx(AlertTriangle, {}),
92
+ title: text.title,
93
+ hint: text.message,
94
+ action: /* @__PURE__ */ jsxs(Fragment, { children: [
95
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: reset, children: text.retry }),
96
+ showDetails && /* @__PURE__ */ jsx(Disclosure, { variant: "bare", title: text.details, className: "basis-full text-start", children: /* @__PURE__ */ jsxs("pre", { className: "max-h-60 overflow-auto whitespace-pre-wrap break-words text-xs text-[var(--text-secondary)]", children: [
97
+ details.message ? `${details.name}: ${details.message}` : details.name,
98
+ details.stack ? `
99
+
100
+ ${details.stack}` : null
101
+ ] }) })
102
+ ] })
103
+ }
104
+ );
105
+ }
106
+ export {
107
+ DEFAULT_ERROR_BOUNDARY_LABELS,
108
+ ErrorBoundary,
109
+ describeThrown
110
+ };
111
+ //# sourceMappingURL=error-boundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/error-boundary.tsx"],"sourcesContent":["import { Component } from \"react\";\nimport type { ErrorInfo, ReactNode } from \"react\";\nimport { AlertTriangle } from \"lucide-react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Button, EmptyState } from \"./ui\";\nimport { Disclosure } from \"./disclosure\";\n\n/** The words of the default fallback. */\nexport interface ErrorBoundaryLabels {\n /** The fallback's heading. */\n title: string;\n /** The line under it. */\n message: string;\n /** The button that clears the error and renders the children again. */\n retry: string;\n /** The disclosure that opens the error's own text (`showDetails`). */\n details: string;\n}\n\nexport const DEFAULT_ERROR_BOUNDARY_LABELS: ErrorBoundaryLabels = {\n title: \"Something went wrong\",\n message: \"This part of the page could not be shown. Try again, or reload the page.\",\n retry: \"Try again\",\n details: \"Error details\",\n};\n\n/**\n * What was thrown, as flat strings, read ONCE when it arrives.\n *\n * React stores whatever was thrown, hostile or not — `throw { get message() { throw … } }`,\n * `throw { name: Symbol() }`, `throw undefined` — and reading one of those during the\n * fallback's own render would throw INSIDE the boundary, escalating to the next one up\n * (keksdose's error-boundary.tsx learned this as feedback #160: at the app level there is\n * none, and the result is the white screen the boundary exists to prevent).\n */\nexport interface ErrorBoundaryDetails {\n name: string;\n message: string;\n stack?: string;\n}\n\nfunction read(get: () => unknown): string | undefined {\n try {\n const v = get();\n return typeof v === \"string\" ? v : v === undefined || v === null ? undefined : String(v);\n } catch {\n return undefined;\n }\n}\n\n/** {@link ErrorBoundaryDetails} of any thrown value, without ever throwing. */\nexport function describeThrown(error: unknown): ErrorBoundaryDetails {\n if (typeof error === \"object\" && error !== null) {\n const e = error as { name?: unknown; message?: unknown; stack?: unknown };\n return {\n name: read(() => e.name) ?? \"Error\",\n message: read(() => e.message) ?? \"\",\n stack: read(() => e.stack),\n };\n }\n return { name: \"Error\", message: read(() => error) ?? \"\" };\n}\n\n/** What a render-prop `fallback` is handed. */\nexport interface ErrorBoundaryFallbackProps {\n /** The thrown value, as it was thrown. Read it with care — see {@link describeThrown}. */\n error: unknown;\n details: ErrorBoundaryDetails;\n /** Clear the error and render the children again. */\n reset: () => void;\n}\n\nexport interface ErrorBoundaryProps {\n children?: ReactNode;\n /** Replaces the default fallback: a node, or a render function handed the error and\n * a `reset`. */\n fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);\n /** Called once per caught error — log it, file a crash report. */\n onError?: (error: unknown, info: ErrorInfo) => void;\n /** Called when the error is cleared, by Retry, `reset` or a `resetKeys` change —\n * drop a cache, refetch. */\n onReset?: () => void;\n /**\n * When any of these changes (compared with `Object.is`) AFTER an error was caught, the\n * error is cleared and the children render again — a change in the same update that\n * threw does not count (it is what the error was shown under) — pass the route path so leaving a broken page does not\n * carry its fallback to the next one (keksdose's `resetKey`).\n */\n resetKeys?: ReadonlyArray<unknown>;\n /** A disclosure under the message with the error's name, message and stack — for an\n * internal tool, or while developing. Off by default: a stack means nothing to a user. */\n showDetails?: boolean;\n /** The default fallback's heading level. Default `h2`: the fallback often IS the page. */\n headingAs?: \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n /** Classes for the default fallback's box (`m-8 min-h-[400px]`, kastlan). */\n className?: string;\n labels?: Partial<ErrorBoundaryLabels>;\n}\n\ninterface ErrorBoundaryState {\n /** A flag of its own, so `throw undefined` is still an error shown. */\n failed: boolean;\n error: unknown;\n details: ErrorBoundaryDetails | null;\n}\n\nconst CLEAR: ErrorBoundaryState = { failed: false, error: null, details: null };\n\nfunction keysChanged(a: ReadonlyArray<unknown> | undefined, b: ReadonlyArray<unknown> | undefined): boolean {\n if (a === b) return false;\n if (!a || !b || a.length !== b.length) return true;\n return a.some((v, i) => !Object.is(v, b[i]));\n}\n\n/**\n * Catch a render error below it and show a fallback instead of an empty page.\n *\n * kastlan (shared/components/error-boundary.tsx) and keksdose (app/error-boundary.tsx)\n * each built one on `EmptyState tone=\"danger\"`: a warning glyph, a title, a line of\n * explanation and a Retry. This is that fallback, translated through the provider\n * (`errorBoundary.*`), with the hooks an app hangs its own behaviour on — `onError`\n * for the crash report, `resetKeys` for the route change, `fallback` for anything else.\n *\n * The fallback is `role=\"alert\"`: a page replaced by an error is news, and a reader\n * that was on it is told.\n */\nexport class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {\n state: ErrorBoundaryState = CLEAR;\n\n static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState> {\n return { failed: true, error, details: describeThrown(error) };\n }\n\n componentDidCatch(error: unknown, info: ErrorInfo): void {\n this.props.onError?.(error, info);\n }\n\n componentDidUpdate(prev: ErrorBoundaryProps, prevState: ErrorBoundaryState): void {\n // Only a change AFTER the error was caught clears it (react-error-boundary's rule).\n // `prevState.failed` is false in the update that caught it: that update may well be\n // the one that changed the keys too — a navigation to a page that throws, with\n // `resetKeys={[pathname]}` — and resetting then would fire `onReset` for nothing and\n // render the failing child a second time. From then on `prev.resetKeys` are the\n // keys the error was shown under, so any change clears it.\n if (this.state.failed && prevState.failed && keysChanged(prev.resetKeys, this.props.resetKeys)) {\n this.reset();\n }\n }\n\n reset = (): void => {\n this.props.onReset?.();\n this.setState(CLEAR);\n };\n\n render(): ReactNode {\n const { failed, error, details } = this.state;\n if (!failed || !details) return this.props.children;\n const { fallback } = this.props;\n if (typeof fallback === \"function\") return fallback({ error, details, reset: this.reset });\n if (fallback !== undefined) return fallback;\n return (\n <ErrorFallback\n details={details}\n reset={this.reset}\n showDetails={this.props.showDetails ?? false}\n headingAs={this.props.headingAs ?? \"h2\"}\n className={this.props.className}\n labels={this.props.labels}\n />\n );\n }\n}\n\nfunction ErrorFallback({\n details,\n reset,\n showDetails,\n headingAs,\n className,\n labels,\n}: {\n details: ErrorBoundaryDetails;\n reset: () => void;\n showDetails: boolean;\n headingAs: NonNullable<ErrorBoundaryProps[\"headingAs\"]>;\n className?: string;\n labels?: Partial<ErrorBoundaryLabels>;\n}) {\n const text = useKitLabels(\"errorBoundary\", DEFAULT_ERROR_BOUNDARY_LABELS, labels);\n return (\n <EmptyState\n role=\"alert\"\n tone=\"danger\"\n headingAs={headingAs}\n className={className}\n icon={<AlertTriangle />}\n title={text.title}\n hint={text.message}\n action={\n <>\n <Button variant=\"secondary\" onClick={reset}>\n {text.retry}\n </Button>\n {showDetails && (\n <Disclosure variant=\"bare\" title={text.details} className=\"basis-full text-start\">\n <pre className=\"max-h-60 overflow-auto whitespace-pre-wrap break-words text-xs text-[var(--text-secondary)]\">\n {details.message ? `${details.name}: ${details.message}` : details.name}\n {details.stack ? `\\n\\n${details.stack}` : null}\n </pre>\n </Disclosure>\n )}\n </>\n }\n />\n );\n}\n"],"mappings":"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","names":[]}
@@ -14,6 +14,29 @@ interface FieldControlProps {
14
14
  * is what a screen reader hears instead. */
15
15
  "aria-required": true | undefined;
16
16
  }
17
+ /**
18
+ * The second argument of a `Field`'s render-prop children: what a GROUP needs that
19
+ * a single control does not. `htmlFor` names an `<input>`, but a `<label>` pointing
20
+ * at a `role="radiogroup"` div names nothing — so a group (a {@link ToggleGroup})
21
+ * takes `aria-labelledby={labelId}` instead. Kept out of {@link FieldControlProps}
22
+ * so the spread every existing control takes is unchanged.
23
+ */
24
+ interface FieldRenderMeta {
25
+ /** The label element's id, or `undefined` when the field has no label. */
26
+ labelId: string | undefined;
27
+ }
28
+ /**
29
+ * Where the label is visible. `"visible"` (default); `"sr-only"`: never drawn, still
30
+ * the control's name; `"below-sm"` / `"below-md"` / `"below-lg"`: drawn only under
31
+ * that breakpoint and screen-reader-only from it up.
32
+ *
33
+ * For table-like editors — kastlan's journal entry lines
34
+ * (accounting/components/wizard/journal-entry-lines-step.tsx) — where a desktop
35
+ * row sits under column headers that already say "Account" and "Amount", but the
36
+ * phone layout stacks each line as a card and needs the labels back. The name stays
37
+ * on the control at every width; only the ink changes.
38
+ */
39
+ type FieldLabelVisibility = "visible" | "sr-only" | "below-sm" | "below-md" | "below-lg";
17
40
  interface FieldProps {
18
41
  /** The label above the control. Omit it for a group whose control is labelled
19
42
  * some other way; the hint and error still render. */
@@ -33,11 +56,14 @@ interface FieldProps {
33
56
  disabled?: boolean;
34
57
  /** The label's size: `sm` for a dense row, `md` otherwise. */
35
58
  labelSize?: "sm" | "md";
59
+ /** See {@link FieldLabelVisibility}. */
60
+ labelVisibility?: FieldLabelVisibility;
36
61
  /** Wrapper className. */
37
62
  className?: string;
38
- /** The control. A function receives {@link FieldControlProps} to spread on it. */
39
- children: ReactNode | ((control: FieldControlProps) => ReactNode);
63
+ /** The control. A function receives {@link FieldControlProps} to spread on it, and
64
+ * {@link FieldRenderMeta} (the label's id, for a group). */
65
+ children: ReactNode | ((control: FieldControlProps, meta: FieldRenderMeta) => ReactNode);
40
66
  }
41
- declare function Field({ label, required, hint, error, htmlFor, disabled, labelSize, className, children, }: FieldProps): react.JSX.Element;
67
+ declare function Field({ label, required, hint, error, htmlFor, disabled, labelSize, labelVisibility, className, children, }: FieldProps): react.JSX.Element;
42
68
 
43
- export { Field, type FieldControlProps, type FieldProps };
69
+ export { Field, type FieldControlProps, type FieldLabelVisibility, type FieldProps, type FieldRenderMeta };
@@ -3,6 +3,13 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
4
  import { cn } from "../lib/cn.js";
5
5
  import { Label } from "./ui.js";
6
+ const LABEL_VISIBILITY = {
7
+ visible: void 0,
8
+ "sr-only": "sr-only",
9
+ "below-sm": "sm:sr-only",
10
+ "below-md": "md:sr-only",
11
+ "below-lg": "lg:sr-only"
12
+ };
6
13
  const isShown = (node) => node !== void 0 && node !== null && node !== false && node !== "";
7
14
  function Field({
8
15
  label,
@@ -12,6 +19,7 @@ function Field({
12
19
  htmlFor,
13
20
  disabled,
14
21
  labelSize,
22
+ labelVisibility = "visible",
15
23
  className,
16
24
  children
17
25
  }) {
@@ -19,6 +27,7 @@ function Field({
19
27
  const id = htmlFor ?? `${generated}-control`;
20
28
  const hintId = `${generated}-hint`;
21
29
  const errorId = `${generated}-error`;
30
+ const labelId = `${generated}-label`;
22
31
  const hasHint = isShown(hint);
23
32
  const hasError = isShown(error);
24
33
  const describedBy = [hasHint && hintId, hasError && errorId].filter(Boolean).join(" ");
@@ -28,23 +37,37 @@ function Field({
28
37
  "aria-invalid": hasError || void 0,
29
38
  "aria-required": required || void 0
30
39
  };
31
- return /* @__PURE__ */ jsxs("div", { "data-slot": "field", className: cn("grid gap-1.5", className), children: [
32
- label !== void 0 && /* @__PURE__ */ jsx(
33
- Label,
34
- {
35
- htmlFor: id,
36
- required,
37
- disabled,
38
- size: labelSize,
39
- "data-error": hasError || void 0,
40
- className: "data-[error=true]:text-[var(--danger)]",
41
- children: label
42
- }
43
- ),
44
- typeof children === "function" ? children(control) : children,
45
- hasHint && /* @__PURE__ */ jsx("p", { id: hintId, className: "text-[11px] leading-tight text-[var(--text-muted)]", children: hint }),
46
- hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: "text-[11px] leading-tight text-[var(--danger)]", children: error })
47
- ] });
40
+ return /* @__PURE__ */ jsxs(
41
+ "div",
42
+ {
43
+ "data-slot": "field",
44
+ className: cn(
45
+ "grid gap-1.5",
46
+ // A hidden label is `position: absolute`; `relative` keeps its containing
47
+ // block here rather than the page's (see sr-only-containment.test).
48
+ labelVisibility !== "visible" && "relative",
49
+ className
50
+ ),
51
+ children: [
52
+ label !== void 0 && /* @__PURE__ */ jsx(
53
+ Label,
54
+ {
55
+ id: labelId,
56
+ htmlFor: id,
57
+ required,
58
+ disabled,
59
+ size: labelSize,
60
+ "data-error": hasError || void 0,
61
+ className: cn("data-[error=true]:text-[var(--danger)]", LABEL_VISIBILITY[labelVisibility]),
62
+ children: label
63
+ }
64
+ ),
65
+ typeof children === "function" ? children(control, { labelId: label !== void 0 ? labelId : void 0 }) : children,
66
+ hasHint && /* @__PURE__ */ jsx("p", { id: hintId, className: "text-[11px] leading-tight text-[var(--text-muted)]", children: hint }),
67
+ hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: "text-[11px] leading-tight text-[var(--danger)]", children: error })
68
+ ]
69
+ }
70
+ );
48
71
  }
49
72
  export {
50
73
  Field
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/field.tsx"],"sourcesContent":["/**\n * A field group with its label ABOVE the control: label, control, hint, error.\n *\n * ```tsx\n * <Field label=\"IBAN\" required hint=\"22 characters\" error={errors.iban}>\n * {(ids) => <Input {...ids} value={iban} onChange={…} />}\n * </Field>\n * ```\n *\n * The kit's `Input` and `Select` float their label inside the field, and the pickers\n * draw theirs the same way. That is the wrong shape for a control the kit did not\n * render, a radio group, a range slider, or a form that sets its labels above the\n * fields throughout — which is what kastlan's wizards are, and why it kept its own\n * `WizardField` after the kit dropped the one it had never rendered. This is that\n * group, with the wiring the old one left to the caller: the label names the control\n * through `htmlFor`, and the control is described by the hint and the error that are\n * actually on screen.\n *\n * ## Wiring\n *\n * Pass a function as `children` and it receives {@link FieldControlProps} — `id`,\n * `aria-describedby`, `aria-invalid` and `aria-required` — to spread on the control.\n * Plain children render as they are, for a control that wires itself (a checkbox that\n * carries its own label, a group with `aria-labelledby`); `htmlFor` then points the\n * label at the control's own id, and the hint and error are on screen but describe\n * nothing — the same as the `WizardField` this replaces.\n *\n * ## Decoupled\n *\n * Deliberately knows neither the wizard nor react-hook-form: `error` is a plain\n * value. Pass `wizard.fieldErrors.iban`, a step-local error, or\n * `fieldState.error?.message` from a `FormField` render — the markup is the same\n * whichever of them gates the step, so a step can move between them without its\n * fields changing. (Inside a react-hook-form form, `FormItem` + `FormLabel` from\n * `@eifi1/ui-kit/rhf` do the same job with the ids taken from the form.)\n *\n * The error is NOT `role=\"alert\"`, for the reason the kit's `error` prop gives (see\n * `useFieldError` in components/ui.tsx): it is read when focus reaches the control.\n */\nimport { useId, type ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Label } from \"./ui\";\n\n/** What a `Field` hands its render-prop children, to spread on the control. */\nexport interface FieldControlProps {\n /** The id the label's `htmlFor` points at. */\n id: string;\n /** The ids of the hint and the error that are rendered, or `undefined` when\n * neither is. */\n \"aria-describedby\": string | undefined;\n /** `true` while the field has an error, `undefined` otherwise. */\n \"aria-invalid\": true | undefined;\n /** `true` when the field is `required`. The label's star is `aria-hidden`; this\n * is what a screen reader hears instead. */\n \"aria-required\": true | undefined;\n}\n\nexport interface FieldProps {\n /** The label above the control. Omit it for a group whose control is labelled\n * some other way; the hint and error still render. */\n label?: ReactNode;\n /** Draws the kit's required mark after the label, and sets `aria-required` on the\n * render-prop control. */\n required?: boolean;\n /** Muted helper text below the control. */\n hint?: ReactNode;\n /** The error below the control. `null`, `false` and `\"\"` are no error — what a\n * `touched && errors.x` evaluates to on the happy path. */\n error?: ReactNode;\n /** The control's id. Defaults to a generated one; set it when the control carries\n * its own id (plain children). */\n htmlFor?: string;\n /** Dims the label with its control. */\n disabled?: boolean;\n /** The label's size: `sm` for a dense row, `md` otherwise. */\n labelSize?: \"sm\" | \"md\";\n /** Wrapper className. */\n className?: string;\n /** The control. A function receives {@link FieldControlProps} to spread on it. */\n children: ReactNode | ((control: FieldControlProps) => ReactNode);\n}\n\nconst isShown = (node: ReactNode) =>\n node !== undefined && node !== null && node !== false && node !== \"\";\n\nexport function Field({\n label,\n required,\n hint,\n error,\n htmlFor,\n disabled,\n labelSize,\n className,\n children,\n}: FieldProps) {\n const generated = useId();\n const id = htmlFor ?? `${generated}-control`;\n const hintId = `${generated}-hint`;\n const errorId = `${generated}-error`;\n const hasHint = isShown(hint);\n const hasError = isShown(error);\n const describedBy = [hasHint && hintId, hasError && errorId].filter(Boolean).join(\" \");\n const control: FieldControlProps = {\n id,\n \"aria-describedby\": describedBy || undefined,\n \"aria-invalid\": hasError || undefined,\n \"aria-required\": required || undefined,\n };\n return (\n <div data-slot=\"field\" className={cn(\"grid gap-1.5\", className)}>\n {label !== undefined && (\n <Label\n htmlFor={id}\n required={required}\n disabled={disabled}\n size={labelSize}\n data-error={hasError || undefined}\n className=\"data-[error=true]:text-[var(--danger)]\"\n >\n {label}\n </Label>\n )}\n {typeof children === \"function\" ? children(control) : children}\n {hasHint && (\n <p id={hintId} className=\"text-[11px] leading-tight text-[var(--text-muted)]\">\n {hint}\n </p>\n )}\n {hasError && (\n <p id={errorId} className=\"text-[11px] leading-tight text-[var(--danger)]\">\n {error}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";AA8GI,SAEI,KAFJ;AAvEJ,SAAS,aAA6B;AACtC,SAAS,UAAU;AACnB,SAAS,aAAa;AAyCtB,MAAM,UAAU,CAAC,SACf,SAAS,UAAa,SAAS,QAAQ,SAAS,SAAS,SAAS;AAE7D,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAe;AACb,QAAM,YAAY,MAAM;AACxB,QAAM,KAAK,WAAW,GAAG,SAAS;AAClC,QAAM,SAAS,GAAG,SAAS;AAC3B,QAAM,UAAU,GAAG,SAAS;AAC5B,QAAM,UAAU,QAAQ,IAAI;AAC5B,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,cAAc,CAAC,WAAW,QAAQ,YAAY,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AACrF,QAAM,UAA6B;AAAA,IACjC;AAAA,IACA,oBAAoB,eAAe;AAAA,IACnC,gBAAgB,YAAY;AAAA,IAC5B,iBAAiB,YAAY;AAAA,EAC/B;AACA,SACE,qBAAC,SAAI,aAAU,SAAQ,WAAW,GAAG,gBAAgB,SAAS,GAC3D;AAAA,cAAU,UACT;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN,cAAY,YAAY;AAAA,QACxB,WAAU;AAAA,QAET;AAAA;AAAA,IACH;AAAA,IAED,OAAO,aAAa,aAAa,SAAS,OAAO,IAAI;AAAA,IACrD,WACC,oBAAC,OAAE,IAAI,QAAQ,WAAU,sDACtB,gBACH;AAAA,IAED,YACC,oBAAC,OAAE,IAAI,SAAS,WAAU,kDACvB,iBACH;AAAA,KAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/field.tsx"],"sourcesContent":["/**\n * A field group with its label ABOVE the control: label, control, hint, error.\n *\n * ```tsx\n * <Field label=\"IBAN\" required hint=\"22 characters\" error={errors.iban}>\n * {(ids) => <Input {...ids} value={iban} onChange={…} />}\n * </Field>\n * ```\n *\n * The kit's `Input` and `Select` float their label inside the field, and the pickers\n * draw theirs the same way. That is the wrong shape for a control the kit did not\n * render, a radio group, a range slider, or a form that sets its labels above the\n * fields throughout — which is what kastlan's wizards are, and why it kept its own\n * `WizardField` after the kit dropped the one it had never rendered. This is that\n * group, with the wiring the old one left to the caller: the label names the control\n * through `htmlFor`, and the control is described by the hint and the error that are\n * actually on screen.\n *\n * ## Wiring\n *\n * Pass a function as `children` and it receives {@link FieldControlProps} — `id`,\n * `aria-describedby`, `aria-invalid` and `aria-required` — to spread on the control.\n * Plain children render as they are, for a control that wires itself (a checkbox that\n * carries its own label, a group with `aria-labelledby`); `htmlFor` then points the\n * label at the control's own id, and the hint and error are on screen but describe\n * nothing — the same as the `WizardField` this replaces.\n *\n * ## Decoupled\n *\n * Deliberately knows neither the wizard nor react-hook-form: `error` is a plain\n * value. Pass `wizard.fieldErrors.iban`, a step-local error, or\n * `fieldState.error?.message` from a `FormField` render — the markup is the same\n * whichever of them gates the step, so a step can move between them without its\n * fields changing. (Inside a react-hook-form form, `FormItem` + `FormLabel` from\n * `@eifi1/ui-kit/rhf` do the same job with the ids taken from the form.)\n *\n * The error is NOT `role=\"alert\"`, for the reason the kit's `error` prop gives (see\n * `useFieldError` in components/ui.tsx): it is read when focus reaches the control.\n */\nimport { useId, type ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Label } from \"./ui\";\n\n/** What a `Field` hands its render-prop children, to spread on the control. */\nexport interface FieldControlProps {\n /** The id the label's `htmlFor` points at. */\n id: string;\n /** The ids of the hint and the error that are rendered, or `undefined` when\n * neither is. */\n \"aria-describedby\": string | undefined;\n /** `true` while the field has an error, `undefined` otherwise. */\n \"aria-invalid\": true | undefined;\n /** `true` when the field is `required`. The label's star is `aria-hidden`; this\n * is what a screen reader hears instead. */\n \"aria-required\": true | undefined;\n}\n\n/**\n * The second argument of a `Field`'s render-prop children: what a GROUP needs that\n * a single control does not. `htmlFor` names an `<input>`, but a `<label>` pointing\n * at a `role=\"radiogroup\"` div names nothing — so a group (a {@link ToggleGroup})\n * takes `aria-labelledby={labelId}` instead. Kept out of {@link FieldControlProps}\n * so the spread every existing control takes is unchanged.\n */\nexport interface FieldRenderMeta {\n /** The label element's id, or `undefined` when the field has no label. */\n labelId: string | undefined;\n}\n\n/**\n * Where the label is visible. `\"visible\"` (default); `\"sr-only\"`: never drawn, still\n * the control's name; `\"below-sm\"` / `\"below-md\"` / `\"below-lg\"`: drawn only under\n * that breakpoint and screen-reader-only from it up.\n *\n * For table-like editors — kastlan's journal entry lines\n * (accounting/components/wizard/journal-entry-lines-step.tsx) — where a desktop\n * row sits under column headers that already say \"Account\" and \"Amount\", but the\n * phone layout stacks each line as a card and needs the labels back. The name stays\n * on the control at every width; only the ink changes.\n */\nexport type FieldLabelVisibility = \"visible\" | \"sr-only\" | \"below-sm\" | \"below-md\" | \"below-lg\";\n\n// Literal class strings so Tailwind's scanner sees each one.\nconst LABEL_VISIBILITY: Record<FieldLabelVisibility, string | undefined> = {\n visible: undefined,\n \"sr-only\": \"sr-only\",\n \"below-sm\": \"sm:sr-only\",\n \"below-md\": \"md:sr-only\",\n \"below-lg\": \"lg:sr-only\",\n};\n\nexport interface FieldProps {\n /** The label above the control. Omit it for a group whose control is labelled\n * some other way; the hint and error still render. */\n label?: ReactNode;\n /** Draws the kit's required mark after the label, and sets `aria-required` on the\n * render-prop control. */\n required?: boolean;\n /** Muted helper text below the control. */\n hint?: ReactNode;\n /** The error below the control. `null`, `false` and `\"\"` are no error — what a\n * `touched && errors.x` evaluates to on the happy path. */\n error?: ReactNode;\n /** The control's id. Defaults to a generated one; set it when the control carries\n * its own id (plain children). */\n htmlFor?: string;\n /** Dims the label with its control. */\n disabled?: boolean;\n /** The label's size: `sm` for a dense row, `md` otherwise. */\n labelSize?: \"sm\" | \"md\";\n /** See {@link FieldLabelVisibility}. */\n labelVisibility?: FieldLabelVisibility;\n /** Wrapper className. */\n className?: string;\n /** The control. A function receives {@link FieldControlProps} to spread on it, and\n * {@link FieldRenderMeta} (the label's id, for a group). */\n children: ReactNode | ((control: FieldControlProps, meta: FieldRenderMeta) => ReactNode);\n}\n\nconst isShown = (node: ReactNode) =>\n node !== undefined && node !== null && node !== false && node !== \"\";\n\nexport function Field({\n label,\n required,\n hint,\n error,\n htmlFor,\n disabled,\n labelSize,\n labelVisibility = \"visible\",\n className,\n children,\n}: FieldProps) {\n const generated = useId();\n const id = htmlFor ?? `${generated}-control`;\n const hintId = `${generated}-hint`;\n const errorId = `${generated}-error`;\n const labelId = `${generated}-label`;\n const hasHint = isShown(hint);\n const hasError = isShown(error);\n const describedBy = [hasHint && hintId, hasError && errorId].filter(Boolean).join(\" \");\n const control: FieldControlProps = {\n id,\n \"aria-describedby\": describedBy || undefined,\n \"aria-invalid\": hasError || undefined,\n \"aria-required\": required || undefined,\n };\n return (\n <div\n data-slot=\"field\"\n className={cn(\n \"grid gap-1.5\",\n // A hidden label is `position: absolute`; `relative` keeps its containing\n // block here rather than the page's (see sr-only-containment.test).\n labelVisibility !== \"visible\" && \"relative\",\n className,\n )}\n >\n {label !== undefined && (\n <Label\n id={labelId}\n htmlFor={id}\n required={required}\n disabled={disabled}\n size={labelSize}\n data-error={hasError || undefined}\n className={cn(\"data-[error=true]:text-[var(--danger)]\", LABEL_VISIBILITY[labelVisibility])}\n >\n {label}\n </Label>\n )}\n {typeof children === \"function\"\n ? children(control, { labelId: label !== undefined ? labelId : undefined })\n : children}\n {hasHint && (\n <p id={hintId} className=\"text-[11px] leading-tight text-[var(--text-muted)]\">\n {hint}\n </p>\n )}\n {hasError && (\n <p id={errorId} className=\"text-[11px] leading-tight text-[var(--danger)]\">\n {error}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";AAqJI,SAWI,KAXJ;AA9GJ,SAAS,aAA6B;AACtC,SAAS,UAAU;AACnB,SAAS,aAAa;AA0CtB,MAAM,mBAAqE;AAAA,EACzE,SAAS;AAAA,EACT,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,YAAY;AACd;AA8BA,MAAM,UAAU,CAAC,SACf,SAAS,UAAa,SAAS,QAAQ,SAAS,SAAS,SAAS;AAE7D,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA;AACF,GAAe;AACb,QAAM,YAAY,MAAM;AACxB,QAAM,KAAK,WAAW,GAAG,SAAS;AAClC,QAAM,SAAS,GAAG,SAAS;AAC3B,QAAM,UAAU,GAAG,SAAS;AAC5B,QAAM,UAAU,GAAG,SAAS;AAC5B,QAAM,UAAU,QAAQ,IAAI;AAC5B,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,cAAc,CAAC,WAAW,QAAQ,YAAY,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AACrF,QAAM,UAA6B;AAAA,IACjC;AAAA,IACA,oBAAoB,eAAe;AAAA,IACnC,gBAAgB,YAAY;AAAA,IAC5B,iBAAiB,YAAY;AAAA,EAC/B;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA;AAAA;AAAA,QAGA,oBAAoB,aAAa;AAAA,QACjC;AAAA,MACF;AAAA,MAEC;AAAA,kBAAU,UACT;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,SAAS;AAAA,YACT;AAAA,YACA;AAAA,YACA,MAAM;AAAA,YACN,cAAY,YAAY;AAAA,YACxB,WAAW,GAAG,0CAA0C,iBAAiB,eAAe,CAAC;AAAA,YAExF;AAAA;AAAA,QACH;AAAA,QAED,OAAO,aAAa,aACjB,SAAS,SAAS,EAAE,SAAS,UAAU,SAAY,UAAU,OAAU,CAAC,IACxE;AAAA,QACH,WACC,oBAAC,OAAE,IAAI,QAAQ,WAAU,sDACtB,gBACH;AAAA,QAED,YACC,oBAAC,OAAE,IAAI,SAAS,WAAU,kDACvB,iBACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}