@eifi1/ui-kit 0.29.0 → 0.31.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 (307) hide show
  1. package/README.md +65 -6
  2. package/dist/account/account-parts.d.ts +21 -0
  3. package/dist/account/account-parts.js +40 -0
  4. package/dist/account/account-parts.js.map +1 -0
  5. package/dist/account/data-export-setting.d.ts +73 -0
  6. package/dist/account/data-export-setting.js +114 -0
  7. package/dist/account/data-export-setting.js.map +1 -0
  8. package/dist/account/delete-account-setting.d.ts +95 -0
  9. package/dist/account/delete-account-setting.js +200 -0
  10. package/dist/account/delete-account-setting.js.map +1 -0
  11. package/dist/account/email-change-setting.d.ts +136 -0
  12. package/dist/account/email-change-setting.js +278 -0
  13. package/dist/account/email-change-setting.js.map +1 -0
  14. package/dist/account/sessions-setting.d.ts +110 -0
  15. package/dist/account/sessions-setting.js +147 -0
  16. package/dist/account/sessions-setting.js.map +1 -0
  17. package/dist/admin/admin-action-confirm.d.ts +95 -0
  18. package/dist/admin/admin-action-confirm.js +276 -0
  19. package/dist/admin/admin-action-confirm.js.map +1 -0
  20. package/dist/admin/admin-action-log.d.ts +105 -0
  21. package/dist/admin/admin-action-log.js +153 -0
  22. package/dist/admin/admin-action-log.js.map +1 -0
  23. package/dist/admin/admin-parts.d.ts +40 -0
  24. package/dist/admin/admin-parts.js +45 -0
  25. package/dist/admin/admin-parts.js.map +1 -0
  26. package/dist/admin/invitations-panel.d.ts +95 -0
  27. package/dist/admin/invitations-panel.js +290 -0
  28. package/dist/admin/invitations-panel.js.map +1 -0
  29. package/dist/admin/reviewer-scope-editor.d.ts +95 -0
  30. package/dist/admin/reviewer-scope-editor.js +134 -0
  31. package/dist/admin/reviewer-scope-editor.js.map +1 -0
  32. package/dist/admin/role-select.d.ts +95 -0
  33. package/dist/admin/role-select.js +128 -0
  34. package/dist/admin/role-select.js.map +1 -0
  35. package/dist/admin/transfer-ownership-dialog.d.ts +95 -0
  36. package/dist/admin/transfer-ownership-dialog.js +83 -0
  37. package/dist/admin/transfer-ownership-dialog.js.map +1 -0
  38. package/dist/admin/user-roster.d.ts +95 -0
  39. package/dist/admin/user-roster.js +285 -0
  40. package/dist/admin/user-roster.js.map +1 -0
  41. package/dist/auth/auth-errors.d.ts +103 -6
  42. package/dist/auth/auth-errors.js +68 -2
  43. package/dist/auth/auth-errors.js.map +1 -1
  44. package/dist/auth/forgot-password-form.d.ts +10 -3
  45. package/dist/auth/forgot-password-form.js +6 -3
  46. package/dist/auth/forgot-password-form.js.map +1 -1
  47. package/dist/auth/register-form.d.ts +21 -12
  48. package/dist/auth/register-form.js +6 -4
  49. package/dist/auth/register-form.js.map +1 -1
  50. package/dist/auth/reset-password-form.d.ts +10 -3
  51. package/dist/auth/reset-password-form.js +6 -3
  52. package/dist/auth/reset-password-form.js.map +1 -1
  53. package/dist/auth/sign-in-form.d.ts +75 -9
  54. package/dist/auth/sign-in-form.js +105 -15
  55. package/dist/auth/sign-in-form.js.map +1 -1
  56. package/dist/auth/verify-email.d.ts +7 -2
  57. package/dist/auth/verify-email.js +44 -8
  58. package/dist/auth/verify-email.js.map +1 -1
  59. package/dist/chart.d.ts +20 -11
  60. package/dist/components/account-chips.d.ts +47 -7
  61. package/dist/components/account-chips.js +19 -3
  62. package/dist/components/account-chips.js.map +1 -1
  63. package/dist/components/account-settings-labels.d.ts +24 -1
  64. package/dist/components/account-settings-labels.js +6 -2
  65. package/dist/components/account-settings-labels.js.map +1 -1
  66. package/dist/components/account-settings.d.ts +69 -8
  67. package/dist/components/account-settings.js +133 -25
  68. package/dist/components/account-settings.js.map +1 -1
  69. package/dist/components/amount-input.d.ts +21 -12
  70. package/dist/components/button-group.d.ts +21 -12
  71. package/dist/components/calculator.d.ts +21 -12
  72. package/dist/components/confirm-dialog.d.ts +21 -12
  73. package/dist/components/copy-button.d.ts +18 -9
  74. package/dist/components/danger-confirm.d.ts +21 -12
  75. package/dist/components/data-table.js +8 -1
  76. package/dist/components/data-table.js.map +1 -1
  77. package/dist/components/date-picker.js +9 -4
  78. package/dist/components/date-picker.js.map +1 -1
  79. package/dist/components/facing-pair.d.ts +21 -12
  80. package/dist/components/file-button.d.ts +21 -12
  81. package/dist/components/file-dropzone.d.ts +21 -12
  82. package/dist/components/form-actions.d.ts +21 -12
  83. package/dist/components/iban-input.d.ts +21 -12
  84. package/dist/components/language-select.d.ts +21 -12
  85. package/dist/components/legal-page.d.ts +21 -12
  86. package/dist/components/legal.d.ts +21 -12
  87. package/dist/components/money-field.d.ts +21 -12
  88. package/dist/components/number-field.d.ts +21 -12
  89. package/dist/components/number-input.d.ts +21 -12
  90. package/dist/components/numpad-sheet.d.ts +21 -12
  91. package/dist/components/passkeys-setting.js +2 -1
  92. package/dist/components/passkeys-setting.js.map +1 -1
  93. package/dist/components/phone-input.d.ts +21 -12
  94. package/dist/components/series-chart.d.ts +21 -12
  95. package/dist/components/settings-fields.d.ts +55 -13
  96. package/dist/components/settings-fields.js +20 -1
  97. package/dist/components/settings-fields.js.map +1 -1
  98. package/dist/components/share-card.d.ts +20 -11
  99. package/dist/components/text-link.d.ts +21 -12
  100. package/dist/components/time-input.d.ts +21 -12
  101. package/dist/components/ui.d.ts +17 -8
  102. package/dist/{register-form-V1OeNlMr.d.ts → delete-account-setting-DCFS5Vof.d.ts} +6612 -5612
  103. package/dist/demo/demo-banner.d.ts +51 -0
  104. package/dist/demo/demo-banner.js +97 -0
  105. package/dist/demo/demo-banner.js.map +1 -0
  106. package/dist/demo/demo-countdown.d.ts +58 -0
  107. package/dist/demo/demo-countdown.js +79 -0
  108. package/dist/demo/demo-countdown.js.map +1 -0
  109. package/dist/demo/demo-ended.d.ts +45 -0
  110. package/dist/demo/demo-ended.js +42 -0
  111. package/dist/demo/demo-ended.js.map +1 -0
  112. package/dist/demo/demo-labels.d.ts +53 -0
  113. package/dist/demo/demo-labels.js +29 -0
  114. package/dist/demo/demo-labels.js.map +1 -0
  115. package/dist/demo/demo-session.d.ts +30 -0
  116. package/dist/demo/demo-session.js +8 -0
  117. package/dist/demo/demo-session.js.map +1 -0
  118. package/dist/demo/demo-start.d.ts +54 -0
  119. package/dist/demo/demo-start.js +68 -0
  120. package/dist/demo/demo-start.js.map +1 -0
  121. package/dist/feedback/feedback-attachment.d.ts +21 -12
  122. package/dist/feedback/feedback-context.d.ts +21 -12
  123. package/dist/feedback/feedback-crash.d.ts +21 -12
  124. package/dist/feedback/feedback-dialog.d.ts +21 -12
  125. package/dist/feedback/feedback-inbox.d.ts +21 -12
  126. package/dist/feedback/feedback-inbox.js +12 -2
  127. package/dist/feedback/feedback-inbox.js.map +1 -1
  128. package/dist/feedback/feedback-labels.d.ts +21 -12
  129. package/dist/feedback/feedback-labels.js +1 -0
  130. package/dist/feedback/feedback-labels.js.map +1 -1
  131. package/dist/feedback/feedback-menu.d.ts +21 -12
  132. package/dist/feedback/feedback-record.d.ts +21 -12
  133. package/dist/feedback/feedback-record.js +2 -0
  134. package/dist/feedback/feedback-record.js.map +1 -1
  135. package/dist/feedback/feedback-row-detail.d.ts +21 -12
  136. package/dist/feedback/feedback-row-detail.js +7 -0
  137. package/dist/feedback/feedback-row-detail.js.map +1 -1
  138. package/dist/feedback/feedback-status-undo.d.ts +21 -12
  139. package/dist/feedback/feedback-submit.d.ts +21 -12
  140. package/dist/feedback/feedback-swipe.d.ts +22 -13
  141. package/dist/feedback/feedback-swipe.js.map +1 -1
  142. package/dist/feedback/feedback-table.d.ts +18 -9
  143. package/dist/feedback/feedback-thread.d.ts +21 -12
  144. package/dist/feedback.d.ts +21 -12
  145. package/dist/hooks/use-anchored-panel.d.ts +6 -2
  146. package/dist/hooks/use-anchored-panel.js +2 -1
  147. package/dist/hooks/use-anchored-panel.js.map +1 -1
  148. package/dist/hooks/use-file-drop.d.ts +21 -12
  149. package/dist/i18n/defaults.d.ts +21 -12
  150. package/dist/i18n/defaults.js +28 -0
  151. package/dist/i18n/defaults.js.map +1 -1
  152. package/dist/i18n/german.d.ts +21 -12
  153. package/dist/i18n/german.js +298 -12
  154. package/dist/i18n/german.js.map +1 -1
  155. package/dist/i18n/kit-labels.d.ts +14 -5
  156. package/dist/i18n/kit-labels.js.map +1 -1
  157. package/dist/i18n/languages.d.ts +15 -6
  158. package/dist/i18n/locales/de-CH.d.ts +21 -12
  159. package/dist/i18n/locales/en.d.ts +21 -12
  160. package/dist/i18n/locales/es.d.ts +21 -12
  161. package/dist/i18n/locales/es.js +287 -10
  162. package/dist/i18n/locales/es.js.map +1 -1
  163. package/dist/i18n/locales/fr.d.ts +21 -12
  164. package/dist/i18n/locales/fr.js +290 -7
  165. package/dist/i18n/locales/fr.js.map +1 -1
  166. package/dist/i18n/locales/hu.d.ts +21 -12
  167. package/dist/i18n/locales/hu.js +288 -12
  168. package/dist/i18n/locales/hu.js.map +1 -1
  169. package/dist/i18n/locales/it.d.ts +21 -12
  170. package/dist/i18n/locales/it.js +289 -7
  171. package/dist/i18n/locales/it.js.map +1 -1
  172. package/dist/i18n/locales/zh.d.ts +21 -12
  173. package/dist/i18n/locales/zh.js +279 -7
  174. package/dist/i18n/locales/zh.js.map +1 -1
  175. package/dist/i18n/review.d.ts +21 -12
  176. package/dist/i18n/review.js +50 -1
  177. package/dist/i18n/review.js.map +1 -1
  178. package/dist/index.d.ts +31 -4
  179. package/dist/index.js +148 -0
  180. package/dist/index.js.map +1 -1
  181. package/dist/landing/access.d.ts +48 -0
  182. package/dist/landing/access.js +38 -0
  183. package/dist/landing/access.js.map +1 -0
  184. package/dist/landing/landing-actions.d.ts +138 -0
  185. package/dist/landing/landing-actions.js +39 -0
  186. package/dist/landing/landing-actions.js.map +1 -0
  187. package/dist/landing/landing-labels.d.ts +43 -0
  188. package/dist/landing/landing-labels.js +23 -0
  189. package/dist/landing/landing-labels.js.map +1 -0
  190. package/dist/landing/landing-sections.d.ts +218 -0
  191. package/dist/landing/landing-sections.js +167 -0
  192. package/dist/landing/landing-sections.js.map +1 -0
  193. package/dist/landing/page-seo.d.ts +99 -0
  194. package/dist/landing/page-seo.js +121 -0
  195. package/dist/landing/page-seo.js.map +1 -0
  196. package/dist/landing/public-header.d.ts +148 -0
  197. package/dist/landing/public-header.js +49 -0
  198. package/dist/landing/public-header.js.map +1 -0
  199. package/dist/landing/routing.d.ts +218 -0
  200. package/dist/landing/routing.js +81 -0
  201. package/dist/landing/routing.js.map +1 -0
  202. package/dist/rhf/fields.d.ts +21 -12
  203. package/dist/rhf/form.d.ts +21 -12
  204. package/dist/rhf.d.ts +21 -12
  205. package/dist/settings/settings-catalogue.d.ts +84 -0
  206. package/dist/settings/settings-catalogue.js +47 -0
  207. package/dist/settings/settings-catalogue.js.map +1 -0
  208. package/dist/settings/settings-context.d.ts +31 -0
  209. package/dist/settings/settings-context.js +11 -0
  210. package/dist/settings/settings-context.js.map +1 -0
  211. package/dist/settings/settings-heading.d.ts +50 -0
  212. package/dist/settings/settings-heading.js +27 -0
  213. package/dist/settings/settings-heading.js.map +1 -0
  214. package/dist/settings/settings-labels.d.ts +41 -0
  215. package/dist/settings/settings-labels.js +38 -0
  216. package/dist/settings/settings-labels.js.map +1 -0
  217. package/dist/settings/settings-layout.d.ts +77 -0
  218. package/dist/settings/settings-layout.js +242 -0
  219. package/dist/settings/settings-layout.js.map +1 -0
  220. package/dist/settings/settings-section.d.ts +133 -0
  221. package/dist/settings/settings-section.js +63 -0
  222. package/dist/settings/settings-section.js.map +1 -0
  223. package/dist/settings/use-account-language.d.ts +83 -0
  224. package/dist/settings/use-account-language.js +62 -0
  225. package/dist/settings/use-account-language.js.map +1 -0
  226. package/dist/settings/use-settings-focus.d.ts +34 -0
  227. package/dist/settings/use-settings-focus.js +77 -0
  228. package/dist/settings/use-settings-focus.js.map +1 -0
  229. package/dist/settings/use-settings-route.d.ts +118 -0
  230. package/dist/settings/use-settings-route.js +120 -0
  231. package/dist/settings/use-settings-route.js.map +1 -0
  232. package/dist/shell/app-shell.d.ts +27 -12
  233. package/dist/shell/app-shell.js +3 -2
  234. package/dist/shell/app-shell.js.map +1 -1
  235. package/dist/shell/top-bar-brand.d.ts +21 -12
  236. package/dist/shell.d.ts +25 -6
  237. package/dist/shell.js +56 -0
  238. package/dist/shell.js.map +1 -1
  239. package/dist/wizard/stepper-nav.d.ts +21 -12
  240. package/dist/wizard.d.ts +21 -12
  241. package/package.json +2 -1
  242. package/src/account/account-parts.ts +67 -0
  243. package/src/account/data-export-setting.tsx +219 -0
  244. package/src/account/delete-account-setting.tsx +363 -0
  245. package/src/account/email-change-setting.tsx +454 -0
  246. package/src/account/sessions-setting.tsx +284 -0
  247. package/src/admin/admin-action-confirm.tsx +495 -0
  248. package/src/admin/admin-action-log.tsx +297 -0
  249. package/src/admin/admin-parts.ts +82 -0
  250. package/src/admin/invitations-panel.tsx +523 -0
  251. package/src/admin/reviewer-scope-editor.tsx +234 -0
  252. package/src/admin/role-select.tsx +286 -0
  253. package/src/admin/transfer-ownership-dialog.tsx +172 -0
  254. package/src/admin/user-roster.tsx +573 -0
  255. package/src/auth/auth-errors.ts +196 -8
  256. package/src/auth/forgot-password-form.tsx +21 -5
  257. package/src/auth/register-form.tsx +24 -6
  258. package/src/auth/reset-password-form.tsx +19 -5
  259. package/src/auth/sign-in-form.tsx +241 -38
  260. package/src/auth/verify-email.tsx +59 -8
  261. package/src/components/account-chips.tsx +64 -7
  262. package/src/components/account-settings-labels.ts +28 -1
  263. package/src/components/account-settings.tsx +231 -34
  264. package/src/components/data-table.tsx +8 -0
  265. package/src/components/date-picker.tsx +9 -4
  266. package/src/components/passkeys-setting.tsx +2 -1
  267. package/src/components/settings-fields.tsx +50 -1
  268. package/src/demo/demo-banner.tsx +159 -0
  269. package/src/demo/demo-countdown.ts +141 -0
  270. package/src/demo/demo-ended.tsx +89 -0
  271. package/src/demo/demo-labels.ts +78 -0
  272. package/src/demo/demo-session.ts +32 -0
  273. package/src/demo/demo-start.tsx +150 -0
  274. package/src/feedback/feedback-inbox.tsx +21 -8
  275. package/src/feedback/feedback-labels.ts +2 -1
  276. package/src/feedback/feedback-record.ts +10 -7
  277. package/src/feedback/feedback-row-detail.tsx +28 -4
  278. package/src/feedback/feedback-swipe.tsx +1 -1
  279. package/src/hooks/use-anchored-panel.ts +14 -3
  280. package/src/i18n/defaults.ts +28 -0
  281. package/src/i18n/german.ts +327 -5
  282. package/src/i18n/kit-labels.tsx +42 -0
  283. package/src/i18n/locales/es.ts +313 -3
  284. package/src/i18n/locales/fr.ts +316 -0
  285. package/src/i18n/locales/hu.ts +308 -5
  286. package/src/i18n/locales/it.ts +313 -0
  287. package/src/i18n/locales/zh.ts +286 -0
  288. package/src/i18n/review.ts +49 -0
  289. package/src/index.ts +184 -0
  290. package/src/landing/access.ts +97 -0
  291. package/src/landing/landing-actions.tsx +92 -0
  292. package/src/landing/landing-labels.ts +59 -0
  293. package/src/landing/landing-sections.tsx +337 -0
  294. package/src/landing/page-seo.ts +251 -0
  295. package/src/landing/public-header.tsx +120 -0
  296. package/src/landing/routing.tsx +209 -0
  297. package/src/settings/settings-catalogue.ts +132 -0
  298. package/src/settings/settings-context.ts +33 -0
  299. package/src/settings/settings-heading.tsx +65 -0
  300. package/src/settings/settings-labels.ts +75 -0
  301. package/src/settings/settings-layout.tsx +379 -0
  302. package/src/settings/settings-section.tsx +108 -0
  303. package/src/settings/use-account-language.ts +146 -0
  304. package/src/settings/use-settings-focus.ts +120 -0
  305. package/src/settings/use-settings-route.ts +258 -0
  306. package/src/shell/app-shell.tsx +10 -2
  307. package/src/shell.ts +45 -0
@@ -0,0 +1,495 @@
1
+ import { Children, useContext, useEffect, useId, useRef, useState } from "react";
2
+ import type { KeyboardEvent, ReactNode } from "react";
3
+ import { useKitLabels } from "../i18n/kit-labels";
4
+ import { AlertBanner } from "../components/alert-banner";
5
+ import { Checkbox } from "../components/checkbox";
6
+ import { DEFAULT_DANGER_CONFIRM_LABELS, TypedConfirmField, typedMatches } from "../components/danger-confirm";
7
+ import type { DangerConsequence } from "../components/danger-confirm";
8
+ import { DialogFrame } from "../components/dialog-frame";
9
+ import { ModalCloseContext } from "../components/modal";
10
+ import { Button } from "../components/ui";
11
+ import { authErrorCode } from "../auth/auth-errors";
12
+ import type { KitErrorCode } from "../auth/auth-errors";
13
+ import { hasMessage, usePersonLabel } from "./admin-parts";
14
+ import type { AdminPerson, MaybePromise } from "./admin-parts";
15
+
16
+ /**
17
+ * The confirmation in front of an admin action, at the level the SERVER states
18
+ * (docs/user-admin-harmonization.md §4.2):
19
+ *
20
+ * | level | what it asks | actions |
21
+ * |---|---|---|
22
+ * | `none` | nothing: it confirms at once and renders nothing | resend a mail, reactivate, the reviewer scope… |
23
+ * | `acknowledge` | a tick under the consequence | force a password change, change a role |
24
+ * | `type_email` | the target's address, typed | deactivate, erase now, transfer |
25
+ *
26
+ * keksdose decides the level per action and per account on the server
27
+ * (`admin_user_actions_service`) and Kurvenschmiede asks for the typed address on its
28
+ * two hard-to-undo actions; both drew the dialog by hand beside the kit's `DangerConfirm`
29
+ * and `useConfirm`. The level is a PROP, never derived here: the server enforces it and
30
+ * a hand-written request never sees this dialog, so a page that guessed its own level
31
+ * could only disagree with the server. The comparison (caseless), the field and the tick
32
+ * are `DangerConfirm`'s (`TypedConfirmField`, the `dangerConfirm` words), so the three
33
+ * confirmations in the kit cannot disagree about what counts as the address.
34
+ *
35
+ * WHY TYPE THE TARGET'S ADDRESS and not the admin's password: the admin proved who they
36
+ * are at the admin gate. What is unproven is which ROW they are on (keksdose's
37
+ * `TYPE_EMAIL` rule, Kurvenschmiede's `TypedConfirm`).
38
+ *
39
+ * THE REFUSALS (§4.1, §9.6) are read by code — `authErrorCode`, which knows the account
40
+ * codes beside the sign-in ones — and stay in the dialog, which stays open so the admin
41
+ * can read them: `last_admin`, `self_action`, `confirmation_mismatch`, and kastlan's
42
+ * `other_companies` — with "Remove from company" offered in its place when the app passes
43
+ * `onRemoveFromCompany`. Anything else is `describeError`'s words, else "That didn't work".
44
+ * At level `none` nothing is drawn while the action runs; a refusal opens a dialog of its
45
+ * own to say it.
46
+ *
47
+ * It never sends a request: `onConfirm` does, with what the guards were answered with.
48
+ */
49
+
50
+ /** How much confirmation an action needs — the server's `confirmation_level(action)`. */
51
+ export type AdminConfirmLevel = "none" | "acknowledge" | "type_email";
52
+
53
+ /** What {@link AdminActionConfirmProps.onConfirm} receives — the guards' answers, for
54
+ * the request body (the server re-checks both). */
55
+ export interface AdminActionConfirmValues {
56
+ /** `type_email`: the address as typed — trimmed, its case the admin's; the body's
57
+ * `confirm_email`, which the server compares with the account's (server-kit's
58
+ * `confirm_email_matches`). */
59
+ confirmEmail?: string;
60
+ /** `acknowledge` and `type_email`: the action was confirmed past a guard — the body's
61
+ * `acknowledged`, which every admin action carries (server-kit 0.4). */
62
+ acknowledged?: true;
63
+ }
64
+
65
+ /** The account the action is about. */
66
+ export type AdminActionTarget = AdminPerson & { email: string };
67
+
68
+ /** The `adminAction` namespace. The tick, the typed field and their held-button reasons
69
+ * are the `dangerConfirm` namespace's, shared with `DangerConfirm`. */
70
+ export interface AdminActionLabels {
71
+ /** The confirm button when the action names none. */
72
+ confirm: string;
73
+ cancel: string;
74
+ /** The refusal dialog's only button (level `none`). */
75
+ close: string;
76
+ /** `409 last_admin`. */
77
+ lastAdmin: string;
78
+ /** `409 self_action`: the admin's own account. */
79
+ self: string;
80
+ /** `confirmation_mismatch`: the typed address is not this account's — the page and
81
+ * the server disagree about which row this is. */
82
+ confirmationMismatch: string;
83
+ /** kastlan's `409 other_companies` (§9.6). */
84
+ otherCompanies: string;
85
+ /** The alternative offered with it. */
86
+ removeFromCompany: string;
87
+ /** Any other failure, unless `describeError` says better. */
88
+ failed: string;
89
+ }
90
+
91
+ export const DEFAULT_ADMIN_ACTION_LABELS: AdminActionLabels = {
92
+ confirm: "Confirm",
93
+ cancel: "Cancel",
94
+ close: "Close",
95
+ lastAdmin: "This is the last active administrator. Make someone else an administrator first.",
96
+ self: "You can’t do this to your own account.",
97
+ confirmationMismatch: "The address doesn’t match this account. Check which account you are on.",
98
+ otherCompanies:
99
+ "This account also belongs to other companies, so only the platform operator can do this. You can remove it from this company instead.",
100
+ removeFromCompany: "Remove from company",
101
+ failed: "That didn’t work. Please try again.",
102
+ };
103
+
104
+ export interface AdminActionConfirmProps {
105
+ /** The level the server states for this action on this account (§4.2). */
106
+ level: AdminConfirmLevel;
107
+ /** Whom it is about: the address (typed at `type_email`) and the name parts. */
108
+ target: AdminActionTarget;
109
+ /** The confirm button — "Deactivate". Also the title when no `title` is given. */
110
+ confirmLabel?: ReactNode;
111
+ /** The dialog's heading — "Deactivate Ada Example?". Default `confirmLabel`. */
112
+ title?: ReactNode;
113
+ /**
114
+ * What the action does to THIS account, said before it happens: a sentence, or lines
115
+ * (`DangerConsequence`, a `severe` one in the danger colour). Derive them from the
116
+ * row's facts rather than reciting the worst case (keksdose dev#488).
117
+ */
118
+ consequence?: ReactNode | readonly (string | DangerConsequence)[];
119
+ /** The tick's words at `acknowledge`. Default `dangerConfirm.acknowledge` — "I have
120
+ * read what this does and want to continue." */
121
+ acknowledgeLabel?: ReactNode;
122
+ /** `danger` (default at `type_email`): a red confirm. `warning` (default at
123
+ * `acknowledge`) and `neutral`: the primary confirm. */
124
+ tone?: "danger" | "warning" | "neutral";
125
+ /**
126
+ * Runs the action. Return a promise and the dialog manages itself: busy until it
127
+ * settles, closed (`onClose(true)`) when it resolves, open with the refusal when it
128
+ * rejects. Return nothing and the app owns the rest — `busy`, `error`, closing it.
129
+ */
130
+ onConfirm: (values: AdminActionConfirmValues) => MaybePromise;
131
+ /** The dialog has closed: `true` after the action (or the alternative) went through,
132
+ * `false` for Cancel, Escape, the backdrop or Back. */
133
+ onClose: (done: boolean) => void;
134
+ /** kastlan: offered with an `other_companies` refusal — `DELETE …/membership`,
135
+ * logged as `membership_remove` (§4.1). Promise handling as `onConfirm`. */
136
+ onRemoveFromCompany?: () => MaybePromise;
137
+ /** The app's words for a failure the kit has no code for. Asked first. */
138
+ describeError?: (error: unknown) => ReactNode | undefined;
139
+ /** A refusal the app caught itself, when `onConfirm` returns nothing. */
140
+ error?: unknown;
141
+ /** The action is running, for an app that tracks it itself. */
142
+ busy?: boolean;
143
+ /**
144
+ * Fields of the action's own, above the guard — the transfer's recipient, kastlan's
145
+ * roles (`RolesEditor`). Their values are the app's; hold the confirm with
146
+ * `confirmDisabledReason` until they are complete.
147
+ */
148
+ children?: ReactNode;
149
+ /** Why the confirm is held for a reason of the app's ("Choose who receives it").
150
+ * Named before the guard's own, since the fields are above it. */
151
+ confirmDisabledReason?: ReactNode;
152
+ /** Kept-mounted mode, as on `DialogFrame`; left out, open while mounted. */
153
+ open?: boolean;
154
+ /** The confirm COMMITS: under a locked `WriteLockProvider` it is held with the lock's
155
+ * reason. Off by default — an admin action is not a write to the open record. */
156
+ commit?: boolean;
157
+ labels?: Partial<AdminActionLabels>;
158
+ }
159
+
160
+ /** Whether a callback answered a promise. */
161
+ function isThenable(value: unknown): value is PromiseLike<unknown> {
162
+ return typeof value === "object" && value !== null && typeof (value as PromiseLike<unknown>).then === "function";
163
+ }
164
+
165
+ /** The refusal's words and code. */
166
+ function refusalOf(
167
+ error: unknown,
168
+ present: boolean,
169
+ describeError: AdminActionConfirmProps["describeError"],
170
+ labels: AdminActionLabels,
171
+ ): { text: ReactNode; code: KitErrorCode | undefined } | null {
172
+ if (!present) return null;
173
+ const code = authErrorCode(error);
174
+ const own = describeError?.(error);
175
+ if (hasMessage(own)) return { text: own, code };
176
+ const text =
177
+ code === "last_admin"
178
+ ? labels.lastAdmin
179
+ : code === "self_action"
180
+ ? labels.self
181
+ : code === "confirmation_mismatch"
182
+ ? labels.confirmationMismatch
183
+ : code === "other_companies"
184
+ ? labels.otherCompanies
185
+ : labels.failed;
186
+ return { text, code };
187
+ }
188
+
189
+ /** The selector for the first field among the caller's `children` — what opening focuses. */
190
+ const FIELD = 'input:not([type="hidden"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), [role="combobox"]';
191
+
192
+ export function AdminActionConfirm({
193
+ level,
194
+ target,
195
+ confirmLabel,
196
+ title,
197
+ consequence,
198
+ acknowledgeLabel,
199
+ tone: toneProp,
200
+ onConfirm,
201
+ onClose,
202
+ onRemoveFromCompany,
203
+ describeError,
204
+ error: errorProp,
205
+ busy: busyProp,
206
+ children,
207
+ confirmDisabledReason,
208
+ open,
209
+ commit,
210
+ labels: labelsProp,
211
+ }: AdminActionConfirmProps) {
212
+ const labels = useKitLabels("adminAction", DEFAULT_ADMIN_ACTION_LABELS, labelsProp);
213
+ const guardLabels = useKitLabels("dangerConfirm", DEFAULT_DANGER_CONFIRM_LABELS);
214
+ const nameOf = usePersonLabel();
215
+ const isOpen = open ?? true;
216
+ const [typed, setTyped] = useState("");
217
+ const [ticked, setTicked] = useState(false);
218
+ const [pending, setPending] = useState(false);
219
+ // The last rejection, boxed so a rejection with `undefined` still counts as one.
220
+ const [failure, setFailure] = useState<{ error: unknown } | null>(null);
221
+ // Resolved: the actions row lowers the panel, and `onClose` answers true.
222
+ const [done, setDone] = useState(false);
223
+ const confirmed = useRef(false);
224
+ const mounted = useRef(true);
225
+ const started = useRef(false);
226
+ const formId = useId();
227
+ const slotRef = useRef<HTMLDivElement>(null);
228
+ const guardRef = useRef<HTMLInputElement>(null);
229
+
230
+ useEffect(() => {
231
+ mounted.current = true;
232
+ return () => {
233
+ mounted.current = false;
234
+ };
235
+ }, []);
236
+
237
+ // A close (kept-mounted) wipes the guards and the refusal: a reopened dialog starts
238
+ // over. During render, like ReauthDialog's, so no frame shows the old answers.
239
+ const [wasOpen, setWasOpen] = useState(isOpen);
240
+ if (wasOpen !== isOpen) {
241
+ setWasOpen(isOpen);
242
+ if (!isOpen) {
243
+ setTyped("");
244
+ setTicked(false);
245
+ setFailure(null);
246
+ setDone(false);
247
+ }
248
+ }
249
+
250
+ const busy = Boolean(busyProp) || pending;
251
+
252
+ /** Runs `call`, managing busy, the refusal and the close — or not, when it answers
253
+ * no promise (the app owns it then). */
254
+ const perform = (call: () => MaybePromise) => {
255
+ let result: MaybePromise;
256
+ try {
257
+ result = call();
258
+ } catch (caught) {
259
+ setFailure({ error: caught });
260
+ return;
261
+ }
262
+ if (!isThenable(result)) return;
263
+ setFailure(null);
264
+ setPending(true);
265
+ result.then(
266
+ () => {
267
+ if (!mounted.current) return;
268
+ setPending(false);
269
+ confirmed.current = true;
270
+ setDone(true);
271
+ },
272
+ (caught: unknown) => {
273
+ if (!mounted.current) return;
274
+ setPending(false);
275
+ setFailure({ error: caught });
276
+ },
277
+ );
278
+ };
279
+
280
+ // Level `none`: confirmed the moment it opens, once per opening. Nothing is drawn
281
+ // while it runs; the promise's answer comes back asynchronously.
282
+ useEffect(() => {
283
+ if (level !== "none") return;
284
+ if (!isOpen) {
285
+ started.current = false;
286
+ return;
287
+ }
288
+ if (started.current) return;
289
+ started.current = true;
290
+ confirmed.current = false;
291
+ let result: unknown;
292
+ try {
293
+ result = onConfirm({});
294
+ } catch (caught) {
295
+ result = Promise.reject(caught);
296
+ }
297
+ // No promise: the app owns the rest (it closes the part, or passes `error`).
298
+ if (!isThenable(result)) return;
299
+ result.then(
300
+ () => {
301
+ if (mounted.current) onClose(true);
302
+ },
303
+ (caught: unknown) => {
304
+ if (mounted.current) setFailure({ error: caught });
305
+ },
306
+ );
307
+ }, [level, isOpen, onConfirm, onClose]);
308
+
309
+ // Focus the first thing the dialog waits for, after Modal's own effect has recorded
310
+ // the trigger to return to (a parent's effects run after its children's).
311
+ useEffect(() => {
312
+ if (!isOpen || level === "none") return;
313
+ const field = slotRef.current?.querySelector<HTMLElement>(FIELD) ?? guardRef.current;
314
+ field?.focus();
315
+ }, [isOpen, level]);
316
+
317
+ const hasRefusal = failure !== null || errorProp !== undefined;
318
+ const refusal = refusalOf(failure ? failure.error : errorProp, hasRefusal, describeError, labels);
319
+ const offerRemoval = refusal?.code === "other_companies" && onRemoveFromCompany !== undefined;
320
+
321
+ const name = nameOf(target);
322
+ const identity = (
323
+ <span data-private>{name && name !== target.email ? `${name} · ${target.email}` : target.email}</span>
324
+ );
325
+ const heading = title ?? confirmLabel ?? labels.confirm;
326
+
327
+ const refusalBanner = refusal && (
328
+ <AlertBanner
329
+ tone="danger"
330
+ size="sm"
331
+ action={
332
+ offerRemoval ? (
333
+ <Button
334
+ type="button"
335
+ size="sm"
336
+ variant="secondary"
337
+ disabled={busy}
338
+ onClick={() => perform(() => onRemoveFromCompany!())}
339
+ >
340
+ {labels.removeFromCompany}
341
+ </Button>
342
+ ) : undefined
343
+ }
344
+ >
345
+ {refusal.text}
346
+ </AlertBanner>
347
+ );
348
+
349
+ if (level === "none") {
350
+ // Nothing to ask: only a refusal is worth a dialog.
351
+ if (!isOpen || !refusal) return null;
352
+ return (
353
+ <DialogFrame
354
+ role="alertdialog"
355
+ title={heading}
356
+ description={identity}
357
+ onClose={() => onClose(confirmed.current)}
358
+ actions={(close) => (
359
+ <CloseOnDone done={done}>
360
+ <Button type="button" variant="secondary" disabled={busy} onClick={close}>
361
+ {labels.close}
362
+ </Button>
363
+ </CloseOnDone>
364
+ )}
365
+ >
366
+ {refusalBanner}
367
+ </DialogFrame>
368
+ );
369
+ }
370
+
371
+ const tone = toneProp ?? (level === "type_email" ? "danger" : "warning");
372
+ const askTyped = level === "type_email";
373
+ const typedOk = !askTyped || typedMatches(typed, target.email, "caseless");
374
+ const tickOk = askTyped || ticked;
375
+ const guardReason: ReactNode = hasMessage(confirmDisabledReason)
376
+ ? confirmDisabledReason
377
+ : !tickOk
378
+ ? guardLabels.needsAcknowledge
379
+ : !typedOk
380
+ ? typeof guardLabels.needsPhrase === "function"
381
+ ? guardLabels.needsPhrase(target.email)
382
+ : guardLabels.needsPhrase
383
+ : undefined;
384
+
385
+ const submit = () => {
386
+ if (busy || guardReason !== undefined) return;
387
+ perform(() =>
388
+ onConfirm(askTyped ? { confirmEmail: typed.trim(), acknowledged: true } : { acknowledged: true }),
389
+ );
390
+ };
391
+
392
+ const lines = Array.isArray(consequence)
393
+ ? (consequence as readonly (string | DangerConsequence)[]).map((item, index) =>
394
+ typeof item === "string"
395
+ ? { key: item, text: item as ReactNode, severe: false }
396
+ : { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) },
397
+ )
398
+ : null;
399
+
400
+ return (
401
+ <DialogFrame
402
+ open={open}
403
+ role="alertdialog"
404
+ title={heading}
405
+ description={identity}
406
+ onClose={() => onClose(confirmed.current)}
407
+ // A request that has left cannot be called back by closing the dialog.
408
+ onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {
409
+ if (busy && event.key === "Escape") event.preventDefault();
410
+ }}
411
+ aria-busy={busy || undefined}
412
+ actions={(close) => (
413
+ <CloseOnDone done={done}>
414
+ <Button type="button" variant="ghost" disabled={busy} onClick={close}>
415
+ {labels.cancel}
416
+ </Button>
417
+ <Button
418
+ type="submit"
419
+ form={formId}
420
+ variant={tone === "danger" ? "danger" : "primary"}
421
+ pending={busy}
422
+ commit={commit}
423
+ disabledReason={busy ? undefined : guardReason}
424
+ >
425
+ {confirmLabel ?? labels.confirm}
426
+ </Button>
427
+ </CloseOnDone>
428
+ )}
429
+ >
430
+ <form
431
+ id={formId}
432
+ noValidate
433
+ className="space-y-3"
434
+ onSubmit={(event) => {
435
+ event.preventDefault();
436
+ submit();
437
+ }}
438
+ >
439
+ {lines ? (
440
+ <ul className="list-disc space-y-1 ps-5 text-sm text-[var(--text-secondary)]">
441
+ {lines.map((line) => (
442
+ <li key={line.key} className={line.severe ? "text-[var(--danger)]" : undefined}>
443
+ {line.text}
444
+ </li>
445
+ ))}
446
+ </ul>
447
+ ) : (
448
+ hasMessage(consequence as ReactNode) && (
449
+ <p className="text-sm text-[var(--text-secondary)]">{consequence as ReactNode}</p>
450
+ )
451
+ )}
452
+ {Children.toArray(children).some(hasMessage) && (
453
+ <div ref={slotRef} className="space-y-3">
454
+ {children}
455
+ </div>
456
+ )}
457
+ {askTyped ? (
458
+ <TypedConfirmField
459
+ ref={guardRef}
460
+ target={target.email}
461
+ match="caseless"
462
+ value={typed}
463
+ onValueChange={setTyped}
464
+ busy={busy}
465
+ data-private
466
+ />
467
+ ) : (
468
+ <Checkbox
469
+ ref={guardRef}
470
+ label={acknowledgeLabel ?? guardLabels.acknowledge}
471
+ checked={ticked}
472
+ // Not `disabled` while busy: the box may hold the focus.
473
+ onCheckedChange={(next) => {
474
+ if (!busy) setTicked(next);
475
+ }}
476
+ />
477
+ )}
478
+ {refusalBanner}
479
+ </form>
480
+ </DialogFrame>
481
+ );
482
+ }
483
+
484
+ /**
485
+ * The actions row's wrapper: it renders INSIDE the Modal, where the panel's animated
486
+ * close is in context, so a resolved action lowers the panel the way Cancel does and
487
+ * Modal then calls `onClose` (ReauthDialog's pattern).
488
+ */
489
+ function CloseOnDone({ done, children }: { done: boolean; children: ReactNode }) {
490
+ const close = useContext(ModalCloseContext);
491
+ useEffect(() => {
492
+ if (done) close?.();
493
+ }, [done, close]);
494
+ return <>{children}</>;
495
+ }