@eifi1/ui-kit 0.48.2 → 0.49.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 (292) hide show
  1. package/README.md +26 -12
  2. package/dist/admin/admin-action-confirm.js +1 -1
  3. package/dist/admin/admin-action-confirm.js.map +1 -1
  4. package/dist/admin/invitations-panel.js +5 -1
  5. package/dist/admin/invitations-panel.js.map +1 -1
  6. package/dist/admin/user-roster.d.ts +11 -5
  7. package/dist/admin/user-roster.js +15 -4
  8. package/dist/admin/user-roster.js.map +1 -1
  9. package/dist/auth/accept-invitation.js +2 -2
  10. package/dist/auth/accept-invitation.js.map +1 -1
  11. package/dist/auth/not-found-page.js +1 -1
  12. package/dist/auth/not-found-page.js.map +1 -1
  13. package/dist/auth/reset-password-form.js +2 -2
  14. package/dist/auth/reset-password-form.js.map +1 -1
  15. package/dist/auth/verify-email.js +2 -2
  16. package/dist/auth/verify-email.js.map +1 -1
  17. package/dist/billing/plan-card.js +2 -2
  18. package/dist/billing/plan-card.js.map +1 -1
  19. package/dist/components/breadcrumbs.d.ts +19 -1
  20. package/dist/components/breadcrumbs.js +11 -4
  21. package/dist/components/breadcrumbs.js.map +1 -1
  22. package/dist/components/button-look.d.ts +37 -7
  23. package/dist/components/button-look.js +33 -6
  24. package/dist/components/button-look.js.map +1 -1
  25. package/dist/components/button.d.ts +4 -1
  26. package/dist/components/button.js +2 -2
  27. package/dist/components/button.js.map +1 -1
  28. package/dist/components/calendar-heatmap.js +2 -1
  29. package/dist/components/calendar-heatmap.js.map +1 -1
  30. package/dist/components/chart-edge.d.ts +10 -0
  31. package/dist/components/chart-edge.js +57 -0
  32. package/dist/components/chart-edge.js.map +1 -0
  33. package/dist/components/chart.js +2 -1
  34. package/dist/components/chart.js.map +1 -1
  35. package/dist/components/chip.d.ts +8 -1
  36. package/dist/components/chip.js +4 -1
  37. package/dist/components/chip.js.map +1 -1
  38. package/dist/components/confirm-context.d.ts +5 -0
  39. package/dist/components/confirm-context.js +7 -0
  40. package/dist/components/confirm-context.js.map +1 -0
  41. package/dist/components/confirm-dialog.js +3 -3
  42. package/dist/components/confirm-dialog.js.map +1 -1
  43. package/dist/components/danger-confirm.d.ts +2 -2
  44. package/dist/components/danger-confirm.js +1 -1
  45. package/dist/components/danger-confirm.js.map +1 -1
  46. package/dist/components/data-table-fit.js +21 -3
  47. package/dist/components/data-table-fit.js.map +1 -1
  48. package/dist/components/data-table-mobile.d.ts +48 -2
  49. package/dist/components/data-table-mobile.js +87 -10
  50. package/dist/components/data-table-mobile.js.map +1 -1
  51. package/dist/components/data-table-row-actions.d.ts +2 -1
  52. package/dist/components/data-table-row-actions.js +7 -0
  53. package/dist/components/data-table-row-actions.js.map +1 -1
  54. package/dist/components/data-table-row.d.ts +1 -1
  55. package/dist/components/data-table-row.js.map +1 -1
  56. package/dist/components/data-table.d.ts +34 -1
  57. package/dist/components/data-table.js +34 -9
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/error-boundary.js +2 -2
  60. package/dist/components/error-boundary.js.map +1 -1
  61. package/dist/components/floating-panel.d.ts +36 -13
  62. package/dist/components/floating-panel.js +27 -7
  63. package/dist/components/floating-panel.js.map +1 -1
  64. package/dist/components/form-actions.d.ts +1 -1
  65. package/dist/components/form-actions.js +1 -1
  66. package/dist/components/form-actions.js.map +1 -1
  67. package/dist/components/icon-button.d.ts +11 -0
  68. package/dist/components/icon-button.js +7 -5
  69. package/dist/components/icon-button.js.map +1 -1
  70. package/dist/components/nav-pills.d.ts +8 -1
  71. package/dist/components/nav-pills.js +61 -38
  72. package/dist/components/nav-pills.js.map +1 -1
  73. package/dist/components/page-header-context.d.ts +8 -0
  74. package/dist/components/page-header-context.js +7 -0
  75. package/dist/components/page-header-context.js.map +1 -0
  76. package/dist/components/page-header.d.ts +14 -2
  77. package/dist/components/page-header.js +10 -1
  78. package/dist/components/page-header.js.map +1 -1
  79. package/dist/components/pie-chart.js +2 -1
  80. package/dist/components/pie-chart.js.map +1 -1
  81. package/dist/components/progress-bar.js +5 -4
  82. package/dist/components/progress-bar.js.map +1 -1
  83. package/dist/components/reauth-dialog.js +1 -1
  84. package/dist/components/reauth-dialog.js.map +1 -1
  85. package/dist/components/row-actions-labels.js +3 -1
  86. package/dist/components/row-actions-labels.js.map +1 -1
  87. package/dist/components/row-actions.d.ts +43 -9
  88. package/dist/components/row-actions.js +78 -15
  89. package/dist/components/row-actions.js.map +1 -1
  90. package/dist/components/section-nav.d.ts +37 -0
  91. package/dist/components/section-nav.js +33 -0
  92. package/dist/components/section-nav.js.map +1 -0
  93. package/dist/components/series-chart.js +8 -3
  94. package/dist/components/series-chart.js.map +1 -1
  95. package/dist/components/share-card.js +1 -1
  96. package/dist/components/share-card.js.map +1 -1
  97. package/dist/components/signature-pad.js +1 -1
  98. package/dist/components/signature-pad.js.map +1 -1
  99. package/dist/components/sparkline.js +2 -1
  100. package/dist/components/sparkline.js.map +1 -1
  101. package/dist/components/stat-tile.js +14 -7
  102. package/dist/components/stat-tile.js.map +1 -1
  103. package/dist/components/status-dot.d.ts +5 -1
  104. package/dist/components/status-dot.js +4 -1
  105. package/dist/components/status-dot.js.map +1 -1
  106. package/dist/components/tooltip.js +1 -1
  107. package/dist/components/tooltip.js.map +1 -1
  108. package/dist/components/translation-review-editor.js +2 -2
  109. package/dist/components/translation-review-editor.js.map +1 -1
  110. package/dist/components/translation-review.js +2 -2
  111. package/dist/components/translation-review.js.map +1 -1
  112. package/dist/components/treemap.d.ts +5 -2
  113. package/dist/components/treemap.js +26 -6
  114. package/dist/components/treemap.js.map +1 -1
  115. package/dist/components/use-table-state.js +37 -6
  116. package/dist/components/use-table-state.js.map +1 -1
  117. package/dist/data-table.js.map +1 -1
  118. package/dist/demo/demo-banner.js +1 -1
  119. package/dist/demo/demo-banner.js.map +1 -1
  120. package/dist/demo/demo-ended.js +1 -1
  121. package/dist/demo/demo-ended.js.map +1 -1
  122. package/dist/demo/demo-start.js +1 -1
  123. package/dist/demo/demo-start.js.map +1 -1
  124. package/dist/eslint/index.js +1 -1
  125. package/dist/feedback/feedback-inbox.js +1 -1
  126. package/dist/feedback/feedback-inbox.js.map +1 -1
  127. package/dist/hooks/use-dismiss.d.ts +9 -1
  128. package/dist/hooks/use-dismiss.js +25 -5
  129. package/dist/hooks/use-dismiss.js.map +1 -1
  130. package/dist/hooks/use-document-title.d.ts +14 -0
  131. package/dist/hooks/use-document-title.js +22 -0
  132. package/dist/hooks/use-document-title.js.map +1 -0
  133. package/dist/i18n/german.js +6 -2
  134. package/dist/i18n/german.js.map +1 -1
  135. package/dist/i18n/kit-labels.d.ts +31 -1
  136. package/dist/i18n/kit-labels.js +43 -2
  137. package/dist/i18n/kit-labels.js.map +1 -1
  138. package/dist/i18n/locales/en.js +6 -2
  139. package/dist/i18n/locales/en.js.map +2 -2
  140. package/dist/i18n/locales/es.js +6 -2
  141. package/dist/i18n/locales/es.js.map +1 -1
  142. package/dist/i18n/locales/fr.js +6 -2
  143. package/dist/i18n/locales/fr.js.map +1 -1
  144. package/dist/i18n/locales/hu.js +6 -2
  145. package/dist/i18n/locales/hu.js.map +1 -1
  146. package/dist/i18n/locales/it.js +6 -2
  147. package/dist/i18n/locales/it.js.map +1 -1
  148. package/dist/i18n/locales/zh.js +6 -2
  149. package/dist/i18n/locales/zh.js.map +1 -1
  150. package/dist/i18n/review.js +2 -0
  151. package/dist/i18n/review.js.map +1 -1
  152. package/dist/i18n/shared-labels.js +1 -0
  153. package/dist/i18n/shared-labels.js.map +1 -1
  154. package/dist/index.d.ts +8 -1
  155. package/dist/index.js +11 -0
  156. package/dist/index.js.map +1 -1
  157. package/dist/landing/landing-actions.js +2 -2
  158. package/dist/landing/landing-actions.js.map +1 -1
  159. package/dist/landing/public-header.js +2 -2
  160. package/dist/landing/public-header.js.map +1 -1
  161. package/dist/lib/clipboard-minus.d.ts +27 -0
  162. package/dist/lib/clipboard-minus.js +63 -0
  163. package/dist/lib/clipboard-minus.js.map +1 -0
  164. package/dist/lib/document-title.d.ts +17 -0
  165. package/dist/lib/document-title.js +29 -0
  166. package/dist/lib/document-title.js.map +1 -0
  167. package/dist/lib/float-clearance.js +8 -2
  168. package/dist/lib/float-clearance.js.map +1 -1
  169. package/dist/lib/format.d.ts +35 -10
  170. package/dist/lib/format.js +55 -11
  171. package/dist/lib/format.js.map +1 -1
  172. package/dist/lib/minus.d.ts +22 -0
  173. package/dist/lib/minus.js +14 -0
  174. package/dist/lib/minus.js.map +1 -0
  175. package/dist/lib/status-tone.d.ts +30 -0
  176. package/dist/lib/status-tone.js +17 -0
  177. package/dist/lib/status-tone.js.map +1 -0
  178. package/dist/settings/settings-layout.js +3 -2
  179. package/dist/settings/settings-layout.js.map +1 -1
  180. package/dist/shell/account-menu-language.d.ts +1 -1
  181. package/dist/shell/account-menu-language.js +9 -1
  182. package/dist/shell/account-menu-language.js.map +1 -1
  183. package/dist/shell/app-shell-labels.js +2 -1
  184. package/dist/shell/app-shell-labels.js.map +1 -1
  185. package/dist/shell/app-shell.d.ts +58 -16
  186. package/dist/shell/app-shell.js +151 -43
  187. package/dist/shell/app-shell.js.map +1 -1
  188. package/dist/shell/language-code.d.ts +3 -0
  189. package/dist/shell/language-code.js +8 -0
  190. package/dist/shell/language-code.js.map +1 -0
  191. package/dist/shell/topbar-controls.d.ts +14 -6
  192. package/dist/shell/topbar-controls.js +14 -17
  193. package/dist/shell/topbar-controls.js.map +1 -1
  194. package/dist/testing/css-variables.generated.d.ts +1 -1
  195. package/dist/testing/css-variables.generated.js +1 -1
  196. package/dist/testing/css-variables.generated.js.map +1 -1
  197. package/dist/theme/chart-palette.d.ts +82 -15
  198. package/dist/theme/chart-palette.js +51 -24
  199. package/dist/theme/chart-palette.js.map +1 -1
  200. package/dist/theme/palette-presets.d.ts +9 -0
  201. package/dist/theme/palette-presets.js +77 -62
  202. package/dist/theme/palette-presets.js.map +1 -1
  203. package/dist/tour/tour.js +1 -1
  204. package/dist/tour/tour.js.map +1 -1
  205. package/dist/wizard/stepper-nav.d.ts +1 -1
  206. package/dist/wizard/stepper-nav.js +3 -3
  207. package/dist/wizard/stepper-nav.js.map +1 -1
  208. package/package.json +1 -1
  209. package/src/admin/admin-action-confirm.tsx +1 -1
  210. package/src/admin/invitations-panel.tsx +7 -3
  211. package/src/admin/user-roster.tsx +23 -6
  212. package/src/auth/accept-invitation.tsx +3 -3
  213. package/src/auth/not-found-page.tsx +1 -1
  214. package/src/auth/reset-password-form.tsx +3 -3
  215. package/src/auth/verify-email.tsx +3 -3
  216. package/src/billing/plan-card.tsx +2 -2
  217. package/src/components/breadcrumbs.tsx +32 -4
  218. package/src/components/button-look.ts +86 -15
  219. package/src/components/button.tsx +6 -3
  220. package/src/components/calendar-heatmap.tsx +2 -1
  221. package/src/components/chart-edge.ts +73 -0
  222. package/src/components/chart.tsx +3 -1
  223. package/src/components/chip.tsx +10 -1
  224. package/src/components/confirm-context.ts +7 -0
  225. package/src/components/confirm-dialog.tsx +3 -3
  226. package/src/components/danger-confirm.tsx +3 -3
  227. package/src/components/data-table-fit.ts +41 -6
  228. package/src/components/data-table-mobile.tsx +162 -11
  229. package/src/components/data-table-row-actions.tsx +9 -1
  230. package/src/components/data-table-row.tsx +2 -0
  231. package/src/components/data-table.tsx +82 -18
  232. package/src/components/error-boundary.tsx +2 -2
  233. package/src/components/floating-panel.tsx +67 -16
  234. package/src/components/form-actions.tsx +2 -2
  235. package/src/components/icon-button.tsx +21 -8
  236. package/src/components/nav-pills.tsx +29 -2
  237. package/src/components/page-header-context.ts +8 -0
  238. package/src/components/page-header.tsx +34 -2
  239. package/src/components/pie-chart.tsx +2 -1
  240. package/src/components/progress-bar.tsx +6 -3
  241. package/src/components/reauth-dialog.tsx +1 -1
  242. package/src/components/row-actions-labels.ts +2 -0
  243. package/src/components/row-actions.tsx +154 -33
  244. package/src/components/section-nav.tsx +65 -0
  245. package/src/components/series-chart.tsx +12 -3
  246. package/src/components/share-card.tsx +1 -1
  247. package/src/components/signature-pad.tsx +1 -1
  248. package/src/components/sparkline.tsx +2 -1
  249. package/src/components/stat-tile.tsx +19 -7
  250. package/src/components/status-dot.tsx +7 -1
  251. package/src/components/tooltip.tsx +3 -1
  252. package/src/components/translation-review-editor.tsx +2 -2
  253. package/src/components/translation-review.tsx +2 -2
  254. package/src/components/treemap.tsx +34 -8
  255. package/src/components/use-table-state.ts +63 -8
  256. package/src/data-table.ts +3 -3
  257. package/src/demo/demo-banner.tsx +1 -1
  258. package/src/demo/demo-ended.tsx +1 -1
  259. package/src/demo/demo-start.tsx +1 -1
  260. package/src/feedback/feedback-inbox.tsx +1 -1
  261. package/src/hooks/use-dismiss.ts +43 -4
  262. package/src/hooks/use-document-title.ts +31 -0
  263. package/src/i18n/german.ts +4 -0
  264. package/src/i18n/kit-labels.tsx +76 -1
  265. package/src/i18n/locales/es.ts +4 -0
  266. package/src/i18n/locales/fr.ts +4 -0
  267. package/src/i18n/locales/hu.ts +4 -0
  268. package/src/i18n/locales/it.ts +4 -0
  269. package/src/i18n/locales/zh.ts +4 -0
  270. package/src/i18n/review.ts +2 -0
  271. package/src/i18n/shared-labels.ts +1 -0
  272. package/src/index.ts +13 -0
  273. package/src/landing/landing-actions.tsx +2 -2
  274. package/src/landing/public-header.tsx +2 -2
  275. package/src/lib/clipboard-minus.ts +94 -0
  276. package/src/lib/document-title.ts +41 -0
  277. package/src/lib/float-clearance.ts +19 -3
  278. package/src/lib/format.ts +115 -30
  279. package/src/lib/minus.ts +40 -0
  280. package/src/lib/status-tone.ts +43 -0
  281. package/src/settings/settings-layout.tsx +3 -2
  282. package/src/shell/account-menu-language.tsx +14 -2
  283. package/src/shell/app-shell-labels.ts +1 -0
  284. package/src/shell/app-shell.tsx +291 -68
  285. package/src/shell/language-code.ts +5 -0
  286. package/src/shell/topbar-controls.tsx +35 -15
  287. package/src/testing/css-variables.generated.ts +6 -6
  288. package/src/theme/chart-palette.ts +102 -20
  289. package/src/theme/palette-presets.ts +121 -84
  290. package/src/tour/tour.tsx +1 -1
  291. package/src/wizard/stepper-nav.tsx +4 -4
  292. package/tokens.css +28 -18
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/user-roster.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport type { PersonName } from \"../lib/person-name\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AccountStateChip, RoleChip, dateColumn } from \"../components/account-chips\";\nimport type { AccountState, RoleVocabulary } from \"../components/account-chips\";\nimport type { ChipTone } from \"../components/chip\";\nimport type { DataTableColumn } from \"../components/data-table\";\nimport { encodeSorts } from \"../components/data-table-sort\";\nimport type { SortState } from \"../components/data-table-sort\";\nimport { RowActions } from \"../components/row-actions\";\nimport type { RowAction, RowActionList } from \"../components/row-actions\";\nimport { UserAvatar } from \"../components/user-avatar\";\nimport { personLabel } from \"./admin-parts\";\nimport { RoleSelect } from \"./role-select\";\nimport type { RoleLock } from \"./role-select\";\nimport { DEFAULT_USER_ROSTER_LABELS } from \"./user-roster-labels\";\n\n/**\n * The admin's user list (docs/user-admin-harmonization.md §3): a column preset for the\n * kit's `DataTable`, not a table of its own.\n *\n * keksdose built its roster by hand (1150 lines, its own Badge), Kurvenschmiede on the\n * kit `DataTable` with the 0.18 chips, kastlan as a settings page. What the three share\n * is the row — name over address, a role, the account's states, created, last login, the\n * app's own columns, and a handful of actions — so that is what the kit draws, and the\n * table, its paging, its sort and its filters stay the app's `DataTable` wiring:\n *\n * ```tsx\n * const columns = useUserRosterColumns<AdminUserRow, Role>({ roles: ROLES, actions, extra });\n * <DataTable rows={page.users} columns={columns} rowKey={(r) => r.id}\n * serverPagination={{ page, pageSize, total: page.total, onPageChange }}\n * sorts={sorts} onSortsChange={setSorts} />\n * // GET /admin/users?sort=${userRosterSort(sorts)}&…\n * ```\n *\n * The column keys ARE the server's sort keys (§3.1): `name`, `email`, `role`, `created`,\n * `last_login` — so {@link userRosterSort} turns the table's sort state into the query\n * as it is. Phone cards come from the same columns: the identity is the card's headline,\n * the role, states, last login and the actions are its fields, created is left off.\n */\n\n/* ── Rows and states ────────────────────────────────────────────────────────── */\n\n/**\n * The fields of the contract's `AdminUserRow` the preset reads (§3.2, the auth\n * contract's `UserResponse` core). An app's row has more — its `extra` — and the\n * preset passes the whole row to every callback.\n */\nexport interface UserRosterRow extends AdminUserStateFields {\n id: string | number;\n email: string;\n first_name?: string | null;\n last_name?: string | null;\n /** The derived whole name — only the fallback when both parts are blank. */\n display_name?: string | null;\n /** keksdose, Kurvenschmiede: one role. */\n role?: string | null;\n /** kastlan: the account's roles in the current company. Wins over `role`. */\n roles?: readonly string[] | null;\n created_at?: DateInput;\n /** Account-wide; kastlan's comes from its session rows (§3.2). */\n last_login_at?: DateInput;\n}\n\n/** The state fields of the contract's row that {@link adminUserStates} maps (§3.2). */\nexport interface AdminUserStateFields {\n is_active?: boolean;\n email_verified?: boolean;\n password_change_required_at?: string | null;\n /** `after_days` mode: the day the account is erased (§6.4). */\n deletion_scheduled_at?: string | null;\n /** When the user asked. The only deletion field in kastlan's `operator` mode, where\n * nothing is scheduled. */\n deletion_requested_at?: string | null;\n}\n\n/** One state chip of a row: an {@link AccountState}, or one with its `date` (the\n * `deletion` day), a `tone` or the app's own words. */\nexport type RosterState =\n | AccountState\n | { state: AccountState; date?: DateInput; tone?: ChipTone; label?: ReactNode };\n\n/**\n * The contract's row as state chips (§3.2: \"map 1:1 to the kit's AccountStateChip\"):\n *\n * - marked for deletion → `deletion` with the scheduled day (none in `operator` mode),\n * IN PLACE of `inactive` — such an account is always deactivated (§2.1);\n * - else `active` / `inactive` from `is_active`;\n * - `unverified` when `email_verified` is `false` (not when it is missing);\n * - `passwordChange` while `password_change_required_at` is set.\n */\nexport function adminUserStates(row: AdminUserStateFields): RosterState[] {\n const out: RosterState[] = [];\n if (row.deletion_scheduled_at || row.deletion_requested_at) {\n out.push({ state: \"deletion\", date: row.deletion_scheduled_at ?? null });\n } else if (row.is_active !== undefined) {\n out.push(row.is_active ? \"active\" : \"inactive\");\n }\n if (row.email_verified === false) out.push(\"unverified\");\n if (row.password_change_required_at) out.push(\"passwordChange\");\n return out;\n}\n\n/** The server's sort keys, which are the preset's column keys (§3.1). */\nexport const USER_ROSTER_SORT_KEYS = [\"name\", \"email\", \"role\", \"created\", \"last_login\"] as const;\nexport type UserRosterSortKey = (typeof USER_ROSTER_SORT_KEYS)[number];\n\n/**\n * The table's sort state as the `sort` query parameter of `GET /admin/users` (§3.1), in\n * the DataTable's own encoding ({@link encodeSorts}: `\"last_login.desc\"`), which\n * server-kit 0.4 reads beside the `-key` form. Only the first criterion on one of the\n * server's keys: an app's own column (`owned`) is the client's to sort, and the query\n * takes one key — a shift-clicked second criterion is not sent. `undefined` when nothing\n * the server knows is sorted, so its default order applies.\n */\nexport function userRosterSort(sorts: readonly SortState[]): string | undefined {\n const first = sorts.find((s) => (USER_ROSTER_SORT_KEYS as readonly string[]).includes(s.key));\n return first ? (encodeSorts([first]) ?? undefined) : undefined;\n}\n\n/* ── Labels ─────────────────────────────────────────────────────────────────── */\n\n/** The `userRoster` namespace: the preset's headers and the action menu's words. */\nexport interface UserRosterLabels {\n /** The identity column — the name with the address under it. */\n name: string;\n /** The separate address column (`emailColumn`). */\n email: string;\n role: string;\n state: string;\n created: string;\n lastLogin: string;\n /** A last login that never happened. */\n never: string;\n /** The actions column's (hidden) header. */\n actions: string;\n /** The action menu's button and panel — \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\n// Defined in ./user-roster-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_USER_ROSTER_LABELS };\n\n/* ── UserIdentityCell ───────────────────────────────────────────────────────── */\n\nexport interface UserIdentityCellProps {\n /** First and last name (auth §3.2), shown in the READER's order. */\n person?: PersonName | null;\n /** A whole name, for an API that still sends `display_name` only. */\n name?: string | null;\n email?: string | null;\n /** The initials avatar before the name. Default true. */\n avatar?: boolean;\n /** After the name, on its line — a \"You\" chip, a demo mark. */\n badge?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * Who an account is, in one cell (§3.3): the initials avatar, the name in the reader's\n * order ({@link formatPersonName} — \"Example Ada\" in Hungarian) and the address under it\n * — the cell both rosters drew by hand. An account with no name yet (a migrated row\n * before `CompleteNameDialog`, an invitee) shows its address once, as the name.\n *\n * Both lines carry `data-private`, so keksdose's demo blur reaches them, and both end\n * in \"…\" at Normal only: from Large up they wrap (`truncate-until-large`, 0.33,\n * docs/text-size-harmonization.md §10.17), since a cut name or address is a finding there.\n */\nexport function UserIdentityCell({\n person,\n name,\n email,\n avatar = true,\n badge,\n locale: localeProp,\n className,\n}: UserIdentityCellProps) {\n const locale = useKitLocale(localeProp);\n const named = personLabel({ first: person?.first, last: person?.last, name }, locale);\n const address = (email ?? \"\").trim();\n return (\n <div className={cn(\"flex min-w-0 items-center gap-2.5\", className)}>\n {avatar && <UserAvatar person={person} name={name} email={email} size=\"sm\" />}\n <div className=\"min-w-0\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span data-private className=\"truncate-until-large font-medium text-primary\">\n {named || address}\n </span>\n {badge}\n </div>\n {named && address && (\n <div data-private className=\"truncate-until-large text-xs text-muted\">\n {address}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n/* ── The row's actions ──────────────────────────────────────────────────────── */\n\n/** One entry of a row's action menu (§4) — the kit's {@link RowAction} since 0.32,\n * which generalised it for every dense row (docs/text-size-harmonization.md §10.8). */\nexport type UserRowAction = RowAction;\n\n/** What `actions(row)` may return: entries, and `false` / `null` for the ones a\n * condition left out (`row.is_active && {…}`). */\nexport type UserRowActionList = RowActionList;\n\nexport interface UserRowActionsProps {\n actions: UserRowActionList;\n /** The account's name, for the menu's name — \"Actions for Ada Example\". */\n name: string;\n labels?: LabelOverride<UserRosterLabels>;\n}\n\n/**\n * A row's actions as one \"…\" button and a menu of them: keksdose had a strip of icon\n * buttons in the expanded row, Kurvenschmiede five text buttons in a cell — neither fits\n * a phone card, and both grow with every action §2.4 adds. The menu is portalled, so a\n * table's scroller cannot clip it; Escape and an outside press close it, and focus goes\n * back to the button, which is where a confirmation dialog then returns it.\n *\n * Since 0.32 this is {@link RowActions} with `collapse=\"menu\"` — the menu at every size —\n * named by the roster's own `actionsFor`.\n */\nexport function UserRowActions({ actions, name, labels: labelsProp }: UserRowActionsProps) {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return (\n <RowActions\n actions={actions}\n name={name}\n collapse=\"menu\"\n labels={{ actionsFor: labels.actionsFor }}\n />\n );\n}\n\n/* ── The preset ─────────────────────────────────────────────────────────────── */\n\n/** How a row's role may be changed inline: `true`, or with reasons. */\nexport interface RoleEditing<R extends string> {\n /** Why the whole select is locked for this row — `\"self\"`, `\"last_admin\"`, or words. */\n disabledReason?: RoleLock;\n /** Why one role cannot be chosen for this row (a string: it goes inside an option). */\n optionDisabledReason?: (role: R) => string | undefined;\n /** The roles that may be chosen. Default: the whole vocabulary. */\n offered?: readonly R[];\n}\n\nexport interface UserRosterColumnsOptions<T extends UserRosterRow, R extends string = string> {\n /** The app's roles, as `RoleChip` reads them. */\n roles: RoleVocabulary<R>;\n /** The row's state chips. Default {@link adminUserStates}, which reads the contract's\n * fields (`is_active`, `email_verified`, `password_change_required_at`,\n * `deletion_scheduled_at`, `deletion_requested_at`). */\n stateOf?: (row: T) => readonly RosterState[];\n /** The app's own columns — keksdose's plan and key custody, Kurvenschmiede's owned\n * counts — after the dates, before the actions. */\n extra?: readonly DataTableColumn<T>[];\n /** The row's action menu (§4). Left out: no actions column. */\n actions?: (row: T) => UserRowActionList;\n /** The actions column's heading (0.46.1, keksdose dev#598 rework — a VISIBLE \"Actions\"\n * on every table with an actions column). Default: `labels.actions`, shown. A node\n * replaces it; `false` keeps the pre-0.46.1 heading, a screen reader's name only. */\n actionsHeader?: ReactNode | false;\n /**\n * Whether this row's role is a select rather than a chip, and with which reasons.\n * Left out, or `false`: a chip. kastlan's several roles are never a select here — they\n * are a \"Manage roles\" action that opens `RolesEditor`.\n */\n editableRole?: (row: T) => boolean | RoleEditing<R>;\n /** A role was picked in a row's select. The app confirms (`acknowledge`) and sends it. */\n onRoleChange?: (row: T, role: R) => void;\n /** The row's role(s). Default `row.roles ?? row.role`. */\n roleOf?: (row: T) => R | readonly R[] | null | undefined;\n /** A separate, sortable \"Email\" column (the server's `email` sort key) beside the\n * identity. Default false: the address is under the name already. */\n emailColumn?: boolean;\n /** The words, already resolved. Default English; {@link useUserRosterColumns} reads\n * the provider's `userRoster` namespace. */\n labels?: LabelOverride<UserRosterLabels>;\n}\n\nfunction rolesOf<T extends UserRosterRow, R extends string>(\n row: T,\n roleOf: UserRosterColumnsOptions<T, R>[\"roleOf\"],\n): R[] {\n const raw = roleOf ? roleOf(row) : ((row.roles ?? row.role) as R | readonly R[] | null | undefined);\n if (raw === null || raw === undefined || raw === \"\") return [];\n return typeof raw === \"string\" ? [raw as R] : [...(raw as readonly R[])];\n}\n\n/** The row's name in the reader's order — a component, because the order is the\n * READER's and the locale comes from the provider. */\nfunction RowName<T extends UserRosterRow>({ row, children }: { row: T; children: (name: string) => ReactNode }) {\n const locale = useKitLocale();\n const name = personLabel(\n { first: row.first_name, last: row.last_name, name: row.display_name, email: row.email },\n locale,\n );\n return <>{children(name)}</>;\n}\n\nfunction StateChips({ states }: { states: readonly RosterState[] }) {\n if (states.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {states.map((entry) => {\n const spec = typeof entry === \"string\" ? { state: entry } : entry;\n return (\n <AccountStateChip key={spec.state} state={spec.state} date={spec.date} tone={spec.tone} size=\"xs\">\n {spec.label}\n </AccountStateChip>\n );\n })}\n </div>\n );\n}\n\n/**\n * The roster's columns, in the contract's order (§3.3):\n *\n * | key | cell | sort | phone |\n * |---|---|---|---|\n * | `name` | {@link UserIdentityCell} | last, first | the card's headline |\n * | `email` | the address (`emailColumn` only) | address | hidden |\n * | `role` | `RoleChip`(s), or {@link RoleSelect} where `editableRole` | role | field |\n * | `state` | `AccountStateChip`s from `stateOf` | — | field |\n * | `created` | `dateColumn` | instant | hidden |\n * | `last_login` | `dateColumn`, relative, \"Never\" | instant | field |\n * | …`extra` | the app's | the app's | the app's |\n * | `actions` | {@link UserRowActions} | — | field |\n *\n * Pure: hand it resolved words, or use {@link useUserRosterColumns}. The keys are stable,\n * so an app may drop or move a column (`columns.filter((c) => c.key !== \"created\")`).\n * Under `serverPagination` the `sortBy`s only make the headers clickable; the app sends\n * the sort ({@link userRosterSort}).\n */\nexport function userRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf = adminUserStates,\n extra = [],\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn = false,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels: UserRosterLabels = { ...DEFAULT_USER_ROSTER_LABELS, ...labelsProp };\n const roleWords = (row: T) =>\n rolesOf(row, roleOf)\n .map((role) => roles[role]?.label ?? role)\n .join(\", \");\n const columns: DataTableColumn<T>[] = [\n {\n key: \"name\",\n header: labels.name,\n cell: (row) => (\n <UserIdentityCell\n person={{ first: row.first_name, last: row.last_name }}\n name={row.display_name}\n email={row.email}\n />\n ),\n // By last name, then first (auth §3.2), as the server sorts `name`.\n sortBy: (row) => `${row.last_name ?? \"\"} ${row.first_name ?? \"\"}`.trim().toLocaleLowerCase() || row.email,\n // Either part, in either order, or the address (auth §10.6).\n filterBy: (row) =>\n [row.first_name, row.last_name, row.last_name, row.first_name, row.display_name, row.email]\n .filter(Boolean)\n .join(\" \"),\n mobilePrimary: true,\n },\n ];\n if (emailColumn) {\n columns.push({\n key: \"email\",\n header: labels.email,\n className: \"whitespace-nowrap\",\n cell: (row) => (\n <span data-private className=\"text-secondary\">\n {row.email}\n </span>\n ),\n sortBy: (row) => row.email.toLowerCase(),\n filterBy: (row) => row.email,\n mobileHidden: true,\n });\n }\n columns.push(\n {\n key: \"role\",\n header: labels.role,\n cell: (row) => {\n const held = rolesOf(row, roleOf);\n const editing = held.length <= 1 ? editableRole?.(row) : false;\n if (editing && onRoleChange) {\n const rules: RoleEditing<R> = editing === true ? {} : editing;\n return (\n <RowName row={row}>\n {(name) => (\n <RoleSelect<R>\n size=\"sm\"\n name={name}\n value={held[0] ?? null}\n roles={roles}\n offered={rules.offered}\n disabledReason={rules.disabledReason}\n optionDisabledReason={rules.optionDisabledReason}\n onChange={(role) => onRoleChange(row, role)}\n />\n )}\n </RowName>\n );\n }\n if (held.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {held.map((role) => (\n <RoleChip<R> key={role} value={role} roles={roles} size=\"xs\" />\n ))}\n </div>\n );\n },\n sortBy: roleWords,\n filter: {\n type: \"select\",\n getValue: (row) => rolesOf(row, roleOf)[0] ?? \"\",\n options: (Object.keys(roles) as R[]).map((value) => ({ value, label: roles[value].label })),\n },\n },\n {\n key: \"state\",\n header: labels.state,\n cell: (row) => <StateChips states={stateOf(row)} />,\n },\n dateColumn<T>({\n key: \"created\",\n header: labels.created,\n value: (row) => row.created_at ?? null,\n column: { mobileHidden: true },\n }),\n // How long ago rather than a date — the question is \"recently?\" — with the exact\n // time beside it (keksdose's last-seen column, Kurvenschmiede's; 0.32 §4: never only\n // in a tooltip).\n dateColumn<T>({\n key: \"last_login\",\n header: labels.lastLogin,\n value: (row) => row.last_login_at ?? null,\n display: \"relative\",\n empty: labels.never,\n }),\n ...extra,\n );\n if (actions) {\n columns.push({\n key: \"actions\",\n // On screen, as every actions column's heading is (0.46.1, keksdose dev#598 rework).\n header:\n actionsHeader === false ? <span className=\"sr-only\">{labels.actions}</span> : (actionsHeader ?? labels.actions),\n headerText: labels.actions,\n // The caller's own words only: the menu reads the provider's itself, and the\n // English defaults merged in above must not shadow them.\n cell: (row) => (\n <RowName row={row}>\n {(name) => <UserRowActions actions={actions(row)} name={name} labels={labelsProp} />}\n </RowName>\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px text-end\",\n // The menu button keeps its own click: a row link must not wrap it.\n noRowLink: true,\n });\n }\n return columns;\n}\n\n/**\n * {@link userRosterColumns} with the `userRoster` words from the provider, memoised on\n * the options — pass stable callbacks (`useCallback`) and a module-level vocabulary, or\n * the columns are rebuilt on every render.\n */\nexport function useUserRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf,\n extra,\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return useMemo(\n () =>\n userRosterColumns<T, R>({\n roles,\n stateOf,\n extra,\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels,\n }),\n [roles, stateOf, extra, actions, actionsHeader, editableRole, onRoleChange, roleOf, emailColumn, labels],\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/admin/user-roster.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport type { PersonName } from \"../lib/person-name\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AccountStateChip, RoleChip, dateColumn } from \"../components/account-chips\";\nimport type { AccountState, RoleVocabulary } from \"../components/account-chips\";\nimport type { ChipTone } from \"../components/chip\";\nimport type { DataTableColumn } from \"../components/data-table\";\nimport { encodeSorts } from \"../components/data-table-sort\";\nimport type { SortState } from \"../components/data-table-sort\";\nimport { RowActions } from \"../components/row-actions\";\nimport type { RowAction, RowActionList } from \"../components/row-actions\";\nimport { UserAvatar } from \"../components/user-avatar\";\nimport { personLabel } from \"./admin-parts\";\nimport { RoleSelect } from \"./role-select\";\nimport type { RoleLock } from \"./role-select\";\nimport { DEFAULT_USER_ROSTER_LABELS } from \"./user-roster-labels\";\n\n/**\n * The admin's user list (docs/user-admin-harmonization.md §3): a column preset for the\n * kit's `DataTable`, not a table of its own.\n *\n * keksdose built its roster by hand (1150 lines, its own Badge), Kurvenschmiede on the\n * kit `DataTable` with the 0.18 chips, kastlan as a settings page. What the three share\n * is the row — name over address, a role, the account's states, created, last login, the\n * app's own columns, and a handful of actions — so that is what the kit draws, and the\n * table, its paging, its sort and its filters stay the app's `DataTable` wiring:\n *\n * ```tsx\n * const columns = useUserRosterColumns<AdminUserRow, Role>({ roles: ROLES, actions, extra });\n * <DataTable rows={page.users} columns={columns} rowKey={(r) => r.id}\n * serverPagination={{ page, pageSize, total: page.total, onPageChange }}\n * sorts={sorts} onSortsChange={setSorts} />\n * // GET /admin/users?sort=${userRosterSort(sorts)}&…\n * ```\n *\n * The column keys ARE the server's sort keys (§3.1): `name`, `email`, `role`, `created`,\n * `last_login` — so {@link userRosterSort} turns the table's sort state into the query\n * as it is. Phone cards come from the same columns: the identity is the card's headline\n * with the account's states as chips beside it, the role, last login and the actions are\n * its three fields, created is left off.\n */\n\n/* ── Rows and states ────────────────────────────────────────────────────────── */\n\n/**\n * The fields of the contract's `AdminUserRow` the preset reads (§3.2, the auth\n * contract's `UserResponse` core). An app's row has more — its `extra` — and the\n * preset passes the whole row to every callback.\n */\nexport interface UserRosterRow extends AdminUserStateFields {\n id: string | number;\n email: string;\n first_name?: string | null;\n last_name?: string | null;\n /** The derived whole name — only the fallback when both parts are blank. */\n display_name?: string | null;\n /** keksdose, Kurvenschmiede: one role. */\n role?: string | null;\n /** kastlan: the account's roles in the current company. Wins over `role`. */\n roles?: readonly string[] | null;\n created_at?: DateInput;\n /** Account-wide; kastlan's comes from its session rows (§3.2). */\n last_login_at?: DateInput;\n}\n\n/** The state fields of the contract's row that {@link adminUserStates} maps (§3.2). */\nexport interface AdminUserStateFields {\n is_active?: boolean;\n email_verified?: boolean;\n password_change_required_at?: string | null;\n /** `after_days` mode: the day the account is erased (§6.4). */\n deletion_scheduled_at?: string | null;\n /** When the user asked. The only deletion field in kastlan's `operator` mode, where\n * nothing is scheduled. */\n deletion_requested_at?: string | null;\n}\n\n/** One state chip of a row: an {@link AccountState}, or one with its `date` (the\n * `deletion` day), a `tone` or the app's own words. */\nexport type RosterState =\n | AccountState\n | { state: AccountState; date?: DateInput; tone?: ChipTone; label?: ReactNode };\n\n/**\n * The contract's row as state chips (§3.2: \"map 1:1 to the kit's AccountStateChip\"):\n *\n * - marked for deletion → `deletion` with the scheduled day (none in `operator` mode),\n * IN PLACE of `inactive` — such an account is always deactivated (§2.1);\n * - else `active` / `inactive` from `is_active`;\n * - `unverified` when `email_verified` is `false` (not when it is missing);\n * - `passwordChange` while `password_change_required_at` is set.\n */\nexport function adminUserStates(row: AdminUserStateFields): RosterState[] {\n const out: RosterState[] = [];\n if (row.deletion_scheduled_at || row.deletion_requested_at) {\n out.push({ state: \"deletion\", date: row.deletion_scheduled_at ?? null });\n } else if (row.is_active !== undefined) {\n out.push(row.is_active ? \"active\" : \"inactive\");\n }\n if (row.email_verified === false) out.push(\"unverified\");\n if (row.password_change_required_at) out.push(\"passwordChange\");\n return out;\n}\n\n/** The server's sort keys, which are the preset's column keys (§3.1). */\nexport const USER_ROSTER_SORT_KEYS = [\"name\", \"email\", \"role\", \"created\", \"last_login\"] as const;\nexport type UserRosterSortKey = (typeof USER_ROSTER_SORT_KEYS)[number];\n\n/**\n * The table's sort state as the `sort` query parameter of `GET /admin/users` (§3.1), in\n * the DataTable's own encoding ({@link encodeSorts}: `\"last_login.desc\"`), which\n * server-kit 0.4 reads beside the `-key` form. Only the first criterion on one of the\n * server's keys: an app's own column (`owned`) is the client's to sort, and the query\n * takes one key — a shift-clicked second criterion is not sent. `undefined` when nothing\n * the server knows is sorted, so its default order applies.\n */\nexport function userRosterSort(sorts: readonly SortState[]): string | undefined {\n const first = sorts.find((s) => (USER_ROSTER_SORT_KEYS as readonly string[]).includes(s.key));\n return first ? (encodeSorts([first]) ?? undefined) : undefined;\n}\n\n/* ── Labels ─────────────────────────────────────────────────────────────────── */\n\n/** The `userRoster` namespace: the preset's headers and the action menu's words. */\nexport interface UserRosterLabels {\n /** The identity column — the name with the address under it. */\n name: string;\n /** The separate address column (`emailColumn`). */\n email: string;\n role: string;\n state: string;\n created: string;\n lastLogin: string;\n /** A last login that never happened. */\n never: string;\n /** The actions column's (hidden) header. */\n actions: string;\n /** The action menu's button and panel — \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\n// Defined in ./user-roster-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_USER_ROSTER_LABELS };\n\n/* ── UserIdentityCell ───────────────────────────────────────────────────────── */\n\nexport interface UserIdentityCellProps {\n /** First and last name (auth §3.2), shown in the READER's order. */\n person?: PersonName | null;\n /** A whole name, for an API that still sends `display_name` only. */\n name?: string | null;\n email?: string | null;\n /** The initials avatar before the name. Default true. */\n avatar?: boolean;\n /** After the name, on its line — a \"You\" chip, a demo mark. */\n badge?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * Who an account is, in one cell (§3.3): the initials avatar, the name in the reader's\n * order ({@link formatPersonName} — \"Example Ada\" in Hungarian) and the address under it\n * — the cell both rosters drew by hand. An account with no name yet (a migrated row\n * before `CompleteNameDialog`, an invitee) shows its address once, as the name.\n *\n * Both lines carry `data-private`, so keksdose's demo blur reaches them, and both end\n * in \"…\" at Normal only: from Large up they wrap (`truncate-until-large`, 0.33,\n * docs/text-size-harmonization.md §10.17), since a cut name or address is a finding there.\n */\nexport function UserIdentityCell({\n person,\n name,\n email,\n avatar = true,\n badge,\n locale: localeProp,\n className,\n}: UserIdentityCellProps) {\n const locale = useKitLocale(localeProp);\n const named = personLabel({ first: person?.first, last: person?.last, name }, locale);\n const address = (email ?? \"\").trim();\n return (\n <div className={cn(\"flex min-w-0 items-center gap-2.5\", className)}>\n {avatar && <UserAvatar person={person} name={name} email={email} size=\"sm\" />}\n <div className=\"min-w-0\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span data-private className=\"truncate-until-large font-medium text-primary\">\n {named || address}\n </span>\n {badge}\n </div>\n {named && address && (\n <div data-private className=\"truncate-until-large text-xs text-muted\">\n {address}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n/* ── The row's actions ──────────────────────────────────────────────────────── */\n\n/** One entry of a row's action menu (§4) — the kit's {@link RowAction} since 0.32,\n * which generalised it for every dense row (docs/text-size-harmonization.md §10.8). */\nexport type UserRowAction = RowAction;\n\n/** What `actions(row)` may return: entries, and `false` / `null` for the ones a\n * condition left out (`row.is_active && {…}`). */\nexport type UserRowActionList = RowActionList;\n\nexport interface UserRowActionsProps {\n actions: UserRowActionList;\n /** The account's name, for the menu's name — \"Actions for Ada Example\". */\n name: string;\n labels?: LabelOverride<UserRosterLabels>;\n}\n\n/**\n * A row's actions as one \"…\" button and a menu of them: keksdose had a strip of icon\n * buttons in the expanded row, Kurvenschmiede five text buttons in a cell — neither fits\n * a phone card, and both grow with every action §2.4 adds. The menu is portalled, so a\n * table's scroller cannot clip it; Escape and an outside press close it, and focus goes\n * back to the button, which is where a confirmation dialog then returns it.\n *\n * Since 0.32 this is {@link RowActions} with `collapse=\"menu\"` — the menu at every size —\n * named by the roster's own `actionsFor`.\n */\nexport function UserRowActions({ actions, name, labels: labelsProp }: UserRowActionsProps) {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return (\n <RowActions\n actions={actions}\n name={name}\n collapse=\"menu\"\n labels={{ actionsFor: labels.actionsFor }}\n />\n );\n}\n\n/* ── The preset ─────────────────────────────────────────────────────────────── */\n\n/** How a row's role may be changed inline: `true`, or with reasons. */\nexport interface RoleEditing<R extends string> {\n /** Why the whole select is locked for this row — `\"self\"`, `\"last_admin\"`, or words. */\n disabledReason?: RoleLock;\n /** Why one role cannot be chosen for this row (a string: it goes inside an option). */\n optionDisabledReason?: (role: R) => string | undefined;\n /** The roles that may be chosen. Default: the whole vocabulary. */\n offered?: readonly R[];\n}\n\nexport interface UserRosterColumnsOptions<T extends UserRosterRow, R extends string = string> {\n /** The app's roles, as `RoleChip` reads them. */\n roles: RoleVocabulary<R>;\n /** The row's state chips. Default {@link adminUserStates}, which reads the contract's\n * fields (`is_active`, `email_verified`, `password_change_required_at`,\n * `deletion_scheduled_at`, `deletion_requested_at`). */\n stateOf?: (row: T) => readonly RosterState[];\n /** The app's own columns — keksdose's plan and key custody, Kurvenschmiede's owned\n * counts — after the dates, before the actions. */\n extra?: readonly DataTableColumn<T>[];\n /** The row's action menu (§4). Left out: no actions column. */\n actions?: (row: T) => UserRowActionList;\n /** The actions column's heading (0.46.1, keksdose dev#598 rework — a VISIBLE \"Actions\"\n * on every table with an actions column). Default: `labels.actions`, shown. A node\n * replaces it; `false` keeps the pre-0.46.1 heading, a screen reader's name only. */\n actionsHeader?: ReactNode | false;\n /**\n * Whether this row's role is a select rather than a chip, and with which reasons.\n * Left out, or `false`: a chip. kastlan's several roles are never a select here — they\n * are a \"Manage roles\" action that opens `RolesEditor`.\n */\n editableRole?: (row: T) => boolean | RoleEditing<R>;\n /** A role was picked in a row's select. The app confirms (`acknowledge`) and sends it. */\n onRoleChange?: (row: T, role: R) => void;\n /** The row's role(s). Default `row.roles ?? row.role`. */\n roleOf?: (row: T) => R | readonly R[] | null | undefined;\n /** A separate, sortable \"Email\" column (the server's `email` sort key) beside the\n * identity. Default false: the address is under the name already. */\n emailColumn?: boolean;\n /** The words, already resolved. Default English; {@link useUserRosterColumns} reads\n * the provider's `userRoster` namespace. */\n labels?: LabelOverride<UserRosterLabels>;\n}\n\nfunction rolesOf<T extends UserRosterRow, R extends string>(\n row: T,\n roleOf: UserRosterColumnsOptions<T, R>[\"roleOf\"],\n): R[] {\n const raw = roleOf ? roleOf(row) : ((row.roles ?? row.role) as R | readonly R[] | null | undefined);\n if (raw === null || raw === undefined || raw === \"\") return [];\n return typeof raw === \"string\" ? [raw as R] : [...(raw as readonly R[])];\n}\n\n/** The row's name in the reader's order — a component, because the order is the\n * READER's and the locale comes from the provider. */\nfunction RowName<T extends UserRosterRow>({ row, children }: { row: T; children: (name: string) => ReactNode }) {\n const locale = useKitLocale();\n const name = personLabel(\n { first: row.first_name, last: row.last_name, name: row.display_name, email: row.email },\n locale,\n );\n return <>{children(name)}</>;\n}\n\nfunction StateChips({ states }: { states: readonly RosterState[] }) {\n if (states.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {states.map((entry) => {\n const spec = typeof entry === \"string\" ? { state: entry } : entry;\n return (\n <AccountStateChip key={spec.state} state={spec.state} date={spec.date} tone={spec.tone} size=\"xs\">\n {spec.label}\n </AccountStateChip>\n );\n })}\n </div>\n );\n}\n\n/**\n * The roster's columns, in the contract's order (§3.3):\n *\n * | key | cell | sort | phone |\n * |---|---|---|---|\n * | `name` | {@link UserIdentityCell} | last, first | the card's headline |\n * | `email` | the address (`emailColumn` only) | address | hidden |\n * | `role` | `RoleChip`(s), or {@link RoleSelect} where `editableRole` | role | field |\n * | `state` | `AccountStateChip`s from `stateOf` | — | chips beside the headline |\n * | `created` | `dateColumn` | instant | hidden |\n * | `last_login` | `dateColumn`, relative, \"Never\" | instant | field |\n * | …`extra` | the app's | the app's | off the card unless marked `mobile: \"secondary\"` |\n * | `actions` | {@link UserRowActions} | — | the card's actions row (`role: \"actions\"`) |\n *\n * The card holds three fields — `role`, `last_login` and one more: an `extra` column marked\n * `mobile: \"secondary\"` takes that place. The actions are `role: \"actions\"` (0.49), drawn\n * after the fields and never one of the three. Drop them from the card with\n * `mobile: \"hidden\"`, or draw a `mobileCard`.\n *\n * Pure: hand it resolved words, or use {@link useUserRosterColumns}. The keys are stable,\n * so an app may drop or move a column (`columns.filter((c) => c.key !== \"created\")`).\n * Under `serverPagination` the `sortBy`s only make the headers clickable; the app sends\n * the sort ({@link userRosterSort}).\n */\nexport function userRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf = adminUserStates,\n extra = [],\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn = false,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels: UserRosterLabels = { ...DEFAULT_USER_ROSTER_LABELS, ...labelsProp };\n const roleWords = (row: T) =>\n rolesOf(row, roleOf)\n .map((role) => roles[role]?.label ?? role)\n .join(\", \");\n const columns: DataTableColumn<T>[] = [\n {\n key: \"name\",\n header: labels.name,\n cell: (row) => (\n <UserIdentityCell\n person={{ first: row.first_name, last: row.last_name }}\n name={row.display_name}\n email={row.email}\n />\n ),\n // By last name, then first (auth §3.2), as the server sorts `name`.\n sortBy: (row) => `${row.last_name ?? \"\"} ${row.first_name ?? \"\"}`.trim().toLocaleLowerCase() || row.email,\n // Either part, in either order, or the address (auth §10.6).\n filterBy: (row) =>\n [row.first_name, row.last_name, row.last_name, row.first_name, row.display_name, row.email]\n .filter(Boolean)\n .join(\" \"),\n mobilePrimary: true,\n },\n ];\n if (emailColumn) {\n columns.push({\n key: \"email\",\n header: labels.email,\n className: \"whitespace-nowrap\",\n cell: (row) => (\n <span data-private className=\"text-secondary\">\n {row.email}\n </span>\n ),\n sortBy: (row) => row.email.toLowerCase(),\n filterBy: (row) => row.email,\n mobileHidden: true,\n });\n }\n columns.push(\n {\n key: \"role\",\n header: labels.role,\n cell: (row) => {\n const held = rolesOf(row, roleOf);\n const editing = held.length <= 1 ? editableRole?.(row) : false;\n if (editing && onRoleChange) {\n const rules: RoleEditing<R> = editing === true ? {} : editing;\n return (\n <RowName row={row}>\n {(name) => (\n <RoleSelect<R>\n size=\"sm\"\n name={name}\n value={held[0] ?? null}\n roles={roles}\n offered={rules.offered}\n disabledReason={rules.disabledReason}\n optionDisabledReason={rules.optionDisabledReason}\n onChange={(role) => onRoleChange(row, role)}\n />\n )}\n </RowName>\n );\n }\n if (held.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {held.map((role) => (\n <RoleChip<R> key={role} value={role} roles={roles} size=\"xs\" />\n ))}\n </div>\n );\n },\n sortBy: roleWords,\n filter: {\n type: \"select\",\n getValue: (row) => rolesOf(row, roleOf)[0] ?? \"\",\n options: (Object.keys(roles) as R[]).map((value) => ({ value, label: roles[value].label })),\n },\n mobile: \"secondary\",\n },\n {\n key: \"state\",\n header: labels.state,\n // No states, no cell: the phone card then shows no chip beside the name.\n cell: (row) => {\n const states = stateOf(row);\n return states.length === 0 ? null : <StateChips states={states} />;\n },\n // Beside the name on the phone card, so the role, last login and the actions keep\n // the card's three fields.\n role: \"status\",\n },\n dateColumn<T>({\n key: \"created\",\n header: labels.created,\n value: (row) => row.created_at ?? null,\n column: { mobileHidden: true },\n }),\n // How long ago rather than a date — the question is \"recently?\" — with the exact\n // time beside it (keksdose's last-seen column, Kurvenschmiede's; 0.32 §4: never only\n // in a tooltip).\n dateColumn<T>({\n key: \"last_login\",\n header: labels.lastLogin,\n value: (row) => row.last_login_at ?? null,\n display: \"relative\",\n empty: labels.never,\n column: { mobile: \"secondary\" },\n }),\n ...extra,\n );\n if (actions) {\n columns.push({\n key: \"actions\",\n // On screen, as every actions column's heading is (0.46.1, keksdose dev#598 rework).\n header:\n actionsHeader === false ? <span className=\"sr-only\">{labels.actions}</span> : (actionsHeader ?? labels.actions),\n headerText: labels.actions,\n // The caller's own words only: the menu reads the provider's itself, and the\n // English defaults merged in above must not shadow them.\n cell: (row) => (\n <RowName row={row}>\n {(name) => <UserRowActions actions={actions(row)} name={name} labels={labelsProp} />}\n </RowName>\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px text-end\",\n // The menu button keeps its own click: a row link must not wrap it.\n noRowLink: true,\n // Always on the phone card, after the fields and never one of the three (0.49).\n role: \"actions\",\n });\n }\n return columns;\n}\n\n/**\n * {@link userRosterColumns} with the `userRoster` words from the provider, memoised on\n * the options — pass stable callbacks (`useCallback`) and a module-level vocabulary, or\n * the columns are rebuilt on every render.\n */\nexport function useUserRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf,\n extra,\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return useMemo(\n () =>\n userRosterColumns<T, R>({\n roles,\n stateOf,\n extra,\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels,\n }),\n [roles, stateOf, extra, actions, actionsHeader, editableRole, onRoleChange, roleOf, emailColumn, labels],\n );\n}\n"],"mappings":"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","names":[]}
@@ -51,7 +51,7 @@ function AcceptInvitation({
51
51
  tone: "success",
52
52
  icon: /* @__PURE__ */ jsx(CheckCircle2, {}),
53
53
  title: settled?.name ? labels.joined(settled.name) : labels.accepted,
54
- action: continueButton("brand"),
54
+ action: continueButton("accent"),
55
55
  className: OUTCOME_CLASS
56
56
  }
57
57
  );
@@ -63,7 +63,7 @@ function AcceptInvitation({
63
63
  title: labels.signInFirst,
64
64
  hint: labels.signInHint,
65
65
  action: /* @__PURE__ */ jsxs(Fragment, { children: [
66
- /* @__PURE__ */ jsx(Button, { href: signInHref, variant: "brand", children: labels.signIn }),
66
+ /* @__PURE__ */ jsx(Button, { href: signInHref, variant: "accent", children: labels.signIn }),
67
67
  registerHref !== void 0 && /* @__PURE__ */ jsx(Button, { href: registerHref, variant: "secondary", children: labels.register })
68
68
  ] }),
69
69
  className: OUTCOME_CLASS
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/accept-invitation.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CheckCircle2, LogIn, TimerOff, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button } from \"../components/button\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { authErrorCode } from \"./auth-errors\";\nimport { OUTCOME_CLASS, STATUS_HEADING_CLASS, useOncePerKey } from \"./status-parts\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_ACCEPT_INVITATION_LABELS } from \"./accept-invitation-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the page renders — the `acceptInvitation` namespace. keksdose's words\n * where its `/join` page has them (`budget_share.join*`). */\nexport interface AcceptInvitationLabels {\n /** The heading. */\n title: string;\n /** While the invitation is accepted. */\n accepting: string;\n /** Accepted, when the app did not say into what. */\n accepted: string;\n /** Accepted, given what it attached the account to — a company, a team, a budget. */\n joined: (name: string) => string;\n /** Into the app, from every outcome but \"sign in first\". */\n continue: string;\n /** The token is unknown, spent, or for another address. */\n invalid: string;\n expired: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** Under `invalid` and `expired`: an invitation is re-sent as a new one (§4.4). */\n askAgain: string;\n /** Nobody is signed in: an existing account accepts after sign-in (§4.4). */\n signInFirst: string;\n /** Under it. */\n signInHint: string;\n signIn: string;\n /** Beside \"Sign in\", with `registerHref`. */\n register: string;\n}\n\n// Defined in ./accept-invitation-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ACCEPT_INVITATION_LABELS };\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\n/** What `onAccept` may resolve with. */\nexport interface AcceptInvitationResult {\n /** What the invitation attached the account to, by name — \"Example Property Ltd\",\n * \"Team North\", a budget. Said in the confirmation. */\n name?: string;\n}\n\n/** What became of an invitation that was not accepted. `signIn`: the server wants a\n * session first (a `401`), which `signedIn` did not know about. */\nexport type AcceptInvitationFailure = \"invalid\" | \"expired\" | \"signIn\";\n\ntype Outcome = \"accepted\" | AcceptInvitationFailure;\n\nexport interface AcceptInvitationProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The invitation's token — the app's `?invite=` / `?token=`. Empty: the link was cut\n * short, and the page says so without asking anyone. */\n token: string;\n /** Someone is signed in on this device. Until then the page asks for a sign-in and\n * sends nothing: accepting attaches the invitation to the SIGNED-IN account. */\n signedIn: boolean;\n /**\n * Accept it — `POST /auth/invitations/accept {token}`, once per token, as soon as\n * someone is signed in. Resolve (with what it attached the account to, if the app\n * knows); reject with the client's error.\n */\n onAccept: (token: string) => Promise<AcceptInvitationResult | void | undefined>;\n /** Which dead end a rejection is. Default: `invitation_expired` is `expired`,\n * anything else `invalid` (§5.2's codes). Map a `401` to `signIn` here. */\n classifyError?: (error: unknown) => AcceptInvitationFailure;\n /** \"Continue\" — into the app. Default `/`. */\n continueHref?: string;\n /** \"Sign in\" — the app's `/login`, with a way back here (`?next=…`). Default\n * `/login`. */\n signInHref?: string;\n /** \"Create account\" beside \"Sign in\", for an invitation that may lead to a new\n * account — the app's `/register?invite=…`. Left out, none. */\n registerHref?: string;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it in\n * `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<AcceptInvitationLabels>;\n}\n\n/**\n * Accept an invitation with an EXISTING account (docs/auth-harmonization.md §4.4): the\n * link leads here, the person signs in if they are not, and the app attaches the\n * invitation — a kastlan user joining a second company, a Kurvenschmiede member a team,\n * a keksdose user a shared budget. A new address never needs this page: its link opens\n * the sign-up form, which spends the invitation itself.\n *\n * keksdose's `JoinSharePage` (features/budgets/join-page.tsx) is the model: it accepts\n * on load, once (the token is single-use, and StrictMode's double effect would send it\n * twice), and shows the outcome where its spinner was. The outcomes: accepted, with the\n * way into the app; not valid; expired (both say to ask for a new invitation, since\n * \"resend\" mints a new token); or \"sign in first\", with nothing sent. \"Continue\" REPLACES\n * this page in the history: the token is spent, so Back must not land here again.\n */\nexport function AcceptInvitation({\n token,\n signedIn,\n onAccept,\n classifyError,\n continueHref = \"/\",\n signInHref = \"/login\",\n registerHref,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: AcceptInvitationProps) {\n const labels = useKitLabels(\"acceptInvitation\", DEFAULT_ACCEPT_INVITATION_LABELS, labelsProp);\n const [settled, setSettled] = useState<{ token: string; outcome: Outcome; name?: string } | null>(null);\n const { announce, regionProps } = useAnnounce();\n\n useOncePerKey(token !== \"\" && signedIn ? token : null, onAccept, (key, result) => {\n if (result.ok) {\n const name = result.value?.name?.trim() || undefined;\n setSettled({ token: key, outcome: \"accepted\", name });\n announce(name ? labels.joined(name) : labels.accepted);\n return;\n }\n const failure: AcceptInvitationFailure =\n classifyError?.(result.error) ?? (authErrorCode(result.error) === \"invitation_expired\" ? \"expired\" : \"invalid\");\n setSettled({ token: key, outcome: failure });\n announce(failure === \"signIn\" ? labels.signInFirst : labels[failure]);\n });\n\n const outcome: Outcome | \"accepting\" =\n token === \"\"\n ? \"invalid\"\n : settled?.token === token\n ? settled.outcome\n : signedIn\n ? \"accepting\"\n : \"signIn\";\n\n const continueButton = (variant: \"brand\" | \"secondary\") => (\n <Button href={continueHref} replace variant={variant}>\n {labels.continue}\n </Button>\n );\n\n let body: ReactNode;\n if (outcome === \"accepting\") {\n body = <LoadingState size=\"sm\" label={labels.accepting} />;\n } else if (outcome === \"accepted\") {\n body = (\n <EmptyState\n tone=\"success\"\n icon={<CheckCircle2 />}\n title={settled?.name ? labels.joined(settled.name) : labels.accepted}\n action={continueButton(\"brand\")}\n className={OUTCOME_CLASS}\n />\n );\n } else if (outcome === \"signIn\") {\n body = (\n <EmptyState\n icon={<LogIn />}\n title={labels.signInFirst}\n hint={labels.signInHint}\n action={\n <>\n <Button href={signInHref} variant=\"brand\">\n {labels.signIn}\n </Button>\n {registerHref !== undefined && (\n <Button href={registerHref} variant=\"secondary\">\n {labels.register}\n </Button>\n )}\n </>\n }\n className={OUTCOME_CLASS}\n />\n );\n } else {\n body = (\n <EmptyState\n tone=\"danger\"\n icon={outcome === \"expired\" ? <TimerOff /> : <XCircle />}\n title={token === \"\" ? labels.noToken : labels[outcome]}\n hint={labels.askAgain}\n action={signedIn ? continueButton(\"secondary\") : undefined}\n className={OUTCOME_CLASS}\n />\n );\n }\n\n return (\n <div\n {...rest}\n data-state={outcome}\n aria-busy={outcome === \"accepting\" || undefined}\n className={cn(\"space-y-3\", className)}\n >\n {title === null ? null : <Heading className={STATUS_HEADING_CLASS}>{title ?? labels.title}</Heading>}\n {body}\n {/* Rendered from the start, so the outcome is read when it arrives. */}\n <div {...regionProps} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/auth/accept-invitation.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CheckCircle2, LogIn, TimerOff, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button } from \"../components/button\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { authErrorCode } from \"./auth-errors\";\nimport { OUTCOME_CLASS, STATUS_HEADING_CLASS, useOncePerKey } from \"./status-parts\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_ACCEPT_INVITATION_LABELS } from \"./accept-invitation-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the page renders — the `acceptInvitation` namespace. keksdose's words\n * where its `/join` page has them (`budget_share.join*`). */\nexport interface AcceptInvitationLabels {\n /** The heading. */\n title: string;\n /** While the invitation is accepted. */\n accepting: string;\n /** Accepted, when the app did not say into what. */\n accepted: string;\n /** Accepted, given what it attached the account to — a company, a team, a budget. */\n joined: (name: string) => string;\n /** Into the app, from every outcome but \"sign in first\". */\n continue: string;\n /** The token is unknown, spent, or for another address. */\n invalid: string;\n expired: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** Under `invalid` and `expired`: an invitation is re-sent as a new one (§4.4). */\n askAgain: string;\n /** Nobody is signed in: an existing account accepts after sign-in (§4.4). */\n signInFirst: string;\n /** Under it. */\n signInHint: string;\n signIn: string;\n /** Beside \"Sign in\", with `registerHref`. */\n register: string;\n}\n\n// Defined in ./accept-invitation-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ACCEPT_INVITATION_LABELS };\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\n/** What `onAccept` may resolve with. */\nexport interface AcceptInvitationResult {\n /** What the invitation attached the account to, by name — \"Example Property Ltd\",\n * \"Team North\", a budget. Said in the confirmation. */\n name?: string;\n}\n\n/** What became of an invitation that was not accepted. `signIn`: the server wants a\n * session first (a `401`), which `signedIn` did not know about. */\nexport type AcceptInvitationFailure = \"invalid\" | \"expired\" | \"signIn\";\n\ntype Outcome = \"accepted\" | AcceptInvitationFailure;\n\nexport interface AcceptInvitationProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The invitation's token — the app's `?invite=` / `?token=`. Empty: the link was cut\n * short, and the page says so without asking anyone. */\n token: string;\n /** Someone is signed in on this device. Until then the page asks for a sign-in and\n * sends nothing: accepting attaches the invitation to the SIGNED-IN account. */\n signedIn: boolean;\n /**\n * Accept it — `POST /auth/invitations/accept {token}`, once per token, as soon as\n * someone is signed in. Resolve (with what it attached the account to, if the app\n * knows); reject with the client's error.\n */\n onAccept: (token: string) => Promise<AcceptInvitationResult | void | undefined>;\n /** Which dead end a rejection is. Default: `invitation_expired` is `expired`,\n * anything else `invalid` (§5.2's codes). Map a `401` to `signIn` here. */\n classifyError?: (error: unknown) => AcceptInvitationFailure;\n /** \"Continue\" — into the app. Default `/`. */\n continueHref?: string;\n /** \"Sign in\" — the app's `/login`, with a way back here (`?next=…`). Default\n * `/login`. */\n signInHref?: string;\n /** \"Create account\" beside \"Sign in\", for an invitation that may lead to a new\n * account — the app's `/register?invite=…`. Left out, none. */\n registerHref?: string;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it in\n * `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<AcceptInvitationLabels>;\n}\n\n/**\n * Accept an invitation with an EXISTING account (docs/auth-harmonization.md §4.4): the\n * link leads here, the person signs in if they are not, and the app attaches the\n * invitation — a kastlan user joining a second company, a Kurvenschmiede member a team,\n * a keksdose user a shared budget. A new address never needs this page: its link opens\n * the sign-up form, which spends the invitation itself.\n *\n * keksdose's `JoinSharePage` (features/budgets/join-page.tsx) is the model: it accepts\n * on load, once (the token is single-use, and StrictMode's double effect would send it\n * twice), and shows the outcome where its spinner was. The outcomes: accepted, with the\n * way into the app; not valid; expired (both say to ask for a new invitation, since\n * \"resend\" mints a new token); or \"sign in first\", with nothing sent. \"Continue\" REPLACES\n * this page in the history: the token is spent, so Back must not land here again.\n */\nexport function AcceptInvitation({\n token,\n signedIn,\n onAccept,\n classifyError,\n continueHref = \"/\",\n signInHref = \"/login\",\n registerHref,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: AcceptInvitationProps) {\n const labels = useKitLabels(\"acceptInvitation\", DEFAULT_ACCEPT_INVITATION_LABELS, labelsProp);\n const [settled, setSettled] = useState<{ token: string; outcome: Outcome; name?: string } | null>(null);\n const { announce, regionProps } = useAnnounce();\n\n useOncePerKey(token !== \"\" && signedIn ? token : null, onAccept, (key, result) => {\n if (result.ok) {\n const name = result.value?.name?.trim() || undefined;\n setSettled({ token: key, outcome: \"accepted\", name });\n announce(name ? labels.joined(name) : labels.accepted);\n return;\n }\n const failure: AcceptInvitationFailure =\n classifyError?.(result.error) ?? (authErrorCode(result.error) === \"invitation_expired\" ? \"expired\" : \"invalid\");\n setSettled({ token: key, outcome: failure });\n announce(failure === \"signIn\" ? labels.signInFirst : labels[failure]);\n });\n\n const outcome: Outcome | \"accepting\" =\n token === \"\"\n ? \"invalid\"\n : settled?.token === token\n ? settled.outcome\n : signedIn\n ? \"accepting\"\n : \"signIn\";\n\n const continueButton = (variant: \"accent\" | \"secondary\") => (\n <Button href={continueHref} replace variant={variant}>\n {labels.continue}\n </Button>\n );\n\n let body: ReactNode;\n if (outcome === \"accepting\") {\n body = <LoadingState size=\"sm\" label={labels.accepting} />;\n } else if (outcome === \"accepted\") {\n body = (\n <EmptyState\n tone=\"success\"\n icon={<CheckCircle2 />}\n title={settled?.name ? labels.joined(settled.name) : labels.accepted}\n action={continueButton(\"accent\")}\n className={OUTCOME_CLASS}\n />\n );\n } else if (outcome === \"signIn\") {\n body = (\n <EmptyState\n icon={<LogIn />}\n title={labels.signInFirst}\n hint={labels.signInHint}\n action={\n <>\n <Button href={signInHref} variant=\"accent\">\n {labels.signIn}\n </Button>\n {registerHref !== undefined && (\n <Button href={registerHref} variant=\"secondary\">\n {labels.register}\n </Button>\n )}\n </>\n }\n className={OUTCOME_CLASS}\n />\n );\n } else {\n body = (\n <EmptyState\n tone=\"danger\"\n icon={outcome === \"expired\" ? <TimerOff /> : <XCircle />}\n title={token === \"\" ? labels.noToken : labels[outcome]}\n hint={labels.askAgain}\n action={signedIn ? continueButton(\"secondary\") : undefined}\n className={OUTCOME_CLASS}\n />\n );\n }\n\n return (\n <div\n {...rest}\n data-state={outcome}\n aria-busy={outcome === \"accepting\" || undefined}\n className={cn(\"space-y-3\", className)}\n >\n {title === null ? null : <Heading className={STATUS_HEADING_CLASS}>{title ?? labels.title}</Heading>}\n {body}\n {/* Rendered from the start, so the outcome is read when it arrives. */}\n <div {...regionProps} />\n </div>\n );\n}\n"],"mappings":";AAsJI,SAyBM,UAzBN,KAyBM,YAzBN;AAtJJ,SAAS,gBAAgB;AAEzB,SAAS,cAAc,OAAO,UAAU,eAAe;AACvD,SAAS,UAAU;AACnB,SAAS,oBAAwC;AACjD,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,eAAe,sBAAsB,qBAAqB;AAEnE,SAAS,wCAAwC;AAiG1C,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAA0B;AACxB,QAAM,SAAS,aAAa,oBAAoB,kCAAkC,UAAU;AAC5F,QAAM,CAAC,SAAS,UAAU,IAAI,SAAoE,IAAI;AACtG,QAAM,EAAE,UAAU,YAAY,IAAI,YAAY;AAE9C,gBAAc,UAAU,MAAM,WAAW,QAAQ,MAAM,UAAU,CAAC,KAAK,WAAW;AAChF,QAAI,OAAO,IAAI;AACb,YAAM,OAAO,OAAO,OAAO,MAAM,KAAK,KAAK;AAC3C,iBAAW,EAAE,OAAO,KAAK,SAAS,YAAY,KAAK,CAAC;AACpD,eAAS,OAAO,OAAO,OAAO,IAAI,IAAI,OAAO,QAAQ;AACrD;AAAA,IACF;AACA,UAAM,UACJ,gBAAgB,OAAO,KAAK,MAAM,cAAc,OAAO,KAAK,MAAM,uBAAuB,YAAY;AACvG,eAAW,EAAE,OAAO,KAAK,SAAS,QAAQ,CAAC;AAC3C,aAAS,YAAY,WAAW,OAAO,cAAc,OAAO,OAAO,CAAC;AAAA,EACtE,CAAC;AAED,QAAM,UACJ,UAAU,KACN,YACA,SAAS,UAAU,QACjB,QAAQ,UACR,WACE,cACA;AAEV,QAAM,iBAAiB,CAAC,YACtB,oBAAC,UAAO,MAAM,cAAc,SAAO,MAAC,SACjC,iBAAO,UACV;AAGF,MAAI;AACJ,MAAI,YAAY,aAAa;AAC3B,WAAO,oBAAC,gBAAa,MAAK,MAAK,OAAO,OAAO,WAAW;AAAA,EAC1D,WAAW,YAAY,YAAY;AACjC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,MAAM,oBAAC,gBAAa;AAAA,QACpB,OAAO,SAAS,OAAO,OAAO,OAAO,QAAQ,IAAI,IAAI,OAAO;AAAA,QAC5D,QAAQ,eAAe,QAAQ;AAAA,QAC/B,WAAW;AAAA;AAAA,IACb;AAAA,EAEJ,WAAW,YAAY,UAAU;AAC/B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,oBAAC,SAAM;AAAA,QACb,OAAO,OAAO;AAAA,QACd,MAAM,OAAO;AAAA,QACb,QACE,iCACE;AAAA,8BAAC,UAAO,MAAM,YAAY,SAAQ,UAC/B,iBAAO,QACV;AAAA,UACC,iBAAiB,UAChB,oBAAC,UAAO,MAAM,cAAc,SAAQ,aACjC,iBAAO,UACV;AAAA,WAEJ;AAAA,QAEF,WAAW;AAAA;AAAA,IACb;AAAA,EAEJ,OAAO;AACL,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,MAAM,YAAY,YAAY,oBAAC,YAAS,IAAK,oBAAC,WAAQ;AAAA,QACtD,OAAO,UAAU,KAAK,OAAO,UAAU,OAAO,OAAO;AAAA,QACrD,MAAM,OAAO;AAAA,QACb,QAAQ,WAAW,eAAe,WAAW,IAAI;AAAA,QACjD,WAAW;AAAA;AAAA,IACb;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,cAAY;AAAA,MACZ,aAAW,YAAY,eAAe;AAAA,MACtC,WAAW,GAAG,aAAa,SAAS;AAAA,MAEnC;AAAA,kBAAU,OAAO,OAAO,oBAAC,WAAQ,WAAW,sBAAuB,mBAAS,OAAO,OAAM;AAAA,QACzF;AAAA,QAED,oBAAC,SAAK,GAAG,aAAa;AAAA;AAAA;AAAA,EACxB;AAEJ;","names":[]}
@@ -23,7 +23,7 @@ function NotFoundPage({
23
23
  /* @__PURE__ */ jsx("p", { className: "max-w-prose text-sm text-secondary", children: labels.body }),
24
24
  children,
25
25
  /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-wrap justify-center gap-3", children: [
26
- /* @__PURE__ */ jsx(Button, { href: homeHref, variant: "brand", children: labels.home }),
26
+ /* @__PURE__ */ jsx(Button, { href: homeHref, variant: "accent", children: labels.home }),
27
27
  appHref !== void 0 && /* @__PURE__ */ jsx(Button, { href: appHref, variant: "secondary", children: labels.app })
28
28
  ] })
29
29
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/not-found-page.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useNoIndex } from \"../hooks/use-noindex\";\nimport { Button } from \"../components/button\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_NOT_FOUND_LABELS } from \"./not-found-page-labels\";\n\n/** Every string the page renders — the `notFound` namespace. keksdose's words\n * (`error.not_found_*`), which kastlan took over. */\nexport interface NotFoundLabels {\n /** The heading. */\n title: string;\n /** Under it. */\n body: string;\n /** The way out for everyone — to `homeHref`. */\n home: string;\n /** The second way out for someone signed in — to `appHref`. */\n app: string;\n}\n\n// Defined in ./not-found-page-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_NOT_FOUND_LABELS };\n\nexport interface NotFoundPageProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** \"Go to the start page\". Default `/`, which takes a signed-in user into the app and\n * anyone else to sign-in in all three apps. */\n homeHref?: string;\n /**\n * \"Back to the app\", beside it — pass it while someone is signed in. A LITERAL page\n * (keksdose's `/budget`), never the \"last visited page\" an app restores on start: a\n * stale one of those pointing at a route that is gone is the likeliest way a\n * signed-in user lands here (keksdose's not-found-page.tsx).\n */\n appHref?: string;\n /** App content under the text — a search field, a support address. */\n children?: ReactNode;\n /** `false` adds no `noindex` — for a preview inside another page. Default `true`. */\n noIndex?: boolean;\n /** The heading's level. Default `h2`, under `AuthLayout`'s `h1`; `h1` where the page\n * stands alone (inside the app shell). */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<NotFoundLabels>;\n}\n\n/**\n * What an unknown address renders (the router's catch-all), signed out and signed in —\n * keksdose's `NotFoundPage` (app/not-found-page.tsx, feedback #129), which kastlan took\n * over on `AuthLayout` (docs/auth-harmonization.md §7).\n *\n * In place of the silent `<Navigate to=\"/\">` keksdose had: bouncing every typo, dead link\n * and fabricated URL to the start page is what Search Console reports as a soft 404, and\n * it hid broken links. An SPA cannot send a real 404 status — the web server answers\n * every path with index.html — so the page says it in words and calls\n * {@link useNoIndex}, which makes a crawler drop it (the server's `X-Robots-Tag` is the\n * authoritative half).\n *\n * It is the page's CONTENT, not a frame, so it sits in either: as `AuthLayout`'s child\n * for a visitor (`headingAs` default `h2`, under the layout's title), or bare inside the\n * app shell for someone signed in (`headingAs=\"h1\"`, and `appHref`). The large \"404\" is\n * decoration — the heading says it in words — and hidden from a screen reader.\n */\nexport function NotFoundPage({\n homeHref = \"/\",\n appHref,\n children,\n noIndex = true,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: NotFoundPageProps) {\n const labels = useKitLabels(\"notFound\", DEFAULT_NOT_FOUND_LABELS, labelsProp);\n useNoIndex(noIndex);\n return (\n <div {...rest} className={cn(\"flex flex-col items-center gap-2 py-4 text-center\", className)}>\n <p aria-hidden className=\"text-6xl font-bold leading-none tracking-tight text-placeholder\">\n 404\n </p>\n <Heading className=\"mt-2 text-xl font-semibold text-primary\">{labels.title}</Heading>\n <p className=\"max-w-prose text-sm text-secondary\">{labels.body}</p>\n {children}\n <div className=\"mt-4 flex flex-wrap justify-center gap-3\">\n <Button href={homeHref} variant=\"brand\">\n {labels.home}\n </Button>\n {appHref !== undefined && (\n <Button href={appHref} variant=\"secondary\">\n {labels.app}\n </Button>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AA4EM,cAMA,YANA;AA3EN,SAAS,UAAU;AACnB,SAAS,oBAAwC;AACjD,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AAEvB,SAAS,gCAAgC;AAwDlC,SAAS,aAAa;AAAA,EAC3B,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,WAAW,UAAU;AAAA,EACrB,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,SAAS,aAAa,YAAY,0BAA0B,UAAU;AAC5E,aAAW,OAAO;AAClB,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,qDAAqD,SAAS,GACzF;AAAA,wBAAC,OAAE,eAAW,MAAC,WAAU,mEAAkE,iBAE3F;AAAA,IACA,oBAAC,WAAQ,WAAU,2CAA2C,iBAAO,OAAM;AAAA,IAC3E,oBAAC,OAAE,WAAU,sCAAsC,iBAAO,MAAK;AAAA,IAC9D;AAAA,IACD,qBAAC,SAAI,WAAU,4CACb;AAAA,0BAAC,UAAO,MAAM,UAAU,SAAQ,SAC7B,iBAAO,MACV;AAAA,MACC,YAAY,UACX,oBAAC,UAAO,MAAM,SAAS,SAAQ,aAC5B,iBAAO,KACV;AAAA,OAEJ;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/auth/not-found-page.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useNoIndex } from \"../hooks/use-noindex\";\nimport { Button } from \"../components/button\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_NOT_FOUND_LABELS } from \"./not-found-page-labels\";\n\n/** Every string the page renders — the `notFound` namespace. keksdose's words\n * (`error.not_found_*`), which kastlan took over. */\nexport interface NotFoundLabels {\n /** The heading. */\n title: string;\n /** Under it. */\n body: string;\n /** The way out for everyone — to `homeHref`. */\n home: string;\n /** The second way out for someone signed in — to `appHref`. */\n app: string;\n}\n\n// Defined in ./not-found-page-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_NOT_FOUND_LABELS };\n\nexport interface NotFoundPageProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** \"Go to the start page\". Default `/`, which takes a signed-in user into the app and\n * anyone else to sign-in in all three apps. */\n homeHref?: string;\n /**\n * \"Back to the app\", beside it — pass it while someone is signed in. A LITERAL page\n * (keksdose's `/budget`), never the \"last visited page\" an app restores on start: a\n * stale one of those pointing at a route that is gone is the likeliest way a\n * signed-in user lands here (keksdose's not-found-page.tsx).\n */\n appHref?: string;\n /** App content under the text — a search field, a support address. */\n children?: ReactNode;\n /** `false` adds no `noindex` — for a preview inside another page. Default `true`. */\n noIndex?: boolean;\n /** The heading's level. Default `h2`, under `AuthLayout`'s `h1`; `h1` where the page\n * stands alone (inside the app shell). */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<NotFoundLabels>;\n}\n\n/**\n * What an unknown address renders (the router's catch-all), signed out and signed in —\n * keksdose's `NotFoundPage` (app/not-found-page.tsx, feedback #129), which kastlan took\n * over on `AuthLayout` (docs/auth-harmonization.md §7).\n *\n * In place of the silent `<Navigate to=\"/\">` keksdose had: bouncing every typo, dead link\n * and fabricated URL to the start page is what Search Console reports as a soft 404, and\n * it hid broken links. An SPA cannot send a real 404 status — the web server answers\n * every path with index.html — so the page says it in words and calls\n * {@link useNoIndex}, which makes a crawler drop it (the server's `X-Robots-Tag` is the\n * authoritative half).\n *\n * It is the page's CONTENT, not a frame, so it sits in either: as `AuthLayout`'s child\n * for a visitor (`headingAs` default `h2`, under the layout's title), or bare inside the\n * app shell for someone signed in (`headingAs=\"h1\"`, and `appHref`). The large \"404\" is\n * decoration — the heading says it in words — and hidden from a screen reader.\n */\nexport function NotFoundPage({\n homeHref = \"/\",\n appHref,\n children,\n noIndex = true,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: NotFoundPageProps) {\n const labels = useKitLabels(\"notFound\", DEFAULT_NOT_FOUND_LABELS, labelsProp);\n useNoIndex(noIndex);\n return (\n <div {...rest} className={cn(\"flex flex-col items-center gap-2 py-4 text-center\", className)}>\n <p aria-hidden className=\"text-6xl font-bold leading-none tracking-tight text-placeholder\">\n 404\n </p>\n <Heading className=\"mt-2 text-xl font-semibold text-primary\">{labels.title}</Heading>\n <p className=\"max-w-prose text-sm text-secondary\">{labels.body}</p>\n {children}\n <div className=\"mt-4 flex flex-wrap justify-center gap-3\">\n <Button href={homeHref} variant=\"accent\">\n {labels.home}\n </Button>\n {appHref !== undefined && (\n <Button href={appHref} variant=\"secondary\">\n {labels.app}\n </Button>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AA4EM,cAMA,YANA;AA3EN,SAAS,UAAU;AACnB,SAAS,oBAAwC;AACjD,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AAEvB,SAAS,gCAAgC;AAwDlC,SAAS,aAAa;AAAA,EAC3B,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,WAAW,UAAU;AAAA,EACrB,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,SAAS,aAAa,YAAY,0BAA0B,UAAU;AAC5E,aAAW,OAAO;AAClB,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,qDAAqD,SAAS,GACzF;AAAA,wBAAC,OAAE,eAAW,MAAC,WAAU,mEAAkE,iBAE3F;AAAA,IACA,oBAAC,WAAQ,WAAU,2CAA2C,iBAAO,OAAM;AAAA,IAC3E,oBAAC,OAAE,WAAU,sCAAsC,iBAAO,MAAK;AAAA,IAC9D;AAAA,IACD,qBAAC,SAAI,WAAU,4CACb;AAAA,0BAAC,UAAO,MAAM,UAAU,SAAQ,UAC7B,iBAAO,MACV;AAAA,MACC,YAAY,UACX,oBAAC,UAAO,MAAM,SAAS,SAAQ,aAC5B,iBAAO,KACV;AAAA,OAEJ;AAAA,KACF;AAEJ;","names":[]}
@@ -76,7 +76,7 @@ function ResetPasswordForm({
76
76
  icon: /* @__PURE__ */ jsx(XCircle, {}),
77
77
  title: token === "" ? labels.noToken : labels.invalid,
78
78
  hint: labels.invalidHint,
79
- action: /* @__PURE__ */ jsx(Button, { href: forgotHref, variant: "brand", children: labels.requestNew }),
79
+ action: /* @__PURE__ */ jsx(Button, { href: forgotHref, variant: "accent", children: labels.requestNew }),
80
80
  className: OUTCOME_CLASS
81
81
  }
82
82
  )
@@ -89,7 +89,7 @@ function ResetPasswordForm({
89
89
  /* @__PURE__ */ jsx(OutcomeMessage, { icon: /* @__PURE__ */ jsx(CheckCircle2, { className: "text-success" }), focusRef: doneRef, children: /* @__PURE__ */ jsx("p", { className: INTRO_CLASS, children: labels.success }) }),
90
90
  notices2,
91
91
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted", children: labels.sessionsEnded }),
92
- onSignIn ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "brand", className: "w-full", onClick: () => onSignIn(email2), children: labels.signIn }) : /* @__PURE__ */ jsx(Button, { href: email2 ? withQuery(signInHref, "email", email2) : signInHref, variant: "brand", className: "w-full", children: labels.signIn })
92
+ onSignIn ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "accent", className: "w-full", onClick: () => onSignIn(email2), children: labels.signIn }) : /* @__PURE__ */ jsx(Button, { href: email2 ? withQuery(signInHref, "email", email2) : signInHref, variant: "accent", className: "w-full", children: labels.signIn })
93
93
  ] });
94
94
  }
95
95
  const { email, notices } = current.check;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/reset-password-form.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, FormEvent, ReactNode } from \"react\";\nimport { CheckCircle2, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button } from \"../components/button\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { authErrorCode, isRateLimited, retryAfterSeconds } from \"./auth-errors\";\nimport { newPasswordProblem } from \"./form-rules\";\nimport { NewPasswordFields } from \"./new-password-fields\";\nimport {\n FORM_HEADING_CLASS,\n INTRO_CLASS,\n OUTCOME_CLASS,\n OutcomeMessage,\n useFocusWhen,\n useOncePerKey,\n withQuery,\n} from \"./status-parts\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_RESET_PASSWORD_LABELS } from \"./reset-password-form-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the form renders — the `resetPassword` namespace. keksdose's words\n * (`auth.reset.*`), which kastlan took over. */\nexport interface ResetPasswordLabels {\n /** The heading, on every state. */\n title: string;\n /** While the link is checked. */\n checking: string;\n /** Over the fields, given the account's address (when the check returned one). */\n intro: (email: string) => string;\n newPassword: string;\n confirmPassword: string;\n /** Under the confirmation while it differs. */\n mismatch: string;\n submit: string;\n /** The new password was not saved, for a reason that is not the link. */\n failed: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** The link is unknown, spent or expired — one sentence, as the server gives one\n * code (`token_invalid`). */\n invalid: string;\n /** Under it. */\n invalidHint: string;\n /** The way forward from an invalid link: back to \"Forgot password\". */\n requestNew: string;\n /** Done. */\n success: string;\n /** Under it: a reset ends every session (§6.3). */\n sessionsEnded: string;\n /** The button out — to sign-in, with the address filled in. */\n signIn: string;\n /** 0.30.0: the save was throttled — HTTP 429 ({@link isRateLimited}) — given the\n * `Retry-After` wait in seconds, or `undefined` without one. OPTIONAL, like every key\n * added to a shipped interface; the provider's `resetPassword`, then English, fill\n * it. */\n rateLimited?: (seconds?: number) => string;\n}\n\n// Defined in ./reset-password-form-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_RESET_PASSWORD_LABELS };\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\n/** What `onCheck` may resolve with — keksdose's `/password-reset/check` answer. */\nexport interface ResetPasswordCheck {\n /** The account's address: named over the fields (\"You're setting a new password for\n * …\") and handed to the password manager, which files the new password under it. */\n email?: string;\n /** The app's notices over the fields, shown BEFORE the password is typed — keksdose's\n * \"your account is end-to-end encrypted: this restores your sign-in, not your data\". */\n notices?: ReactNode;\n}\n\n/** What `onSubmit` hands over. */\nexport interface ResetPasswordValues {\n token: string;\n password: string;\n}\n\n/** What `onSubmit` resolves with — keksdose's `/password-reset/confirm` answer, which\n * carries no tokens (§6.3). */\nexport interface ResetPasswordResult {\n /** The account's address — the sign-in it leads to is filled in with it. */\n email: string;\n /** The app's outcomes, shown on the done state: keksdose's recovery-code notice for an\n * encrypted account, \"Your 2 personal access tokens were revoked\". */\n notices?: ReactNode;\n}\n\ntype Phase =\n | { kind: \"form\"; check: ResetPasswordCheck }\n | { kind: \"invalid\" }\n | { kind: \"done\"; result: ResetPasswordResult };\n\nexport interface ResetPasswordFormProps\n extends Omit<ComponentPropsWithRef<\"div\">, \"onSubmit\" | \"children\" | \"title\"> {\n /** The link's token — the app's `?token=` (keksdose, kastlan: `/reset-password?token=…`).\n * Empty: the link was cut short, and the page says so without asking anyone. */\n token: string;\n /**\n * Check the link before the form shows (`/auth/password-reset/check`) — once per\n * token, on mount. Checking does not use it up, so a reload is free. Resolve (with\n * the address and any notices) for a live link; reject for a dead one, and the page\n * says \"This link is invalid or has expired\" with the way to a new one.\n */\n onCheck: (token: string) => Promise<ResetPasswordCheck | void | undefined>;\n /**\n * The new password was submitted: send `/auth/password-reset/confirm`. Resolve with\n * the account's address (and the app's notices) — no tokens: a reset is not a sign-in\n * (§6.3). Reject with the client's error: `token_invalid` (the link expired or was\n * used in another tab while the form was open) goes to the invalid state; anything\n * else stays on the form with the password kept.\n */\n onSubmit: (values: ResetPasswordValues) => Promise<ResetPasswordResult>;\n /** \"Go to sign in\" calls this with the address, for an app that routes in code.\n * Without it, the button is a link to `signInHref`. */\n onSignIn?: (email: string) => void;\n /** Where \"Go to sign in\" leads, with `?email=` added — read it into `SignInForm`'s\n * `defaultEmail`. Default `/login`. */\n signInHref?: string;\n /** \"Request a new link\" — the app's \"Forgot password\" page. Default\n * `/forgot-password`. */\n forgotHref?: string;\n /** The app's words for a failed save (the device offline), or `undefined` for the\n * kit's: `rateLimited` for a bare `429` (0.30.0), else `failed`. Not asked for\n * `token_invalid` or `token_expired`, which are a state. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it\n * in `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<ResetPasswordLabels>;\n}\n\n/**\n * Redeem a password-reset link (`/reset-password?token=…`, docs/auth-harmonization.md\n * §6.3, §7) — keksdose's `ResetPasswordPage` (features/auth/reset-password-page.tsx),\n * which kastlan built its own on.\n *\n * **The link is checked first** (`onCheck`), and only then does the form appear: an\n * expired link should say so before someone types a new password twice, not after.\n *\n * **A reset is not a sign-in** (§6.3, §10.11). The confirm answers without tokens, so a\n * 2FA account cannot skip its second factor by way of its inbox, and the page ends on\n * \"Go to sign in\" with the address filled in — the person has just typed the new\n * password twice; signing in is one step. What else the reset did is said there:\n * every other session ended (the kit's line, true in all three apps), and the app's own\n * outcomes in `notices` — keksdose's recovery-code notice and its revoked API tokens.\n *\n * **The password** is chosen with the same fields as `RegisterForm` and `SignInForm`'s\n * set-password step ({@link NewPasswordFields}): the strength meter and its checklist as\n * advice, and only the two hard rules — at least 8 characters, at most 72 bytes (§8).\n * The submit waits for both and a matching confirmation.\n *\n * The form never sends a request itself (§8): the token and the password go to the\n * callbacks, and the page renders what they resolve with.\n */\nexport function ResetPasswordForm({\n token,\n onCheck,\n onSubmit,\n onSignIn,\n signInHref = \"/login\",\n forgotHref = \"/forgot-password\",\n describeError,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: ResetPasswordFormProps) {\n const labels = useKitLabels(\"resetPassword\", DEFAULT_RESET_PASSWORD_LABELS, labelsProp);\n // The phase belongs to the token it was reached with: a new token starts over at the\n // check without a reset effect.\n const [phase, setPhase] = useState<{ token: string; phase: Phase } | null>(null);\n const current: Phase | \"checking\" =\n token === \"\" ? { kind: \"invalid\" } : phase?.token === token ? phase.phase : \"checking\";\n\n useOncePerKey(token === \"\" ? null : token, onCheck, (key, result) =>\n setPhase({\n token: key,\n phase: result.ok ? { kind: \"form\", check: result.value ?? {} } : { kind: \"invalid\" },\n }),\n );\n\n const [password, setPassword] = useState(\"\");\n const [confirm, setConfirm] = useState(\"\");\n const [pending, setPending] = useState(false);\n const [failure, setFailure] = useState<ReactNode>(null);\n const headingId = useId();\n const passwordRef = useRef<HTMLInputElement>(null);\n const doneRef = useRef<HTMLDivElement>(null);\n const kind = current === \"checking\" ? \"checking\" : current.kind;\n\n // The form arrives after the check: the field is where the person is going.\n useEffect(() => {\n if (kind === \"form\") passwordRef.current?.focus();\n }, [kind]);\n useFocusWhen(kind === \"done\", doneRef);\n\n const heading =\n title === null ? null : (\n <Heading id={headingId} className={FORM_HEADING_CLASS}>\n {title ?? labels.title}\n </Heading>\n );\n const rootClass = cn(\"space-y-3\", className);\n\n if (current === \"checking\") {\n return (\n <div {...rest} data-state=\"checking\" aria-busy className={rootClass}>\n {heading}\n <LoadingState size=\"sm\" label={labels.checking} />\n </div>\n );\n }\n\n if (current.kind === \"invalid\") {\n return (\n <div {...rest} data-state=\"invalid\" className={rootClass}>\n {heading}\n <EmptyState\n tone=\"danger\"\n icon={<XCircle />}\n title={token === \"\" ? labels.noToken : labels.invalid}\n hint={labels.invalidHint}\n action={\n <Button href={forgotHref} variant=\"brand\">\n {labels.requestNew}\n </Button>\n }\n className={OUTCOME_CLASS}\n />\n </div>\n );\n }\n\n if (current.kind === \"done\") {\n const { email, notices } = current.result;\n return (\n <div {...rest} data-state=\"done\" className={rootClass}>\n {heading}\n <OutcomeMessage icon={<CheckCircle2 className=\"text-success\" />} focusRef={doneRef}>\n <p className={INTRO_CLASS}>{labels.success}</p>\n </OutcomeMessage>\n {notices}\n <p className=\"text-xs text-muted\">{labels.sessionsEnded}</p>\n {onSignIn ? (\n <Button type=\"button\" variant=\"brand\" className=\"w-full\" onClick={() => onSignIn(email)}>\n {labels.signIn}\n </Button>\n ) : (\n <Button href={email ? withQuery(signInHref, \"email\", email) : signInHref} variant=\"brand\" className=\"w-full\">\n {labels.signIn}\n </Button>\n )}\n </div>\n );\n }\n\n const { email, notices } = current.check;\n const problem = newPasswordProblem(password, confirm);\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n if (pending || problem !== null) return;\n setFailure(null);\n setPending(true);\n Promise.resolve()\n .then(() => onSubmit({ token, password }))\n .then(\n (result) => {\n setPending(false);\n setPassword(\"\");\n setConfirm(\"\");\n setPhase({ token, phase: { kind: \"done\", result } });\n },\n (error: unknown) => {\n setPending(false);\n // A link that died between the check and the save — used, or run out — is the\n // dead-link state, under either code (0.31.1: keksdose mapped `token_expired`\n // to it itself, `asDeadLink`).\n const code = authErrorCode(error);\n if (code === \"token_invalid\" || code === \"token_expired\") {\n setPassword(\"\");\n setConfirm(\"\");\n setPhase({ token, phase: { kind: \"invalid\" } });\n return;\n }\n // The app's words first, then the throttle's, then the kit's catch-all.\n setFailure(\n describeError?.(error) ??\n (isRateLimited(error)\n ? (labels.rateLimited ?? DEFAULT_RESET_PASSWORD_LABELS.rateLimited)(retryAfterSeconds(error))\n : labels.failed),\n );\n },\n );\n };\n\n return (\n <div {...rest} data-state=\"form\" className={rootClass}>\n <form className=\"space-y-3\" aria-labelledby={title === null ? undefined : headingId} onSubmit={submit}>\n {heading}\n {email ? <p className={INTRO_CLASS}>{labels.intro(email)}</p> : null}\n {notices}\n {/* Which account the new password is for, for the password manager: it files\n the new password under this username instead of guessing (kastlan). */}\n {email ? <input type=\"email\" name=\"username\" autoComplete=\"username\" value={email} readOnly hidden /> : null}\n <NewPasswordFields\n password={password}\n confirm={confirm}\n onPasswordChange={(next) => {\n setPassword(next);\n if (failure) setFailure(null);\n }}\n onConfirmChange={setConfirm}\n busy={pending}\n passwordRef={passwordRef}\n text={{ password: labels.newPassword, confirm: labels.confirmPassword, mismatch: labels.mismatch }}\n />\n {failure != null && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n <Button type=\"submit\" className=\"w-full\" disabled={!pending && problem !== null} pending={pending}>\n {labels.submit}\n </Button>\n </form>\n </div>\n );\n}\n"],"mappings":"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","names":["email","notices"]}
1
+ {"version":3,"sources":["../../src/auth/reset-password-form.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, FormEvent, ReactNode } from \"react\";\nimport { CheckCircle2, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button } from \"../components/button\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { authErrorCode, isRateLimited, retryAfterSeconds } from \"./auth-errors\";\nimport { newPasswordProblem } from \"./form-rules\";\nimport { NewPasswordFields } from \"./new-password-fields\";\nimport {\n FORM_HEADING_CLASS,\n INTRO_CLASS,\n OUTCOME_CLASS,\n OutcomeMessage,\n useFocusWhen,\n useOncePerKey,\n withQuery,\n} from \"./status-parts\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_RESET_PASSWORD_LABELS } from \"./reset-password-form-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the form renders — the `resetPassword` namespace. keksdose's words\n * (`auth.reset.*`), which kastlan took over. */\nexport interface ResetPasswordLabels {\n /** The heading, on every state. */\n title: string;\n /** While the link is checked. */\n checking: string;\n /** Over the fields, given the account's address (when the check returned one). */\n intro: (email: string) => string;\n newPassword: string;\n confirmPassword: string;\n /** Under the confirmation while it differs. */\n mismatch: string;\n submit: string;\n /** The new password was not saved, for a reason that is not the link. */\n failed: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** The link is unknown, spent or expired — one sentence, as the server gives one\n * code (`token_invalid`). */\n invalid: string;\n /** Under it. */\n invalidHint: string;\n /** The way forward from an invalid link: back to \"Forgot password\". */\n requestNew: string;\n /** Done. */\n success: string;\n /** Under it: a reset ends every session (§6.3). */\n sessionsEnded: string;\n /** The button out — to sign-in, with the address filled in. */\n signIn: string;\n /** 0.30.0: the save was throttled — HTTP 429 ({@link isRateLimited}) — given the\n * `Retry-After` wait in seconds, or `undefined` without one. OPTIONAL, like every key\n * added to a shipped interface; the provider's `resetPassword`, then English, fill\n * it. */\n rateLimited?: (seconds?: number) => string;\n}\n\n// Defined in ./reset-password-form-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_RESET_PASSWORD_LABELS };\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\n/** What `onCheck` may resolve with — keksdose's `/password-reset/check` answer. */\nexport interface ResetPasswordCheck {\n /** The account's address: named over the fields (\"You're setting a new password for\n * …\") and handed to the password manager, which files the new password under it. */\n email?: string;\n /** The app's notices over the fields, shown BEFORE the password is typed — keksdose's\n * \"your account is end-to-end encrypted: this restores your sign-in, not your data\". */\n notices?: ReactNode;\n}\n\n/** What `onSubmit` hands over. */\nexport interface ResetPasswordValues {\n token: string;\n password: string;\n}\n\n/** What `onSubmit` resolves with — keksdose's `/password-reset/confirm` answer, which\n * carries no tokens (§6.3). */\nexport interface ResetPasswordResult {\n /** The account's address — the sign-in it leads to is filled in with it. */\n email: string;\n /** The app's outcomes, shown on the done state: keksdose's recovery-code notice for an\n * encrypted account, \"Your 2 personal access tokens were revoked\". */\n notices?: ReactNode;\n}\n\ntype Phase =\n | { kind: \"form\"; check: ResetPasswordCheck }\n | { kind: \"invalid\" }\n | { kind: \"done\"; result: ResetPasswordResult };\n\nexport interface ResetPasswordFormProps\n extends Omit<ComponentPropsWithRef<\"div\">, \"onSubmit\" | \"children\" | \"title\"> {\n /** The link's token — the app's `?token=` (keksdose, kastlan: `/reset-password?token=…`).\n * Empty: the link was cut short, and the page says so without asking anyone. */\n token: string;\n /**\n * Check the link before the form shows (`/auth/password-reset/check`) — once per\n * token, on mount. Checking does not use it up, so a reload is free. Resolve (with\n * the address and any notices) for a live link; reject for a dead one, and the page\n * says \"This link is invalid or has expired\" with the way to a new one.\n */\n onCheck: (token: string) => Promise<ResetPasswordCheck | void | undefined>;\n /**\n * The new password was submitted: send `/auth/password-reset/confirm`. Resolve with\n * the account's address (and the app's notices) — no tokens: a reset is not a sign-in\n * (§6.3). Reject with the client's error: `token_invalid` (the link expired or was\n * used in another tab while the form was open) goes to the invalid state; anything\n * else stays on the form with the password kept.\n */\n onSubmit: (values: ResetPasswordValues) => Promise<ResetPasswordResult>;\n /** \"Go to sign in\" calls this with the address, for an app that routes in code.\n * Without it, the button is a link to `signInHref`. */\n onSignIn?: (email: string) => void;\n /** Where \"Go to sign in\" leads, with `?email=` added — read it into `SignInForm`'s\n * `defaultEmail`. Default `/login`. */\n signInHref?: string;\n /** \"Request a new link\" — the app's \"Forgot password\" page. Default\n * `/forgot-password`. */\n forgotHref?: string;\n /** The app's words for a failed save (the device offline), or `undefined` for the\n * kit's: `rateLimited` for a bare `429` (0.30.0), else `failed`. Not asked for\n * `token_invalid` or `token_expired`, which are a state. */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it\n * in `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<ResetPasswordLabels>;\n}\n\n/**\n * Redeem a password-reset link (`/reset-password?token=…`, docs/auth-harmonization.md\n * §6.3, §7) — keksdose's `ResetPasswordPage` (features/auth/reset-password-page.tsx),\n * which kastlan built its own on.\n *\n * **The link is checked first** (`onCheck`), and only then does the form appear: an\n * expired link should say so before someone types a new password twice, not after.\n *\n * **A reset is not a sign-in** (§6.3, §10.11). The confirm answers without tokens, so a\n * 2FA account cannot skip its second factor by way of its inbox, and the page ends on\n * \"Go to sign in\" with the address filled in — the person has just typed the new\n * password twice; signing in is one step. What else the reset did is said there:\n * every other session ended (the kit's line, true in all three apps), and the app's own\n * outcomes in `notices` — keksdose's recovery-code notice and its revoked API tokens.\n *\n * **The password** is chosen with the same fields as `RegisterForm` and `SignInForm`'s\n * set-password step ({@link NewPasswordFields}): the strength meter and its checklist as\n * advice, and only the two hard rules — at least 8 characters, at most 72 bytes (§8).\n * The submit waits for both and a matching confirmation.\n *\n * The form never sends a request itself (§8): the token and the password go to the\n * callbacks, and the page renders what they resolve with.\n */\nexport function ResetPasswordForm({\n token,\n onCheck,\n onSubmit,\n onSignIn,\n signInHref = \"/login\",\n forgotHref = \"/forgot-password\",\n describeError,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: ResetPasswordFormProps) {\n const labels = useKitLabels(\"resetPassword\", DEFAULT_RESET_PASSWORD_LABELS, labelsProp);\n // The phase belongs to the token it was reached with: a new token starts over at the\n // check without a reset effect.\n const [phase, setPhase] = useState<{ token: string; phase: Phase } | null>(null);\n const current: Phase | \"checking\" =\n token === \"\" ? { kind: \"invalid\" } : phase?.token === token ? phase.phase : \"checking\";\n\n useOncePerKey(token === \"\" ? null : token, onCheck, (key, result) =>\n setPhase({\n token: key,\n phase: result.ok ? { kind: \"form\", check: result.value ?? {} } : { kind: \"invalid\" },\n }),\n );\n\n const [password, setPassword] = useState(\"\");\n const [confirm, setConfirm] = useState(\"\");\n const [pending, setPending] = useState(false);\n const [failure, setFailure] = useState<ReactNode>(null);\n const headingId = useId();\n const passwordRef = useRef<HTMLInputElement>(null);\n const doneRef = useRef<HTMLDivElement>(null);\n const kind = current === \"checking\" ? \"checking\" : current.kind;\n\n // The form arrives after the check: the field is where the person is going.\n useEffect(() => {\n if (kind === \"form\") passwordRef.current?.focus();\n }, [kind]);\n useFocusWhen(kind === \"done\", doneRef);\n\n const heading =\n title === null ? null : (\n <Heading id={headingId} className={FORM_HEADING_CLASS}>\n {title ?? labels.title}\n </Heading>\n );\n const rootClass = cn(\"space-y-3\", className);\n\n if (current === \"checking\") {\n return (\n <div {...rest} data-state=\"checking\" aria-busy className={rootClass}>\n {heading}\n <LoadingState size=\"sm\" label={labels.checking} />\n </div>\n );\n }\n\n if (current.kind === \"invalid\") {\n return (\n <div {...rest} data-state=\"invalid\" className={rootClass}>\n {heading}\n <EmptyState\n tone=\"danger\"\n icon={<XCircle />}\n title={token === \"\" ? labels.noToken : labels.invalid}\n hint={labels.invalidHint}\n action={\n <Button href={forgotHref} variant=\"accent\">\n {labels.requestNew}\n </Button>\n }\n className={OUTCOME_CLASS}\n />\n </div>\n );\n }\n\n if (current.kind === \"done\") {\n const { email, notices } = current.result;\n return (\n <div {...rest} data-state=\"done\" className={rootClass}>\n {heading}\n <OutcomeMessage icon={<CheckCircle2 className=\"text-success\" />} focusRef={doneRef}>\n <p className={INTRO_CLASS}>{labels.success}</p>\n </OutcomeMessage>\n {notices}\n <p className=\"text-xs text-muted\">{labels.sessionsEnded}</p>\n {onSignIn ? (\n <Button type=\"button\" variant=\"accent\" className=\"w-full\" onClick={() => onSignIn(email)}>\n {labels.signIn}\n </Button>\n ) : (\n <Button href={email ? withQuery(signInHref, \"email\", email) : signInHref} variant=\"accent\" className=\"w-full\">\n {labels.signIn}\n </Button>\n )}\n </div>\n );\n }\n\n const { email, notices } = current.check;\n const problem = newPasswordProblem(password, confirm);\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n if (pending || problem !== null) return;\n setFailure(null);\n setPending(true);\n Promise.resolve()\n .then(() => onSubmit({ token, password }))\n .then(\n (result) => {\n setPending(false);\n setPassword(\"\");\n setConfirm(\"\");\n setPhase({ token, phase: { kind: \"done\", result } });\n },\n (error: unknown) => {\n setPending(false);\n // A link that died between the check and the save — used, or run out — is the\n // dead-link state, under either code (0.31.1: keksdose mapped `token_expired`\n // to it itself, `asDeadLink`).\n const code = authErrorCode(error);\n if (code === \"token_invalid\" || code === \"token_expired\") {\n setPassword(\"\");\n setConfirm(\"\");\n setPhase({ token, phase: { kind: \"invalid\" } });\n return;\n }\n // The app's words first, then the throttle's, then the kit's catch-all.\n setFailure(\n describeError?.(error) ??\n (isRateLimited(error)\n ? (labels.rateLimited ?? DEFAULT_RESET_PASSWORD_LABELS.rateLimited)(retryAfterSeconds(error))\n : labels.failed),\n );\n },\n );\n };\n\n return (\n <div {...rest} data-state=\"form\" className={rootClass}>\n <form className=\"space-y-3\" aria-labelledby={title === null ? undefined : headingId} onSubmit={submit}>\n {heading}\n {email ? <p className={INTRO_CLASS}>{labels.intro(email)}</p> : null}\n {notices}\n {/* Which account the new password is for, for the password manager: it files\n the new password under this username instead of guessing (kastlan). */}\n {email ? <input type=\"email\" name=\"username\" autoComplete=\"username\" value={email} readOnly hidden /> : null}\n <NewPasswordFields\n password={password}\n confirm={confirm}\n onPasswordChange={(next) => {\n setPassword(next);\n if (failure) setFailure(null);\n }}\n onConfirmChange={setConfirm}\n busy={pending}\n passwordRef={passwordRef}\n text={{ password: labels.newPassword, confirm: labels.confirmPassword, mismatch: labels.mismatch }}\n />\n {failure != null && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n <Button type=\"submit\" className=\"w-full\" disabled={!pending && problem !== null} pending={pending}>\n {labels.submit}\n </Button>\n </form>\n </div>\n );\n}\n"],"mappings":"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","names":["email","notices"]}
@@ -114,7 +114,7 @@ function VerifyEmailStatus({
114
114
  announce(labels[next]);
115
115
  });
116
116
  const href = continueHref ?? (signedIn ? "/" : "/login");
117
- const continueButton = /* @__PURE__ */ jsx(Button, { href, replace: true, variant: outcome === "verified" ? "brand" : "secondary", children: labels.continue });
117
+ const continueButton = /* @__PURE__ */ jsx(Button, { href, replace: true, variant: outcome === "verified" ? "accent" : "secondary", children: labels.continue });
118
118
  const canResend = signedIn && onResend !== void 0;
119
119
  let body;
120
120
  if (outcome === "verifying") {
@@ -144,7 +144,7 @@ function VerifyEmailStatus({
144
144
  title: token === "" ? labels.noToken : labels[outcome],
145
145
  hint,
146
146
  action: /* @__PURE__ */ jsxs(Fragment, { children: [
147
- canResend && /* @__PURE__ */ jsx(ResendButton, { state: resend, labels, variant: "brand" }),
147
+ canResend && /* @__PURE__ */ jsx(ResendButton, { state: resend, labels, variant: "accent" }),
148
148
  continueButton
149
149
  ] }),
150
150
  className: OUTCOME_CLASS
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/verify-email.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CheckCircle2, MailWarning, TimerOff, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button } from \"../components/button\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { OUTCOME_CLASS, STATUS_HEADING_CLASS, isThenable, useOncePerKey } from \"./status-parts\";\nimport { authErrorCode, englishRateLimited, isRateLimited, retryAfterSeconds } from \"./auth-errors\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_VERIFY_EMAIL_LABELS } from \"./verify-email-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the verification page and banner render — the `verifyEmail` namespace.\n * keksdose's words (`verify_email.*`, and its server's \"has expired\"). */\nexport interface VerifyEmailLabels {\n /** The page's heading. */\n title: string;\n /** While the link is redeemed. */\n verifying: string;\n verified: string;\n /** The way on from the page — into the app, or to sign-in. */\n continue: string;\n invalid: string;\n expired: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** Under `invalid`: why a link stops working. */\n invalidHint: string;\n /** Under a dead link, signed out: where a new one comes from. */\n requestInApp: string;\n /** Under a dead link, signed in, beside \"Send again\". */\n requestHere: string;\n /** The resend button, on the page and in the banner. */\n resend: string;\n /** The resend button while a just-sent mail cools down, given the seconds left. */\n resendIn: (seconds: number) => string;\n /** Said (and shown) after a resend went out. */\n resent: string;\n /** Said (and shown) after a resend failed. */\n resendError: string;\n /** 0.31.1: a resend refused as throttled — HTTP 429 (`isRateLimited`) — given the\n * `Retry-After` seconds, when the app's `describeError` has no words of its own\n * (keksdose borrowed the sign-in namespace's). */\n rateLimited?: (seconds?: number) => string;\n /** The banner's message. */\n banner: string;\n /** The banner's ×: it defers, it does not close anything. */\n dismiss: string;\n}\n\n// Defined in ./verify-email-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_VERIFY_EMAIL_LABELS };\n\n/** A minute: long enough that an impatient second click does not mail twice, short\n * enough that a mail lost to a spam filter can be re-sent without leaving the page. The\n * server's own limit is the real one (kastlan: 3 an hour per address). */\nconst DEFAULT_COOLDOWN = 60;\n\n/* ── Resend ──────────────────────────────────────────────────────────────── */\n\ntype ResendPhase = \"idle\" | \"sending\" | \"sent\" | \"failed\";\n\n/**\n * \"Send again\" with its own memory: busy while the request runs, then — after a mail\n * went out — held for `cooldown` seconds with the countdown on the button, so a second\n * click cannot mail twice and a throttled server is not asked to refuse. A failure frees\n * it at once. The outcome is a polite live region that is always rendered (a region that\n * mounts with its message is often not read), and visible.\n */\nfunction useResend(onResend: (() => Promise<unknown> | void) | undefined, cooldown: number) {\n const [phase, setPhase] = useState<ResendPhase>(\"idle\");\n // The refusal itself, for `describeError` (0.30.0): the server's own sentence — keksdose's\n // per-address throttle, \"try again in N minutes\" — beats a fixed \"could not send\".\n const [error, setError] = useState<unknown>(undefined);\n const [remaining, setRemaining] = useState(0);\n const counting = remaining > 0;\n useEffect(() => {\n if (!counting) return;\n const id = setInterval(() => setRemaining((r) => Math.max(0, r - 1)), 1000);\n return () => clearInterval(id);\n }, [counting]);\n\n const sent = () => {\n setPhase(\"sent\");\n setRemaining(Math.max(0, Math.round(cooldown)));\n };\n const send = () => {\n if (!onResend || phase === \"sending\" || counting) return;\n let result: unknown;\n try {\n result = onResend();\n } catch (e) {\n setError(e);\n setPhase(\"failed\");\n return;\n }\n if (!isThenable(result)) {\n sent();\n return;\n }\n setPhase(\"sending\");\n result.then(sent, (e: unknown) => {\n setError(e);\n setPhase(\"failed\");\n });\n };\n return { phase, remaining, send, error };\n}\n\nfunction ResendButton({\n state,\n labels,\n variant,\n}: {\n state: ReturnType<typeof useResend>;\n labels: VerifyEmailLabels;\n variant: \"secondary\" | \"brand\";\n}) {\n return (\n <Button\n type=\"button\"\n variant={variant}\n size={variant === \"secondary\" ? \"sm\" : undefined}\n onClick={state.send}\n pending={state.phase === \"sending\"}\n // `disabledReason`, not `disabled`: the button just pressed has the focus, and a\n // native `disabled` would drop it to <body>. The reason is what just happened.\n disabledReason={state.remaining > 0 ? labels.resent : undefined}\n >\n {state.remaining > 0 ? labels.resendIn(state.remaining) : labels.resend}\n </Button>\n );\n}\n\nfunction ResendStatus({\n phase,\n error,\n describeError,\n labels,\n className,\n}: {\n phase: ResendPhase;\n error?: unknown;\n describeError?: (error: unknown) => string | undefined;\n labels: VerifyEmailLabels;\n className?: string;\n}) {\n // The app's words first, then the throttle's, then the kit's catch-all.\n const failed =\n phase === \"failed\"\n ? (describeError?.(error) ??\n (isRateLimited(error)\n ? (labels.rateLimited ?? englishRateLimited)(retryAfterSeconds(error))\n : labels.resendError))\n : \"\";\n return (\n <span\n role=\"status\"\n className={cn(phase === \"failed\" ? \"text-danger\" : \"text-success\", className)}\n >\n {phase === \"sent\" ? labels.resent : failed}\n </span>\n );\n}\n\n/* ── The page ────────────────────────────────────────────────────────────── */\n\n/** What became of a confirmation link that did not verify. */\nexport type VerifyEmailFailure = \"invalid\" | \"expired\";\n\n/** The default when the app gives no `classifyError`: the coded refusal decides. */\nfunction defaultVerifyFailure(error: unknown): VerifyEmailFailure {\n return authErrorCode(error) === \"token_expired\" ? \"expired\" : \"invalid\";\n}\n\nexport interface VerifyEmailStatusProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The link's token — the app's `?token=`. Empty: the link was cut short, and the\n * page says so without asking anyone. */\n token: string;\n /**\n * Redeem it — `POST /auth/verify-email {token}`, once per token, on mount. A POST and\n * not the link's own GET (§10.10): a mail scanner that opens every link must not\n * confirm an address nobody read. Resolve when it verified (an address that already\n * was counts: keksdose's redeem is idempotent); reject with the client's error.\n */\n onVerify: (token: string) => Promise<unknown>;\n /** Which dead end a rejection is. Default: `invalid` for everything — §5.2's servers\n * answer one `token_invalid`; an app whose server tells expiry apart maps it here. */\n classifyError?: (error: unknown) => VerifyEmailFailure;\n /** 0.30.0: the words for a refused resend — the server's own sentence (a throttle's\n * \"try again in N minutes\"), else the kit's `resendError`. */\n describeError?: (error: unknown) => string | undefined;\n /** Someone is signed in on this device: \"Continue\" goes into the app, and a dead link\n * offers \"Send again\" (with `onResend`). */\n signedIn?: boolean;\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`) — offered on a dead\n * link while signed in. */\n onResend?: () => Promise<unknown> | void;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** Where \"Continue\" goes. Default `/` signed in, `/login` signed out. */\n continueHref?: string;\n /** App content under \"confirmed\" — keksdose's \"Others can now share a budget with you\n * directly.\" */\n verifiedContent?: ReactNode;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it in\n * `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<VerifyEmailLabels>;\n}\n\ntype VerifyOutcome = \"verified\" | VerifyEmailFailure;\n\n/**\n * Redeem an email-confirmation link (`/verify-email?token=…`, docs/auth-harmonization.md\n * §7) — keksdose's `VerifyEmailPage` (features/auth/verify-email-page.tsx), with\n * kastlan's outcome blocks and its separate \"expired\".\n *\n * Opened from an inbox, often in a browser with no session, so the token is the only\n * credential and the page renders signed-out. It redeems once per token on mount\n * (StrictMode's double effect included — the token is single-use) and shows one of:\n * confirmed; not valid; expired; or the link had no token at all. Verification gates\n * nothing (§2.8), so every outcome ends on \"Continue\". Signed in, a dead link also\n * offers \"Send again\", which waits `cooldown` seconds after a mail went out.\n *\n * The outcome replaces the spinner, which says nothing to a screen reader on its own,\n * so it is also announced.\n */\nexport function VerifyEmailStatus({\n token,\n onVerify,\n classifyError,\n describeError,\n signedIn = false,\n onResend,\n cooldown = DEFAULT_COOLDOWN,\n continueHref,\n verifiedContent,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: VerifyEmailStatusProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const [settled, setSettled] = useState<{ token: string; outcome: VerifyOutcome } | null>(null);\n const outcome: VerifyOutcome | \"verifying\" =\n token === \"\" ? \"invalid\" : settled?.token === token ? settled.outcome : \"verifying\";\n const { announce, regionProps } = useAnnounce();\n const resend = useResend(onResend, cooldown);\n\n useOncePerKey(token === \"\" ? null : token, onVerify, (key, result) => {\n // Without a classifier, the coded refusal decides (0.30.0): `token_expired` is\n // \"expired\", anything else \"invalid\".\n const next: VerifyOutcome = result.ok\n ? \"verified\"\n : (classifyError ?? defaultVerifyFailure)(result.error);\n setSettled({ token: key, outcome: next });\n announce(labels[next]);\n });\n\n const href = continueHref ?? (signedIn ? \"/\" : \"/login\");\n // `replace`: the token is spent, so Back must not land on this page again (keksdose F1).\n const continueButton = (\n <Button href={href} replace variant={outcome === \"verified\" ? \"brand\" : \"secondary\"}>\n {labels.continue}\n </Button>\n );\n const canResend = signedIn && onResend !== undefined;\n\n let body: ReactNode;\n if (outcome === \"verifying\") {\n body = <LoadingState size=\"sm\" label={labels.verifying} />;\n } else if (outcome === \"verified\") {\n body = (\n <EmptyState\n tone=\"success\"\n icon={<CheckCircle2 />}\n title={labels.verified}\n hint={verifiedContent}\n action={continueButton}\n className={OUTCOME_CLASS}\n />\n );\n } else {\n const hint = (\n <>\n {outcome === \"invalid\" && token !== \"\" ? `${labels.invalidHint} ` : \"\"}\n {canResend ? labels.requestHere : labels.requestInApp}\n </>\n );\n body = (\n <EmptyState\n tone=\"danger\"\n icon={outcome === \"expired\" ? <TimerOff /> : <XCircle />}\n title={token === \"\" ? labels.noToken : labels[outcome]}\n hint={hint}\n action={\n <>\n {canResend && <ResendButton state={resend} labels={labels} variant=\"brand\" />}\n {continueButton}\n </>\n }\n className={OUTCOME_CLASS}\n />\n );\n }\n\n return (\n <div\n {...rest}\n data-state={outcome}\n aria-busy={outcome === \"verifying\" || undefined}\n className={cn(\"space-y-3\", className)}\n >\n {title === null ? null : <Heading className={STATUS_HEADING_CLASS}>{title ?? labels.title}</Heading>}\n {body}\n {canResend && (\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"block text-center text-xs\"\n />\n )}\n {/* Rendered from the start, so the outcome is read when it arrives. */}\n <div {...regionProps} />\n </div>\n );\n}\n\n/* ── The banner ──────────────────────────────────────────────────────────── */\n\nexport interface EmailVerificationBannerProps {\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`). Resolve when it\n * went out; reject and the banner says it could not. */\n onResend: () => Promise<unknown> | void;\n /** 0.30.0: the words for a refused resend — the server's own sentence, else the kit's. */\n describeError?: (error: unknown) => string | undefined;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** The ×, \"Not now\". The banner does not hide itself: the app stops rendering it —\n * keksdose for the rest of the session. Left out, no ×. */\n onDismiss?: () => void;\n /** `strip` (default): keksdose's band across the top of the shell. `box`: kastlan's\n * callout inside the page. */\n variant?: \"strip\" | \"box\";\n /** The app's sentence after the kit's — keksdose's \"Until you do, nobody can share a\n * budget with you directly.\" */\n children?: ReactNode;\n className?: string;\n labels?: LabelOverride<VerifyEmailLabels>;\n /**\n * 0.48: on a phone hold the message to one line with a More / Less toggle —\n * {@link AlertBanner}'s `clampMobile`, passed through (kastlan S2: its banner took ~110px\n * of a 390px phone). The resend's outcome (\"Sent\", the error) then moves out of the\n * message to sit after \"Send again\", at every width, so the line that answers the press\n * is never hidden behind \"More\". Default `false`: the outcome stays in the message.\n */\n clampMobile?: boolean;\n}\n\n/**\n * The nudge for a signed-in account whose address is not confirmed — keksdose's\n * `VerifyEmailBanner` (features/auth/verify-email-banner.tsx), kastlan's copy of it.\n *\n * A prompt, not a wall: verification gates nothing (§2.8). The app renders it while\n * `/auth/me` says `email_verified: false`, and stops when the user dismisses it or the\n * page above confirms the address. \"Send again\" waits `cooldown` seconds after a mail\n * went out (the button says how long), and its outcome is written into the banner and\n * read out — no toast needed.\n */\nexport function EmailVerificationBanner({\n onResend,\n describeError,\n cooldown = DEFAULT_COOLDOWN,\n onDismiss,\n variant = \"strip\",\n children,\n className,\n labels: labelsProp,\n clampMobile = false,\n}: EmailVerificationBannerProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const resend = useResend(onResend, cooldown);\n // One live region either way, mounted from the start (a region added later is often not\n // read out): in the message, or — clamped — after the button, outside the clamp.\n const status = (\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"font-medium\"\n />\n );\n const button = <ResendButton state={resend} labels={labels} variant=\"secondary\" />;\n return (\n <AlertBanner\n tone=\"warning\"\n variant={variant}\n icon={<MailWarning />}\n action={\n clampMobile ? (\n <>\n {button}\n {status}\n </>\n ) : (\n button\n )\n }\n onDismiss={onDismiss}\n dismissLabel={onDismiss ? labels.dismiss : undefined}\n clampMobile={clampMobile}\n className={className}\n >\n {labels.banner}\n {children != null && children !== false && <> {children}</>}\n {!clampMobile && <> {status}</>}\n </AlertBanner>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/auth/verify-email.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CheckCircle2, MailWarning, TimerOff, XCircle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { LoadingState } from \"../components/loading-state\";\nimport { Button } from \"../components/button\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { OUTCOME_CLASS, STATUS_HEADING_CLASS, isThenable, useOncePerKey } from \"./status-parts\";\nimport { authErrorCode, englishRateLimited, isRateLimited, retryAfterSeconds } from \"./auth-errors\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\nimport { DEFAULT_VERIFY_EMAIL_LABELS } from \"./verify-email-labels\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the verification page and banner render — the `verifyEmail` namespace.\n * keksdose's words (`verify_email.*`, and its server's \"has expired\"). */\nexport interface VerifyEmailLabels {\n /** The page's heading. */\n title: string;\n /** While the link is redeemed. */\n verifying: string;\n verified: string;\n /** The way on from the page — into the app, or to sign-in. */\n continue: string;\n invalid: string;\n expired: string;\n /** The page was opened without a `token`. */\n noToken: string;\n /** Under `invalid`: why a link stops working. */\n invalidHint: string;\n /** Under a dead link, signed out: where a new one comes from. */\n requestInApp: string;\n /** Under a dead link, signed in, beside \"Send again\". */\n requestHere: string;\n /** The resend button, on the page and in the banner. */\n resend: string;\n /** The resend button while a just-sent mail cools down, given the seconds left. */\n resendIn: (seconds: number) => string;\n /** Said (and shown) after a resend went out. */\n resent: string;\n /** Said (and shown) after a resend failed. */\n resendError: string;\n /** 0.31.1: a resend refused as throttled — HTTP 429 (`isRateLimited`) — given the\n * `Retry-After` seconds, when the app's `describeError` has no words of its own\n * (keksdose borrowed the sign-in namespace's). */\n rateLimited?: (seconds?: number) => string;\n /** The banner's message. */\n banner: string;\n /** The banner's ×: it defers, it does not close anything. */\n dismiss: string;\n}\n\n// Defined in ./verify-email-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_VERIFY_EMAIL_LABELS };\n\n/** A minute: long enough that an impatient second click does not mail twice, short\n * enough that a mail lost to a spam filter can be re-sent without leaving the page. The\n * server's own limit is the real one (kastlan: 3 an hour per address). */\nconst DEFAULT_COOLDOWN = 60;\n\n/* ── Resend ──────────────────────────────────────────────────────────────── */\n\ntype ResendPhase = \"idle\" | \"sending\" | \"sent\" | \"failed\";\n\n/**\n * \"Send again\" with its own memory: busy while the request runs, then — after a mail\n * went out — held for `cooldown` seconds with the countdown on the button, so a second\n * click cannot mail twice and a throttled server is not asked to refuse. A failure frees\n * it at once. The outcome is a polite live region that is always rendered (a region that\n * mounts with its message is often not read), and visible.\n */\nfunction useResend(onResend: (() => Promise<unknown> | void) | undefined, cooldown: number) {\n const [phase, setPhase] = useState<ResendPhase>(\"idle\");\n // The refusal itself, for `describeError` (0.30.0): the server's own sentence — keksdose's\n // per-address throttle, \"try again in N minutes\" — beats a fixed \"could not send\".\n const [error, setError] = useState<unknown>(undefined);\n const [remaining, setRemaining] = useState(0);\n const counting = remaining > 0;\n useEffect(() => {\n if (!counting) return;\n const id = setInterval(() => setRemaining((r) => Math.max(0, r - 1)), 1000);\n return () => clearInterval(id);\n }, [counting]);\n\n const sent = () => {\n setPhase(\"sent\");\n setRemaining(Math.max(0, Math.round(cooldown)));\n };\n const send = () => {\n if (!onResend || phase === \"sending\" || counting) return;\n let result: unknown;\n try {\n result = onResend();\n } catch (e) {\n setError(e);\n setPhase(\"failed\");\n return;\n }\n if (!isThenable(result)) {\n sent();\n return;\n }\n setPhase(\"sending\");\n result.then(sent, (e: unknown) => {\n setError(e);\n setPhase(\"failed\");\n });\n };\n return { phase, remaining, send, error };\n}\n\nfunction ResendButton({\n state,\n labels,\n variant,\n}: {\n state: ReturnType<typeof useResend>;\n labels: VerifyEmailLabels;\n variant: \"secondary\" | \"accent\";\n}) {\n return (\n <Button\n type=\"button\"\n variant={variant}\n size={variant === \"secondary\" ? \"sm\" : undefined}\n onClick={state.send}\n pending={state.phase === \"sending\"}\n // `disabledReason`, not `disabled`: the button just pressed has the focus, and a\n // native `disabled` would drop it to <body>. The reason is what just happened.\n disabledReason={state.remaining > 0 ? labels.resent : undefined}\n >\n {state.remaining > 0 ? labels.resendIn(state.remaining) : labels.resend}\n </Button>\n );\n}\n\nfunction ResendStatus({\n phase,\n error,\n describeError,\n labels,\n className,\n}: {\n phase: ResendPhase;\n error?: unknown;\n describeError?: (error: unknown) => string | undefined;\n labels: VerifyEmailLabels;\n className?: string;\n}) {\n // The app's words first, then the throttle's, then the kit's catch-all.\n const failed =\n phase === \"failed\"\n ? (describeError?.(error) ??\n (isRateLimited(error)\n ? (labels.rateLimited ?? englishRateLimited)(retryAfterSeconds(error))\n : labels.resendError))\n : \"\";\n return (\n <span\n role=\"status\"\n className={cn(phase === \"failed\" ? \"text-danger\" : \"text-success\", className)}\n >\n {phase === \"sent\" ? labels.resent : failed}\n </span>\n );\n}\n\n/* ── The page ────────────────────────────────────────────────────────────── */\n\n/** What became of a confirmation link that did not verify. */\nexport type VerifyEmailFailure = \"invalid\" | \"expired\";\n\n/** The default when the app gives no `classifyError`: the coded refusal decides. */\nfunction defaultVerifyFailure(error: unknown): VerifyEmailFailure {\n return authErrorCode(error) === \"token_expired\" ? \"expired\" : \"invalid\";\n}\n\nexport interface VerifyEmailStatusProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** The link's token — the app's `?token=`. Empty: the link was cut short, and the\n * page says so without asking anyone. */\n token: string;\n /**\n * Redeem it — `POST /auth/verify-email {token}`, once per token, on mount. A POST and\n * not the link's own GET (§10.10): a mail scanner that opens every link must not\n * confirm an address nobody read. Resolve when it verified (an address that already\n * was counts: keksdose's redeem is idempotent); reject with the client's error.\n */\n onVerify: (token: string) => Promise<unknown>;\n /** Which dead end a rejection is. Default: `invalid` for everything — §5.2's servers\n * answer one `token_invalid`; an app whose server tells expiry apart maps it here. */\n classifyError?: (error: unknown) => VerifyEmailFailure;\n /** 0.30.0: the words for a refused resend — the server's own sentence (a throttle's\n * \"try again in N minutes\"), else the kit's `resendError`. */\n describeError?: (error: unknown) => string | undefined;\n /** Someone is signed in on this device: \"Continue\" goes into the app, and a dead link\n * offers \"Send again\" (with `onResend`). */\n signedIn?: boolean;\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`) — offered on a dead\n * link while signed in. */\n onResend?: () => Promise<unknown> | void;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** Where \"Continue\" goes. Default `/` signed in, `/login` signed out. */\n continueHref?: string;\n /** App content under \"confirmed\" — keksdose's \"Others can now share a budget with you\n * directly.\" */\n verifiedContent?: ReactNode;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it in\n * `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<VerifyEmailLabels>;\n}\n\ntype VerifyOutcome = \"verified\" | VerifyEmailFailure;\n\n/**\n * Redeem an email-confirmation link (`/verify-email?token=…`, docs/auth-harmonization.md\n * §7) — keksdose's `VerifyEmailPage` (features/auth/verify-email-page.tsx), with\n * kastlan's outcome blocks and its separate \"expired\".\n *\n * Opened from an inbox, often in a browser with no session, so the token is the only\n * credential and the page renders signed-out. It redeems once per token on mount\n * (StrictMode's double effect included — the token is single-use) and shows one of:\n * confirmed; not valid; expired; or the link had no token at all. Verification gates\n * nothing (§2.8), so every outcome ends on \"Continue\". Signed in, a dead link also\n * offers \"Send again\", which waits `cooldown` seconds after a mail went out.\n *\n * The outcome replaces the spinner, which says nothing to a screen reader on its own,\n * so it is also announced.\n */\nexport function VerifyEmailStatus({\n token,\n onVerify,\n classifyError,\n describeError,\n signedIn = false,\n onResend,\n cooldown = DEFAULT_COOLDOWN,\n continueHref,\n verifiedContent,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: VerifyEmailStatusProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const [settled, setSettled] = useState<{ token: string; outcome: VerifyOutcome } | null>(null);\n const outcome: VerifyOutcome | \"verifying\" =\n token === \"\" ? \"invalid\" : settled?.token === token ? settled.outcome : \"verifying\";\n const { announce, regionProps } = useAnnounce();\n const resend = useResend(onResend, cooldown);\n\n useOncePerKey(token === \"\" ? null : token, onVerify, (key, result) => {\n // Without a classifier, the coded refusal decides (0.30.0): `token_expired` is\n // \"expired\", anything else \"invalid\".\n const next: VerifyOutcome = result.ok\n ? \"verified\"\n : (classifyError ?? defaultVerifyFailure)(result.error);\n setSettled({ token: key, outcome: next });\n announce(labels[next]);\n });\n\n const href = continueHref ?? (signedIn ? \"/\" : \"/login\");\n // `replace`: the token is spent, so Back must not land on this page again (keksdose F1).\n const continueButton = (\n <Button href={href} replace variant={outcome === \"verified\" ? \"accent\" : \"secondary\"}>\n {labels.continue}\n </Button>\n );\n const canResend = signedIn && onResend !== undefined;\n\n let body: ReactNode;\n if (outcome === \"verifying\") {\n body = <LoadingState size=\"sm\" label={labels.verifying} />;\n } else if (outcome === \"verified\") {\n body = (\n <EmptyState\n tone=\"success\"\n icon={<CheckCircle2 />}\n title={labels.verified}\n hint={verifiedContent}\n action={continueButton}\n className={OUTCOME_CLASS}\n />\n );\n } else {\n const hint = (\n <>\n {outcome === \"invalid\" && token !== \"\" ? `${labels.invalidHint} ` : \"\"}\n {canResend ? labels.requestHere : labels.requestInApp}\n </>\n );\n body = (\n <EmptyState\n tone=\"danger\"\n icon={outcome === \"expired\" ? <TimerOff /> : <XCircle />}\n title={token === \"\" ? labels.noToken : labels[outcome]}\n hint={hint}\n action={\n <>\n {canResend && <ResendButton state={resend} labels={labels} variant=\"accent\" />}\n {continueButton}\n </>\n }\n className={OUTCOME_CLASS}\n />\n );\n }\n\n return (\n <div\n {...rest}\n data-state={outcome}\n aria-busy={outcome === \"verifying\" || undefined}\n className={cn(\"space-y-3\", className)}\n >\n {title === null ? null : <Heading className={STATUS_HEADING_CLASS}>{title ?? labels.title}</Heading>}\n {body}\n {canResend && (\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"block text-center text-xs\"\n />\n )}\n {/* Rendered from the start, so the outcome is read when it arrives. */}\n <div {...regionProps} />\n </div>\n );\n}\n\n/* ── The banner ──────────────────────────────────────────────────────────── */\n\nexport interface EmailVerificationBannerProps {\n /** Send a new confirmation mail (`POST /auth/verify-email/resend`). Resolve when it\n * went out; reject and the banner says it could not. */\n onResend: () => Promise<unknown> | void;\n /** 0.30.0: the words for a refused resend — the server's own sentence, else the kit's. */\n describeError?: (error: unknown) => string | undefined;\n /** Seconds \"Send again\" waits after a mail went out. Default 60; `0` never waits. */\n cooldown?: number;\n /** The ×, \"Not now\". The banner does not hide itself: the app stops rendering it —\n * keksdose for the rest of the session. Left out, no ×. */\n onDismiss?: () => void;\n /** `strip` (default): keksdose's band across the top of the shell. `box`: kastlan's\n * callout inside the page. */\n variant?: \"strip\" | \"box\";\n /** The app's sentence after the kit's — keksdose's \"Until you do, nobody can share a\n * budget with you directly.\" */\n children?: ReactNode;\n className?: string;\n labels?: LabelOverride<VerifyEmailLabels>;\n /**\n * 0.48: on a phone hold the message to one line with a More / Less toggle —\n * {@link AlertBanner}'s `clampMobile`, passed through (kastlan S2: its banner took ~110px\n * of a 390px phone). The resend's outcome (\"Sent\", the error) then moves out of the\n * message to sit after \"Send again\", at every width, so the line that answers the press\n * is never hidden behind \"More\". Default `false`: the outcome stays in the message.\n */\n clampMobile?: boolean;\n}\n\n/**\n * The nudge for a signed-in account whose address is not confirmed — keksdose's\n * `VerifyEmailBanner` (features/auth/verify-email-banner.tsx), kastlan's copy of it.\n *\n * A prompt, not a wall: verification gates nothing (§2.8). The app renders it while\n * `/auth/me` says `email_verified: false`, and stops when the user dismisses it or the\n * page above confirms the address. \"Send again\" waits `cooldown` seconds after a mail\n * went out (the button says how long), and its outcome is written into the banner and\n * read out — no toast needed.\n */\nexport function EmailVerificationBanner({\n onResend,\n describeError,\n cooldown = DEFAULT_COOLDOWN,\n onDismiss,\n variant = \"strip\",\n children,\n className,\n labels: labelsProp,\n clampMobile = false,\n}: EmailVerificationBannerProps) {\n const labels = useKitLabels(\"verifyEmail\", DEFAULT_VERIFY_EMAIL_LABELS, labelsProp);\n const resend = useResend(onResend, cooldown);\n // One live region either way, mounted from the start (a region added later is often not\n // read out): in the message, or — clamped — after the button, outside the clamp.\n const status = (\n <ResendStatus\n phase={resend.phase}\n error={resend.error}\n describeError={describeError}\n labels={labels}\n className=\"font-medium\"\n />\n );\n const button = <ResendButton state={resend} labels={labels} variant=\"secondary\" />;\n return (\n <AlertBanner\n tone=\"warning\"\n variant={variant}\n icon={<MailWarning />}\n action={\n clampMobile ? (\n <>\n {button}\n {status}\n </>\n ) : (\n button\n )\n }\n onDismiss={onDismiss}\n dismissLabel={onDismiss ? labels.dismiss : undefined}\n clampMobile={clampMobile}\n className={className}\n >\n {labels.banner}\n {children != null && children !== false && <> {children}</>}\n {!clampMobile && <> {status}</>}\n </AlertBanner>\n );\n}\n"],"mappings":"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","names":[]}
@@ -236,7 +236,7 @@ function PlanPicker({
236
236
  Button,
237
237
  {
238
238
  type: "button",
239
- variant: "primary",
239
+ variant: "default",
240
240
  className: "max-sm:w-full",
241
241
  disabledReason: blocked,
242
242
  pending,
@@ -280,7 +280,7 @@ function GrantWarningConfirm({ open, onOpenChange, onConfirm, labels: labelsProp
280
280
  /* @__PURE__ */ jsx(
281
281
  Button,
282
282
  {
283
- variant: "primary",
283
+ variant: "default",
284
284
  onClick: () => {
285
285
  onConfirm();
286
286
  onOpenChange(false);