@eifi1/ui-kit 0.48.2 → 0.50.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 (377) hide show
  1. package/README.md +34 -16
  2. package/dist/admin/admin-action-confirm.js +1 -1
  3. package/dist/admin/admin-action-confirm.js.map +1 -1
  4. package/dist/admin/invitations-panel.js +5 -1
  5. package/dist/admin/invitations-panel.js.map +1 -1
  6. package/dist/admin/user-roster.d.ts +24 -12
  7. package/dist/admin/user-roster.js +21 -6
  8. package/dist/admin/user-roster.js.map +1 -1
  9. package/dist/auth/accept-invitation.js +2 -2
  10. package/dist/auth/accept-invitation.js.map +1 -1
  11. package/dist/auth/not-found-page.js +1 -1
  12. package/dist/auth/not-found-page.js.map +1 -1
  13. package/dist/auth/reset-password-form.js +2 -2
  14. package/dist/auth/reset-password-form.js.map +1 -1
  15. package/dist/auth/verify-email.d.ts +9 -1
  16. package/dist/auth/verify-email.js +5 -3
  17. package/dist/auth/verify-email.js.map +1 -1
  18. package/dist/billing/plan-card.js +2 -2
  19. package/dist/billing/plan-card.js.map +1 -1
  20. package/dist/components/amount-input.d.ts +2 -1
  21. package/dist/components/amount-input.js +3 -2
  22. package/dist/components/amount-input.js.map +1 -1
  23. package/dist/components/autocomplete.js +1 -0
  24. package/dist/components/autocomplete.js.map +1 -1
  25. package/dist/components/breadcrumbs.d.ts +19 -1
  26. package/dist/components/breadcrumbs.js +11 -4
  27. package/dist/components/breadcrumbs.js.map +1 -1
  28. package/dist/components/button-look.d.ts +37 -7
  29. package/dist/components/button-look.js +33 -6
  30. package/dist/components/button-look.js.map +1 -1
  31. package/dist/components/button.d.ts +4 -1
  32. package/dist/components/button.js +2 -2
  33. package/dist/components/button.js.map +1 -1
  34. package/dist/components/calendar-heatmap.js +2 -1
  35. package/dist/components/calendar-heatmap.js.map +1 -1
  36. package/dist/components/chart-edge.d.ts +10 -0
  37. package/dist/components/chart-edge.js +57 -0
  38. package/dist/components/chart-edge.js.map +1 -0
  39. package/dist/components/chart.js +2 -1
  40. package/dist/components/chart.js.map +1 -1
  41. package/dist/components/chip.d.ts +8 -1
  42. package/dist/components/chip.js +4 -1
  43. package/dist/components/chip.js.map +1 -1
  44. package/dist/components/combobox-core.js +1 -0
  45. package/dist/components/combobox-core.js.map +1 -1
  46. package/dist/components/combobox-suggestions.js +1 -0
  47. package/dist/components/combobox-suggestions.js.map +1 -1
  48. package/dist/components/confirm-context.d.ts +5 -0
  49. package/dist/components/confirm-context.js +7 -0
  50. package/dist/components/confirm-context.js.map +1 -0
  51. package/dist/components/confirm-dialog.js +3 -3
  52. package/dist/components/confirm-dialog.js.map +1 -1
  53. package/dist/components/currency-select.d.ts +28 -2
  54. package/dist/components/currency-select.js +56 -10
  55. package/dist/components/currency-select.js.map +1 -1
  56. package/dist/components/danger-confirm.d.ts +2 -2
  57. package/dist/components/danger-confirm.js +1 -1
  58. package/dist/components/danger-confirm.js.map +1 -1
  59. package/dist/components/data-table-fit.js +21 -3
  60. package/dist/components/data-table-fit.js.map +1 -1
  61. package/dist/components/data-table-mobile.d.ts +58 -3
  62. package/dist/components/data-table-mobile.js +147 -16
  63. package/dist/components/data-table-mobile.js.map +1 -1
  64. package/dist/components/data-table-row-actions.d.ts +2 -1
  65. package/dist/components/data-table-row-actions.js +7 -0
  66. package/dist/components/data-table-row-actions.js.map +1 -1
  67. package/dist/components/data-table-row.d.ts +1 -1
  68. package/dist/components/data-table-row.js.map +1 -1
  69. package/dist/components/data-table.d.ts +34 -1
  70. package/dist/components/data-table.js +37 -10
  71. package/dist/components/data-table.js.map +1 -1
  72. package/dist/components/dropdown.js +1 -0
  73. package/dist/components/dropdown.js.map +1 -1
  74. package/dist/components/error-boundary.js +2 -2
  75. package/dist/components/error-boundary.js.map +1 -1
  76. package/dist/components/floating-panel.d.ts +36 -13
  77. package/dist/components/floating-panel.js +27 -7
  78. package/dist/components/floating-panel.js.map +1 -1
  79. package/dist/components/form-actions.d.ts +1 -1
  80. package/dist/components/form-actions.js +1 -1
  81. package/dist/components/form-actions.js.map +1 -1
  82. package/dist/components/full-bleed-dialog.js +1 -0
  83. package/dist/components/full-bleed-dialog.js.map +1 -1
  84. package/dist/components/icon-button.d.ts +11 -0
  85. package/dist/components/icon-button.js +7 -5
  86. package/dist/components/icon-button.js.map +1 -1
  87. package/dist/components/modal.js +1 -0
  88. package/dist/components/modal.js.map +1 -1
  89. package/dist/components/nav-pills.d.ts +8 -1
  90. package/dist/components/nav-pills.js +61 -38
  91. package/dist/components/nav-pills.js.map +1 -1
  92. package/dist/components/number-input.d.ts +2 -1
  93. package/dist/components/number-input.js +6 -2
  94. package/dist/components/number-input.js.map +1 -1
  95. package/dist/components/numpad-sheet.js +2 -0
  96. package/dist/components/numpad-sheet.js.map +1 -1
  97. package/dist/components/page-header-context.d.ts +8 -0
  98. package/dist/components/page-header-context.js +7 -0
  99. package/dist/components/page-header-context.js.map +1 -0
  100. package/dist/components/page-header.d.ts +14 -2
  101. package/dist/components/page-header.js +10 -1
  102. package/dist/components/page-header.js.map +1 -1
  103. package/dist/components/picker-sheet.js +2 -0
  104. package/dist/components/picker-sheet.js.map +1 -1
  105. package/dist/components/pie-chart.js +2 -1
  106. package/dist/components/pie-chart.js.map +1 -1
  107. package/dist/components/popover.js +1 -0
  108. package/dist/components/popover.js.map +1 -1
  109. package/dist/components/progress-bar.js +5 -4
  110. package/dist/components/progress-bar.js.map +1 -1
  111. package/dist/components/reauth-dialog.js +1 -1
  112. package/dist/components/reauth-dialog.js.map +1 -1
  113. package/dist/components/row-actions-labels.js +3 -1
  114. package/dist/components/row-actions-labels.js.map +1 -1
  115. package/dist/components/row-actions.d.ts +43 -9
  116. package/dist/components/row-actions.js +78 -15
  117. package/dist/components/row-actions.js.map +1 -1
  118. package/dist/components/section-nav.d.ts +37 -0
  119. package/dist/components/section-nav.js +33 -0
  120. package/dist/components/section-nav.js.map +1 -0
  121. package/dist/components/series-chart.js +8 -3
  122. package/dist/components/series-chart.js.map +1 -1
  123. package/dist/components/share-card.js +1 -1
  124. package/dist/components/share-card.js.map +1 -1
  125. package/dist/components/signature-pad.js +1 -1
  126. package/dist/components/signature-pad.js.map +1 -1
  127. package/dist/components/sparkline.js +2 -1
  128. package/dist/components/sparkline.js.map +1 -1
  129. package/dist/components/stat-tile.js +14 -7
  130. package/dist/components/stat-tile.js.map +1 -1
  131. package/dist/components/status-dot.d.ts +5 -1
  132. package/dist/components/status-dot.js +4 -1
  133. package/dist/components/status-dot.js.map +1 -1
  134. package/dist/components/tooltip.js +1 -1
  135. package/dist/components/tooltip.js.map +1 -1
  136. package/dist/components/translation-review-editor.js +2 -2
  137. package/dist/components/translation-review-editor.js.map +1 -1
  138. package/dist/components/translation-review.js +2 -2
  139. package/dist/components/translation-review.js.map +1 -1
  140. package/dist/components/treemap.d.ts +5 -2
  141. package/dist/components/treemap.js +26 -6
  142. package/dist/components/treemap.js.map +1 -1
  143. package/dist/components/use-table-state.js +37 -6
  144. package/dist/components/use-table-state.js.map +1 -1
  145. package/dist/data-table.js.map +1 -1
  146. package/dist/demo/demo-banner.js +1 -1
  147. package/dist/demo/demo-banner.js.map +1 -1
  148. package/dist/demo/demo-ended.js +1 -1
  149. package/dist/demo/demo-ended.js.map +1 -1
  150. package/dist/demo/demo-start.js +1 -1
  151. package/dist/demo/demo-start.js.map +1 -1
  152. package/dist/eslint/index.js +1 -1
  153. package/dist/feedback/feedback-inbox.js +1 -1
  154. package/dist/feedback/feedback-inbox.js.map +1 -1
  155. package/dist/hooks/use-dismiss.d.ts +9 -1
  156. package/dist/hooks/use-dismiss.js +25 -5
  157. package/dist/hooks/use-dismiss.js.map +1 -1
  158. package/dist/hooks/use-document-title.d.ts +14 -0
  159. package/dist/hooks/use-document-title.js +22 -0
  160. package/dist/hooks/use-document-title.js.map +1 -0
  161. package/dist/i18n/defaults.js +2 -0
  162. package/dist/i18n/defaults.js.map +1 -1
  163. package/dist/i18n/german.js +20 -3
  164. package/dist/i18n/german.js.map +1 -1
  165. package/dist/i18n/kit-labels.d.ts +34 -1
  166. package/dist/i18n/kit-labels.js +43 -2
  167. package/dist/i18n/kit-labels.js.map +1 -1
  168. package/dist/i18n/locales/en.js +22 -3
  169. package/dist/i18n/locales/en.js.map +3 -3
  170. package/dist/i18n/locales/es.js +20 -3
  171. package/dist/i18n/locales/es.js.map +1 -1
  172. package/dist/i18n/locales/fr.js +20 -3
  173. package/dist/i18n/locales/fr.js.map +1 -1
  174. package/dist/i18n/locales/hu.js +20 -3
  175. package/dist/i18n/locales/hu.js.map +1 -1
  176. package/dist/i18n/locales/it.js +20 -3
  177. package/dist/i18n/locales/it.js.map +1 -1
  178. package/dist/i18n/locales/zh.js +20 -3
  179. package/dist/i18n/locales/zh.js.map +1 -1
  180. package/dist/i18n/review.js +3 -0
  181. package/dist/i18n/review.js.map +1 -1
  182. package/dist/i18n/shared-labels.js +1 -0
  183. package/dist/i18n/shared-labels.js.map +1 -1
  184. package/dist/index.d.ts +8 -1
  185. package/dist/index.js +11 -0
  186. package/dist/index.js.map +1 -1
  187. package/dist/landing/landing-actions.js +2 -2
  188. package/dist/landing/landing-actions.js.map +1 -1
  189. package/dist/landing/public-header.js +2 -2
  190. package/dist/landing/public-header.js.map +1 -1
  191. package/dist/lib/clipboard-minus.d.ts +27 -0
  192. package/dist/lib/clipboard-minus.js +63 -0
  193. package/dist/lib/clipboard-minus.js.map +1 -0
  194. package/dist/lib/data-attributes.d.ts +11 -0
  195. package/dist/lib/data-attributes.js +10 -0
  196. package/dist/lib/data-attributes.js.map +1 -0
  197. package/dist/lib/document-title.d.ts +17 -0
  198. package/dist/lib/document-title.js +29 -0
  199. package/dist/lib/document-title.js.map +1 -0
  200. package/dist/lib/float-clearance.js +8 -2
  201. package/dist/lib/float-clearance.js.map +1 -1
  202. package/dist/lib/format.d.ts +36 -10
  203. package/dist/lib/format.js +57 -11
  204. package/dist/lib/format.js.map +1 -1
  205. package/dist/lib/minus.d.ts +32 -0
  206. package/dist/lib/minus.js +14 -0
  207. package/dist/lib/minus.js.map +1 -0
  208. package/dist/lib/status-tone.d.ts +30 -0
  209. package/dist/lib/status-tone.js +17 -0
  210. package/dist/lib/status-tone.js.map +1 -0
  211. package/dist/settings/settings-layout.js +3 -2
  212. package/dist/settings/settings-layout.js.map +1 -1
  213. package/dist/shell/account-menu-language.d.ts +1 -1
  214. package/dist/shell/account-menu-language.js +9 -1
  215. package/dist/shell/account-menu-language.js.map +1 -1
  216. package/dist/shell/app-shell-labels.js +2 -1
  217. package/dist/shell/app-shell-labels.js.map +1 -1
  218. package/dist/shell/app-shell.d.ts +58 -16
  219. package/dist/shell/app-shell.js +151 -43
  220. package/dist/shell/app-shell.js.map +1 -1
  221. package/dist/shell/language-code.d.ts +3 -0
  222. package/dist/shell/language-code.js +8 -0
  223. package/dist/shell/language-code.js.map +1 -0
  224. package/dist/shell/topbar-controls.d.ts +14 -6
  225. package/dist/shell/topbar-controls.js +14 -17
  226. package/dist/shell/topbar-controls.js.map +1 -1
  227. package/dist/testing/css-variables.generated.d.ts +1 -1
  228. package/dist/testing/css-variables.generated.js +1 -1
  229. package/dist/testing/css-variables.generated.js.map +1 -1
  230. package/dist/testing/react.d.ts +25 -0
  231. package/dist/testing/react.js +19 -0
  232. package/dist/testing/react.js.map +1 -0
  233. package/dist/theme/chart-palette.d.ts +82 -15
  234. package/dist/theme/chart-palette.js +51 -24
  235. package/dist/theme/chart-palette.js.map +1 -1
  236. package/dist/theme/palette-presets.d.ts +9 -0
  237. package/dist/theme/palette-presets.js +77 -62
  238. package/dist/theme/palette-presets.js.map +1 -1
  239. package/dist/tour/tour-dock.d.ts +42 -0
  240. package/dist/tour/tour-dock.js +80 -0
  241. package/dist/tour/tour-dock.js.map +1 -0
  242. package/dist/tour/tour-gates.d.ts +71 -0
  243. package/dist/tour/tour-gates.js +114 -0
  244. package/dist/tour/tour-gates.js.map +1 -0
  245. package/dist/tour/tour-guide-button.d.ts +27 -0
  246. package/dist/tour/tour-guide-button.js +45 -0
  247. package/dist/tour/tour-guide-button.js.map +1 -0
  248. package/dist/tour/tour-guide-labels.d.ts +8 -0
  249. package/dist/tour/tour-guide-labels.js +9 -0
  250. package/dist/tour/tour-guide-labels.js.map +1 -0
  251. package/dist/tour/tour-labels.js +9 -1
  252. package/dist/tour/tour-labels.js.map +1 -1
  253. package/dist/tour/tour.d.ts +133 -9
  254. package/dist/tour/tour.js +594 -70
  255. package/dist/tour/tour.js.map +1 -1
  256. package/dist/tour/use-tour-url-sync.d.ts +5 -2
  257. package/dist/tour/use-tour-url-sync.js +7 -1
  258. package/dist/tour/use-tour-url-sync.js.map +1 -1
  259. package/dist/tour.d.ts +2 -0
  260. package/dist/tour.js +3 -0
  261. package/dist/tour.js.map +1 -1
  262. package/dist/wizard/stepper-nav.d.ts +1 -1
  263. package/dist/wizard/stepper-nav.js +18 -5
  264. package/dist/wizard/stepper-nav.js.map +1 -1
  265. package/dist/wizard/types.d.ts +11 -0
  266. package/dist/wizard/types.js.map +1 -1
  267. package/dist/wizard/use-wizard.js +11 -5
  268. package/dist/wizard/use-wizard.js.map +1 -1
  269. package/package.json +9 -1
  270. package/src/admin/admin-action-confirm.tsx +1 -1
  271. package/src/admin/invitations-panel.tsx +7 -3
  272. package/src/admin/user-roster.tsx +41 -14
  273. package/src/auth/accept-invitation.tsx +3 -3
  274. package/src/auth/not-found-page.tsx +1 -1
  275. package/src/auth/reset-password-form.tsx +3 -3
  276. package/src/auth/verify-email.tsx +13 -3
  277. package/src/billing/plan-card.tsx +2 -2
  278. package/src/components/amount-input.tsx +5 -4
  279. package/src/components/autocomplete.tsx +2 -0
  280. package/src/components/breadcrumbs.tsx +32 -4
  281. package/src/components/button-look.ts +86 -15
  282. package/src/components/button.tsx +6 -3
  283. package/src/components/calendar-heatmap.tsx +2 -1
  284. package/src/components/chart-edge.ts +73 -0
  285. package/src/components/chart.tsx +3 -1
  286. package/src/components/chip.tsx +10 -1
  287. package/src/components/combobox-core.tsx +2 -0
  288. package/src/components/combobox-suggestions.tsx +2 -0
  289. package/src/components/confirm-context.ts +7 -0
  290. package/src/components/confirm-dialog.tsx +3 -3
  291. package/src/components/currency-select.tsx +95 -6
  292. package/src/components/danger-confirm.tsx +3 -3
  293. package/src/components/data-table-fit.ts +41 -6
  294. package/src/components/data-table-mobile.tsx +259 -17
  295. package/src/components/data-table-row-actions.tsx +9 -1
  296. package/src/components/data-table-row.tsx +3 -0
  297. package/src/components/data-table.tsx +91 -19
  298. package/src/components/dropdown.tsx +2 -0
  299. package/src/components/error-boundary.tsx +2 -2
  300. package/src/components/floating-panel.tsx +67 -16
  301. package/src/components/form-actions.tsx +2 -2
  302. package/src/components/full-bleed-dialog.tsx +3 -0
  303. package/src/components/icon-button.tsx +21 -8
  304. package/src/components/modal.tsx +3 -0
  305. package/src/components/nav-pills.tsx +29 -2
  306. package/src/components/number-input.tsx +8 -3
  307. package/src/components/numpad-sheet.tsx +4 -0
  308. package/src/components/page-header-context.ts +8 -0
  309. package/src/components/page-header.tsx +34 -2
  310. package/src/components/picker-sheet.tsx +4 -0
  311. package/src/components/pie-chart.tsx +2 -1
  312. package/src/components/popover.tsx +2 -0
  313. package/src/components/progress-bar.tsx +6 -3
  314. package/src/components/reauth-dialog.tsx +1 -1
  315. package/src/components/row-actions-labels.ts +2 -0
  316. package/src/components/row-actions.tsx +154 -33
  317. package/src/components/section-nav.tsx +65 -0
  318. package/src/components/series-chart.tsx +12 -3
  319. package/src/components/share-card.tsx +1 -1
  320. package/src/components/signature-pad.tsx +1 -1
  321. package/src/components/sparkline.tsx +2 -1
  322. package/src/components/stat-tile.tsx +19 -7
  323. package/src/components/status-dot.tsx +7 -1
  324. package/src/components/tooltip.tsx +3 -1
  325. package/src/components/translation-review-editor.tsx +2 -2
  326. package/src/components/translation-review.tsx +2 -2
  327. package/src/components/treemap.tsx +34 -8
  328. package/src/components/use-table-state.ts +63 -8
  329. package/src/data-table.ts +3 -3
  330. package/src/demo/demo-banner.tsx +1 -1
  331. package/src/demo/demo-ended.tsx +1 -1
  332. package/src/demo/demo-start.tsx +1 -1
  333. package/src/feedback/feedback-inbox.tsx +1 -1
  334. package/src/hooks/use-dismiss.ts +43 -4
  335. package/src/hooks/use-document-title.ts +31 -0
  336. package/src/i18n/defaults.ts +2 -0
  337. package/src/i18n/german.ts +17 -0
  338. package/src/i18n/kit-labels.tsx +79 -1
  339. package/src/i18n/locales/es.ts +17 -0
  340. package/src/i18n/locales/fr.ts +17 -0
  341. package/src/i18n/locales/hu.ts +17 -0
  342. package/src/i18n/locales/it.ts +17 -0
  343. package/src/i18n/locales/zh.ts +17 -0
  344. package/src/i18n/review.ts +3 -0
  345. package/src/i18n/shared-labels.ts +1 -0
  346. package/src/index.ts +13 -0
  347. package/src/landing/landing-actions.tsx +2 -2
  348. package/src/landing/public-header.tsx +2 -2
  349. package/src/lib/clipboard-minus.ts +94 -0
  350. package/src/lib/data-attributes.ts +14 -0
  351. package/src/lib/document-title.ts +41 -0
  352. package/src/lib/float-clearance.ts +19 -3
  353. package/src/lib/format.ts +117 -30
  354. package/src/lib/minus.ts +50 -0
  355. package/src/lib/status-tone.ts +43 -0
  356. package/src/settings/settings-layout.tsx +3 -2
  357. package/src/shell/account-menu-language.tsx +14 -2
  358. package/src/shell/app-shell-labels.ts +1 -0
  359. package/src/shell/app-shell.tsx +291 -68
  360. package/src/shell/language-code.ts +5 -0
  361. package/src/shell/topbar-controls.tsx +35 -15
  362. package/src/testing/css-variables.generated.ts +6 -6
  363. package/src/testing/react.ts +59 -0
  364. package/src/theme/chart-palette.ts +102 -20
  365. package/src/theme/palette-presets.ts +121 -84
  366. package/src/tour/tour-dock.ts +132 -0
  367. package/src/tour/tour-gates.ts +195 -0
  368. package/src/tour/tour-guide-button.tsx +64 -0
  369. package/src/tour/tour-guide-labels.ts +15 -0
  370. package/src/tour/tour-labels.ts +8 -0
  371. package/src/tour/tour.tsx +949 -86
  372. package/src/tour/use-tour-url-sync.ts +14 -4
  373. package/src/tour.ts +3 -0
  374. package/src/wizard/stepper-nav.tsx +27 -7
  375. package/src/wizard/types.ts +12 -0
  376. package/src/wizard/use-wizard.ts +16 -9
  377. package/tokens.css +28 -18
@@ -0,0 +1,73 @@
1
+ // The live side of `chartEdge` (theme/chart-palette.ts): which colours a FILLED mark is
2
+ // painted with and which grounds it sits on, read off <html> as the page resolves them —
3
+ // so the 1px ink edge follows a theme toggle and a palette switch without the chart
4
+ // knowing either store. Shared by SeriesChart (bars, areas) and Treemap (tiles).
5
+ import { useCallback, useSyncExternalStore } from "react";
6
+ import { chartEdge } from "../theme/chart-palette";
7
+ import { parseHex } from "../theme/color";
8
+
9
+ /** The tokens a theme or palette change can move, read as ONE string so
10
+ * `useSyncExternalStore` compares a primitive (a fresh array would never be equal). */
11
+ const WATCHED = [
12
+ "--bg-surface",
13
+ "--bg-page",
14
+ ...Array.from({ length: 9 }, (_, i) => `--chart-${i + 1}`),
15
+ "--chart-ink",
16
+ "--chart-muted",
17
+ "--money-income",
18
+ "--money-expense",
19
+ "--money-net",
20
+ "--money-neutral",
21
+ "--brand",
22
+ ];
23
+
24
+ function readWatched(): string {
25
+ const style = getComputedStyle(document.documentElement);
26
+ return WATCHED.map((name) => style.getPropertyValue(name).trim()).join(",");
27
+ }
28
+
29
+ function subscribe(onChange: () => void): () => void {
30
+ const observer = new MutationObserver(onChange);
31
+ observer.observe(document.documentElement, {
32
+ attributes: true,
33
+ attributeFilter: ["class", "style", "data-theme", "data-contrast"],
34
+ });
35
+ return () => observer.disconnect();
36
+ }
37
+
38
+ /** Server render: nothing measured, so no edge — the client adds it on hydration. */
39
+ const SERVER = "";
40
+
41
+ const BARE_VAR = /^var\(\s*(--[A-Za-z0-9_-]+)\s*\)$/;
42
+
43
+ /** A colour as the page resolves it: a hex as it is, a bare `var(--token)` read off
44
+ * <html>, anything else (a `color-mix()`, a var with a fallback) null. */
45
+ export function resolveChartColor(color: string, style: Pick<CSSStyleDeclaration, "getPropertyValue">): string | null {
46
+ const trimmed = color.trim();
47
+ if (parseHex(trimmed)) return trimmed;
48
+ const m = BARE_VAR.exec(trimmed);
49
+ if (!m) return null;
50
+ const value = style.getPropertyValue(m[1]).trim();
51
+ return parseHex(value) ? value : null;
52
+ }
53
+
54
+ /**
55
+ * `(fill) => edge`: the ink stroke a filled mark in `fill` needs on this page, or
56
+ * `undefined` — only for a colour under 3:1 against the card or the page (WCAG 1.4.11).
57
+ * `fill` may be a hex or a bare `var(--chart-N)` / `var(--money-…)`; anything it cannot
58
+ * measure gets no edge.
59
+ */
60
+ export function useChartEdge(): (fill: string | undefined) => string | undefined {
61
+ const snapshot = useSyncExternalStore(subscribe, readWatched, () => SERVER);
62
+ return useCallback(
63
+ (fill: string | undefined) => {
64
+ if (!fill || snapshot === SERVER || typeof document === "undefined") return undefined;
65
+ const style = getComputedStyle(document.documentElement);
66
+ const hex = resolveChartColor(fill, style);
67
+ if (!hex) return undefined;
68
+ const surfaces = ["--bg-surface", "--bg-page"].map((n) => style.getPropertyValue(n).trim());
69
+ return chartEdge(hex, surfaces);
70
+ },
71
+ [snapshot],
72
+ );
73
+ }
@@ -17,6 +17,7 @@ import {
17
17
  } from "recharts";
18
18
  import { cn } from "../lib/cn";
19
19
  import { isRtl } from "../lib/direction";
20
+ import { TYPOGRAPHIC_MINUS } from "../lib/minus";
20
21
 
21
22
  export interface ChartSeriesConfig {
22
23
  label: ReactNode;
@@ -376,7 +377,8 @@ export function ChartTooltipContent({
376
377
  if (inRow && !shown) text = NO_VALUE;
377
378
  else if (formatValue) text = formatValue(raw);
378
379
  else if (raw == null) text = NO_VALUE;
379
- else text = valueFormatter ? valueFormatter(raw) : raw;
380
+ // The bare number's minus is U+2212, as every kit figure's (0.49, KIT-1).
381
+ else text = valueFormatter ? valueFormatter(raw) : String(raw).replace(/-/g, TYPOGRAPHIC_MINUS);
380
382
  return (
381
383
  <div
382
384
  key={`${cfgKey}-${i}`}
@@ -3,6 +3,7 @@ import type { KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref } from "re
3
3
  import { Check, X } from "lucide-react";
4
4
  import type { LucideIcon } from "lucide-react";
5
5
  import { cn } from "../lib/cn";
6
+ import { statusTone, type StatusKind } from "../lib/status-tone";
6
7
  import { TOUCH_TARGET_LARGE } from "./button-look";
7
8
  import { FOCUS_RING } from "./focus-ring";
8
9
  import { assignRef, LockedReason, type DisabledReasonDisplay } from "./field-parts";
@@ -342,6 +343,12 @@ const REMOVE_HIT_AREA_LARGE =
342
343
  interface ChipBaseProps {
343
344
  children: ReactNode;
344
345
  tone?: ChipTone;
346
+ /**
347
+ * What the value means as a status (0.49 KIT-8), instead of a `tone`: `good` (success),
348
+ * `attention` (warning), `problem` (danger), `active` (info), `inactive` (neutral) —
349
+ * {@link statusTone}. An explicit `tone` wins. Never `brand` for a status.
350
+ */
351
+ status?: StatusKind;
345
352
  size?: ChipSize;
346
353
  /** See {@link ChipVariant}. Default `soft`. */
347
354
  variant?: ChipVariant;
@@ -537,7 +544,8 @@ export type ChipProps = ChipBaseProps &
537
544
  export function Chip({
538
545
  ref,
539
546
  children,
540
- tone = "neutral",
547
+ tone: toneProp,
548
+ status,
541
549
  size = "md",
542
550
  variant = "soft",
543
551
  shape = "pill",
@@ -563,6 +571,7 @@ export function Chip({
563
571
  commit,
564
572
  ...rest
565
573
  }: ChipProps) {
574
+ const tone: ChipTone = toneProp ?? (status ? statusTone(status) : "neutral");
566
575
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
567
576
  const reasonId = useId();
568
577
  const disabledReason = useCommitReason(commit, ownDisabledReason);
@@ -1064,6 +1064,8 @@ export function ComboboxPanel<V extends string | number>({
1064
1064
  // `aria-describedby` has nothing to collide with.
1065
1065
  {...rest}
1066
1066
  ref={panelRef}
1067
+ // A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).
1068
+ data-kit-layer=""
1067
1069
  dir={dir}
1068
1070
  onKeyDown={onKeyDown}
1069
1071
  // A press in a panel opened from a row is inside this list for the core's outside
@@ -92,6 +92,8 @@ export function SuggestionList({
92
92
  return createPortal(
93
93
  <div
94
94
  ref={panelRef}
95
+ // A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).
96
+ data-kit-layer=""
95
97
  // z-50, not the old z-30: the list is a child of <body> now, so it is
96
98
  // competing with the app's own overlays rather than with its own siblings.
97
99
  className="fixed z-50"
@@ -0,0 +1,7 @@
1
+ import { createContext } from "react";
2
+ import type { ConfirmFn } from "./confirm-dialog";
3
+
4
+ /** The nearest `ConfirmProvider`'s `confirm`, or `null`. Internal: its own module so a
5
+ * part that may sit outside a provider (`RowActions`, 0.49 KIT-12) can ask without
6
+ * `useConfirm()`'s throw, and without adding a name to the barrel. */
7
+ export const ConfirmContext = createContext<ConfirmFn | null>(null);
@@ -1,4 +1,4 @@
1
- import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useContext, useEffect, useRef, useState } from "react";
2
2
  import type { ReactNode } from "react";
3
3
 
4
4
  import { useKitLabels } from "../i18n/kit-labels";
@@ -8,6 +8,7 @@ import { DialogFrame } from "./dialog-frame";
8
8
  import { Button } from "./button";
9
9
  import { useWriteLock, writeLockFor, type CommitScope } from "./write-lock";
10
10
  import { DEFAULT_CONFIRM_DIALOG_LABELS } from "./confirm-dialog-labels";
11
+ import { ConfirmContext } from "./confirm-context";
11
12
 
12
13
  /**
13
14
  * The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback
@@ -89,7 +90,6 @@ export interface ConfirmOptions {
89
90
 
90
91
  export type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;
91
92
 
92
- const ConfirmContext = createContext<ConfirmFn | null>(null);
93
93
 
94
94
  /**
95
95
  * `const confirm = useConfirm()` → `if (!(await confirm({ title }))) return`.
@@ -298,7 +298,7 @@ function ConfirmDialogView({
298
298
  <Button
299
299
  ref={confirmRef}
300
300
  type="button"
301
- variant={tone === "danger" ? "danger" : "primary"}
301
+ variant={tone === "danger" ? "danger" : "default"}
302
302
  disabled={!matches}
303
303
  disabledReason={request.lockReason}
304
304
  onClick={() => {
@@ -2,7 +2,16 @@ import { useCallback, useId } from "react";
2
2
  import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
3
3
  import { FieldChevron, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from "./field-base";
4
4
  import { FieldLabel } from "./field-parts";
5
- import { assignRef, EndHintRow, useFieldMessages, type FieldHelpProps } from "./field-parts";
5
+ import {
6
+ assignRef,
7
+ EndHintRow,
8
+ LockedReason,
9
+ useFieldMessages,
10
+ useLockReason,
11
+ type DisabledReasonDisplay,
12
+ type FieldHelpProps,
13
+ } from "./field-parts";
14
+ import type { CommitScope } from "./write-lock";
6
15
  import { cn } from "../lib/cn";
7
16
  import { useComboboxCore } from "./combobox-core";
8
17
  import {
@@ -90,6 +99,31 @@ export interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div"
90
99
  * filter that only matches the hidden English name is worse than no filter.
91
100
  */
92
101
  currencyNames?: Record<string, string>;
102
+ /** As {@link Select}'s: no focus, no list, the settled look, no chevron. */
103
+ disabled?: boolean;
104
+ /**
105
+ * Why the currency cannot be changed — {@link Select}'s `disabledReason` (keksdose,
106
+ * 2026-10-23: a budget's currency for a guest or a viewer, and while offline, which
107
+ * keksdose drew as a one-option `Select` to keep the reason and lost the flag). The
108
+ * trigger stays focusable and `aria-disabled` (never the `disabled` attribute), keeps
109
+ * its flag and code, opens nothing, and `onChange` is never called; the reason is a
110
+ * line under the field at Large and on a touch screen, else in the kit
111
+ * {@link Tooltip} (`disabledReasonDisplay`), and on the trigger's `aria-describedby`
112
+ * (after a caption, before an error). It wears the settled look of a disabled field
113
+ * and drops its chevron, as a locked Select does. It wins over `disabled`.
114
+ */
115
+ disabledReason?: ReactNode;
116
+ /** Where the reason shows — {@link Select}'s `disabledReasonDisplay`: a line under the
117
+ * field at Large and on a touch screen by default, the tooltip otherwise. */
118
+ disabledReasonDisplay?: DisabledReasonDisplay;
119
+ /**
120
+ * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is
121
+ * locked the `disabledReason` way with the lock's reason (which wins over its own).
122
+ * No provider, or an unlocked one: no effect. {@link Select}'s `commit`. A currency
123
+ * field inside a form with its own Save stays editable under the lock — leave this off
124
+ * there, and put `commit` on the Save.
125
+ */
126
+ commit?: CommitScope;
93
127
  /** The TRIGGER — the focusable `<button>` — as a React 19 ref prop, as on
94
128
  * {@link MultiSelect} and {@link CountrySelect}: react-hook-form's focus-on-error
95
129
  * calls `focus()` on it (0.34, API-3, refactor audit 2026-10). Not the wrapper. */
@@ -109,6 +143,10 @@ export function CurrencySelect({
109
143
  help,
110
144
  labels,
111
145
  currencyNames,
146
+ disabled,
147
+ disabledReason,
148
+ disabledReasonDisplay,
149
+ commit,
112
150
  "aria-label": ariaLabel,
113
151
  "aria-describedby": ariaDescribedBy,
114
152
  "aria-invalid": ariaInvalid,
@@ -137,6 +175,15 @@ export function CurrencySelect({
137
175
 
138
176
  const selected = getCurrency(value);
139
177
 
178
+ // The lock — Select's (`useLockReason`): the WriteLock's reason under `commit`, else
179
+ // the caller's own. Locked or `disabled`, nothing opens the list or picks a value.
180
+ const lock = useLockReason(commit, disabledReason);
181
+ const locked = lock.locked;
182
+ const inert = locked || Boolean(disabled);
183
+ // A lock (or `disabled`) arriving while the list is up closes it — adjusted while
184
+ // rendering, as CountrySelect does, so no frame shows a list that cannot be used.
185
+ if (inert && open) setOpen(false);
186
+
140
187
  const listboxId = `${useId()}-listbox`;
141
188
  // Text is a caption under the field; anything else (a FieldHint) rides the label
142
189
  // line — and a FieldHint is that caption at Large and on touch (§4). See `hint`. On
@@ -152,6 +199,8 @@ export function CurrencySelect({
152
199
  invalid,
153
200
  describedBy: ariaDescribedBy,
154
201
  ariaInvalid,
202
+ // A lock's reason sits between the caption and the error, as on Select.
203
+ reasonId: locked && lock.reasonId,
155
204
  });
156
205
  const { labelHint, describedBy, isInvalid } = field;
157
206
 
@@ -169,10 +218,21 @@ export function CurrencySelect({
169
218
  ref={setTrigger}
170
219
  type="button"
171
220
  aria-label={name}
172
- onClick={() => setOpen((v) => !v)}
221
+ // The focusable kind of disabled while locked (Select's rule): native `disabled`
222
+ // would take the field out of the tab order, and the reason with it.
223
+ disabled={locked ? undefined : disabled}
224
+ aria-disabled={locked || undefined}
225
+ onClick={(e) => {
226
+ if (inert) {
227
+ e.preventDefault();
228
+ return;
229
+ }
230
+ setOpen((v) => !v);
231
+ }}
173
232
  // Down/Up opens the list from the closed trigger, per the APG; Enter and Space
174
233
  // already do through the button's own click.
175
234
  onKeyDown={(e) => {
235
+ if (inert) return;
176
236
  if (e.key === "ArrowDown" || e.key === "ArrowUp") {
177
237
  e.preventDefault();
178
238
  setOpen(true);
@@ -191,12 +251,27 @@ export function CurrencySelect({
191
251
  aria-expanded={open}
192
252
  aria-invalid={isInvalid || undefined}
193
253
  aria-describedby={describedBy}
194
- className={cn(FIELD_TRIGGER, "pe-9", label !== undefined && FIELD_FLOATING_PAD, isInvalid && FIELD_INVALID)}
254
+ className={cn(
255
+ FIELD_TRIGGER,
256
+ "pe-9",
257
+ label !== undefined && FIELD_FLOATING_PAD,
258
+ // FIELD_BASE's `disabled:` look, for a trigger locked without being `disabled`
259
+ // (the `:disabled` variants cannot match it) — Select's FIELD_LOCKED. No hover:
260
+ // nothing here answers a press.
261
+ locked && "cursor-not-allowed bg-surface-2 text-muted hover:bg-surface-2",
262
+ isInvalid && FIELD_INVALID,
263
+ )}
195
264
  >
196
265
  {/* The trigger stays COMPACT — flag + code only — so it never clips in a
197
266
  narrow field; the full names live in the (wider) popup (feedback
198
267
  #308). */}
199
- <span className={cn("flex items-center gap-2 min-w-0", selected ? "text-primary" : "text-placeholder")}>
268
+ <span
269
+ className={cn(
270
+ "flex items-center gap-2 min-w-0",
271
+ // The settled look reaches the code too: `text-primary` here would outweigh it.
272
+ !selected ? "text-placeholder" : inert ? "text-muted" : "text-primary",
273
+ )}
274
+ >
200
275
  {selected ? (
201
276
  <>
202
277
  <CurrencyFlag country={selected.country} />
@@ -206,10 +281,23 @@ export function CurrencySelect({
206
281
  placeholder ?? text.currency
207
282
  )}
208
283
  </span>
209
- <FieldChevron />
284
+ {/* No menu to drop while disabled or locked, so no chevron — the arrow is the one
285
+ thing on the control that promises a choice (dev#474, as on Select). */}
286
+ {!inert && <FieldChevron />}
210
287
  </button>
211
288
  );
212
289
 
290
+ // Select's frame: the line under the trigger, or the Tooltip with a hidden copy for
291
+ // `aria-describedby`. The wrapper `<div>` keeps `className`, so the layout holds.
292
+ // `flex w-full items-stretch`: the trigger keeps the field's full width in it.
293
+ const control = locked ? (
294
+ <LockedReason lock={lock} display={disabledReasonDisplay} className="flex w-full items-stretch">
295
+ {trigger}
296
+ </LockedReason>
297
+ ) : (
298
+ trigger
299
+ );
300
+
213
301
  return (
214
302
  // The wrapper takes `rest`; the trigger takes the name. This field's label is a
215
303
  // floating <span> rather than a <label for>, so a caller who wants the control to
@@ -230,7 +318,7 @@ export function CurrencySelect({
230
318
  ))}
231
319
  {/* No label line to ride: at the field's end edge, outside the box, as an
232
320
  unlabelled Select places its FieldHint. */}
233
- <EndHintRow hint={label === undefined ? labelHint : undefined}>{trigger}</EndHintRow>
321
+ <EndHintRow hint={label === undefined ? labelHint : undefined}>{control}</EndHintRow>
234
322
  {field.below}
235
323
  {/* Portalled to <body> and anchored to the trigger (dev#548), so where it stands
236
324
  in the flow no longer moves it. */}
@@ -246,6 +334,7 @@ export function CurrencySelect({
246
334
  // to say what it asks for (live #200).
247
335
  sheetTitle={label ?? text.currency}
248
336
  onChoose={(code) => {
337
+ if (inert) return;
249
338
  onChange(code);
250
339
  // Back to the trigger: the panel holding focus is about to unmount, and a
251
340
  // keyboard user who has answered the field should stand on it, ready to Tab on.
@@ -449,14 +449,14 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithRef<"div">, "
449
449
  tone?: "danger" | "warning";
450
450
  /**
451
451
  * The confirm button's variant, apart from `tone` (0.24, keksdose). Default: what
452
- * `tone` picks, as before — `"danger"` for `"danger"`, `"primary"` for `"warning"`.
452
+ * `tone` picks, as before — `"danger"` for `"danger"`, `"default"` for `"warning"`.
453
453
  *
454
454
  * `tone` says how bad the outcome is; this says how loud the last press is, and the two
455
455
  * need not agree. keksdose's `UserActionConfirm` (an admin resets somebody else's
456
456
  * password) asks an AMBER question — what it guards against is mis-targeting, not data
457
457
  * loss, so the danger colour stays its one `severe` consequence's — and ends in a RED
458
458
  * Go, as its hand-built panel did before it moved onto this tile in 0.23; with only
459
- * `tone`, the amber question made Go the neutral primary fill. `tone="warning"
459
+ * `tone`, the amber question made Go the neutral `default` fill. `tone="warning"
460
460
  * confirmVariant="danger"` is that panel again.
461
461
  */
462
462
  confirmVariant?: ButtonVariant;
@@ -914,7 +914,7 @@ export function DangerConfirm({
914
914
  <Button
915
915
  id={confirmId}
916
916
  type="submit"
917
- variant={confirmVariant ?? (tone === "warning" ? "primary" : "danger")}
917
+ variant={confirmVariant ?? (tone === "warning" ? "default" : "danger")}
918
918
  disabled={!canConfirm}
919
919
  // Held by a guard (the caller's or a built-in one), or armed and then locked
920
920
  // (or rendered armed under a lock): the confirm says why the way Button
@@ -15,7 +15,9 @@ import type { DataTableColumn } from "./data-table";
15
15
  // room for them (its need re-measured, plus the column's own header width from when
16
16
  // it was left out), so narrower content (a filter, another page) counts as much as a
17
17
  // wider scroller. The scroller's own width, through a ResizeObserver: a sidebar, a
18
- // narrower window, the Columns drawer and Large text all count the same.
18
+ // narrower window, the Columns drawer and Large text all count the same. A column
19
+ // whose return the very next measure undoes stays out until the table has more room
20
+ // for it than it had then (0.48.3, keksdose B2: see the bring-back in `measure`).
19
21
 
20
22
  /** The least a `grow` column keeps before the table starts leaving columns out: about
21
23
  * a sentence of a title. In rem, so Large text asks for more. */
@@ -94,6 +96,13 @@ export function useColumnFit<T>({
94
96
  // table's need as it is now, plus this, fits the scroller (keksdose S3, 0.48: not the
95
97
  // whole table's need at hide time, which went stale when the content narrowed).
96
98
  const ownWidth = useRef(new Map<string, number>());
99
+ // A column whose return the very next measure undid, at the same scroller width: the
100
+ // room the table had for it when it came back (keksdose B2, 0.48.3). It stays out
101
+ // until the table has more. Dropped once the column is no longer left out for width.
102
+ const stuck = useRef(new Map<string, number>());
103
+ // What the last measure brought back, each column with the room it had then, and at
104
+ // which scroller width: read (and cleared) by the measure right after it.
105
+ const lastShown = useRef<{ avail: number; rooms: Map<string, number> } | null>(null);
97
106
 
98
107
  const hasFit = columns.some((c) => c.fitPriority != null);
99
108
  const active = enabled && hasFit;
@@ -118,6 +127,10 @@ export function useColumnFit<T>({
118
127
 
119
128
  const measure = useCallback(() => {
120
129
  const { order, current, growKey, active, scroller } = latest.current;
130
+ // Only the measure right after a bring-back can undo it.
131
+ const flip = lastShown.current;
132
+ lastShown.current = null;
133
+ for (const k of stuck.current.keys()) if (!current.includes(k)) stuck.current.delete(k);
121
134
  const el = scroller();
122
135
  if (!active || !el) return;
123
136
  const table = el.querySelector("table");
@@ -150,20 +163,42 @@ export function useColumnFit<T>({
150
163
  need -= w;
151
164
  next.push(col.key);
152
165
  }
153
- if (next.length !== current.length) setHidden(next);
166
+ if (next.length === current.length) return;
167
+ // The last measure brought these back at this same width, and the table cannot
168
+ // hold them: their header width fell short of what they cost drawn (see below).
169
+ if (flip && Math.abs(flip.avail - avail) <= 0.5)
170
+ for (const k of next.slice(current.length)) {
171
+ const room = flip.rooms.get(k);
172
+ if (room != null) stuck.current.set(k, room);
173
+ }
174
+ setHidden(next);
154
175
  return;
155
176
  }
156
177
  // Bring back, the last left out first, while the table as it is now has room for
157
- // them. Should the column have grown since (wider content), the next measure leaves
158
- // it out again before paint and records its new width, so it cannot flicker.
178
+ // them by their recorded header width. A column that costs more drawn than that is
179
+ // left out again by the next measure, before paint. That alone does not settle it:
180
+ // the re-hide records its header's width again, and the header can measure the same
181
+ // while the column costs more (`virtualize`'s locked widths, keksdose B2, 0.48.3: the
182
+ // drawn column cost the table ~80–110px more than its header), so it came back, went, came back … until React's
183
+ // update limit (#185). So a column whose return the next measure undid, at the same
184
+ // scroller width, waits for more room than it had then: a wider scroller or narrower
185
+ // content. Whatever the error in its width, that is one invisible flip per room it is
186
+ // tried at.
159
187
  let n = current.length;
188
+ const rooms = new Map<string, number>();
160
189
  while (n > 0) {
161
- const w = ownWidth.current.get(current[n - 1]);
190
+ const k = current[n - 1];
191
+ const w = ownWidth.current.get(k);
162
192
  if (w == null || need + w > avail + 0.5) break;
193
+ const room = stuck.current.get(k);
194
+ if (room != null && avail - need <= room + 0.5) break;
195
+ rooms.set(k, avail - need);
163
196
  need += w;
164
197
  n--;
165
198
  }
166
- if (n !== current.length) setHidden(current.slice(0, n));
199
+ if (n === current.length) return;
200
+ lastShown.current = { avail, rooms };
201
+ setHidden(current.slice(0, n));
167
202
  }, [headRefs]);
168
203
 
169
204
  // After a render that changed what is drawn: rows, a filter or a page can widen a