@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
@@ -16,7 +16,7 @@ import { LanguageSelect } from "../components/language-select";
16
16
  import { SectionLabel, Caption } from "../components/text";
17
17
  import { RowActions } from "../components/row-actions";
18
18
  import { Button, Input, Select, Spinner } from "../components/ui";
19
- import { useWriteLock } from "../components/write-lock";
19
+ import { COMMIT_EXCEPT_BILLING, useWriteLock } from "../components/write-lock";
20
20
  import { hasMessage, settle, useDayText, usePersonLabel } from "./admin-parts";
21
21
  import type { AdminPerson, MaybePromise } from "./admin-parts";
22
22
  import { RoleSelect } from "./role-select";
@@ -45,6 +45,11 @@ import { RoleSelect } from "./role-select";
45
45
  * `revoked`; the status chip simply never says it (§5).
46
46
  *
47
47
  * It never sends a request: `onInvite`, `onResend` and `onRevoke` do.
48
+ *
49
+ * Under a write lock, inviting and resending are refused; a lapsed plan's lock (`kind:
50
+ * "billing"`) still lets an invitation be revoked, since removing access is what a
51
+ * lapsed plan allows (docs/billing-harmonization.md §12.13, §12.36). A demo lock refuses
52
+ * all three.
48
53
  */
49
54
 
50
55
  export type InvitationStatus = "open" | "accepted" | "expired" | "revoked";
@@ -453,7 +458,7 @@ export function InvitationsPanel<R extends string = string>({
453
458
  <li key={key} data-status={invitation.status} className="py-2 first:pt-0 last:pb-0">
454
459
  <div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5">
455
460
  <div className="min-w-0 flex-1 basis-48 space-y-1">
456
- <div data-private className="truncate text-sm font-medium text-[var(--text-primary)]">
461
+ <div data-private className="truncate-until-large text-sm font-medium text-[var(--text-primary)]">
457
462
  {invitation.email}
458
463
  </div>
459
464
  <div className="flex flex-wrap items-center gap-1">
@@ -509,7 +514,8 @@ export function InvitationsPanel<R extends string = string>({
509
514
  label: labels.revoke(invitation.email),
510
515
  icon: Trash2,
511
516
  tone: "danger",
512
- commit: true,
517
+ // Removing access stays open under a lapsed plan (§12.36).
518
+ commit: COMMIT_EXCEPT_BILLING,
513
519
  pending: busy === `revoke:${key}`,
514
520
  disabled: busy !== null,
515
521
  onSelect: () => void run(`revoke:${key}`, () => onRevoke(invitation)),
@@ -0,0 +1,459 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
5
+ import { formatNumber, EMPTY_FORMATTED_VALUE } from "../lib/format";
6
+ import { toLocalIso } from "../lib/dates";
7
+ import type { DataTableColumn } from "../components/data-table";
8
+ import { DatePicker } from "../components/date-picker";
9
+ import { Tooltip } from "../components/tooltip";
10
+ import { Select } from "../components/ui";
11
+ import { COMMIT_EXCEPT_BILLING } from "../components/write-lock";
12
+ import { useBillingLabels } from "../billing/billing-labels";
13
+ import type { SubscriptionStatus } from "../billing/billing-labels";
14
+ import { SubscriptionStatusChip } from "../billing/subscription-status-chip";
15
+ import { AdminActionConfirm } from "./admin-action-confirm";
16
+ import type { AdminActionConfirmValues } from "./admin-action-confirm";
17
+ import { personLabel, useDayText } from "./admin-parts";
18
+ import type { AdminPerson, MaybePromise } from "./admin-parts";
19
+
20
+ /**
21
+ * The operator's plan parts (docs/billing-harmonization.md §14.12, decision 27):
22
+ * {@link planColumn} for the roster or the companies list, {@link PlanChangeConfirm}
23
+ * for the grant, and {@link usePlanChangeResult} for what the answer says.
24
+ *
25
+ * All three apps have the route (`POST /admin/users/{id}/plan`, kastlan's
26
+ * `POST /platform/companies/{id}/plan`) and server-kit's `PlanChangeRequest` /
27
+ * `PlanChangeResponse`; only keksdose had a UI (`users-panel.tsx`, `PlanConfirm` in
28
+ * `user-admin-confirm.tsx`), and these are its parts, lifted. The plan names are the
29
+ * app's (§3.1): every part takes `planName` or the plans with their names. The plan
30
+ * codes come from `GET /admin/plans` (kastlan `GET /platform/plans`), which answers
31
+ * whatever the billing switch says.
32
+ */
33
+
34
+ /** The `planChange` namespace: the column, the grant dialog and the result's lines. */
35
+ export interface PlanChangeLabels {
36
+ /** The column's header: "Plan". */
37
+ column: string;
38
+ /** The dialog's title: "Change plan". */
39
+ title: string;
40
+ /** Its confirm button: "Change plan". */
41
+ confirm: string;
42
+ /** The plan select's label: "Plan". */
43
+ plan: string;
44
+ /** The line over the select: "Current plan: Free". */
45
+ current: (plan: string) => string;
46
+ /** Under the select — the fact that is not on the button. */
47
+ keepsItems: string;
48
+ /** The end of a free grant's field: "Free until". */
49
+ until: string;
50
+ /** Under it. */
51
+ untilHint: string;
52
+ /** Why the confirm is held while nothing would change. */
53
+ needsChange: string;
54
+ /** The result: the plan moved from one to another. */
55
+ changed: (from: string, to: string) => string;
56
+ /** The result: an account with no plan before, or the same plan again. */
57
+ set: (to: string) => string;
58
+ /** The result: a running beta kept its own end (§12.34); only the plan moved. */
59
+ keptBeta: string;
60
+ /** The result: the grant runs until `date` (formatted). */
61
+ untilDone: (date: string) => string;
62
+ /** The result: the account owns more than the new plan allows. */
63
+ overLimit: string;
64
+ /** The column's usage, spelled out (its tooltip): "1 of 5 budgets". Figures arrive
65
+ * formatted; `dimension` is the app's word, as `usage` gives it. */
66
+ usageOf: (used: string, limit: string, dimension: string) => string;
67
+ }
68
+
69
+ export const DEFAULT_PLAN_CHANGE_LABELS: PlanChangeLabels = {
70
+ column: "Plan",
71
+ title: "Change plan",
72
+ confirm: "Change plan",
73
+ plan: "Plan",
74
+ current: (plan) => `Current plan: ${plan}`,
75
+ keepsItems: "A smaller plan only blocks creating more; nothing is deleted.",
76
+ until: "Free until",
77
+ untilHint: "Leave empty for no end. A running beta keeps its own end.",
78
+ needsChange: "Pick another plan or an end date.",
79
+ changed: (from, to) => `Plan changed from ${from} to ${to}.`,
80
+ set: (to) => `Plan set to ${to}.`,
81
+ keptBeta: "The beta keeps its end; only the plan changed.",
82
+ untilDone: (date) => `Free until ${date}.`,
83
+ overLimit: "Above the new plan’s limit: nothing is removed, new items are blocked.",
84
+ usageOf: (used, limit, dimension) => `${used} of ${limit} ${dimension}`,
85
+ };
86
+
87
+ /** A plan code as the parts compare it: lowercase (§12.16 — an older row's "FREE" is
88
+ * the same plan as "free"), and no code at all as null. */
89
+ function codeOf(raw: string | null | undefined): string | null {
90
+ const code = (raw ?? "").trim().toLowerCase();
91
+ return code === "" ? null : code;
92
+ }
93
+
94
+ /* ── The column ────────────────────────────────────────────────────────────── */
95
+
96
+ /** What a row's usage is, for the line under its plan: "1/5". */
97
+ export interface PlanColumnUsage {
98
+ used: number;
99
+ /** The plan's limit for it; `null` (unlimited, or none known): no line. */
100
+ limit: number | null;
101
+ /** The app's word for what is counted, as in "1 of 5 budgets". */
102
+ label: string;
103
+ }
104
+
105
+ export interface PlanColumnOptions<T> {
106
+ /** The column's key. Default `"plan"`. */
107
+ key?: string;
108
+ /** The row's plan code, read without case (§12.16); null for none. */
109
+ plan: (row: T) => string | null;
110
+ /** The plan's display name, the app's (§3.1). Gets the lowercase code. */
111
+ planName: (code: string) => string;
112
+ /** The row's standing, as a `SubscriptionStatusChip` beside the name. Left out, or
113
+ * undefined for a row: no chip — billing off, where a status says nothing. */
114
+ status?: (row: T) => SubscriptionStatus | undefined;
115
+ /** The row's usage of its plan's limit: "1/5" under the name, "1 of 5 budgets" in
116
+ * the tooltip. Undefined for a row, or a `null` limit: no line. */
117
+ usage?: (row: T) => PlanColumnUsage | undefined;
118
+ /** A select filter over these codes, each named by `planName`. Left out: none. */
119
+ filterPlans?: readonly string[];
120
+ /**
121
+ * The sort. Default: by the plan code (keksdose's, `users-panel.tsx:739`). A function:
122
+ * by its value — the catalogue's `sort` for rank order. `false`: not sortable.
123
+ */
124
+ sortBy?: false | ((row: T) => string | number | null | undefined);
125
+ /** The header's text. Default `planChange.column`. */
126
+ headerText?: string;
127
+ /** The words, already resolved. Default English; {@link usePlanColumn} reads the
128
+ * provider's `planChange` namespace. */
129
+ labels?: Partial<PlanChangeLabels>;
130
+ }
131
+
132
+ /** The cell: a component, so the figures follow the provider's locale. */
133
+ function PlanCell({
134
+ code,
135
+ name,
136
+ status,
137
+ usage,
138
+ usageOf,
139
+ }: {
140
+ code: string | null;
141
+ name: string;
142
+ status: SubscriptionStatus | undefined;
143
+ usage: PlanColumnUsage | undefined;
144
+ usageOf: PlanChangeLabels["usageOf"];
145
+ }) {
146
+ const locale = useKitLocale();
147
+ const figure = (value: number) => formatNumber(value, { locale });
148
+ return (
149
+ <div className="whitespace-nowrap" data-plan={code ?? undefined}>
150
+ <div className="flex items-center gap-1.5">
151
+ <span className={code ? "font-medium" : "text-[var(--text-muted)]"}>{name}</span>
152
+ {/* The standing at a glance (§6): the kit's word and tone, never colour alone. */}
153
+ {status !== undefined && <SubscriptionStatusChip status={status} size="xs" />}
154
+ </div>
155
+ {usage && usage.limit !== null && (
156
+ // "1/5" under the plan, "1 of 5 budgets" in the tooltip: the sentence spelled out
157
+ // inline was the widest thing in keksdose's table.
158
+ <Tooltip label={usageOf(figure(usage.used), figure(usage.limit), usage.label)}>
159
+ <span className="block text-xs tabular-nums text-[var(--text-muted)]">
160
+ {figure(usage.used)}/{figure(usage.limit)}
161
+ </span>
162
+ </Tooltip>
163
+ )}
164
+ </div>
165
+ );
166
+ }
167
+
168
+ /**
169
+ * The roster's (or the companies list's) plan column: the plan's display name, the
170
+ * standing chip beside it while billing is on, and the usage under it — keksdose's
171
+ * column, `users-panel.tsx:706-745`. Pass it in `userRosterColumns`' `extra`, or in
172
+ * any `DataTable`'s columns:
173
+ *
174
+ * extra: [planColumn<AdminUserRow>({
175
+ * plan: (u) => u.plan,
176
+ * planName,
177
+ * status: billingOn ? (u) => u.subscription?.status : undefined,
178
+ * usage: (u) => ({ used: u.budgets_owned, limit: u.limits.budgets, label: t("budgets") }),
179
+ * filterPlans: plans.map((p) => p.code),
180
+ * })]
181
+ *
182
+ * Pure: hand it resolved words, or use {@link usePlanColumn}.
183
+ */
184
+ export function planColumn<T>(options: PlanColumnOptions<T>): DataTableColumn<T> {
185
+ const { key = "plan", plan, planName, status, usage, filterPlans, sortBy, headerText, labels: labelsProp } = options;
186
+ const labels: PlanChangeLabels = { ...DEFAULT_PLAN_CHANGE_LABELS, ...labelsProp };
187
+ const header = headerText ?? labels.column;
188
+ const nameOf = (code: string | null) => (code ? planName(code) : EMPTY_FORMATTED_VALUE);
189
+ return {
190
+ key,
191
+ header,
192
+ headerText: header,
193
+ cell: (row) => {
194
+ const code = codeOf(plan(row));
195
+ return (
196
+ <PlanCell
197
+ code={code}
198
+ name={nameOf(code)}
199
+ status={status?.(row)}
200
+ usage={usage?.(row)}
201
+ usageOf={labels.usageOf}
202
+ />
203
+ );
204
+ },
205
+ sortBy: sortBy === false ? undefined : (sortBy ?? ((row) => codeOf(plan(row)) ?? "")),
206
+ filter: filterPlans
207
+ ? {
208
+ type: "select",
209
+ getValue: (row) => codeOf(plan(row)) ?? "",
210
+ options: [...new Set(filterPlans.map((code) => codeOf(code)).filter((code) => code !== null))].map(
211
+ (code) => ({ value: code, label: planName(code) }),
212
+ ),
213
+ }
214
+ : undefined,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * {@link planColumn} with the `planChange` words from the provider, memoised on the
220
+ * options — pass stable callbacks (`useCallback`), or the column is rebuilt on every
221
+ * render.
222
+ */
223
+ export function usePlanColumn<T>(options: PlanColumnOptions<T>): DataTableColumn<T> {
224
+ const { key, plan, planName, status, usage, filterPlans, sortBy, headerText, labels: labelsProp } = options;
225
+ const labels = useKitLabels("planChange", DEFAULT_PLAN_CHANGE_LABELS, labelsProp);
226
+ return useMemo(
227
+ () => planColumn<T>({ key, plan, planName, status, usage, filterPlans, sortBy, headerText, labels }),
228
+ [key, plan, planName, status, usage, filterPlans, sortBy, headerText, labels],
229
+ );
230
+ }
231
+
232
+ /* ── The grant ─────────────────────────────────────────────────────────────── */
233
+
234
+ /** What {@link PlanChangeConfirmProps.onConfirm} sends: server-kit's
235
+ * `PlanChangeRequest` without its `acknowledged`, which comes in `values`. */
236
+ export interface PlanChange {
237
+ /** The picked plan's code. */
238
+ plan: string;
239
+ /** The end of the free grant, as ISO UTC — only when one was picked. */
240
+ comped_until?: string;
241
+ }
242
+
243
+ export interface PlanChangeConfirmProps {
244
+ /**
245
+ * Whom it is about — a user (`AdminActionTarget`), or kastlan's company, which has no
246
+ * address: its name is said instead (the level is `acknowledge`, so nothing is typed).
247
+ */
248
+ target: AdminPerson & { email?: string | null };
249
+ /** The plans to offer, in the catalogue's order, by their display names (§3.1) — from
250
+ * `GET /admin/plans`. */
251
+ plans: readonly { code: string; name: string }[];
252
+ /** The account's plan now; null for none. The select opens on it. */
253
+ current: string | null;
254
+ /** What the account uses, in the app's sentence: "3 of 5 budgets". */
255
+ usage?: ReactNode;
256
+ /** Offer "Free until" — only meaningful while billing is on, where a grant's end
257
+ * means something to the payer. */
258
+ grantEnd?: boolean;
259
+ /**
260
+ * Sends it: `{plan}`, and `comped_until` only when an end was picked — the END of the
261
+ * picked day in the operator's time zone, as ISO UTC (a grant "until 31 January" lasts
262
+ * through it). No end: a grant without one, or on a running beta only the plan moves
263
+ * and the beta keeps its own end (§12.34). `values.acknowledged` is the body's
264
+ * `acknowledged`. A promise is handled as `AdminActionConfirm`'s.
265
+ */
266
+ onConfirm: (change: PlanChange, values: AdminActionConfirmValues) => MaybePromise;
267
+ onClose: (done: boolean) => void;
268
+ /** The app's words for a failure the kit has no code for. */
269
+ describeError?: (error: unknown) => ReactNode | undefined;
270
+ labels?: Partial<PlanChangeLabels>;
271
+ }
272
+
273
+ /** The end of the day `date` (YYYY-MM-DD) names, in this device's time zone, as ISO
274
+ * UTC — keksdose's `endOfDay` (`user-admin-confirm.tsx:44-46`). */
275
+ function endOfDay(date: string): string {
276
+ return new Date(`${date}T23:59:59`).toISOString();
277
+ }
278
+
279
+ /**
280
+ * An operator's plan change (§14.12): `AdminActionConfirm` at `acknowledge`, the line
281
+ * with the current plan and the app's usage sentence, a plan select, and — with
282
+ * `grantEnd` — the end of a free grant. The confirm is held while nothing would change.
283
+ *
284
+ * Exempt from a BILLING lock, built in (`COMMIT_EXCEPT_BILLING`, §12.36): a plan change
285
+ * is an admin route (§12.13), and the operator's own account or company may be lapsed —
286
+ * kastlan's review. A demo lock still holds it.
287
+ */
288
+ export function PlanChangeConfirm({
289
+ target,
290
+ plans,
291
+ current: currentProp,
292
+ usage,
293
+ grantEnd = false,
294
+ onConfirm,
295
+ onClose,
296
+ describeError,
297
+ labels: labelsProp,
298
+ }: PlanChangeConfirmProps) {
299
+ const labels = useKitLabels("planChange", DEFAULT_PLAN_CHANGE_LABELS, labelsProp);
300
+ const locale = useKitLocale();
301
+ // Seeded from the account, so the picker opens on the truth and confirming without
302
+ // touching it is held rather than a silent free → free in the audit.
303
+ const current = codeOf(currentProp);
304
+ const [plan, setPlan] = useState(current ?? "");
305
+ const [until, setUntil] = useState("");
306
+ // A grant ends today at the earliest; read once, not on every render.
307
+ const [today] = useState(() => toLocalIso(new Date()));
308
+ const options = plans.map((p) => ({ code: codeOf(p.code) ?? p.code, name: p.name }));
309
+ const nameOf = (code: string) => options.find((p) => p.code === code)?.name ?? code;
310
+ // An account on a plan the catalogue no longer lists still opens on it.
311
+ if (current && !options.some((p) => p.code === current)) options.unshift({ code: current, name: current });
312
+ const unchanged = plan === "" || (plan === current && !until);
313
+ // A company has no address: the dialog says its name in the address's place.
314
+ const who = { ...target, email: target.email || personLabel(target, locale) };
315
+
316
+ return (
317
+ <AdminActionConfirm
318
+ level="acknowledge"
319
+ tone="warning"
320
+ target={who}
321
+ title={labels.title}
322
+ confirmLabel={labels.confirm}
323
+ confirmDisabledReason={unchanged ? labels.needsChange : undefined}
324
+ describeError={describeError}
325
+ commit={COMMIT_EXCEPT_BILLING}
326
+ onConfirm={(values) => onConfirm({ plan, ...(until ? { comped_until: endOfDay(until) } : {}) }, values)}
327
+ onClose={onClose}
328
+ >
329
+ {(current !== null || usage !== undefined) && (
330
+ <div className="space-y-0.5 text-xs text-[var(--text-muted)]" data-plan-current={current ?? undefined}>
331
+ {current !== null && <p>{labels.current(nameOf(current))}</p>}
332
+ {usage !== undefined && <p>{usage}</p>}
333
+ </div>
334
+ )}
335
+ <Select label={labels.plan} hint={labels.keepsItems} value={plan} onChange={(e) => setPlan(e.target.value)}>
336
+ {plan === "" && (
337
+ <option value="" disabled>
338
+ {EMPTY_FORMATTED_VALUE}
339
+ </option>
340
+ )}
341
+ {options.map((option) => (
342
+ <option key={option.code} value={option.code}>
343
+ {option.name}
344
+ </option>
345
+ ))}
346
+ </Select>
347
+ {grantEnd && (
348
+ <DatePicker
349
+ label={labels.until}
350
+ hint={labels.untilHint}
351
+ value={until}
352
+ onChange={setUntil}
353
+ min={today}
354
+ clearable
355
+ />
356
+ )}
357
+ </AdminActionConfirm>
358
+ );
359
+ }
360
+
361
+ /* ── The answer ────────────────────────────────────────────────────────────── */
362
+
363
+ /**
364
+ * A `PlanChangeResponse` as the page reads it — server-kit 0.7's (§14.12), which gained
365
+ * `previous_plan` nullable, `usage`, `kept_beta` and `comped_until`.
366
+ */
367
+ export interface PlanChangeOutcome {
368
+ /** null: the account had no subscription. */
369
+ previous_plan: string | null;
370
+ plan: string;
371
+ over_limit: boolean;
372
+ /** The new plan's limits; null for unlimited. */
373
+ limits: Readonly<Record<string, number | null>>;
374
+ /** What the payer has, per dimension — the figures for the over-limit lines:
375
+ * `{ budgets: 7 }`. */
376
+ usage: Readonly<Record<string, number>>;
377
+ /** A running beta kept its own end (§12.34). */
378
+ kept_beta?: boolean;
379
+ /** The grant's end as the change wrote it; null: no end, or no grant. */
380
+ comped_until?: string | null;
381
+ }
382
+
383
+ /** A line's tone: the app's toast kind. */
384
+ export type PlanChangeTone = "success" | "info" | "warning";
385
+
386
+ export interface PlanChangeLine {
387
+ tone: PlanChangeTone;
388
+ text: string;
389
+ }
390
+
391
+ export interface PlanChangeResultOptions {
392
+ /** The app's word per dimension, for the over-limit figures: `{ budgets: "Budgets" }`.
393
+ * A dimension without one is named by its key. */
394
+ dimensionLabels?: Readonly<Record<string, string>>;
395
+ /** How a figure is written. Default: a number in the kit's locale. */
396
+ formatValue?: (dimension: string, value: number) => string;
397
+ }
398
+
399
+ /**
400
+ * The lines for a plan change's answer, each with its tone, for the app's toasts —
401
+ * echoing the SERVER's before and after, not what was picked: a toast that reported the
402
+ * request would say "done" to a write that landed differently (keksdose's rule).
403
+ *
404
+ * const lines = usePlanChangeResult();
405
+ * for (const line of lines(res, planName)) toast[line.tone](line.text);
406
+ *
407
+ * - `changed(from, to)`, or `set(to)` with no previous plan (or the same one): success;
408
+ * - a running beta that kept its end: `keptBeta`, info; else a grant's end:
409
+ * `untilDone(date)`, success;
410
+ * - over the new limit: `overLimit`, then one figure line per dimension over it ("Budgets:
411
+ * 7 of 5", `billing.limitUsageLine`), from `usage` and `limits`: warning. A `null`
412
+ * limit gives no figure line.
413
+ */
414
+ export function usePlanChangeResult(): (
415
+ res: PlanChangeOutcome,
416
+ planName: (code: string) => string,
417
+ options?: PlanChangeResultOptions,
418
+ ) => PlanChangeLine[] {
419
+ const labels = useKitLabels("planChange", DEFAULT_PLAN_CHANGE_LABELS);
420
+ const billing = useBillingLabels();
421
+ const locale = useKitLocale();
422
+ const dayText = useDayText();
423
+ return useCallback(
424
+ (res, planName, options = {}) => {
425
+ const { dimensionLabels, formatValue } = options;
426
+ const name = (raw: string) => planName(codeOf(raw) ?? raw);
427
+ const to = name(res.plan);
428
+ const from = codeOf(res.previous_plan);
429
+ const lines: PlanChangeLine[] = [
430
+ {
431
+ tone: "success",
432
+ text: from !== null && from !== codeOf(res.plan) ? labels.changed(name(from), to) : labels.set(to),
433
+ },
434
+ ];
435
+ if (res.kept_beta) lines.push({ tone: "info", text: labels.keptBeta });
436
+ else if (res.comped_until) lines.push({ tone: "success", text: labels.untilDone(dayText(res.comped_until)) });
437
+ if (res.over_limit) {
438
+ lines.push({ tone: "warning", text: labels.overLimit });
439
+ const figure = (dimension: string, value: number) =>
440
+ formatValue ? formatValue(dimension, value) : formatNumber(value, { locale });
441
+ // Read defensively: an app's server from before 0.7 sent neither.
442
+ for (const [dimension, limit] of Object.entries(res.limits ?? {})) {
443
+ const used = res.usage?.[dimension];
444
+ if (limit === null || used === undefined || used <= limit) continue;
445
+ lines.push({
446
+ tone: "warning",
447
+ text: billing.limitUsageLine(
448
+ dimensionLabels?.[dimension] ?? dimension,
449
+ figure(dimension, used),
450
+ figure(dimension, limit),
451
+ ),
452
+ });
453
+ }
454
+ }
455
+ return lines;
456
+ },
457
+ [labels, billing, locale, dayText],
458
+ );
459
+ }
@@ -10,6 +10,7 @@ import { FormActions } from "../components/form-actions";
10
10
  import { Caption } from "../components/text";
11
11
  import { hasMessage } from "./admin-parts";
12
12
  import type { MaybePromise } from "./admin-parts";
13
+ import type { CommitScope } from "../components/write-lock";
13
14
 
14
15
  /**
15
16
  * Who may review which translations: the REVIEWER role and its scope, in one editor
@@ -107,7 +108,7 @@ export interface ReviewerScopeEditorProps {
107
108
  /** Columns for the languages from `sm` up. Default 2 (Kurvenschmiede's dialog). */
108
109
  columns?: 1 | 2 | 3 | 4;
109
110
  /** Save COMMITS: under a locked `WriteLockProvider` it is held with the lock's reason. */
110
- commit?: boolean;
111
+ commit?: CommitScope;
111
112
  labels?: Partial<ReviewerScopeLabels>;
112
113
  }
113
114
 
@@ -5,6 +5,7 @@ import { CheckboxGroup } from "../components/checkbox-group";
5
5
  import type { CheckboxGroupOption } from "../components/checkbox-group";
6
6
  import { Select } from "../components/ui";
7
7
  import type { SelectProps } from "../components/ui";
8
+ import type { CommitScope } from "../components/write-lock";
8
9
 
9
10
  /**
10
11
  * The role of an account, chosen by an admin (docs/user-admin-harmonization.md §3.3,
@@ -219,7 +220,7 @@ export interface RolesEditorProps<R extends string = string> {
219
220
  error?: ReactNode;
220
221
  columns?: 1 | 2 | 3 | 4;
221
222
  /** The editor SAVES on change — the write lock's opt-in (CheckboxGroup's `commit`). */
222
- commit?: boolean;
223
+ commit?: CommitScope;
223
224
  className?: string;
224
225
  labels?: Partial<RoleSelectLabels>;
225
226
  }
@@ -175,7 +175,9 @@ export interface UserIdentityCellProps {
175
175
  * — the cell both rosters drew by hand. An account with no name yet (a migrated row
176
176
  * before `CompleteNameDialog`, an invitee) shows its address once, as the name.
177
177
  *
178
- * Both lines carry `data-private`, so keksdose's demo blur reaches them.
178
+ * Both lines carry `data-private`, so keksdose's demo blur reaches them, and both end
179
+ * in "…" at Normal only: from Large up they wrap (`truncate-until-large`, 0.33,
180
+ * docs/text-size-harmonization.md §10.17), since a cut name or address is a finding there.
179
181
  */
180
182
  export function UserIdentityCell({
181
183
  person,
@@ -194,13 +196,13 @@ export function UserIdentityCell({
194
196
  {avatar && <UserAvatar person={person} name={name} email={email} size="sm" />}
195
197
  <div className="min-w-0">
196
198
  <div className="flex min-w-0 items-center gap-1.5">
197
- <span data-private className="truncate font-medium text-[var(--text-primary)]">
199
+ <span data-private className="truncate-until-large font-medium text-[var(--text-primary)]">
198
200
  {named || address}
199
201
  </span>
200
202
  {badge}
201
203
  </div>
202
204
  {named && address && (
203
- <div data-private className="truncate text-xs text-[var(--text-muted)]">
205
+ <div data-private className="truncate-until-large text-xs text-[var(--text-muted)]">
204
206
  {address}
205
207
  </div>
206
208
  )}
@@ -102,8 +102,27 @@ export type DemoErrorCode =
102
102
  * creating gets `billing_read_only` instead, never this (§12.3).
103
103
  * - `billing_not_configured` — billing is switched on but the provider's settings are
104
104
  * missing (the secrets, a price id): the operator's mistake, not the payer's.
105
+ *
106
+ * 0.33.0 (server-kit 0.7, §14.2): the provider's refusals, with the `billing_` prefix of
107
+ * the others (decision 22).
108
+ * - `billing_provider_unavailable` — `502`: Paddle didn't answer, or refused the request.
109
+ * Try again.
110
+ * - `billing_not_at_provider` — `409`: a portal request before the payer reached the
111
+ * provider (§14.5) — a stale page; the overview's `at_provider` says it first.
112
+ * - `billing_already_subscribed` — `409`: a checkout while a provider subscription runs
113
+ * (§14.6). Plan changes go through the portal.
114
+ * - `billing_plan_not_sold` — `422`: a checkout for a plan, currency or interval the
115
+ * catalogue doesn't sell.
105
116
  */
106
- export type BillingErrorCode = "billing_disabled" | "billing_read_only" | "plan_limit" | "billing_not_configured";
117
+ export type BillingErrorCode =
118
+ | "billing_disabled"
119
+ | "billing_read_only"
120
+ | "plan_limit"
121
+ | "billing_not_configured"
122
+ | "billing_provider_unavailable"
123
+ | "billing_not_at_provider"
124
+ | "billing_already_subscribed"
125
+ | "billing_plan_not_sold";
107
126
 
108
127
  /** Every code the kit reads: the sign-in refusals, the account ones (0.30.0), the demo's
109
128
  * (0.31.0) and billing's (0.32.0). */
@@ -114,6 +133,10 @@ const BILLING_CODES: ReadonlySet<BillingErrorCode> = new Set<BillingErrorCode>([
114
133
  "billing_read_only",
115
134
  "plan_limit",
116
135
  "billing_not_configured",
136
+ "billing_provider_unavailable",
137
+ "billing_not_at_provider",
138
+ "billing_already_subscribed",
139
+ "billing_plan_not_sold",
117
140
  ]);
118
141
 
119
142
  const CODES: ReadonlySet<string> = new Set<KitErrorCode>([
@@ -210,7 +233,7 @@ export function isAuthError(err: unknown, code?: KitErrorCode): boolean {
210
233
 
211
234
  /**
212
235
  * Whether `err` is one of billing's coded refusals (0.32.0, docs/billing-harmonization.md
213
- * §10) — any of the four, or the one named. {@link isAuthError} answers the same for a
236
+ * §10, §14.2) — any of them, or the one named. {@link isAuthError} answers the same for a
214
237
  * named code; this one also answers "is it billing's at all", which an app's error
215
238
  * handler asks before it decides between the write lock, a `PlanLimitNotice` and a toast.
216
239
  */
@@ -7,6 +7,7 @@ import { DialogFrame } from "../components/dialog-frame";
7
7
  import { ModalCloseContext } from "../components/modal";
8
8
  import { Button, Input } from "../components/ui";
9
9
  import { personNameOk, PERSON_NAME_MAX_LENGTH } from "./form-rules";
10
+ import { COMMIT_EXCEPT_BILLING } from "../components/write-lock";
10
11
 
11
12
  /** What `CompleteNameDialog` hands `onSave`: both names, trimmed, 1–120 characters —
12
13
  * `PATCH /auth/me {first_name, last_name}` (§6.1). */
@@ -80,7 +81,10 @@ export interface CompleteNameDialogProps {
80
81
  * The promise handling is `ReauthDialog`'s: busy while `onSave` runs (the fields
81
82
  * read-only, "Later" disabled, Escape held — a request that has left cannot be called
82
83
  * back by closing the dialog), closed on resolve, kept open on reject. Save is a
83
- * `commit` control, so a `WriteLockProvider` above it locks it with its reason.
84
+ * `commit` control, so a `WriteLockProvider` above it locks it with its reason — every
85
+ * lock but a lapsed plan's (`COMMIT_EXCEPT_BILLING`, 0.33): one's own name is the
86
+ * account's own settings, which billing never locks (docs/billing-harmonization.md §3.3,
87
+ * §12.36).
84
88
  *
85
89
  * Focus starts in the first EMPTY field — the last name, as a rule — because filling it
86
90
  * in is the whole of what the dialog is for.
@@ -245,7 +249,7 @@ function CompleteNameActions({
245
249
  <Button type="button" variant="ghost" disabled={busy} onClick={() => close?.()}>
246
250
  {laterLabel}
247
251
  </Button>
248
- <Button type="submit" form={formId} commit disabled={!busy && !canSave} pending={busy}>
252
+ <Button type="submit" form={formId} commit={COMMIT_EXCEPT_BILLING} disabled={!busy && !canSave} pending={busy}>
249
253
  {saveLabel}
250
254
  </Button>
251
255
  </>
@@ -48,7 +48,9 @@ interface BillingBannerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "
48
48
  */
49
49
  actionHref?: string;
50
50
  /** What the action does — the app's portal call (`POST /billing/portal`, then off to
51
- * its `url`). The kit sends no request. */
51
+ * its `url`): for `payment-failed` with `{target: "payment_method"}`, the portal's
52
+ * page for the card (§14.5); for `processing`, `useCheckoutProcessing`'s
53
+ * `checkAgain`. The kit sends no request. */
52
54
  onAction?: () => void;
53
55
  /**
54
56
  * Over the kind's action words: "Choose a plan" (trial, grant, plan ended), "Update