@eifi1/ui-kit 0.17.0 → 0.18.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 (167) hide show
  1. package/README.md +29 -4
  2. package/dist/chart.d.ts +10 -1
  3. package/dist/components/account-chips.d.ts +153 -0
  4. package/dist/components/account-chips.js +135 -0
  5. package/dist/components/account-chips.js.map +1 -0
  6. package/dist/components/account-settings-labels.d.ts +10 -2
  7. package/dist/components/account-settings-labels.js +3 -0
  8. package/dist/components/account-settings-labels.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +10 -1
  10. package/dist/components/button-group.d.ts +10 -1
  11. package/dist/components/calculator.d.ts +10 -1
  12. package/dist/components/chip.d.ts +25 -1
  13. package/dist/components/chip.js +143 -6
  14. package/dist/components/chip.js.map +1 -1
  15. package/dist/components/confirm-dialog.d.ts +42 -1
  16. package/dist/components/confirm-dialog.js +36 -5
  17. package/dist/components/confirm-dialog.js.map +1 -1
  18. package/dist/components/copy-button.d.ts +10 -1
  19. package/dist/components/danger-confirm.d.ts +55 -4
  20. package/dist/components/danger-confirm.js +67 -30
  21. package/dist/components/danger-confirm.js.map +1 -1
  22. package/dist/components/data-table-cells.d.ts +1 -1
  23. package/dist/components/data-table-filter-popover.d.ts +1 -1
  24. package/dist/components/data-table-filters.d.ts +1 -1
  25. package/dist/components/data-table.d.ts +1 -1
  26. package/dist/components/data-table.js +2 -0
  27. package/dist/components/data-table.js.map +1 -1
  28. package/dist/components/facing-pair.d.ts +10 -1
  29. package/dist/components/file-button.d.ts +10 -1
  30. package/dist/components/file-dropzone.d.ts +10 -1
  31. package/dist/components/form-actions.d.ts +10 -1
  32. package/dist/components/form-actions.js +3 -0
  33. package/dist/components/form-actions.js.map +1 -1
  34. package/dist/components/money-field.d.ts +10 -1
  35. package/dist/components/number-field.d.ts +11 -2
  36. package/dist/components/number-field.js +23 -22
  37. package/dist/components/number-field.js.map +1 -1
  38. package/dist/components/number-input.d.ts +10 -1
  39. package/dist/components/numpad-sheet.d.ts +10 -1
  40. package/dist/components/passkeys-setting.d.ts +27 -1
  41. package/dist/components/passkeys-setting.js +31 -5
  42. package/dist/components/passkeys-setting.js.map +1 -1
  43. package/dist/components/progress-bar.d.ts +15 -1
  44. package/dist/components/progress-bar.js +8 -1
  45. package/dist/components/progress-bar.js.map +1 -1
  46. package/dist/components/reauth-dialog.d.ts +90 -0
  47. package/dist/components/reauth-dialog.js +158 -0
  48. package/dist/components/reauth-dialog.js.map +1 -0
  49. package/dist/components/series-chart.d.ts +10 -1
  50. package/dist/components/server-wake.d.ts +58 -0
  51. package/dist/components/server-wake.js +58 -0
  52. package/dist/components/server-wake.js.map +1 -0
  53. package/dist/components/settings-fields.d.ts +10 -1
  54. package/dist/components/share-card.d.ts +61 -0
  55. package/dist/components/share-card.js +350 -0
  56. package/dist/components/share-card.js.map +1 -0
  57. package/dist/components/status-dot.d.ts +2 -0
  58. package/dist/components/text-link.d.ts +10 -1
  59. package/dist/components/time-input.d.ts +10 -1
  60. package/dist/components/toast.d.ts +10 -1
  61. package/dist/components/toast.js +42 -2
  62. package/dist/components/toast.js.map +1 -1
  63. package/dist/components/toggle-group.js +5 -2
  64. package/dist/components/toggle-group.js.map +1 -1
  65. package/dist/components/ui.d.ts +10 -1
  66. package/dist/components/ui.js +43 -28
  67. package/dist/components/ui.js.map +1 -1
  68. package/dist/components/use-table-state.d.ts +1 -1
  69. package/dist/components/user-avatar.d.ts +2 -0
  70. package/dist/components/write-lock.d.ts +68 -0
  71. package/dist/components/write-lock.js +33 -0
  72. package/dist/components/write-lock.js.map +1 -0
  73. package/dist/{data-table-CpkWUJBY.d.ts → data-table-3mi2R3so.d.ts} +19 -1
  74. package/dist/data-table.d.ts +1 -1
  75. package/dist/hooks/use-file-drop.d.ts +10 -1
  76. package/dist/hooks/use-overlay-history.js +16 -0
  77. package/dist/hooks/use-overlay-history.js.map +1 -1
  78. package/dist/i18n/defaults.d.ts +10 -1
  79. package/dist/i18n/defaults.js +11 -1
  80. package/dist/i18n/defaults.js.map +1 -1
  81. package/dist/i18n/german.d.ts +78 -0
  82. package/dist/i18n/{locales/de.js → german.js} +59 -9
  83. package/dist/i18n/german.js.map +1 -0
  84. package/dist/i18n/kit-labels.d.ts +10 -1
  85. package/dist/i18n/kit-labels.js +5 -0
  86. package/dist/i18n/kit-labels.js.map +1 -1
  87. package/dist/i18n/locales/de-CH.d.ts +23 -8
  88. package/dist/i18n/locales/de-CH.js +7 -3
  89. package/dist/i18n/locales/de-CH.js.map +1 -1
  90. package/dist/i18n/locales/en.d.ts +10 -1
  91. package/dist/i18n/locales/es.d.ts +10 -1
  92. package/dist/i18n/locales/es.js +51 -1
  93. package/dist/i18n/locales/es.js.map +1 -1
  94. package/dist/i18n/locales/fr.d.ts +10 -1
  95. package/dist/i18n/locales/fr.js +51 -1
  96. package/dist/i18n/locales/fr.js.map +1 -1
  97. package/dist/i18n/locales/hu.d.ts +10 -1
  98. package/dist/i18n/locales/hu.js +54 -1
  99. package/dist/i18n/locales/hu.js.map +1 -1
  100. package/dist/i18n/locales/it.d.ts +10 -1
  101. package/dist/i18n/locales/it.js +51 -1
  102. package/dist/i18n/locales/it.js.map +1 -1
  103. package/dist/i18n/locales/zh.d.ts +10 -1
  104. package/dist/i18n/locales/zh.js +50 -1
  105. package/dist/i18n/locales/zh.js.map +1 -1
  106. package/dist/i18n/swiss.d.ts +3 -3
  107. package/dist/i18n/swiss.js.map +1 -1
  108. package/dist/index.d.ts +9 -4
  109. package/dist/index.js +33 -1
  110. package/dist/index.js.map +1 -1
  111. package/dist/{kit-labels-C4uB_zmg.d.ts → kit-labels-CPbsjlXr.d.ts} +229 -9
  112. package/dist/lib/server-wake.d.ts +130 -0
  113. package/dist/lib/server-wake.js +121 -0
  114. package/dist/lib/server-wake.js.map +1 -0
  115. package/dist/rhf/fields.d.ts +10 -1
  116. package/dist/rhf/form.d.ts +10 -1
  117. package/dist/rhf.d.ts +10 -1
  118. package/dist/search/global-search.d.ts +3 -1
  119. package/dist/search/global-search.js.map +1 -1
  120. package/dist/shell/app-shell.d.ts +10 -1
  121. package/dist/shell/top-bar-brand.d.ts +10 -1
  122. package/dist/shell/topbar-action-menu.d.ts +2 -0
  123. package/dist/shell.d.ts +10 -1
  124. package/dist/wizard/stepper-nav.d.ts +10 -1
  125. package/dist/wizard.d.ts +10 -1
  126. package/package.json +1 -1
  127. package/src/components/account-chips.tsx +302 -0
  128. package/src/components/account-settings-labels.ts +16 -2
  129. package/src/components/chip.tsx +239 -6
  130. package/src/components/confirm-dialog.tsx +82 -7
  131. package/src/components/danger-confirm.tsx +118 -33
  132. package/src/components/data-table.tsx +20 -0
  133. package/src/components/form-actions.tsx +22 -3
  134. package/src/components/number-field.tsx +32 -26
  135. package/src/components/passkeys-setting.tsx +68 -7
  136. package/src/components/progress-bar.tsx +21 -1
  137. package/src/components/reauth-dialog.tsx +273 -0
  138. package/src/components/server-wake.tsx +124 -0
  139. package/src/components/share-card.tsx +615 -0
  140. package/src/components/toast.tsx +72 -2
  141. package/src/components/toggle-group.tsx +5 -2
  142. package/src/components/ui.tsx +89 -32
  143. package/src/components/write-lock.tsx +96 -0
  144. package/src/hooks/use-overlay-history.ts +51 -0
  145. package/src/i18n/defaults.ts +10 -0
  146. package/src/i18n/{locales/de.ts → german.ts} +68 -13
  147. package/src/i18n/kit-labels.tsx +22 -2
  148. package/src/i18n/locales/de-CH.ts +17 -8
  149. package/src/i18n/locales/es.ts +51 -0
  150. package/src/i18n/locales/fr.ts +52 -0
  151. package/src/i18n/locales/hu.ts +55 -0
  152. package/src/i18n/locales/it.ts +51 -0
  153. package/src/i18n/locales/zh.ts +50 -0
  154. package/src/i18n/swiss.ts +3 -3
  155. package/src/index.ts +23 -1
  156. package/src/lib/server-wake.ts +270 -0
  157. package/src/search/global-search.tsx +3 -1
  158. package/dist/i18n/locales/de-CH-informal.d.ts +0 -61
  159. package/dist/i18n/locales/de-CH-informal.js +0 -8
  160. package/dist/i18n/locales/de-CH-informal.js.map +0 -1
  161. package/dist/i18n/locales/de-informal.d.ts +0 -82
  162. package/dist/i18n/locales/de-informal.js +0 -37
  163. package/dist/i18n/locales/de-informal.js.map +0 -1
  164. package/dist/i18n/locales/de.d.ts +0 -66
  165. package/dist/i18n/locales/de.js.map +0 -1
  166. package/src/i18n/locales/de-CH-informal.ts +0 -10
  167. package/src/i18n/locales/de-informal.ts +0 -61
@@ -2,11 +2,13 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
4
4
  import { useKitLabels } from "../i18n/kit-labels.js";
5
+ import { typedMatches } from "./danger-confirm.js";
5
6
  import { DialogFrame } from "./dialog-frame.js";
6
- import { Button } from "./ui.js";
7
+ import { Button, Input } from "./ui.js";
7
8
  const DEFAULT_CONFIRM_DIALOG_LABELS = {
8
9
  confirm: "Confirm",
9
- cancel: "Cancel"
10
+ cancel: "Cancel",
11
+ typed: (text) => `Type \u201C${text}\u201D to confirm`
10
12
  };
11
13
  const ConfirmContext = createContext(null);
12
14
  function useConfirm() {
@@ -58,9 +60,14 @@ function ConfirmDialogView({
58
60
  const confirmRef = useRef(null);
59
61
  const cancelRef = useRef(null);
60
62
  const accepted = useRef(false);
63
+ const typedRef = useRef(null);
64
+ const [typed, setTyped] = useState("");
65
+ const target = request.requireTyped;
66
+ const matches = target === void 0 || typedMatches(typed, target, request.typedMatch ?? "caseless");
61
67
  useEffect(() => {
62
- (tone === "danger" ? cancelRef : confirmRef).current?.focus();
63
- }, [tone]);
68
+ (target !== void 0 ? typedRef : tone === "danger" ? cancelRef : confirmRef).current?.focus();
69
+ }, [tone, target]);
70
+ const typedLabel = target === void 0 ? "" : request.typedLabel ?? (typeof labels.typed === "string" ? labels.typed : labels.typed(target));
64
71
  return /* @__PURE__ */ jsx(
65
72
  DialogFrame,
66
73
  {
@@ -77,6 +84,7 @@ function ConfirmDialogView({
77
84
  ref: confirmRef,
78
85
  type: "button",
79
86
  variant: tone === "danger" ? "danger" : "primary",
87
+ disabled: !matches,
80
88
  onClick: () => {
81
89
  accepted.current = true;
82
90
  close();
@@ -84,7 +92,30 @@ function ConfirmDialogView({
84
92
  children: request.confirmLabel ?? labels.confirm
85
93
  }
86
94
  )
87
- ] })
95
+ ] }),
96
+ children: target !== void 0 && /* @__PURE__ */ jsx(
97
+ "form",
98
+ {
99
+ noValidate: true,
100
+ onSubmit: (e) => {
101
+ e.preventDefault();
102
+ confirmRef.current?.click();
103
+ },
104
+ children: /* @__PURE__ */ jsx(
105
+ Input,
106
+ {
107
+ ref: typedRef,
108
+ label: typedLabel,
109
+ value: typed,
110
+ autoComplete: "off",
111
+ autoCapitalize: "off",
112
+ autoCorrect: "off",
113
+ spellCheck: false,
114
+ onChange: (e) => setTyped(e.target.value)
115
+ }
116
+ )
117
+ }
118
+ )
88
119
  }
89
120
  );
90
121
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/confirm-dialog.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport { Button } from \"./ui\";\n\n/**\n * The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback\n * text. A call that knows what it is confirming says so per call (`confirmLabel:\n * \"Delete budget\"`), which is what the dialog is for — these are for the rest.\n */\nexport interface ConfirmDialogLabels {\n confirm: string;\n cancel: string;\n}\n\nexport const DEFAULT_CONFIRM_DIALOG_LABELS: ConfirmDialogLabels = {\n confirm: \"Confirm\",\n cancel: \"Cancel\",\n};\n\nexport type ConfirmTone = \"danger\" | \"warning\" | \"neutral\";\n\nexport interface ConfirmOptions {\n /** The question, and the dialog's accessible name. */\n title: ReactNode;\n /** The smaller line under it — the consequence. Wired to `aria-describedby`. */\n body?: ReactNode;\n /** Default: `confirmDialog.confirm` from the provider, else \"Confirm\". */\n confirmLabel?: ReactNode;\n /** Default: `confirmDialog.cancel` from the provider, else \"Cancel\". */\n cancelLabel?: ReactNode;\n /**\n * `\"danger\"` for what cannot be undone (a delete): the confirm is the destructive\n * button and focus starts on CANCEL. `\"warning\"` and `\"neutral\"` (default) start on\n * the confirm. See {@link ConfirmProvider} for why the two differ.\n */\n tone?: ConfirmTone;\n}\n\nexport type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;\n\nconst ConfirmContext = createContext<ConfirmFn | null>(null);\n\n/**\n * `const confirm = useConfirm()` → `if (!(await confirm({ title }))) return`.\n *\n * The returned function is STABLE for the life of the provider, so it can sit in an\n * effect's or a `useCallback`'s dependency list without re-running it.\n *\n * Throws outside a {@link ConfirmProvider}, rather than falling back to\n * `window.confirm`: a silent fallback is exactly the native dialog the three apps are\n * migrating away from, and it would look like the migration had worked.\n */\nexport function useConfirm(): ConfirmFn {\n const confirm = useContext(ConfirmContext);\n if (!confirm) {\n throw new Error(\"useConfirm() must be called below a <ConfirmProvider>.\");\n }\n return confirm;\n}\n\ninterface Request extends ConfirmOptions {\n id: number;\n resolve: (answer: boolean) => void;\n}\n\nexport interface ConfirmProviderProps {\n children: ReactNode;\n}\n\n/**\n * The host for {@link useConfirm}: ONE confirm dialog for the whole app, rendered on\n * {@link DialogFrame}, answering a promise.\n *\n * Asked for by all three apps at once — keksdose has 20+ `window.confirm` calls,\n * kastlan 7, lenkbank 1. The native dialog cannot be translated past its OK/Cancel\n * (those are the BROWSER's language, not the app's), cannot say which button is the\n * destructive one, and on an installed PWA it prints the origin over the question.\n * A promise is what lets each of those call sites change by one `await`:\n *\n * if (!window.confirm(t(\"budget.delete_confirm\"))) return;\n * // becomes\n * if (!(await confirm({ title: t(\"budget.delete_confirm\"), tone: \"danger\" }))) return;\n *\n * ## Its own provider, not part of `UiKitProvider`\n *\n * `UiKitProvider` renders nothing: it is strings and a locale, it is optional, and it\n * NESTS (an inner one re-labels one table). A dialog host inside it would mount a host\n * per nested provider, and a component under the inner one would open a second dialog\n * layer of its own. So the host is its own component, mounted once — inside the\n * `UiKitProvider` whose strings it should speak, which is also the order the labels\n * need.\n *\n * ## Focus\n *\n * On CANCEL for `tone: \"danger\"`, on CONFIRM otherwise. A confirm is often answered by\n * a reflexive Enter — the key that pressed the Delete button in the first place may\n * still be down — and for an action that cannot be undone that reflex must land on the\n * harmless answer; `DangerConfirm` makes the same call (\"never to the destructive\n * button itself\"). For everything else the confirm is the expected answer and Enter\n * accepting it is what `window.confirm` taught users, which the migrating call sites'\n * users already rely on. Focus returns to whatever opened it, through `Modal`'s trap.\n *\n * ## Escape, the backdrop, Back and Cancel all answer `false`\n *\n * Only the confirm button answers `true`. Anything that merely dismisses is a no.\n *\n * ## A second `confirm` while one is open QUEUES\n *\n * It is shown when the first is answered, and each call gets its own answer. The\n * alternative — answering the open one `false` to make room — would answer a question\n * the user never answered: a background prompt (\"your session is about to expire\")\n * would cancel the delete they were reading. `false` has to mean \"the user said no\".\n * A double-click cannot queue two of the same: the first click opens the modal\n * synchronously (a discrete event's update is flushed before the next one), so the\n * second lands on the open dialog rather than on the button — at worst on its\n * backdrop, which answers `false`, the safe way to be wrong.\n *\n * Unmounting the provider answers everything still pending `false`, so no caller's\n * `await` hangs forever.\n *\n * ## keksdose's synchronous call (transaction-swipe-plan.ts)\n *\n * `buildTxSwipeActions` is synchronous, but its `window.confirm` sits inside the\n * action THUNK (`confirmed(key, run)` returns `() => { if (confirm(…)) run() }`), and a\n * thunk may be async without its caller knowing. So the plan builder stays\n * synchronous; it takes the confirm as an argument and the thunk awaits it:\n *\n * // mobile-transaction-list.tsx\n * const confirm = useConfirm();\n * buildTxSwipeActions({ tx, …, confirm });\n *\n * // transaction-swipe-plan.ts\n * const confirmed = (messageKey: string, run: () => void) => () => {\n * void confirm({ title: t(messageKey), tone: \"danger\" }).then((ok) => ok && run());\n * };\n *\n * Its test then passes `confirm: vi.fn(async () => false)` instead of stubbing the\n * global, and asserts after an `await` (the mutation now runs a microtask later). The\n * row snaps back when the swipe commits, before the answer — which is right: the\n * dialog, not the half-swiped row, is where the question now is.\n */\nexport function ConfirmProvider({ children }: ConfirmProviderProps) {\n const [queue, setQueue] = useState<Request[]>([]);\n const nextId = useRef(0);\n // Every unanswered request, including the ones not yet in state — the unmount\n // cleanup below must reach a request made in the same tick it unmounts.\n const pending = useRef(new Set<Request>());\n\n const confirm = useCallback<ConfirmFn>(\n (options) =>\n new Promise<boolean>((resolve) => {\n const request: Request = { ...options, id: ++nextId.current, resolve };\n pending.current.add(request);\n setQueue((q) => [...q, request]);\n }),\n [],\n );\n\n useEffect(() => {\n const open = pending.current;\n return () => {\n for (const r of open) r.resolve(false);\n open.clear();\n };\n }, []);\n\n const answer = useCallback((request: Request, value: boolean) => {\n if (!pending.current.delete(request)) return;\n request.resolve(value);\n setQueue((q) => q.filter((r) => r !== request));\n }, []);\n\n const current = queue[0];\n return (\n <ConfirmContext.Provider value={confirm}>\n {children}\n {current && (\n // Keyed per request, so a queued one gets a fresh Modal: the first one's trap\n // hands focus back to the page, and the next one's captures that as its own\n // restore target instead of a button that has just been unmounted.\n <ConfirmDialogView key={current.id} request={current} onAnswer={answer} />\n )}\n </ConfirmContext.Provider>\n );\n}\n\nfunction ConfirmDialogView({\n request,\n onAnswer,\n}: {\n request: Request;\n onAnswer: (request: Request, value: boolean) => void;\n}) {\n const labels = useKitLabels(\"confirmDialog\", DEFAULT_CONFIRM_DIALOG_LABELS);\n const tone = request.tone ?? \"neutral\";\n const confirmRef = useRef<HTMLButtonElement>(null);\n const cancelRef = useRef<HTMLButtonElement>(null);\n // Set by the confirm button, read once the panel has finished lowering. Everything\n // else that ends in `onClose` — Escape, the backdrop, Back, Cancel — leaves it false.\n const accepted = useRef(false);\n\n // In THIS component's effect, which runs after `Modal`'s own (a parent's effects run\n // after its children's). `Modal` engages its focus trap on the panel first — and\n // records the page's focused element as the restore target — and only then does\n // focus move to the button. Focusing it from inside the panel would run first and\n // make the button the restore target.\n useEffect(() => {\n (tone === \"danger\" ? cancelRef : confirmRef).current?.focus();\n }, [tone]);\n\n return (\n <DialogFrame\n title={request.title}\n description={request.body}\n onClose={() => onAnswer(request, accepted.current)}\n // A confirmation interrupts to ask a question that must be answered.\n role=\"alertdialog\"\n data-confirm-tone={tone}\n actions={(close) => (\n <>\n <Button ref={cancelRef} type=\"button\" variant=\"ghost\" onClick={close}>\n {request.cancelLabel ?? labels.cancel}\n </Button>\n <Button\n ref={confirmRef}\n type=\"button\"\n variant={tone === \"danger\" ? \"danger\" : \"primary\"}\n onClick={() => {\n accepted.current = true;\n close();\n }}\n >\n {request.confirmLabel ?? labels.confirm}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":";AAiLI,SA6CI,UAvCA,KANJ;AAjLJ,SAAS,eAAe,aAAa,YAAY,WAAW,QAAQ,gBAAgB;AAGpF,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AAYhB,MAAM,gCAAqD;AAAA,EAChE,SAAS;AAAA,EACT,QAAQ;AACV;AAuBA,MAAM,iBAAiB,cAAgC,IAAI;AAYpD,SAAS,aAAwB;AACtC,QAAM,UAAU,WAAW,cAAc;AACzC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AACA,SAAO;AACT;AAmFO,SAAS,gBAAgB,EAAE,SAAS,GAAyB;AAClE,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAoB,CAAC,CAAC;AAChD,QAAM,SAAS,OAAO,CAAC;AAGvB,QAAM,UAAU,OAAO,oBAAI,IAAa,CAAC;AAEzC,QAAM,UAAU;AAAA,IACd,CAAC,YACC,IAAI,QAAiB,CAAC,YAAY;AAChC,YAAM,UAAmB,EAAE,GAAG,SAAS,IAAI,EAAE,OAAO,SAAS,QAAQ;AACrE,cAAQ,QAAQ,IAAI,OAAO;AAC3B,eAAS,CAAC,MAAM,CAAC,GAAG,GAAG,OAAO,CAAC;AAAA,IACjC,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAEA,YAAU,MAAM;AACd,UAAM,OAAO,QAAQ;AACrB,WAAO,MAAM;AACX,iBAAW,KAAK,KAAM,GAAE,QAAQ,KAAK;AACrC,WAAK,MAAM;AAAA,IACb;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,SAAS,YAAY,CAAC,SAAkB,UAAmB;AAC/D,QAAI,CAAC,QAAQ,QAAQ,OAAO,OAAO,EAAG;AACtC,YAAQ,QAAQ,KAAK;AACrB,aAAS,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,MAAM,OAAO,CAAC;AAAA,EAChD,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM,CAAC;AACvB,SACE,qBAAC,eAAe,UAAf,EAAwB,OAAO,SAC7B;AAAA;AAAA,IACA;AAAA;AAAA;AAAA,IAIC,oBAAC,qBAAmC,SAAS,SAAS,UAAU,UAAxC,QAAQ,EAAwC;AAAA,KAE5E;AAEJ;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AACF,GAGG;AACD,QAAM,SAAS,aAAa,iBAAiB,6BAA6B;AAC1E,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,aAAa,OAA0B,IAAI;AACjD,QAAM,YAAY,OAA0B,IAAI;AAGhD,QAAM,WAAW,OAAO,KAAK;AAO7B,YAAU,MAAM;AACd,KAAC,SAAS,WAAW,YAAY,YAAY,SAAS,MAAM;AAAA,EAC9D,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,QAAQ;AAAA,MACf,aAAa,QAAQ;AAAA,MACrB,SAAS,MAAM,SAAS,SAAS,SAAS,OAAO;AAAA,MAEjD,MAAK;AAAA,MACL,qBAAmB;AAAA,MACnB,SAAS,CAAC,UACR,iCACE;AAAA,4BAAC,UAAO,KAAK,WAAW,MAAK,UAAS,SAAQ,SAAQ,SAAS,OAC5D,kBAAQ,eAAe,OAAO,QACjC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,SAAS,SAAS,WAAW,WAAW;AAAA,YACxC,SAAS,MAAM;AACb,uBAAS,UAAU;AACnB,oBAAM;AAAA,YACR;AAAA,YAEC,kBAAQ,gBAAgB,OAAO;AAAA;AAAA,QAClC;AAAA,SACF;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/confirm-dialog.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { typedMatches } from \"./danger-confirm\";\nimport type { TypedMatch } from \"./danger-confirm\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport { Button, Input } from \"./ui\";\n\n/**\n * The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback\n * text. A call that knows what it is confirming says so per call (`confirmLabel:\n * \"Delete budget\"`), which is what the dialog is for — these are for the rest.\n */\nexport interface ConfirmDialogLabels {\n confirm: string;\n cancel: string;\n /**\n * 0.18: label of the field a `requireTyped` confirm shows. A FUNCTION of the text to\n * type by default — where the address sits in the sentence moves with the language —\n * or a finished STRING. A call's own `typedLabel` wins over it.\n *\n * Required, like every key of a namespace: the shipped catalogues\n * (`@eifi1/ui-kit/i18n/<code>`) translate it, and an app's own full catalogue should\n * hear about the new string from the compiler rather than show it in English.\n */\n typed: string | ((text: string) => string);\n}\n\n/** `satisfies` rather than a type annotation, so `.typed` stays known-callable for a\n * caller composing its own label from it. */\nexport const DEFAULT_CONFIRM_DIALOG_LABELS = {\n confirm: \"Confirm\",\n cancel: \"Cancel\",\n typed: (text: string) => `Type “${text}” to confirm`,\n} satisfies ConfirmDialogLabels;\n\nexport type ConfirmTone = \"danger\" | \"warning\" | \"neutral\";\n\nexport interface ConfirmOptions {\n /** The question, and the dialog's accessible name. */\n title: ReactNode;\n /** The smaller line under it — the consequence. Wired to `aria-describedby`. */\n body?: ReactNode;\n /** Default: `confirmDialog.confirm` from the provider, else \"Confirm\". */\n confirmLabel?: ReactNode;\n /** Default: `confirmDialog.cancel` from the provider, else \"Cancel\". */\n cancelLabel?: ReactNode;\n /**\n * `\"danger\"` for what cannot be undone (a delete): the confirm is the destructive\n * button and focus starts on CANCEL. `\"warning\"` and `\"neutral\"` (default) start on\n * the confirm. See {@link ConfirmProvider} for why the two differ.\n */\n tone?: ConfirmTone;\n /**\n * 0.18: make the user TYPE this before confirm is enabled — the target account's\n * e-mail address, a budget's name. For the action whose risk is not \"did you mean\n * it\" but \"is this the right row\": an admin who deactivates somebody has already\n * proven who THEY are (they passed the admin gate); typing the address is what\n * proves they read which account they are on. Both keksdose (`TYPE_EMAIL`) and\n * Kurvenschmiede (`TypedConfirm`) built this beside `useConfirm` because it had no\n * field.\n *\n * The dialog then shows one text field under the body and focuses it (whatever the\n * tone: a reflexive Enter cannot confirm a field that does not match yet). Confirm\n * stays disabled until the text matches; Enter in the field confirms once it does.\n * The answer is still `true` / `false` — the text matched what you passed, so send\n * that to the server if it asks for the address too, not the user's spelling.\n */\n requireTyped?: string;\n /** Label of that field. Default: `confirmDialog.typed` — `Type “a@b.c” to confirm`. */\n typedLabel?: string;\n /**\n * How the typed text is compared. Default `\"caseless\"`: surrounding spaces and case\n * ignored, the rule both apps applied to an address. `\"trim\"` keeps case, `\"exact\"`\n * keeps spaces too — for a phrase like \"DELETE\" whose contract is \"exactly this\".\n */\n typedMatch?: TypedMatch;\n}\n\nexport type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;\n\nconst ConfirmContext = createContext<ConfirmFn | null>(null);\n\n/**\n * `const confirm = useConfirm()` → `if (!(await confirm({ title }))) return`.\n *\n * The returned function is STABLE for the life of the provider, so it can sit in an\n * effect's or a `useCallback`'s dependency list without re-running it.\n *\n * Throws outside a {@link ConfirmProvider}, rather than falling back to\n * `window.confirm`: a silent fallback is exactly the native dialog the three apps are\n * migrating away from, and it would look like the migration had worked.\n */\nexport function useConfirm(): ConfirmFn {\n const confirm = useContext(ConfirmContext);\n if (!confirm) {\n throw new Error(\"useConfirm() must be called below a <ConfirmProvider>.\");\n }\n return confirm;\n}\n\ninterface Request extends ConfirmOptions {\n id: number;\n resolve: (answer: boolean) => void;\n}\n\nexport interface ConfirmProviderProps {\n children: ReactNode;\n}\n\n/**\n * The host for {@link useConfirm}: ONE confirm dialog for the whole app, rendered on\n * {@link DialogFrame}, answering a promise.\n *\n * Asked for by all three apps at once — keksdose has 20+ `window.confirm` calls,\n * kastlan 7, lenkbank 1. The native dialog cannot be translated past its OK/Cancel\n * (those are the BROWSER's language, not the app's), cannot say which button is the\n * destructive one, and on an installed PWA it prints the origin over the question.\n * A promise is what lets each of those call sites change by one `await`:\n *\n * if (!window.confirm(t(\"budget.delete_confirm\"))) return;\n * // becomes\n * if (!(await confirm({ title: t(\"budget.delete_confirm\"), tone: \"danger\" }))) return;\n *\n * ## Its own provider, not part of `UiKitProvider`\n *\n * `UiKitProvider` renders nothing: it is strings and a locale, it is optional, and it\n * NESTS (an inner one re-labels one table). A dialog host inside it would mount a host\n * per nested provider, and a component under the inner one would open a second dialog\n * layer of its own. So the host is its own component, mounted once — inside the\n * `UiKitProvider` whose strings it should speak, which is also the order the labels\n * need.\n *\n * ## Focus\n *\n * On CANCEL for `tone: \"danger\"`, on CONFIRM otherwise. A confirm is often answered by\n * a reflexive Enter — the key that pressed the Delete button in the first place may\n * still be down — and for an action that cannot be undone that reflex must land on the\n * harmless answer; `DangerConfirm` makes the same call (\"never to the destructive\n * button itself\"). For everything else the confirm is the expected answer and Enter\n * accepting it is what `window.confirm` taught users, which the migrating call sites'\n * users already rely on. Focus returns to whatever opened it, through `Modal`'s trap.\n *\n * ## Escape, the backdrop, Back and Cancel all answer `false`\n *\n * Only the confirm button answers `true`. Anything that merely dismisses is a no.\n *\n * ## A second `confirm` while one is open QUEUES\n *\n * It is shown when the first is answered, and each call gets its own answer. The\n * alternative — answering the open one `false` to make room — would answer a question\n * the user never answered: a background prompt (\"your session is about to expire\")\n * would cancel the delete they were reading. `false` has to mean \"the user said no\".\n * A double-click cannot queue two of the same: the first click opens the modal\n * synchronously (a discrete event's update is flushed before the next one), so the\n * second lands on the open dialog rather than on the button — at worst on its\n * backdrop, which answers `false`, the safe way to be wrong.\n *\n * Unmounting the provider answers everything still pending `false`, so no caller's\n * `await` hangs forever.\n *\n * ## keksdose's synchronous call (transaction-swipe-plan.ts)\n *\n * `buildTxSwipeActions` is synchronous, but its `window.confirm` sits inside the\n * action THUNK (`confirmed(key, run)` returns `() => { if (confirm(…)) run() }`), and a\n * thunk may be async without its caller knowing. So the plan builder stays\n * synchronous; it takes the confirm as an argument and the thunk awaits it:\n *\n * // mobile-transaction-list.tsx\n * const confirm = useConfirm();\n * buildTxSwipeActions({ tx, …, confirm });\n *\n * // transaction-swipe-plan.ts\n * const confirmed = (messageKey: string, run: () => void) => () => {\n * void confirm({ title: t(messageKey), tone: \"danger\" }).then((ok) => ok && run());\n * };\n *\n * Its test then passes `confirm: vi.fn(async () => false)` instead of stubbing the\n * global, and asserts after an `await` (the mutation now runs a microtask later). The\n * row snaps back when the swipe commits, before the answer — which is right: the\n * dialog, not the half-swiped row, is where the question now is.\n */\nexport function ConfirmProvider({ children }: ConfirmProviderProps) {\n const [queue, setQueue] = useState<Request[]>([]);\n const nextId = useRef(0);\n // Every unanswered request, including the ones not yet in state — the unmount\n // cleanup below must reach a request made in the same tick it unmounts.\n const pending = useRef(new Set<Request>());\n\n const confirm = useCallback<ConfirmFn>(\n (options) =>\n new Promise<boolean>((resolve) => {\n const request: Request = { ...options, id: ++nextId.current, resolve };\n pending.current.add(request);\n setQueue((q) => [...q, request]);\n }),\n [],\n );\n\n useEffect(() => {\n const open = pending.current;\n return () => {\n for (const r of open) r.resolve(false);\n open.clear();\n };\n }, []);\n\n const answer = useCallback((request: Request, value: boolean) => {\n if (!pending.current.delete(request)) return;\n request.resolve(value);\n setQueue((q) => q.filter((r) => r !== request));\n }, []);\n\n const current = queue[0];\n return (\n <ConfirmContext.Provider value={confirm}>\n {children}\n {current && (\n // Keyed per request, so a queued one gets a fresh Modal: the first one's trap\n // hands focus back to the page, and the next one's captures that as its own\n // restore target instead of a button that has just been unmounted.\n <ConfirmDialogView key={current.id} request={current} onAnswer={answer} />\n )}\n </ConfirmContext.Provider>\n );\n}\n\nfunction ConfirmDialogView({\n request,\n onAnswer,\n}: {\n request: Request;\n onAnswer: (request: Request, value: boolean) => void;\n}) {\n const labels = useKitLabels(\"confirmDialog\", DEFAULT_CONFIRM_DIALOG_LABELS);\n const tone = request.tone ?? \"neutral\";\n const confirmRef = useRef<HTMLButtonElement>(null);\n const cancelRef = useRef<HTMLButtonElement>(null);\n // Set by the confirm button, read once the panel has finished lowering. Everything\n // else that ends in `onClose` — Escape, the backdrop, Back, Cancel — leaves it false.\n const accepted = useRef(false);\n const typedRef = useRef<HTMLInputElement>(null);\n const [typed, setTyped] = useState(\"\");\n const target = request.requireTyped;\n const matches = target === undefined || typedMatches(typed, target, request.typedMatch ?? \"caseless\");\n\n // In THIS component's effect, which runs after `Modal`'s own (a parent's effects run\n // after its children's). `Modal` engages its focus trap on the panel first — and\n // records the page's focused element as the restore target — and only then does\n // focus move to the button. Focusing it from inside the panel would run first and\n // make the button the restore target. A typed confirm starts in its field: typing is\n // the one thing the dialog is waiting for.\n useEffect(() => {\n (target !== undefined ? typedRef : tone === \"danger\" ? cancelRef : confirmRef).current?.focus();\n }, [tone, target]);\n\n const typedLabel =\n target === undefined\n ? \"\"\n : (request.typedLabel ??\n (typeof labels.typed === \"string\" ? labels.typed : labels.typed(target)));\n\n return (\n <DialogFrame\n title={request.title}\n description={request.body}\n onClose={() => onAnswer(request, accepted.current)}\n // A confirmation interrupts to ask a question that must be answered.\n role=\"alertdialog\"\n data-confirm-tone={tone}\n actions={(close) => (\n <>\n <Button ref={cancelRef} type=\"button\" variant=\"ghost\" onClick={close}>\n {request.cancelLabel ?? labels.cancel}\n </Button>\n <Button\n ref={confirmRef}\n type=\"button\"\n variant={tone === \"danger\" ? \"danger\" : \"primary\"}\n disabled={!matches}\n onClick={() => {\n accepted.current = true;\n close();\n }}\n >\n {request.confirmLabel ?? labels.confirm}\n </Button>\n </>\n )}\n >\n {/* The field, when there is one, is the body; a plain confirm keeps having none. */}\n {target !== undefined && (\n <form\n noValidate\n onSubmit={(e) => {\n e.preventDefault();\n // Through the button, so Enter and a click are one path — and a disabled\n // (not yet matching) confirm ignores the click.\n confirmRef.current?.click();\n }}\n >\n <Input\n ref={typedRef}\n label={typedLabel}\n value={typed}\n autoComplete=\"off\"\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n onChange={(e) => setTyped(e.target.value)}\n />\n </form>\n )}\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  import 'react';
2
- export { D as CopyButton, E as CopyButtonLabels, F as CopyButtonProps, L as DEFAULT_COPY_BUTTON_LABELS } from '../kit-labels-C4uB_zmg.js';
2
+ export { D as CopyButton, E as CopyButtonLabels, F as CopyButtonProps, L as DEFAULT_COPY_BUTTON_LABELS } from '../kit-labels-CPbsjlXr.js';
3
3
  import './tooltip.js';
4
4
  import './data-table-labels.js';
5
5
  import './mini-calendar.js';
@@ -50,3 +50,12 @@ import './authed-image.js';
50
50
  import '../hooks/use-authed-src.js';
51
51
  import './image-grid.js';
52
52
  import './lightbox.js';
53
+ import './write-lock.js';
54
+ import './account-chips.js';
55
+ import '../lib/format.js';
56
+ import '../data-table-3mi2R3so.js';
57
+ import './data-table-sort.js';
58
+ import './swipeable-row.js';
59
+ import './reauth-dialog.js';
60
+ import './server-wake.js';
61
+ import '../lib/server-wake.js';
@@ -1,6 +1,55 @@
1
1
  import * as react from 'react';
2
- import { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ import { Ref, ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
 
4
+ /**
5
+ * How typed text is compared with the text it has to repeat — shared by
6
+ * `DangerConfirm`'s `phraseMatch` and `useConfirm`'s `typedMatch`.
7
+ *
8
+ * - `"trim"` ignores spaces around the typed text — a phone keyboard's autocomplete
9
+ * likes to add one. Case counts: "delete" is not "DELETE".
10
+ * - `"exact"` compares character for character, spaces included.
11
+ * - `"caseless"` trims AND ignores case: for an e-mail address, whose domain is
12
+ * case-insensitive and whose local part every real mail server treats so. Both apps'
13
+ * "type the account's address" rules (keksdose's `TYPE_EMAIL`, Kurvenschmiede's
14
+ * `TypedConfirm`) compared `trim().toLowerCase()`, and a capital the user's keyboard
15
+ * put at the start must not make the right row look like the wrong one.
16
+ */
17
+ type TypedMatch = "trim" | "exact" | "caseless";
18
+ /** Whether `typed` repeats `target` under `mode` — see {@link TypedMatch}. Exported for
19
+ * a caller that draws its own field (a dialog that also picks a recipient) and wants
20
+ * the same rule as the kit's. */
21
+ declare function typedMatches(typed: string, target: string, mode?: TypedMatch): boolean;
22
+ /**
23
+ * Busy-while-a-promise-runs, for an action that MAY return one: `run(result, onResolved)`
24
+ * marks the caller pending until `result` settles, calls `onResolved` if it fulfilled,
25
+ * and only clears the flag if it rejected — the caller shows why and the user retries.
26
+ * Nothing runs after unmount. Shared by `DangerConfirm` and `ReauthDialog`, whose
27
+ * contract is the same: resolve = done, reject = stay put.
28
+ *
29
+ * @internal
30
+ */
31
+ declare function usePromisePending(): {
32
+ pending: boolean;
33
+ run: (result: unknown, onResolved: () => void) => boolean;
34
+ };
35
+ /**
36
+ * The "prove it is you" field: a password input the browser's password manager fills
37
+ * with the CURRENT password (`autocomplete="current-password"`), never offers to
38
+ * generate a new one for. `readOnly` rather than `disabled` while busy: disabling the
39
+ * field that has focus (Enter was pressed in it) drops focus to <body>.
40
+ *
41
+ * @internal DangerConfirm's field. ReauthDialog spells it out with the field's own
42
+ * `error`, which keeps the `<input>` mounted while its message comes and goes.
43
+ */
44
+ declare function CurrentPasswordInput({ inputRef, label, value, onValueChange, busy, invalid, describedBy, }: {
45
+ inputRef?: Ref<HTMLInputElement>;
46
+ label: string;
47
+ value: string;
48
+ onValueChange: (value: string) => void;
49
+ busy: boolean;
50
+ invalid?: boolean;
51
+ describedBy?: string;
52
+ }): react.JSX.Element;
4
53
  /**
5
54
  * Every string the tile renders — the `dangerConfirm` namespace of
6
55
  * `<UiKitProvider labels>`, overridable per instance through `labels`. The arm and
@@ -59,9 +108,11 @@ interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">, "onCh
59
108
  * How the typed text is compared with `phrase`. `"trim"` (default) ignores spaces
60
109
  * around it — a phone keyboard's autocomplete likes to add one. `"exact"` compares
61
110
  * character for character, spaces included, for an app whose contract is "type
62
- * exactly this".
111
+ * exactly this". `"caseless"` (0.18) also ignores case — for a phrase that is an
112
+ * e-mail address (keksdose's "type the user's address" admin rule). See
113
+ * {@link TypedMatch}.
63
114
  */
64
- phraseMatch?: "trim" | "exact";
115
+ phraseMatch?: TypedMatch;
65
116
  /** The warning above the fields. Defaults to `labels.prompt`. */
66
117
  prompt?: ReactNode;
67
118
  /** `"danger"` (default) for what cannot be undone; `"warning"` for what can, at a
@@ -112,4 +163,4 @@ interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">, "onCh
112
163
  */
113
164
  declare function DangerConfirm({ onConfirm, requirePassword, phrase, phraseMatch, prompt, tone, armLabel, confirmLabel, busy: busyProp, disabled, lockedReason, armed: armedProp, onArmedChange, labels: labelsProp, className, ...rest }: DangerConfirmProps): react.JSX.Element;
114
165
 
115
- export { DEFAULT_DANGER_CONFIRM_LABELS, DangerConfirm, type DangerConfirmLabels, type DangerConfirmProps };
166
+ export { CurrentPasswordInput, DEFAULT_DANGER_CONFIRM_LABELS, DangerConfirm, type DangerConfirmLabels, type DangerConfirmProps, type TypedMatch, typedMatches, usePromisePending };
@@ -1,10 +1,65 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
4
4
  import { cn } from "../lib/cn.js";
5
5
  import { useKitLabels } from "../i18n/kit-labels.js";
6
6
  import { Button, Input, Label, Spinner } from "./ui.js";
7
7
  import { Tooltip } from "./tooltip.js";
8
+ function typedMatches(typed, target, mode = "trim") {
9
+ if (mode === "exact") return typed === target;
10
+ if (mode === "trim") return typed.trim() === target;
11
+ return typed.trim().toLowerCase() === target.trim().toLowerCase();
12
+ }
13
+ function usePromisePending() {
14
+ const [pending, setPending] = useState(false);
15
+ const mounted = useRef(true);
16
+ useEffect(() => {
17
+ mounted.current = true;
18
+ return () => {
19
+ mounted.current = false;
20
+ };
21
+ }, []);
22
+ const run = useCallback((result, onResolved) => {
23
+ if (!result || typeof result.then !== "function") return false;
24
+ setPending(true);
25
+ result.then(
26
+ () => {
27
+ if (!mounted.current) return;
28
+ setPending(false);
29
+ onResolved();
30
+ },
31
+ () => {
32
+ if (mounted.current) setPending(false);
33
+ }
34
+ );
35
+ return true;
36
+ }, []);
37
+ return { pending, run };
38
+ }
39
+ function CurrentPasswordInput({
40
+ inputRef,
41
+ label,
42
+ value,
43
+ onValueChange,
44
+ busy,
45
+ invalid,
46
+ describedBy
47
+ }) {
48
+ return /* @__PURE__ */ jsx(
49
+ Input,
50
+ {
51
+ ref: inputRef,
52
+ type: "password",
53
+ autoComplete: "current-password",
54
+ label,
55
+ value,
56
+ readOnly: busy,
57
+ invalid,
58
+ "aria-describedby": describedBy,
59
+ onChange: (e) => onValueChange(e.target.value)
60
+ }
61
+ );
62
+ }
8
63
  const DEFAULT_DANGER_CONFIRM_LABELS = {
9
64
  arm: "Delete\u2026",
10
65
  confirm: "Delete",
@@ -36,7 +91,7 @@ function DangerConfirm({
36
91
  const armed = armedProp ?? armedState;
37
92
  const [password, setPassword] = useState("");
38
93
  const [typed, setTyped] = useState("");
39
- const [pending, setPending] = useState(false);
94
+ const { pending, run } = usePromisePending();
40
95
  const busy = Boolean(busyProp) || pending;
41
96
  const promptId = useId();
42
97
  const reasonId = useId();
@@ -45,13 +100,6 @@ function DangerConfirm({
45
100
  const cancelId = useId();
46
101
  const firstFieldRef = useRef(null);
47
102
  const moveFocus = useRef(false);
48
- const mounted = useRef(true);
49
- useEffect(() => {
50
- mounted.current = true;
51
- return () => {
52
- mounted.current = false;
53
- };
54
- }, []);
55
103
  const [wasArmed, setWasArmed] = useState(armed);
56
104
  if (wasArmed !== armed) {
57
105
  setWasArmed(armed);
@@ -81,24 +129,12 @@ function DangerConfirm({
81
129
  };
82
130
  const locked = lockedReason !== void 0 && lockedReason !== null && lockedReason !== false && lockedReason !== "";
83
131
  const passwordOk = !requirePassword || password !== "";
84
- const phraseOk = phrase === void 0 || (phraseMatch === "exact" ? typed : typed.trim()) === phrase;
132
+ const phraseOk = phrase === void 0 || typedMatches(typed, phrase, phraseMatch);
85
133
  const canConfirm = passwordOk && phraseOk && !busy;
86
134
  const submit = (e) => {
87
135
  e.preventDefault();
88
136
  if (!canConfirm) return;
89
- const result = onConfirm(requirePassword ? password : void 0);
90
- if (!result || typeof result.then !== "function") return;
91
- setPending(true);
92
- result.then(
93
- () => {
94
- if (!mounted.current) return;
95
- setPending(false);
96
- setArmed(false);
97
- },
98
- () => {
99
- if (mounted.current) setPending(false);
100
- }
101
- );
137
+ run(onConfirm(requirePassword ? password : void 0), () => setArmed(false));
102
138
  };
103
139
  const phrasePlaceholder = phrase === void 0 || labels.phrasePlaceholder === void 0 ? void 0 : typeof labels.phrasePlaceholder === "function" ? labels.phrasePlaceholder(phrase) : labels.phrasePlaceholder;
104
140
  const phraseLabel = phrase === void 0 ? "" : typeof labels.phrase === "function" ? labels.phrase(phrase) : labels.phrase;
@@ -162,15 +198,13 @@ function DangerConfirm({
162
198
  ] })
163
199
  ) : /* @__PURE__ */ jsx(Input, { label: phraseLabel, ...phraseFieldProps })),
164
200
  requirePassword && /* @__PURE__ */ jsx(
165
- Input,
201
+ CurrentPasswordInput,
166
202
  {
167
- ref: phrase === void 0 ? firstFieldRef : void 0,
168
- type: "password",
169
- autoComplete: "current-password",
203
+ inputRef: phrase === void 0 ? firstFieldRef : void 0,
170
204
  label: labels.password,
171
205
  value: password,
172
- readOnly: busy,
173
- onChange: (e) => setPassword(e.target.value)
206
+ busy,
207
+ onValueChange: setPassword
174
208
  }
175
209
  ),
176
210
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
@@ -194,7 +228,10 @@ function DangerConfirm({
194
228
  ) });
195
229
  }
196
230
  export {
231
+ CurrentPasswordInput,
197
232
  DEFAULT_DANGER_CONFIRM_LABELS,
198
- DangerConfirm
233
+ DangerConfirm,
234
+ typedMatches,
235
+ usePromisePending
199
236
  };
200
237
  //# sourceMappingURL=danger-confirm.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/danger-confirm.tsx"],"sourcesContent":["import { 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 { Tooltip } from \"./tooltip\";\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\".\n */\n phraseMatch?: \"trim\" | \"exact\";\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, setPending] = useState(false);\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 const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\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 || (phraseMatch === \"exact\" ? typed : typed.trim()) === phrase;\n const canConfirm = passwordOk && phraseOk && !busy;\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n if (!canConfirm) return;\n const result = onConfirm(requirePassword ? password : undefined);\n if (!result || typeof (result as Promise<unknown>).then !== \"function\") return;\n setPending(true);\n (result as Promise<unknown>).then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n setArmed(false);\n },\n () => {\n // Stays armed, fields kept: the caller shows why, the user retries.\n if (mounted.current) setPending(false);\n },\n );\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 <Input\n // The phrase field takes the ref when both are there — it comes first.\n ref={phrase === undefined ? firstFieldRef : undefined}\n type=\"password\"\n autoComplete=\"current-password\"\n label={labels.password}\n value={password}\n readOnly={busy}\n onChange={(e) => setPassword(e.target.value)}\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 { 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,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { DataTableLabels } from './data-table-labels.js';
4
- import { b as DataTableColumn } from '../data-table-CpkWUJBY.js';
4
+ import { b as DataTableColumn } from '../data-table-3mi2R3so.js';
5
5
  import './data-table-sort.js';
6
6
  import './swipeable-row.js';
7
7
 
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { b as DataTableColumn, f as FilterValue } from '../data-table-CpkWUJBY.js';
2
+ import { b as DataTableColumn, f as FilterValue } from '../data-table-3mi2R3so.js';
3
3
  import { DataTableLabels } from './data-table-labels.js';
4
4
  import './data-table-sort.js';
5
5
  import './swipeable-row.js';
@@ -1,4 +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-CpkWUJBY.js';
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';
2
2
  import 'react';
3
3
  import './data-table-sort.js';
4
4
  import './swipeable-row.js';
@@ -1,5 +1,5 @@
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-CpkWUJBY.js';
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';
3
3
  export { SortState } from './data-table-sort.js';
4
4
  import './swipeable-row.js';
5
5
  import './data-table-labels.js';
@@ -181,6 +181,7 @@ function DataTable({
181
181
  locale: localeProp,
182
182
  storageKeyPrefix = DEFAULT_PERSIST_PREFIX,
183
183
  mobileExpandAsDialog = false,
184
+ mobileDialogBackCloses = true,
184
185
  mobileGroupBy,
185
186
  mobileGroupLabel,
186
187
  mobileCard,
@@ -839,6 +840,7 @@ function DataTable({
839
840
  onClose: closeDialog,
840
841
  closeLabel: labels.close,
841
842
  header: mobileDialogRow ? mobilePrimaryCol?.cell(mobileDialogRow) : null,
843
+ backCloses: mobileDialogBackCloses,
842
844
  children: mobileDialogContent
843
845
  }
844
846
  )