@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
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The way back from the provider's checkout (docs/billing-harmonization.md §14.4,
3
+ * decision 20): `?checkout=done` on the app's subscription page — kastlan's
4
+ * `/admin/billing`, keksdose's and Kurvenschmiede's `/settings/subscription`.
5
+ *
6
+ * The checkout request carries NO success URL: a redirect the client chooses is an open
7
+ * redirect, and Paddle's create-transaction takes none. The way back is configuration
8
+ * instead — the pay page's Paddle.js `successUrl` (`checkoutReturnUrl(returnUrl)`), or a
9
+ * hosted checkout's redirect in Paddle's dashboard — and the page that lands reads the
10
+ * marker with {@link isCheckoutReturn}, then drops it with `replace`
11
+ * ({@link withoutCheckoutReturn}) so a reload or a shared link does not count twice.
12
+ *
13
+ * Pure and router-agnostic, and imports nothing: the pay page (`dist/pay/`, no React)
14
+ * bundles this module too. server-kit names the same two values
15
+ * (`CHECKOUT_RETURN_PARAM`, `CHECKOUT_RETURN_VALUE`, `checkout_return_url`) for the
16
+ * deploy notes and its tests.
17
+ */
18
+
19
+ /** The query parameter of the way back. */
20
+ export const CHECKOUT_RETURN_PARAM = "checkout";
21
+ /** Its one value: the buyer came back from a completed checkout. */
22
+ export const CHECKOUT_RETURN_VALUE = "done";
23
+
24
+ /** The query of `where`, however the app holds it: a search string ("?a=1"), a whole
25
+ * URL, `URLSearchParams`, or a location (`{ search }`). */
26
+ function paramsOf(where: string | URLSearchParams | { search: string }): URLSearchParams {
27
+ if (where instanceof URLSearchParams) return where;
28
+ const text = typeof where === "string" ? where : where.search;
29
+ const query = text.includes("?") ? text.slice(text.indexOf("?") + 1) : text;
30
+ // A whole URL's hash is not part of its query.
31
+ return new URLSearchParams(query.split("#")[0]);
32
+ }
33
+
34
+ /**
35
+ * Whether the page was reached as the way back from a checkout: `?checkout=done`.
36
+ *
37
+ * const returned = isCheckoutReturn(location.search); // react-router's useLocation()
38
+ * const returned = isCheckoutReturn(searchParams); // useSearchParams()[0]
39
+ */
40
+ export function isCheckoutReturn(where: string | URLSearchParams | { search: string }): boolean {
41
+ return paramsOf(where).get(CHECKOUT_RETURN_PARAM) === CHECKOUT_RETURN_VALUE;
42
+ }
43
+
44
+ /**
45
+ * The same parameters without the marker — every other one kept, in order. A new
46
+ * object; the one passed in is not changed:
47
+ *
48
+ * setSearchParams((p) => withoutCheckoutReturn(p), { replace: true });
49
+ */
50
+ export function withoutCheckoutReturn(params: URLSearchParams): URLSearchParams {
51
+ const next = new URLSearchParams(params);
52
+ next.delete(CHECKOUT_RETURN_PARAM);
53
+ return next;
54
+ }
55
+
56
+ /**
57
+ * The subscription page's way-back URL: `page` with `checkout=done` in its query —
58
+ * after any query it already has, before its hash. A marker already there is replaced,
59
+ * not doubled.
60
+ *
61
+ * checkoutReturnUrl("https://keksdose.app/settings/subscription")
62
+ * // → "https://keksdose.app/settings/subscription?checkout=done"
63
+ *
64
+ * What the pay page hands Paddle.js as `successUrl`, and what a hosted checkout's
65
+ * redirect is set to in Paddle's dashboard.
66
+ */
67
+ export function checkoutReturnUrl(page: string): string {
68
+ const hashAt = page.indexOf("#");
69
+ const hash = hashAt === -1 ? "" : page.slice(hashAt);
70
+ const beforeHash = hashAt === -1 ? page : page.slice(0, hashAt);
71
+ const queryAt = beforeHash.indexOf("?");
72
+ const path = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
73
+ // Kept as written, part by part: a URLSearchParams round trip would re-encode the
74
+ // page's own parameters (a space as "+").
75
+ const kept = (queryAt === -1 ? "" : beforeHash.slice(queryAt + 1))
76
+ .split("&")
77
+ .filter((part) => part !== "" && decodeKey(part) !== CHECKOUT_RETURN_PARAM);
78
+ kept.push(`${CHECKOUT_RETURN_PARAM}=${CHECKOUT_RETURN_VALUE}`);
79
+ return `${path}?${kept.join("&")}${hash}`;
80
+ }
81
+
82
+ /** The decoded key of one `key=value` part of a query. */
83
+ function decodeKey(part: string): string {
84
+ const key = part.split("=")[0].replace(/\+/g, " ");
85
+ try {
86
+ return decodeURIComponent(key);
87
+ } catch {
88
+ return key;
89
+ }
90
+ }
@@ -0,0 +1,242 @@
1
+ import type { UiKitLabels } from "../i18n/kit-labels";
2
+ import { checkoutReturnUrl } from "./checkout-return";
3
+
4
+ /**
5
+ * The kit's pay page (docs/billing-harmonization.md §14.4, decision 19 as amended): a
6
+ * standalone static page that ui-kit ships as `dist/pay/`, and that each app serves on
7
+ * its own pay host, `pay.<app domain>` — a separate origin, approved in Paddle, where
8
+ * Paddle.js shares the origin with nothing: no sign-in, no app code, no storage.
9
+ *
10
+ * Paddle's create-transaction answers `checkout.url`: the pay page's URL plus
11
+ * `?_ptxn=<txn>`, and Paddle.js on that page opens the transaction by itself. The app
12
+ * sends the buyer there with {@link payPageUrl}, which adds the buyer's language; the
13
+ * page maps it to Paddle's checkout locale ({@link paddleLocale}). Everything else the
14
+ * page needs — the client-side token, sandbox or live, the way back — is the app's
15
+ * `pay-config.json`, written at its web build by the `eifi1-pay-page` script.
16
+ *
17
+ * Pure, and React-free: the page's own script (`pay-page/pay.ts`) and that build script
18
+ * bundle this module. The exports below the barrel's four are theirs.
19
+ */
20
+
21
+ /** Paddle's two environments: the sandbox's client-side tokens start `test_`, live
22
+ * ones `live_`. */
23
+ export type PaddleEnvironment = "sandbox" | "live";
24
+
25
+ /** The pay page's language parameter: `?lang=<kit locale>`. */
26
+ export const PAY_PAGE_LANG_PARAM = "lang";
27
+
28
+ /** The kit's seven languages (`KitLanguageCode`), as the page reads them. */
29
+ const PAY_PAGE_LANGUAGES = ["de-CH", "en", "es", "fr", "it", "hu", "zh"] as const;
30
+
31
+ /** One of the kit's languages, as the page speaks it. */
32
+ export type PayPageLanguage = (typeof PAY_PAGE_LANGUAGES)[number];
33
+
34
+ /**
35
+ * The kit's language → Paddle's checkout locale (`Paddle.Checkout.open`,
36
+ * `settings.locale`, read 2026-10-09: ar, zh-Hans, zh-TW, da, nl, en, fr, de, it, ja, ko,
37
+ * no, pl, pt, pt-BR, tr, ru, es, sv). Paddle has no Hungarian: a Hungarian buyer gets
38
+ * English always, not Paddle's guess from the browser. The page's own lines stay in the
39
+ * kit's language, Hungarian included.
40
+ */
41
+ const PADDLE_LOCALES: Readonly<Record<PayPageLanguage, string>> = {
42
+ en: "en",
43
+ "de-CH": "de",
44
+ fr: "fr",
45
+ it: "it",
46
+ es: "es",
47
+ zh: "zh-Hans",
48
+ hu: "en",
49
+ };
50
+
51
+ /** Paddle's transaction parameter on a pay page link. @internal */
52
+ export const PAY_PAGE_TRANSACTION_PARAM = "_ptxn";
53
+
54
+ /**
55
+ * The kit language one tag names, or undefined — `resolveLanguage`'s rule
56
+ * (src/i18n/languages.ts), restated here because that module loads the seven catalogues
57
+ * and the page must stay a few kilobytes: exact first, ignoring case and reading `_` as
58
+ * `-`; then by language, so every German is `de-CH`, every Chinese `zh` (Simplified),
59
+ * and any regional form its base (`fr-CA` → `fr`).
60
+ */
61
+ function kitLanguageOf(tag: unknown): PayPageLanguage | undefined {
62
+ if (typeof tag !== "string") return undefined;
63
+ const clean = tag.trim().replace(/_/g, "-");
64
+ if (!clean) return undefined;
65
+ const lower = clean.toLowerCase();
66
+ const exact = PAY_PAGE_LANGUAGES.find((code) => code.toLowerCase() === lower);
67
+ if (exact) return exact;
68
+ const language = lower.split("-")[0];
69
+ return PAY_PAGE_LANGUAGES.find((code) => code.split("-")[0].toLowerCase() === language);
70
+ }
71
+
72
+ /**
73
+ * The kit's locale → Paddle's checkout locale: `de-CH` → `de`, `zh` → `zh-Hans`, `hu` →
74
+ * `en` (Paddle has no Hungarian), the others as they are. Any tag is read as the kit
75
+ * reads it (`de-AT` is the one German, `zh-TW` the one Chinese); one the kit doesn't
76
+ * ship is English.
77
+ */
78
+ export function paddleLocale(kitLocale: string): string {
79
+ return PADDLE_LOCALES[kitLanguageOf(kitLocale) ?? "en"];
80
+ }
81
+
82
+ /**
83
+ * The checkout's `{url}` with the buyer's language: `lang=<kit locale>` on a pay page
84
+ * link (one that carries `_ptxn`); any other URL — a hosted checkout's, which takes its
85
+ * own `locale` — unchanged, as is a locale the kit doesn't ship.
86
+ *
87
+ * const { url } = await api.checkout({ plan, interval, currency });
88
+ * noteCheckoutStarted(user.id, overview);
89
+ * window.location.assign(payPageUrl(url, i18n.language));
90
+ *
91
+ * A wrong `lang` costs nothing but the language: the page falls back to the browser's.
92
+ */
93
+ export function payPageUrl(checkoutUrl: string, locale: string): string {
94
+ const language = kitLanguageOf(locale);
95
+ if (!language) return checkoutUrl;
96
+ let url: URL;
97
+ try {
98
+ url = new URL(checkoutUrl);
99
+ } catch {
100
+ return checkoutUrl;
101
+ }
102
+ if (!url.searchParams.has(PAY_PAGE_TRANSACTION_PARAM)) return checkoutUrl;
103
+ url.searchParams.set(PAY_PAGE_LANG_PARAM, language);
104
+ return url.toString();
105
+ }
106
+
107
+ /**
108
+ * The language the page speaks: its `lang` parameter, else the browser's languages in
109
+ * their order, each matched to the kit's; else English. @internal
110
+ */
111
+ export function payPageLanguage(
112
+ lang: string | null | undefined,
113
+ browserLanguages: readonly string[] = [],
114
+ ): PayPageLanguage {
115
+ for (const candidate of [lang, ...browserLanguages]) {
116
+ const found = kitLanguageOf(candidate);
117
+ if (found) return found;
118
+ }
119
+ return "en";
120
+ }
121
+
122
+ /* ── The page's words ──────────────────────────────────────────────────────── */
123
+
124
+ /**
125
+ * The page's words in one language — built into `pay.js` at the kit's build from the
126
+ * seven catalogues, since the page has no React and no provider. @internal
127
+ */
128
+ export interface PayPageStrings {
129
+ /** `billing.payOpening`, while the checkout opens. */
130
+ opening: string;
131
+ /** `billing.payNothing`: no `_ptxn`, or no token (billing off). */
132
+ nothing: string;
133
+ /** `billing.payFailed`: Paddle.js didn't load, or the configuration is wrong. */
134
+ failed: string;
135
+ /** `billing.payBack`: the buyer closed the checkout. */
136
+ closed: string;
137
+ /** `common.back`: the link to the app's subscription page. */
138
+ back: string;
139
+ /** `legal.links.terms` and `legal.links.privacy`: the footer's links. */
140
+ terms: string;
141
+ privacy: string;
142
+ /** `legal.navLabel`: the footer's `<nav>`. */
143
+ legal: string;
144
+ }
145
+
146
+ /** The page's words, read from one of the kit's catalogues. @internal */
147
+ export function payPageStrings(labels: UiKitLabels): PayPageStrings {
148
+ return {
149
+ opening: labels.billing.payOpening,
150
+ nothing: labels.billing.payNothing,
151
+ failed: labels.billing.payFailed,
152
+ closed: labels.billing.payBack,
153
+ back: labels.common.back,
154
+ terms: labels.legal.links.terms,
155
+ privacy: labels.legal.links.privacy,
156
+ legal: labels.legal.navLabel,
157
+ };
158
+ }
159
+
160
+ /* ── pay-config.json ───────────────────────────────────────────────────────── */
161
+
162
+ /** The configuration file beside the page, written at each app's web build. @internal */
163
+ export const PAY_CONFIG_FILE = "pay-config.json";
164
+
165
+ /**
166
+ * `pay-config.json`: what the page needs that differs per app and deployment. Read by
167
+ * the page from its own origin — never from the query, where Paddle's own links (it
168
+ * puts only `_ptxn` there) would lack it and a return URL would be an open redirect on
169
+ * an approved payment domain. @internal
170
+ */
171
+ export interface PayPageConfig {
172
+ /** Paddle's client-side token (`test_…` for the sandbox, `live_…` for live); null
173
+ * while billing is off, and the page says `payNothing`. */
174
+ token: string | null;
175
+ environment: PaddleEnvironment | null;
176
+ /** The app's subscription page. Paddle.js's `successUrl` is
177
+ * `checkoutReturnUrl(returnUrl)`; "Back" goes here. */
178
+ returnUrl: string;
179
+ appName: string;
180
+ termsUrl: string;
181
+ privacyUrl: string;
182
+ }
183
+
184
+ /** Paddle's client-side token prefixes, per environment. */
185
+ const TOKEN_PREFIX: Readonly<Record<PaddleEnvironment, string>> = { sandbox: "test_", live: "live_" };
186
+
187
+ /** `https://`, or `http://` on this machine only (`localhost`, `127.0.0.1`, `[::1]`). */
188
+ function isPageUrl(value: unknown): boolean {
189
+ if (typeof value !== "string") return false;
190
+ let url: URL;
191
+ try {
192
+ url = new URL(value);
193
+ } catch {
194
+ return false;
195
+ }
196
+ if (url.protocol === "https:") return true;
197
+ return url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
198
+ }
199
+
200
+ /**
201
+ * What is wrong with a `pay-config.json`, one line each — `[]` when nothing is. The
202
+ * build script refuses to write a file with any (so a wrong pairing fails the build,
203
+ * not a buyer), and the page says `payFailed` for one it reads:
204
+ *
205
+ * - a token whose prefix disagrees with the environment (`test_` ↔ sandbox, `live_` ↔
206
+ * live), or a token without an environment;
207
+ * - a return, terms or privacy URL that isn't `https://` (`http://` for localhost only);
208
+ * - no app name. @internal
209
+ */
210
+ export function payPageConfigProblems(config: unknown): string[] {
211
+ if (typeof config !== "object" || config === null) return ["the configuration is not an object"];
212
+ const c = config as Record<string, unknown>;
213
+ const problems: string[] = [];
214
+ const { token, environment } = c;
215
+ if (environment !== null && environment !== undefined && environment !== "sandbox" && environment !== "live") {
216
+ problems.push(`environment must be "sandbox" or "live", not ${JSON.stringify(environment)}`);
217
+ }
218
+ if (token !== null && token !== undefined) {
219
+ if (typeof token !== "string" || token === "") {
220
+ problems.push("token must be Paddle's client-side token, or null while billing is off");
221
+ } else if (environment !== "sandbox" && environment !== "live") {
222
+ problems.push("a token needs its environment: sandbox or live");
223
+ } else if (!token.startsWith(TOKEN_PREFIX[environment]) || token.length === TOKEN_PREFIX[environment].length) {
224
+ const other = environment === "sandbox" ? "live" : "sandbox";
225
+ problems.push(
226
+ token.startsWith(TOKEN_PREFIX[other])
227
+ ? `the token is a ${other} token (${TOKEN_PREFIX[other]}…), but the environment is ${environment}`
228
+ : `a ${environment} token starts with ${TOKEN_PREFIX[environment]}`,
229
+ );
230
+ }
231
+ }
232
+ for (const key of ["returnUrl", "termsUrl", "privacyUrl"] as const) {
233
+ if (!isPageUrl(c[key])) problems.push(`${key} must be an https:// URL (http:// for localhost only)`);
234
+ }
235
+ if (typeof c.appName !== "string" || c.appName.trim() === "") problems.push("appName is missing");
236
+ return problems;
237
+ }
238
+
239
+ /** Paddle.js's `successUrl` for a configuration: the way back, `?checkout=done`. @internal */
240
+ export function paySuccessUrl(config: Pick<PayPageConfig, "returnUrl">): string {
241
+ return checkoutReturnUrl(config.returnUrl);
242
+ }
@@ -0,0 +1,91 @@
1
+ import { useCallback } from "react";
2
+
3
+ import { useKitLocale } from "../i18n/kit-labels";
4
+ import type { LabelOverride } from "../i18n/kit-labels";
5
+ import { documentNavigation } from "../lib/document-navigation";
6
+ import { formatNumber } from "../lib/format";
7
+ import { toast } from "../components/toast";
8
+ import type { ToastAction, ToastId } from "../components/toast";
9
+ import { useBillingLabels } from "./billing-labels";
10
+ import type { BillingLabels } from "./billing-labels";
11
+ import type { PlanLimitRefusal } from "./plan-limit";
12
+ import { planLimitMailto } from "./plan-limit-notice";
13
+
14
+ export interface PlanLimitToastOptions {
15
+ /** `upgrade`: plans are bought — "Choose a plan". `contact`: the operator grants them
16
+ * by hand — "Ask for more", a mail to support. */
17
+ mode: "upgrade" | "contact";
18
+ /** `upgrade`: the app's navigation to its subscription page (the kit has no router).
19
+ * Left out, the toast has no action. */
20
+ onChoosePlan?: () => void;
21
+ /** `contact`: support@ the app's domain, opened as `planLimitMailto(email,
22
+ * contactSubject(name))`. Left out, the toast has no action. */
23
+ email?: string;
24
+ /** The app's word per dimension: `{ budgets: "Budgets", scans: "Scans this month" }`.
25
+ * A dimension without one is named by its key. */
26
+ dimensionLabels?: Readonly<Record<string, string>>;
27
+ /** How a figure is written — kastlan's storage limit is bytes. Default: a number in
28
+ * the kit's locale. */
29
+ formatValue?: (dimension: string, value: number) => string;
30
+ /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
31
+ labels?: LabelOverride<BillingLabels>;
32
+ }
33
+
34
+ /**
35
+ * `PlanLimitNotice`'s words as a toast (docs/billing-harmonization.md §14.11), for a
36
+ * refused create with no room for the notice — a row's button, a share target: "Plan
37
+ * limit reached", what to do, the figure when the refusal has one ("Budgets: 3 of 3"),
38
+ * and the mode's action.
39
+ *
40
+ * const showLimit = usePlanLimitToast({
41
+ * mode: "upgrade",
42
+ * onChoosePlan: () => navigate("/settings/subscription"),
43
+ * dimensionLabels: { budgets: t("billing.dim_budgets") },
44
+ * });
45
+ * catch (err) { const hit = isPlanLimit(err); if (hit) showLimit(hit); }
46
+ *
47
+ * One toast per dimension: its id is `plan-limit:<dimension>`, so a repeat replaces it
48
+ * in place instead of stacking a second (keksdose's own hook stacked). Called with no
49
+ * hit, it says the words without a figure, under `plan-limit`. No `limit` in the
50
+ * refusal, no figure line; keksdose's legacy `plan_budget_limit` reads as `budgets`
51
+ * (`isPlanLimit`, §12.16). Returns the toast's id.
52
+ */
53
+ export function usePlanLimitToast(options: PlanLimitToastOptions): (hit?: PlanLimitRefusal) => ToastId {
54
+ const { mode, onChoosePlan, email, dimensionLabels, formatValue, labels: labelsProp } = options;
55
+ const labels = useBillingLabels(labelsProp);
56
+ const locale = useKitLocale();
57
+ return useCallback(
58
+ (hit?: PlanLimitRefusal) => {
59
+ const dimension = hit?.dimension ?? "";
60
+ const name = dimensionLabels?.[dimension] ?? dimension;
61
+ const figure = (value: number) =>
62
+ formatValue ? formatValue(dimension, value) : formatNumber(value, { locale });
63
+ // A body without `used` was refused for being AT the limit (PlanLimitNotice's rule).
64
+ const usage =
65
+ hit?.limit !== undefined
66
+ ? labels.limitUsageLine(name, figure(hit.used ?? hit.limit), figure(hit.limit))
67
+ : undefined;
68
+ const action: ToastAction | undefined =
69
+ mode === "upgrade"
70
+ ? onChoosePlan && { label: labels.choosePlan, onClick: () => onChoosePlan() }
71
+ : email
72
+ ? {
73
+ label: labels.askForMore,
74
+ // A mailto: hands the address to the reader's mail client; the page stays.
75
+ onClick: () => documentNavigation.replace(planLimitMailto(email, labels.contactSubject(name))),
76
+ }
77
+ : undefined;
78
+ return toast.warning(labels.limitReached, {
79
+ id: dimension ? `plan-limit:${dimension}` : "plan-limit",
80
+ description: (
81
+ <span data-plan-limit-mode={mode} data-dimension={dimension || undefined}>
82
+ <span className="block">{mode === "upgrade" ? labels.limitUpgrade : labels.limitContact}</span>
83
+ {usage !== undefined && <span className="block tabular-nums">{usage}</span>}
84
+ </span>
85
+ ),
86
+ action,
87
+ });
88
+ },
89
+ [mode, onChoosePlan, email, dimensionLabels, formatValue, labels, locale],
90
+ );
91
+ }
@@ -5,18 +5,36 @@ import { cn } from "../lib/cn";
5
5
  import { Button } from "../components/ui";
6
6
  import { useBillingLabels } from "./billing-labels";
7
7
  import type { BillingLabels } from "./billing-labels";
8
+ import type { PortalTarget } from "./billing-overview";
8
9
 
9
10
  export interface SubscriptionActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
10
11
  /** "Payment and invoices", as a link — or {@link onManage}. Without either, none. */
11
12
  manageHref?: string;
12
- /** "Payment and invoices", as the app's call: `POST /billing/portal`, then off to its
13
- * `url` (§4). The kit sends no request. */
13
+ /** "Payment and invoices", as the app's call: `POST /billing/portal` with
14
+ * `{target: "overview"}`, then off to its `url` (§4, §14.5). The kit sends no
15
+ * request. */
14
16
  onManage?: () => void;
15
17
  /** "Cancel subscription", as a link — or {@link onCancel}. Without either, none. */
16
18
  cancelHref?: string;
17
- /** "Cancel subscription", as the app's call — usually the portal again, where the
18
- * provider's cancel and withdrawal functions are (§12.26). */
19
+ /** "Cancel subscription", as the app's call: the portal again, with `{target:
20
+ * "cancel"}` — its cancel link, where the provider's cancel and withdrawal functions
21
+ * are (§12.26, §14.5). Not the overview. */
19
22
  onCancel?: () => void;
23
+ /**
24
+ * Both, as one call that says where to ({@link PortalTarget}, the body of
25
+ * `POST /billing/portal`): `"overview"` for "Payment and invoices",
26
+ * `"cancel"` for "Cancel subscription" — so the target can't be mixed up (kastlan's
27
+ * cancel opened the overview). {@link onManage} and {@link onCancel} win over it.
28
+ */
29
+ onPortal?: (target: PortalTarget) => void;
30
+ /**
31
+ * The overview's `at_provider` (§14.5): whether the payer has reached the provider.
32
+ * `false`: no customer there yet, so nothing to open — a caption (`notAtProvider`)
33
+ * stands in place of both actions. `true`: both show, WHATEVER the status or source —
34
+ * a payer who bought under a grant keeps the way to the invoices and to "Cancel"
35
+ * (§12.26). Default true (0.32's behaviour).
36
+ */
37
+ atProvider?: boolean;
20
38
  /** Which of the two is under way: its spinner, no second press. */
21
39
  pending?: "manage" | "cancel" | null;
22
40
  /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
@@ -34,25 +52,40 @@ export interface SubscriptionActionsProps extends Omit<ComponentPropsWithoutRef<
34
52
  *
35
53
  * Neither takes the write lock: a read-only payer must be able to pay, leave or cancel
36
54
  * (§3.3).
55
+ *
56
+ * Before the payer has reached the provider (`atProvider={false}`, a trial without a
57
+ * card, a beta) there is no portal to open: the part says so in a caption instead of
58
+ * offering two buttons that could only end in `409 billing_not_at_provider`.
37
59
  */
38
60
  export function SubscriptionActions({
39
61
  manageHref,
40
62
  onManage,
41
63
  cancelHref,
42
64
  onCancel,
65
+ onPortal,
66
+ atProvider = true,
43
67
  pending,
44
68
  labels: labelsProp,
45
69
  className,
46
70
  ...rest
47
71
  }: SubscriptionActionsProps) {
48
72
  const labels = useBillingLabels(labelsProp);
73
+ if (!atProvider) {
74
+ return (
75
+ <div {...rest} data-at-provider="false" className={cn("text-sm text-[var(--text-muted)]", className)}>
76
+ <p>{labels.notAtProvider}</p>
77
+ </div>
78
+ );
79
+ }
80
+ const manageCall = onManage ?? (onPortal ? () => onPortal("overview") : undefined);
81
+ const cancelCall = onCancel ?? (onPortal ? () => onPortal("cancel") : undefined);
49
82
  const manage =
50
83
  manageHref !== undefined ? (
51
84
  <Button href={manageHref} variant="secondary">
52
85
  {labels.manage}
53
86
  </Button>
54
- ) : onManage !== undefined ? (
55
- <Button type="button" variant="secondary" onClick={onManage} pending={pending === "manage"}>
87
+ ) : manageCall !== undefined ? (
88
+ <Button type="button" variant="secondary" onClick={manageCall} pending={pending === "manage"}>
56
89
  {labels.manage}
57
90
  </Button>
58
91
  ) : null;
@@ -61,8 +94,8 @@ export function SubscriptionActions({
61
94
  <Button href={cancelHref} variant="link" tone="danger">
62
95
  {labels.cancel}
63
96
  </Button>
64
- ) : onCancel !== undefined ? (
65
- <Button type="button" variant="link" tone="danger" onClick={onCancel} pending={pending === "cancel"}>
97
+ ) : cancelCall !== undefined ? (
98
+ <Button type="button" variant="link" tone="danger" onClick={cancelCall} pending={pending === "cancel"}>
66
99
  {labels.cancel}
67
100
  </Button>
68
101
  ) : null;
@@ -22,6 +22,7 @@ import type {
22
22
  ProfileSettingLabels,
23
23
  TwoFactorSettingLabels,
24
24
  } from "./account-settings-labels";
25
+ import { COMMIT_EXCEPT_BILLING } from "./write-lock";
25
26
 
26
27
  // The label types moved beside the namespace's defaults (0.12.0); re-exported here so
27
28
  // every existing `import type { ProfileSettingLabels }` keeps resolving.
@@ -115,7 +116,10 @@ export type ProfileSettingProps = ProfileSettingDisplayNameProps | ProfileSettin
115
116
  *
116
117
  * The two-field form owns its drafts, starting from the saved names and following them
117
118
  * when they change; Save waits for a change, holds — saying why — while a name is
118
- * blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo).
119
+ * blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo) —
120
+ * every lock but a lapsed plan's (`COMMIT_EXCEPT_BILLING`, 0.33): a profile is the
121
+ * account's own settings, which billing never locks (docs/billing-harmonization.md §3.3,
122
+ * §12.36).
119
123
  */
120
124
  export function ProfileSetting(props: ProfileSettingProps) {
121
125
  const { name, email, role, memberSince, labels: labelsProp, id } = props;
@@ -271,7 +275,7 @@ function PersonNameFields({
271
275
  </div>
272
276
  <Button
273
277
  type="submit"
274
- commit
278
+ commit={COMMIT_EXCEPT_BILLING}
275
279
  pending={busy}
276
280
  // A blank name is a reason the person can act on; an unchanged one is not —
277
281
  // there is nothing to save, and the button is simply off.
@@ -33,6 +33,7 @@ import {
33
33
  useLockReason,
34
34
  } from "./field-parts";
35
35
  import { mergeDescribedBy } from "./choice-parts";
36
+ import type { CommitScope } from "./write-lock";
36
37
 
37
38
  /**
38
39
  * `value`/`onChange` are the TEXT's, and `onSelect` is "a suggestion was taken" —
@@ -161,7 +162,7 @@ export interface AutocompleteProps<V extends string | number = string>
161
162
  * whose result is only draft state — the form's Save writes it — leaves this off and
162
163
  * stays live under the lock. No provider, or an unlocked one: no effect.
163
164
  */
164
- commit?: boolean;
165
+ commit?: CommitScope;
165
166
  }
166
167
 
167
168
  function AutocompleteInner<V extends string | number = string>(
@@ -329,8 +330,10 @@ function AutocompleteInner<V extends string | number = string>(
329
330
  // label's first letters, which float at the same start inset. On the
330
331
  // value line it is beside what it describes, and the label keeps the
331
332
  // start edge every other label in the form's column shares.
333
+ // At Large the label stands above the field and the strip is gone
334
+ // (0.33, §10.17): the value's line is under FIELD_BASE's `py-2`.
332
335
  hasLabel
333
- ? "top-[calc(1rem+1px)] h-5 items-center"
336
+ ? "top-[calc(1rem+1px)] h-5 items-center large:top-[calc(0.5rem+1px)]"
334
337
  : "top-1/2 -translate-y-1/2",
335
338
  )}
336
339
  >
@@ -178,7 +178,10 @@ export interface ButtonGroupLinkProps extends Omit<ComponentPropsWithoutRef<"a">
178
178
  ref?: Ref<HTMLAnchorElement>;
179
179
  }
180
180
 
181
- const LINK_CURRENT = "bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]";
181
+ // The brand AS TEXT is `--brand-muted` (0.33, docs/colour-roles-harmonization.md §5.5):
182
+ // `--brand` itself is solved to 3:1 and read 2.97:1 on its own wash in the derived dark
183
+ // presets; the muted form holds 4.5:1 on it in every preset.
184
+ const LINK_CURRENT = "bg-[var(--brand-bg)] text-brand-muted hover:bg-[var(--brand-bg-hover)]";
182
185
 
183
186
  /**
184
187
  * A {@link ButtonGroup} member that navigates: a link drawn as a {@link Button}, so it
@@ -73,13 +73,17 @@ const KEYS: Key[] = [
73
73
  const KEY_BASE =
74
74
  "flex h-9 items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-[length:var(--focus-ring-width)] focus:ring-[var(--border-strong)]";
75
75
  // Two key families, two fills: digits sit on the inset surface and darken on
76
- // hover, operators sit a step down on `--border` and lift toward that surface —
76
+ // hover, operators sit a step down on `--bg-active` and lift toward that surface —
77
77
  // the same split `NumberPadSheet` uses for this keypad on a phone. The tokens
78
78
  // flip with the theme, so neither needs a `dark:` twin.
79
- const KEY_DIGIT =
80
- "bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)]";
81
- const KEY_ACCENT =
82
- "bg-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--bg-surface-2)]";
79
+ //
80
+ // 0.33 (docs/colour-roles-harmonization.md §12.2, §12.9): the digits hover onto
81
+ // `--bg-active`, 1.11–1.18:1 from the key — `--bg-hover` is mixed against the card and
82
+ // sat only 1.04:1 from the well, which in light is the page. The operators rest on
83
+ // `--bg-active` rather than `--border`: More contrast steps the border until it is a
84
+ // 3:1 LINE, and as a fill under the label that left 2.2–2.7:1.
85
+ const KEY_DIGIT = "bg-surface-2 text-primary hover:bg-active";
86
+ const KEY_ACCENT = "bg-active text-secondary hover:bg-surface-2";
83
87
 
84
88
  function seed(initial: string): string {
85
89
  const t = initial.trim();
@@ -186,7 +190,7 @@ function CalculatorPanel({
186
190
  // "C" is a convention, not a name: read aloud it is the letter.
187
191
  aria-label={labels.clear}
188
192
  onClick={clearAll}
189
- className={cn(KEY_BASE, "col-span-2 bg-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--bg-surface-2)]")}
193
+ className={cn(KEY_BASE, "col-span-2", KEY_ACCENT)}
190
194
  >
191
195
  C
192
196
  </button>
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
3
3
  import { cn } from "../lib/cn";
4
4
  import { Checkbox } from "./checkbox";
5
5
  import { hasMessage, mergeDescribedBy } from "./choice-parts";
6
- import { useCommitReason } from "./write-lock";
6
+ import { useCommitReason, type CommitScope } from "./write-lock";
7
7
  import { RequiredStarOnLegend } from "./field-parts";
8
8
 
9
9
  /** One box of a {@link CheckboxGroup}. */
@@ -76,7 +76,7 @@ export interface CheckboxGroupProps<T extends string = string> {
76
76
  * per-locale grants). Under a locked {@link WriteLockProvider} every box is locked
77
77
  * the focusable way with the lock's reason. See {@link Checkbox}'s `commit`.
78
78
  */
79
- commit?: boolean;
79
+ commit?: CommitScope;
80
80
  /** Why no box can be changed — every box gets it, and it wins over `disabled` (the
81
81
  * boxes stay focusable to say it). See {@link Checkbox}'s `disabledReason`. */
82
82
  disabledReason?: ReactNode;
@@ -12,6 +12,7 @@ import {
12
12
  type DisabledReasonDisplay,
13
13
  } from "./field-parts";
14
14
  import { FOCUS_RING } from "./focus-ring";
15
+ import type { CommitScope } from "./write-lock";
15
16
 
16
17
  /**
17
18
  * A checkbox that is still `<input type="checkbox">`.
@@ -134,7 +135,7 @@ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement
134
135
  * locked the `disabledReason` way with the lock's reason (which wins over its own).
135
136
  * No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.
136
137
  */
137
- commit?: boolean;
138
+ commit?: CommitScope;
138
139
  }
139
140
 
140
141
  /**