@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/danger-confirm.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { ChangeEvent, ComponentPropsWithoutRef, FormEvent, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Button, Input, Label, Spinner } from \"./ui\";\nimport { Tooltip } from \"./tooltip\";\n\n/**\n * How typed text is compared with the text it has to repeat — shared by\n * `DangerConfirm`'s `phraseMatch` and `useConfirm`'s `typedMatch`.\n *\n * - `\"trim\"` ignores spaces around the typed text — a phone keyboard's autocomplete\n * likes to add one. Case counts: \"delete\" is not \"DELETE\".\n * - `\"exact\"` compares character for character, spaces included.\n * - `\"caseless\"` trims AND ignores case: for an e-mail address, whose domain is\n * case-insensitive and whose local part every real mail server treats so. Both apps'\n * \"type the account's address\" rules (keksdose's `TYPE_EMAIL`, Kurvenschmiede's\n * `TypedConfirm`) compared `trim().toLowerCase()`, and a capital the user's keyboard\n * put at the start must not make the right row look like the wrong one.\n */\nexport type TypedMatch = \"trim\" | \"exact\" | \"caseless\";\n\n/** Whether `typed` repeats `target` under `mode` — see {@link TypedMatch}. Exported for\n * a caller that draws its own field (a dialog that also picks a recipient) and wants\n * the same rule as the kit's. */\nexport function typedMatches(typed: string, target: string, mode: TypedMatch = \"trim\"): boolean {\n if (mode === \"exact\") return typed === target;\n if (mode === \"trim\") return typed.trim() === target;\n // `toLowerCase`, not `toLocaleLowerCase`: the comparison must not change with the\n // reader's locale (a Turkish dotless i would otherwise fail an ASCII address).\n return typed.trim().toLowerCase() === target.trim().toLowerCase();\n}\n\n/**\n * Busy-while-a-promise-runs, for an action that MAY return one: `run(result, onResolved)`\n * marks the caller pending until `result` settles, calls `onResolved` if it fulfilled,\n * and only clears the flag if it rejected — the caller shows why and the user retries.\n * Nothing runs after unmount. Shared by `DangerConfirm` and `ReauthDialog`, whose\n * contract is the same: resolve = done, reject = stay put.\n *\n * @internal\n */\nexport function usePromisePending() {\n const [pending, setPending] = useState(false);\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const run = useCallback((result: unknown, onResolved: () => void): boolean => {\n if (!result || typeof (result as Promise<unknown>).then !== \"function\") return false;\n setPending(true);\n (result as Promise<unknown>).then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n onResolved();\n },\n () => {\n if (mounted.current) setPending(false);\n },\n );\n return true;\n }, []);\n return { pending, run };\n}\n\n/**\n * The \"prove it is you\" field: a password input the browser's password manager fills\n * with the CURRENT password (`autocomplete=\"current-password\"`), never offers to\n * generate a new one for. `readOnly` rather than `disabled` while busy: disabling the\n * field that has focus (Enter was pressed in it) drops focus to <body>.\n *\n * @internal DangerConfirm's field. ReauthDialog spells it out with the field's own\n * `error`, which keeps the `<input>` mounted while its message comes and goes.\n */\nexport function CurrentPasswordInput({\n inputRef,\n label,\n value,\n onValueChange,\n busy,\n invalid,\n describedBy,\n}: {\n inputRef?: Ref<HTMLInputElement>;\n label: string;\n value: string;\n onValueChange: (value: string) => void;\n busy: boolean;\n invalid?: boolean;\n describedBy?: string;\n}) {\n return (\n <Input\n ref={inputRef}\n type=\"password\"\n autoComplete=\"current-password\"\n label={label}\n value={value}\n readOnly={busy}\n invalid={invalid}\n aria-describedby={describedBy}\n onChange={(e) => onValueChange(e.target.value)}\n />\n );\n}\n\n/**\n * Every string the tile renders — the `dangerConfirm` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. The arm and\n * confirm buttons are usually worded per action (\"Delete budget\"), which is what the\n * `armLabel` / `confirmLabel` props are for; these are the fallbacks.\n */\nexport interface DangerConfirmLabels {\n /** The button that arms the tile. */\n arm: string;\n /** The button that runs the action once the guards are satisfied. */\n confirm: string;\n cancel: string;\n /** The warning above the fields, when no `prompt` is given. */\n prompt: string;\n /** Label of the password field (`requirePassword`). */\n password: string;\n /** Label of the type-to-confirm field. A FUNCTION of the phrase by default, like\n * every message that carries a value: where the phrase sits in the sentence moves\n * with the language. A plain STRING is taken as the finished label — for an app\n * whose catalogue already words it (\"Type DELETE to confirm\") and would otherwise\n * wrap it as `() => label`. */\n phrase: string | ((phrase: string) => string);\n /** Placeholder of the type-to-confirm field — a string, or a function of the\n * phrase (`(p) => p` echoes it). Optional and unset by default: the field keeps its\n * floating label, as before. Given, the label moves ABOVE the field (a floating\n * label occupies the empty field, which is exactly where a placeholder shows) and\n * the placeholder fills the field. An empty string means none. */\n phrasePlaceholder?: string | ((phrase: string) => string);\n}\n\n/** `satisfies` rather than a type annotation, so `DEFAULT_DANGER_CONFIRM_LABELS.phrase`\n * stays callable for a caller that composes its own label from it. */\nexport const DEFAULT_DANGER_CONFIRM_LABELS = {\n arm: \"Delete…\",\n confirm: \"Delete\",\n cancel: \"Cancel\",\n prompt: \"This cannot be undone.\",\n password: \"Password\",\n phrase: (phrase: string) => `Type “${phrase}” to confirm`,\n} satisfies DangerConfirmLabels;\n\nexport interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /**\n * Runs the action, with the entered password when `requirePassword` is on.\n *\n * Return a promise and the tile manages itself: busy until it settles, disarmed\n * (fields wiped) when it resolves, still armed when it rejects — so the user can\n * correct a wrong password and retry. The rejection is not swallowed for you to\n * miss: handle it (and show why) in the caller, as with any mutation.\n */\n onConfirm: (password?: string) => void | Promise<unknown>;\n /** Show a password field; confirm stays disabled until it is filled. */\n requirePassword?: boolean;\n /** Show a \"type <phrase> to confirm\" field; confirm stays disabled until the field\n * matches (case-sensitive; surrounding spaces ignored unless `phraseMatch=\"exact\"`). */\n phrase?: string;\n /**\n * How the typed text is compared with `phrase`. `\"trim\"` (default) ignores spaces\n * around it — a phone keyboard's autocomplete likes to add one. `\"exact\"` compares\n * character for character, spaces included, for an app whose contract is \"type\n * exactly this\". `\"caseless\"` (0.18) also ignores case — for a phrase that is an\n * e-mail address (keksdose's \"type the user's address\" admin rule). See\n * {@link TypedMatch}.\n */\n phraseMatch?: TypedMatch;\n /** The warning above the fields. Defaults to `labels.prompt`. */\n prompt?: ReactNode;\n /** `\"danger\"` (default) for what cannot be undone; `\"warning\"` for what can, at a\n * cost (loading demo data over your own). Colours the prompt and the confirm. */\n tone?: \"danger\" | \"warning\";\n /** Visible text of the arm button; defaults to `labels.arm`. */\n armLabel?: ReactNode;\n /** Visible text of the confirm button; defaults to `labels.confirm`. */\n confirmLabel?: ReactNode;\n /** The action is running: confirm shows a spinner and nothing can be pressed. For a\n * caller that tracks the mutation itself (a `useMutation`'s `isPending`); a\n * promise returned from `onConfirm` does the same on its own. */\n busy?: boolean;\n /** The arm button is disabled. */\n disabled?: boolean;\n /**\n * Why the action is not available — a read-only demo, a write lock, a missing\n * permission. Disables the arm button like `disabled`, and SAYS so: the sentence is\n * shown under the button and attached to it with `aria-describedby`, and the button\n * stays focusable (`aria-disabled`) so a keyboard user can land on it and hear why.\n *\n * On the ARM button rather than the confirm: arming asks for a password, and asking\n * for a password for a write that can never land is the worse of the two.\n */\n lockedReason?: ReactNode;\n /** Controlled armed state. The parent can then collapse the tile from a mutation's\n * own `onSuccess` without returning a promise. */\n armed?: boolean;\n /** Called with the next armed state — on arm, cancel, and a resolved `onConfirm`. */\n onArmedChange?: (armed: boolean) => void;\n /** User-facing strings; see {@link DangerConfirmLabels}. */\n labels?: Partial<DangerConfirmLabels>;\n}\n\n/**\n * An \"arm → confirm\" tile for destructive actions: one button, which expands into a\n * warning, an optional password field, an optional type-to-confirm field and a\n * confirm that stays disabled until every guard is satisfied.\n *\n * Keksdose hand-rolled it three times (load demo data, wipe everything, reset a\n * budget) and then as `shared/components/danger-confirm.tsx`; the only app-specific\n * part was its write-lock hook, which is `lockedReason` here.\n *\n * The fields are a `<form>`, so Enter confirms once the guards allow it. They are\n * wiped every time the tile disarms — a password must not sit in a collapsed tile.\n * Arming moves focus to the first field (or Cancel, when there is none — never to the\n * destructive button itself), and disarming moves it back to the arm button, so the\n * keyboard user is never left on an element that just vanished.\n */\nexport function DangerConfirm({\n onConfirm,\n requirePassword,\n phrase,\n phraseMatch = \"trim\",\n prompt,\n tone = \"danger\",\n armLabel,\n confirmLabel,\n busy: busyProp,\n disabled,\n lockedReason,\n armed: armedProp,\n onArmedChange,\n labels: labelsProp,\n className,\n ...rest\n}: DangerConfirmProps) {\n const labels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);\n const [armedState, setArmedState] = useState(false);\n const armed = armedProp ?? armedState;\n const [password, setPassword] = useState(\"\");\n const [typed, setTyped] = useState(\"\");\n const { pending, run } = usePromisePending();\n const busy = Boolean(busyProp) || pending;\n // `readOnly` rather than `disabled` on the fields while busy: disabling the field\n // that has focus (Enter was pressed in it) drops focus to <body>.\n const promptId = useId();\n const reasonId = useId();\n const phraseFieldId = useId();\n\n // The kit's Button takes no ref, so the two buttons focus is moved to are found by id.\n const armId = useId();\n const cancelId = useId();\n const firstFieldRef = useRef<HTMLInputElement>(null);\n // Focus moves only after a transition — never on mount, so a tile that renders armed\n // (controlled) does not take the page's focus merely by existing.\n const moveFocus = useRef(false);\n\n // A disarm from anywhere (cancel, a resolved confirm, the parent) wipes the fields.\n // During render, like NumberField's draft, so no frame shows a collapsed tile that\n // still holds a password.\n const [wasArmed, setWasArmed] = useState(armed);\n if (wasArmed !== armed) {\n setWasArmed(armed);\n if (!armed) {\n setPassword(\"\");\n setTyped(\"\");\n }\n }\n\n const prevArmed = useRef(armed);\n useEffect(() => {\n const changed = prevArmed.current !== armed;\n prevArmed.current = armed;\n const byUser = moveFocus.current;\n moveFocus.current = false;\n if (!changed) return;\n if (armed) {\n if (byUser) (firstFieldRef.current ?? document.getElementById(cancelId))?.focus();\n return;\n }\n // A controlled parent collapsing the tile from its own `onSuccess` did not go\n // through `setArmed`; if focus went down with the form, bring it back too.\n const lost = document.activeElement === null || document.activeElement === document.body;\n if (byUser || lost) document.getElementById(armId)?.focus();\n }, [armed, armId, cancelId]);\n\n const setArmed = (next: boolean) => {\n moveFocus.current = true;\n if (armedProp === undefined) setArmedState(next);\n onArmedChange?.(next);\n };\n\n const locked = lockedReason !== undefined && lockedReason !== null && lockedReason !== false && lockedReason !== \"\";\n const passwordOk = !requirePassword || password !== \"\";\n const phraseOk = phrase === undefined || typedMatches(typed, phrase, phraseMatch);\n const canConfirm = passwordOk && phraseOk && !busy;\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n if (!canConfirm) return;\n // Disarms when it resolves; a rejection leaves it armed, fields kept: the caller\n // shows why, the user retries.\n run(onConfirm(requirePassword ? password : undefined), () => setArmed(false));\n };\n\n const phrasePlaceholder =\n phrase === undefined || labels.phrasePlaceholder === undefined\n ? undefined\n : typeof labels.phrasePlaceholder === \"function\"\n ? labels.phrasePlaceholder(phrase)\n : labels.phrasePlaceholder;\n\n const phraseLabel =\n phrase === undefined ? \"\" : typeof labels.phrase === \"function\" ? labels.phrase(phrase) : labels.phrase;\n const phraseFieldProps = {\n ref: firstFieldRef,\n value: typed,\n autoComplete: \"off\",\n autoCapitalize: \"off\",\n spellCheck: false,\n readOnly: busy,\n onChange: (e: ChangeEvent<HTMLInputElement>) => setTyped(e.target.value),\n };\n\n const toneText = tone === \"warning\" ? \"text-[var(--warning)]\" : \"text-[var(--danger)]\";\n\n if (!armed) {\n const arm = (\n <Button\n id={armId}\n type=\"button\"\n variant={tone === \"warning\" ? \"secondary\" : \"danger\"}\n disabled={disabled}\n aria-disabled={locked || undefined}\n aria-describedby={locked ? reasonId : undefined}\n onClick={() => {\n if (!locked) setArmed(true);\n }}\n className=\"aria-disabled:cursor-not-allowed aria-disabled:opacity-50\"\n >\n {armLabel ?? labels.arm}\n </Button>\n );\n return (\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {locked ? (\n // The reason on the button itself too, where the pointer that tried it is —\n // the line below can sit out of view in a long settings card. Portalled, so\n // a card that scrolls neither clips it nor grows by it.\n //\n // The button in a FRAGMENT on purpose: Tooltip clones an element child to\n // add the bubble to its `aria-describedby`, and the button is already\n // described by the visible line below — the same sentence twice, read out\n // on every focus. A fragment is left as it is, so the bubble stays visual.\n <Tooltip label={lockedReason} portal>\n <>{arm}</>\n </Tooltip>\n ) : (\n arm\n )}\n {locked && (\n <p id={reasonId} className=\"text-xs text-[var(--text-muted)]\">\n {lockedReason}\n </p>\n )}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"space-y-2\", className)}>\n <form\n className=\"space-y-2\"\n aria-describedby={promptId}\n aria-busy={busy || undefined}\n onSubmit={submit}\n noValidate\n >\n <p id={promptId} className={cn(\"text-xs font-medium\", toneText)}>\n {prompt ?? labels.prompt}\n </p>\n {phrase !== undefined &&\n (phrasePlaceholder ? (\n // A static label above, so the placeholder has the empty field to itself.\n <div className=\"space-y-1.5\">\n <Label htmlFor={phraseFieldId}>{phraseLabel}</Label>\n <Input id={phraseFieldId} placeholder={phrasePlaceholder} {...phraseFieldProps} />\n </div>\n ) : (\n <Input label={phraseLabel} {...phraseFieldProps} />\n ))}\n {requirePassword && (\n <CurrentPasswordInput\n // The phrase field takes the ref when both are there — it comes first.\n inputRef={phrase === undefined ? firstFieldRef : undefined}\n label={labels.password}\n value={password}\n busy={busy}\n onValueChange={setPassword}\n />\n )}\n <div className=\"flex flex-wrap gap-2\">\n <Button id={cancelId} type=\"button\" variant=\"ghost\" disabled={busy} onClick={() => setArmed(false)}>\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n variant={tone === \"warning\" ? \"primary\" : \"danger\"}\n disabled={!canConfirm}\n aria-busy={busy || undefined}\n >\n {busy && <Spinner label={null} className=\"h-4 w-4\" />}\n {confirmLabel ?? labels.confirm}\n </Button>\n </div>\n </form>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/danger-confirm.tsx"],"sourcesContent":["import { forwardRef, useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { ChangeEvent, ComponentPropsWithoutRef, FormEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Button, Input, Label, Spinner } from \"./ui\";\nimport type { InputProps } from \"./ui\";\nimport { Checkbox } from \"./checkbox\";\nimport { Tooltip } from \"./tooltip\";\nimport { useCommitReason } from \"./write-lock\";\n\n/**\n * How typed text is compared with the text it has to repeat — shared by\n * `DangerConfirm`'s `phraseMatch` and `useConfirm`'s `typedMatch`.\n *\n * - `\"trim\"` ignores spaces around the typed text — a phone keyboard's autocomplete\n * likes to add one. Case counts: \"delete\" is not \"DELETE\".\n * - `\"exact\"` compares character for character, spaces included.\n * - `\"caseless\"` trims AND ignores case: for an e-mail address, whose domain is\n * case-insensitive and whose local part every real mail server treats so. Both apps'\n * \"type the account's address\" rules (keksdose's `TYPE_EMAIL`, Kurvenschmiede's\n * `TypedConfirm`) compared `trim().toLowerCase()`, and a capital the user's keyboard\n * put at the start must not make the right row look like the wrong one.\n */\nexport type TypedMatch = \"trim\" | \"exact\" | \"caseless\";\n\n/** Whether `typed` repeats `target` under `mode` — see {@link TypedMatch}. Exported for\n * a caller that draws its own field (a dialog that also picks a recipient) and wants\n * the same rule as the kit's. */\nexport function typedMatches(typed: string, target: string, mode: TypedMatch = \"trim\"): boolean {\n if (mode === \"exact\") return typed === target;\n if (mode === \"trim\") return typed.trim() === target;\n // `toLowerCase`, not `toLocaleLowerCase`: the comparison must not change with the\n // reader's locale (a Turkish dotless i would otherwise fail an ASCII address).\n return typed.trim().toLowerCase() === target.trim().toLowerCase();\n}\n\n/**\n * Busy-while-a-promise-runs, for an action that MAY return one: `run(result, onResolved)`\n * marks the caller pending until `result` settles, calls `onResolved` if it fulfilled,\n * and only clears the flag if it rejected — the caller shows why and the user retries.\n * Nothing runs after unmount. Shared by `DangerConfirm` and `ReauthDialog`, whose\n * contract is the same: resolve = done, reject = stay put.\n *\n * @internal\n */\nexport function usePromisePending() {\n const [pending, setPending] = useState(false);\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const run = useCallback((result: unknown, onResolved: () => void): boolean => {\n if (!result || typeof (result as Promise<unknown>).then !== \"function\") return false;\n setPending(true);\n (result as Promise<unknown>).then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n onResolved();\n },\n () => {\n if (mounted.current) setPending(false);\n },\n );\n return true;\n }, []);\n return { pending, run };\n}\n\n/** A label that is really there — `null`, `false` and `\"\"` are what a caller's\n * `cond && reason` evaluates to on the path where there is none. */\nfunction hasContent(node: ReactNode): boolean {\n return node !== undefined && node !== null && node !== false && node !== \"\";\n}\n\n/** The one word {@link CurrentPasswordInput} says on its own behalf — the\n * `dangerConfirm.password` label, shared with DangerConfirm's password field, so an\n * app translates \"Password\" once. */\nexport type CurrentPasswordInputLabels = Pick<DangerConfirmLabels, \"password\">;\n\n/**\n * Everything {@link Input} takes except what makes this field what it is: `type` is\n * always `\"password\"` and `autoComplete` always `\"current-password\"`.\n */\nexport interface CurrentPasswordInputProps extends Omit<InputProps, \"type\" | \"autoComplete\" | \"label\"> {\n /**\n * The visible label. Default `dangerConfirm.password` (\"Password\") — unless the field\n * is named another way (`aria-label` / `aria-labelledby`), which then stays the only\n * name. Pass the app's own word on a sign-in form (\"Password\", \"Your password\"), or\n * `null` for a field whose `<label htmlFor>` is drawn elsewhere (a form library's\n * `FormLabel`), which would otherwise be labelled twice.\n */\n label?: ReactNode;\n /** The value as a string — the `(next) => …` shape, so a caller holding it in\n * `useState` need not unwrap `event.target.value`. Fires alongside `onChange`,\n * never instead of it, so a form library's `onChange` keeps working. */\n onValueChange?: (value: string) => void;\n /**\n * A check is running (the sign-in request, the re-auth). The field turns `readOnly`,\n * NOT `disabled`: the field being submitted is usually the focused one (Enter was\n * pressed in it), and disabling a focused field drops focus to `<body>` — the user\n * lands nowhere when the \"wrong password\" answer comes back.\n */\n busy?: boolean;\n /** Per-instance override of `dangerConfirm.password`. */\n labels?: Partial<CurrentPasswordInputLabels>;\n}\n\n/**\n * The \"prove it is you\" field: a password input that a password manager fills with\n * the CURRENT password and never offers to generate a new one for.\n *\n * It is the field of a sign-in form, a re-auth step ({@link ReauthDialog} draws the same\n * field), and {@link DangerConfirm}'s `requirePassword`. Public since 0.22 for kastlan's\n * sign-in and re-auth (kastlan 1), which wrote `type=\"password\"` +\n * `autoComplete=\"current-password\"` by hand — and a sign-in field that forgets the\n * second half gets the browser's \"suggest a strong password\" offer on the one form where\n * a NEW password is never what is wanted. The reverse matters as much: a field that sets\n * a NEW password (a passphrase, a sign-up) must NOT be this one — keksdose #170 keeps\n * its passphrase doors on `new-password` / `off` for that reason.\n *\n * Everything else is {@link Input}: the reveal toggle, `error` (under the field,\n * `aria-describedby`, `aria-invalid`), `invalid`, `name`, `id`, `autoFocus`, `required`.\n * The ref is the `<input>`, so it works with `@eifi1/ui-kit/rhf`'s `FormControl` /\n * a `register()` spread, and controlled or uncontrolled.\n *\n * ```tsx\n * <CurrentPasswordInput name=\"password\" label={t(\"password\")} error={errors.password?.message}\n * busy={isSubmitting} {...register(\"password\")} />\n * ```\n */\nexport const CurrentPasswordInput = forwardRef<HTMLInputElement, CurrentPasswordInputProps>(\n function CurrentPasswordInput({ label, onValueChange, onChange, busy, readOnly, labels: labelsProp, ...rest }, ref) {\n const labels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);\n const namedOtherwise = rest[\"aria-label\"] !== undefined || rest[\"aria-labelledby\"] !== undefined;\n return (\n <Input\n {...rest}\n ref={ref}\n // After the spread: a props object spread at the field must not turn it into a\n // text field, or into one the password manager offers a new password for.\n type=\"password\"\n autoComplete=\"current-password\"\n label={label === undefined ? (namedOtherwise ? undefined : labels.password) : hasContent(label) ? label : undefined}\n readOnly={Boolean(busy) || readOnly}\n onChange={(e) => {\n onChange?.(e);\n onValueChange?.(e.target.value);\n }}\n />\n );\n },\n);\nCurrentPasswordInput.displayName = \"CurrentPasswordInput\";\n\n/** The words {@link TypedConfirmField} says on its own behalf — the `dangerConfirm`\n * namespace's `phrase` and `phrasePlaceholder`, shared with DangerConfirm's own field,\n * so the sentence \"Type “X” to confirm\" is translated once. */\nexport type TypedConfirmFieldLabels = Pick<DangerConfirmLabels, \"phrase\" | \"phrasePlaceholder\">;\n\nexport interface TypedConfirmFieldProps\n extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\" | \"label\" | \"placeholder\"> {\n /** The text to repeat — a word (\"DELETE\"), a name, an e-mail address. */\n target: string;\n /** How the typed text is compared with `target` — {@link TypedMatch}. Default\n * `\"trim\"`, as {@link typedMatches}; `\"caseless\"` for an address or an\n * acknowledgement word a user may type in any case. */\n match?: TypedMatch;\n /** Controlled value. Left out, the field keeps its own (start: `defaultValue`). */\n value?: string;\n defaultValue?: string;\n /** The typed text as a string, on every keystroke. Fires alongside `onChange`. */\n onValueChange?: (value: string) => void;\n /**\n * The typed text started or stopped matching `target` — fired from the keystroke\n * that flipped it, so an uncontrolled field can still drive the button it guards. A\n * controlled caller that resets the value itself (on a close) knows the answer\n * already, and gets no call for that; it can also compute it with {@link typedMatches}.\n */\n onMatchedChange?: (matched: boolean) => void;\n /** The label. Default `dangerConfirm.phrase(target)` — \"Type “DELETE” to confirm\". */\n label?: ReactNode;\n /** Text inside the empty field. Default `dangerConfirm.phrasePlaceholder` (unset).\n * Given, the label moves ABOVE the field: a floating label occupies the empty\n * field, which is exactly where a placeholder shows. `\"\"` means none. */\n placeholder?: string;\n /** The guarded action is running: the field turns `readOnly` (not `disabled`, which\n * would drop the focus Enter left in it). */\n busy?: boolean;\n labels?: Partial<TypedConfirmFieldLabels>;\n}\n\n/**\n * The \"type this to confirm\" field — {@link DangerConfirm}'s `phrase` field and\n * `useConfirm({ requireTyped })`'s, on its own (keksdose K7).\n *\n * keksdose writes it by hand four times: the privacy enrol, upgrade and sweep dialogs\n * (type the acknowledgement word, then the key doors appear) and the admin's \"type the\n * user's address\" (`UserActionConfirm`), each an `Input` plus its own\n * `trim().toUpperCase()` / `toLowerCase()` comparison. None of those places is an\n * arm → confirm tile or a modal confirm — what is guarded is a whole form, or the\n * next step of a dialog — so the field had to stand alone. What it brings over a plain\n * `Input`: one match rule ({@link typedMatches}, which lower-cases locale-independently\n * on purpose), the label worded from the target in the app's language, and the\n * attributes a phone keyboard needs to leave the text alone (no autocomplete, no\n * auto-capital, no autocorrect, no spellcheck — a keyboard that \"corrects\" an e-mail\n * address makes the guard unpassable).\n *\n * The field reports whether it matches (`onMatchedChange`, and `data-matched` on the\n * `<input>` for styling); what the match unlocks is the caller's. The ref is the\n * `<input>`.\n *\n * ```tsx\n * const [ok, setOk] = useState(false);\n * <TypedConfirmField target={t(\"privacy.enroll_ack_word\")} match=\"caseless\" onMatchedChange={setOk} />\n * {ok && <KeyDoorsForm … />}\n * ```\n */\nexport const TypedConfirmField = forwardRef<HTMLInputElement, TypedConfirmFieldProps>(function TypedConfirmField(\n {\n target,\n match = \"trim\",\n value: valueProp,\n defaultValue = \"\",\n onValueChange,\n onMatchedChange,\n onChange,\n label: labelProp,\n placeholder: placeholderProp,\n busy,\n readOnly,\n labels: labelsProp,\n id,\n className,\n ...rest\n },\n ref,\n) {\n const labels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);\n const [own, setOwn] = useState(defaultValue);\n const value = valueProp ?? own;\n const matched = typedMatches(value, target, match);\n const generated = useId();\n const fieldId = id ?? generated;\n const label =\n labelProp !== undefined\n ? labelProp\n : typeof labels.phrase === \"function\"\n ? labels.phrase(target)\n : labels.phrase;\n const fromLabels =\n labels.phrasePlaceholder === undefined\n ? undefined\n : typeof labels.phrasePlaceholder === \"function\"\n ? labels.phrasePlaceholder(target)\n : labels.phrasePlaceholder;\n const placeholder = placeholderProp ?? fromLabels;\n\n const fieldProps = {\n ...rest,\n ref,\n id: fieldId,\n value,\n autoComplete: \"off\",\n autoCapitalize: \"off\",\n autoCorrect: \"off\",\n spellCheck: false,\n readOnly: Boolean(busy) || readOnly,\n \"data-matched\": matched || undefined,\n onChange: (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const next = e.target.value;\n if (valueProp === undefined) setOwn(next);\n onValueChange?.(next);\n const nextMatched = typedMatches(next, target, match);\n if (nextMatched !== matched) onMatchedChange?.(nextMatched);\n },\n };\n\n if (placeholder) {\n return (\n // A static label above, so the placeholder has the empty field to itself.\n <div className={cn(\"space-y-1.5\", className)}>\n <Label htmlFor={fieldId}>{label}</Label>\n <Input {...fieldProps} placeholder={placeholder} />\n </div>\n );\n }\n return <Input {...fieldProps} label={label} className={className} />;\n});\nTypedConfirmField.displayName = \"TypedConfirmField\";\n\n/**\n * Every string the tile renders — the `dangerConfirm` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. The arm and\n * confirm buttons are usually worded per action (\"Delete budget\"), which is what the\n * `armLabel` / `confirmLabel` props are for; these are the fallbacks.\n */\nexport interface DangerConfirmLabels {\n /** The button that arms the tile. */\n arm: string;\n /** The button that runs the action once the guards are satisfied. */\n confirm: string;\n cancel: string;\n /** The warning above the fields, when no `prompt` is given. */\n prompt: string;\n /** Label of the password field (`requirePassword`). */\n password: string;\n /** Label of the type-to-confirm field. A FUNCTION of the phrase by default, like\n * every message that carries a value: where the phrase sits in the sentence moves\n * with the language. A plain STRING is taken as the finished label — for an app\n * whose catalogue already words it (\"Type DELETE to confirm\") and would otherwise\n * wrap it as `() => label`. */\n phrase: string | ((phrase: string) => string);\n /** Placeholder of the type-to-confirm field — a string, or a function of the\n * phrase (`(p) => p` echoes it). Optional and unset by default: the field keeps its\n * floating label, as before. Given, the label moves ABOVE the field (a floating\n * label occupies the empty field, which is exactly where a placeholder shows) and\n * the placeholder fills the field. An empty string means none. */\n phrasePlaceholder?: string | ((phrase: string) => string);\n /** 0.22: the checkbox `requireAcknowledge={true}` shows — \"I have read what this does\n * and want to continue\" (keksdose's `admin.users.confirm_ack`). */\n acknowledge: string;\n /**\n * 0.23: why the armed confirm is held while the typed phrase does not match yet — in\n * the confirm's tooltip and its description (keksdose G4a). A FUNCTION of the phrase\n * by default (\"Type “DELETE” to confirm\"), or a finished STRING, as `phrase`.\n */\n needsPhrase: string | ((phrase: string) => string);\n /** 0.23: why the armed confirm is held while the `requireAcknowledge` box is unticked\n * — \"Tick the box to confirm\". */\n needsAcknowledge: string;\n /** 0.23: why the armed confirm is held while the `requirePassword` field is empty —\n * \"Enter your password to confirm\". */\n needsPassword: string;\n}\n\n/** `satisfies` rather than a type annotation, so `DEFAULT_DANGER_CONFIRM_LABELS.phrase`\n * stays callable for a caller that composes its own label from it. */\nexport const DEFAULT_DANGER_CONFIRM_LABELS = {\n arm: \"Delete…\",\n confirm: \"Delete\",\n cancel: \"Cancel\",\n prompt: \"This cannot be undone.\",\n password: \"Password\",\n phrase: (phrase: string) => `Type “${phrase}” to confirm`,\n acknowledge: \"I have read what this does and want to continue.\",\n needsPhrase: (phrase: string) => `Type “${phrase}” to confirm`,\n needsAcknowledge: \"Tick the box to confirm\",\n needsPassword: \"Enter your password to confirm\",\n} satisfies DangerConfirmLabels;\n\n/**\n * What the guards were answered with — the second argument of\n * {@link DangerConfirmProps.onConfirm} (0.23, keksdose G4b). A key is there only when its\n * guard was asked for, so the object can go into a request body as it is.\n */\nexport interface DangerConfirmValues {\n /**\n * The text typed into the `phrase` field — as the match rule compared it: trimmed\n * under `phraseMatch` `\"trim\"` and `\"caseless\"` (the spaces a phone keyboard adds are\n * not part of it), exactly as typed under `\"exact\"`. The case is the user's, even\n * under `\"caseless\"`: a server that re-checks applies its own rule to it. Absent\n * without a `phrase`.\n */\n typed?: string;\n /** The entered password — the same value as the first argument. Absent without\n * `requirePassword`. */\n password?: string;\n /** `true` when the `requireAcknowledge` box was shown — it was ticked, since the\n * confirm is held until it is. Absent without one. */\n acknowledged?: true;\n}\n\n/**\n * One line of {@link DangerConfirmProps.consequences}: what the action will do. A plain\n * string, or this, for a key of its own and the `severe` mark.\n */\nexport interface DangerConsequence {\n /** React key — keksdose keys its lines by what they say (\"api_tokens\", \"only_door\").\n * Default: the text when it is a string, else the position. */\n key?: string;\n text: ReactNode;\n /** \"There is no way back\" — painted in the danger colour rather than the list's\n * muted one. keksdose reserves it for the one line that loses something for good. */\n severe?: boolean;\n}\n\nexport interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /**\n * Runs the action, with the entered password when `requirePassword` is on.\n *\n * Return a promise and the tile manages itself: busy until it settles, disarmed\n * (fields wiped) when it resolves, still armed when it rejects — so the user can\n * correct a wrong password and retry. The rejection is not swallowed for you to\n * miss: handle it (and show why) in the caller, as with any mutation.\n *\n * 0.23: the second argument is what every guard was answered with — the typed phrase,\n * the password, the tick ({@link DangerConfirmValues}). keksdose G4b: its admin\n * actions send the address the admin TYPED, and the server re-checks it against the\n * account (`assert_confirmed`), so a client that put the wrong row's id in the request\n * is refused rather than obeyed; the tile kept the text to itself.\n *\n * onConfirm={(_password, { typed }) =>\n * reset.mutateAsync({ id, confirm: { acknowledged: true, confirm_email: typed ?? null } })}\n *\n * A second argument rather than one object in place of the password: every\n * `(password) => …` and `() => …` handler, and a `mutation.mutate` passed as it is,\n * keeps working unchanged, and a handler that needs one guard's answer names it.\n */\n onConfirm: (password: string | undefined, values: DangerConfirmValues) => void | Promise<unknown>;\n /** Show a password field; confirm is held until it is filled. */\n requirePassword?: boolean;\n /**\n * Show an \"I understand\" checkbox; confirm is held until it is ticked\n * (keksdose K7). `true` words it with `labels.acknowledge`; any other node IS the\n * checkbox's label (\"I understand Anna will be signed out everywhere\").\n *\n * The guard for an action whose risk is MIS-TARGETING less than carelessness:\n * keksdose's admin password reset asks for a typed address when the account has an\n * encrypted door to lose, and a tick otherwise (`UserActionConfirm`), and its plan\n * editor asks for the tick alone (`UserPlanEditor`). Both were hand-built beside this\n * tile because it had no tick. It combines with `phrase` and `requirePassword`: every\n * guard given must be satisfied.\n */\n requireAcknowledge?: ReactNode;\n /**\n * What the action will do, said before it happens — a list under the prompt, shown\n * once armed (keksdose K7, `UserActionConfirm`'s consequences). Strings, or\n * {@link DangerConsequence}s for a `severe` line in the danger colour.\n *\n * Derive them from the row's own facts rather than reciting the worst case: an admin\n * who reads the same paragraph on every row stops reading it (keksdose dev#488).\n */\n consequences?: readonly (string | DangerConsequence)[];\n /** Show a \"type <phrase> to confirm\" field; confirm is held until the field matches\n * (case-sensitive; surrounding spaces ignored unless `phraseMatch=\"exact\"`). */\n phrase?: string;\n /**\n * How the typed text is compared with `phrase`. `\"trim\"` (default) ignores spaces\n * around it — a phone keyboard's autocomplete likes to add one. `\"exact\"` compares\n * character for character, spaces included, for an app whose contract is \"type\n * exactly this\". `\"caseless\"` (0.18) also ignores case — for a phrase that is an\n * e-mail address (keksdose's \"type the user's address\" admin rule). See\n * {@link TypedMatch}.\n */\n phraseMatch?: TypedMatch;\n /** The warning above the fields. Defaults to `labels.prompt`. */\n prompt?: ReactNode;\n /** `\"danger\"` (default) for what cannot be undone; `\"warning\"` for what can, at a\n * cost (loading demo data over your own). Colours the prompt and the confirm. */\n tone?: \"danger\" | \"warning\";\n /** Visible text of the arm button; defaults to `labels.arm`. */\n armLabel?: ReactNode;\n /** Visible text of the confirm button; defaults to `labels.confirm`. */\n confirmLabel?: ReactNode;\n /** The action is running: confirm shows a spinner and nothing can be pressed. For a\n * caller that tracks the mutation itself (a `useMutation`'s `isPending`); a\n * promise returned from `onConfirm` does the same on its own. */\n busy?: boolean;\n /** The arm button is disabled. */\n disabled?: boolean;\n /**\n * Why the action is not available — a read-only demo, a write lock, a missing\n * permission. Disables the arm button like `disabled`, and SAYS so: the sentence is\n * shown under the button and attached to it with `aria-describedby`, and the button\n * stays focusable (`aria-disabled`) so a keyboard user can land on it and hear why.\n *\n * On the ARM button rather than the confirm: arming asks for a password, and asking\n * for a password for a write that can never land is the worse of the two. A tile that\n * is ALREADY armed when the reason arrives (controlled `armed`, or a lock that landed\n * while it was open) keeps its fields but its confirm takes the reason the same way\n * — focusable, `aria-disabled`, the reason in its tooltip — and Enter does nothing.\n */\n lockedReason?: ReactNode;\n /**\n * This tile COMMITS — the 0.18 write-lock opt-in that {@link Button} has (keksdose\n * K3). Under a locked {@link WriteLockProvider} the lock's reason becomes\n * `lockedReason` (and wins over one of the tile's own), so a page that mounts the\n * provider no longer threads `lockedReason={lock.locked ? lock.reason : undefined}`\n * into every tile. No provider, or an unlocked one: no effect.\n */\n commit?: boolean;\n /** Controlled armed state. The parent can then collapse the tile from a mutation's\n * own `onSuccess` without returning a promise. */\n armed?: boolean;\n /** Called with the next armed state — on arm, cancel, and a resolved `onConfirm`. */\n onArmedChange?: (armed: boolean) => void;\n /** User-facing strings; see {@link DangerConfirmLabels}. */\n labels?: Partial<DangerConfirmLabels>;\n}\n\n/**\n * An \"arm → confirm\" tile for destructive actions: one button, which expands into a\n * warning, an optional list of consequences, an optional \"I understand\" tick, an\n * optional type-to-confirm field, an optional password field and a confirm that is held\n * until every guard is satisfied.\n *\n * A held confirm SAYS which guard is still open (0.23, keksdose G4a): it is\n * `aria-disabled` rather than `disabled` — still focusable, so a keyboard user can land\n * on it — with the reason in the kit {@link Tooltip} and its description (\"Type\n * “DELETE” to confirm\", \"Tick the box to confirm\", \"Enter your password to confirm\";\n * `labels.needs*`), the first open guard in the order they are drawn. FormActions'\n * `submitDisabledReason` does the same for a form's Save. Pressing it, or Enter in a\n * field, does nothing. A lock's reason wins over a guard's; while the action runs the\n * confirm is plainly disabled, its spinner saying why.\n *\n * Keksdose hand-rolled it three times (load demo data, wipe everything, reset a\n * budget) and then as `shared/components/danger-confirm.tsx`; the only app-specific\n * part was its write-lock hook, which is `lockedReason` here.\n *\n * The fields are a `<form>`, so Enter confirms once the guards allow it. They are\n * wiped every time the tile disarms — a password must not sit in a collapsed tile.\n * Arming moves focus to the first field (or Cancel, when there is none — never to the\n * destructive button itself), and disarming moves it back to the arm button, so the\n * keyboard user is never left on an element that just vanished.\n */\nexport function DangerConfirm({\n onConfirm,\n requirePassword,\n requireAcknowledge,\n consequences,\n phrase,\n phraseMatch = \"trim\",\n prompt,\n tone = \"danger\",\n armLabel,\n confirmLabel,\n busy: busyProp,\n disabled,\n lockedReason: ownLockedReason,\n commit,\n armed: armedProp,\n onArmedChange,\n labels: labelsProp,\n className,\n ...rest\n}: DangerConfirmProps) {\n const labels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);\n const lockedReason = useCommitReason(commit, ownLockedReason);\n const [armedState, setArmedState] = useState(false);\n const armed = armedProp ?? armedState;\n const [password, setPassword] = useState(\"\");\n const [typed, setTyped] = useState(\"\");\n const [acknowledged, setAcknowledged] = useState(false);\n const { pending, run } = usePromisePending();\n const busy = Boolean(busyProp) || pending;\n // `readOnly` rather than `disabled` on the fields while busy: disabling the field\n // that has focus (Enter was pressed in it) drops focus to <body>.\n const promptId = useId();\n const consequencesId = useId();\n const reasonId = useId();\n\n // The kit's Button takes no ref, so the two buttons focus is moved to are found by id.\n const armId = useId();\n const cancelId = useId();\n const firstFieldRef = useRef<HTMLInputElement>(null);\n // Focus moves only after a transition — never on mount, so a tile that renders armed\n // (controlled) does not take the page's focus merely by existing.\n const moveFocus = useRef(false);\n\n // A disarm from anywhere (cancel, a resolved confirm, the parent) wipes the fields.\n // During render, like NumberField's draft, so no frame shows a collapsed tile that\n // still holds a password.\n const [wasArmed, setWasArmed] = useState(armed);\n if (wasArmed !== armed) {\n setWasArmed(armed);\n if (!armed) {\n setPassword(\"\");\n setTyped(\"\");\n setAcknowledged(false);\n }\n }\n\n const prevArmed = useRef(armed);\n useEffect(() => {\n const changed = prevArmed.current !== armed;\n prevArmed.current = armed;\n const byUser = moveFocus.current;\n moveFocus.current = false;\n if (!changed) return;\n if (armed) {\n if (byUser) (firstFieldRef.current ?? document.getElementById(cancelId))?.focus();\n return;\n }\n // A controlled parent collapsing the tile from its own `onSuccess` did not go\n // through `setArmed`; if focus went down with the form, bring it back too.\n const lost = document.activeElement === null || document.activeElement === document.body;\n if (byUser || lost) document.getElementById(armId)?.focus();\n }, [armed, armId, cancelId]);\n\n const setArmed = (next: boolean) => {\n moveFocus.current = true;\n if (armedProp === undefined) setArmedState(next);\n onArmedChange?.(next);\n };\n\n const locked = hasContent(lockedReason);\n // `true` words the tick with the namespace; any other node is its label; `false`,\n // `null` and `\"\"` are no tick at all.\n const acknowledgeLabel: ReactNode =\n requireAcknowledge === true ? labels.acknowledge : hasContent(requireAcknowledge) ? requireAcknowledge : null;\n const asksAcknowledge = acknowledgeLabel !== null;\n const passwordOk = !requirePassword || password !== \"\";\n const phraseOk = phrase === undefined || typedMatches(typed, phrase, phraseMatch);\n const acknowledgeOk = !asksAcknowledge || acknowledged;\n const canConfirm = passwordOk && phraseOk && acknowledgeOk && !busy;\n // The first guard still open, in the order the fields are drawn — the one the user\n // meets next, so the sentence points at it.\n const guardReason: string | undefined = !acknowledgeOk\n ? labels.needsAcknowledge\n : !phraseOk\n ? typeof labels.needsPhrase === \"function\"\n ? labels.needsPhrase(phrase ?? \"\")\n : labels.needsPhrase\n : !passwordOk\n ? labels.needsPassword\n : undefined;\n // The lock first (no guard can lift it); none while busy, when the guards were met and\n // the spinner is the state.\n const confirmReason: ReactNode = locked ? lockedReason : busy ? undefined : guardReason;\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n // `locked` too: Enter in a field submits the form without the button's say.\n if (!canConfirm || locked) return;\n const values: DangerConfirmValues = {\n ...(phrase !== undefined && { typed: phraseMatch === \"exact\" ? typed : typed.trim() }),\n ...(requirePassword && { password }),\n ...(asksAcknowledge && { acknowledged: true as const }),\n };\n // Disarms when it resolves; a rejection leaves it armed, fields kept: the caller\n // shows why, the user retries.\n run(onConfirm(values.password, values), () => setArmed(false));\n };\n\n // The first field takes focus on arm: the tick, then the phrase, then the password —\n // the order they are drawn in.\n const firstField = asksAcknowledge ? \"acknowledge\" : phrase !== undefined ? \"phrase\" : \"password\";\n const lines = (consequences ?? []).map((item, index) =>\n typeof item === \"string\"\n ? { key: item, text: item as ReactNode, severe: false }\n : { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) },\n );\n\n const toneText = tone === \"warning\" ? \"text-[var(--warning)]\" : \"text-[var(--danger)]\";\n\n if (!armed) {\n const arm = (\n <Button\n id={armId}\n type=\"button\"\n variant={tone === \"warning\" ? \"secondary\" : \"danger\"}\n disabled={disabled}\n aria-disabled={locked || undefined}\n aria-describedby={locked ? reasonId : undefined}\n onClick={() => {\n if (!locked) setArmed(true);\n }}\n className=\"aria-disabled:cursor-not-allowed aria-disabled:opacity-50\"\n >\n {armLabel ?? labels.arm}\n </Button>\n );\n return (\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {locked ? (\n // The reason on the button itself too, where the pointer that tried it is —\n // the line below can sit out of view in a long settings card. Portalled, so\n // a card that scrolls neither clips it nor grows by it.\n //\n // The button in a FRAGMENT on purpose: Tooltip clones an element child to\n // add the bubble to its `aria-describedby`, and the button is already\n // described by the visible line below — the same sentence twice, read out\n // on every focus. A fragment is left as it is, so the bubble stays visual.\n <Tooltip label={lockedReason} portal>\n <>{arm}</>\n </Tooltip>\n ) : (\n arm\n )}\n {locked && (\n <p id={reasonId} className=\"text-xs text-[var(--text-muted)]\">\n {lockedReason}\n </p>\n )}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"space-y-2\", className)}>\n <form\n className=\"space-y-2\"\n aria-describedby={lines.length > 0 ? `${promptId} ${consequencesId}` : promptId}\n aria-busy={busy || undefined}\n onSubmit={submit}\n noValidate\n >\n <p id={promptId} className={cn(\"text-xs font-medium\", toneText)}>\n {prompt ?? labels.prompt}\n </p>\n {lines.length > 0 && (\n <ul id={consequencesId} className=\"list-disc space-y-1 ps-5 text-xs text-[var(--text-secondary)]\">\n {lines.map((line) => (\n <li key={line.key} className={line.severe ? \"text-[var(--danger)]\" : undefined}>\n {line.text}\n </li>\n ))}\n </ul>\n )}\n {asksAcknowledge && (\n <Checkbox\n ref={firstField === \"acknowledge\" ? firstFieldRef : undefined}\n label={acknowledgeLabel}\n checked={acknowledged}\n // Not `disabled` while busy, for the same reason the fields are readOnly:\n // the box may hold the focus. A change while busy is simply not taken.\n onCheckedChange={(next) => {\n if (!busy) setAcknowledged(next);\n }}\n />\n )}\n {phrase !== undefined && (\n <TypedConfirmField\n ref={firstField === \"phrase\" ? firstFieldRef : undefined}\n target={phrase}\n match={phraseMatch}\n value={typed}\n onValueChange={setTyped}\n busy={busy}\n // Already resolved through this tile's own `labels` prop; handed on so a\n // per-instance phrase wording reaches the field.\n labels={{ phrase: labels.phrase, phrasePlaceholder: labels.phrasePlaceholder }}\n />\n )}\n {requirePassword && (\n <CurrentPasswordInput\n ref={firstField === \"password\" ? firstFieldRef : undefined}\n label={labels.password}\n value={password}\n busy={busy}\n onValueChange={setPassword}\n />\n )}\n <div className=\"flex flex-wrap gap-2\">\n <Button id={cancelId} type=\"button\" variant=\"ghost\" disabled={busy} onClick={() => setArmed(false)}>\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n variant={tone === \"warning\" ? \"primary\" : \"danger\"}\n disabled={!canConfirm}\n // Held by a guard, or armed and then locked (or rendered armed under a lock):\n // the confirm says why the way Button does — focusable, `aria-disabled`, the\n // reason in its tooltip and description, a press swallowed (the submit with\n // it) — and the fields stay as typed. Only busy is the native `disabled`.\n disabledReason={confirmReason}\n aria-busy={busy || undefined}\n >\n {busy && <Spinner label={null} className=\"h-4 w-4\" />}\n {confirmLabel ?? labels.confirm}\n </Button>\n </div>\n {locked && <p className=\"text-xs text-[var(--text-muted)]\">{lockedReason}</p>}\n </form>\n </div>\n );\n}\n"],"mappings":"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","names":["CurrentPasswordInput","TypedConfirmField"]}
@@ -1,7 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { DataTableLabels } from './data-table-labels.js';
4
- import { b as DataTableColumn } from '../data-table-3mi2R3so.js';
3
+ import { d as DataTableColumn, f as DataTableLabels } from '../data-table-labels-B7OdnM0S.js';
5
4
  import './data-table-sort.js';
6
5
  import './swipeable-row.js';
7
6
 
@@ -1,6 +1,5 @@
1
1
  import * as react from 'react';
2
- import { b as DataTableColumn, f as FilterValue } from '../data-table-3mi2R3so.js';
3
- import { DataTableLabels } from './data-table-labels.js';
2
+ import { d as DataTableColumn, j as FilterValue, f as DataTableLabels } from '../data-table-labels-B7OdnM0S.js';
4
3
  import './data-table-sort.js';
5
4
  import './swipeable-row.js';
6
5
 
@@ -1,5 +1,4 @@
1
- export { C as ColumnFilter, f as FilterValue, j as dateFilter, k as decodeFilterValue, l as decodeFilterValueOfType, m as defaultFilterState, n as encodeFilterValue, o as filterHref, p as isFilterActive, q as numberFilter, s as resolveFilter, t as rowMatches, u as selectFilter, v as textFilter } from '../data-table-3mi2R3so.js';
1
+ export { C as ColumnFilter, j as FilterValue, n as dateFilter, o as decodeFilterValue, p as decodeFilterValueOfType, q as defaultFilterState, r as encodeFilterValue, s as filterHref, t as isFilterActive, v as numberFilter, y as resolveFilter, z as rowMatches, A as selectFilter, B as textFilter } from '../data-table-labels-B7OdnM0S.js';
2
2
  import 'react';
3
3
  import './data-table-sort.js';
4
4
  import './swipeable-row.js';
5
- import './data-table-labels.js';
@@ -1,97 +1,4 @@
1
- interface DataTableLabels {
2
- columns: string;
3
- selectAllRows: string;
4
- sortHint: string;
5
- filter: string;
6
- close: string;
7
- selectRow: string;
8
- autoSize: string;
9
- loading: string;
10
- filters: string;
11
- clearAll: string;
12
- done: string;
13
- pageSize: string;
14
- pageSizeAll: string;
15
- prevPage: string;
16
- nextPage: string;
17
- clearFilter: string;
18
- filterPlaceholder: string;
19
- selectFilter: string;
20
- selectAll: string;
21
- selectNone: string;
22
- dateFrom: string;
23
- dateTo: string;
24
- numberMin: string;
25
- numberMax: string;
26
- numberAbs: string;
27
- presets: Record<string, string>;
28
- /**
29
- * The table's own accessible name. Deliberately a label rather than a second,
30
- * parallel `tableLabel` prop: the audit already records "three different prop
31
- * names for the accessible name" as a finding, and a table that is not "Data
32
- * table" says so with `labels={{ table: "Transactions" }}` at the call site.
33
- */
34
- table: string;
35
- /** After a filter changes: "12 of 137 rows". */
36
- filterResults: (shown: number, total: number) => string;
37
- /** After a header is activated: "Sorted by Name, ascending". */
38
- sortedAscending: (column: string) => string;
39
- sortedDescending: (column: string) => string;
40
- /** The third click, which removes the column from the sort entirely. */
41
- sortCleared: (column: string) => string;
42
- /** After paging: "Page 3 of 14". `page` is 1-BASED, as spoken. */
43
- pageChanged: (page: number, totalPages: number) => string;
44
- /** The pagination range summary: "1–25 / 137". */
45
- pageRange: (from: number, to: number, total: number) => string;
46
- /** The same summary when the page size is "all" — just the row count. */
47
- rowCount: (total: number) => string;
48
- /** The column-settings rail and its heading: "Columns (4/9)". */
49
- columnsCount: (visible: number, total: number) => string;
50
- /** The body of a table with no rows and no `empty` prop of its own. A sentence, not
51
- * "—": a dash in the only row of a table reads as one row with a missing value. */
52
- empty: string;
53
- /** The accessible name of the `rowActions` column (its header is visually empty). */
54
- actions: string;
55
- /** The `kind: "edit"` row action's name — its tooltip and its swipe label. */
56
- edit: string;
57
- /** The `kind: "delete"` row action's name. */
58
- delete: string;
59
- /** What {@link BooleanMark} says to a screen reader for `true` / `false` / nothing. */
60
- booleanTrue: string;
61
- booleanFalse: string;
62
- booleanUnset: string;
63
- }
64
- declare const DEFAULT_DATA_TABLE_LABELS: DataTableLabels;
65
- /**
66
- * Merge caller overrides onto the English defaults (presets merged too).
67
- *
68
- * The parameter is `Partial` on purpose — a table with no labels at all still
69
- * renders — but that is also how a translated app ends up half English and never
70
- * hears about it: lenkbank ships nineteen of these keys in English in a German UI
71
- * today, because every one of them is backfilled silently. The kit will not start
72
- * throwing over it (three apps depend on the lenient merge), so
73
- * {@link missingDataTableLabels} is the other half: it answers which keys fell back,
74
- * and an app asserts on that in its OWN test, where the answer is actionable.
75
- */
76
- declare function resolveDataTableLabels(partial?: Partial<DataTableLabels>): DataTableLabels;
77
- /**
78
- * The label keys `labels` does not supply, and which therefore come back English
79
- * from {@link resolveDataTableLabels}.
80
- *
81
- * For a consumer's own test:
82
- *
83
- * ```ts
84
- * expect(missingDataTableLabels(de.dataTable)).toEqual([]);
85
- * ```
86
- *
87
- * which fails the day the kit adds a key — which is the point. Adding a label is
88
- * additive here and a silent regression in a translated app, and nothing else in
89
- * either repository can tell the two apart.
90
- *
91
- * Preset labels are reported one at a time (`presets.yesterday`), because `presets`
92
- * is a `Record` merged key by key: answering `"presets"` would say nothing about
93
- * which of the eleven are still English.
94
- */
95
- declare function missingDataTableLabels(labels?: Partial<DataTableLabels>): string[];
96
-
97
- export { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels, missingDataTableLabels, resolveDataTableLabels };
1
+ export { D as DEFAULT_DATA_TABLE_LABELS, f as DataTableLabels, u as missingDataTableLabels, x as resolveDataTableLabels } from '../data-table-labels-B7OdnM0S.js';
2
+ import 'react';
3
+ import './data-table-sort.js';
4
+ import './swipeable-row.js';
@@ -54,7 +54,15 @@ const DEFAULT_DATA_TABLE_LABELS = {
54
54
  delete: "Delete",
55
55
  booleanTrue: "Yes",
56
56
  booleanFalse: "No",
57
- booleanUnset: "Not set"
57
+ booleanUnset: "Not set",
58
+ // The phone sort control (0.22.0). The same words as `DEFAULT_DATA_TABLE_SORT_LABELS`
59
+ // in data-table.tsx, written out rather than spread in: importing that VALUE here
60
+ // would make data-table.tsx and this module import each other (data-table.tsx reads
61
+ // its resolver from here), and would pull the whole table into `i18n/defaults`.
62
+ sortBy: "Sort by",
63
+ sortDefault: "Default order",
64
+ sortAscending: "Ascending",
65
+ sortDescending: "Descending"
58
66
  };
59
67
  function resolveDataTableLabels(partial) {
60
68
  if (!partial) return DEFAULT_DATA_TABLE_LABELS;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-labels.ts"],"sourcesContent":["// All user-facing strings for the DataTable system — the `dataTable` namespace of\n// `<UiKitProvider labels>`, and per table the `labels` prop over that. The package\n// ships no translations; the English defaults below make the table usable with no\n// wiring.\n\nexport interface DataTableLabels {\n // Table chrome\n columns: string;\n selectAllRows: string;\n sortHint: string;\n filter: string;\n close: string;\n selectRow: string;\n autoSize: string;\n loading: string;\n filters: string;\n clearAll: string;\n done: string;\n // Pagination\n pageSize: string;\n pageSizeAll: string;\n prevPage: string;\n nextPage: string;\n // Filter popover\n clearFilter: string;\n filterPlaceholder: string;\n selectFilter: string;\n selectAll: string;\n selectNone: string;\n dateFrom: string;\n dateTo: string;\n numberMin: string;\n numberMax: string;\n numberAbs: string;\n // Date-range preset labels, keyed by preset key (see dateRangePresets()).\n presets: Record<string, string>;\n\n /**\n * The table's own accessible name. Deliberately a label rather than a second,\n * parallel `tableLabel` prop: the audit already records \"three different prop\n * names for the accessible name\" as a finding, and a table that is not \"Data\n * table\" says so with `labels={{ table: \"Transactions\" }}` at the call site.\n */\n table: string;\n\n // ---- Announcements ----\n // Spoken through the live region, never rendered. Each takes its numbers as\n // ARGUMENTS: a count glued into an English template literal is untranslatable,\n // and the grammar around it moves with the number in most languages.\n\n /** After a filter changes: \"12 of 137 rows\". */\n filterResults: (shown: number, total: number) => string;\n /** After a header is activated: \"Sorted by Name, ascending\". */\n sortedAscending: (column: string) => string;\n sortedDescending: (column: string) => string;\n /** The third click, which removes the column from the sort entirely. */\n sortCleared: (column: string) => string;\n /** After paging: \"Page 3 of 14\". `page` is 1-BASED, as spoken. */\n pageChanged: (page: number, totalPages: number) => string;\n\n // ---- Counted chrome ----\n // Visible text that interpolates numbers. These were template literals until the\n // audit flagged them: a consumer could translate every key in this file and still\n // be left with English (and with ASCII digit grouping) in the footer.\n\n /** The pagination range summary: \"1–25 / 137\". */\n pageRange: (from: number, to: number, total: number) => string;\n /** The same summary when the page size is \"all\" — just the row count. */\n rowCount: (total: number) => string;\n /** The column-settings rail and its heading: \"Columns (4/9)\". */\n columnsCount: (visible: number, total: number) => string;\n\n // ---- Empty state, row actions, boolean cells (0.12.0) ----\n\n /** The body of a table with no rows and no `empty` prop of its own. A sentence, not\n * \"—\": a dash in the only row of a table reads as one row with a missing value. */\n empty: string;\n /** The accessible name of the `rowActions` column (its header is visually empty). */\n actions: string;\n /** The `kind: \"edit\"` row action's name — its tooltip and its swipe label. */\n edit: string;\n /** The `kind: \"delete\"` row action's name. */\n delete: string;\n /** What {@link BooleanMark} says to a screen reader for `true` / `false` / nothing. */\n booleanTrue: string;\n booleanFalse: string;\n booleanUnset: string;\n}\n\nexport const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {\n columns: \"Columns\",\n selectAllRows: \"Select all rows\",\n sortHint: \"Click to sort · Shift-click to add a sort\",\n filter: \"Filter\",\n close: \"Close\",\n selectRow: \"Select row\",\n autoSize: \"Auto-size columns\",\n loading: \"Loading…\",\n filters: \"Filters\",\n clearAll: \"Clear all\",\n done: \"Done\",\n pageSize: \"Rows per page\",\n pageSizeAll: \"All\",\n prevPage: \"Previous page\",\n nextPage: \"Next page\",\n clearFilter: \"Clear filter\",\n filterPlaceholder: \"Filter…\",\n selectFilter: \"Select\",\n selectAll: \"All\",\n selectNone: \"None\",\n dateFrom: \"From\",\n dateTo: \"To\",\n numberMin: \"Min\",\n numberMax: \"Max\",\n numberAbs: \"Absolute value\",\n presets: {\n today: \"Today\",\n yesterday: \"Yesterday\",\n this_week: \"This week\",\n last_week: \"Last week\",\n last_7_days: \"Last 7 days\",\n last_30_days: \"Last 30 days\",\n this_month: \"This month\",\n last_month: \"Last month\",\n last_3_months: \"Last 3 months\",\n ytd: \"Year to date\",\n last_year: \"Last year\",\n },\n table: \"Data table\",\n filterResults: (shown, total) => `${shown} of ${total} rows`,\n sortedAscending: (column) => `Sorted by ${column}, ascending`,\n sortedDescending: (column) => `Sorted by ${column}, descending`,\n sortCleared: (column) => `Sort cleared on ${column}`,\n pageChanged: (page, totalPages) => `Page ${page} of ${totalPages}`,\n // The separators are the ones that shipped, so adding the seam changes no screen.\n pageRange: (from, to, total) => `${from}–${to} / ${total}`,\n rowCount: (total) => `${total}`,\n columnsCount: (visible, total) => `Columns (${visible}/${total})`,\n empty: \"No entries\",\n actions: \"Actions\",\n edit: \"Edit\",\n delete: \"Delete\",\n booleanTrue: \"Yes\",\n booleanFalse: \"No\",\n booleanUnset: \"Not set\",\n};\n\n/**\n * Merge caller overrides onto the English defaults (presets merged too).\n *\n * The parameter is `Partial` on purpose — a table with no labels at all still\n * renders — but that is also how a translated app ends up half English and never\n * hears about it: lenkbank ships nineteen of these keys in English in a German UI\n * today, because every one of them is backfilled silently. The kit will not start\n * throwing over it (three apps depend on the lenient merge), so\n * {@link missingDataTableLabels} is the other half: it answers which keys fell back,\n * and an app asserts on that in its OWN test, where the answer is actionable.\n */\nexport function resolveDataTableLabels(partial?: Partial<DataTableLabels>): DataTableLabels {\n if (!partial) return DEFAULT_DATA_TABLE_LABELS;\n const merged: DataTableLabels = {\n ...DEFAULT_DATA_TABLE_LABELS,\n ...partial,\n presets: { ...DEFAULT_DATA_TABLE_LABELS.presets, ...(partial.presets ?? {}) },\n };\n // `columnsCount` was split out of `columns` + a template literal. Consumers have\n // been translating `columns` since 0.1, and backfilling the English default over\n // that would hand them \"Columns (4/9)\" back for having translated the key that\n // existed at the time — the same silent-English failure the audit records against\n // this file. So the count is derived from the RESOLVED `columns` unless the caller\n // states the whole sentence, which is the escape hatch for a language where the\n // parenthetical does not fit.\n if (partial.columns !== undefined && partial.columnsCount === undefined) {\n merged.columnsCount = (visible, total) => `${merged.columns} (${visible}/${total})`;\n }\n return merged;\n}\n\n/**\n * The label keys `labels` does not supply, and which therefore come back English\n * from {@link resolveDataTableLabels}.\n *\n * For a consumer's own test:\n *\n * ```ts\n * expect(missingDataTableLabels(de.dataTable)).toEqual([]);\n * ```\n *\n * which fails the day the kit adds a key — which is the point. Adding a label is\n * additive here and a silent regression in a translated app, and nothing else in\n * either repository can tell the two apart.\n *\n * Preset labels are reported one at a time (`presets.yesterday`), because `presets`\n * is a `Record` merged key by key: answering `\"presets\"` would say nothing about\n * which of the eleven are still English.\n */\nexport function missingDataTableLabels(labels?: Partial<DataTableLabels>): string[] {\n const missing: string[] = [];\n for (const key of Object.keys(DEFAULT_DATA_TABLE_LABELS) as Array<keyof DataTableLabels>) {\n if (key === \"presets\") continue;\n if (labels?.[key] !== undefined) continue;\n // `columnsCount` is DERIVED from a translated `columns` by the resolver above,\n // so a caller who set that one is not leaking English here. Reporting it would\n // send them to translate a key they have already covered, and an `toEqual([])`\n // assertion built on this would never go green.\n if (key === \"columnsCount\" && labels?.columns !== undefined) continue;\n missing.push(key);\n }\n for (const preset of Object.keys(DEFAULT_DATA_TABLE_LABELS.presets)) {\n if (labels?.presets?.[preset] === undefined) missing.push(`presets.${preset}`);\n }\n return missing;\n}\n"],"mappings":";AAyFO,MAAM,4BAA6C;AAAA,EACxD,SAAS;AAAA,EACT,eAAe;AAAA,EACf,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,KAAK;AAAA,IACL,WAAW;AAAA,EACb;AAAA,EACA,OAAO;AAAA,EACP,eAAe,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,KAAK;AAAA,EACrD,iBAAiB,CAAC,WAAW,aAAa,MAAM;AAAA,EAChD,kBAAkB,CAAC,WAAW,aAAa,MAAM;AAAA,EACjD,aAAa,CAAC,WAAW,mBAAmB,MAAM;AAAA,EAClD,aAAa,CAAC,MAAM,eAAe,QAAQ,IAAI,OAAO,UAAU;AAAA;AAAA,EAEhE,WAAW,CAAC,MAAM,IAAI,UAAU,GAAG,IAAI,SAAI,EAAE,MAAM,KAAK;AAAA,EACxD,UAAU,CAAC,UAAU,GAAG,KAAK;AAAA,EAC7B,cAAc,CAAC,SAAS,UAAU,YAAY,OAAO,IAAI,KAAK;AAAA,EAC9D,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,cAAc;AAAA,EACd,cAAc;AAChB;AAaO,SAAS,uBAAuB,SAAqD;AAC1F,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,SAA0B;AAAA,IAC9B,GAAG;AAAA,IACH,GAAG;AAAA,IACH,SAAS,EAAE,GAAG,0BAA0B,SAAS,GAAI,QAAQ,WAAW,CAAC,EAAG;AAAA,EAC9E;AAQA,MAAI,QAAQ,YAAY,UAAa,QAAQ,iBAAiB,QAAW;AACvE,WAAO,eAAe,CAAC,SAAS,UAAU,GAAG,OAAO,OAAO,KAAK,OAAO,IAAI,KAAK;AAAA,EAClF;AACA,SAAO;AACT;AAoBO,SAAS,uBAAuB,QAA6C;AAClF,QAAM,UAAoB,CAAC;AAC3B,aAAW,OAAO,OAAO,KAAK,yBAAyB,GAAmC;AACxF,QAAI,QAAQ,UAAW;AACvB,QAAI,SAAS,GAAG,MAAM,OAAW;AAKjC,QAAI,QAAQ,kBAAkB,QAAQ,YAAY,OAAW;AAC7D,YAAQ,KAAK,GAAG;AAAA,EAClB;AACA,aAAW,UAAU,OAAO,KAAK,0BAA0B,OAAO,GAAG;AACnE,QAAI,QAAQ,UAAU,MAAM,MAAM,OAAW,SAAQ,KAAK,WAAW,MAAM,EAAE;AAAA,EAC/E;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-labels.ts"],"sourcesContent":["// All user-facing strings for the DataTable system — the `dataTable` namespace of\n// `<UiKitProvider labels>`, and per table the `labels` prop over that. The package\n// ships no translations; the English defaults below make the table usable with no\n// wiring.\n\n// A type-only import: erased at build time, so this module stays free of the table\n// itself (and of react-router), which `i18n/defaults` relies on.\nimport type { DataTableSortLabels } from \"./data-table\";\n\n/** The phone sort control's four keys (`sortBy`, `sortDefault`, `sortAscending`,\n * `sortDescending`, 0.22.0) are {@link DataTableSortLabels}', declared beside the\n * control and part of this namespace like every other key. */\nexport interface DataTableLabels extends DataTableSortLabels {\n // Table chrome\n columns: string;\n selectAllRows: string;\n sortHint: string;\n filter: string;\n close: string;\n selectRow: string;\n autoSize: string;\n loading: string;\n filters: string;\n clearAll: string;\n done: string;\n // Pagination\n pageSize: string;\n pageSizeAll: string;\n prevPage: string;\n nextPage: string;\n // Filter popover\n clearFilter: string;\n filterPlaceholder: string;\n selectFilter: string;\n selectAll: string;\n selectNone: string;\n dateFrom: string;\n dateTo: string;\n numberMin: string;\n numberMax: string;\n numberAbs: string;\n // Date-range preset labels, keyed by preset key (see dateRangePresets()).\n presets: Record<string, string>;\n\n /**\n * The table's own accessible name. Deliberately a label rather than a second,\n * parallel `tableLabel` prop: the audit already records \"three different prop\n * names for the accessible name\" as a finding, and a table that is not \"Data\n * table\" says so with `labels={{ table: \"Transactions\" }}` at the call site.\n */\n table: string;\n\n // ---- Announcements ----\n // Spoken through the live region, never rendered. Each takes its numbers as\n // ARGUMENTS: a count glued into an English template literal is untranslatable,\n // and the grammar around it moves with the number in most languages.\n\n /** After a filter changes: \"12 of 137 rows\". */\n filterResults: (shown: number, total: number) => string;\n /** After a header is activated: \"Sorted by Name, ascending\". */\n sortedAscending: (column: string) => string;\n sortedDescending: (column: string) => string;\n /** The third click, which removes the column from the sort entirely. */\n sortCleared: (column: string) => string;\n /** After paging: \"Page 3 of 14\". `page` is 1-BASED, as spoken. */\n pageChanged: (page: number, totalPages: number) => string;\n\n // ---- Counted chrome ----\n // Visible text that interpolates numbers. These were template literals until the\n // audit flagged them: a consumer could translate every key in this file and still\n // be left with English (and with ASCII digit grouping) in the footer.\n\n /** The pagination range summary: \"1–25 / 137\". */\n pageRange: (from: number, to: number, total: number) => string;\n /** The same summary when the page size is \"all\" — just the row count. */\n rowCount: (total: number) => string;\n /** The column-settings rail and its heading: \"Columns (4/9)\". */\n columnsCount: (visible: number, total: number) => string;\n\n // ---- Empty state, row actions, boolean cells (0.12.0) ----\n\n /** The body of a table with no rows and no `empty` prop of its own. A sentence, not\n * \"—\": a dash in the only row of a table reads as one row with a missing value. */\n empty: string;\n /** The accessible name of the `rowActions` column (its header is visually empty). */\n actions: string;\n /** The `kind: \"edit\"` row action's name — its tooltip and its swipe label. */\n edit: string;\n /** The `kind: \"delete\"` row action's name. */\n delete: string;\n /** What {@link BooleanMark} says to a screen reader for `true` / `false` / nothing. */\n booleanTrue: string;\n booleanFalse: string;\n booleanUnset: string;\n}\n\nexport const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {\n columns: \"Columns\",\n selectAllRows: \"Select all rows\",\n sortHint: \"Click to sort · Shift-click to add a sort\",\n filter: \"Filter\",\n close: \"Close\",\n selectRow: \"Select row\",\n autoSize: \"Auto-size columns\",\n loading: \"Loading…\",\n filters: \"Filters\",\n clearAll: \"Clear all\",\n done: \"Done\",\n pageSize: \"Rows per page\",\n pageSizeAll: \"All\",\n prevPage: \"Previous page\",\n nextPage: \"Next page\",\n clearFilter: \"Clear filter\",\n filterPlaceholder: \"Filter…\",\n selectFilter: \"Select\",\n selectAll: \"All\",\n selectNone: \"None\",\n dateFrom: \"From\",\n dateTo: \"To\",\n numberMin: \"Min\",\n numberMax: \"Max\",\n numberAbs: \"Absolute value\",\n presets: {\n today: \"Today\",\n yesterday: \"Yesterday\",\n this_week: \"This week\",\n last_week: \"Last week\",\n last_7_days: \"Last 7 days\",\n last_30_days: \"Last 30 days\",\n this_month: \"This month\",\n last_month: \"Last month\",\n last_3_months: \"Last 3 months\",\n ytd: \"Year to date\",\n last_year: \"Last year\",\n },\n table: \"Data table\",\n filterResults: (shown, total) => `${shown} of ${total} rows`,\n sortedAscending: (column) => `Sorted by ${column}, ascending`,\n sortedDescending: (column) => `Sorted by ${column}, descending`,\n sortCleared: (column) => `Sort cleared on ${column}`,\n pageChanged: (page, totalPages) => `Page ${page} of ${totalPages}`,\n // The separators are the ones that shipped, so adding the seam changes no screen.\n pageRange: (from, to, total) => `${from}–${to} / ${total}`,\n rowCount: (total) => `${total}`,\n columnsCount: (visible, total) => `Columns (${visible}/${total})`,\n empty: \"No entries\",\n actions: \"Actions\",\n edit: \"Edit\",\n delete: \"Delete\",\n booleanTrue: \"Yes\",\n booleanFalse: \"No\",\n booleanUnset: \"Not set\",\n // The phone sort control (0.22.0). The same words as `DEFAULT_DATA_TABLE_SORT_LABELS`\n // in data-table.tsx, written out rather than spread in: importing that VALUE here\n // would make data-table.tsx and this module import each other (data-table.tsx reads\n // its resolver from here), and would pull the whole table into `i18n/defaults`.\n sortBy: \"Sort by\",\n sortDefault: \"Default order\",\n sortAscending: \"Ascending\",\n sortDescending: \"Descending\",\n};\n\n/**\n * Merge caller overrides onto the English defaults (presets merged too).\n *\n * The parameter is `Partial` on purpose — a table with no labels at all still\n * renders — but that is also how a translated app ends up half English and never\n * hears about it: lenkbank ships nineteen of these keys in English in a German UI\n * today, because every one of them is backfilled silently. The kit will not start\n * throwing over it (three apps depend on the lenient merge), so\n * {@link missingDataTableLabels} is the other half: it answers which keys fell back,\n * and an app asserts on that in its OWN test, where the answer is actionable.\n */\nexport function resolveDataTableLabels(partial?: Partial<DataTableLabels>): DataTableLabels {\n if (!partial) return DEFAULT_DATA_TABLE_LABELS;\n const merged: DataTableLabels = {\n ...DEFAULT_DATA_TABLE_LABELS,\n ...partial,\n presets: { ...DEFAULT_DATA_TABLE_LABELS.presets, ...(partial.presets ?? {}) },\n };\n // `columnsCount` was split out of `columns` + a template literal. Consumers have\n // been translating `columns` since 0.1, and backfilling the English default over\n // that would hand them \"Columns (4/9)\" back for having translated the key that\n // existed at the time — the same silent-English failure the audit records against\n // this file. So the count is derived from the RESOLVED `columns` unless the caller\n // states the whole sentence, which is the escape hatch for a language where the\n // parenthetical does not fit.\n if (partial.columns !== undefined && partial.columnsCount === undefined) {\n merged.columnsCount = (visible, total) => `${merged.columns} (${visible}/${total})`;\n }\n return merged;\n}\n\n/**\n * The label keys `labels` does not supply, and which therefore come back English\n * from {@link resolveDataTableLabels}.\n *\n * For a consumer's own test:\n *\n * ```ts\n * expect(missingDataTableLabels(de.dataTable)).toEqual([]);\n * ```\n *\n * which fails the day the kit adds a key — which is the point. Adding a label is\n * additive here and a silent regression in a translated app, and nothing else in\n * either repository can tell the two apart.\n *\n * Preset labels are reported one at a time (`presets.yesterday`), because `presets`\n * is a `Record` merged key by key: answering `\"presets\"` would say nothing about\n * which of the eleven are still English.\n */\nexport function missingDataTableLabels(labels?: Partial<DataTableLabels>): string[] {\n const missing: string[] = [];\n for (const key of Object.keys(DEFAULT_DATA_TABLE_LABELS) as Array<keyof DataTableLabels>) {\n if (key === \"presets\") continue;\n if (labels?.[key] !== undefined) continue;\n // `columnsCount` is DERIVED from a translated `columns` by the resolver above,\n // so a caller who set that one is not leaking English here. Reporting it would\n // send them to translate a key they have already covered, and an `toEqual([])`\n // assertion built on this would never go green.\n if (key === \"columnsCount\" && labels?.columns !== undefined) continue;\n missing.push(key);\n }\n for (const preset of Object.keys(DEFAULT_DATA_TABLE_LABELS.presets)) {\n if (labels?.presets?.[preset] === undefined) missing.push(`presets.${preset}`);\n }\n return missing;\n}\n"],"mappings":";AAgGO,MAAM,4BAA6C;AAAA,EACxD,SAAS;AAAA,EACT,eAAe;AAAA,EACf,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,KAAK;AAAA,IACL,WAAW;AAAA,EACb;AAAA,EACA,OAAO;AAAA,EACP,eAAe,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,KAAK;AAAA,EACrD,iBAAiB,CAAC,WAAW,aAAa,MAAM;AAAA,EAChD,kBAAkB,CAAC,WAAW,aAAa,MAAM;AAAA,EACjD,aAAa,CAAC,WAAW,mBAAmB,MAAM;AAAA,EAClD,aAAa,CAAC,MAAM,eAAe,QAAQ,IAAI,OAAO,UAAU;AAAA;AAAA,EAEhE,WAAW,CAAC,MAAM,IAAI,UAAU,GAAG,IAAI,SAAI,EAAE,MAAM,KAAK;AAAA,EACxD,UAAU,CAAC,UAAU,GAAG,KAAK;AAAA,EAC7B,cAAc,CAAC,SAAS,UAAU,YAAY,OAAO,IAAI,KAAK;AAAA,EAC9D,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,cAAc;AAAA,EACd,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA,EAKd,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,eAAe;AAAA,EACf,gBAAgB;AAClB;AAaO,SAAS,uBAAuB,SAAqD;AAC1F,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,SAA0B;AAAA,IAC9B,GAAG;AAAA,IACH,GAAG;AAAA,IACH,SAAS,EAAE,GAAG,0BAA0B,SAAS,GAAI,QAAQ,WAAW,CAAC,EAAG;AAAA,EAC9E;AAQA,MAAI,QAAQ,YAAY,UAAa,QAAQ,iBAAiB,QAAW;AACvE,WAAO,eAAe,CAAC,SAAS,UAAU,GAAG,OAAO,OAAO,KAAK,OAAO,IAAI,KAAK;AAAA,EAClF;AACA,SAAO;AACT;AAoBO,SAAS,uBAAuB,QAA6C;AAClF,QAAM,UAAoB,CAAC;AAC3B,aAAW,OAAO,OAAO,KAAK,yBAAyB,GAAmC;AACxF,QAAI,QAAQ,UAAW;AACvB,QAAI,SAAS,GAAG,MAAM,OAAW;AAKjC,QAAI,QAAQ,kBAAkB,QAAQ,YAAY,OAAW;AAC7D,YAAQ,KAAK,GAAG;AAAA,EAClB;AACA,aAAW,UAAU,OAAO,KAAK,0BAA0B,OAAO,GAAG;AACnE,QAAI,QAAQ,UAAU,MAAM,MAAM,OAAW,SAAQ,KAAK,WAAW,MAAM,EAAE;AAAA,EAC/E;AACA,SAAO;AACT;","names":[]}
@@ -1,5 +1,7 @@
1
1
  import * as react from 'react';
2
- import { DataTableLabels } from './data-table-labels.js';
2
+ import { f as DataTableLabels } from '../data-table-labels-B7OdnM0S.js';
3
+ import './data-table-sort.js';
4
+ import './swipeable-row.js';
3
5
 
4
6
  declare const PAGE_SIZE_OPTIONS: readonly [10, 25, 50, 100, 200];
5
7
  interface PaginationProps {
@@ -1,5 +1,4 @@
1
1
  import 'react';
2
- export { D as DataTable, x as DataTableCellProps, a as DataTableChrome, b as DataTableColumn, c as DataTableDensity, y as DataTableHeadProps, d as DataTableProps, e as DataTableRowAction, F as FilterState, M as MobileSwipeActions, S as ServerPagination } from '../data-table-3mi2R3so.js';
2
+ export { a as DEFAULT_DATA_TABLE_SORT_LABELS, b as DataTable, G as DataTableCellProps, c as DataTableChrome, d as DataTableColumn, e as DataTableDensity, H as DataTableHeadProps, g as DataTableProps, h as DataTableRowAction, i as DataTableSortLabels, F as FilterState, M as MobileSwipeActions, S as ServerPagination } from '../data-table-labels-B7OdnM0S.js';
3
3
  export { SortState } from './data-table-sort.js';
4
4
  import './swipeable-row.js';
5
- import './data-table-labels.js';
@@ -5,8 +5,10 @@ import { Fragment as Fragment2, useCallback, useEffect, useId, useMemo, useRef,
5
5
  import { createPortal } from "react-dom";
6
6
  import {
7
7
  ArrowDown,
8
+ ArrowDownWideNarrow,
8
9
  ArrowUp,
9
10
  ArrowUpDown,
11
+ ArrowUpNarrowWide,
10
12
  ChevronDown,
11
13
  ChevronRight,
12
14
  Filter,
@@ -14,7 +16,7 @@ import {
14
16
  Trash2,
15
17
  X
16
18
  } from "lucide-react";
17
- import { Card, IconButton, Spinner } from "./ui.js";
19
+ import { Card, IconButton, Select, Spinner } from "./ui.js";
18
20
  import { cn } from "../lib/cn.js";
19
21
  import {
20
22
  defaultFilterState,
@@ -33,7 +35,7 @@ import { FullBleedDialog } from "./full-bleed-dialog.js";
33
35
  import { Popover } from "./popover.js";
34
36
  import { useMediaQuery } from "../hooks/use-media-query.js";
35
37
  import { useAnnounce } from "../hooks/use-announce.js";
36
- import { resolveDataTableLabels } from "./data-table-labels.js";
38
+ import { DEFAULT_DATA_TABLE_LABELS, resolveDataTableLabels } from "./data-table-labels.js";
37
39
  import { useKitLabelOverrides, useKitLocale } from "../i18n/kit-labels.js";
38
40
  import { Tooltip } from "./tooltip.js";
39
41
  import { dirOf, isRtl } from "../lib/direction.js";
@@ -97,7 +99,68 @@ function cleanAttrs(attrs) {
97
99
  }
98
100
  function columnLabel(col, labels) {
99
101
  if (col.key === ROW_ACTIONS_KEY && labels) return labels.actions;
100
- return typeof col.header === "string" ? col.header : col.key;
102
+ return col.headerText ?? (typeof col.header === "string" ? col.header : col.key);
103
+ }
104
+ const DEFAULT_DATA_TABLE_SORT_LABELS = {
105
+ sortBy: DEFAULT_DATA_TABLE_LABELS.sortBy,
106
+ sortDefault: DEFAULT_DATA_TABLE_LABELS.sortDefault,
107
+ sortAscending: DEFAULT_DATA_TABLE_LABELS.sortAscending,
108
+ sortDescending: DEFAULT_DATA_TABLE_LABELS.sortDescending
109
+ };
110
+ const flipDir = (d) => d === "asc" ? "desc" : "asc";
111
+ function MobileSort({
112
+ columns,
113
+ sorts,
114
+ sortCycle,
115
+ compact,
116
+ labels,
117
+ onPick,
118
+ onFlip
119
+ }) {
120
+ const selectId = useId();
121
+ const sortLabels = labels;
122
+ if (columns.length === 0) return null;
123
+ const primary = sorts[0] && columns.some((c) => c.key === sorts[0].key) ? sorts[0] : null;
124
+ const offerDefault = sortCycle === "tri" || primary === null;
125
+ const desc = primary?.dir === "desc";
126
+ return /* @__PURE__ */ jsxs(
127
+ "div",
128
+ {
129
+ "data-table-mobile-sort": "",
130
+ className: cn(
131
+ "flex items-center gap-2 border-b border-[var(--border)]",
132
+ compact ? "px-3 py-1.5" : "px-4 py-2"
133
+ ),
134
+ children: [
135
+ /* @__PURE__ */ jsx("label", { htmlFor: selectId, className: "shrink-0 text-xs text-[var(--text-muted)]", children: sortLabels.sortBy }),
136
+ /* @__PURE__ */ jsxs(
137
+ Select,
138
+ {
139
+ id: selectId,
140
+ size: compact ? "sm" : "md",
141
+ className: "min-w-0 flex-1",
142
+ value: primary?.key ?? "",
143
+ onChange: (e) => onPick(e.target.value || null),
144
+ children: [
145
+ offerDefault && /* @__PURE__ */ jsx("option", { value: "", children: sortLabels.sortDefault }),
146
+ columns.map((col) => /* @__PURE__ */ jsx("option", { value: col.key, children: columnLabel(col, labels) }, col.key))
147
+ ]
148
+ }
149
+ ),
150
+ /* @__PURE__ */ jsx(
151
+ IconButton,
152
+ {
153
+ size: compact ? "xs" : "md",
154
+ variant: "secondary",
155
+ label: desc ? sortLabels.sortDescending : sortLabels.sortAscending,
156
+ disabled: primary === null,
157
+ onClick: onFlip,
158
+ children: desc ? /* @__PURE__ */ jsx(ArrowDownWideNarrow, {}) : /* @__PURE__ */ jsx(ArrowUpNarrowWide, {})
159
+ }
160
+ )
161
+ ]
162
+ }
163
+ );
101
164
  }
102
165
  const OWN_CONTROL = 'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="checkbox"],[role="switch"],[role="menuitem"],[role="option"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
103
166
  function fromOutside(target, row) {
@@ -185,6 +248,7 @@ function DataTable({
185
248
  mobileGroupBy,
186
249
  mobileGroupLabel,
187
250
  mobileCard,
251
+ mobileSort = false,
188
252
  mobileSwipeActions,
189
253
  leadingRow,
190
254
  density = "comfortable",
@@ -367,6 +431,10 @@ function DataTable({
367
431
  firstDir: col?.firstSort,
368
432
  cycle: sortCycle
369
433
  });
434
+ commitSorts(next, key);
435
+ };
436
+ const commitSorts = (next, key) => {
437
+ const col = columns.find((c) => c.key === key);
370
438
  if (onSortsChange) onSortsChange(next);
371
439
  else setInternalSorts(next);
372
440
  const name = col ? columnLabel(col, labels) : key;
@@ -751,6 +819,32 @@ function DataTable({
751
819
  locale
752
820
  }
753
821
  ),
822
+ mobileSort && /* @__PURE__ */ jsx(
823
+ MobileSort,
824
+ {
825
+ columns: visibleColumns.filter((c) => (!isServer || !!onSortsChange) && !!c.sortBy),
826
+ sorts,
827
+ sortCycle,
828
+ compact,
829
+ labels,
830
+ onPick: (key) => {
831
+ if (key === null) {
832
+ if (sorts.length > 0) commitSorts([], sorts[0].key);
833
+ return;
834
+ }
835
+ if (sorts[0]?.key === key) return;
836
+ const col = columns.find((c) => c.key === key);
837
+ commitSorts([{ key, dir: col?.firstSort ?? "asc" }], key);
838
+ },
839
+ onFlip: () => {
840
+ if (sorts.length === 0) return;
841
+ commitSorts(
842
+ sorts.map((s, i) => i === 0 ? { ...s, dir: flipDir(s.dir) } : s),
843
+ sorts[0].key
844
+ );
845
+ }
846
+ }
847
+ ),
754
848
  /* @__PURE__ */ jsxs(
755
849
  "ul",
756
850
  {
@@ -1471,6 +1565,7 @@ function MobileFilters({
1471
1565
  ] });
1472
1566
  }
1473
1567
  export {
1568
+ DEFAULT_DATA_TABLE_SORT_LABELS,
1474
1569
  DataTable
1475
1570
  };
1476
1571
  //# sourceMappingURL=data-table.js.map