@eifi1/ui-kit 0.32.2 → 0.33.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 (363) hide show
  1. package/README.md +16 -8
  2. package/dist/account/delete-account-setting.d.ts +10 -10
  3. package/dist/account/delete-account-setting.js +2 -1
  4. package/dist/account/delete-account-setting.js.map +1 -1
  5. package/dist/account/email-change-setting.d.ts +5 -0
  6. package/dist/account/email-change-setting.js +4 -3
  7. package/dist/account/email-change-setting.js.map +1 -1
  8. package/dist/account/sessions-setting.d.ts +4 -0
  9. package/dist/account/sessions-setting.js +3 -2
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.d.ts +10 -10
  12. package/dist/admin/admin-action-confirm.js.map +1 -1
  13. package/dist/admin/invitations-panel.d.ts +10 -10
  14. package/dist/admin/invitations-panel.js +4 -3
  15. package/dist/admin/invitations-panel.js.map +1 -1
  16. package/dist/admin/plan-change.d.ts +100 -0
  17. package/dist/admin/plan-change.js +208 -0
  18. package/dist/admin/plan-change.js.map +1 -0
  19. package/dist/admin/reviewer-scope-editor.d.ts +10 -10
  20. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  21. package/dist/admin/role-select.d.ts +7 -7
  22. package/dist/admin/role-select.js.map +1 -1
  23. package/dist/admin/transfer-ownership-dialog.d.ts +10 -10
  24. package/dist/admin/user-roster.d.ts +7 -7
  25. package/dist/admin/user-roster.js +2 -2
  26. package/dist/admin/user-roster.js.map +1 -1
  27. package/dist/auth/auth-errors.d.ts +13 -2
  28. package/dist/auth/auth-errors.js +5 -1
  29. package/dist/auth/auth-errors.js.map +1 -1
  30. package/dist/auth/complete-name-dialog.d.ts +4 -1
  31. package/dist/auth/complete-name-dialog.js +2 -1
  32. package/dist/auth/complete-name-dialog.js.map +1 -1
  33. package/dist/auth/register-form.d.ts +10 -10
  34. package/dist/billing/billing-banner.d.ts +13 -11
  35. package/dist/billing/billing-banner.js.map +1 -1
  36. package/dist/billing/billing-labels.d.ts +10 -10
  37. package/dist/billing/billing-labels.js +10 -1
  38. package/dist/billing/billing-labels.js.map +1 -1
  39. package/dist/billing/billing-lock.d.ts +39 -12
  40. package/dist/billing/billing-lock.js +9 -3
  41. package/dist/billing/billing-lock.js.map +1 -1
  42. package/dist/billing/billing-overview.d.ts +154 -0
  43. package/dist/billing/billing-overview.js +2 -0
  44. package/dist/billing/billing-overview.js.map +1 -0
  45. package/dist/billing/billing-standing.d.ts +10 -10
  46. package/dist/billing/checkout-processing.d.ts +217 -0
  47. package/dist/billing/checkout-processing.js +198 -0
  48. package/dist/billing/checkout-processing.js.map +1 -0
  49. package/dist/billing/checkout-return.d.ts +51 -0
  50. package/dist/billing/checkout-return.js +43 -0
  51. package/dist/billing/checkout-return.js.map +1 -0
  52. package/dist/billing/pay-page.d.ts +210 -0
  53. package/dist/billing/pay-page.js +113 -0
  54. package/dist/billing/pay-page.js.map +1 -0
  55. package/dist/billing/plan-card.d.ts +10 -10
  56. package/dist/billing/plan-limit-notice.d.ts +10 -10
  57. package/dist/billing/plan-limit-toast.d.ts +143 -0
  58. package/dist/billing/plan-limit-toast.js +40 -0
  59. package/dist/billing/plan-limit-toast.js.map +1 -0
  60. package/dist/billing/subscription-actions.d.ts +38 -15
  61. package/dist/billing/subscription-actions.js +9 -2
  62. package/dist/billing/subscription-actions.js.map +1 -1
  63. package/dist/billing/subscription-status-chip.d.ts +9 -9
  64. package/dist/bin/eifi1-pay-page.mjs +166 -0
  65. package/dist/chart.d.ts +10 -10
  66. package/dist/components/account-chips.d.ts +6 -1
  67. package/dist/components/account-settings.d.ts +4 -1
  68. package/dist/components/account-settings.js +2 -1
  69. package/dist/components/account-settings.js.map +1 -1
  70. package/dist/components/amount-input.d.ts +10 -10
  71. package/dist/components/autocomplete.d.ts +2 -1
  72. package/dist/components/autocomplete.js +3 -1
  73. package/dist/components/autocomplete.js.map +1 -1
  74. package/dist/components/button-group.d.ts +10 -10
  75. package/dist/components/button-group.js +1 -1
  76. package/dist/components/button-group.js.map +1 -1
  77. package/dist/components/calculator.d.ts +10 -10
  78. package/dist/components/calculator.js +3 -3
  79. package/dist/components/calculator.js.map +1 -1
  80. package/dist/components/checkbox-group.d.ts +2 -1
  81. package/dist/components/checkbox-group.js.map +1 -1
  82. package/dist/components/checkbox.d.ts +2 -1
  83. package/dist/components/checkbox.js.map +1 -1
  84. package/dist/components/chip.d.ts +2 -1
  85. package/dist/components/chip.js +19 -17
  86. package/dist/components/chip.js.map +1 -1
  87. package/dist/components/choice-card.d.ts +3 -1
  88. package/dist/components/choice-card.js +1 -1
  89. package/dist/components/choice-card.js.map +1 -1
  90. package/dist/components/combobox.d.ts +4 -3
  91. package/dist/components/combobox.js.map +1 -1
  92. package/dist/components/confirm-dialog.d.ts +10 -10
  93. package/dist/components/confirm-dialog.js +6 -3
  94. package/dist/components/confirm-dialog.js.map +1 -1
  95. package/dist/components/copy-button.d.ts +11 -11
  96. package/dist/components/country-select.d.ts +2 -1
  97. package/dist/components/country-select.js.map +1 -1
  98. package/dist/components/danger-confirm.d.ts +10 -10
  99. package/dist/components/danger-confirm.js.map +1 -1
  100. package/dist/components/data-table-cells.d.ts +12 -1
  101. package/dist/components/data-table-filter-popover.d.ts +12 -1
  102. package/dist/components/data-table-filters.d.ts +12 -1
  103. package/dist/components/data-table-labels.d.ts +12 -1
  104. package/dist/components/data-table-pagination.d.ts +12 -1
  105. package/dist/components/data-table.d.ts +12 -1
  106. package/dist/components/data-table.js +3 -3
  107. package/dist/components/data-table.js.map +1 -1
  108. package/dist/components/date-picker.js +6 -4
  109. package/dist/components/date-picker.js.map +1 -1
  110. package/dist/components/entity-combobox.d.ts +3 -2
  111. package/dist/components/entity-combobox.js.map +1 -1
  112. package/dist/components/facing-pair.d.ts +10 -10
  113. package/dist/components/field-parts.d.ts +32 -4
  114. package/dist/components/field-parts.js +54 -19
  115. package/dist/components/field-parts.js.map +1 -1
  116. package/dist/components/file-button.d.ts +10 -10
  117. package/dist/components/file-button.js +3 -2
  118. package/dist/components/file-button.js.map +1 -1
  119. package/dist/components/file-dropzone.d.ts +11 -11
  120. package/dist/components/file-dropzone.js.map +1 -1
  121. package/dist/components/floating-panel.js +5 -5
  122. package/dist/components/floating-panel.js.map +1 -1
  123. package/dist/components/form-actions.d.ts +9 -9
  124. package/dist/components/form-actions.js +3 -3
  125. package/dist/components/form-actions.js.map +1 -1
  126. package/dist/components/iban-input.d.ts +10 -10
  127. package/dist/components/inline-edit-field.d.ts +2 -1
  128. package/dist/components/inline-edit-field.js +6 -6
  129. package/dist/components/inline-edit-field.js.map +1 -1
  130. package/dist/components/language-select.d.ts +10 -10
  131. package/dist/components/legal-page.d.ts +10 -10
  132. package/dist/components/legal.d.ts +10 -10
  133. package/dist/components/list.d.ts +2 -1
  134. package/dist/components/list.js +4 -5
  135. package/dist/components/list.js.map +1 -1
  136. package/dist/components/money-field.d.ts +10 -10
  137. package/dist/components/month-picker.js +54 -44
  138. package/dist/components/month-picker.js.map +1 -1
  139. package/dist/components/multi-entity-combobox.d.ts +3 -2
  140. package/dist/components/multi-entity-combobox.js.map +1 -1
  141. package/dist/components/number-field.d.ts +10 -10
  142. package/dist/components/number-input.d.ts +10 -10
  143. package/dist/components/numpad-sheet.d.ts +10 -10
  144. package/dist/components/numpad-sheet.js +2 -2
  145. package/dist/components/numpad-sheet.js.map +1 -1
  146. package/dist/components/phone-input.d.ts +10 -10
  147. package/dist/components/row-actions.d.ts +5 -5
  148. package/dist/components/row-actions.js +130 -32
  149. package/dist/components/row-actions.js.map +1 -1
  150. package/dist/components/series-chart.d.ts +10 -10
  151. package/dist/components/settings-fields.d.ts +10 -10
  152. package/dist/components/share-card.d.ts +7 -7
  153. package/dist/components/share-card.js +26 -10
  154. package/dist/components/share-card.js.map +1 -1
  155. package/dist/components/status-dot.d.ts +1 -0
  156. package/dist/components/status-dot.js +2 -2
  157. package/dist/components/status-dot.js.map +1 -1
  158. package/dist/components/swatch-picker.d.ts +2 -1
  159. package/dist/components/swatch-picker.js.map +1 -1
  160. package/dist/components/swipeable-row.d.ts +88 -17
  161. package/dist/components/swipeable-row.js +46 -9
  162. package/dist/components/swipeable-row.js.map +1 -1
  163. package/dist/components/switch.d.ts +2 -1
  164. package/dist/components/switch.js.map +1 -1
  165. package/dist/components/table.d.ts +2 -1
  166. package/dist/components/table.js +6 -0
  167. package/dist/components/table.js.map +1 -1
  168. package/dist/components/text-link.d.ts +9 -9
  169. package/dist/components/time-input.d.ts +10 -10
  170. package/dist/components/toggle-group.d.ts +2 -1
  171. package/dist/components/toggle-group.js +1 -1
  172. package/dist/components/toggle-group.js.map +1 -1
  173. package/dist/components/translation-review-editor.d.ts +4 -0
  174. package/dist/components/translation-review-editor.js +4 -3
  175. package/dist/components/translation-review-editor.js.map +1 -1
  176. package/dist/components/translation-review-parts.d.ts +1 -0
  177. package/dist/components/translation-review.d.ts +1 -0
  178. package/dist/components/translation-review.js +15 -18
  179. package/dist/components/translation-review.js.map +1 -1
  180. package/dist/components/ui.d.ts +9 -9
  181. package/dist/components/ui.js +47 -27
  182. package/dist/components/ui.js.map +1 -1
  183. package/dist/components/use-table-state.d.ts +12 -1
  184. package/dist/components/user-avatar.d.ts +1 -0
  185. package/dist/components/write-lock.d.ts +101 -13
  186. package/dist/components/write-lock.js +37 -13
  187. package/dist/components/write-lock.js.map +1 -1
  188. package/dist/{data-table-labels-kpAAVi6g.d.ts → data-table-labels-BJ5JE_X2.d.ts} +17 -0
  189. package/dist/data-table.d.ts +12 -1
  190. package/dist/{delete-account-setting-Dy4kJgUm.d.ts → delete-account-setting-DiPX4Vue.d.ts} +355 -41
  191. package/dist/feedback/feedback-attachment.d.ts +10 -10
  192. package/dist/feedback/feedback-attachment.js +7 -7
  193. package/dist/feedback/feedback-attachment.js.map +1 -1
  194. package/dist/feedback/feedback-context.d.ts +10 -10
  195. package/dist/feedback/feedback-crash.d.ts +10 -10
  196. package/dist/feedback/feedback-dialog.d.ts +10 -10
  197. package/dist/feedback/feedback-inbox.d.ts +9 -9
  198. package/dist/feedback/feedback-inbox.js +7 -3
  199. package/dist/feedback/feedback-inbox.js.map +1 -1
  200. package/dist/feedback/feedback-labels.d.ts +10 -10
  201. package/dist/feedback/feedback-menu.d.ts +3 -3
  202. package/dist/feedback/feedback-record.d.ts +10 -10
  203. package/dist/feedback/feedback-row-detail.d.ts +10 -10
  204. package/dist/feedback/feedback-row-detail.js.map +1 -1
  205. package/dist/feedback/feedback-status-undo.d.ts +10 -10
  206. package/dist/feedback/feedback-submit.d.ts +10 -10
  207. package/dist/feedback/feedback-swipe.d.ts +10 -10
  208. package/dist/feedback/feedback-swipe.js +3 -6
  209. package/dist/feedback/feedback-swipe.js.map +1 -1
  210. package/dist/feedback/feedback-table.d.ts +10 -10
  211. package/dist/feedback/feedback-table.js.map +1 -1
  212. package/dist/feedback/feedback-thread.d.ts +10 -10
  213. package/dist/feedback.d.ts +10 -10
  214. package/dist/hooks/use-file-drop.d.ts +10 -10
  215. package/dist/i18n/defaults.d.ts +10 -10
  216. package/dist/i18n/defaults.js +2 -0
  217. package/dist/i18n/defaults.js.map +1 -1
  218. package/dist/i18n/german.d.ts +10 -10
  219. package/dist/i18n/german.js +34 -1
  220. package/dist/i18n/german.js.map +1 -1
  221. package/dist/i18n/kit-labels.d.ts +4 -4
  222. package/dist/i18n/kit-labels.js.map +1 -1
  223. package/dist/i18n/languages.d.ts +10 -10
  224. package/dist/i18n/locales/de-CH.d.ts +10 -10
  225. package/dist/i18n/locales/en.d.ts +10 -10
  226. package/dist/i18n/locales/es.d.ts +10 -10
  227. package/dist/i18n/locales/es.js +34 -1
  228. package/dist/i18n/locales/es.js.map +1 -1
  229. package/dist/i18n/locales/fr.d.ts +10 -10
  230. package/dist/i18n/locales/fr.js +35 -1
  231. package/dist/i18n/locales/fr.js.map +1 -1
  232. package/dist/i18n/locales/hu.d.ts +10 -10
  233. package/dist/i18n/locales/hu.js +36 -1
  234. package/dist/i18n/locales/hu.js.map +1 -1
  235. package/dist/i18n/locales/it.d.ts +10 -10
  236. package/dist/i18n/locales/it.js +34 -1
  237. package/dist/i18n/locales/it.js.map +1 -1
  238. package/dist/i18n/locales/zh.d.ts +10 -10
  239. package/dist/i18n/locales/zh.js +34 -1
  240. package/dist/i18n/locales/zh.js.map +1 -1
  241. package/dist/i18n/review.d.ts +10 -10
  242. package/dist/i18n/review.js +8 -1
  243. package/dist/i18n/review.js.map +1 -1
  244. package/dist/index.d.ts +10 -5
  245. package/dist/index.js +40 -2
  246. package/dist/index.js.map +1 -1
  247. package/dist/landing/landing-actions.d.ts +10 -10
  248. package/dist/landing/landing-sections.d.ts +10 -10
  249. package/dist/landing/public-header.d.ts +10 -10
  250. package/dist/landing/routing.d.ts +10 -10
  251. package/dist/pay/.well-known/apple-developer-merchantid-domain-association +1 -0
  252. package/dist/pay/index.html +30 -0
  253. package/dist/pay/pay.css +126 -0
  254. package/dist/pay/pay.js +198 -0
  255. package/dist/rhf/fields.d.ts +10 -10
  256. package/dist/rhf/form.d.ts +10 -10
  257. package/dist/rhf.d.ts +10 -10
  258. package/dist/settings/settings-section.d.ts +10 -10
  259. package/dist/shell/app-shell.d.ts +9 -9
  260. package/dist/shell/top-bar-brand.d.ts +10 -10
  261. package/dist/shell/topbar-action-menu.d.ts +1 -0
  262. package/dist/shell.d.ts +10 -10
  263. package/dist/testing/css-variables.d.ts +74 -0
  264. package/dist/testing/css-variables.generated.d.ts +7 -0
  265. package/dist/testing/css-variables.generated.js +8 -0
  266. package/dist/testing/css-variables.generated.js.map +1 -0
  267. package/dist/testing/css-variables.js +93 -0
  268. package/dist/testing/css-variables.js.map +1 -0
  269. package/dist/testing/index.d.ts +1 -0
  270. package/dist/testing/index.js +12 -0
  271. package/dist/testing/index.js.map +1 -0
  272. package/dist/theme/contrast-tokens.d.ts +44 -1
  273. package/dist/theme/contrast-tokens.js +21 -0
  274. package/dist/theme/contrast-tokens.js.map +1 -1
  275. package/dist/theme/contrast.d.ts +2 -2
  276. package/dist/theme/palette-derive.js +60 -11
  277. package/dist/theme/palette-derive.js.map +1 -1
  278. package/dist/theme/palette-presets.js +45 -43
  279. package/dist/theme/palette-presets.js.map +1 -1
  280. package/dist/wizard/stepper-nav.d.ts +11 -11
  281. package/dist/wizard/stepper-nav.js.map +1 -1
  282. package/dist/wizard.d.ts +10 -10
  283. package/package.json +10 -2
  284. package/src/account/delete-account-setting.tsx +6 -1
  285. package/src/account/email-change-setting.tsx +9 -3
  286. package/src/account/sessions-setting.tsx +7 -2
  287. package/src/admin/admin-action-confirm.tsx +2 -1
  288. package/src/admin/invitations-panel.tsx +9 -3
  289. package/src/admin/plan-change.tsx +459 -0
  290. package/src/admin/reviewer-scope-editor.tsx +2 -1
  291. package/src/admin/role-select.tsx +2 -1
  292. package/src/admin/user-roster.tsx +5 -3
  293. package/src/auth/auth-errors.ts +25 -2
  294. package/src/auth/complete-name-dialog.tsx +6 -2
  295. package/src/billing/billing-banner.tsx +3 -1
  296. package/src/billing/billing-labels.ts +45 -0
  297. package/src/billing/billing-lock.ts +36 -3
  298. package/src/billing/billing-overview.ts +58 -0
  299. package/src/billing/checkout-processing.ts +391 -0
  300. package/src/billing/checkout-return.ts +90 -0
  301. package/src/billing/pay-page.ts +242 -0
  302. package/src/billing/plan-limit-toast.tsx +91 -0
  303. package/src/billing/subscription-actions.tsx +41 -8
  304. package/src/components/account-settings.tsx +6 -2
  305. package/src/components/autocomplete.tsx +5 -2
  306. package/src/components/button-group.tsx +4 -1
  307. package/src/components/calculator.tsx +10 -6
  308. package/src/components/checkbox-group.tsx +2 -2
  309. package/src/components/checkbox.tsx +2 -1
  310. package/src/components/chip.tsx +37 -32
  311. package/src/components/choice-card.tsx +4 -2
  312. package/src/components/combobox.tsx +4 -4
  313. package/src/components/confirm-dialog.tsx +13 -6
  314. package/src/components/country-select.tsx +2 -2
  315. package/src/components/danger-confirm.tsx +2 -2
  316. package/src/components/data-table.tsx +20 -9
  317. package/src/components/date-picker.tsx +10 -7
  318. package/src/components/entity-combobox.tsx +3 -3
  319. package/src/components/field-parts.tsx +61 -14
  320. package/src/components/file-button.tsx +5 -4
  321. package/src/components/file-dropzone.tsx +2 -2
  322. package/src/components/floating-panel.tsx +7 -5
  323. package/src/components/form-actions.tsx +12 -5
  324. package/src/components/inline-edit-field.tsx +13 -13
  325. package/src/components/list.tsx +10 -9
  326. package/src/components/month-picker.tsx +8 -2
  327. package/src/components/multi-entity-combobox.tsx +3 -3
  328. package/src/components/numpad-sheet.tsx +8 -4
  329. package/src/components/row-actions.tsx +208 -58
  330. package/src/components/share-card.tsx +67 -13
  331. package/src/components/status-dot.tsx +7 -7
  332. package/src/components/swatch-picker.tsx +2 -2
  333. package/src/components/swipeable-row.tsx +138 -27
  334. package/src/components/switch.tsx +2 -1
  335. package/src/components/table.tsx +8 -1
  336. package/src/components/toggle-group.tsx +5 -3
  337. package/src/components/translation-review-editor.tsx +8 -3
  338. package/src/components/translation-review.tsx +18 -22
  339. package/src/components/ui.tsx +92 -43
  340. package/src/components/write-lock.tsx +150 -24
  341. package/src/feedback/feedback-attachment.tsx +13 -9
  342. package/src/feedback/feedback-inbox.tsx +10 -6
  343. package/src/feedback/feedback-row-detail.tsx +3 -2
  344. package/src/feedback/feedback-swipe.tsx +6 -11
  345. package/src/feedback/feedback-table.tsx +2 -1
  346. package/src/i18n/defaults.ts +2 -0
  347. package/src/i18n/german.ts +38 -0
  348. package/src/i18n/kit-labels.tsx +4 -0
  349. package/src/i18n/locales/es.ts +34 -0
  350. package/src/i18n/locales/fr.ts +39 -0
  351. package/src/i18n/locales/hu.ts +37 -0
  352. package/src/i18n/locales/it.ts +35 -0
  353. package/src/i18n/locales/zh.ts +33 -0
  354. package/src/i18n/review.ts +7 -0
  355. package/src/index.ts +48 -4
  356. package/src/testing/css-variables.generated.ts +112 -0
  357. package/src/testing/css-variables.ts +191 -0
  358. package/src/testing/index.ts +13 -0
  359. package/src/theme/contrast-tokens.ts +53 -2
  360. package/src/theme/palette-derive.ts +87 -10
  361. package/src/theme/palette-presets.ts +45 -43
  362. package/src/wizard/stepper-nav.tsx +2 -1
  363. package/tokens.css +306 -46
@@ -52,10 +52,10 @@ function UserIdentityCell({
52
52
  avatar && /* @__PURE__ */ jsx(UserAvatar, { person, name, email, size: "sm" }),
53
53
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
54
54
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [
55
- /* @__PURE__ */ jsx("span", { "data-private": true, className: "truncate font-medium text-[var(--text-primary)]", children: named || address }),
55
+ /* @__PURE__ */ jsx("span", { "data-private": true, className: "truncate-until-large font-medium text-[var(--text-primary)]", children: named || address }),
56
56
  badge
57
57
  ] }),
58
- named && address && /* @__PURE__ */ jsx("div", { "data-private": true, className: "truncate text-xs text-[var(--text-muted)]", children: address })
58
+ named && address && /* @__PURE__ */ jsx("div", { "data-private": true, className: "truncate-until-large text-xs text-[var(--text-muted)]", children: address })
59
59
  ] })
60
60
  ] });
61
61
  }
@@ -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 } 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\";\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\nexport const DEFAULT_USER_ROSTER_LABELS: UserRosterLabels = {\n name: \"Name\",\n email: \"Email\",\n role: \"Role\",\n state: \"State\",\n created: \"Created\",\n lastLogin: \"Last login\",\n never: \"Never\",\n actions: \"Actions\",\n actionsFor: (name) => `Actions for ${name}`,\n};\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.\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 font-medium text-[var(--text-primary)]\">\n {named || address}\n </span>\n {badge}\n </div>\n {named && address && (\n <div data-private className=\"truncate text-xs text-[var(--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?: Partial<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 /**\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?: Partial<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 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-[var(--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 header: <span className=\"sr-only\">{labels.actions}</span>,\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\",\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 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 editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels,\n }),\n [roles, stateOf, extra, actions, 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 } 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\";\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\nexport const DEFAULT_USER_ROSTER_LABELS: UserRosterLabels = {\n name: \"Name\",\n email: \"Email\",\n role: \"Role\",\n state: \"State\",\n created: \"Created\",\n lastLogin: \"Last login\",\n never: \"Never\",\n actions: \"Actions\",\n actionsFor: (name) => `Actions for ${name}`,\n};\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-[var(--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-[var(--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?: Partial<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 /**\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?: Partial<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 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-[var(--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 header: <span className=\"sr-only\">{labels.actions}</span>,\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\",\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 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 editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels,\n }),\n [roles, stateOf, extra, actions, editableRole, onRoleChange, roleOf, emailColumn, labels],\n );\n}\n"],"mappings":"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","names":[]}
@@ -79,8 +79,19 @@ type DemoErrorCode = "demo_disabled" | "demo_rate_limited" | "demo_capacity" | "
79
79
  * creating gets `billing_read_only` instead, never this (§12.3).
80
80
  * - `billing_not_configured` — billing is switched on but the provider's settings are
81
81
  * missing (the secrets, a price id): the operator's mistake, not the payer's.
82
+ *
83
+ * 0.33.0 (server-kit 0.7, §14.2): the provider's refusals, with the `billing_` prefix of
84
+ * the others (decision 22).
85
+ * - `billing_provider_unavailable` — `502`: Paddle didn't answer, or refused the request.
86
+ * Try again.
87
+ * - `billing_not_at_provider` — `409`: a portal request before the payer reached the
88
+ * provider (§14.5) — a stale page; the overview's `at_provider` says it first.
89
+ * - `billing_already_subscribed` — `409`: a checkout while a provider subscription runs
90
+ * (§14.6). Plan changes go through the portal.
91
+ * - `billing_plan_not_sold` — `422`: a checkout for a plan, currency or interval the
92
+ * catalogue doesn't sell.
82
93
  */
83
- type BillingErrorCode = "billing_disabled" | "billing_read_only" | "plan_limit" | "billing_not_configured";
94
+ type BillingErrorCode = "billing_disabled" | "billing_read_only" | "plan_limit" | "billing_not_configured" | "billing_provider_unavailable" | "billing_not_at_provider" | "billing_already_subscribed" | "billing_plan_not_sold";
84
95
  /** Every code the kit reads: the sign-in refusals, the account ones (0.30.0), the demo's
85
96
  * (0.31.0) and billing's (0.32.0). */
86
97
  type KitErrorCode = AuthErrorCode | AccountErrorCode | DemoErrorCode | BillingErrorCode;
@@ -123,7 +134,7 @@ declare function authErrorCode(err: unknown): KitErrorCode | undefined;
123
134
  declare function isAuthError(err: unknown, code?: KitErrorCode): boolean;
124
135
  /**
125
136
  * Whether `err` is one of billing's coded refusals (0.32.0, docs/billing-harmonization.md
126
- * §10) — any of the four, or the one named. {@link isAuthError} answers the same for a
137
+ * §10, §14.2) — any of them, or the one named. {@link isAuthError} answers the same for a
127
138
  * named code; this one also answers "is it billing's at all", which an app's error
128
139
  * handler asks before it decides between the write lock, a `PlanLimitNotice` and a toast.
129
140
  */
@@ -3,7 +3,11 @@ const BILLING_CODES = /* @__PURE__ */ new Set([
3
3
  "billing_disabled",
4
4
  "billing_read_only",
5
5
  "plan_limit",
6
- "billing_not_configured"
6
+ "billing_not_configured",
7
+ "billing_provider_unavailable",
8
+ "billing_not_at_provider",
9
+ "billing_already_subscribed",
10
+ "billing_plan_not_sold"
7
11
  ]);
8
12
  const CODES = /* @__PURE__ */ new Set([
9
13
  "invalid_credentials",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/auth-errors.ts"],"sourcesContent":["/**\n * The coded refusals of sign-in and sign-up (docs/auth-harmonization.md §5.2): every\n * refusal the auth routes answer carries a machine-readable `code`, and the pages\n * recognise the CODE, never the server's sentence.\n *\n * Today each app's pages print the server's `detail` string (keksdose's\n * `extractApiErrorMessage`), which is English on every locale and, on a credential\n * check, would be an account-enumeration oracle if it ever said more than one thing.\n * keksdose already keys one refusal on a code (`features/budgets/plan-limit.ts`,\n * `402 {detail, code: \"plan_budget_limit\"}`); §5.2 makes that the rule for auth. Each\n * app switches its server and its pages together (§9).\n *\n * - `invalid_credentials` — unknown address, wrong password AND deactivated account:\n * one answer, in the same time (§2.10). `401`.\n * - `registration_closed` — the sign-up gate refused the address. `403`.\n * - `email_taken` — an account with this address exists. `409`.\n * - `invitation_invalid`, `invitation_expired` — the `invite` token of a sign-up or an\n * accept (§4.4).\n * - `token_invalid` — a one-time token (reset, verification, a sign-in challenge) is\n * unknown, spent or expired.\n * - `account_inactive` — **no server answers it.** §2.10 decided that a deactivated\n * account gets `invalid_credentials`, because a separate answer after a correct\n * password would confirm a leaked one. It stays in the union only for a future server\n * step that is past the password (an admin view, a signed-in refresh) and may say so.\n */\nexport type AuthErrorCode =\n | \"invalid_credentials\"\n | \"registration_closed\"\n | \"email_taken\"\n | \"invitation_invalid\"\n | \"invitation_expired\"\n | \"token_invalid\"\n /** 0.29.1: a link that WAS valid and ran out (verification, reset) — keksdose's step-3\n * refusals tell it from `token_invalid`, so a page can say \"expired, ask for a new one\". */\n | \"token_expired\"\n | \"account_inactive\";\n\n/**\n * 0.30.0: the coded refusals of user administration and the account's own settings\n * (docs/user-admin-harmonization.md §4, §6) — server-kit 0.4.0's `AccountErrorCode`,\n * value for value. The same family of answer as the sign-in codes (`{detail, code}`,\n * plus `companies` on kastlan's `last_admin`), read by the same {@link authErrorCode}.\n *\n * - `last_admin` — the action would leave no active admin: deactivating, demoting or\n * deleting the last one (in kastlan, of a company; the answer names the `companies`).\n * - `self_action` — an admin acting on their own account where that is never allowed.\n * - `other_companies` — kastlan: the account belongs to other companies too, so a\n * company admin may not deactivate it; \"Remove from company\" instead (§9.6).\n * - `household_has_members` — keksdose's guard on a deletion request (§6.4).\n * - `confirmation_required`, `confirmation_mismatch` — the action's confirmation level\n * asked for a typed address (or a tick), and the request carried none, or another\n * address (§4.2).\n * - `password_incorrect` — the CURRENT password is wrong on a signed-in route (the email\n * change, the deletion request). `400`, not `401`: an app's client reads a 401 as an\n * ended session and signs the user out, the wrong answer to a typo. Every other one\n * is a `409`.\n */\nexport type AccountErrorCode =\n | \"last_admin\"\n | \"self_action\"\n | \"other_companies\"\n | \"household_has_members\"\n | \"confirmation_required\"\n | \"confirmation_mismatch\"\n | \"password_incorrect\";\n\n/**\n * 0.31.0: the demo's coded refusals (docs/landing-demo-harmonization.md §6.2) —\n * server-kit 0.5's `DemoErrorCode`, value for value, answered as `{detail, code}` like\n * the others and read by the same {@link authErrorCode}.\n *\n * - `demo_disabled` — `404`: the demo switch is off (it is \"not there\", not forbidden).\n * - `demo_rate_limited` — `429` with `Retry-After`: the per-IP window of demo starts.\n * - `demo_capacity` — `429`, no `Retry-After`: the cap of live demo users.\n * - `demo_not_ready` — `503`: the demo data has not been seeded yet.\n * - `demo_read_only` — `403`, model R: any write by a demo user. The app's write lock\n * already explains it, so it shows no toast (§5.4).\n * - `demo_refused` — `403`, both models: an action a demo never may — a way in or out,\n * mail, uploads, the account export (§6.4).\n */\nexport type DemoErrorCode =\n | \"demo_disabled\"\n | \"demo_rate_limited\"\n | \"demo_capacity\"\n | \"demo_not_ready\"\n | \"demo_read_only\"\n | \"demo_refused\";\n\n/**\n * 0.32.0: billing's coded refusals (docs/billing-harmonization.md §3.3, §3.4, §4, §12.3) —\n * server-kit 0.6's `BillingError` and `PlanLimitError`, value for value, answered as\n * `{detail, code}` like the others and read by the same {@link authErrorCode}.\n *\n * - `billing_disabled` — `404`: the billing switch is off (§2.9), so every billing route\n * but `GET /billing/status` is \"not there\", as a switched-off demo is.\n * - `billing_read_only` — `402`: a write by (or into the data of) a payer out of good\n * standing (§3.3, §12.1). Inside a sync reply it marks the changes that were refused\n * and not applied (§12.5). The app's write lock and banner already say why, so — like\n * `demo_read_only` — it needs no toast of its own.\n * - `plan_limit` — `402`: a create past the plan's limit (§3.4), with the extra fields\n * `dimension`, `plan`, `limit` and `used` — read them with `isPlanLimit`. A lapsed payer\n * creating gets `billing_read_only` instead, never this (§12.3).\n * - `billing_not_configured` — billing is switched on but the provider's settings are\n * missing (the secrets, a price id): the operator's mistake, not the payer's.\n */\nexport type BillingErrorCode = \"billing_disabled\" | \"billing_read_only\" | \"plan_limit\" | \"billing_not_configured\";\n\n/** Every code the kit reads: the sign-in refusals, the account ones (0.30.0), the demo's\n * (0.31.0) and billing's (0.32.0). */\nexport type KitErrorCode = AuthErrorCode | AccountErrorCode | DemoErrorCode | BillingErrorCode;\n\nconst BILLING_CODES: ReadonlySet<BillingErrorCode> = new Set<BillingErrorCode>([\n \"billing_disabled\",\n \"billing_read_only\",\n \"plan_limit\",\n \"billing_not_configured\",\n]);\n\nconst CODES: ReadonlySet<string> = new Set<KitErrorCode>([\n \"invalid_credentials\",\n \"registration_closed\",\n \"email_taken\",\n \"invitation_invalid\",\n \"invitation_expired\",\n \"token_invalid\",\n \"token_expired\",\n \"account_inactive\",\n \"last_admin\",\n \"self_action\",\n \"other_companies\",\n \"household_has_members\",\n \"confirmation_required\",\n \"confirmation_mismatch\",\n \"password_incorrect\",\n \"demo_disabled\",\n \"demo_rate_limited\",\n \"demo_capacity\",\n \"demo_not_ready\",\n \"demo_read_only\",\n \"demo_refused\",\n ...BILLING_CODES,\n]);\n\ntype Bag = Record<string, unknown>;\n\nconst isBag = (value: unknown): value is Bag => typeof value === \"object\" && value !== null;\n\n/** A known code, or undefined. */\nfunction known(value: unknown): KitErrorCode | undefined {\n return typeof value === \"string\" && CODES.has(value) ? (value as KitErrorCode) : undefined;\n}\n\n/** `{code}` on a parsed body, or FastAPI's `{detail: {code}}` — what an\n * `HTTPException(detail={\"code\": …})` serialises to. */\nfunction codeOfBody(body: unknown): KitErrorCode | undefined {\n if (!isBag(body)) return undefined;\n return known(body.code) ?? (isBag(body.detail) ? known(body.detail.code) : undefined);\n}\n\n/**\n * The code an error carries — an {@link AuthErrorCode}, since 0.30.0 an\n * {@link AccountErrorCode}, since 0.31.0 a {@link DemoErrorCode}, since 0.32.0 a\n * {@link BillingErrorCode} — or `undefined` when it carries none.\n *\n * Reads the parsed response body wherever the apps' HTTP clients put it, without\n * depending on any of them:\n *\n * 1. `err.response.data` — axios (keksdose's, kastlan's and Kurvenschmiede's clients;\n * keksdose's `isPlanBudgetLimitRefusal` reads the same path);\n * 2. `err.data` — ofetch's `FetchError`, and most `fetch` wrappers that parse the body;\n * 3. `err.body` — the generated OpenAPI clients' `ApiError`;\n * 4. `err` itself — a body thrown as it came (`throw await res.json()`).\n *\n * In each, `code` or FastAPI's nested `detail.code`. Last, `err.code` — but only when\n * it is one of the codes above: axios writes its OWN codes there (`ERR_NETWORK`,\n * `ECONNABORTED`, `ERR_BAD_REQUEST`), and so does Node, so an unknown value is never\n * read as a refusal. The HTTP status is not checked: the code is the contract, and the\n * status differs per refusal (401, 402, 403, 404, 409, 410).\n */\nexport function authErrorCode(err: unknown): KitErrorCode | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n return (\n (isBag(response) ? codeOfBody(response.data) : undefined) ??\n codeOfBody(err.data) ??\n codeOfBody(err.body) ??\n codeOfBody(err)\n );\n}\n\n/**\n * Whether `err` is a coded auth refusal — any of them, or the one named:\n *\n * ```ts\n * try { await api.register(values) }\n * catch (err) { if (isAuthError(err, \"email_taken\")) … }\n * ```\n *\n * The kit's `SignInForm` and `RegisterForm` call it on what their callbacks reject\n * with, so an app's `onSubmit` can simply let its client's error through. See\n * {@link authErrorCode} for the shapes it reads. Since 0.30.0 it knows the account codes\n * too: `isAuthError(err, \"last_admin\")`; since 0.31.0 the demo's:\n * `isAuthError(err, \"demo_read_only\")`; since 0.32.0 billing's:\n * `isAuthError(err, \"billing_read_only\")` (or {@link isBillingError}).\n */\nexport function isAuthError(err: unknown, code?: KitErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && (code === undefined || found === code);\n}\n\n/**\n * Whether `err` is one of billing's coded refusals (0.32.0, docs/billing-harmonization.md\n * §10) — any of the four, or the one named. {@link isAuthError} answers the same for a\n * named code; this one also answers \"is it billing's at all\", which an app's error\n * handler asks before it decides between the write lock, a `PlanLimitNotice` and a toast.\n */\nexport function isBillingError(err: unknown, code?: BillingErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && BILLING_CODES.has(found as BillingErrorCode) && (code === undefined || found === code);\n}\n\n/**\n * The parsed body that carries a code, wherever {@link authErrorCode} would find it (in\n * the same order) — the object with the `code`, so FastAPI's nested `detail` when the code\n * is there. For a reader of a refusal's EXTRA fields (`isPlanLimit`: `dimension`, `limit`,\n * `used` sit beside the code). `accept` decides which codes count; default, the known ones.\n *\n * @internal Not part of the barrel.\n */\nexport function errorBodyWithCode(\n err: unknown,\n accept: (code: unknown) => boolean = (code) => known(code) !== undefined,\n): Record<string, unknown> | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n for (const body of [isBag(response) ? response.data : undefined, err.data, err.body, err]) {\n if (!isBag(body)) continue;\n if (accept(body.code)) return body;\n if (isBag(body.detail) && accept(body.detail.code)) return body.detail;\n }\n return undefined;\n}\n\n/** An HTTP status as a number, or undefined. */\nfunction statusOf(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) ? value : undefined;\n}\n\n/**\n * Whether `err` is a throttled request — HTTP `429 Too Many Requests` (0.30.0, from the\n * apps' 0.29 adoption: every app answered a throttled sign-in, sign-up or reset through\n * its own `describeError`, with three wordings of one sentence).\n *\n * A throttle carries no `code` in the contract — the STATUS is the answer, set by the\n * apps' limiters (server-kit's `AuthLimiters`, keksdose's per-IP limit) — so this reads\n * the status, wherever the apps' HTTP clients put it, without depending on any of them:\n *\n * 1. `err.response.status` — axios (all three apps' clients);\n * 2. `err.status` — ofetch's `FetchError`, the generated OpenAPI clients' `ApiError`, and\n * a `Response` thrown as it came;\n * 3. `err.statusCode` — ofetch's alias, and Node-style errors.\n *\n * There is no `rate_limited` code: keksdose answers its 429s uncoded, with a\n * `Retry-After` header, and the status is the one signal every limiter gives.\n *\n * The kit's `SignInForm`, `RegisterForm`, `ForgotPasswordForm` and `ResetPasswordForm`\n * call it AFTER the app's `describeError` — an app with words of its own keeps them —\n * and before their generic failure, so \"Too many attempts. Try again in 30 s.\" (with\n * {@link retryAfterSeconds}) or \"… Wait a moment and try again.\" needs no app code.\n */\nexport function isRateLimited(err: unknown): boolean {\n if (!isBag(err)) return false;\n const response = err.response;\n const status =\n (isBag(response) ? statusOf(response.status) : undefined) ?? statusOf(err.status) ?? statusOf(err.statusCode);\n return status === 429;\n}\n\n/** A header's value from a `Headers` (fetch, ofetch's `response`), axios' `AxiosHeaders`\n * or a plain record — whose keys may come in any case. */\nfunction headerOf(headers: unknown, name: string): unknown {\n if (!isBag(headers)) return undefined;\n if (typeof headers.get === \"function\") {\n try {\n const value: unknown = (headers.get as (key: string) => unknown).call(headers, name);\n if (value !== undefined && value !== null) return value;\n } catch {\n // A `get` that is not a header lookup; the record below is.\n }\n }\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === name) return value;\n }\n return undefined;\n}\n\n/** `Retry-After` as seconds from `now`: delta-seconds (\"30\"), or an HTTP date\n * (\"Wed, 07 Oct 2026 10:00:00 GMT\") counted from now, never below 0 (RFC 9110 §10.2.3). */\nfunction parseRetryAfter(value: unknown, now: number): number | undefined {\n const raw = Array.isArray(value) ? (value as unknown[])[0] : value;\n if (typeof raw === \"number\") return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : undefined;\n if (typeof raw !== \"string\") return undefined;\n const text = raw.trim();\n if (/^\\d+$/.test(text)) return Number(text);\n // An HTTP date names its day and month (\"Wed, 07 Oct 2026 …\"). Without a letter it is\n // a malformed number (\"-5\", \"1.5\"), which `Date.parse` would happily read as a year.\n if (!/[a-z]/i.test(text)) return undefined;\n const at = Date.parse(text);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, Math.ceil((at - now) / 1000));\n}\n\n/**\n * How long a throttled request asks to wait, in whole seconds — its `Retry-After`\n * header (0.30.0, keksdose: its 429s carry no code, only the header) — or `undefined`\n * when there is none, or none that parses.\n *\n * Read where the apps' clients keep the response headers, without depending on any of\n * them: `err.response.headers` (axios' `AxiosHeaders` or a plain record; ofetch's\n * `FetchError.response`, a fetch `Headers`), then `err.headers` (a `Response` thrown as\n * it came). The value is delta-seconds, or an HTTP date turned into seconds from `now`\n * (default: the present moment) — never below 0.\n *\n * The forms hand it to their `rateLimited` label when {@link isRateLimited} is true:\n * \"Too many attempts. Try again in 30 s.\" — and, without one, \"… Wait a moment and try\n * again.\" A browser only exposes `Retry-After` to a cross-origin page when the server\n * lists it in `Access-Control-Expose-Headers`.\n */\nexport function retryAfterSeconds(err: unknown, now: number | Date = Date.now()): number | undefined {\n if (!isBag(err)) return undefined;\n const at = typeof now === \"number\" ? now : now.getTime();\n const response = err.response;\n const value =\n (isBag(response) ? headerOf(response.headers, \"retry-after\") : undefined) ?? headerOf(err.headers, \"retry-after\");\n return value === undefined ? undefined : parseRetryAfter(value, at);\n}\n\n/**\n * The English of every `rateLimited` label (0.30.0): \"Too many attempts. Try again in\n * 30 s.\" with a wait under a minute, in whole minutes from one minute up (keksdose's\n * reset throttle answers in hours, and \"3600 s\" is a number nobody reads), and \"Wait a\n * moment and try again.\" without one — or with a `0`.\n *\n * @internal The namespaces' English; not part of the barrel.\n */\nexport function englishRateLimited(seconds?: number): string {\n if (!seconds || seconds <= 0) return \"Too many attempts. Wait a moment and try again.\";\n return `Too many attempts. Try again in ${englishWait(seconds)}.`;\n}\n\n/** \"30 s\", \"2 min\" — a wait in English, rounded up. @internal */\nexport function englishWait(seconds: number): string {\n return seconds < 60 ? `${Math.ceil(seconds)} s` : `${Math.ceil(seconds / 60)} min`;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/auth/auth-errors.ts"],"sourcesContent":["/**\n * The coded refusals of sign-in and sign-up (docs/auth-harmonization.md §5.2): every\n * refusal the auth routes answer carries a machine-readable `code`, and the pages\n * recognise the CODE, never the server's sentence.\n *\n * Today each app's pages print the server's `detail` string (keksdose's\n * `extractApiErrorMessage`), which is English on every locale and, on a credential\n * check, would be an account-enumeration oracle if it ever said more than one thing.\n * keksdose already keys one refusal on a code (`features/budgets/plan-limit.ts`,\n * `402 {detail, code: \"plan_budget_limit\"}`); §5.2 makes that the rule for auth. Each\n * app switches its server and its pages together (§9).\n *\n * - `invalid_credentials` — unknown address, wrong password AND deactivated account:\n * one answer, in the same time (§2.10). `401`.\n * - `registration_closed` — the sign-up gate refused the address. `403`.\n * - `email_taken` — an account with this address exists. `409`.\n * - `invitation_invalid`, `invitation_expired` — the `invite` token of a sign-up or an\n * accept (§4.4).\n * - `token_invalid` — a one-time token (reset, verification, a sign-in challenge) is\n * unknown, spent or expired.\n * - `account_inactive` — **no server answers it.** §2.10 decided that a deactivated\n * account gets `invalid_credentials`, because a separate answer after a correct\n * password would confirm a leaked one. It stays in the union only for a future server\n * step that is past the password (an admin view, a signed-in refresh) and may say so.\n */\nexport type AuthErrorCode =\n | \"invalid_credentials\"\n | \"registration_closed\"\n | \"email_taken\"\n | \"invitation_invalid\"\n | \"invitation_expired\"\n | \"token_invalid\"\n /** 0.29.1: a link that WAS valid and ran out (verification, reset) — keksdose's step-3\n * refusals tell it from `token_invalid`, so a page can say \"expired, ask for a new one\". */\n | \"token_expired\"\n | \"account_inactive\";\n\n/**\n * 0.30.0: the coded refusals of user administration and the account's own settings\n * (docs/user-admin-harmonization.md §4, §6) — server-kit 0.4.0's `AccountErrorCode`,\n * value for value. The same family of answer as the sign-in codes (`{detail, code}`,\n * plus `companies` on kastlan's `last_admin`), read by the same {@link authErrorCode}.\n *\n * - `last_admin` — the action would leave no active admin: deactivating, demoting or\n * deleting the last one (in kastlan, of a company; the answer names the `companies`).\n * - `self_action` — an admin acting on their own account where that is never allowed.\n * - `other_companies` — kastlan: the account belongs to other companies too, so a\n * company admin may not deactivate it; \"Remove from company\" instead (§9.6).\n * - `household_has_members` — keksdose's guard on a deletion request (§6.4).\n * - `confirmation_required`, `confirmation_mismatch` — the action's confirmation level\n * asked for a typed address (or a tick), and the request carried none, or another\n * address (§4.2).\n * - `password_incorrect` — the CURRENT password is wrong on a signed-in route (the email\n * change, the deletion request). `400`, not `401`: an app's client reads a 401 as an\n * ended session and signs the user out, the wrong answer to a typo. Every other one\n * is a `409`.\n */\nexport type AccountErrorCode =\n | \"last_admin\"\n | \"self_action\"\n | \"other_companies\"\n | \"household_has_members\"\n | \"confirmation_required\"\n | \"confirmation_mismatch\"\n | \"password_incorrect\";\n\n/**\n * 0.31.0: the demo's coded refusals (docs/landing-demo-harmonization.md §6.2) —\n * server-kit 0.5's `DemoErrorCode`, value for value, answered as `{detail, code}` like\n * the others and read by the same {@link authErrorCode}.\n *\n * - `demo_disabled` — `404`: the demo switch is off (it is \"not there\", not forbidden).\n * - `demo_rate_limited` — `429` with `Retry-After`: the per-IP window of demo starts.\n * - `demo_capacity` — `429`, no `Retry-After`: the cap of live demo users.\n * - `demo_not_ready` — `503`: the demo data has not been seeded yet.\n * - `demo_read_only` — `403`, model R: any write by a demo user. The app's write lock\n * already explains it, so it shows no toast (§5.4).\n * - `demo_refused` — `403`, both models: an action a demo never may — a way in or out,\n * mail, uploads, the account export (§6.4).\n */\nexport type DemoErrorCode =\n | \"demo_disabled\"\n | \"demo_rate_limited\"\n | \"demo_capacity\"\n | \"demo_not_ready\"\n | \"demo_read_only\"\n | \"demo_refused\";\n\n/**\n * 0.32.0: billing's coded refusals (docs/billing-harmonization.md §3.3, §3.4, §4, §12.3) —\n * server-kit 0.6's `BillingError` and `PlanLimitError`, value for value, answered as\n * `{detail, code}` like the others and read by the same {@link authErrorCode}.\n *\n * - `billing_disabled` — `404`: the billing switch is off (§2.9), so every billing route\n * but `GET /billing/status` is \"not there\", as a switched-off demo is.\n * - `billing_read_only` — `402`: a write by (or into the data of) a payer out of good\n * standing (§3.3, §12.1). Inside a sync reply it marks the changes that were refused\n * and not applied (§12.5). The app's write lock and banner already say why, so — like\n * `demo_read_only` — it needs no toast of its own.\n * - `plan_limit` — `402`: a create past the plan's limit (§3.4), with the extra fields\n * `dimension`, `plan`, `limit` and `used` — read them with `isPlanLimit`. A lapsed payer\n * creating gets `billing_read_only` instead, never this (§12.3).\n * - `billing_not_configured` — billing is switched on but the provider's settings are\n * missing (the secrets, a price id): the operator's mistake, not the payer's.\n *\n * 0.33.0 (server-kit 0.7, §14.2): the provider's refusals, with the `billing_` prefix of\n * the others (decision 22).\n * - `billing_provider_unavailable` — `502`: Paddle didn't answer, or refused the request.\n * Try again.\n * - `billing_not_at_provider` — `409`: a portal request before the payer reached the\n * provider (§14.5) — a stale page; the overview's `at_provider` says it first.\n * - `billing_already_subscribed` — `409`: a checkout while a provider subscription runs\n * (§14.6). Plan changes go through the portal.\n * - `billing_plan_not_sold` — `422`: a checkout for a plan, currency or interval the\n * catalogue doesn't sell.\n */\nexport type BillingErrorCode =\n | \"billing_disabled\"\n | \"billing_read_only\"\n | \"plan_limit\"\n | \"billing_not_configured\"\n | \"billing_provider_unavailable\"\n | \"billing_not_at_provider\"\n | \"billing_already_subscribed\"\n | \"billing_plan_not_sold\";\n\n/** Every code the kit reads: the sign-in refusals, the account ones (0.30.0), the demo's\n * (0.31.0) and billing's (0.32.0). */\nexport type KitErrorCode = AuthErrorCode | AccountErrorCode | DemoErrorCode | BillingErrorCode;\n\nconst BILLING_CODES: ReadonlySet<BillingErrorCode> = new Set<BillingErrorCode>([\n \"billing_disabled\",\n \"billing_read_only\",\n \"plan_limit\",\n \"billing_not_configured\",\n \"billing_provider_unavailable\",\n \"billing_not_at_provider\",\n \"billing_already_subscribed\",\n \"billing_plan_not_sold\",\n]);\n\nconst CODES: ReadonlySet<string> = new Set<KitErrorCode>([\n \"invalid_credentials\",\n \"registration_closed\",\n \"email_taken\",\n \"invitation_invalid\",\n \"invitation_expired\",\n \"token_invalid\",\n \"token_expired\",\n \"account_inactive\",\n \"last_admin\",\n \"self_action\",\n \"other_companies\",\n \"household_has_members\",\n \"confirmation_required\",\n \"confirmation_mismatch\",\n \"password_incorrect\",\n \"demo_disabled\",\n \"demo_rate_limited\",\n \"demo_capacity\",\n \"demo_not_ready\",\n \"demo_read_only\",\n \"demo_refused\",\n ...BILLING_CODES,\n]);\n\ntype Bag = Record<string, unknown>;\n\nconst isBag = (value: unknown): value is Bag => typeof value === \"object\" && value !== null;\n\n/** A known code, or undefined. */\nfunction known(value: unknown): KitErrorCode | undefined {\n return typeof value === \"string\" && CODES.has(value) ? (value as KitErrorCode) : undefined;\n}\n\n/** `{code}` on a parsed body, or FastAPI's `{detail: {code}}` — what an\n * `HTTPException(detail={\"code\": …})` serialises to. */\nfunction codeOfBody(body: unknown): KitErrorCode | undefined {\n if (!isBag(body)) return undefined;\n return known(body.code) ?? (isBag(body.detail) ? known(body.detail.code) : undefined);\n}\n\n/**\n * The code an error carries — an {@link AuthErrorCode}, since 0.30.0 an\n * {@link AccountErrorCode}, since 0.31.0 a {@link DemoErrorCode}, since 0.32.0 a\n * {@link BillingErrorCode} — or `undefined` when it carries none.\n *\n * Reads the parsed response body wherever the apps' HTTP clients put it, without\n * depending on any of them:\n *\n * 1. `err.response.data` — axios (keksdose's, kastlan's and Kurvenschmiede's clients;\n * keksdose's `isPlanBudgetLimitRefusal` reads the same path);\n * 2. `err.data` — ofetch's `FetchError`, and most `fetch` wrappers that parse the body;\n * 3. `err.body` — the generated OpenAPI clients' `ApiError`;\n * 4. `err` itself — a body thrown as it came (`throw await res.json()`).\n *\n * In each, `code` or FastAPI's nested `detail.code`. Last, `err.code` — but only when\n * it is one of the codes above: axios writes its OWN codes there (`ERR_NETWORK`,\n * `ECONNABORTED`, `ERR_BAD_REQUEST`), and so does Node, so an unknown value is never\n * read as a refusal. The HTTP status is not checked: the code is the contract, and the\n * status differs per refusal (401, 402, 403, 404, 409, 410).\n */\nexport function authErrorCode(err: unknown): KitErrorCode | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n return (\n (isBag(response) ? codeOfBody(response.data) : undefined) ??\n codeOfBody(err.data) ??\n codeOfBody(err.body) ??\n codeOfBody(err)\n );\n}\n\n/**\n * Whether `err` is a coded auth refusal — any of them, or the one named:\n *\n * ```ts\n * try { await api.register(values) }\n * catch (err) { if (isAuthError(err, \"email_taken\")) … }\n * ```\n *\n * The kit's `SignInForm` and `RegisterForm` call it on what their callbacks reject\n * with, so an app's `onSubmit` can simply let its client's error through. See\n * {@link authErrorCode} for the shapes it reads. Since 0.30.0 it knows the account codes\n * too: `isAuthError(err, \"last_admin\")`; since 0.31.0 the demo's:\n * `isAuthError(err, \"demo_read_only\")`; since 0.32.0 billing's:\n * `isAuthError(err, \"billing_read_only\")` (or {@link isBillingError}).\n */\nexport function isAuthError(err: unknown, code?: KitErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && (code === undefined || found === code);\n}\n\n/**\n * Whether `err` is one of billing's coded refusals (0.32.0, docs/billing-harmonization.md\n * §10, §14.2) — any of them, or the one named. {@link isAuthError} answers the same for a\n * named code; this one also answers \"is it billing's at all\", which an app's error\n * handler asks before it decides between the write lock, a `PlanLimitNotice` and a toast.\n */\nexport function isBillingError(err: unknown, code?: BillingErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && BILLING_CODES.has(found as BillingErrorCode) && (code === undefined || found === code);\n}\n\n/**\n * The parsed body that carries a code, wherever {@link authErrorCode} would find it (in\n * the same order) — the object with the `code`, so FastAPI's nested `detail` when the code\n * is there. For a reader of a refusal's EXTRA fields (`isPlanLimit`: `dimension`, `limit`,\n * `used` sit beside the code). `accept` decides which codes count; default, the known ones.\n *\n * @internal Not part of the barrel.\n */\nexport function errorBodyWithCode(\n err: unknown,\n accept: (code: unknown) => boolean = (code) => known(code) !== undefined,\n): Record<string, unknown> | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n for (const body of [isBag(response) ? response.data : undefined, err.data, err.body, err]) {\n if (!isBag(body)) continue;\n if (accept(body.code)) return body;\n if (isBag(body.detail) && accept(body.detail.code)) return body.detail;\n }\n return undefined;\n}\n\n/** An HTTP status as a number, or undefined. */\nfunction statusOf(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) ? value : undefined;\n}\n\n/**\n * Whether `err` is a throttled request — HTTP `429 Too Many Requests` (0.30.0, from the\n * apps' 0.29 adoption: every app answered a throttled sign-in, sign-up or reset through\n * its own `describeError`, with three wordings of one sentence).\n *\n * A throttle carries no `code` in the contract — the STATUS is the answer, set by the\n * apps' limiters (server-kit's `AuthLimiters`, keksdose's per-IP limit) — so this reads\n * the status, wherever the apps' HTTP clients put it, without depending on any of them:\n *\n * 1. `err.response.status` — axios (all three apps' clients);\n * 2. `err.status` — ofetch's `FetchError`, the generated OpenAPI clients' `ApiError`, and\n * a `Response` thrown as it came;\n * 3. `err.statusCode` — ofetch's alias, and Node-style errors.\n *\n * There is no `rate_limited` code: keksdose answers its 429s uncoded, with a\n * `Retry-After` header, and the status is the one signal every limiter gives.\n *\n * The kit's `SignInForm`, `RegisterForm`, `ForgotPasswordForm` and `ResetPasswordForm`\n * call it AFTER the app's `describeError` — an app with words of its own keeps them —\n * and before their generic failure, so \"Too many attempts. Try again in 30 s.\" (with\n * {@link retryAfterSeconds}) or \"… Wait a moment and try again.\" needs no app code.\n */\nexport function isRateLimited(err: unknown): boolean {\n if (!isBag(err)) return false;\n const response = err.response;\n const status =\n (isBag(response) ? statusOf(response.status) : undefined) ?? statusOf(err.status) ?? statusOf(err.statusCode);\n return status === 429;\n}\n\n/** A header's value from a `Headers` (fetch, ofetch's `response`), axios' `AxiosHeaders`\n * or a plain record — whose keys may come in any case. */\nfunction headerOf(headers: unknown, name: string): unknown {\n if (!isBag(headers)) return undefined;\n if (typeof headers.get === \"function\") {\n try {\n const value: unknown = (headers.get as (key: string) => unknown).call(headers, name);\n if (value !== undefined && value !== null) return value;\n } catch {\n // A `get` that is not a header lookup; the record below is.\n }\n }\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === name) return value;\n }\n return undefined;\n}\n\n/** `Retry-After` as seconds from `now`: delta-seconds (\"30\"), or an HTTP date\n * (\"Wed, 07 Oct 2026 10:00:00 GMT\") counted from now, never below 0 (RFC 9110 §10.2.3). */\nfunction parseRetryAfter(value: unknown, now: number): number | undefined {\n const raw = Array.isArray(value) ? (value as unknown[])[0] : value;\n if (typeof raw === \"number\") return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : undefined;\n if (typeof raw !== \"string\") return undefined;\n const text = raw.trim();\n if (/^\\d+$/.test(text)) return Number(text);\n // An HTTP date names its day and month (\"Wed, 07 Oct 2026 …\"). Without a letter it is\n // a malformed number (\"-5\", \"1.5\"), which `Date.parse` would happily read as a year.\n if (!/[a-z]/i.test(text)) return undefined;\n const at = Date.parse(text);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, Math.ceil((at - now) / 1000));\n}\n\n/**\n * How long a throttled request asks to wait, in whole seconds — its `Retry-After`\n * header (0.30.0, keksdose: its 429s carry no code, only the header) — or `undefined`\n * when there is none, or none that parses.\n *\n * Read where the apps' clients keep the response headers, without depending on any of\n * them: `err.response.headers` (axios' `AxiosHeaders` or a plain record; ofetch's\n * `FetchError.response`, a fetch `Headers`), then `err.headers` (a `Response` thrown as\n * it came). The value is delta-seconds, or an HTTP date turned into seconds from `now`\n * (default: the present moment) — never below 0.\n *\n * The forms hand it to their `rateLimited` label when {@link isRateLimited} is true:\n * \"Too many attempts. Try again in 30 s.\" — and, without one, \"… Wait a moment and try\n * again.\" A browser only exposes `Retry-After` to a cross-origin page when the server\n * lists it in `Access-Control-Expose-Headers`.\n */\nexport function retryAfterSeconds(err: unknown, now: number | Date = Date.now()): number | undefined {\n if (!isBag(err)) return undefined;\n const at = typeof now === \"number\" ? now : now.getTime();\n const response = err.response;\n const value =\n (isBag(response) ? headerOf(response.headers, \"retry-after\") : undefined) ?? headerOf(err.headers, \"retry-after\");\n return value === undefined ? undefined : parseRetryAfter(value, at);\n}\n\n/**\n * The English of every `rateLimited` label (0.30.0): \"Too many attempts. Try again in\n * 30 s.\" with a wait under a minute, in whole minutes from one minute up (keksdose's\n * reset throttle answers in hours, and \"3600 s\" is a number nobody reads), and \"Wait a\n * moment and try again.\" without one — or with a `0`.\n *\n * @internal The namespaces' English; not part of the barrel.\n */\nexport function englishRateLimited(seconds?: number): string {\n if (!seconds || seconds <= 0) return \"Too many attempts. Wait a moment and try again.\";\n return `Too many attempts. Try again in ${englishWait(seconds)}.`;\n}\n\n/** \"30 s\", \"2 min\" — a wait in English, rounded up. @internal */\nexport function englishWait(seconds: number): string {\n return seconds < 60 ? `${Math.ceil(seconds)} s` : `${Math.ceil(seconds / 60)} min`;\n}\n"],"mappings":"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","names":[]}
@@ -61,7 +61,10 @@ interface CompleteNameDialogProps {
61
61
  * The promise handling is `ReauthDialog`'s: busy while `onSave` runs (the fields
62
62
  * read-only, "Later" disabled, Escape held — a request that has left cannot be called
63
63
  * back by closing the dialog), closed on resolve, kept open on reject. Save is a
64
- * `commit` control, so a `WriteLockProvider` above it locks it with its reason.
64
+ * `commit` control, so a `WriteLockProvider` above it locks it with its reason — every
65
+ * lock but a lapsed plan's (`COMMIT_EXCEPT_BILLING`, 0.33): one's own name is the
66
+ * account's own settings, which billing never locks (docs/billing-harmonization.md §3.3,
67
+ * §12.36).
65
68
  *
66
69
  * Focus starts in the first EMPTY field — the last name, as a rule — because filling it
67
70
  * in is the whole of what the dialog is for.
@@ -7,6 +7,7 @@ import { DialogFrame } from "../components/dialog-frame.js";
7
7
  import { ModalCloseContext } from "../components/modal.js";
8
8
  import { Button, Input } from "../components/ui.js";
9
9
  import { personNameOk, PERSON_NAME_MAX_LENGTH } from "./form-rules.js";
10
+ import { COMMIT_EXCEPT_BILLING } from "../components/write-lock.js";
10
11
  const DEFAULT_COMPLETE_NAME_LABELS = {
11
12
  title: "Complete your name",
12
13
  description: "First and last name are now asked for separately. Check what is filled in and add what is missing.",
@@ -158,7 +159,7 @@ function CompleteNameActions({
158
159
  }, [done, close]);
159
160
  return /* @__PURE__ */ jsxs(Fragment, { children: [
160
161
  /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", disabled: busy, onClick: () => close?.(), children: laterLabel }),
161
- /* @__PURE__ */ jsx(Button, { type: "submit", form: formId, commit: true, disabled: !busy && !canSave, pending: busy, children: saveLabel })
162
+ /* @__PURE__ */ jsx(Button, { type: "submit", form: formId, commit: COMMIT_EXCEPT_BILLING, disabled: !busy && !canSave, pending: busy, children: saveLabel })
162
163
  ] });
163
164
  }
164
165
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/complete-name-dialog.tsx"],"sourcesContent":["import { useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { usePromisePending } from \"../components/danger-confirm\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { ModalCloseContext } from \"../components/modal\";\nimport { Button, Input } from \"../components/ui\";\nimport { personNameOk, PERSON_NAME_MAX_LENGTH } from \"./form-rules\";\n\n/** What `CompleteNameDialog` hands `onSave`: both names, trimmed, 1–120 characters —\n * `PATCH /auth/me {first_name, last_name}` (§6.1). */\nexport interface CompleteNameValues {\n firstName: string;\n lastName: string;\n}\n\n/**\n * Every string the dialog renders — the `completeName` namespace of `<UiKitProvider\n * labels>`, overridable per instance through `labels`.\n */\nexport interface CompleteNameLabels {\n title: string;\n description: string;\n firstName: string;\n lastName: string;\n save: string;\n /** Dismisses the dialog until the next sign-in. */\n later: string;\n /** Under the fields when `onSave` rejected and the app passed no `error`. */\n failed: string;\n}\n\nexport const DEFAULT_COMPLETE_NAME_LABELS: CompleteNameLabels = {\n title: \"Complete your name\",\n description: \"First and last name are now asked for separately. Check what is filled in and add what is missing.\",\n firstName: \"First name\",\n lastName: \"Last name\",\n save: \"Save\",\n later: \"Later\",\n failed: \"Your name could not be saved. Please try again.\",\n};\n\nexport interface CompleteNameDialogProps {\n /** What the account holds now — after the migration, the old display name (§3.3). */\n firstName?: string;\n /** Usually empty: that is why the dialog is up. */\n lastName?: string;\n /**\n * Save the names (`PATCH /auth/me`). Resolve and the dialog closes (`onClose(true)`);\n * reject and it stays open with the names kept and `error` — or \"Your name could not\n * be saved\" — under them.\n */\n onSave: (values: CompleteNameValues) => Promise<unknown>;\n /**\n * The dialog has closed: `true` after a resolved `onSave`, `false` for \"Later\", Escape,\n * the backdrop and Back. `false` only DEFERS it: the app shows it again after the\n * next sign-in while `/auth/me` still answers `name_incomplete: true`.\n */\n onClose: (saved: boolean) => void;\n /** The app's words for the last failed save, instead of `labels.failed`. Hidden again\n * once a name is edited. */\n error?: ReactNode;\n /** Kept-mounted mode, as on `Modal`: left out, the dialog is open while mounted. */\n open?: boolean;\n labels?: Partial<CompleteNameLabels>;\n}\n\n/**\n * \"Complete your name\" — asked once after sign-in of a user whose name was migrated\n * (docs/auth-harmonization.md §3.3). keksdose's and Kurvenschmiede's accounts had one\n * `display_name`; it moved whole into `first_name`, `last_name` stayed empty, and\n * nothing was split by guesswork (§2.4). The app shows this while `/auth/me` answers\n * `name_incomplete: true` — never for a demo user.\n *\n * Both names prefilled with what is there; \"Save\" waits until both have 1–120\n * characters after trimming. **\"Later\" only defers it**: nothing else waits on a\n * complete name, no page is blocked, and the app asks again at the next sign-in.\n *\n * The promise handling is `ReauthDialog`'s: busy while `onSave` runs (the fields\n * read-only, \"Later\" disabled, Escape held — a request that has left cannot be called\n * back by closing the dialog), closed on resolve, kept open on reject. Save is a\n * `commit` control, so a `WriteLockProvider` above it locks it with its reason.\n *\n * Focus starts in the first EMPTY field — the last name, as a rule — because filling it\n * in is the whole of what the dialog is for.\n */\nexport function CompleteNameDialog({\n firstName: initialFirst = \"\",\n lastName: initialLast = \"\",\n onSave,\n onClose,\n error,\n open,\n labels: labelsProp,\n}: CompleteNameDialogProps) {\n const labels = useKitLabels(\"completeName\", DEFAULT_COMPLETE_NAME_LABELS, labelsProp);\n const [first, setFirst] = useState(initialFirst);\n const [last, setLast] = useState(initialLast);\n // A failed save describes the names that were sent; an edit makes it stale.\n const [failed, setFailed] = useState(false);\n const { pending, run } = usePromisePending();\n const [done, setDone] = useState(false);\n const saved = useRef(false);\n const formId = useId();\n const firstRef = useRef<HTMLInputElement>(null);\n const lastRef = useRef<HTMLInputElement>(null);\n const isOpen = open ?? true;\n\n // Reopened (kept-mounted): start again from what the account holds. During render,\n // as ReauthDialog wipes its password, so no frame shows the old draft.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (isOpen) {\n setFirst(initialFirst);\n setLast(initialLast);\n setFailed(false);\n setDone(false);\n }\n }\n\n // After Modal's own effect, so its trap has recorded the trigger before focus moves.\n useEffect(() => {\n if (!isOpen) return;\n saved.current = false;\n const target = firstRef.current?.value.trim() ? lastRef.current : firstRef.current;\n target?.focus();\n }, [isOpen]);\n\n const canSave = personNameOk(first) && personNameOk(last);\n\n const submit = () => {\n if (pending || !canSave) return;\n setFailed(false);\n const values = { firstName: first.trim(), lastName: last.trim() };\n let result: Promise<unknown>;\n try {\n result = Promise.resolve(onSave(values));\n } catch (err) {\n result = Promise.reject(err);\n }\n // A rejection must not go unhandled; usePromisePending clears `pending` on it.\n result.catch(() => setFailed(true));\n run(result, () => {\n saved.current = true;\n setDone(true);\n });\n };\n\n const edit = (set: (value: string) => void) => (value: string) => {\n set(value);\n setFailed(false);\n };\n\n const message = failed ? (error ?? labels.failed) : undefined;\n\n return (\n <DialogFrame\n open={open}\n title={labels.title}\n description={labels.description}\n onClose={() => onClose(saved.current)}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (pending && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={pending || undefined}\n actions={\n <CompleteNameActions\n formId={formId}\n done={done}\n busy={pending}\n canSave={canSave}\n laterLabel={labels.later}\n saveLabel={labels.save}\n />\n }\n >\n <form\n id={formId}\n noValidate\n className=\"grid gap-3 @container\"\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n <div className=\"grid gap-3 @xs:grid-cols-2\">\n <Input\n ref={firstRef}\n name=\"given-name\"\n autoComplete=\"given-name\"\n label={labels.firstName}\n value={first}\n onChange={(e) => edit(setFirst)(e.target.value)}\n maxLength={PERSON_NAME_MAX_LENGTH}\n readOnly={pending}\n required\n />\n <Input\n ref={lastRef}\n name=\"family-name\"\n autoComplete=\"family-name\"\n label={labels.lastName}\n value={last}\n onChange={(e) => edit(setLast)(e.target.value)}\n maxLength={PERSON_NAME_MAX_LENGTH}\n readOnly={pending}\n required\n />\n </div>\n {message !== undefined && (\n <p role=\"alert\" className=\"text-xs text-[var(--danger)]\">\n {message}\n </p>\n )}\n </form>\n </DialogFrame>\n );\n}\n\n/** The actions row, inside the `Modal`, where its animated close is in context: a\n * resolved save lowers the panel through it, as \"Later\" and Escape do. */\nfunction CompleteNameActions({\n formId,\n done,\n busy,\n canSave,\n laterLabel,\n saveLabel,\n}: {\n formId: string;\n done: boolean;\n busy: boolean;\n canSave: boolean;\n laterLabel: string;\n saveLabel: string;\n}) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return (\n <>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={() => close?.()}>\n {laterLabel}\n </Button>\n <Button type=\"submit\" form={formId} commit disabled={!busy && !canSave} pending={busy}>\n {saveLabel}\n </Button>\n </>\n );\n}\n"],"mappings":";AAwKQ,SA2EJ,UA3EI,KAmBA,YAnBA;AAxKR,SAAS,YAAY,WAAW,OAAO,QAAQ,gBAAgB;AAG/D,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,QAAQ,aAAa;AAC9B,SAAS,cAAc,8BAA8B;AAyB9C,MAAM,+BAAmD;AAAA,EAC9D,OAAO;AAAA,EACP,aAAa;AAAA,EACb,WAAW;AAAA,EACX,UAAU;AAAA,EACV,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AACV;AA8CO,SAAS,mBAAmB;AAAA,EACjC,WAAW,eAAe;AAAA,EAC1B,UAAU,cAAc;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAA4B;AAC1B,QAAM,SAAS,aAAa,gBAAgB,8BAA8B,UAAU;AACpF,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,YAAY;AAC/C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,WAAW;AAE5C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,EAAE,SAAS,IAAI,IAAI,kBAAkB;AAC3C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM,SAAS,MAAM;AACrB,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,UAAU,OAAyB,IAAI;AAC7C,QAAM,SAAS,QAAQ;AAIvB,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,MAAM;AAC7C,MAAI,YAAY,QAAQ;AACtB,eAAW,MAAM;AACjB,QAAI,QAAQ;AACV,eAAS,YAAY;AACrB,cAAQ,WAAW;AACnB,gBAAU,KAAK;AACf,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAGA,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,UAAM,UAAU;AAChB,UAAM,SAAS,SAAS,SAAS,MAAM,KAAK,IAAI,QAAQ,UAAU,SAAS;AAC3E,YAAQ,MAAM;AAAA,EAChB,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,UAAU,aAAa,KAAK,KAAK,aAAa,IAAI;AAExD,QAAM,SAAS,MAAM;AACnB,QAAI,WAAW,CAAC,QAAS;AACzB,cAAU,KAAK;AACf,UAAM,SAAS,EAAE,WAAW,MAAM,KAAK,GAAG,UAAU,KAAK,KAAK,EAAE;AAChE,QAAI;AACJ,QAAI;AACF,eAAS,QAAQ,QAAQ,OAAO,MAAM,CAAC;AAAA,IACzC,SAAS,KAAK;AACZ,eAAS,QAAQ,OAAO,GAAG;AAAA,IAC7B;AAEA,WAAO,MAAM,MAAM,UAAU,IAAI,CAAC;AAClC,QAAI,QAAQ,MAAM;AAChB,YAAM,UAAU;AAChB,cAAQ,IAAI;AAAA,IACd,CAAC;AAAA,EACH;AAEA,QAAM,OAAO,CAAC,QAAiC,CAAC,UAAkB;AAChE,QAAI,KAAK;AACT,cAAU,KAAK;AAAA,EACjB;AAEA,QAAM,UAAU,SAAU,SAAS,OAAO,SAAU;AAEpD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,SAAS,MAAM,QAAQ,MAAM,OAAO;AAAA,MACpC,WAAW,CAAC,MAAqC;AAC/C,YAAI,WAAW,EAAE,QAAQ,SAAU,GAAE,eAAe;AAAA,MACtD;AAAA,MACA,aAAW,WAAW;AAAA,MACtB,SACE;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN;AAAA,UACA,YAAY,OAAO;AAAA,UACnB,WAAW,OAAO;AAAA;AAAA,MACpB;AAAA,MAGF;AAAA,QAAC;AAAA;AAAA,UACC,IAAI;AAAA,UACJ,YAAU;AAAA,UACV,WAAU;AAAA,UACV,UAAU,CAAC,MAAM;AACf,cAAE,eAAe;AACjB,mBAAO;AAAA,UACT;AAAA,UAEA;AAAA,iCAAC,SAAI,WAAU,8BACb;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,KAAK;AAAA,kBACL,MAAK;AAAA,kBACL,cAAa;AAAA,kBACb,OAAO,OAAO;AAAA,kBACd,OAAO;AAAA,kBACP,UAAU,CAAC,MAAM,KAAK,QAAQ,EAAE,EAAE,OAAO,KAAK;AAAA,kBAC9C,WAAW;AAAA,kBACX,UAAU;AAAA,kBACV,UAAQ;AAAA;AAAA,cACV;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,KAAK;AAAA,kBACL,MAAK;AAAA,kBACL,cAAa;AAAA,kBACb,OAAO,OAAO;AAAA,kBACd,OAAO;AAAA,kBACP,UAAU,CAAC,MAAM,KAAK,OAAO,EAAE,EAAE,OAAO,KAAK;AAAA,kBAC7C,WAAW;AAAA,kBACX,UAAU;AAAA,kBACV,UAAQ;AAAA;AAAA,cACV;AAAA,eACF;AAAA,YACC,YAAY,UACX,oBAAC,OAAE,MAAK,SAAQ,WAAU,gCACvB,mBACH;AAAA;AAAA;AAAA,MAEJ;AAAA;AAAA,EACF;AAEJ;AAIA,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,QAAQ,WAAW,iBAAiB;AAC1C,YAAU,MAAM;AACd,QAAI,KAAM,SAAQ;AAAA,EACpB,GAAG,CAAC,MAAM,KAAK,CAAC;AAChB,SACE,iCACE;AAAA,wBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,UAAU,MAAM,SAAS,MAAM,QAAQ,GAC1E,sBACH;AAAA,IACA,oBAAC,UAAO,MAAK,UAAS,MAAM,QAAQ,QAAM,MAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,SAAS,MAC9E,qBACH;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/auth/complete-name-dialog.tsx"],"sourcesContent":["import { useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { usePromisePending } from \"../components/danger-confirm\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { ModalCloseContext } from \"../components/modal\";\nimport { Button, Input } from \"../components/ui\";\nimport { personNameOk, PERSON_NAME_MAX_LENGTH } from \"./form-rules\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\n\n/** What `CompleteNameDialog` hands `onSave`: both names, trimmed, 1–120 characters —\n * `PATCH /auth/me {first_name, last_name}` (§6.1). */\nexport interface CompleteNameValues {\n firstName: string;\n lastName: string;\n}\n\n/**\n * Every string the dialog renders — the `completeName` namespace of `<UiKitProvider\n * labels>`, overridable per instance through `labels`.\n */\nexport interface CompleteNameLabels {\n title: string;\n description: string;\n firstName: string;\n lastName: string;\n save: string;\n /** Dismisses the dialog until the next sign-in. */\n later: string;\n /** Under the fields when `onSave` rejected and the app passed no `error`. */\n failed: string;\n}\n\nexport const DEFAULT_COMPLETE_NAME_LABELS: CompleteNameLabels = {\n title: \"Complete your name\",\n description: \"First and last name are now asked for separately. Check what is filled in and add what is missing.\",\n firstName: \"First name\",\n lastName: \"Last name\",\n save: \"Save\",\n later: \"Later\",\n failed: \"Your name could not be saved. Please try again.\",\n};\n\nexport interface CompleteNameDialogProps {\n /** What the account holds now — after the migration, the old display name (§3.3). */\n firstName?: string;\n /** Usually empty: that is why the dialog is up. */\n lastName?: string;\n /**\n * Save the names (`PATCH /auth/me`). Resolve and the dialog closes (`onClose(true)`);\n * reject and it stays open with the names kept and `error` — or \"Your name could not\n * be saved\" — under them.\n */\n onSave: (values: CompleteNameValues) => Promise<unknown>;\n /**\n * The dialog has closed: `true` after a resolved `onSave`, `false` for \"Later\", Escape,\n * the backdrop and Back. `false` only DEFERS it: the app shows it again after the\n * next sign-in while `/auth/me` still answers `name_incomplete: true`.\n */\n onClose: (saved: boolean) => void;\n /** The app's words for the last failed save, instead of `labels.failed`. Hidden again\n * once a name is edited. */\n error?: ReactNode;\n /** Kept-mounted mode, as on `Modal`: left out, the dialog is open while mounted. */\n open?: boolean;\n labels?: Partial<CompleteNameLabels>;\n}\n\n/**\n * \"Complete your name\" — asked once after sign-in of a user whose name was migrated\n * (docs/auth-harmonization.md §3.3). keksdose's and Kurvenschmiede's accounts had one\n * `display_name`; it moved whole into `first_name`, `last_name` stayed empty, and\n * nothing was split by guesswork (§2.4). The app shows this while `/auth/me` answers\n * `name_incomplete: true` — never for a demo user.\n *\n * Both names prefilled with what is there; \"Save\" waits until both have 1–120\n * characters after trimming. **\"Later\" only defers it**: nothing else waits on a\n * complete name, no page is blocked, and the app asks again at the next sign-in.\n *\n * The promise handling is `ReauthDialog`'s: busy while `onSave` runs (the fields\n * read-only, \"Later\" disabled, Escape held — a request that has left cannot be called\n * back by closing the dialog), closed on resolve, kept open on reject. Save is a\n * `commit` control, so a `WriteLockProvider` above it locks it with its reason — every\n * lock but a lapsed plan's (`COMMIT_EXCEPT_BILLING`, 0.33): one's own name is the\n * account's own settings, which billing never locks (docs/billing-harmonization.md §3.3,\n * §12.36).\n *\n * Focus starts in the first EMPTY field — the last name, as a rule — because filling it\n * in is the whole of what the dialog is for.\n */\nexport function CompleteNameDialog({\n firstName: initialFirst = \"\",\n lastName: initialLast = \"\",\n onSave,\n onClose,\n error,\n open,\n labels: labelsProp,\n}: CompleteNameDialogProps) {\n const labels = useKitLabels(\"completeName\", DEFAULT_COMPLETE_NAME_LABELS, labelsProp);\n const [first, setFirst] = useState(initialFirst);\n const [last, setLast] = useState(initialLast);\n // A failed save describes the names that were sent; an edit makes it stale.\n const [failed, setFailed] = useState(false);\n const { pending, run } = usePromisePending();\n const [done, setDone] = useState(false);\n const saved = useRef(false);\n const formId = useId();\n const firstRef = useRef<HTMLInputElement>(null);\n const lastRef = useRef<HTMLInputElement>(null);\n const isOpen = open ?? true;\n\n // Reopened (kept-mounted): start again from what the account holds. During render,\n // as ReauthDialog wipes its password, so no frame shows the old draft.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (isOpen) {\n setFirst(initialFirst);\n setLast(initialLast);\n setFailed(false);\n setDone(false);\n }\n }\n\n // After Modal's own effect, so its trap has recorded the trigger before focus moves.\n useEffect(() => {\n if (!isOpen) return;\n saved.current = false;\n const target = firstRef.current?.value.trim() ? lastRef.current : firstRef.current;\n target?.focus();\n }, [isOpen]);\n\n const canSave = personNameOk(first) && personNameOk(last);\n\n const submit = () => {\n if (pending || !canSave) return;\n setFailed(false);\n const values = { firstName: first.trim(), lastName: last.trim() };\n let result: Promise<unknown>;\n try {\n result = Promise.resolve(onSave(values));\n } catch (err) {\n result = Promise.reject(err);\n }\n // A rejection must not go unhandled; usePromisePending clears `pending` on it.\n result.catch(() => setFailed(true));\n run(result, () => {\n saved.current = true;\n setDone(true);\n });\n };\n\n const edit = (set: (value: string) => void) => (value: string) => {\n set(value);\n setFailed(false);\n };\n\n const message = failed ? (error ?? labels.failed) : undefined;\n\n return (\n <DialogFrame\n open={open}\n title={labels.title}\n description={labels.description}\n onClose={() => onClose(saved.current)}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (pending && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={pending || undefined}\n actions={\n <CompleteNameActions\n formId={formId}\n done={done}\n busy={pending}\n canSave={canSave}\n laterLabel={labels.later}\n saveLabel={labels.save}\n />\n }\n >\n <form\n id={formId}\n noValidate\n className=\"grid gap-3 @container\"\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n <div className=\"grid gap-3 @xs:grid-cols-2\">\n <Input\n ref={firstRef}\n name=\"given-name\"\n autoComplete=\"given-name\"\n label={labels.firstName}\n value={first}\n onChange={(e) => edit(setFirst)(e.target.value)}\n maxLength={PERSON_NAME_MAX_LENGTH}\n readOnly={pending}\n required\n />\n <Input\n ref={lastRef}\n name=\"family-name\"\n autoComplete=\"family-name\"\n label={labels.lastName}\n value={last}\n onChange={(e) => edit(setLast)(e.target.value)}\n maxLength={PERSON_NAME_MAX_LENGTH}\n readOnly={pending}\n required\n />\n </div>\n {message !== undefined && (\n <p role=\"alert\" className=\"text-xs text-[var(--danger)]\">\n {message}\n </p>\n )}\n </form>\n </DialogFrame>\n );\n}\n\n/** The actions row, inside the `Modal`, where its animated close is in context: a\n * resolved save lowers the panel through it, as \"Later\" and Escape do. */\nfunction CompleteNameActions({\n formId,\n done,\n busy,\n canSave,\n laterLabel,\n saveLabel,\n}: {\n formId: string;\n done: boolean;\n busy: boolean;\n canSave: boolean;\n laterLabel: string;\n saveLabel: string;\n}) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return (\n <>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={() => close?.()}>\n {laterLabel}\n </Button>\n <Button type=\"submit\" form={formId} commit={COMMIT_EXCEPT_BILLING} disabled={!busy && !canSave} pending={busy}>\n {saveLabel}\n </Button>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,18 +1,23 @@
1
1
  import 'react';
2
- export { cL as DEFAULT_REGISTER_LABELS, eu as RegisterForm, ev as RegisterFormProps, ew as RegisterLabels, ex as RegisterValues } from '../delete-account-setting-Dy4kJgUm.js';
2
+ export { cM as DEFAULT_REGISTER_LABELS, eF as RegisterForm, eG as RegisterFormProps, eH as RegisterLabels, eI as RegisterValues } from '../delete-account-setting-DiPX4Vue.js';
3
3
  import '../components/tooltip.js';
4
4
  import '../hooks/use-anchored-rect.js';
5
5
  import '../lib/clipping.js';
6
- import '../data-table-labels-kpAAVi6g.js';
6
+ import '../data-table-labels-BJ5JE_X2.js';
7
7
  import '../components/data-table-sort.js';
8
8
  import '../components/swipeable-row.js';
9
+ import '../components/status-dot.js';
10
+ import '../components/progress-bar.js';
11
+ import '../components/signed-amount.js';
12
+ import '../components/stat-tile.js';
13
+ import '../components/chip.js';
14
+ import 'lucide-react';
15
+ import '../components/field-parts.js';
16
+ import '../components/write-lock.js';
9
17
  import '../components/mini-calendar.js';
10
18
  import '../components/calendar-heatmap.js';
11
19
  import '../components/popover.js';
12
20
  import '../hooks/use-breakpoint.js';
13
- import '../components/chip.js';
14
- import 'lucide-react';
15
- import '../components/field-parts.js';
16
21
  import '../components/field-sync.js';
17
22
  import '../wizard/types.js';
18
23
  import '../tour/tour.js';
@@ -24,7 +29,6 @@ import '../components/page-contents.js';
24
29
  import '../components/series-chart-labels.js';
25
30
  import '../components/pie-chart-labels.js';
26
31
  import '../components/sparkline.js';
27
- import '../components/stat-tile.js';
28
32
  import '../components/signature-pad.js';
29
33
  import '../components/password-strength.js';
30
34
  import '../components/swatch-picker.js';
@@ -36,9 +40,6 @@ import '../hooks/use-search-param-state.js';
36
40
  import '../components/measured-grid.js';
37
41
  import '../components/user-avatar.js';
38
42
  import '../lib/person-name.js';
39
- import '../components/status-dot.js';
40
- import '../components/progress-bar.js';
41
- import '../components/signed-amount.js';
42
43
  import '../shell/topbar-action-menu.js';
43
44
  import '../components/menu-item.js';
44
45
  import '../components/floating-panel.js';
@@ -54,7 +55,6 @@ import '../components/error-boundary.js';
54
55
  import '../components/authed-image.js';
55
56
  import '../components/image-grid.js';
56
57
  import '../components/lightbox.js';
57
- import '../components/write-lock.js';
58
58
  import './sign-in-form.js';
59
59
  import '../landing/access.js';
60
60
  import '../landing/landing-labels.js';
@@ -1,20 +1,25 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef } from 'react';
3
- import { e4 as LabelOverride, bH as BillingLabels } from '../delete-account-setting-Dy4kJgUm.js';
3
+ import { e5 as LabelOverride, bH as BillingLabels } from '../delete-account-setting-DiPX4Vue.js';
4
4
  import { AlertTone, AlertSize } from '../components/alert-banner.js';
5
5
  import '../components/tooltip.js';
6
6
  import '../hooks/use-anchored-rect.js';
7
7
  import '../lib/clipping.js';
8
- import '../data-table-labels-kpAAVi6g.js';
8
+ import '../data-table-labels-BJ5JE_X2.js';
9
9
  import '../components/data-table-sort.js';
10
10
  import '../components/swipeable-row.js';
11
+ import '../components/status-dot.js';
12
+ import '../components/progress-bar.js';
13
+ import '../components/signed-amount.js';
14
+ import '../components/stat-tile.js';
15
+ import '../components/chip.js';
16
+ import 'lucide-react';
17
+ import '../components/field-parts.js';
18
+ import '../components/write-lock.js';
11
19
  import '../components/mini-calendar.js';
12
20
  import '../components/calendar-heatmap.js';
13
21
  import '../components/popover.js';
14
22
  import '../hooks/use-breakpoint.js';
15
- import '../components/chip.js';
16
- import 'lucide-react';
17
- import '../components/field-parts.js';
18
23
  import '../components/field-sync.js';
19
24
  import '../wizard/types.js';
20
25
  import '../tour/tour.js';
@@ -26,7 +31,6 @@ import '../components/page-contents.js';
26
31
  import '../components/series-chart-labels.js';
27
32
  import '../components/pie-chart-labels.js';
28
33
  import '../components/sparkline.js';
29
- import '../components/stat-tile.js';
30
34
  import '../components/signature-pad.js';
31
35
  import '../components/password-strength.js';
32
36
  import '../components/swatch-picker.js';
@@ -38,9 +42,6 @@ import '../hooks/use-search-param-state.js';
38
42
  import '../components/measured-grid.js';
39
43
  import '../components/user-avatar.js';
40
44
  import '../lib/person-name.js';
41
- import '../components/status-dot.js';
42
- import '../components/progress-bar.js';
43
- import '../components/signed-amount.js';
44
45
  import '../shell/topbar-action-menu.js';
45
46
  import '../components/menu-item.js';
46
47
  import '../components/floating-panel.js';
@@ -56,7 +57,6 @@ import '../components/error-boundary.js';
56
57
  import '../components/authed-image.js';
57
58
  import '../components/image-grid.js';
58
59
  import '../components/lightbox.js';
59
- import '../components/write-lock.js';
60
60
  import '../auth/sign-in-form.js';
61
61
  import '../landing/access.js';
62
62
  import '../landing/landing-labels.js';
@@ -134,7 +134,9 @@ interface BillingBannerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "
134
134
  */
135
135
  actionHref?: string;
136
136
  /** What the action does — the app's portal call (`POST /billing/portal`, then off to
137
- * its `url`). The kit sends no request. */
137
+ * its `url`): for `payment-failed` with `{target: "payment_method"}`, the portal's
138
+ * page for the card (§14.5); for `processing`, `useCheckoutProcessing`'s
139
+ * `checkAgain`. The kit sends no request. */
138
140
  onAction?: () => void;
139
141
  /**
140
142
  * Over the kind's action words: "Choose a plan" (trial, grant, plan ended), "Update
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/billing/billing-banner.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { CreditCard, Hourglass, PencilOff } from \"lucide-react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport type { AlertSize, AlertTone } from \"../components/alert-banner\";\nimport { Button, Spinner } from \"../components/ui\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\n\n/**\n * The billing banners (docs/billing-harmonization.md §3.3, §7, §12.6, §12.21):\n *\n * - `trial-ending` — from 7 days before `trial_ends_at` (§3.3);\n * - `grant-ending` — from 7 days before `comped_until`: a beta user's 12 months, an\n * operator's grant;\n * - `payment-failed` — from the first failed payment (`past_due`), while the provider\n * retries;\n * - `plan-ended` — out of good standing: read-only, never locked (§2.3);\n * - `guest` — a guest in someone else's read-only item (§12.6);\n * - `processing` — back from checkout before the webhook landed (§12.21).\n */\nexport type BillingBannerKind =\n | \"trial-ending\"\n | \"grant-ending\"\n | \"payment-failed\"\n | \"plan-ended\"\n | \"guest\"\n | \"processing\";\n\ninterface BillingBannerBaseProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"onClick\"> {\n /**\n * Over the kind's tone: `trial-ending` and `grant-ending` are `info` until the last\n * three days and `warning` in them; `payment-failed` is `danger` (money did not\n * arrive — kastlan's past-due strip was red); `plan-ended` is `warning`, a state with\n * everything still there rather than an error; `guest` and `processing` are `info`.\n */\n tone?: AlertTone;\n /** `strip` (default): the band under the top bar, on every page. `box`: the same\n * message on the subscription page itself. */\n variant?: \"strip\" | \"box\";\n /** Default `sm` for a strip (the demo's banner), `md` for a box. */\n size?: AlertSize;\n /**\n * Where the action goes — the subscription page, say. The app's action, as an `href`\n * or as {@link onAction}; without either there is no button. Never a write-locked\n * commit: billing itself always works (§3.3).\n */\n actionHref?: string;\n /** What the action does — the app's portal call (`POST /billing/portal`, then off to\n * its `url`). The kit sends no request. */\n onAction?: () => void;\n /**\n * Over the kind's action words: \"Choose a plan\" (trial, grant, plan ended), \"Update\n * payment method\" (payment failed), \"Check again\" (processing). The guest banner has\n * none of its own: an action there needs this.\n */\n actionLabel?: string;\n /** The action is under way: the button's spinner, no second press. */\n actionPending?: boolean;\n /** An × that calls this; the app removes the banner and remembers that it did. */\n onDismiss?: () => void;\n dismissLabel?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport type BillingBannerProps = BillingBannerBaseProps &\n (\n | {\n kind: \"trial-ending\" | \"grant-ending\";\n /** Calendar days left — `billingDaysLeft(trial_ends_at)`. 0 is today. */\n daysLeft: number;\n item?: never;\n }\n | {\n kind: \"guest\";\n /** The item's own name (\"Household 2026\"), or nothing. NEVER the owner's status:\n * the banner says \"for now\" and nothing more (§12.6). */\n item?: string;\n daysLeft?: never;\n }\n | {\n kind: \"payment-failed\" | \"plan-ended\" | \"processing\";\n daysLeft?: never;\n item?: never;\n }\n );\n\n/** The last days, in which an ending trial or grant turns from news into a warning. */\nconst LAST_DAYS = 3;\n\nfunction defaultTone(kind: BillingBannerKind, daysLeft: number | undefined): AlertTone {\n switch (kind) {\n case \"trial-ending\":\n case \"grant-ending\":\n return (daysLeft ?? 0) > LAST_DAYS ? \"info\" : \"warning\";\n case \"payment-failed\":\n return \"danger\";\n case \"plan-ended\":\n return \"warning\";\n default:\n return \"info\";\n }\n}\n\n/**\n * One of the billing banners, as a kit `AlertBanner` strip (§7): the kind's sentence, its\n * glyph and tone, and the app's action. Carries `data-billing-banner` with the kind.\n *\n * The app decides WHICH banner shows, from `GET /billing/overview` (and hides all of them\n * while billing is off, §4); the kit only says it. A guest's banner takes no status at\n * all — the type does not let one in — so the owner's payment state cannot leak into it\n * by a wrong prop (§12.6).\n *\n * `processing` is a live region (`role=\"status\"`): it appears when the person comes back\n * from checkout and goes when the overview catches up (§12.21), and a screen reader\n * should hear both. The others are page furniture, role-less as the strip is.\n */\nexport function BillingBanner(props: BillingBannerProps) {\n const {\n kind,\n daysLeft,\n item,\n tone,\n variant = \"strip\",\n size,\n actionHref,\n onAction,\n actionLabel,\n actionPending,\n onDismiss,\n dismissLabel,\n labels: labelsProp,\n role,\n ...rest\n } = props;\n const labels = useBillingLabels(labelsProp);\n const days = Math.max(0, Math.floor(daysLeft ?? 0));\n\n let text: string;\n let icon: ReactNode;\n let ownAction: string | undefined;\n switch (kind) {\n case \"trial-ending\":\n text = labels.trialEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"grant-ending\":\n text = labels.grantEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"payment-failed\":\n text = labels.paymentFailed;\n icon = <CreditCard />;\n ownAction = labels.updatePayment;\n break;\n case \"plan-ended\":\n text = labels.planEnded;\n icon = <PencilOff />;\n ownAction = labels.choosePlan;\n break;\n case \"guest\":\n text = labels.guestReadOnly(item);\n icon = <PencilOff />;\n ownAction = undefined;\n break;\n case \"processing\":\n text = labels.processing;\n icon = <Spinner label={null} />;\n ownAction = labels.checkAgain;\n break;\n }\n\n const words = actionLabel ?? ownAction;\n const action =\n words === undefined || (actionHref === undefined && onAction === undefined) ? undefined : actionHref !== undefined ? (\n <Button href={actionHref} variant=\"secondary\" size=\"sm\">\n {words}\n </Button>\n ) : (\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onAction} pending={actionPending}>\n {words}\n </Button>\n );\n\n return (\n <AlertBanner\n {...rest}\n role={role ?? (kind === \"processing\" ? \"status\" : undefined)}\n variant={variant}\n size={size ?? (variant === \"strip\" ? \"sm\" : \"md\")}\n tone={tone ?? defaultTone(kind, days)}\n icon={icon}\n action={action}\n onDismiss={onDismiss}\n dismissLabel={dismissLabel}\n data-billing-banner={kind}\n >\n {text}\n </AlertBanner>\n );\n}\n"],"mappings":";AAkJa;AAjJb,SAAS,YAAY,WAAW,iBAAiB;AAGjD,SAAS,mBAAmB;AAE5B,SAAS,QAAQ,eAAe;AAChC,SAAS,wBAAwB;AAmFjC,MAAM,YAAY;AAElB,SAAS,YAAY,MAAyB,UAAyC;AACrF,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AACH,cAAQ,YAAY,KAAK,YAAY,SAAS;AAAA,IAChD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAeO,SAAS,cAAc,OAA2B;AACvD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,CAAC,CAAC;AAElD,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,cAAW;AACnB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,cAAc,IAAI;AAChC,aAAO,oBAAC,aAAU;AAClB,kBAAY;AACZ;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,WAAQ,OAAO,MAAM;AAC7B,kBAAY,OAAO;AACnB;AAAA,EACJ;AAEA,QAAM,QAAQ,eAAe;AAC7B,QAAM,SACJ,UAAU,UAAc,eAAe,UAAa,aAAa,SAAa,SAAY,eAAe,SACvG,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,iBACH,IAEA,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,UAAU,SAAS,eAC7E,iBACH;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAM,SAAS,SAAS,eAAe,WAAW;AAAA,MAClD;AAAA,MACA,MAAM,SAAS,YAAY,UAAU,OAAO;AAAA,MAC5C,MAAM,QAAQ,YAAY,MAAM,IAAI;AAAA,MACpC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,uBAAqB;AAAA,MAEpB;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/billing/billing-banner.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { CreditCard, Hourglass, PencilOff } from \"lucide-react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport type { AlertSize, AlertTone } from \"../components/alert-banner\";\nimport { Button, Spinner } from \"../components/ui\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\n\n/**\n * The billing banners (docs/billing-harmonization.md §3.3, §7, §12.6, §12.21):\n *\n * - `trial-ending` — from 7 days before `trial_ends_at` (§3.3);\n * - `grant-ending` — from 7 days before `comped_until`: a beta user's 12 months, an\n * operator's grant;\n * - `payment-failed` — from the first failed payment (`past_due`), while the provider\n * retries;\n * - `plan-ended` — out of good standing: read-only, never locked (§2.3);\n * - `guest` — a guest in someone else's read-only item (§12.6);\n * - `processing` — back from checkout before the webhook landed (§12.21).\n */\nexport type BillingBannerKind =\n | \"trial-ending\"\n | \"grant-ending\"\n | \"payment-failed\"\n | \"plan-ended\"\n | \"guest\"\n | \"processing\";\n\ninterface BillingBannerBaseProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"onClick\"> {\n /**\n * Over the kind's tone: `trial-ending` and `grant-ending` are `info` until the last\n * three days and `warning` in them; `payment-failed` is `danger` (money did not\n * arrive — kastlan's past-due strip was red); `plan-ended` is `warning`, a state with\n * everything still there rather than an error; `guest` and `processing` are `info`.\n */\n tone?: AlertTone;\n /** `strip` (default): the band under the top bar, on every page. `box`: the same\n * message on the subscription page itself. */\n variant?: \"strip\" | \"box\";\n /** Default `sm` for a strip (the demo's banner), `md` for a box. */\n size?: AlertSize;\n /**\n * Where the action goes — the subscription page, say. The app's action, as an `href`\n * or as {@link onAction}; without either there is no button. Never a write-locked\n * commit: billing itself always works (§3.3).\n */\n actionHref?: string;\n /** What the action does — the app's portal call (`POST /billing/portal`, then off to\n * its `url`): for `payment-failed` with `{target: \"payment_method\"}`, the portal's\n * page for the card (§14.5); for `processing`, `useCheckoutProcessing`'s\n * `checkAgain`. The kit sends no request. */\n onAction?: () => void;\n /**\n * Over the kind's action words: \"Choose a plan\" (trial, grant, plan ended), \"Update\n * payment method\" (payment failed), \"Check again\" (processing). The guest banner has\n * none of its own: an action there needs this.\n */\n actionLabel?: string;\n /** The action is under way: the button's spinner, no second press. */\n actionPending?: boolean;\n /** An × that calls this; the app removes the banner and remembers that it did. */\n onDismiss?: () => void;\n dismissLabel?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport type BillingBannerProps = BillingBannerBaseProps &\n (\n | {\n kind: \"trial-ending\" | \"grant-ending\";\n /** Calendar days left — `billingDaysLeft(trial_ends_at)`. 0 is today. */\n daysLeft: number;\n item?: never;\n }\n | {\n kind: \"guest\";\n /** The item's own name (\"Household 2026\"), or nothing. NEVER the owner's status:\n * the banner says \"for now\" and nothing more (§12.6). */\n item?: string;\n daysLeft?: never;\n }\n | {\n kind: \"payment-failed\" | \"plan-ended\" | \"processing\";\n daysLeft?: never;\n item?: never;\n }\n );\n\n/** The last days, in which an ending trial or grant turns from news into a warning. */\nconst LAST_DAYS = 3;\n\nfunction defaultTone(kind: BillingBannerKind, daysLeft: number | undefined): AlertTone {\n switch (kind) {\n case \"trial-ending\":\n case \"grant-ending\":\n return (daysLeft ?? 0) > LAST_DAYS ? \"info\" : \"warning\";\n case \"payment-failed\":\n return \"danger\";\n case \"plan-ended\":\n return \"warning\";\n default:\n return \"info\";\n }\n}\n\n/**\n * One of the billing banners, as a kit `AlertBanner` strip (§7): the kind's sentence, its\n * glyph and tone, and the app's action. Carries `data-billing-banner` with the kind.\n *\n * The app decides WHICH banner shows, from `GET /billing/overview` (and hides all of them\n * while billing is off, §4); the kit only says it. A guest's banner takes no status at\n * all — the type does not let one in — so the owner's payment state cannot leak into it\n * by a wrong prop (§12.6).\n *\n * `processing` is a live region (`role=\"status\"`): it appears when the person comes back\n * from checkout and goes when the overview catches up (§12.21), and a screen reader\n * should hear both. The others are page furniture, role-less as the strip is.\n */\nexport function BillingBanner(props: BillingBannerProps) {\n const {\n kind,\n daysLeft,\n item,\n tone,\n variant = \"strip\",\n size,\n actionHref,\n onAction,\n actionLabel,\n actionPending,\n onDismiss,\n dismissLabel,\n labels: labelsProp,\n role,\n ...rest\n } = props;\n const labels = useBillingLabels(labelsProp);\n const days = Math.max(0, Math.floor(daysLeft ?? 0));\n\n let text: string;\n let icon: ReactNode;\n let ownAction: string | undefined;\n switch (kind) {\n case \"trial-ending\":\n text = labels.trialEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"grant-ending\":\n text = labels.grantEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"payment-failed\":\n text = labels.paymentFailed;\n icon = <CreditCard />;\n ownAction = labels.updatePayment;\n break;\n case \"plan-ended\":\n text = labels.planEnded;\n icon = <PencilOff />;\n ownAction = labels.choosePlan;\n break;\n case \"guest\":\n text = labels.guestReadOnly(item);\n icon = <PencilOff />;\n ownAction = undefined;\n break;\n case \"processing\":\n text = labels.processing;\n icon = <Spinner label={null} />;\n ownAction = labels.checkAgain;\n break;\n }\n\n const words = actionLabel ?? ownAction;\n const action =\n words === undefined || (actionHref === undefined && onAction === undefined) ? undefined : actionHref !== undefined ? (\n <Button href={actionHref} variant=\"secondary\" size=\"sm\">\n {words}\n </Button>\n ) : (\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onAction} pending={actionPending}>\n {words}\n </Button>\n );\n\n return (\n <AlertBanner\n {...rest}\n role={role ?? (kind === \"processing\" ? \"status\" : undefined)}\n variant={variant}\n size={size ?? (variant === \"strip\" ? \"sm\" : \"md\")}\n tone={tone ?? defaultTone(kind, days)}\n icon={icon}\n action={action}\n onDismiss={onDismiss}\n dismissLabel={dismissLabel}\n data-billing-banner={kind}\n >\n {text}\n </AlertBanner>\n );\n}\n"],"mappings":";AAoJa;AAnJb,SAAS,YAAY,WAAW,iBAAiB;AAGjD,SAAS,mBAAmB;AAE5B,SAAS,QAAQ,eAAe;AAChC,SAAS,wBAAwB;AAqFjC,MAAM,YAAY;AAElB,SAAS,YAAY,MAAyB,UAAyC;AACrF,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AACH,cAAQ,YAAY,KAAK,YAAY,SAAS;AAAA,IAChD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAeO,SAAS,cAAc,OAA2B;AACvD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,CAAC,CAAC;AAElD,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,cAAW;AACnB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,cAAc,IAAI;AAChC,aAAO,oBAAC,aAAU;AAClB,kBAAY;AACZ;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,WAAQ,OAAO,MAAM;AAC7B,kBAAY,OAAO;AACnB;AAAA,EACJ;AAEA,QAAM,QAAQ,eAAe;AAC7B,QAAM,SACJ,UAAU,UAAc,eAAe,UAAa,aAAa,SAAa,SAAY,eAAe,SACvG,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,iBACH,IAEA,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,UAAU,SAAS,eAC7E,iBACH;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAM,SAAS,SAAS,eAAe,WAAW;AAAA,MAClD;AAAA,MACA,MAAM,SAAS,YAAY,UAAU,OAAO;AAAA,MAC5C,MAAM,QAAQ,YAAY,MAAM,IAAI;AAAA,MACpC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,uBAAqB;AAAA,MAEpB;AAAA;AAAA,EACH;AAEJ;","names":[]}