@eifi1/ui-kit 0.21.0 → 0.23.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 (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/file-dropzone.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, MouseEvent, ReactNode } from \"react\";\nimport { Upload, X } from \"lucide-react\";\nimport { Button, IconButton, Spinner } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useKitFileLabels, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n DEFAULT_FILE_PICKER_LABELS,\n formatAccept,\n judgePick,\n screenFiles,\n summariseRejections,\n} from \"./file-button\";\nimport type { FilePickHandler, FilePickerLabels, FileRejection } from \"./file-button\";\nimport { useDragTarget } from \"../hooks/use-file-drop\";\nimport { toast } from \"./toast\";\n\n/**\n * Where a refused file's message goes.\n *\n * - `\"toast\"` — a `sonner` toast (the 0.5 behaviour, kept as the default for a caller\n * that handles nothing itself so no existing dropzone goes quiet on upgrade).\n * - `\"inline\"` — under the drop target, in the danger colour, tied to it with\n * `aria-describedby` (plus `data-invalid` for styling), and spoken through a live\n * region. Cleared by the next accepted pick.\n * - `\"none\"` — nothing visible; the caller shows it from `onReject` / `onInvalid`.\n * Still spoken, so a host rendering it should not ALSO make it `role=\"alert\"`.\n */\nexport type FileDropzoneRejectionFeedback = \"toast\" | \"inline\" | \"none\";\n\n/** What a custom {@link FileDropzoneProps.renderBody} is handed. */\nexport interface FileDropzoneState {\n /** A file drag is over the zone right now. */\n dragOver: boolean;\n disabled: boolean;\n busy: boolean;\n /** What is chosen: `files` in multiple mode, `[file]` or `[]` in single mode. */\n files: readonly File[];\n /** The inline refusal, when `rejectionFeedback=\"inline\"` has one to show. */\n error: string | null;\n /** Open the system picker — for a body that brings its own trigger (a second\n * \"Take photo\" button). Does nothing while `disabled` or `busy`. */\n open: () => void;\n /** The resolved `filePicker` strings, so a custom body speaks the same language. */\n labels: FilePickerLabels;\n}\n\n/**\n * `onInvalid` is omitted from the `<div>` attributes and kept as this component's own.\n * The DOM event of that name belongs to form validation and takes a `FormEvent`; this\n * one takes the rejected `File`, and the kit's meaning is the one every caller already\n * writes. Everything else a `<div>` takes reaches the root — which carries\n * `role=\"group\"` named by `dropLabel`, so that is also where an `aria-label` or an\n * `aria-describedby` belongs.\n *\n * Two modes. SINGLE (the default, and all of 0.5): `file` + `onFileSelected`. MULTIPLE\n * (`multiple`): `files` + `onFilesSelected`. They are optional rather than a union so\n * `FileDropzoneProps` stays an interface a caller can extend; a single-mode pick also\n * reaches `onFilesSelected` as a one-element array, so one handler can serve both.\n */\nexport interface FileDropzoneProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onInvalid\"> {\n /** Single mode: the chosen file, echoed with its size. */\n file?: File | null;\n /** Single mode: called with the accepted file. */\n onFileSelected?: (file: File) => void;\n /** Take several files per pick or drop. Without it a drop of several keeps the first. */\n multiple?: boolean;\n /** Multiple mode: the chosen files, listed in the zone. */\n files?: readonly File[];\n /** Every accepted file of one pick, in order. Never called with an empty array. */\n onFilesSelected?: (files: File[]) => void;\n /**\n * Show a remove button inside the zone once something is chosen (kastlan asked for\n * it: the only way to un-choose a file was to reload). Single mode: removes the file.\n * Multiple mode: a \"remove all\" button. The host clears its own state here — the\n * zone is controlled. Focus moves to the Browse button, and the removal is announced.\n */\n onClear?: () => void;\n /** Multiple mode: a remove button per listed file. */\n onRemove?: (file: File, index: number) => void;\n accept: string;\n /**\n * The caller's check. Optional since 0.6: without it the zone checks `accept` itself\n * (a drop ignores the dialog's filter). With it, `accept` is NOT re-checked — every\n * 0.5 caller's `isValid` is its own type check, and doubling it could refuse a file\n * the caller accepts (a `.zip` the OS labelled `application/octet-stream`).\n */\n isValid?: (file: File) => boolean;\n /** The message for an `isValid` refusal. Defaults to `labels.rejectedInvalid`. */\n invalidMessage?: string;\n /** Bytes; larger files are refused. */\n maxSize?: number;\n /** Multiple mode: files one pick may add. Pass what is LEFT for a running cap. */\n maxFiles?: number;\n /**\n * Called once per refused file. The 0.5 hook, kept as it was; {@link onReject}\n * carries the reason and the translated message as well.\n *\n * Its existence used to be what switched the toast off, and it still does (see\n * {@link rejectionFeedback}). The toast is the kit's `toast`, which loads `sonner`\n * lazily: it is an OPTIONAL peer, and a static import here once broke\n * `import { Button }` for every app without it (see `optional-peer-imports.test.tsx`).\n */\n onInvalid?: (file: File) => void;\n /** Every refused file of one pick, with its reason and message. */\n onReject?: (rejections: FileRejection[]) => void;\n /** The whole pick at once, before any of the above — return `false` to refuse all\n * of it (see {@link FilePickHandler}). A refused pick selects nothing; its one\n * message (`labels.rejectedPick`) goes where `rejectionFeedback` says. */\n onPick?: FilePickHandler;\n /** See {@link FileDropzoneRejectionFeedback}. Defaults to `\"toast\"` when neither\n * `onInvalid` nor `onReject` is given, `\"none\"` when one is. */\n rejectionFeedback?: FileDropzoneRejectionFeedback;\n /** Per-instance overrides of the `filePicker` label namespace. */\n labels?: Partial<FilePickerLabels>;\n /** The dropzone's accessible name. A caller's own `aria-label` wins over it — see\n * the root element below. Optional since 0.8, like the three below: each falls back\n * to its `filePicker` string from `<UiKitProvider labels>`, then to English. They\n * were required, so every call site passed four strings through `t()` by hand, and a\n * zone nested in a kit component (the feedback form) had no call site to pass them. */\n dropLabel?: string;\n /** Default `filePicker.browse`. */\n browseLabel?: string;\n /** What the zone says while nothing is chosen. Default `filePicker.empty` /\n * `emptyMultiple`. */\n emptyLabel?: string;\n /** The second line, e.g. what may be dropped. Default `filePicker.hint(accept)`. */\n hint?: string;\n /**\n * Not taking files: no drop, no picker, no remove buttons; `aria-disabled` on the\n * group. keksdose locks its invoice zone while another form holds unsaved changes,\n * and hand-rolled the whole zone to be able to (invoice-upload.tsx, \"Locked\").\n */\n disabled?: boolean;\n /**\n * Working on what it was given — an upload, an OCR pass. Inert like `disabled`, plus\n * `aria-busy`, a spinner in place of the upload icon, and `filePicker.busy` as the\n * text. The other half of why keksdose's invoice zone was hand-rolled.\n */\n busy?: boolean;\n /**\n * Replace the zone's body — the icon, the text and the chosen-file list — with your\n * own, given the zone's state: keksdose's spinner-plus-\"uploading\", its multi-shot\n * hint (\"several photos are possible\"), a second trigger for the camera.\n *\n * The Browse button, the inline error and the live regions stay: Browse is the\n * keyboard path, and a body that forgot it would leave the zone unreachable without\n * a pointer. A click on a control inside the body does not also open the picker.\n */\n renderBody?: (state: FileDropzoneState) => ReactNode;\n /** Extra classes for the dropzone's root. */\n className?: string;\n}\n\n/** Drag-and-drop file picker shared by the import wizards (YNAB zip, CAMT xml) and\n * kastlan's document forms. Refusals go to `onReject`/`onInvalid` and to\n * `rejectionFeedback`; the chosen file(s) are echoed with their size, otherwise\n * `emptyLabel` + `hint` describe what to drop. For a trigger with no drop area, see\n * `FileButton`. */\nexport function FileDropzone({\n file,\n onFileSelected,\n multiple,\n files,\n onFilesSelected,\n onClear,\n onRemove,\n accept,\n isValid,\n invalidMessage,\n maxSize,\n maxFiles,\n onInvalid,\n onReject,\n onPick,\n rejectionFeedback,\n labels: labelsProp,\n dropLabel: dropLabelProp,\n browseLabel: browseLabelProp,\n emptyLabel: emptyLabelProp,\n hint: hintProp,\n disabled,\n busy,\n renderBody,\n className,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": describedByProp,\n ...rest\n}: FileDropzoneProps) {\n const [error, setError] = useState<string | null>(null);\n const inert = Boolean(disabled || busy);\n // State rather than a ref: `open` is handed to `renderBody` during render, and a\n // function that reads a ref there is exactly what the refs rule cannot prove safe.\n const [fileInput, setFileInput] = useState<HTMLInputElement | null>(null);\n const browseRef = useRef<HTMLButtonElement>(null);\n const errorId = useId();\n // The size through the kit's `file.size` label (the provider's locale by default).\n // This was `${n / 1024} KB`: ASCII digits, an English unit, and a binary kilobyte\n // mislabelled as a decimal one, all in one template literal.\n const fileText = useKitFileLabels();\n const labels = useKitLabels(\"filePicker\", DEFAULT_FILE_PICKER_LABELS, labelsProp);\n // Two regions, two urgencies: a pick or a removal is news, a refusal is a failure.\n // The echoed file name is only text in the zone, and text changing somewhere on the\n // page is not announced by itself.\n const status = useAnnounce();\n const alert = useAnnounce({ politeness: \"assertive\" });\n const feedback: FileDropzoneRejectionFeedback =\n rejectionFeedback ?? (onInvalid || onReject ? \"none\" : \"toast\");\n const dropLabel = dropLabelProp ?? labels.dropzone;\n const browseLabel = browseLabelProp ?? labels.browse;\n const emptyLabel = emptyLabelProp ?? (multiple ? labels.emptyMultiple : labels.empty);\n const hint = hintProp ?? labels.hint(formatAccept(accept));\n\n const acceptFiles = (list: ArrayLike<File> | null | undefined) => {\n if (inert || !list || list.length === 0) return;\n const all = Array.from(list);\n const picked = multiple ? all : all.slice(0, 1);\n const { accepted, rejected } = screenFiles(\n picked,\n {\n accept: isValid ? undefined : accept,\n maxSize,\n maxFiles: multiple ? maxFiles : undefined,\n isValid,\n invalidMessage,\n },\n labels,\n fileText.size,\n );\n const stands = judgePick(onPick, accepted, rejected);\n if (stands && accepted.length > 0) {\n setError(null);\n if (multiple) {\n onFilesSelected?.(accepted);\n } else {\n onFileSelected?.(accepted[0]);\n onFilesSelected?.(accepted);\n }\n status.announce(labels.selected(accepted.length, accepted[0].name));\n }\n if (!stands || rejected.length > 0) {\n for (const r of rejected) onInvalid?.(r.file);\n if (rejected.length > 0) onReject?.(rejected);\n const message = stands\n ? summariseRejections(rejected, labels)\n : labels.rejectedPick(picked.length);\n if (feedback === \"toast\") {\n // The kit's `toast`, which loads sonner (an optional peer) lazily, so an app\n // that never trips this never has to install it. Nothing downstream depends\n // on the toast having appeared. The toaster is a live region, so this path\n // does not announce a second time.\n toast.error(message);\n } else {\n if (feedback === \"inline\") setError(message);\n alert.announce(message);\n }\n }\n };\n\n // The remove buttons unmount with the file they removed, and focus would fall to\n // <body>. Browse is where the next action (pick again) starts.\n const afterRemoval = (message: string) => {\n browseRef.current?.focus();\n status.announce(message);\n };\n\n // The enter/leave counter lives in the shared hook: the boolean this was flickered\n // across every child of the zone in Safari, whose drag events carry no\n // `relatedTarget` to tell a move onto a child from a real leave.\n const { dropProps, isOver: dragOver } = useDragTarget<HTMLDivElement>({\n disabled: inert,\n onDropFiles: acceptFiles,\n });\n const open = () => {\n if (!inert) fileInput?.click();\n };\n\n const chosen: readonly File[] = multiple ? (files ?? []) : file ? [file] : [];\n const showError = feedback === \"inline\" && error !== null;\n const describedBy = cn(describedByProp, showError && errorId) || undefined;\n\n return (\n <>\n {/* A named group, NOT a `role=\"button\"`. It was one, with real buttons (Browse,\n the remove buttons) inside it: nested interactive controls, which ARIA forbids\n because a button's children are presentational — a screen reader either read\n the zone as one opaque button or skipped the inner ones, and the zone was an\n extra Tab stop that did what Browse does. Now the keyboard path is Browse, a\n real button; the zone keeps its drop target and its click-anywhere for a\n pointer, which is a convenience on top of Browse rather than the only way in. */}\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions -- the click is a pointer shortcut for Browse, see above */}\n <div\n // `...rest` first: every handler below is the drop gesture itself.\n {...rest}\n {...dropProps}\n // Pointer only; every control inside stops its own click from reaching here\n // (and one in a custom body is skipped below). The keyboard equivalent is the\n // Browse button, so no key handler belongs on a group.\n onClick={(e) => {\n if (fromControl(e)) return;\n open();\n }}\n role=\"group\"\n // The DOM spelling wins over `dropLabel`, which stays the name for every caller\n // that passes no `aria-label` — i.e. all of them today. Standardising on\n // `aria-label` (audit §api-design) does not get to silently rename an existing\n // required prop.\n aria-label={ariaLabel ?? dropLabel}\n // No `aria-invalid`: ARIA does not allow it on `role=\"group\"`. The message is\n // reached through the description instead, and spoken when it appears.\n aria-describedby={describedBy}\n data-invalid={showError || undefined}\n aria-disabled={disabled || undefined}\n aria-busy={busy || undefined}\n data-drag-over={dragOver || undefined}\n className={cn(\n // `relative` is load-bearing, not cosmetic. The file input below is `sr-only`,\n // which Tailwind implements as `position: absolute` — so without a positioned\n // ancestor its containing block is the INITIAL one, and it is laid out at its\n // own offset from the top of the document. Nothing looks wrong, because the\n // input is 1x1 and clipped; what breaks is the page height. On a long page\n // every escaped sr-only element extends `documentElement.scrollHeight` to its\n // own offset, which produces a second, whole-document scrollbar alongside the\n // app shell's own — one that scrolls past the end of the content into nothing.\n // Measured on the showcase: body 900px, document 47,919px.\n \"relative flex flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed px-4 py-6 text-center transition-colors\",\n // Three states out of two border tokens: the target rests on the plain\n // hairline, hover pulls it to `--border-strong`, and a live drag keeps that\n // border and adds the `--bg-active` wash on top — so \"let go here\" still reads\n // one step louder than \"you are over it\". A refusal shown inline takes the\n // danger border until the next good pick.\n // Inert zones keep the resting hairline and lose the hover pull: a border that\n // answers the pointer promises a drop the zone will refuse.\n disabled\n ? \"cursor-not-allowed border-[var(--border)] opacity-60\"\n : busy\n ? \"cursor-progress border-[var(--border)]\"\n : dragOver\n ? \"cursor-pointer border-[var(--border-strong)] bg-[var(--bg-active)]\"\n : showError\n ? \"cursor-pointer border-[var(--danger-border-strong)]\"\n : \"cursor-pointer border-[var(--border)] hover:border-[var(--border-strong)]\",\n className,\n )}\n >\n {renderBody ? (\n renderBody({\n dragOver,\n disabled: Boolean(disabled),\n busy: Boolean(busy),\n files: chosen,\n error: showError ? error : null,\n open,\n labels,\n })\n ) : busy ? (\n <>\n {/* `label={null}`: the zone is `aria-busy` and the line below says the\n same in words; a spoken \"Loading\" would only run into it. */}\n <Spinner label={null} className=\"size-5\" />\n <div className=\"text-sm text-[var(--text-secondary)]\">{labels.busy}</div>\n </>\n ) : (\n <>\n <Upload aria-hidden className=\"size-5 text-[var(--text-muted)]\" />\n {multiple && chosen.length > 0 ? (\n <ul className=\"flex w-full max-w-sm flex-col gap-1 text-start\">\n {chosen.map((f, i) => (\n <li\n // Name + size + position: two files may share a name, and the index\n // alone would re-key every row below a removal.\n key={`${f.name}-${f.size}-${i}`}\n className=\"flex items-center gap-2 text-sm text-[var(--text-secondary)]\"\n >\n <span className=\"min-w-0 flex-1 truncate font-medium\">{f.name}</span>\n <span className=\"shrink-0 text-xs text-[var(--text-muted)]\">{fileText.size(f.size)}</span>\n {onRemove && (\n <IconButton\n type=\"button\"\n size=\"sm\"\n disabled={inert}\n aria-label={labels.remove(f.name)}\n onClick={(e) => {\n e.stopPropagation();\n onRemove(f, i);\n afterRemoval(labels.removed(f.name));\n }}\n >\n <X aria-hidden />\n </IconButton>\n )}\n </li>\n ))}\n </ul>\n ) : (\n <>\n <div className=\"flex items-center gap-1 text-sm text-[var(--text-secondary)]\">\n {/* `!multiple`: in multiple mode an empty `files` list is \"nothing chosen\",\n whatever a stray `file` prop says — it used to show that file's name. */}\n {!multiple && file ? <span className=\"font-medium\">{file.name}</span> : emptyLabel}\n {!multiple && file && onClear && (\n <IconButton\n type=\"button\"\n size=\"sm\"\n disabled={inert}\n aria-label={labels.remove(file.name)}\n onClick={(e) => {\n e.stopPropagation();\n onClear();\n afterRemoval(labels.removed(file.name));\n }}\n >\n <X aria-hidden />\n </IconButton>\n )}\n </div>\n <div className=\"text-xs text-[var(--text-muted)]\">\n {file && !multiple ? fileText.size(file.size) : hint}\n </div>\n </>\n )}\n </>\n )}\n <div className=\"flex flex-wrap items-center justify-center gap-2\">\n <Button\n ref={browseRef}\n type=\"button\"\n variant=\"secondary\"\n disabled={inert}\n onClick={(e) => {\n e.stopPropagation();\n open();\n }}\n >\n {browseLabel}\n </Button>\n {multiple && chosen.length > 0 && onClear && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n disabled={inert}\n onClick={(e) => {\n e.stopPropagation();\n onClear();\n afterRemoval(labels.cleared);\n }}\n >\n {labels.clearAll}\n </Button>\n )}\n </div>\n {showError && (\n <p id={errorId} className=\"text-xs text-[var(--danger)]\">\n {error}\n </p>\n )}\n <input\n ref={setFileInput}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n disabled={inert}\n onChange={(e) => {\n // Copy, THEN reset: `files` is a live view of the value, and without the\n // reset picking the same file a second time fires no `change` at all —\n // the retry after a failed upload silently did nothing (kastlan).\n const picked = Array.from(e.currentTarget.files ?? []);\n e.currentTarget.value = \"\";\n acceptFiles(picked);\n }}\n // Clicks are forwarded here programmatically; one reaching the zone's own\n // handler again would open the dialog a second time.\n onClick={(e) => e.stopPropagation()}\n className=\"sr-only\"\n />\n </div>\n {/* Outside the zone, where they have always been (the zone used to be a\n `role=\"button\"`, whose content is presentational). `sr-only-fixed`, so they\n need no positioned ancestor. */}\n <span {...status.regionProps} />\n <span {...alert.regionProps} />\n </>\n );\n}\n\n/** A click that landed on a control inside the zone (a custom body's own button, a\n * link in the hint) is that control's, not a request to open the picker. */\nfunction fromControl(e: MouseEvent<HTMLElement>): boolean {\n const target = e.target as Element | null;\n const control = target?.closest?.(\"button, a[href], input, select, textarea, label, [role='button']\");\n return Boolean(control && control !== e.currentTarget && e.currentTarget.contains(control));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/file-dropzone.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, MouseEvent, ReactNode } from \"react\";\nimport { Upload, X } from \"lucide-react\";\nimport { Button, IconButton, Spinner } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useKitFileLabels, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n DEFAULT_FILE_PICKER_LABELS,\n formatAccept,\n judgePick,\n screenFiles,\n summariseRejections,\n} from \"./file-button\";\nimport type { FilePickHandler, FilePickerLabels, FileRejection } from \"./file-button\";\nimport { useDragTarget } from \"../hooks/use-file-drop\";\nimport { toast } from \"./toast\";\nimport { useCommitReason } from \"./write-lock\";\n\n/**\n * Where a refused file's message goes.\n *\n * - `\"toast\"` — a `sonner` toast (the 0.5 behaviour, kept as the default for a caller\n * that handles nothing itself so no existing dropzone goes quiet on upgrade).\n * - `\"inline\"` — under the drop target, in the danger colour, tied to it with\n * `aria-describedby` (plus `data-invalid` for styling), and spoken through a live\n * region. Cleared by the next accepted pick.\n * - `\"none\"` — nothing visible; the caller shows it from `onReject` / `onInvalid`.\n * Still spoken, so a host rendering it should not ALSO make it `role=\"alert\"`.\n */\nexport type FileDropzoneRejectionFeedback = \"toast\" | \"inline\" | \"none\";\n\n/** What a custom {@link FileDropzoneProps.renderBody} is handed. */\nexport interface FileDropzoneState {\n /** A file drag is over the zone right now. */\n dragOver: boolean;\n disabled: boolean;\n busy: boolean;\n /** 0.22: locked with a reason (`disabledReason`, or `commit` under a locked\n * {@link WriteLockProvider}) — takes no files, like `disabled`. */\n locked: boolean;\n /** The reason while `locked`, for a custom body to show; `undefined` otherwise. */\n disabledReason: ReactNode | undefined;\n /** What is chosen: `files` in multiple mode, `[file]` or `[]` in single mode. */\n files: readonly File[];\n /** The inline refusal, when `rejectionFeedback=\"inline\"` has one to show. */\n error: string | null;\n /** Open the system picker — for a body that brings its own trigger (a second\n * \"Take photo\" button). Does nothing while `disabled` or `busy`. */\n open: () => void;\n /** The resolved `filePicker` strings, so a custom body speaks the same language. */\n labels: FilePickerLabels;\n}\n\n/**\n * `onInvalid` is omitted from the `<div>` attributes and kept as this component's own.\n * The DOM event of that name belongs to form validation and takes a `FormEvent`; this\n * one takes the rejected `File`, and the kit's meaning is the one every caller already\n * writes. Everything else a `<div>` takes reaches the root — which carries\n * `role=\"group\"` named by `dropLabel`, so that is also where an `aria-label` or an\n * `aria-describedby` belongs.\n *\n * Two modes. SINGLE (the default, and all of 0.5): `file` + `onFileSelected`. MULTIPLE\n * (`multiple`): `files` + `onFilesSelected`. They are optional rather than a union so\n * `FileDropzoneProps` stays an interface a caller can extend; a single-mode pick also\n * reaches `onFilesSelected` as a one-element array, so one handler can serve both.\n */\nexport interface FileDropzoneProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onInvalid\"> {\n /** Single mode: the chosen file, echoed with its size. */\n file?: File | null;\n /** Single mode: called with the accepted file. */\n onFileSelected?: (file: File) => void;\n /** Take several files per pick or drop. Without it a drop of several keeps the first. */\n multiple?: boolean;\n /** Multiple mode: the chosen files, listed in the zone. */\n files?: readonly File[];\n /** Every accepted file of one pick, in order. Never called with an empty array. */\n onFilesSelected?: (files: File[]) => void;\n /**\n * Show a remove button inside the zone once something is chosen (kastlan asked for\n * it: the only way to un-choose a file was to reload). Single mode: removes the file.\n * Multiple mode: a \"remove all\" button. The host clears its own state here — the\n * zone is controlled. Focus moves to the Browse button, and the removal is announced.\n */\n onClear?: () => void;\n /** Multiple mode: a remove button per listed file. */\n onRemove?: (file: File, index: number) => void;\n accept: string;\n /**\n * The caller's check. Optional since 0.6: without it the zone checks `accept` itself\n * (a drop ignores the dialog's filter). With it, `accept` is NOT re-checked — every\n * 0.5 caller's `isValid` is its own type check, and doubling it could refuse a file\n * the caller accepts (a `.zip` the OS labelled `application/octet-stream`).\n */\n isValid?: (file: File) => boolean;\n /** The message for an `isValid` refusal. Defaults to `labels.rejectedInvalid`. */\n invalidMessage?: string;\n /** Bytes; larger files are refused. */\n maxSize?: number;\n /** Multiple mode: files one pick may add. Pass what is LEFT for a running cap. */\n maxFiles?: number;\n /**\n * Called once per refused file. The 0.5 hook, kept as it was; {@link onReject}\n * carries the reason and the translated message as well.\n *\n * Its existence used to be what switched the toast off, and it still does (see\n * {@link rejectionFeedback}). The toast is the kit's `toast`, which loads `sonner`\n * lazily: it is an OPTIONAL peer, and a static import here once broke\n * `import { Button }` for every app without it (see `optional-peer-imports.test.tsx`).\n */\n onInvalid?: (file: File) => void;\n /** Every refused file of one pick, with its reason and message. */\n onReject?: (rejections: FileRejection[]) => void;\n /** The whole pick at once, before any of the above — return `false` to refuse all\n * of it (see {@link FilePickHandler}). A refused pick selects nothing; its one\n * message (`labels.rejectedPick`) goes where `rejectionFeedback` says. */\n onPick?: FilePickHandler;\n /** See {@link FileDropzoneRejectionFeedback}. Defaults to `\"toast\"` when neither\n * `onInvalid` nor `onReject` is given, `\"none\"` when one is. */\n rejectionFeedback?: FileDropzoneRejectionFeedback;\n /** Per-instance overrides of the `filePicker` label namespace. */\n labels?: Partial<FilePickerLabels>;\n /** The dropzone's accessible name. A caller's own `aria-label` wins over it — see\n * the root element below. Optional since 0.8, like the three below: each falls back\n * to its `filePicker` string from `<UiKitProvider labels>`, then to English. They\n * were required, so every call site passed four strings through `t()` by hand, and a\n * zone nested in a kit component (the feedback form) had no call site to pass them. */\n dropLabel?: string;\n /** Default `filePicker.browse`. */\n browseLabel?: string;\n /** What the zone says while nothing is chosen. Default `filePicker.empty` /\n * `emptyMultiple`. */\n emptyLabel?: string;\n /** The second line, e.g. what may be dropped. Default `filePicker.hint(accept)`. */\n hint?: string;\n /**\n * Not taking files: no drop, no picker, no remove buttons; `aria-disabled` on the\n * group. keksdose locks its invoice zone while another form holds unsaved changes,\n * and hand-rolled the whole zone to be able to (invoice-upload.tsx, \"Locked\").\n */\n disabled?: boolean;\n /**\n * Working on what it was given — an upload, an OCR pass. Inert like `disabled`, plus\n * `aria-busy`, a spinner in place of the upload icon, and `filePicker.busy` as the\n * text. The other half of why keksdose's invoice zone was hand-rolled.\n */\n busy?: boolean;\n /**\n * Why the zone is not taking files — the write lock's sentence, a quota, a closed\n * period (keksdose K3). Takes no drop, no picker and no removal, like `disabled`, and\n * SAYS so: the Browse button (and every remove button) stays focusable,\n * `aria-disabled`, with the reason in the kit Tooltip and its description, the zone's\n * group is described by it, and the default body shows it where the hint was. Not\n * dimmed: what is chosen stays readable. Wins over `disabled`'s silent look.\n */\n disabledReason?: ReactNode;\n /**\n * This zone COMMITS — an upload that starts on drop. Under a locked\n * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's\n * reason (which wins over one of its own). No provider, or an unlocked one: no\n * effect. A zone that only stages a file for a form saved later is not a commit.\n */\n commit?: boolean;\n /**\n * Replace the zone's body — the icon, the text and the chosen-file list — with your\n * own, given the zone's state: keksdose's spinner-plus-\"uploading\", its multi-shot\n * hint (\"several photos are possible\"), a second trigger for the camera.\n *\n * The Browse button, the inline error and the live regions stay: Browse is the\n * keyboard path, and a body that forgot it would leave the zone unreachable without\n * a pointer. A click on a control inside the body does not also open the picker.\n */\n renderBody?: (state: FileDropzoneState) => ReactNode;\n /** Extra classes for the dropzone's root. */\n className?: string;\n}\n\n/** Drag-and-drop file picker shared by the import wizards (YNAB zip, CAMT xml) and\n * kastlan's document forms. Refusals go to `onReject`/`onInvalid` and to\n * `rejectionFeedback`; the chosen file(s) are echoed with their size, otherwise\n * `emptyLabel` + `hint` describe what to drop. For a trigger with no drop area, see\n * `FileButton`. */\nexport function FileDropzone({\n file,\n onFileSelected,\n multiple,\n files,\n onFilesSelected,\n onClear,\n onRemove,\n accept,\n isValid,\n invalidMessage,\n maxSize,\n maxFiles,\n onInvalid,\n onReject,\n onPick,\n rejectionFeedback,\n labels: labelsProp,\n dropLabel: dropLabelProp,\n browseLabel: browseLabelProp,\n emptyLabel: emptyLabelProp,\n hint: hintProp,\n disabled,\n busy,\n disabledReason: ownDisabledReason,\n commit,\n renderBody,\n className,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": describedByProp,\n ...rest\n}: FileDropzoneProps) {\n const [error, setError] = useState<string | null>(null);\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasReason(disabledReason);\n // Locked counts as inert for every file path (drop, click, Browse, remove); only the\n // LOOK and the native `disabled` keep it apart, so its controls stay focusable.\n const inert = Boolean(disabled || busy) || locked;\n const reasonId = useId();\n // State rather than a ref: `open` is handed to `renderBody` during render, and a\n // function that reads a ref there is exactly what the refs rule cannot prove safe.\n const [fileInput, setFileInput] = useState<HTMLInputElement | null>(null);\n const browseRef = useRef<HTMLButtonElement>(null);\n const errorId = useId();\n // The size through the kit's `file.size` label (the provider's locale by default).\n // This was `${n / 1024} KB`: ASCII digits, an English unit, and a binary kilobyte\n // mislabelled as a decimal one, all in one template literal.\n const fileText = useKitFileLabels();\n const labels = useKitLabels(\"filePicker\", DEFAULT_FILE_PICKER_LABELS, labelsProp);\n // Two regions, two urgencies: a pick or a removal is news, a refusal is a failure.\n // The echoed file name is only text in the zone, and text changing somewhere on the\n // page is not announced by itself.\n const status = useAnnounce();\n const alert = useAnnounce({ politeness: \"assertive\" });\n const feedback: FileDropzoneRejectionFeedback =\n rejectionFeedback ?? (onInvalid || onReject ? \"none\" : \"toast\");\n const dropLabel = dropLabelProp ?? labels.dropzone;\n const browseLabel = browseLabelProp ?? labels.browse;\n const emptyLabel = emptyLabelProp ?? (multiple ? labels.emptyMultiple : labels.empty);\n const hint = hintProp ?? labels.hint(formatAccept(accept));\n\n const acceptFiles = (list: ArrayLike<File> | null | undefined) => {\n if (inert || !list || list.length === 0) return;\n const all = Array.from(list);\n const picked = multiple ? all : all.slice(0, 1);\n const { accepted, rejected } = screenFiles(\n picked,\n {\n accept: isValid ? undefined : accept,\n maxSize,\n maxFiles: multiple ? maxFiles : undefined,\n isValid,\n invalidMessage,\n },\n labels,\n fileText.size,\n );\n const stands = judgePick(onPick, accepted, rejected);\n if (stands && accepted.length > 0) {\n setError(null);\n if (multiple) {\n onFilesSelected?.(accepted);\n } else {\n onFileSelected?.(accepted[0]);\n onFilesSelected?.(accepted);\n }\n status.announce(labels.selected(accepted.length, accepted[0].name));\n }\n if (!stands || rejected.length > 0) {\n for (const r of rejected) onInvalid?.(r.file);\n if (rejected.length > 0) onReject?.(rejected);\n const message = stands\n ? summariseRejections(rejected, labels)\n : labels.rejectedPick(picked.length);\n if (feedback === \"toast\") {\n // The kit's `toast`, which loads sonner (an optional peer) lazily, so an app\n // that never trips this never has to install it. Nothing downstream depends\n // on the toast having appeared. The toaster is a live region, so this path\n // does not announce a second time.\n toast.error(message);\n } else {\n if (feedback === \"inline\") setError(message);\n alert.announce(message);\n }\n }\n };\n\n // The remove buttons unmount with the file they removed, and focus would fall to\n // <body>. Browse is where the next action (pick again) starts.\n const afterRemoval = (message: string) => {\n browseRef.current?.focus();\n status.announce(message);\n };\n\n // The enter/leave counter lives in the shared hook: the boolean this was flickered\n // across every child of the zone in Safari, whose drag events carry no\n // `relatedTarget` to tell a move onto a child from a real leave.\n const { dropProps, isOver: dragOver } = useDragTarget<HTMLDivElement>({\n disabled: inert,\n onDropFiles: acceptFiles,\n });\n const open = () => {\n if (!inert) fileInput?.click();\n };\n\n const chosen: readonly File[] = multiple ? (files ?? []) : file ? [file] : [];\n const showError = feedback === \"inline\" && error !== null;\n const describedBy = cn(describedByProp, showError && errorId, locked && reasonId) || undefined;\n // The controls inside: natively disabled while disabled or busy; locked, they take\n // the reason instead and stay reachable (Button's `disabledReason` path).\n const controlDisabled = Boolean(disabled || busy);\n const controlReason = locked ? disabledReason : undefined;\n const reasonLine = locked && (\n <div data-slot=\"file-dropzone-reason\" className=\"text-xs text-[var(--text-muted)]\">\n {disabledReason}\n </div>\n );\n\n return (\n <>\n {/* A named group, NOT a `role=\"button\"`. It was one, with real buttons (Browse,\n the remove buttons) inside it: nested interactive controls, which ARIA forbids\n because a button's children are presentational — a screen reader either read\n the zone as one opaque button or skipped the inner ones, and the zone was an\n extra Tab stop that did what Browse does. Now the keyboard path is Browse, a\n real button; the zone keeps its drop target and its click-anywhere for a\n pointer, which is a convenience on top of Browse rather than the only way in. */}\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions -- the click is a pointer shortcut for Browse, see above */}\n <div\n // `...rest` first: every handler below is the drop gesture itself.\n {...rest}\n {...dropProps}\n // Pointer only; every control inside stops its own click from reaching here\n // (and one in a custom body is skipped below). The keyboard equivalent is the\n // Browse button, so no key handler belongs on a group.\n onClick={(e) => {\n if (fromControl(e)) return;\n open();\n }}\n role=\"group\"\n // The DOM spelling wins over `dropLabel`, which stays the name for every caller\n // that passes no `aria-label` — i.e. all of them today. Standardising on\n // `aria-label` (audit §api-design) does not get to silently rename an existing\n // required prop.\n aria-label={ariaLabel ?? dropLabel}\n // No `aria-invalid`: ARIA does not allow it on `role=\"group\"`. The message is\n // reached through the description instead, and spoken when it appears.\n aria-describedby={describedBy}\n data-invalid={showError || undefined}\n aria-disabled={disabled || locked || undefined}\n aria-busy={busy || undefined}\n data-drag-over={dragOver || undefined}\n className={cn(\n // `relative` is load-bearing, not cosmetic. The file input below is `sr-only`,\n // which Tailwind implements as `position: absolute` — so without a positioned\n // ancestor its containing block is the INITIAL one, and it is laid out at its\n // own offset from the top of the document. Nothing looks wrong, because the\n // input is 1x1 and clipped; what breaks is the page height. On a long page\n // every escaped sr-only element extends `documentElement.scrollHeight` to its\n // own offset, which produces a second, whole-document scrollbar alongside the\n // app shell's own — one that scrolls past the end of the content into nothing.\n // Measured on the showcase: body 900px, document 47,919px.\n \"relative flex flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed px-4 py-6 text-center transition-colors\",\n // Three states out of two border tokens: the target rests on the plain\n // hairline, hover pulls it to `--border-strong`, and a live drag keeps that\n // border and adds the `--bg-active` wash on top — so \"let go here\" still reads\n // one step louder than \"you are over it\". A refusal shown inline takes the\n // danger border until the next good pick.\n // Inert zones keep the resting hairline and lose the hover pull: a border that\n // answers the pointer promises a drop the zone will refuse.\n disabled\n ? \"cursor-not-allowed border-[var(--border)] opacity-60\"\n : busy\n ? \"cursor-progress border-[var(--border)]\"\n : locked\n ? \"cursor-not-allowed border-[var(--border)]\"\n : dragOver\n ? \"cursor-pointer border-[var(--border-strong)] bg-[var(--bg-active)]\"\n : showError\n ? \"cursor-pointer border-[var(--danger-border-strong)]\"\n : \"cursor-pointer border-[var(--border)] hover:border-[var(--border-strong)]\",\n className,\n )}\n >\n {renderBody ? (\n renderBody({\n dragOver,\n disabled: Boolean(disabled),\n busy: Boolean(busy),\n locked,\n disabledReason: locked ? disabledReason : undefined,\n files: chosen,\n error: showError ? error : null,\n open,\n labels,\n })\n ) : busy ? (\n <>\n {/* `label={null}`: the zone is `aria-busy` and the line below says the\n same in words; a spoken \"Loading\" would only run into it. */}\n <Spinner label={null} className=\"size-5\" />\n <div className=\"text-sm text-[var(--text-secondary)]\">{labels.busy}</div>\n </>\n ) : (\n <>\n <Upload aria-hidden className=\"size-5 text-[var(--text-muted)]\" />\n {multiple && chosen.length > 0 ? (\n <>\n <ul className=\"flex w-full max-w-sm flex-col gap-1 text-start\">\n {chosen.map((f, i) => (\n <li\n // Name + size + position: two files may share a name, and the index\n // alone would re-key every row below a removal.\n key={`${f.name}-${f.size}-${i}`}\n className=\"flex items-center gap-2 text-sm text-[var(--text-secondary)]\"\n >\n <span className=\"min-w-0 flex-1 truncate font-medium\">{f.name}</span>\n <span className=\"shrink-0 text-xs text-[var(--text-muted)]\">{fileText.size(f.size)}</span>\n {onRemove && (\n <IconButton\n type=\"button\"\n size=\"sm\"\n disabled={controlDisabled}\n disabledReason={controlReason}\n aria-label={labels.remove(f.name)}\n onClick={(e) => {\n e.stopPropagation();\n onRemove(f, i);\n afterRemoval(labels.removed(f.name));\n }}\n >\n <X aria-hidden />\n </IconButton>\n )}\n </li>\n ))}\n </ul>\n {reasonLine}\n </>\n ) : (\n <>\n <div className=\"flex items-center gap-1 text-sm text-[var(--text-secondary)]\">\n {/* `!multiple`: in multiple mode an empty `files` list is \"nothing chosen\",\n whatever a stray `file` prop says — it used to show that file's name. */}\n {!multiple && file ? <span className=\"font-medium\">{file.name}</span> : emptyLabel}\n {!multiple && file && onClear && (\n <IconButton\n type=\"button\"\n size=\"sm\"\n disabled={controlDisabled}\n disabledReason={controlReason}\n aria-label={labels.remove(file.name)}\n onClick={(e) => {\n e.stopPropagation();\n onClear();\n afterRemoval(labels.removed(file.name));\n }}\n >\n <X aria-hidden />\n </IconButton>\n )}\n </div>\n {/* Locked, the reason takes the hint's place: what may be dropped is moot\n while nothing may. A chosen file keeps its size, the reason below. */}\n {locked && !(file && !multiple) ? (\n reasonLine\n ) : (\n <div className=\"text-xs text-[var(--text-muted)]\">\n {file && !multiple ? fileText.size(file.size) : hint}\n </div>\n )}\n {locked && file && !multiple && reasonLine}\n </>\n )}\n </>\n )}\n <div className=\"flex flex-wrap items-center justify-center gap-2\">\n <Button\n ref={browseRef}\n type=\"button\"\n variant=\"secondary\"\n disabled={controlDisabled}\n disabledReason={controlReason}\n onClick={(e) => {\n e.stopPropagation();\n open();\n }}\n >\n {browseLabel}\n </Button>\n {multiple && chosen.length > 0 && onClear && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n disabled={controlDisabled}\n disabledReason={controlReason}\n onClick={(e) => {\n e.stopPropagation();\n onClear();\n afterRemoval(labels.cleared);\n }}\n >\n {labels.clearAll}\n </Button>\n )}\n </div>\n {showError && (\n <p id={errorId} className=\"text-xs text-[var(--danger)]\">\n {error}\n </p>\n )}\n {/* The group's description while locked: a copy of its own, so a custom body\n that does not show the reason still has the zone say it. */}\n {locked && (\n <span id={reasonId} hidden>\n {disabledReason}\n </span>\n )}\n <input\n ref={setFileInput}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n disabled={inert}\n onChange={(e) => {\n // Copy, THEN reset: `files` is a live view of the value, and without the\n // reset picking the same file a second time fires no `change` at all —\n // the retry after a failed upload silently did nothing (kastlan).\n const picked = Array.from(e.currentTarget.files ?? []);\n e.currentTarget.value = \"\";\n acceptFiles(picked);\n }}\n // Clicks are forwarded here programmatically; one reaching the zone's own\n // handler again would open the dialog a second time.\n onClick={(e) => e.stopPropagation()}\n className=\"sr-only\"\n />\n </div>\n {/* Outside the zone, where they have always been (the zone used to be a\n `role=\"button\"`, whose content is presentational). `sr-only-fixed`, so they\n need no positioned ancestor. */}\n <span {...status.regionProps} />\n <span {...alert.regionProps} />\n </>\n );\n}\n\n/** A reason that is really there — `null`, `false` and `\"\"` are no reason. */\nfunction hasReason(reason: ReactNode): boolean {\n return reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n}\n\n/** A click that landed on a control inside the zone (a custom body's own button, a\n * link in the hint) is that control's, not a request to open the picker. */\nfunction fromControl(e: MouseEvent<HTMLElement>): boolean {\n const target = e.target as Element | null;\n const control = target?.closest?.(\"button, a[href], input, select, textarea, label, [role='button']\");\n return Boolean(control && control !== e.currentTarget && e.currentTarget.contains(control));\n}\n"],"mappings":"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","names":[]}
@@ -1,15 +1,19 @@
1
1
  import 'react';
2
2
  import 'lucide-react';
3
- export { Q as DEFAULT_FORM_ACTIONS_LABELS, an as FormActions, ao as FormActionsAlign, ap as FormActionsDestructive, aq as FormActionsLabels, ar as FormActionsPlacement, as as FormActionsProps, at as FormActionsStickyWithin, au as FormActionsSubmitProps, aO as ResponsiveFormActionsPlacement } from '../kit-labels-D1J2_jyD.js';
4
- import './data-table-labels.js';
3
+ export { a7 as DEFAULT_FORM_ACTIONS_LABELS, aW as FormActions, aX as FormActionsAlign, aY as FormActionsDestructive, aZ as FormActionsLabels, a_ as FormActionsPlacement, a$ as FormActionsProps, b0 as FormActionsStickyWithin, b1 as FormActionsSubmitProps, b2 as FormActionsSubmitShortcut, bu as ResponsiveFormActionsPlacement } from '../feedback-BxeQVzwq.js';
4
+ import '../feedback-attachment-fGAzZPf0.js';
5
+ import '../feedback/feedback-inbox.js';
6
+ import './tooltip.js';
7
+ import '../hooks/use-anchored-rect.js';
8
+ import '../lib/clipping.js';
9
+ import '../data-table-labels-B7OdnM0S.js';
10
+ import './data-table-sort.js';
11
+ import './swipeable-row.js';
5
12
  import './mini-calendar.js';
6
13
  import './calendar-heatmap.js';
7
14
  import './popover.js';
8
15
  import './chip.js';
9
16
  import './field-sync.js';
10
- import './tooltip.js';
11
- import '../hooks/use-anchored-rect.js';
12
- import '../lib/clipping.js';
13
17
  import '../wizard/types.js';
14
18
  import '../tour/tour.js';
15
19
  import '../search/command-palette.js';
@@ -23,7 +27,6 @@ import './sparkline.js';
23
27
  import './stat-tile.js';
24
28
  import './signature-pad.js';
25
29
  import './password-strength.js';
26
- import './danger-confirm.js';
27
30
  import './swatch-picker.js';
28
31
  import './tile-radio.js';
29
32
  import './icon-picker.js';
@@ -31,11 +34,7 @@ import './dialog-frame.js';
31
34
  import './modal.js';
32
35
  import '../hooks/use-search-param-state.js';
33
36
  import './measured-grid.js';
34
- import '../feedback-attachment-WFiXJ8We.js';
35
- import '../feedback/feedback-thread.js';
36
- import '../feedback/feedback-inbox.js';
37
37
  import './account-settings-labels.js';
38
- import './confirm-dialog.js';
39
38
  import './floating-panel.js';
40
39
  import './bulk-action-bar.js';
41
40
  import './list.js';
@@ -54,11 +53,17 @@ import './write-lock.js';
54
53
  import './legal.js';
55
54
  import './account-chips.js';
56
55
  import '../lib/format.js';
57
- import '../data-table-3mi2R3so.js';
58
- import './data-table-sort.js';
59
- import './swipeable-row.js';
60
56
  import './reauth-dialog.js';
61
57
  import './server-wake.js';
62
58
  import '../lib/server-wake.js';
63
59
  import './translation-review-labels.js';
64
60
  import '../lib/translation-review.js';
61
+ import './country-select.js';
62
+ import './inline-edit-field.js';
63
+ import '../lib/iban.js';
64
+ import '../lib/phone.js';
65
+ import './sign-chip.js';
66
+ import './column-mapper.js';
67
+ import '../lib/column-mapping.js';
68
+ import '../lib/table-text.js';
69
+ import './field-parts.js';
@@ -1,13 +1,16 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { isValidElement } from "react";
3
+ import { isValidElement, useCallback, useEffect, useRef } from "react";
4
4
  import { cn } from "../lib/cn.js";
5
5
  import { useKitLabels } from "../i18n/kit-labels.js";
6
6
  import { useMediaQuery } from "../hooks/use-media-query.js";
7
+ import { isApplePlatform } from "../hooks/use-hotkey.js";
7
8
  import { Button, Spinner } from "./ui.js";
9
+ import { useWriteLock } from "./write-lock.js";
8
10
  const DEFAULT_FORM_ACTIONS_LABELS = {
9
11
  save: "Save",
10
- cancel: "Cancel"
12
+ cancel: "Cancel",
13
+ submitShortcut: (apple) => apple ? "\u2318 Enter" : "Ctrl+Enter"
11
14
  };
12
15
  const BREAKPOINTS = [
13
16
  ["xl", "(min-width: 1280px)"],
@@ -43,6 +46,16 @@ const PLACEMENT_CLASS = {
43
46
  sticky: "sticky bottom-0 z-10 border-t border-[var(--border)] bg-[var(--bg-surface)] pt-3",
44
47
  dialog: ""
45
48
  };
49
+ function assignRef(ref, el) {
50
+ if (typeof ref === "function") ref(el);
51
+ else if (ref) ref.current = el;
52
+ }
53
+ function shortcutScope(row, formId) {
54
+ if (!row) return null;
55
+ const named = formId ? row.ownerDocument.getElementById(formId) : null;
56
+ if (named instanceof HTMLFormElement) return named;
57
+ return row.closest("form") ?? row.closest('[role="dialog"]') ?? row.parentElement;
58
+ }
46
59
  function isDestructiveData(value) {
47
60
  return typeof value === "object" && value !== null && !isValidElement(value) && "onClick" in value;
48
61
  }
@@ -59,6 +72,8 @@ function FormActions({
59
72
  submitDisabledReason,
60
73
  commit = false,
61
74
  submitVariant = "brand",
75
+ cancelVariant = "secondary",
76
+ size = "md",
62
77
  destructive,
63
78
  start: startSlot,
64
79
  align,
@@ -66,12 +81,44 @@ function FormActions({
66
81
  stickyWithin = "viewport",
67
82
  bleed,
68
83
  form,
84
+ submitShortcut,
85
+ submitShortcutHint = false,
69
86
  children,
70
87
  className,
71
88
  style,
72
89
  ...rest
73
90
  }) {
74
91
  const labels = useKitLabels("form", DEFAULT_FORM_ACTIONS_LABELS);
92
+ const lock = useWriteLock();
93
+ const rootRef = useRef(null);
94
+ const saveRef = useRef(null);
95
+ const callerSaveRef = submitProps?.ref;
96
+ const setSaveRef = useCallback(
97
+ (el) => {
98
+ saveRef.current = el;
99
+ assignRef(callerSaveRef, el);
100
+ },
101
+ [callerSaveRef]
102
+ );
103
+ const canSave = !pending && !submitDisabled && !(commit && lock.locked);
104
+ const canSaveRef = useRef(canSave);
105
+ useEffect(() => {
106
+ canSaveRef.current = canSave;
107
+ });
108
+ useEffect(() => {
109
+ if (submitShortcut !== "mod-enter") return;
110
+ const onKeyDown = (e) => {
111
+ if (e.key !== "Enter" || !(e.ctrlKey || e.metaKey) || e.shiftKey || e.altKey) return;
112
+ if (e.repeat || e.isComposing || e.defaultPrevented || !canSaveRef.current) return;
113
+ const scope = shortcutScope(rootRef.current, form);
114
+ if (!scope || !(e.target instanceof Node) || !scope.contains(e.target)) return;
115
+ e.preventDefault();
116
+ saveRef.current?.click();
117
+ };
118
+ document.addEventListener("keydown", onKeyDown);
119
+ return () => document.removeEventListener("keydown", onKeyDown);
120
+ }, [submitShortcut, form]);
121
+ const apple = submitShortcut ? isApplePlatform() : false;
75
122
  const placement = useResolvedPlacement(placementProp);
76
123
  const bleedLength = bleed === void 0 || bleed === "" ? void 0 : cssLength(bleed);
77
124
  const hasDestructive = destructive !== void 0 && destructive !== null && destructive !== false;
@@ -83,6 +130,7 @@ function FormActions({
83
130
  type: "button",
84
131
  variant: "ghost",
85
132
  tone: "danger",
133
+ size,
86
134
  onClick: destructive.onClick,
87
135
  disabled: destructive.disabled,
88
136
  disabledReason: destructive.disabledReason,
@@ -91,9 +139,12 @@ function FormActions({
91
139
  }
92
140
  ) : destructive;
93
141
  const saveLabel = submitLabel ?? labels.save;
142
+ const saveSize = submitProps?.size ?? size;
143
+ const glyphClass = saveSize === "sm" ? "size-3.5" : "size-4";
94
144
  return /* @__PURE__ */ jsxs(
95
145
  "div",
96
146
  {
147
+ ref: rootRef,
97
148
  "data-slot": "form-actions",
98
149
  "data-placement": placement,
99
150
  "data-sticky-within": placement === "sticky" ? stickyWithin : void 0,
@@ -121,22 +172,35 @@ function FormActions({
121
172
  ] }),
122
173
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
123
174
  children,
124
- onCancel && /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, children: cancelLabel ?? labels.cancel }),
175
+ onCancel && /* @__PURE__ */ jsx(Button, { type: "button", variant: cancelVariant, size, onClick: onCancel, children: cancelLabel ?? labels.cancel }),
125
176
  /* @__PURE__ */ jsxs(
126
177
  Button,
127
178
  {
128
179
  ...submitProps,
180
+ ref: setSaveRef,
181
+ "aria-keyshortcuts": submitShortcut === "mod-enter" ? "Control+Enter Meta+Enter" : submitProps?.["aria-keyshortcuts"],
129
182
  type: onSubmit ? "button" : "submit",
130
183
  form,
131
184
  onClick: onSubmit,
132
185
  variant: submitVariant,
186
+ size: saveSize,
133
187
  disabled: pending || submitDisabled,
134
188
  disabledReason: pending ? void 0 : submitDisabledReason,
135
189
  commit: commit && !pending,
136
190
  "aria-busy": pending || void 0,
137
191
  children: [
138
- pending ? /* @__PURE__ */ jsx(Spinner, { label: null, className: "size-4" }) : SubmitIcon && /* @__PURE__ */ jsx(SubmitIcon, { "aria-hidden": true, className: "size-4 shrink-0" }),
139
- pending && pendingLabel !== void 0 ? pendingLabel : saveLabel
192
+ pending ? /* @__PURE__ */ jsx(Spinner, { label: null, className: glyphClass }) : SubmitIcon && /* @__PURE__ */ jsx(SubmitIcon, { "aria-hidden": true, className: cn(glyphClass, "shrink-0") }),
193
+ pending && pendingLabel !== void 0 ? pendingLabel : saveLabel,
194
+ submitShortcut && submitShortcutHint && // Hidden from the accessibility tree: `aria-keyshortcuts` already says it,
195
+ // and in the name it would make the button "Save Ctrl+Enter".
196
+ /* @__PURE__ */ jsx(
197
+ "kbd",
198
+ {
199
+ "aria-hidden": true,
200
+ className: "ms-1 font-sans text-[0.85em] font-normal whitespace-nowrap opacity-70 pointer-coarse:hidden",
201
+ children: labels.submitShortcut(apple)
202
+ }
203
+ )
140
204
  ]
141
205
  }
142
206
  )
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/form-actions.tsx"],"sourcesContent":["import { isValidElement } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { Button, Spinner, type ButtonProps, type ButtonVariant } from \"./ui\";\n\n/** The words {@link FormActions} renders on its own behalf — the `form` namespace. */\nexport interface FormActionsLabels {\n save: string;\n cancel: string;\n}\n\nexport const DEFAULT_FORM_ACTIONS_LABELS: FormActionsLabels = {\n save: \"Save\",\n cancel: \"Cancel\",\n};\n\nexport type FormActionsAlign = \"start\" | \"center\" | \"end\" | \"between\";\n\n/**\n * Where the row sits:\n * - `inline` (default): under the form's last field, `pt-4` above it.\n * - `sticky`: stuck to the bottom of the scrolling page or pane, on the surface\n * colour with a rule above, clear of the phone's home indicator — for a long form\n * whose Save should not be a scroll away.\n * - `dialog`: no spacing of its own, for a dialog's footer slot (DialogFrame,\n * FullBleedDialog's `footer`), which already pads and rules it.\n */\nexport type FormActionsPlacement = \"inline\" | \"sticky\" | \"dialog\";\n\n/**\n * A placement per breakpoint, mobile first — `{ base: \"sticky\", md: \"inline\" }` is a\n * Save row stuck to the bottom of a phone's long form and in the flow under the last\n * field from 768px up, where the form fits. The breakpoints are Tailwind's (sm 640,\n * md 768, lg 1024, xl 1280px), as `BulkActionBar`'s `variant` breakpoints are, and\n * resolved the same way, in JS: the sticky row is positioned by inline style, which a\n * `md:` class cannot reach. keksdose switched `placement` on its own media query at\n * every long form for want of this. The breakpoints are the VIEWPORT's, not the\n * container's: a form in a narrow pane on a wide screen resolves to `md`.\n */\nexport interface ResponsiveFormActionsPlacement {\n base: FormActionsPlacement;\n sm?: FormActionsPlacement;\n md?: FormActionsPlacement;\n lg?: FormActionsPlacement;\n xl?: FormActionsPlacement;\n}\n\nconst BREAKPOINTS = [\n [\"xl\", \"(min-width: 1280px)\"],\n [\"lg\", \"(min-width: 1024px)\"],\n [\"md\", \"(min-width: 768px)\"],\n [\"sm\", \"(min-width: 640px)\"],\n] as const;\n\n/** The placement in force: the widest breakpoint that matches AND names one, else\n * `base`. The queries are subscribed unconditionally (hooks cannot be skipped); a\n * plain string ignores them. Without `matchMedia` (SSR, tests) none match, so the row\n * renders its `base`, the phone's. The same resolution as BulkActionBar's variant. */\nfunction useResolvedPlacement(\n placement: FormActionsPlacement | ResponsiveFormActionsPlacement,\n): FormActionsPlacement {\n const matches = {\n xl: useMediaQuery(BREAKPOINTS[0][1], false),\n lg: useMediaQuery(BREAKPOINTS[1][1], false),\n md: useMediaQuery(BREAKPOINTS[2][1], false),\n sm: useMediaQuery(BREAKPOINTS[3][1], false),\n };\n if (typeof placement === \"string\") return placement;\n for (const [key] of BREAKPOINTS) {\n const p = placement[key];\n if (matches[key] && p !== undefined) return p;\n }\n return placement.base;\n}\n\n/** A CSS length: a number is pixels. */\nfunction cssLength(value: string | number): string {\n return typeof value === \"number\" ? `${value}px` : value;\n}\n\n/**\n * What a `sticky` row sticks to — see {@link FormActionsProps.stickyWithin}.\n * - `viewport` (default): the page's (or AppShell pane's) bottom edge, lifted onto\n * the phone's bottom nav by `--app-nav-h`.\n * - `container`: the bottom of whatever scroll container it is in — a dialog body, a\n * side pane, a panel with `overflow-y-auto` — at `bottom: 0`, with no nav offset.\n */\nexport type FormActionsStickyWithin = \"viewport\" | \"container\";\n\n/**\n * Extra attributes for the save button — an `id`, `data-*` hooks for tests or\n * analytics, an `aria-describedby`. The props FormActions drives itself (type, click,\n * disabled, variant, form, children, the busy state) are not among them.\n */\nexport type FormActionsSubmitProps = Omit<\n ButtonProps,\n \"type\" | \"onClick\" | \"disabled\" | \"disabledReason\" | \"variant\" | \"form\" | \"children\" | \"aria-busy\" | \"commit\"\n> & { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/** The start-side action of {@link FormActionsProps.destructive}, as data. */\nexport interface FormActionsDestructive {\n label: ReactNode;\n onClick: () => void;\n disabled?: boolean;\n /** See Button's `disabledReason`. */\n disabledReason?: ReactNode;\n}\n\nconst ALIGN_CLASS: Record<FormActionsAlign, string> = {\n start: \"justify-start\",\n center: \"justify-center\",\n end: \"justify-end\",\n between: \"justify-between\",\n};\n\nconst PLACEMENT_CLASS: Record<FormActionsPlacement, string> = {\n inline: \"pt-4\",\n sticky: \"sticky bottom-0 z-10 border-t border-[var(--border)] bg-[var(--bg-surface)] pt-3\",\n dialog: \"\",\n};\n\nexport interface FormActionsProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n /** Called by Cancel. Omitted: no Cancel button (a form that only saves). */\n onCancel?: () => void;\n /**\n * Given: the save button is `type=\"button\"` and calls this — for a dialog or a row\n * that saves from a handler. Omitted: it is `type=\"submit\"` and submits the\n * enclosing `<form>` (or the one named by {@link form}).\n */\n onSubmit?: () => void;\n /** The save button's text. Default `form.save` from the provider, else \"Save\". */\n submitLabel?: ReactNode;\n /** The cancel button's text. Default `form.cancel` from the provider, else \"Cancel\". */\n cancelLabel?: ReactNode;\n /** The save button's text while `pending` (\"Saving…\"). Default: `submitLabel`. */\n pendingLabel?: ReactNode;\n /**\n * A Lucide icon before the save button's text. While `pending` the spinner takes\n * ITS place rather than being added in front of it — an icon written into\n * `submitLabel` itself stays, so the button would show the spinner AND the glyph,\n * two statuses side by side (keksdose's crop Apply needed a `pendingLabel` copy of\n * its text only to drop the check mark).\n */\n submitIcon?: LucideIcon;\n /** Attributes passed through to the save button: `id`, `data-*`, `aria-*`, a\n * `className`. See {@link FormActionsSubmitProps}. */\n submitProps?: FormActionsSubmitProps;\n /**\n * The save is running: a spinner in the save button, `aria-busy` on it, and the\n * button disabled, so a second click cannot send the form twice. Cancel stays\n * usable. The spinner goes in front of the text, or in place of {@link submitIcon}.\n */\n pending?: boolean;\n /** Disable save for a reason of the form's own (an unbalanced entry). */\n submitDisabled?: boolean;\n /** Why save is disabled — see Button's `disabledReason`. Keeps it focusable. */\n submitDisabledReason?: ReactNode;\n /**\n * Save is a commit: under a locked {@link WriteLockProvider} it is disabled with the\n * lock's reason (over `submitDisabledReason`), focusable, the reason in its tooltip —\n * Button's `commit`. The `{ label, onClick }` form of {@link destructive} is a\n * commit too and is locked with it; Cancel and `children` are not (closing a form\n * writes nothing). No provider, or an unlocked one: no effect. Default `false`.\n *\n * keksdose wrote `submitDisabled={lock.locked || …}` and\n * `submitDisabledReason={lock.locked ? lock.reason : undefined}` at each form; this is\n * that pair, read from the provider.\n */\n commit?: boolean;\n /** The save button's variant. Default `brand`; `danger` for a save that destroys\n * (kastlan's `destructive` flag on its own FormActions). */\n submitVariant?: ButtonVariant;\n /**\n * A destructive action at the START of the row, apart from Save and Cancel — the\n * \"Delete\" of an edit dialog. Pass `{ label, onClick }` for the kit's quiet danger\n * button, or an element of your own (a {@link DangerConfirm}, say). With it the row\n * is `between`-aligned unless `align` says otherwise.\n */\n destructive?: ReactNode | FormActionsDestructive;\n /**\n * Something neutral at the START of the row — a caption (\"Last saved 2 min ago\",\n * \"Drag the corners to crop\"), a \"View activity\" link. After {@link destructive}\n * when both are given; like it, makes the row `between`-aligned unless `align` says\n * otherwise. `children` are for further ACTIONS, beside Cancel.\n */\n start?: ReactNode;\n /** Horizontal alignment. Default `end` (`between` with a `destructive` action). */\n align?: FormActionsAlign;\n /** See {@link FormActionsPlacement}, or one per breakpoint — see\n * {@link ResponsiveFormActionsPlacement}. Default `inline`. */\n placement?: FormActionsPlacement | ResponsiveFormActionsPlacement;\n /**\n * While the row is `sticky`: pull it out over its container's inline padding by this\n * much (a CSS length, a number in px) and give the same back as padding — so the\n * rule above it and its surface run edge to edge, while the buttons stay on the\n * content's line. Without it a sticky row in a padded card or pane is a strip inset\n * from both sides, with the content scrolling past it in the gutters (keksdose wrote\n * `@max-md:-mx-3 @max-md:px-3` by hand). Pass the container's padding (`\"0.75rem\"`,\n * `12`, `\"var(--pane-px)\"`). Applied only while the resolved placement is sticky, so\n * `placement={{ base: \"sticky\", md: \"inline\" }}` bleeds on the phone alone. The margin\n * is `calc(-1 * X)`; jsdom folds that to `calc(-X)`, so match it loosely in tests.\n */\n bleed?: string | number;\n /**\n * With `placement=\"sticky\"`: what the row sticks to. Default `viewport`.\n *\n * CSS sticky always sticks to the NEAREST scroll container, so the default row\n * already holds inside a scrolling pane — but it offsets itself by `--app-nav-h`,\n * which AppShell publishes on the document: inside a pane or a portalled dialog\n * body on a phone it then floats a nav's height above that container's bottom,\n * with the content scrolling through the gap. `container` drops that offset.\n *\n * What neither value can fix, because it is the layout, not the row:\n * - sticky moves only within its PARENT box. Inside a DataTable expansion row the\n * parent is the expansion cell, so the row pins to the table's own scroller\n * (`overflow-auto`, bounded by `maxBodyHeight`) and only while the cell spans\n * that scroller's bottom edge — not to the page.\n * - any `overflow` other than `visible`/`clip` between the row and the scroller\n * you mean (an `overflow-x-auto` wrapper, an `overflow-hidden` card) becomes the\n * scroll container; if THAT box does not scroll, the row never sticks.\n * - the offset is resolved against the scroller's content box, so a scroller with\n * bottom padding leaves that strip uncovered; give the row a negative `bottom`\n * through `style` and the same amount back as padding.\n */\n stickyWithin?: FormActionsStickyWithin;\n /** The `id` of the `<form>` the save button submits, when the row is rendered\n * outside it (a dialog's footer slot). */\n form?: string;\n /** Extra actions, placed before Cancel. */\n children?: ReactNode;\n}\n\nfunction isDestructiveData(value: unknown): value is FormActionsDestructive {\n return typeof value === \"object\" && value !== null && !isValidElement(value) && \"onClick\" in value;\n}\n\n/**\n * The Save / Cancel row every form, wizard-step form and dialog ends in — kastlan's\n * `FormActions`, with the labels from the provider, a pending state and a place for\n * the destructive action.\n *\n * ```tsx\n * <form onSubmit={form.handleSubmit(save)}>\n * …\n * <FormActions onCancel={close} pending={mutation.isPending} />\n * </form>\n * ```\n *\n * The spinner is the kit's {@link Spinner}, drawn inside the button with `label={null}`\n * so the button's name stays its text; `aria-busy` is what says it is working.\n */\nexport function FormActions({\n onCancel,\n onSubmit,\n submitLabel,\n cancelLabel,\n pendingLabel,\n submitIcon: SubmitIcon,\n submitProps,\n pending = false,\n submitDisabled = false,\n submitDisabledReason,\n commit = false,\n submitVariant = \"brand\",\n destructive,\n start: startSlot,\n align,\n placement: placementProp = \"inline\",\n stickyWithin = \"viewport\",\n bleed,\n form,\n children,\n className,\n style,\n ...rest\n}: FormActionsProps) {\n const labels = useKitLabels(\"form\", DEFAULT_FORM_ACTIONS_LABELS);\n const placement = useResolvedPlacement(placementProp);\n const bleedLength = bleed === undefined || bleed === \"\" ? undefined : cssLength(bleed);\n const hasDestructive = destructive !== undefined && destructive !== null && destructive !== false;\n const hasStart = startSlot !== undefined && startSlot !== null && startSlot !== false;\n const justify = align ?? (hasDestructive || hasStart ? \"between\" : \"end\");\n const destructiveNode = !hasDestructive ? null : isDestructiveData(destructive) ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n tone=\"danger\"\n onClick={destructive.onClick}\n disabled={destructive.disabled}\n disabledReason={destructive.disabledReason}\n commit={commit}\n >\n {destructive.label}\n </Button>\n ) : (\n destructive\n );\n const saveLabel = submitLabel ?? labels.save;\n\n return (\n <div\n data-slot=\"form-actions\"\n data-placement={placement}\n data-sticky-within={placement === \"sticky\" ? stickyWithin : undefined}\n {...rest}\n style={\n placement === \"sticky\"\n ? {\n paddingBottom: \"max(0.75rem, env(safe-area-inset-bottom))\",\n // On the phone's bottom nav, not behind it (keksdose live #361): `bottom-0`\n // pinned the row under AppShell's nav, which covers the page's last ~56px.\n // `--app-nav-h` is the nav's measured height (0px from md up, and outside an\n // AppShell); the 1px overlaps the two top borders into one line. In a\n // container the nav is not underneath: `--app-nav-h` is the document's,\n // and would hold the row that far above the container's own edge.\n bottom: stickyWithin === \"container\" ? 0 : \"max(0px, calc(var(--app-nav-h, 0px) - 1px))\",\n ...(bleedLength !== undefined && {\n marginInline: `calc(-1 * ${bleedLength})`,\n paddingInline: bleedLength,\n }),\n ...style,\n }\n : style\n }\n className={cn(\"flex flex-wrap items-center gap-2\", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className)}\n >\n {(hasDestructive || hasStart) && (\n <div className=\"me-auto flex min-w-0 flex-wrap items-center gap-2\">\n {destructiveNode}\n {hasStart && startSlot}\n </div>\n )}\n <div className=\"flex flex-wrap items-center gap-2\">\n {children}\n {onCancel && (\n <Button type=\"button\" variant=\"secondary\" onClick={onCancel}>\n {cancelLabel ?? labels.cancel}\n </Button>\n )}\n <Button\n {...submitProps}\n type={onSubmit ? \"button\" : \"submit\"}\n form={form}\n onClick={onSubmit}\n variant={submitVariant}\n disabled={pending || submitDisabled}\n disabledReason={pending ? undefined : submitDisabledReason}\n // Not while pending: the save already left, and the lock's look over the\n // spinner would say it had not.\n commit={commit && !pending}\n aria-busy={pending || undefined}\n >\n {pending ? (\n <Spinner label={null} className=\"size-4\" />\n ) : (\n SubmitIcon && <SubmitIcon aria-hidden className=\"size-4 shrink-0\" />\n )}\n {pending && pendingLabel !== undefined ? pendingLabel : saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AA8RI,cA4CI,YA5CJ;AA9RJ,SAAS,sBAAsB;AAG/B,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,QAAQ,eAAqD;AAQ/D,MAAM,8BAAiD;AAAA,EAC5D,MAAM;AAAA,EACN,QAAQ;AACV;AAiCA,MAAM,cAAc;AAAA,EAClB,CAAC,MAAM,qBAAqB;AAAA,EAC5B,CAAC,MAAM,qBAAqB;AAAA,EAC5B,CAAC,MAAM,oBAAoB;AAAA,EAC3B,CAAC,MAAM,oBAAoB;AAC7B;AAMA,SAAS,qBACP,WACsB;AACtB,QAAM,UAAU;AAAA,IACd,IAAI,cAAc,YAAY,CAAC,EAAE,CAAC,GAAG,KAAK;AAAA,IAC1C,IAAI,cAAc,YAAY,CAAC,EAAE,CAAC,GAAG,KAAK;AAAA,IAC1C,IAAI,cAAc,YAAY,CAAC,EAAE,CAAC,GAAG,KAAK;AAAA,IAC1C,IAAI,cAAc,YAAY,CAAC,EAAE,CAAC,GAAG,KAAK;AAAA,EAC5C;AACA,MAAI,OAAO,cAAc,SAAU,QAAO;AAC1C,aAAW,CAAC,GAAG,KAAK,aAAa;AAC/B,UAAM,IAAI,UAAU,GAAG;AACvB,QAAI,QAAQ,GAAG,KAAK,MAAM,OAAW,QAAO;AAAA,EAC9C;AACA,SAAO,UAAU;AACnB;AAGA,SAAS,UAAU,OAAgC;AACjD,SAAO,OAAO,UAAU,WAAW,GAAG,KAAK,OAAO;AACpD;AA8BA,MAAM,cAAgD;AAAA,EACpD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,SAAS;AACX;AAEA,MAAM,kBAAwD;AAAA,EAC5D,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AACV;AAiHA,SAAS,kBAAkB,OAAiD;AAC1E,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,eAAe,KAAK,KAAK,aAAa;AAC/F;AAiBO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB;AAAA,EACA,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,WAAW,gBAAgB;AAAA,EAC3B,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,aAAa,QAAQ,2BAA2B;AAC/D,QAAM,YAAY,qBAAqB,aAAa;AACpD,QAAM,cAAc,UAAU,UAAa,UAAU,KAAK,SAAY,UAAU,KAAK;AACrF,QAAM,iBAAiB,gBAAgB,UAAa,gBAAgB,QAAQ,gBAAgB;AAC5F,QAAM,WAAW,cAAc,UAAa,cAAc,QAAQ,cAAc;AAChF,QAAM,UAAU,UAAU,kBAAkB,WAAW,YAAY;AACnE,QAAM,kBAAkB,CAAC,iBAAiB,OAAO,kBAAkB,WAAW,IAC5E;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,SAAS,YAAY;AAAA,MACrB,UAAU,YAAY;AAAA,MACtB,gBAAgB,YAAY;AAAA,MAC5B;AAAA,MAEC,sBAAY;AAAA;AAAA,EACf,IAEA;AAEF,QAAM,YAAY,eAAe,OAAO;AAExC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,kBAAgB;AAAA,MAChB,sBAAoB,cAAc,WAAW,eAAe;AAAA,MAC3D,GAAG;AAAA,MACJ,OACE,cAAc,WACV;AAAA,QACE,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOf,QAAQ,iBAAiB,cAAc,IAAI;AAAA,QAC3C,GAAI,gBAAgB,UAAa;AAAA,UAC/B,cAAc,aAAa,WAAW;AAAA,UACtC,eAAe;AAAA,QACjB;AAAA,QACA,GAAG;AAAA,MACL,IACA;AAAA,MAEN,WAAW,GAAG,qCAAqC,YAAY,OAAO,GAAG,gBAAgB,SAAS,GAAG,SAAS;AAAA,MAE5G;AAAA,2BAAkB,aAClB,qBAAC,SAAI,WAAU,qDACZ;AAAA;AAAA,UACA,YAAY;AAAA,WACf;AAAA,QAEF,qBAAC,SAAI,WAAU,qCACZ;AAAA;AAAA,UACA,YACC,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,SAAS,UAChD,yBAAe,OAAO,QACzB;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACE,GAAG;AAAA,cACJ,MAAM,WAAW,WAAW;AAAA,cAC5B;AAAA,cACA,SAAS;AAAA,cACT,SAAS;AAAA,cACT,UAAU,WAAW;AAAA,cACrB,gBAAgB,UAAU,SAAY;AAAA,cAGtC,QAAQ,UAAU,CAAC;AAAA,cACnB,aAAW,WAAW;AAAA,cAErB;AAAA,0BACC,oBAAC,WAAQ,OAAO,MAAM,WAAU,UAAS,IAEzC,cAAc,oBAAC,cAAW,eAAW,MAAC,WAAU,mBAAkB;AAAA,gBAEnE,WAAW,iBAAiB,SAAY,eAAe;AAAA;AAAA;AAAA,UAC1D;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/form-actions.tsx"],"sourcesContent":["import { isValidElement, useCallback, useEffect, useRef } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { isApplePlatform } from \"../hooks/use-hotkey\";\nimport { Button, Spinner, type ButtonProps, type ButtonSize, type ButtonVariant } from \"./ui\";\nimport { useWriteLock } from \"./write-lock\";\n\n/** The words {@link FormActions} renders on its own behalf — the `form` namespace. */\nexport interface FormActionsLabels {\n save: string;\n cancel: string;\n /**\n * The {@link FormActionsProps.submitShortcut} hint beside Save's text, when\n * {@link FormActionsProps.submitShortcutHint} shows it. `apple` is true on a Mac,\n * iPhone or iPad, where the key is Cmd: \"⌘ Enter\" there, \"Ctrl+Enter\" elsewhere\n * (a German keyboard says \"Strg\").\n */\n submitShortcut: (apple: boolean) => string;\n}\n\nexport const DEFAULT_FORM_ACTIONS_LABELS: FormActionsLabels = {\n save: \"Save\",\n cancel: \"Cancel\",\n submitShortcut: (apple) => (apple ? \"⌘ Enter\" : \"Ctrl+Enter\"),\n};\n\n/** See {@link FormActionsProps.submitShortcut}. */\nexport type FormActionsSubmitShortcut = \"mod-enter\";\n\nexport type FormActionsAlign = \"start\" | \"center\" | \"end\" | \"between\";\n\n/**\n * Where the row sits:\n * - `inline` (default): under the form's last field, `pt-4` above it.\n * - `sticky`: stuck to the bottom of the scrolling page or pane, on the surface\n * colour with a rule above, clear of the phone's home indicator — for a long form\n * whose Save should not be a scroll away.\n * - `dialog`: no spacing of its own, for a dialog's footer slot (DialogFrame,\n * FullBleedDialog's `footer`), which already pads and rules it.\n */\nexport type FormActionsPlacement = \"inline\" | \"sticky\" | \"dialog\";\n\n/**\n * A placement per breakpoint, mobile first — `{ base: \"sticky\", md: \"inline\" }` is a\n * Save row stuck to the bottom of a phone's long form and in the flow under the last\n * field from 768px up, where the form fits. The breakpoints are Tailwind's (sm 640,\n * md 768, lg 1024, xl 1280px), as `BulkActionBar`'s `variant` breakpoints are, and\n * resolved the same way, in JS: the sticky row is positioned by inline style, which a\n * `md:` class cannot reach. keksdose switched `placement` on its own media query at\n * every long form for want of this. The breakpoints are the VIEWPORT's, not the\n * container's: a form in a narrow pane on a wide screen resolves to `md`.\n */\nexport interface ResponsiveFormActionsPlacement {\n base: FormActionsPlacement;\n sm?: FormActionsPlacement;\n md?: FormActionsPlacement;\n lg?: FormActionsPlacement;\n xl?: FormActionsPlacement;\n}\n\nconst BREAKPOINTS = [\n [\"xl\", \"(min-width: 1280px)\"],\n [\"lg\", \"(min-width: 1024px)\"],\n [\"md\", \"(min-width: 768px)\"],\n [\"sm\", \"(min-width: 640px)\"],\n] as const;\n\n/** The placement in force: the widest breakpoint that matches AND names one, else\n * `base`. The queries are subscribed unconditionally (hooks cannot be skipped); a\n * plain string ignores them. Without `matchMedia` (SSR, tests) none match, so the row\n * renders its `base`, the phone's. The same resolution as BulkActionBar's variant. */\nfunction useResolvedPlacement(\n placement: FormActionsPlacement | ResponsiveFormActionsPlacement,\n): FormActionsPlacement {\n const matches = {\n xl: useMediaQuery(BREAKPOINTS[0][1], false),\n lg: useMediaQuery(BREAKPOINTS[1][1], false),\n md: useMediaQuery(BREAKPOINTS[2][1], false),\n sm: useMediaQuery(BREAKPOINTS[3][1], false),\n };\n if (typeof placement === \"string\") return placement;\n for (const [key] of BREAKPOINTS) {\n const p = placement[key];\n if (matches[key] && p !== undefined) return p;\n }\n return placement.base;\n}\n\n/** A CSS length: a number is pixels. */\nfunction cssLength(value: string | number): string {\n return typeof value === \"number\" ? `${value}px` : value;\n}\n\n/**\n * What a `sticky` row sticks to — see {@link FormActionsProps.stickyWithin}.\n * - `viewport` (default): the page's (or AppShell pane's) bottom edge, lifted onto\n * the phone's bottom nav by `--app-nav-h`.\n * - `container`: the bottom of whatever scroll container it is in — a dialog body, a\n * side pane, a panel with `overflow-y-auto` — at `bottom: 0`, with no nav offset.\n */\nexport type FormActionsStickyWithin = \"viewport\" | \"container\";\n\n/**\n * Extra attributes for the save button — an `id`, `data-*` hooks for tests or\n * analytics, an `aria-describedby`. The props FormActions drives itself (type, click,\n * disabled, variant, form, children, the busy state) are not among them.\n */\nexport type FormActionsSubmitProps = Omit<\n ButtonProps,\n \"type\" | \"onClick\" | \"disabled\" | \"disabledReason\" | \"variant\" | \"form\" | \"children\" | \"aria-busy\" | \"commit\"\n> & { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/** The start-side action of {@link FormActionsProps.destructive}, as data. */\nexport interface FormActionsDestructive {\n label: ReactNode;\n onClick: () => void;\n disabled?: boolean;\n /** See Button's `disabledReason`. */\n disabledReason?: ReactNode;\n}\n\nconst ALIGN_CLASS: Record<FormActionsAlign, string> = {\n start: \"justify-start\",\n center: \"justify-center\",\n end: \"justify-end\",\n between: \"justify-between\",\n};\n\nconst PLACEMENT_CLASS: Record<FormActionsPlacement, string> = {\n inline: \"pt-4\",\n sticky: \"sticky bottom-0 z-10 border-t border-[var(--border)] bg-[var(--bg-surface)] pt-3\",\n dialog: \"\",\n};\n\nexport interface FormActionsProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n /** Called by Cancel. Omitted: no Cancel button (a form that only saves). */\n onCancel?: () => void;\n /**\n * Given: the save button is `type=\"button\"` and calls this — for a dialog or a row\n * that saves from a handler. Omitted: it is `type=\"submit\"` and submits the\n * enclosing `<form>` (or the one named by {@link form}).\n */\n onSubmit?: () => void;\n /** The save button's text. Default `form.save` from the provider, else \"Save\". */\n submitLabel?: ReactNode;\n /** The cancel button's text. Default `form.cancel` from the provider, else \"Cancel\". */\n cancelLabel?: ReactNode;\n /** The save button's text while `pending` (\"Saving…\"). Default: `submitLabel`. */\n pendingLabel?: ReactNode;\n /**\n * A Lucide icon before the save button's text. While `pending` the spinner takes\n * ITS place rather than being added in front of it — an icon written into\n * `submitLabel` itself stays, so the button would show the spinner AND the glyph,\n * two statuses side by side (keksdose's crop Apply needed a `pendingLabel` copy of\n * its text only to drop the check mark).\n */\n submitIcon?: LucideIcon;\n /** Attributes passed through to the save button: `id`, `data-*`, `aria-*`, a\n * `className`. See {@link FormActionsSubmitProps}. */\n submitProps?: FormActionsSubmitProps;\n /**\n * The save is running: a spinner in the save button, `aria-busy` on it, and the\n * button disabled, so a second click cannot send the form twice. Cancel stays\n * usable. The spinner goes in front of the text, or in place of {@link submitIcon}.\n */\n pending?: boolean;\n /** Disable save for a reason of the form's own (an unbalanced entry). */\n submitDisabled?: boolean;\n /** Why save is disabled — see Button's `disabledReason`. Keeps it focusable. */\n submitDisabledReason?: ReactNode;\n /**\n * Save is a commit: under a locked {@link WriteLockProvider} it is disabled with the\n * lock's reason (over `submitDisabledReason`), focusable, the reason in its tooltip —\n * Button's `commit`. The `{ label, onClick }` form of {@link destructive} is a\n * commit too and is locked with it; Cancel and `children` are not (closing a form\n * writes nothing). No provider, or an unlocked one: no effect. Default `false`.\n *\n * keksdose wrote `submitDisabled={lock.locked || …}` and\n * `submitDisabledReason={lock.locked ? lock.reason : undefined}` at each form; this is\n * that pair, read from the provider.\n */\n commit?: boolean;\n /** The save button's variant. Default `brand`; `danger` for a save that destroys\n * (kastlan's `destructive` flag on its own FormActions). */\n submitVariant?: ButtonVariant;\n /**\n * The Cancel button's variant. Default `secondary` (the outlined button, as before).\n * `ghost` for a row where Cancel should step back further than an outline — keksdose\n * G3: a quiet inline row (a confirm under a table row, a card's own small editor)\n * whose Cancel was a ghost button before it moved onto FormActions, which drew every\n * Cancel outlined.\n */\n cancelVariant?: ButtonVariant;\n /**\n * The size of every button the row draws itself — Save, Cancel and the\n * `{ label, onClick }` form of {@link destructive}: Button's `size`. Default `md`.\n * `sm` (12px text, `px-2 py-1`) for a dense row: a confirm inside a table's expansion\n * row, a toolbar card. keksdose G3: two such rows had small hand-built buttons and lost\n * them on adopting FormActions, which had no size. At `sm` the spinner and\n * {@link submitIcon} shrink with the text.\n *\n * Your own elements (`children`, an element passed as `destructive`, `start`) are\n * yours to size — pass them `size=\"sm\"` too. A `size` in {@link submitProps} still\n * wins for Save, as it always has.\n */\n size?: ButtonSize;\n /**\n * A destructive action at the START of the row, apart from Save and Cancel — the\n * \"Delete\" of an edit dialog. Pass `{ label, onClick }` for the kit's quiet danger\n * button, or an element of your own (a {@link DangerConfirm}, say). With it the row\n * is `between`-aligned unless `align` says otherwise.\n */\n destructive?: ReactNode | FormActionsDestructive;\n /**\n * Something neutral at the START of the row — a caption (\"Last saved 2 min ago\",\n * \"Drag the corners to crop\"), a \"View activity\" link. After {@link destructive}\n * when both are given; like it, makes the row `between`-aligned unless `align` says\n * otherwise. `children` are for further ACTIONS, beside Cancel.\n */\n start?: ReactNode;\n /** Horizontal alignment. Default `end` (`between` with a `destructive` action). */\n align?: FormActionsAlign;\n /** See {@link FormActionsPlacement}, or one per breakpoint — see\n * {@link ResponsiveFormActionsPlacement}. Default `inline`. */\n placement?: FormActionsPlacement | ResponsiveFormActionsPlacement;\n /**\n * While the row is `sticky`: pull it out over its container's inline padding by this\n * much (a CSS length, a number in px) and give the same back as padding — so the\n * rule above it and its surface run edge to edge, while the buttons stay on the\n * content's line. Without it a sticky row in a padded card or pane is a strip inset\n * from both sides, with the content scrolling past it in the gutters (keksdose wrote\n * `@max-md:-mx-3 @max-md:px-3` by hand). Pass the container's padding (`\"0.75rem\"`,\n * `12`, `\"var(--pane-px)\"`). Applied only while the resolved placement is sticky, so\n * `placement={{ base: \"sticky\", md: \"inline\" }}` bleeds on the phone alone. The margin\n * is `calc(-1 * X)`; jsdom folds that to `calc(-X)`, so match it loosely in tests.\n */\n bleed?: string | number;\n /**\n * With `placement=\"sticky\"`: what the row sticks to. Default `viewport`.\n *\n * CSS sticky always sticks to the NEAREST scroll container, so the default row\n * already holds inside a scrolling pane — but it offsets itself by `--app-nav-h`,\n * which AppShell publishes on the document: inside a pane or a portalled dialog\n * body on a phone it then floats a nav's height above that container's bottom,\n * with the content scrolling through the gap. `container` drops that offset.\n *\n * What neither value can fix, because it is the layout, not the row:\n * - sticky moves only within its PARENT box. Inside a DataTable expansion row the\n * parent is the expansion cell, so the row pins to the table's own scroller\n * (`overflow-auto`, bounded by `maxBodyHeight`) and only while the cell spans\n * that scroller's bottom edge — not to the page.\n * - any `overflow` other than `visible`/`clip` between the row and the scroller\n * you mean (an `overflow-x-auto` wrapper, an `overflow-hidden` card) becomes the\n * scroll container; if THAT box does not scroll, the row never sticks.\n * - the offset is resolved against the scroller's content box, so a scroller with\n * bottom padding leaves that strip uncovered; give the row a negative `bottom`\n * through `style` and the same amount back as padding.\n */\n stickyWithin?: FormActionsStickyWithin;\n /** The `id` of the `<form>` the save button submits, when the row is rendered\n * outside it (a dialog's footer slot). */\n form?: string;\n /**\n * A keyboard shortcut that presses Save from anywhere in the form. `\"mod-enter\"`:\n * Ctrl+Enter, or Cmd+Enter on a Mac — both are accepted everywhere, as keksdose's\n * editors always have. keksdose K14 (inputs audit 2026-10-02, S5): its transaction\n * editor, category editor and create card each wrote this as an `onKeyDown` on a\n * container, re-stating `!pending && !saveDisabled && !lock.locked` every time, and\n * the one that forgot the lock let the demo's refused save through from the keyboard.\n *\n * What it does is what pressing Save does — the same button is clicked, so a form\n * submits with its validation and its submitter, and `onSubmit` is called in the\n * `type=\"button\"` mode — and it does NOTHING while Save could not be pressed:\n * `pending`, `submitDisabled`, or a `commit` under a locked {@link WriteLockProvider}.\n *\n * Scoped to the form Save submits (the `form` prop's, else the enclosing `<form>`):\n * the chord counts only when the focus is inside it, so two editors on one page each\n * answer for their own fields. Without a form (an `onSubmit` row in a plain container)\n * the scope is the nearest `role=\"dialog\"`, else the row's parent element — the\n * container the row ends.\n *\n * Listened for on the document, so it runs AFTER every React handler: a field that\n * takes Ctrl+Enter for itself calls `preventDefault()` or `stopPropagation()` and\n * the row stays out of it (keksdose live #202 — the create card's Ctrl+Shift+Enter\n * must create ONE row; the chord here is exactly Ctrl/Cmd+Enter, never with Shift or\n * Alt). Not during IME composition, and not on a held key's repeats.\n *\n * The save button announces it with `aria-keyshortcuts`; {@link submitShortcutHint}\n * also shows it.\n */\n submitShortcut?: FormActionsSubmitShortcut;\n /** Show the {@link submitShortcut} beside Save's text (\"Ctrl+Enter\", \"⌘ Enter\"; the\n * `form.submitShortcut` label), dimmed. Hidden on a touch-only device, which has no\n * keyboard to press it on. Default `false`. */\n submitShortcutHint?: boolean;\n /** Extra actions, placed before Cancel. */\n children?: ReactNode;\n}\n\n/** Hand `el` to a caller's ref, whichever kind it is. */\nfunction assignRef<E>(ref: Ref<E> | undefined, el: E | null): void {\n if (typeof ref === \"function\") ref(el);\n else if (ref) (ref as { current: E | null }).current = el;\n}\n\n/**\n * Where {@link FormActionsProps.submitShortcut} listens: the form Save submits (named\n * by `formId`, else the enclosing one), else the nearest dialog, else the row's parent.\n */\nfunction shortcutScope(row: HTMLElement | null, formId: string | undefined): Element | null {\n if (!row) return null;\n const named = formId ? row.ownerDocument.getElementById(formId) : null;\n if (named instanceof HTMLFormElement) return named;\n return row.closest(\"form\") ?? row.closest('[role=\"dialog\"]') ?? row.parentElement;\n}\n\nfunction isDestructiveData(value: unknown): value is FormActionsDestructive {\n return typeof value === \"object\" && value !== null && !isValidElement(value) && \"onClick\" in value;\n}\n\n/**\n * The Save / Cancel row every form, wizard-step form and dialog ends in — kastlan's\n * `FormActions`, with the labels from the provider, a pending state and a place for\n * the destructive action.\n *\n * ```tsx\n * <form onSubmit={form.handleSubmit(save)}>\n * …\n * <FormActions onCancel={close} pending={mutation.isPending} />\n * </form>\n * ```\n *\n * The spinner is the kit's {@link Spinner}, drawn inside the button with `label={null}`\n * so the button's name stays its text; `aria-busy` is what says it is working.\n */\nexport function FormActions({\n onCancel,\n onSubmit,\n submitLabel,\n cancelLabel,\n pendingLabel,\n submitIcon: SubmitIcon,\n submitProps,\n pending = false,\n submitDisabled = false,\n submitDisabledReason,\n commit = false,\n submitVariant = \"brand\",\n cancelVariant = \"secondary\",\n size = \"md\",\n destructive,\n start: startSlot,\n align,\n placement: placementProp = \"inline\",\n stickyWithin = \"viewport\",\n bleed,\n form,\n submitShortcut,\n submitShortcutHint = false,\n children,\n className,\n style,\n ...rest\n}: FormActionsProps) {\n const labels = useKitLabels(\"form\", DEFAULT_FORM_ACTIONS_LABELS);\n const lock = useWriteLock();\n const rootRef = useRef<HTMLDivElement>(null);\n const saveRef = useRef<HTMLButtonElement | null>(null);\n // The caller's own `submitProps.ref` still gets the element.\n const callerSaveRef = submitProps?.ref;\n const setSaveRef = useCallback(\n (el: HTMLButtonElement | null) => {\n saveRef.current = el;\n assignRef(callerSaveRef, el);\n },\n [callerSaveRef],\n );\n // Read by the shortcut's listener at the moment of the key press, so the listener\n // itself is attached once and never sees a stale `pending`.\n const canSave = !pending && !submitDisabled && !(commit && lock.locked);\n const canSaveRef = useRef(canSave);\n useEffect(() => {\n canSaveRef.current = canSave;\n });\n useEffect(() => {\n if (submitShortcut !== \"mod-enter\") return;\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key !== \"Enter\" || !(e.ctrlKey || e.metaKey) || e.shiftKey || e.altKey) return;\n if (e.repeat || e.isComposing || e.defaultPrevented || !canSaveRef.current) return;\n const scope = shortcutScope(rootRef.current, form);\n if (!scope || !(e.target instanceof Node) || !scope.contains(e.target)) return;\n e.preventDefault();\n saveRef.current?.click();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [submitShortcut, form]);\n const apple = submitShortcut ? isApplePlatform() : false;\n const placement = useResolvedPlacement(placementProp);\n const bleedLength = bleed === undefined || bleed === \"\" ? undefined : cssLength(bleed);\n const hasDestructive = destructive !== undefined && destructive !== null && destructive !== false;\n const hasStart = startSlot !== undefined && startSlot !== null && startSlot !== false;\n const justify = align ?? (hasDestructive || hasStart ? \"between\" : \"end\");\n const destructiveNode = !hasDestructive ? null : isDestructiveData(destructive) ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n tone=\"danger\"\n size={size}\n onClick={destructive.onClick}\n disabled={destructive.disabled}\n disabledReason={destructive.disabledReason}\n commit={commit}\n >\n {destructive.label}\n </Button>\n ) : (\n destructive\n );\n const saveLabel = submitLabel ?? labels.save;\n // The glyphs follow the text: 16px beside `md`'s 14px text, 14px beside `sm`'s 12px.\n const saveSize = submitProps?.size ?? size;\n const glyphClass = saveSize === \"sm\" ? \"size-3.5\" : \"size-4\";\n\n return (\n <div\n ref={rootRef}\n data-slot=\"form-actions\"\n data-placement={placement}\n data-sticky-within={placement === \"sticky\" ? stickyWithin : undefined}\n {...rest}\n style={\n placement === \"sticky\"\n ? {\n paddingBottom: \"max(0.75rem, env(safe-area-inset-bottom))\",\n // On the phone's bottom nav, not behind it (keksdose live #361): `bottom-0`\n // pinned the row under AppShell's nav, which covers the page's last ~56px.\n // `--app-nav-h` is the nav's measured height (0px from md up, and outside an\n // AppShell); the 1px overlaps the two top borders into one line. In a\n // container the nav is not underneath: `--app-nav-h` is the document's,\n // and would hold the row that far above the container's own edge.\n bottom: stickyWithin === \"container\" ? 0 : \"max(0px, calc(var(--app-nav-h, 0px) - 1px))\",\n ...(bleedLength !== undefined && {\n marginInline: `calc(-1 * ${bleedLength})`,\n paddingInline: bleedLength,\n }),\n ...style,\n }\n : style\n }\n className={cn(\"flex flex-wrap items-center gap-2\", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className)}\n >\n {(hasDestructive || hasStart) && (\n <div className=\"me-auto flex min-w-0 flex-wrap items-center gap-2\">\n {destructiveNode}\n {hasStart && startSlot}\n </div>\n )}\n <div className=\"flex flex-wrap items-center gap-2\">\n {children}\n {onCancel && (\n <Button type=\"button\" variant={cancelVariant} size={size} onClick={onCancel}>\n {cancelLabel ?? labels.cancel}\n </Button>\n )}\n <Button\n {...submitProps}\n ref={setSaveRef}\n aria-keyshortcuts={\n submitShortcut === \"mod-enter\" ? \"Control+Enter Meta+Enter\" : submitProps?.[\"aria-keyshortcuts\"]\n }\n type={onSubmit ? \"button\" : \"submit\"}\n form={form}\n onClick={onSubmit}\n variant={submitVariant}\n size={saveSize}\n disabled={pending || submitDisabled}\n disabledReason={pending ? undefined : submitDisabledReason}\n // Not while pending: the save already left, and the lock's look over the\n // spinner would say it had not.\n commit={commit && !pending}\n aria-busy={pending || undefined}\n >\n {pending ? (\n <Spinner label={null} className={glyphClass} />\n ) : (\n SubmitIcon && <SubmitIcon aria-hidden className={cn(glyphClass, \"shrink-0\")} />\n )}\n {pending && pendingLabel !== undefined ? pendingLabel : saveLabel}\n {submitShortcut && submitShortcutHint && (\n // Hidden from the accessibility tree: `aria-keyshortcuts` already says it,\n // and in the name it would make the button \"Save Ctrl+Enter\".\n <kbd\n aria-hidden\n className=\"ms-1 font-sans text-[0.85em] font-normal whitespace-nowrap opacity-70 pointer-coarse:hidden\"\n >\n {labels.submitShortcut(apple)}\n </kbd>\n )}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,69 @@
1
+ import 'react';
2
+ import '../lib/iban.js';
3
+ export { a8 as DEFAULT_IBAN_INPUT_LABELS, b3 as IbanInput, b4 as IbanInputLabels, b5 as IbanInputProps } from '../feedback-BxeQVzwq.js';
4
+ import '../feedback-attachment-fGAzZPf0.js';
5
+ import '../feedback/feedback-inbox.js';
6
+ import 'lucide-react';
7
+ import './tooltip.js';
8
+ import '../hooks/use-anchored-rect.js';
9
+ import '../lib/clipping.js';
10
+ import '../data-table-labels-B7OdnM0S.js';
11
+ import './data-table-sort.js';
12
+ import './swipeable-row.js';
13
+ import './mini-calendar.js';
14
+ import './calendar-heatmap.js';
15
+ import './popover.js';
16
+ import './chip.js';
17
+ import './field-sync.js';
18
+ import '../wizard/types.js';
19
+ import '../tour/tour.js';
20
+ import '../search/command-palette.js';
21
+ import '../search/global-search.js';
22
+ import '../search/search-index.js';
23
+ import './month-picker.js';
24
+ import './page-contents.js';
25
+ import './series-chart-labels.js';
26
+ import './pie-chart-labels.js';
27
+ import './sparkline.js';
28
+ import './stat-tile.js';
29
+ import './signature-pad.js';
30
+ import './password-strength.js';
31
+ import './swatch-picker.js';
32
+ import './tile-radio.js';
33
+ import './icon-picker.js';
34
+ import './dialog-frame.js';
35
+ import './modal.js';
36
+ import '../hooks/use-search-param-state.js';
37
+ import './measured-grid.js';
38
+ import './account-settings-labels.js';
39
+ import './floating-panel.js';
40
+ import './bulk-action-bar.js';
41
+ import './list.js';
42
+ import './signed-amount.js';
43
+ import './breadcrumbs.js';
44
+ import './toast.js';
45
+ import './description-list.js';
46
+ import './line-items.js';
47
+ import './progress-bar.js';
48
+ import './error-boundary.js';
49
+ import './authed-image.js';
50
+ import '../hooks/use-authed-src.js';
51
+ import './image-grid.js';
52
+ import './lightbox.js';
53
+ import './write-lock.js';
54
+ import './legal.js';
55
+ import './account-chips.js';
56
+ import '../lib/format.js';
57
+ import './reauth-dialog.js';
58
+ import './server-wake.js';
59
+ import '../lib/server-wake.js';
60
+ import './translation-review-labels.js';
61
+ import '../lib/translation-review.js';
62
+ import './country-select.js';
63
+ import './inline-edit-field.js';
64
+ import '../lib/phone.js';
65
+ import './sign-chip.js';
66
+ import './column-mapper.js';
67
+ import '../lib/column-mapping.js';
68
+ import '../lib/table-text.js';
69
+ import './field-parts.js';