@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
@@ -10,7 +10,7 @@ import { DisabledReasonLine, useDisabledReasonLine, type DisabledReasonDisplay }
10
10
  import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink, useKitLocale } from "../i18n/kit-labels";
11
11
  import { pickLinkRenderer } from "./text-link";
12
12
  import { Tooltip } from "./tooltip";
13
- import { useCommitReason } from "./write-lock";
13
+ import { useCommitReason, type CommitScope } from "./write-lock";
14
14
 
15
15
  /**
16
16
  * A chip: a compact pill carrying one value.
@@ -137,7 +137,9 @@ const TONE: Record<ChipTone, { idle: string; selected: string }> = {
137
137
  // outline toggle that stays an outline when on has no visible state.
138
138
  const OUTLINE: Record<ChipTone, string> = {
139
139
  neutral: "border-[var(--border-strong)] bg-transparent text-[var(--text-secondary)]",
140
- brand: "border-[var(--brand)] bg-transparent text-[var(--brand)]",
140
+ // The brand AS TEXT is `--brand-muted` (0.33): `--brand` itself is solved to 3:1, a
141
+ // fill's boundary, and as text it fell to 3.0:1 in the derived dark presets.
142
+ brand: "border-[var(--brand)] bg-transparent text-brand-muted",
141
143
  danger: "border-[var(--danger-border)] bg-transparent text-[var(--danger)]",
142
144
  warning: "border-[var(--warning-border)] bg-transparent text-[var(--warning)]",
143
145
  success: "border-[var(--success-border)] bg-transparent text-[var(--success)]",
@@ -151,25 +153,27 @@ const OUTLINE: Record<ChipTone, string> = {
151
153
  orange: "border-[var(--hue-orange-border)] bg-transparent text-[var(--hue-orange)]",
152
154
  };
153
155
 
154
- // `solid`: the fill under its contrast pair. Only brand and danger have a declared
155
- // `-contrast` token; the other hues are 700s in the light theme and 300s in the dark
156
- // one, so `--text-inverse` (the surface colour) is the side of the pair that contrasts
157
- // with them in both — which is what the inverse token is for. The border goes
158
- // transparent rather than away, so a solid chip is exactly as tall as a soft one.
156
+ // `solid`: the fill under its own foreground, `<fill>-contrast` (0.33, colour-roles
157
+ // §5.1): white in light, the tone's 950 (the page, for money) on the dark theme's
158
+ // pastels. Until 0.33 only brand and danger had one and the rest borrowed
159
+ // `--text-inverse`, the surface colour — which left light warning at 4.24:1 and orange
160
+ // at 4.38:1. Neutral keeps the inverse pair: a count pill, louder than the neutral
161
+ // fill. The border goes transparent rather than away, so a solid chip is exactly as
162
+ // tall as a soft one.
159
163
  const SOLID: Record<ChipTone, string> = {
160
- neutral: "border-transparent bg-[var(--bg-inverse)] text-[var(--text-inverse)]",
161
- brand: "border-transparent bg-[var(--brand)] text-[var(--brand-contrast)]",
162
- danger: "border-transparent bg-[var(--danger)] text-[var(--danger-contrast)]",
163
- warning: "border-transparent bg-[var(--warning)] text-[var(--text-inverse)]",
164
- success: "border-transparent bg-[var(--success)] text-[var(--text-inverse)]",
165
- info: "border-transparent bg-[var(--info)] text-[var(--text-inverse)]",
166
- income: "border-transparent bg-[var(--money-income)] text-[var(--text-inverse)]",
167
- expense: "border-transparent bg-[var(--money-expense)] text-[var(--text-inverse)]",
168
- blue: "border-transparent bg-[var(--hue-blue)] text-[var(--text-inverse)]",
169
- indigo: "border-transparent bg-[var(--hue-indigo)] text-[var(--text-inverse)]",
170
- purple: "border-transparent bg-[var(--hue-purple)] text-[var(--text-inverse)]",
171
- teal: "border-transparent bg-[var(--hue-teal)] text-[var(--text-inverse)]",
172
- orange: "border-transparent bg-[var(--hue-orange)] text-[var(--text-inverse)]",
164
+ neutral: "border-transparent bg-inverse text-inverse",
165
+ brand: "border-transparent bg-brand text-brand-contrast",
166
+ danger: "border-transparent bg-danger text-danger-contrast",
167
+ warning: "border-transparent bg-warning text-warning-contrast",
168
+ success: "border-transparent bg-success text-success-contrast",
169
+ info: "border-transparent bg-info text-info-contrast",
170
+ income: "border-transparent bg-money-pos text-money-income-contrast",
171
+ expense: "border-transparent bg-money-neg text-money-expense-contrast",
172
+ blue: "border-transparent bg-hue-blue text-hue-blue-contrast",
173
+ indigo: "border-transparent bg-hue-indigo text-hue-indigo-contrast",
174
+ purple: "border-transparent bg-hue-purple text-hue-purple-contrast",
175
+ teal: "border-transparent bg-hue-teal text-hue-teal-contrast",
176
+ orange: "border-transparent bg-hue-orange text-hue-orange-contrast",
173
177
  };
174
178
 
175
179
  /** `dot`: the fill of the dot itself — the tone's strongest colour, which is the one
@@ -380,7 +384,8 @@ export function refreshChipEdges(): void {
380
384
  for (let pass = 0; pass <= n && pending.length; pass++) {
381
385
  const reads = pending.map((el) => {
382
386
  const r = el.getBoundingClientRect();
383
- const label = el.querySelector<HTMLElement>(".truncate");
387
+ // The text label (CHIP_LABEL_TRUNCATE; `.truncate` before 0.33).
388
+ const label = el.querySelector<HTMLElement>(".truncate-until-large");
384
389
  // Not laid out (a hidden ancestor, jsdom), or truncated at its container's edge,
385
390
  // where a wider chip would overflow: leave it alone. So too a label wrapped onto
386
391
  // a second line at Large (0.32.1): the chip is already as wide as its row lets it
@@ -511,16 +516,16 @@ const CHIP_BASE = `${CHIP_PILL} ${CHIP_RING}`;
511
516
  * "…". keksdose's "Zur Prüfung zurücklegen" read "Zur Prüfung zu…" on a phone at Extra
512
517
  * large, which is a different instruction. No size has a fixed height (padding and
513
518
  * `min-h-*` only), so the pill grows with its lines; the mark before the text stays
514
- * centred on them, as in a pill. A `large:` class, so Normal keeps its one-line chip.
519
+ * centred on them, as in a pill. A `large:` rule, so Normal keeps its one-line chip.
515
520
  *
516
- * `break-words`, not `[overflow-wrap:anywhere]`: a status chip often sits in a table
517
- * cell, and `anywhere` lowers the label's min-content width to one letter, so an
518
- * auto-sized column can squeeze it to a letter per line (StatusDot's label did, in the
519
- * showcase's tone table at 360 px, Extra large). `break-word` keeps each word whole
520
- * wherever the width is the content's to choose, and still breaks one that cannot fit
521
- * the row at all.
521
+ * 0.33 (§10.17): the kit's one `truncate-until-large` (tokens.css) in place of this
522
+ * file's own `truncate` + `large:whitespace-normal large:break-words`, as StatusDot's
523
+ * label took it. The utility breaks with `overflow-wrap: anywhere`, which lowers the
524
+ * label's min-content width to one letter: a status chip in an auto-sized table column
525
+ * wants the column given a width at Large, or it is squeezed to a letter per line
526
+ * (StatusDot's label was, in the showcase's tone table at 360 px, Extra large).
522
527
  */
523
- const CHIP_LABEL_WRAP_AT_LARGE = "large:whitespace-normal large:break-words";
528
+ const CHIP_LABEL_TRUNCATE = "truncate-until-large";
524
529
 
525
530
  /**
526
531
  * An interactive chip's touch target at Large and Extra large (docs/text-size-harmonization.md
@@ -720,7 +725,7 @@ export type ChipProps = ChipBaseProps &
720
725
  /** This chip COMMITS — pressing it saves. Under a locked
721
726
  * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
722
727
  * reason, as {@link Button}'s `commit` does. */
723
- commit?: boolean;
728
+ commit?: CommitScope;
724
729
  }
725
730
  );
726
731
 
@@ -841,7 +846,7 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
841
846
  ) : (
842
847
  <Icon className={cn(s.icon, "shrink-0")} aria-hidden />
843
848
  ))}
844
- {/* Text truncates at Normal and wraps at Large (CHIP_LABEL_WRAP_AT_LARGE). Mixed
849
+ {/* Text truncates at Normal and wraps at Large (CHIP_LABEL_TRUNCATE). Mixed
845
850
  children (an icon and a word) sit in a row instead: as a plain span, the svg —
846
851
  a block under Tailwind's preflight — stacked above the text (keksdose live
847
852
  #358). */}
@@ -849,7 +854,7 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
849
854
  className={cn(
850
855
  "min-w-0",
851
856
  typeof children === "string" || typeof children === "number"
852
- ? cn("truncate", CHIP_LABEL_WRAP_AT_LARGE)
857
+ ? CHIP_LABEL_TRUNCATE
853
858
  : "inline-flex items-center gap-1 [&>svg]:size-[1em] [&>svg]:shrink-0",
854
859
  )}
855
860
  >
@@ -247,13 +247,15 @@ const META_TONE: Record<ActionCardMetaTone, string> = {
247
247
  };
248
248
 
249
249
  /** The colour of {@link ActionCardProps.icon}. `default` is the secondary text colour
250
- * the icon has always had; `brand` tints it (keksdose's privacy enrolment). */
250
+ * the icon has always had; `brand` tints it (keksdose's privacy enrolment) with the
251
+ * brand as text, `--brand-muted` (0.33, docs/colour-roles-harmonization.md §5.5):
252
+ * `--brand` read 3.03:1 on the card's hover in the derived dark presets. */
251
253
  export type ActionCardIconTone = "default" | "muted" | "brand" | "warning" | "danger" | "info" | "success";
252
254
 
253
255
  const ICON_TONE: Record<ActionCardIconTone, string> = {
254
256
  default: "text-[var(--text-secondary)]",
255
257
  ...META_TONE,
256
- brand: "text-[var(--brand)]",
258
+ brand: "text-brand-muted",
257
259
  };
258
260
 
259
261
  /** What {@link ActionCardProps.renderLink} (and the provider's `linkComponent`) is
@@ -34,7 +34,7 @@ import {
34
34
  useLockReason,
35
35
  } from "./field-parts";
36
36
  import { hasMessage, mergeDescribedBy } from "./choice-parts";
37
- import { useCommitReason } from "./write-lock";
37
+ import { useCommitReason, type CommitScope } from "./write-lock";
38
38
 
39
39
  // One look for both combobox flavors below — the suggestion list and its rows
40
40
  // must stay pixel-identical between the free-text and the id-keyed variant (and
@@ -207,7 +207,7 @@ export interface ComboboxProps
207
207
  /** This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the
208
208
  * `disabledReason` way with the lock's reason; inside a form with its own Save, leave
209
209
  * it off and put `commit` on the Save. No provider, or an unlocked one: no effect. */
210
- commit?: boolean;
210
+ commit?: CommitScope;
211
211
  /** Heading an option belongs under. Supplying it makes this list read exactly
212
212
  * like {@link InlineEntityCombobox}'s — one heading per group with its rows
213
213
  * indented beneath — instead of a flat list (feedback #136 rework: the payee
@@ -849,7 +849,7 @@ export interface InlineEntityComboboxProps<V extends string | number, C extends
849
849
  * A picker inside a form with its own Save stays editable under the lock — leave this
850
850
  * off there and put `commit` on the Save. No provider, or an unlocked one: no effect.
851
851
  */
852
- commit?: boolean;
852
+ commit?: CommitScope;
853
853
  /**
854
854
  * A last row that makes a new record from what was typed — keksdose G9, whose "Create
855
855
  * cash account" had to become a separate button because this picker had no such row.
@@ -894,7 +894,7 @@ export interface InlineEntityComboboxProps<V extends string | number, C extends
894
894
  * one explanation a phone cannot show), and `onCreate` is not called. Not needed with
895
895
  * `commit`: a locked field opens no list at all.
896
896
  */
897
- createCommit?: boolean;
897
+ createCommit?: CommitScope;
898
898
  /**
899
899
  * The `<input>` — the focusable control, the one a reader types in — as a React 19
900
900
  * ref prop (0.24, kastlan). Its account picker is this component inside an
@@ -6,7 +6,7 @@ import { TypedConfirmField, typedMatches } from "./danger-confirm";
6
6
  import type { TypedMatch } from "./danger-confirm";
7
7
  import { DialogFrame } from "./dialog-frame";
8
8
  import { Button } from "./ui";
9
- import { useWriteLock } from "./write-lock";
9
+ import { useWriteLock, writeLockFor, type CommitScope } from "./write-lock";
10
10
 
11
11
  /**
12
12
  * The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback
@@ -83,8 +83,12 @@ export interface ConfirmOptions {
83
83
  * host sits — it is `aria-disabled` with the lock's reason, so a read-only page (a
84
84
  * demo, a viewer's budget) can't confirm a delete through a dialog. One prop for the
85
85
  * many confirm sites an app has (kastlan's 0.31 adoption counted ~23).
86
+ *
87
+ * 0.33: a {@link CommitScope} — `confirm({ commit: COMMIT_EXCEPT_BILLING })` for a
88
+ * confirm a lapsed plan still allows (removing access, the account's own settings;
89
+ * docs/billing-harmonization.md §12.36).
86
90
  */
87
- commit?: boolean;
91
+ commit?: CommitScope;
88
92
  }
89
93
 
90
94
  export type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;
@@ -107,12 +111,15 @@ export function useConfirm(): ConfirmFn {
107
111
  throw new Error("useConfirm() must be called below a <ConfirmProvider>.");
108
112
  }
109
113
  // The lock of the CALLER's place: the dialog host usually sits above the app's
110
- // `WriteLockProvider`, so it could not read the lock itself.
114
+ // `WriteLockProvider`, so it could not read the lock itself. Each call's own `commit`
115
+ // scope is applied to it when the call runs (`writeLockFor`, §12.36).
111
116
  const lock = useWriteLock();
112
117
  return useCallback(
113
- (options: ConfirmOptions) =>
114
- confirm(options.commit && lock.locked ? ({ ...options, lockReason: lock.reason } as ConfirmOptions) : options),
115
- [confirm, lock.locked, lock.reason],
118
+ (options: ConfirmOptions) => {
119
+ const scoped = writeLockFor(lock, options.commit);
120
+ return confirm(scoped.locked ? ({ ...options, lockReason: scoped.reason } as ConfirmOptions) : options);
121
+ },
122
+ [confirm, lock],
116
123
  );
117
124
  }
118
125
 
@@ -13,7 +13,7 @@ import {
13
13
  type ComboOption,
14
14
  } from "./combobox-core";
15
15
  import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts";
16
- import { useCommitReason } from "./write-lock";
16
+ import { useCommitReason, type CommitScope } from "./write-lock";
17
17
  import {
18
18
  DEFAULT_COMBOBOX_LABELS,
19
19
  DEFAULT_COMMON_LABELS,
@@ -147,7 +147,7 @@ export interface CountrySelectProps<Clearable extends boolean = false>
147
147
  * reason. A country field inside a form that has its own Save stays editable under
148
148
  * the lock — leave this off there, and put `commit` on the Save.
149
149
  */
150
- commit?: boolean;
150
+ commit?: CommitScope;
151
151
  /** The language the names are in. Default: the `<UiKitProvider locale>`, else
152
152
  * English (see {@link countryName}). */
153
153
  locale?: string;
@@ -6,7 +6,7 @@ import { Button, Input, Label, Spinner } from "./ui";
6
6
  import type { ButtonVariant, InputProps } from "./ui";
7
7
  import { Checkbox } from "./checkbox";
8
8
  import { Tooltip } from "./tooltip";
9
- import { useCommitReason } from "./write-lock";
9
+ import { useCommitReason, type CommitScope } from "./write-lock";
10
10
 
11
11
  /**
12
12
  * How typed text is compared with the text it has to repeat — shared by
@@ -567,7 +567,7 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">
567
567
  * provider no longer threads `lockedReason={lock.locked ? lock.reason : undefined}`
568
568
  * into every tile. No provider, or an unlocked one: no effect.
569
569
  */
570
- commit?: boolean;
570
+ commit?: CommitScope;
571
571
  /** Controlled armed state. The parent can then collapse the tile from a mutation's
572
572
  * own `onSuccess` without returning a promise. */
573
573
  armed?: boolean;
@@ -246,6 +246,23 @@ export interface DataTableProps<T> {
246
246
  rowHref?: (row: T) => string | undefined;
247
247
  expandedRow?: (row: T) => ReactNode | null;
248
248
  isExpanded?: (row: T) => boolean;
249
+ /**
250
+ * Extra classes for a row — the `<tr>` on desktop, the card on mobile. Merged last,
251
+ * so a `hover:` fill here replaces the row's own `hover:bg-[var(--bg-hover)]`.
252
+ *
253
+ * **A row you paint has to pick its own hover** (0.33, docs/colour-roles-harmonization.md
254
+ * §12.2): the kit cannot see your rest colour, and `--bg-hover` is mixed against the
255
+ * card. The rule: a hover differs from its rest by 1.08:1 or more, and every text role
256
+ * on it keeps 4.5:1.
257
+ * - A row that rests on the well (`bg-surface-2`) hovers to the CARD,
258
+ * `hover:bg-surface`: in light the well is the page, and `--bg-hover` sits 1.04:1
259
+ * from it.
260
+ * - A row whose well MARKS A STATE (selected) keeps its fill and draws a 1 px inside
261
+ * line instead, since the card would read as "not selected":
262
+ * `bg-surface-2 hover:bg-surface-2 hover:outline hover:-outline-offset-1 hover:outline-strong`.
263
+ * An outline, not a shadow: a shadow on a `<tr>` is not painted by every engine.
264
+ * - Not `--bg-active` for a selected row: its muted and money text fall to 3.8–4.7:1.
265
+ */
249
266
  rowClassName?: (row: T) => string | undefined;
250
267
  /**
251
268
  * Extra DOM attributes for a row's container — the `<tr>` on desktop, the
@@ -1199,15 +1216,9 @@ export function DataTable<T>({
1199
1216
  label: a.label,
1200
1217
  icon: a.icon,
1201
1218
  onCommit: () => void runRowAction(a.action, row),
1202
- // Each fill's own text colour — the fills are pastels in dark mode (0.26).
1203
- className:
1204
- a.tone === "danger"
1205
- ? "bg-[var(--danger)] text-[var(--danger-contrast)]"
1206
- : "bg-[var(--brand)] text-[var(--brand-contrast)]",
1207
- armedClassName:
1208
- a.tone === "danger"
1209
- ? "bg-[var(--danger-hover)] text-[var(--danger-contrast)]"
1210
- : "bg-[var(--brand-hover)] text-[var(--brand-contrast)]",
1219
+ // The row action's tone, painted by SwipeableRow: its soft wash idle, its fill
1220
+ // and that fill's foreground armed (0.33).
1221
+ tone: a.tone === "danger" ? "danger" : "brand",
1211
1222
  });
1212
1223
  return {
1213
1224
  start: swipes.filter((a) => a.swipe === "start").map(toSwipe),
@@ -17,7 +17,7 @@ import { splitTriggerAria } from "./trigger-aria";
17
17
  import type { TriggerAria } from "./trigger-aria";
18
18
  import { Button, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
19
19
  import { FOCUS_RING_WIDTH } from "./focus-ring";
20
- import { FieldBox, FieldLabelLine, useFieldMessages } from "./field-parts";
20
+ import { FieldBox, FieldLabelLine, LABEL_ABOVE_BOX, useFieldMessages } from "./field-parts";
21
21
  import { FullBleedDialog } from "./full-bleed-dialog";
22
22
  import { usePhoneLayout, type RemLength } from "../hooks/use-breakpoint";
23
23
  import { DEFAULT_MINI_CALENDAR_LABELS, MiniCalendar, type MiniCalendarProps } from "./mini-calendar";
@@ -210,11 +210,12 @@ function CustomTrigger({
210
210
  * The trigger's value wraps instead of truncating at Large and Extra large (0.32.1, the
211
211
  * 360 px Extra-large sweep; docs/text-size-harmonization.md §4 "nothing truncates"). A
212
212
  * range ("01/09/2026 – 30/09/2026") is 312 px at 150 %, and on a 360 px phone it read
213
- * "01/09/2026 – 30/…" — its end, the half a reader needs, was the half cut off. It breaks
214
- * at its spaces; a single date breaks only where it cannot fit at all. A `large:` class,
215
- * so Normal keeps its one-line field.
213
+ * "01/09/2026 – 30/…" — its end, the half a reader needs, was the half cut off. A
214
+ * `large:` class, so Normal keeps its one-line field: since 0.33 (§10.17) the kit's one
215
+ * `truncate-until-large` (tokens.css) in place of this file's own `truncate` +
216
+ * `large:whitespace-normal large:break-words`.
216
217
  */
217
- const VALUE_WRAP_AT_LARGE = "large:whitespace-normal large:break-words";
218
+ const VALUE_TRUNCATE = "truncate-until-large";
218
219
 
219
220
  /**
220
221
  * The field-shaped button that opens the calendar.
@@ -330,7 +331,7 @@ function DateFieldTrigger({
330
331
 
331
332
  return (
332
333
  <button {...attrs}>
333
- <span id={valueId} className={cn("truncate", VALUE_WRAP_AT_LARGE, !hasValue && "text-[var(--text-placeholder)]")}>
334
+ <span id={valueId} className={cn(VALUE_TRUNCATE, !hasValue && "text-[var(--text-placeholder)]")}>
334
335
  {/* `|| " "` (a NON-BREAKING space) — triggerText is "" when there is no value and no
335
336
  placeholder was passed. An empty span has no line box, so the
336
337
  trigger collapses to its padding and sits shorter than every
@@ -515,7 +516,9 @@ function DateField({
515
516
  return (
516
517
  // The caller's attributes land here, on the field's own box — the trigger inside is
517
518
  // named by `aria-labelledby` and must keep the id pair it is given.
518
- <div {...wrapperRest} ref={rootRef} className={cn("relative", className)}>
519
+ // At Large a labelled box is a grid: the label above, the calendar glyph and the
520
+ // clear × measured against the trigger's row (LABEL_ABOVE_BOX, 0.33 §10.17).
521
+ <div {...wrapperRest} ref={rootRef} className={cn("relative", label !== undefined && LABEL_ABOVE_BOX, className)}>
519
522
  {label !== undefined && <FieldLabelLine label={label} hint={labelHint} />}
520
523
  {/* The visible FieldLabel is a plain span, not a `<label htmlFor>`, so it names
521
524
  nothing on its own — this hidden twin is what the trigger is named by.
@@ -23,7 +23,7 @@ import {
23
23
  useLockReason,
24
24
  } from "./field-parts";
25
25
  import { assignRef, mergeDescribedBy } from "./choice-parts";
26
- import { useCommitReason } from "./write-lock";
26
+ import { useCommitReason, type CommitScope } from "./write-lock";
27
27
 
28
28
  export type { ComboClearValue, ComboOption } from "./combobox-core";
29
29
 
@@ -89,7 +89,7 @@ export interface EntityComboboxProps<V extends string | number, C extends ComboC
89
89
  * A picker inside a form with its own Save stays editable under the lock: leave this
90
90
  * off there and put `commit` on the Save. No provider, or an unlocked one: no effect.
91
91
  */
92
- commit?: boolean;
92
+ commit?: CommitScope;
93
93
  /** When set, a "create" row appears for a non-empty query with no exact match. */
94
94
  onCreate?: (query: string) => void;
95
95
  createLabel?: (query: string) => string;
@@ -106,7 +106,7 @@ export interface EntityComboboxProps<V extends string | number, C extends ComboC
106
106
  * taken, with the lock's reason as its second line — the picker itself stays live.
107
107
  * keksdose dev#496: on the read-only demo choosing is draft state, minting is not.
108
108
  */
109
- createCommit?: boolean;
109
+ createCommit?: CommitScope;
110
110
  /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
111
111
  invalid?: boolean;
112
112
  /** What is wrong with the value, as {@link Input}'s `error`: rendered under the
@@ -5,7 +5,7 @@ import { cn } from "../lib/cn";
5
5
  import { useInlineFacts } from "../hooks/use-large-text";
6
6
  import { FOCUS_RING } from "./focus-ring";
7
7
  import { Tooltip, type TooltipSide } from "./tooltip";
8
- import { useCommitReason } from "./write-lock";
8
+ import { useCommitReason, type CommitScope } from "./write-lock";
9
9
  import { hasMessage, mergeDescribedBy } from "./choice-parts";
10
10
 
11
11
  /**
@@ -34,12 +34,45 @@ import { hasMessage, mergeDescribedBy } from "./choice-parts";
34
34
  export const STATIC_LABEL_TYPE =
35
35
  "text-caption leading-tight text-[var(--text-muted)] peer-disabled:opacity-50";
36
36
 
37
+ /**
38
+ * A field's label at Large and Extra large (0.33, docs/text-size-harmonization.md
39
+ * §10.17): a static label ABOVE the field — the phone card's "label above value" (§4) —
40
+ * not a label squeezed into the field's top strip, where a long one ended in "…". It
41
+ * stops being absolute, so it wraps like any text (`truncate-until-large` beside it); it
42
+ * takes the pointer, since nothing of the field lies under it any more; and the field
43
+ * no longer reserves the strip for it (`FIELD_FLOATING_PAD`). In the first row of a box
44
+ * that is a grid at Large ({@link LABEL_ABOVE_BOX}), wherever it sits in the DOM — a
45
+ * floating label FOLLOWS its input, for `peer-*`. `large:` classes, so it costs no
46
+ * render and is right on the first paint; Normal keeps the floating label.
47
+ *
48
+ * The placement only: a label element adds `large:block` (a `<span>` or `<label>` in
49
+ * flow), a label ROW keeps its `flex`.
50
+ */
51
+ export const LABEL_ABOVE_AT_LARGE =
52
+ "large:static large:row-start-1 large:mb-1 large:max-w-none large:pointer-events-auto";
53
+
54
+ /**
55
+ * The box a field's label is positioned in — {@link FloatingField}'s `relative` wrapper,
56
+ * a date or month picker's root — at Large, while it holds a label above
57
+ * ({@link LABEL_ABOVE_AT_LARGE}): one column, the label's row over the field's. The
58
+ * field's own absolute parts in the box (a chevron, a clear ×, a reveal toggle, a
59
+ * calculator) are placed in the second row, so their `top-1/2` and `inset-y-*` still
60
+ * measure the FIELD and not label and field together; the label's own elements carry
61
+ * `data-field-label` and stay in the first. Only on a box with a visible label: without
62
+ * one the second row would be empty.
63
+ */
64
+ export const LABEL_ABOVE_BOX =
65
+ "large:grid large:grid-cols-[minmax(0,1fr)] large:[&>.absolute:not([data-field-label])]:row-start-2";
66
+
37
67
  // A field that always has a value (select / dropdown trigger) keeps the label
38
- // permanently in the floated position — small, in the top strip, value below.
68
+ // permanently in the floated position — small, in the top strip, value below. At Large,
69
+ // a label above the field (LABEL_ABOVE_AT_LARGE).
39
70
  export const FLOATING_LABEL_STATIC = cn(
40
71
  "pointer-events-none absolute start-3 top-1",
41
72
  STATIC_LABEL_TYPE,
42
- "max-w-[calc(100%-1.5rem)] truncate",
73
+ "max-w-[calc(100%-1.5rem)] truncate-until-large",
74
+ LABEL_ABOVE_AT_LARGE,
75
+ "large:block",
43
76
  );
44
77
 
45
78
  /**
@@ -53,7 +86,9 @@ export interface FieldLabelProps extends ComponentPropsWithoutRef<"span"> {
53
86
 
54
87
  export function FieldLabel({ children, className, ...rest }: FieldLabelProps) {
55
88
  return (
56
- <span {...rest} className={cn(FLOATING_LABEL_STATIC, "z-10", className)}>
89
+ // `data-field-label`: a label of the box it stands in, kept in the first row at Large
90
+ // (LABEL_ABOVE_BOX).
91
+ <span data-field-label="" {...rest} className={cn(FLOATING_LABEL_STATIC, "z-10", className)}>
57
92
  {children}
58
93
  </span>
59
94
  );
@@ -207,9 +242,10 @@ export function FieldCaption({ parts, className }: { parts: FieldHintParts; clas
207
242
  * Classes that take a static floating label (`FLOATING_LABEL_STATIC`: `FieldLabel`,
208
243
  * `ComboboxFieldLabel`) out of its own absolute slot and into a {@link StaticLabelRow}.
209
244
  * `static` wins over `absolute` through `cn`'s merge; `min-w-0` lets it truncate in
210
- * the row.
245
+ * the row. At Large the ROW stands above the field, so the label inside it drops the
246
+ * margin a label above takes alone (`large:mb-0`), and stays level with its "?".
211
247
  */
212
- export const LABEL_IN_ROW = "static min-w-0";
248
+ export const LABEL_IN_ROW = "static min-w-0 large:mb-0";
213
249
 
214
250
  /**
215
251
  * A static label and its "?" on one line, in the top strip of a field that lays out its
@@ -222,7 +258,10 @@ export const LABEL_IN_ROW = "static min-w-0";
222
258
  */
223
259
  export function StaticLabelRow({ hint, children }: { hint: ReactNode; children: ReactNode }) {
224
260
  return (
225
- <div className="pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1">
261
+ <div
262
+ data-field-label=""
263
+ className={cn("pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1", LABEL_ABOVE_AT_LARGE)}
264
+ >
226
265
  {children}
227
266
  <span className="pointer-events-auto flex shrink-0 items-center">{hint}</span>
228
267
  </div>
@@ -270,7 +309,7 @@ export interface LockReason {
270
309
  * switch, a checkbox or a select that saves on change, a list row whose click writes.
271
310
  * The lock's reason wins over the control's own (`useCommitReason`).
272
311
  */
273
- export function useLockReason(commit: boolean | undefined, disabledReason: ReactNode): LockReason {
312
+ export function useLockReason(commit: CommitScope | undefined, disabledReason: ReactNode): LockReason {
274
313
  const reason = useCommitReason(commit, disabledReason);
275
314
  const reasonId = useId();
276
315
  return { reason, locked: hasMessage(reason), reasonId };
@@ -510,9 +549,12 @@ export function FieldLabelLine({ label, hint }: { label: ReactNode; hint?: React
510
549
  if (!hasContent(hint)) return <FieldLabel>{label}</FieldLabel>;
511
550
  return (
512
551
  // `z-10` as FieldLabel has it: the trigger after it is `relative` and would paint
513
- // over the row (and take the "?"'s hover) otherwise.
514
- <div className="pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1">
515
- <FieldLabel className="static min-w-0 max-w-full">{label}</FieldLabel>
552
+ // over the row (and take the "?"'s hover) otherwise. Above the field at Large.
553
+ <div
554
+ data-field-label=""
555
+ className={cn("pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1", LABEL_ABOVE_AT_LARGE)}
556
+ >
557
+ <FieldLabel className={cn(LABEL_IN_ROW, "max-w-full")}>{label}</FieldLabel>
516
558
  <span className="pointer-events-auto flex shrink-0 items-center">{hint}</span>
517
559
  </div>
518
560
  );
@@ -570,12 +612,17 @@ export function LabelStrip({
570
612
  children: ReactNode;
571
613
  }) {
572
614
  return (
573
- <div {...rest} className={cn("relative min-w-0", LABEL_STRIP_PAD[pad], className)}>
574
- <div className="pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1">
615
+ // At Large the row is a label above the group, in flow (0.33, §10.17), and the
616
+ // strip's padding goes: nothing lies under it any more.
617
+ <div {...rest} className={cn("relative min-w-0", LABEL_STRIP_PAD[pad], "large:pt-0", className)}>
618
+ <div
619
+ data-field-label=""
620
+ className={cn("pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1", LABEL_ABOVE_AT_LARGE)}
621
+ >
575
622
  {/* `static`: FieldLabel positions itself absolutely; here the ROW is
576
623
  positioned, so the label and its "?" are centred on one line by the flex
577
624
  layout (dev#468). */}
578
- <FieldLabel id={labelId} className={cn("static min-w-0 max-w-full", disabled && "opacity-50")}>
625
+ <FieldLabel id={labelId} className={cn(LABEL_IN_ROW, "max-w-full", disabled && "opacity-50")}>
579
626
  {label}
580
627
  </FieldLabel>
581
628
  {hasContent(hint) && <span className="pointer-events-auto flex shrink-0 items-center">{hint}</span>}
@@ -5,7 +5,7 @@ import { useAnnounce } from "../hooks/use-announce";
5
5
  import { useKitFileLabels, useKitLabels } from "../i18n/kit-labels";
6
6
  import { Button, Spinner } from "./ui";
7
7
  import { Tooltip } from "./tooltip";
8
- import { useCommitReason } from "./write-lock";
8
+ import { useCommitReason, type CommitScope } from "./write-lock";
9
9
 
10
10
  /**
11
11
  * A button that opens the file picker — the shape all three apps kept writing by hand
@@ -444,7 +444,7 @@ export interface FileButtonProps
444
444
  * one: no effect. A picker that only fills a form the user saves later is not a
445
445
  * commit, and is left live — the form's Save carries the lock.
446
446
  */
447
- commit?: boolean;
447
+ commit?: CommitScope;
448
448
  }
449
449
 
450
450
  /** A reason that is really there — `null`, `false` and `""` are no reason. */
@@ -612,7 +612,8 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
612
612
  }
613
613
  return (
614
614
  // One inline row, so the name sits beside the button wherever the button sits; the
615
- // name truncates rather than pushing the row wider, and the full list is in the
615
+ // name truncates rather than pushing the row wider (at Normal: from Large up it wraps,
616
+ // `truncate-until-large`, text size §10.17), and the full list is in the
616
617
  // kit's own Tooltip — not a native `title` (0.16.0 shipped one: the one-tooltip rule
617
618
  // since keksdose dev#523, and a title never opens on touch). Lazy, so the closed
618
619
  // bubble adds nothing to the read-out's text.
@@ -628,7 +629,7 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
628
629
  className="flex min-w-0 text-sm text-[var(--text-secondary)]"
629
630
  >
630
631
  <Tooltip label={picked.map((f) => f.name).join(", ")} lazy className="min-w-0">
631
- <span className="block truncate">{pickedText}</span>
632
+ <span className="block truncate-until-large">{pickedText}</span>
632
633
  </Tooltip>
633
634
  </span>
634
635
  {picker.element}
@@ -15,7 +15,7 @@ import {
15
15
  import type { FilePickHandler, FilePickerLabels, FileRejection } from "./file-button";
16
16
  import { useDragTarget } from "../hooks/use-file-drop";
17
17
  import { toast } from "./toast";
18
- import { useCommitReason } from "./write-lock";
18
+ import { useCommitReason, type CommitScope } from "./write-lock";
19
19
 
20
20
  /**
21
21
  * Where a refused file's message goes.
@@ -160,7 +160,7 @@ export interface FileDropzoneProps extends Omit<ComponentPropsWithoutRef<"div">,
160
160
  * reason (which wins over one of its own). No provider, or an unlocked one: no
161
161
  * effect. A zone that only stages a file for a form saved later is not a commit.
162
162
  */
163
- commit?: boolean;
163
+ commit?: CommitScope;
164
164
  /**
165
165
  * Replace the zone's body — the icon, the text and the chosen-file list — with your
166
166
  * own, given the zone's state: keksdose's spinner-plus-"uploading", its multi-shot
@@ -243,7 +243,7 @@ export function FloatingActionButton({
243
243
  pressed === false &&
244
244
  "bg-[var(--bg-surface)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]",
245
245
  pressed === true &&
246
- "border-[var(--brand)] bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]",
246
+ "border-[var(--brand)] bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)] hover:text-brand-muted",
247
247
  "outline-none focus-visible:ring-offset-2",
248
248
  FOCUS_RING,
249
249
  className,
@@ -650,10 +650,12 @@ export interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButto
650
650
  ref?: Ref<HTMLButtonElement>;
651
651
  }
652
652
 
653
+ // Each fill under its own foreground (0.33): warning borrowed `--text-inverse`, the
654
+ // surface colour, which was 4.24:1 on the light warning fill.
653
655
  const BADGE_TONE: Record<NonNullable<FloatingActionProps["badgeTone"]>, string> = {
654
- warning: "bg-[var(--warning)] text-[var(--text-inverse)]",
655
- danger: "bg-[var(--danger)] text-[var(--danger-contrast)]",
656
- brand: "bg-[var(--brand)] text-[var(--brand-contrast)]",
656
+ warning: "bg-warning text-warning-contrast",
657
+ danger: "bg-danger text-danger-contrast",
658
+ brand: "bg-brand text-brand-contrast",
657
659
  };
658
660
 
659
661
  /**
@@ -695,7 +697,7 @@ export function FloatingAction({
695
697
  variant === "primary"
696
698
  ? "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]"
697
699
  : pressed && !isLink && pressedStyle === "tint"
698
- ? "bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]"
700
+ ? "bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)] hover:text-brand-muted"
699
701
  : "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]",
700
702
  className,
701
703
  );