@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
@@ -1,23 +1,45 @@
1
1
  // Chart colour system. Single source of truth lives in the design tokens
2
2
  // (:root / .dark in tokens.css): brand = indigo, money = teal/amber/purple,
3
- // chart-1..9 = Paul Tol "Muted" (CVD-safe across protan/deutan/tritan).
3
+ // chart-1..8 = anyplot's "Imprint" hues (https://anyplot.ai/palette, 0.49 — Paul Tol
4
+ // "Muted" before, still selectable as the `paul-tol` preset).
4
5
  // Returning CSS vars here makes every Recharts series theme-aware for free — the
5
6
  // SVG fill/stroke resolves --chart-N against the current light/dark token set.
6
7
 
7
8
  import { contrast, parseHex as parseRgbHex } from "./color";
8
9
 
9
- const PALETTE_SIZE = 9;
10
+ /** How many categorical slots `paletteFor` cycles through: anyplot's eight hues. Amber,
11
+ * anyplot's ninth colour, is a semantic warning anchor and not a series slot. A preset
12
+ * that carries more hues (Paul Tol's nine) still writes them as `--chart-N`, but the
13
+ * by-index ramp wraps after eight, so the same series keeps the same colour whichever
14
+ * preset is active. */
15
+ export const PALETTE_SIZE = 8;
16
+
17
+ /** anyplot's colour-vision-deficiency-optimal order over the eight slots — the order
18
+ * that keeps the FIRST n series furthest apart under protan/deutan/tritan simulation.
19
+ * An option (`paletteFor(i, { order: "cvd" })`), not the default: the default order is
20
+ * anyplot's own slot order, so series 1 is always the green. */
21
+ export const CVD_ORDER = [0, 1, 7, 6, 4, 5, 2, 3] as const;
22
+
23
+ export interface PaletteForOptions {
24
+ /** `"slot"` (default): anyplot's slot order. `"cvd"`: {@link CVD_ORDER}. */
25
+ order?: "slot" | "cvd";
26
+ }
10
27
 
11
28
  /** CVD-safe categorical colour for series `index`, as a theme-aware CSS var.
29
+ *
30
+ * The first series is always slot 0 (`--chart-1`, anyplot's green), and the ramp
31
+ * wraps after {@link PALETTE_SIZE}.
12
32
  *
13
33
  * Wraps in both directions, the way `strokeDash` does: JS `%` keeps the sign,
14
- * so `-1 % 9` is `-1` and the plain formula named `--chart-0`, a token that does not
34
+ * so `-1 % 8` is `-1` and the plain formula named `--chart-0`, a token that does not
15
35
  * exist — a series with no stroke at all. A fractional index is truncated, and one
16
36
  * that is not a number (NaN, ±Infinity — an `indexOf` miss divided by something)
17
37
  * takes the first colour rather than `var(--chart-NaN)`. */
18
- export function paletteFor(index: number): string {
38
+ export function paletteFor(index: number, options?: PaletteForOptions): string {
19
39
  const i = Number.isFinite(index) ? Math.trunc(index) : 0;
20
- return `var(--chart-${(((i % PALETTE_SIZE) + PALETTE_SIZE) % PALETTE_SIZE) + 1})`;
40
+ const position = ((i % PALETTE_SIZE) + PALETTE_SIZE) % PALETTE_SIZE;
41
+ const slot = options?.order === "cvd" ? CVD_ORDER[position] : position;
42
+ return `var(--chart-${slot + 1})`;
21
43
  }
22
44
 
23
45
  /** Semantic money colours, theme-aware via tokens. Pair with +/- and arrow
@@ -26,10 +48,65 @@ export const CHART_COLORS = {
26
48
  income: "var(--money-income)",
27
49
  expense: "var(--money-expense)",
28
50
  net: "var(--money-net)",
51
+ neutral: "var(--money-neutral)",
29
52
  assigned: "var(--brand)",
30
53
  activity: "var(--money-expense)",
54
+ /** A generic total line — not money-coloured, the chart's neutral ink. */
55
+ total: "var(--chart-ink)",
56
+ } as const;
57
+
58
+ /**
59
+ * What a chart draws that is NOT a series (anyplot's roles, 0.49). Two inks, which are
60
+ * the only chart colours that change between light and dark — the eight hues do not:
61
+ *
62
+ * - `other` / `rest` / `disabled` and confidence-`band` fills → the MUTED ink. A
63
+ * treemap's "rest" bucket or a pie's "Other" slice takes this instead of wrapping
64
+ * round to a hue the reader will match to a real series.
65
+ * - `total` / `baseline` / `outline` → the neutral INK.
66
+ */
67
+ export const CHART_ROLES = {
68
+ other: "var(--chart-muted)",
69
+ rest: "var(--chart-muted)",
70
+ disabled: "var(--chart-muted)",
71
+ band: "var(--chart-muted)",
72
+ total: "var(--chart-ink)",
73
+ baseline: "var(--chart-ink)",
74
+ outline: "var(--chart-ink)",
31
75
  } as const;
32
76
 
77
+ /** `--chart-ink` per theme, as hex (tokens.css holds the same two). */
78
+ export const CHART_INK_HEX = { light: "#1a1a17", dark: "#f0efe8" } as const;
79
+ /** `--chart-muted` per theme, as hex (tokens.css holds the same two). */
80
+ export const CHART_MUTED_HEX = { light: "#6b6a63", dark: "#a8a79f" } as const;
81
+
82
+ /** The 3:1 WCAG 1.4.11 asks of a filled graphical object against its surface. */
83
+ export const EDGE_MIN_CONTRAST = 3;
84
+
85
+ /**
86
+ * The 1px edge a FILLED mark (a bar, an area, a treemap tile) needs, or `undefined`.
87
+ *
88
+ * anyplot's answer to hues that sit under 3:1 on the ground (cyan, lime, lavender, ochre
89
+ * and green on cream; matte red and rose on the dark ground): outline the shape in ink,
90
+ * so its boundary is legible while the fill keeps the hue that identifies the series.
91
+ * Only those — a hue that clears 3:1 on every surface given stays unstroked, because an
92
+ * outline on every bar is noise.
93
+ *
94
+ * `fill` must be a measurable `#rgb`/`#rrggbb`; `surfaces` are the grounds the mark can
95
+ * sit on (the card and the page), and the WORST of them decides. Anything it cannot
96
+ * measure gets no edge rather than a guessed one.
97
+ */
98
+ export function chartEdge(
99
+ fill: string,
100
+ surfaces: readonly string[],
101
+ ink: string = CHART_ROLES.outline,
102
+ ): string | undefined {
103
+ if (!parseRgbHex(fill)) return undefined;
104
+ const measurable = surfaces.filter((s) => parseRgbHex(s));
105
+ if (measurable.length === 0) return undefined;
106
+ const worst = Math.min(...measurable.map((s) => contrast(fill, s)));
107
+ return worst < EDGE_MIN_CONTRAST ? ink : undefined;
108
+ }
109
+
33
110
  // ── Raw hex stops, by theme ────────────────────────────────────────────────
34
111
  // The heatmaps interpolate colours in JS (CSS vars can't be lerped), so they
35
112
  // need concrete hex per theme — i.e. the same colours DEFAULT_PRESET already
@@ -42,9 +119,14 @@ export const CHART_COLORS = {
42
119
  // DEFAULT_PRESET" — which is not optional: they HAD drifted, five of the six light
43
120
  // heat stops, and the light `neutral` was still Tailwind's slate-100, a cool grey on
44
121
  // the warm cream page this theme has used since #328.
122
+ //
123
+ // 0.49: the same eight hues in both themes — anyplot does not re-tune a hue for the dark
124
+ // ground, and neither does the kit any more; `chartEdge` outlines the two that fall
125
+ // under 3:1 there (matte red, rose) instead of lifting them into different colours.
126
+ const IMPRINT_HEX = ["#009e73", "#c475fd", "#4467a3", "#bd8233", "#ae3030", "#2abccd", "#954477", "#99b314"] as const;
45
127
  export const PALETTE_HEX = {
46
- light: ["#332288", "#88ccee", "#44aa99", "#117733", "#999933", "#ddcc77", "#cc6677", "#882255", "#aa4499"],
47
- dark: ["#7e72d6", "#9fd8f2", "#5fc4b0", "#3fa45f", "#bfbf5e", "#e8dda0", "#e08c9a", "#c25a78", "#cc78be"],
128
+ light: IMPRINT_HEX,
129
+ dark: IMPRINT_HEX,
48
130
  } as const;
49
131
 
50
132
  /** A heatmap's interpolation stops for one theme. Open string fields so any
@@ -59,24 +141,24 @@ export interface HeatStops {
59
141
  }
60
142
 
61
143
  /** Diverging (variance: under←0→over) and sequential (spending intensity)
62
- heatmap stops, per theme. Teal = under/good, amber = over/spend = bad. */
144
+ heatmap stops, per theme — anyplot's scales (0.49). Sequential runs anyplot green →
145
+ blue; diverging runs blue (under) ↔ paper/ink (0, the midpoint) ↔ matte red (over).
146
+ The hues are the same in both themes; only the midpoint flips with the ground. */
63
147
  export const HEATMAP_HEX = {
64
148
  light: {
65
- neutral: "#f1e7d2", // the cream surface itself, not a cool grey
66
- under: "#0b6650", // DEFAULT_PRESET.light.moneyIncome
67
- over: "#9c6418", // DEFAULT_PRESET.light.moneyExpense, warmed to the page
68
- seqLow: "#f6ecd0",
69
- seqHigh: "#9c6418",
149
+ neutral: "#faf8f1", // anyplot's light paper — the diverging scale's 0
150
+ under: "#4467a3",
151
+ over: "#ae3030",
152
+ seqLow: "#009e73",
153
+ seqHigh: "#4467a3",
70
154
  empty: "rgba(148,163,184,0.14)",
71
155
  },
72
156
  dark: {
73
- neutral: "#363a5a", // one step above --border on the indigo-noir surface
74
- under: "#22c3b6", // DEFAULT_PRESET.dark.moneyIncome
75
- // Brighter than dark.moneyExpense (#e4b035) on purpose: a gradient endpoint is
76
- // meant to be the loudest thing in its scale, and it is a fill, not a figure.
77
- over: "#fbbf24",
78
- seqLow: "#3a2c14", // dim amber, sits just above the page
79
- seqHigh: "#fbbf24",
157
+ neutral: "#1a1a17", // anyplot's ink — the diverging scale's 0 on the dark ground
158
+ under: "#4467a3",
159
+ over: "#ae3030",
160
+ seqLow: "#009e73",
161
+ seqHigh: "#4467a3",
80
162
  empty: "rgba(148,163,184,0.10)",
81
163
  },
82
164
  } as const;
@@ -1,7 +1,8 @@
1
1
  // Appearance presets — the single source of truth for the "palette switcher"
2
2
  // (feedback #307). Each preset is a full set of design-token values for BOTH
3
3
  // light and dark, covering the page/surface backgrounds, the brand accent, the
4
- // semantic money trio and the 9-colour categorical chart palette, plus the
4
+ // semantic money trio and the categorical chart palette (anyplot's eight "Imprint"
5
+ // hues by default since 0.49, Paul Tol's nine in `PAUL_TOL_PRESET`), plus the
5
6
  // heatmap stops. A preset is applied at runtime by writing these as CSS custom
6
7
  // properties onto <html> (see palette-store.ts), which instantly re-skins every
7
8
  // token-driven surface and chart. The two heatmaps and the treemap interpolate
@@ -40,7 +41,9 @@ export interface TokenSet {
40
41
  moneyExpense: string;
41
42
  moneyNet: string;
42
43
  moneyNeutral: string;
43
- // Categorical chart series (Paul Tol "Muted" by default — CVD-safe), exactly 9
44
+ // Categorical chart series, written as `--chart-1…9`. anyplot's eight "Imprint" hues by
45
+ // default (0.49), Paul Tol "Muted"'s nine in `PAUL_TOL_PRESET`. `paletteFor` cycles
46
+ // eight; a shorter ramp is repeated to fill `--chart-9` (see `applyTokenSet`).
44
47
  chart: string[];
45
48
  // Heatmap interpolation stops (variance diverging + spending sequential)
46
49
  heat: HeatStops;
@@ -54,6 +57,59 @@ export interface PalettePreset {
54
57
  dark: TokenSet;
55
58
  }
56
59
 
60
+ // ── "Imprint" chart hues (feedback #119; the default since 0.49) ─────────────
61
+ // anyplot's categorical palette (https://anyplot.ai/palette), by a colleague of
62
+ // Marcel's. 8 hues + an amber semantic anchor, derived from Wong 2011 by a
63
+ // Petroff-style max-min ΔE search in CAM02-UCS with Machado 2009 CVD simulation —
64
+ // the same lineage as Paul Tol "Muted", one generation on, and tuned for exactly the
65
+ // warm cream paper our light theme uses.
66
+ //
67
+ // 0.49 (Marcel, 2026-10-22): the DEFAULT, in anyplot's slot order, eight slots — amber
68
+ // #DDCC77 left the pool, it is anyplot's semantic warning anchor, not a series colour.
69
+ // The SAME hues in both themes: 0.48 lifted matte red and rose for the dark ground
70
+ // (#BF413E, #A25083), which made the dark chart a different palette from the light one.
71
+ // anyplot answers a hue under 3:1 with a 1px ink edge instead, and so does the kit now
72
+ // (`chartEdge`, drawn by SeriesChart's bars and areas and by Treemap's tiles).
73
+ //
74
+ // Measured on our surfaces (CIEDE2000, Machado severity-100, 8 like-for-like slots on
75
+ // the cream light surface #f6ecd9) — neither palette dominates:
76
+ // Paul Tol Muted min-contrast 1.38 · worst-CVD ΔE00 11.8 · normal ΔE00 16.1
77
+ // imprint min-contrast 1.95 · worst-CVD ΔE00 7.2 · normal ΔE00 22.2
78
+ // i.e. imprint is punchier and has no near-invisible pale slot (Tol's #88ccee sits at
79
+ // 1.5:1 on cream), while Tol separates better under colour-vision deficiency — which is
80
+ // why `paletteFor` offers anyplot's CVD-optimal order (`{ order: "cvd" }`).
81
+ const IMPRINT_HUES = [
82
+ "#009e73", // brand green — first series, always
83
+ "#c475fd", // lavender — creative
84
+ "#4467a3", // blue — cool / info
85
+ "#bd8233", // ochre — earth / commodity
86
+ "#ae3030", // matte red — bad / loss / error
87
+ "#2abccd", // cyan — sky / tech-cool
88
+ "#954477", // rose — wellness / health
89
+ "#99b314", // lime — growth / nature
90
+ ];
91
+
92
+ /** anyplot's scales (0.49): sequential green → blue, diverging blue (under) ↔ paper or
93
+ * ink (0) ↔ matte red (over). `chart-palette.ts`'s `HEATMAP_HEX` holds the same. */
94
+ const ANYPLOT_HEAT: Record<"light" | "dark", HeatStops> = {
95
+ light: {
96
+ neutral: "#faf8f1",
97
+ under: "#4467a3",
98
+ over: "#ae3030",
99
+ seqLow: "#009e73",
100
+ seqHigh: "#4467a3",
101
+ empty: "rgba(148,163,184,0.14)",
102
+ },
103
+ dark: {
104
+ neutral: "#1a1a17",
105
+ under: "#4467a3",
106
+ over: "#ae3030",
107
+ seqLow: "#009e73",
108
+ seqHigh: "#4467a3",
109
+ empty: "rgba(148,163,184,0.10)",
110
+ },
111
+ };
112
+
57
113
  // The shipped default (feedback #328): SOLAR-DUSK warm light + INDIGO-NOIR deep
58
114
  // dark surfaces — the two curated presets the user picked per mode — but with a
59
115
  // SINGLE unified brand accent (indigo) across both modes rather than each
@@ -62,10 +118,15 @@ export interface PalettePreset {
62
118
  // the dark block is Indigo Noir (already indigo). The money trio
63
119
  // (income/expense/net) drives the chart bars and stays palette-driven so those
64
120
  // semantic colours adapt to each mode's contrast (best practice — feedback #328).
121
+ //
122
+ // 0.49: its chart ramp and heat stops are anyplot's ("Imprint", above) — the money
123
+ // trio stays the kit's own (Marcel, 2026-10-22: chart income/expense/net/neutral keep
124
+ // the money tokens, never anyplot's green/red). The 0.48 default, the same surfaces
125
+ // with Paul Tol's ramp, is `PAUL_TOL_PRESET`.
65
126
  export const DEFAULT_PRESET: PalettePreset = {
66
127
  id: "default",
67
- name: "Default",
68
- blurb: "Solar-dusk light · indigo-noir dark",
128
+ name: "Imprint",
129
+ blurb: "anyplot.ai chart hues · solar-dusk light · indigo-noir dark",
69
130
  light: {
70
131
  // Solar Dusk (light) surfaces, with the brand unified to indigo (see above).
71
132
  bgPage: "#ede3cf",
@@ -84,15 +145,8 @@ export const DEFAULT_PRESET: PalettePreset = {
84
145
  moneyExpense: "#905902",
85
146
  moneyNet: "#7a4fa3",
86
147
  moneyNeutral: "#6b6453",
87
- chart: ["#332288", "#88ccee", "#44aa99", "#117733", "#999933", "#ddcc77", "#cc6677", "#882255", "#aa4499"],
88
- heat: {
89
- neutral: "#f1e7d2",
90
- under: "#0b6650",
91
- over: "#9c6418",
92
- seqLow: "#f6ecd0",
93
- seqHigh: "#9c6418",
94
- empty: "rgba(148,163,184,0.14)",
95
- },
148
+ chart: IMPRINT_HUES,
149
+ heat: ANYPLOT_HEAT.light,
96
150
  },
97
151
  dark: {
98
152
  // Indigo Noir (dark)
@@ -115,6 +169,36 @@ export const DEFAULT_PRESET: PalettePreset = {
115
169
  moneyExpense: "#e4b035",
116
170
  moneyNet: "#b39aef",
117
171
  moneyNeutral: "#9aa1bd",
172
+ chart: IMPRINT_HUES,
173
+ heat: ANYPLOT_HEAT.dark,
174
+ },
175
+ };
176
+
177
+
178
+ /**
179
+ * The 0.48 default: the same surfaces, brand and money, with Paul Tol's "Muted" nine
180
+ * as the chart ramp and the teal/amber heat stops that went with it. Selectable, no
181
+ * longer the default (0.49). `paletteFor` cycles eight, so its ninth hue (#aa4499) is
182
+ * reachable only as `var(--chart-9)`.
183
+ */
184
+ export const PAUL_TOL_PRESET: PalettePreset = {
185
+ id: "paul-tol",
186
+ name: "Paul Tol muted",
187
+ blurb: "Paul Tol's CVD-safe chart hues · solar-dusk light · indigo-noir dark",
188
+ light: {
189
+ ...DEFAULT_PRESET.light,
190
+ chart: ["#332288", "#88ccee", "#44aa99", "#117733", "#999933", "#ddcc77", "#cc6677", "#882255", "#aa4499"],
191
+ heat: {
192
+ neutral: "#f1e7d2",
193
+ under: "#0b6650",
194
+ over: "#9c6418",
195
+ seqLow: "#f6ecd0",
196
+ seqHigh: "#9c6418",
197
+ empty: "rgba(148,163,184,0.14)",
198
+ },
199
+ },
200
+ dark: {
201
+ ...DEFAULT_PRESET.dark,
118
202
  chart: ["#7e72d6", "#9fd8f2", "#5fc4b0", "#3fa45f", "#bfbf5e", "#e8dda0", "#e08c9a", "#c25a78", "#cc78be"],
119
203
  heat: {
120
204
  neutral: "#363a5a",
@@ -127,73 +211,20 @@ export const DEFAULT_PRESET: PalettePreset = {
127
211
  },
128
212
  };
129
213
 
130
- // ── "Imprint" chart hues (feedback #119) ──────────────────────────────────────
131
- // anyplot's categorical palette (https://anyplot.ai/palette), by a colleague of
132
- // Marcel's. 8 hues + an amber semantic anchor, derived from Wong 2011 by a
133
- // Petroff-style max-min ΔE search in CAM02-UCS with Machado 2009 CVD simulation —
134
- // the same lineage as our default Paul Tol "Muted", one generation on, and tuned
135
- // for exactly the warm cream paper our light theme uses.
136
- //
137
- // This preset is DEFAULT_PRESET with ONLY the 9 categorical chart hues swapped;
138
- // surfaces, brand and the semantic money trio stay put, because those were their
139
- // own deliberate decisions (#307/#328) and are not what the palette is for. The
140
- // heatmap stops likewise stay teal/amber: they encode under/over, and imprint's
141
- // own diverging ramp is red↔blue, which would fight the money semantics.
142
- //
143
- // Measured on our surfaces (CIEDE2000, Machado severity-100, 8 like-for-like
144
- // slots on the cream light surface #f6ecd9) — neither palette dominates:
145
- // Paul Tol Muted min-contrast 1.38 · worst-CVD ΔE00 11.8 · normal ΔE00 16.1
146
- // imprint min-contrast 1.95 · worst-CVD ΔE00 7.2 · normal ΔE00 22.2
147
- // i.e. imprint is punchier and has no near-invisible pale slot (Tol's #88ccee
148
- // sits at 1.5:1 on cream), while Tol separates better under colour-vision
149
- // deficiency. So this ships as a CHOICE, not as a new default.
150
- //
151
- // The dark set keeps imprint's hues verbatim except matte-red and rose, whose
152
- // lightness is lifted (oklch L 0.50→0.55) because verbatim they land at 2.67:1
153
- // and 2.76:1 on our deep-indigo dark surface — under WCAG 2.1 SC 1.4.11's 3:1
154
- // non-text minimum. Lifted they clear it at 3.30:1 / 3.28:1. anyplot documents
155
- // the same shortfall on their own dark ground and answers it with a stroke halo;
156
- // lifting keeps the fix in the token layer instead of every chart component.
157
- const IMPRINT_HUES_LIGHT = [
158
- "#009E73", // brand green — first series
159
- "#C475FD", // lavender — creative
160
- "#4467A3", // blue — cool / info
161
- "#BD8233", // ochre — earth / commodity
162
- "#AE3030", // matte red — bad / loss / error
163
- "#2ABCCD", // cyan — sky / tech-cool
164
- "#954477", // rose — wellness / health
165
- "#99B314", // lime — growth / nature
166
- "#DDCC77", // amber — anyplot's semantic anchor, used here as slot 9
167
- ];
168
- const IMPRINT_HUES_DARK = [
169
- "#009E73",
170
- "#C475FD",
171
- "#4467A3",
172
- "#BD8233",
173
- "#BF413E", // matte red, L-lifted for our dark surface
174
- "#2ABCCD",
175
- "#A25083", // rose, L-lifted for our dark surface
176
- "#99B314",
177
- "#DDCC77",
178
- ];
179
-
180
- export const IMPRINT_PRESET: PalettePreset = {
181
- id: "imprint",
182
- name: "Imprint",
183
- blurb: "Colourblind-safe chart hues by anyplot.ai",
184
- light: { ...DEFAULT_PRESET.light, chart: IMPRINT_HUES_LIGHT },
185
- dark: { ...DEFAULT_PRESET.dark, chart: IMPRINT_HUES_DARK },
186
- };
214
+ /** @deprecated 0.49 — use `DEFAULT_PRESET` (Imprint IS the default now); removed in 0.51.
215
+ * A persisted `"imprint"` id resolves to it through `presetById`'s fallback. */
216
+ export const IMPRINT_PRESET: PalettePreset = DEFAULT_PRESET;
187
217
 
188
218
  // The full-theme switcher from #307 stayed retired (feedback #328) — the default
189
219
  // is still the single source of truth for surfaces, brand and money (its five
190
220
  // alternatives are gone, see the header). What IS selectable is
191
- // the categorical chart ramp (feedback #119), so these two presets are the live
192
- // set; presetById falls back to the default for any stale persisted id.
221
+ // the categorical chart ramp (feedback #119), so Imprint (the default) and Paul Tol are
222
+ // the live set; presetById falls back to the default for any stale persisted id.
193
223
  /**
194
224
  * The categorical chart ramp, shared by every derived preset below.
195
225
  *
196
- * Paul Tol's "Muted" set, unchanged, and deliberately NOT re-derived per preset. A
226
+ * The default's ramp (anyplot's eight since 0.49, Paul Tol's "Muted" before), and
227
+ * deliberately NOT re-derived per preset. A
197
228
  * categorical ramp solves a different problem from a UI palette — separability among
198
229
  * nine unknown siblings, rather than contrast against one known surface — and a set
199
230
  * optimised against real dichromat confusion lines beats one spaced evenly by formula.
@@ -205,7 +236,7 @@ export const IMPRINT_PRESET: PalettePreset = {
205
236
  * SINGLE_LIGHTNESS_MAX_SERIES — so a chart with more than four series must carry a
206
237
  * second channel (a direct label, a pattern, a legend adjacent to the mark).
207
238
  */
208
- const PAUL_TOL = {
239
+ const KIT_RAMP = {
209
240
  light: DEFAULT_PRESET.light.chart,
210
241
  dark: DEFAULT_PRESET.dark.chart,
211
242
  } as const;
@@ -249,7 +280,7 @@ export const DERIVED_PRESETS: PalettePreset[] = [
249
280
  moneyExpense: "#8e5900",
250
281
  moneyNet: "#7356a2",
251
282
  moneyNeutral: "#61646c",
252
- chart: PAUL_TOL.light,
283
+ chart: KIT_RAMP.light,
253
284
  heat: {
254
285
  neutral: "#dce2ef",
255
286
  under: "#00725f",
@@ -274,7 +305,7 @@ export const DERIVED_PRESETS: PalettePreset[] = [
274
305
  moneyExpense: "#b77b29",
275
306
  moneyNet: "#9679c9",
276
307
  moneyNeutral: "#a1a5ac",
277
- chart: PAUL_TOL.dark,
308
+ chart: KIT_RAMP.dark,
278
309
  heat: {
279
310
  neutral: "#1d212b",
280
311
  under: "#009a80",
@@ -304,7 +335,7 @@ export const DERIVED_PRESETS: PalettePreset[] = [
304
335
  moneyExpense: "#8e5900",
305
336
  moneyNet: "#7356a2",
306
337
  moneyNeutral: "#5f6662",
307
- chart: PAUL_TOL.light,
338
+ chart: KIT_RAMP.light,
308
339
  heat: {
309
340
  neutral: "#d8e6dd",
310
341
  under: "#00725f",
@@ -329,7 +360,7 @@ export const DERIVED_PRESETS: PalettePreset[] = [
329
360
  moneyExpense: "#b87c2a",
330
361
  moneyNet: "#977ac9",
331
362
  moneyNeutral: "#9fa7a2",
332
- chart: PAUL_TOL.dark,
363
+ chart: KIT_RAMP.dark,
333
364
  heat: {
334
365
  neutral: "#1a241e",
335
366
  under: "#009a80",
@@ -359,7 +390,7 @@ export const DERIVED_PRESETS: PalettePreset[] = [
359
390
  moneyExpense: "#8d5800",
360
391
  moneyNet: "#7355a2",
361
392
  moneyNeutral: "#676269",
362
- chart: PAUL_TOL.light,
393
+ chart: KIT_RAMP.light,
363
394
  heat: {
364
395
  neutral: "#e7dfea",
365
396
  under: "#00715e",
@@ -384,7 +415,7 @@ export const DERIVED_PRESETS: PalettePreset[] = [
384
415
  moneyExpense: "#b77b29",
385
416
  moneyNet: "#9679c9",
386
417
  moneyNeutral: "#a8a2aa",
387
- chart: PAUL_TOL.dark,
418
+ chart: KIT_RAMP.dark,
388
419
  heat: {
389
420
  neutral: "#251f27",
390
421
  under: "#009980",
@@ -414,7 +445,7 @@ export const DERIVED_PRESETS: PalettePreset[] = [
414
445
  moneyExpense: "#8d5800",
415
446
  moneyNet: "#7255a1",
416
447
  moneyNeutral: "#616467",
417
- chart: PAUL_TOL.light,
448
+ chart: KIT_RAMP.light,
418
449
  heat: {
419
450
  neutral: "#e1e2e3",
420
451
  under: "#00715e",
@@ -439,7 +470,7 @@ export const DERIVED_PRESETS: PalettePreset[] = [
439
470
  moneyExpense: "#b77b29",
440
471
  moneyNet: "#9679c9",
441
472
  moneyNeutral: "#a2a5a8",
442
- chart: PAUL_TOL.dark,
473
+ chart: KIT_RAMP.dark,
443
474
  heat: {
444
475
  neutral: "#212122",
445
476
  under: "#009a80",
@@ -452,7 +483,7 @@ export const DERIVED_PRESETS: PalettePreset[] = [
452
483
  },
453
484
  ];
454
485
 
455
- export const PALETTES: PalettePreset[] = [DEFAULT_PRESET, IMPRINT_PRESET, ...DERIVED_PRESETS];
486
+ export const PALETTES: PalettePreset[] = [DEFAULT_PRESET, PAUL_TOL_PRESET, ...DERIVED_PRESETS];
456
487
 
457
488
  export function presetById(id: string): PalettePreset {
458
489
  return PALETTES.find((p) => p.id === id) ?? DEFAULT_PRESET;
@@ -508,5 +539,11 @@ export function applyTokenSet(
508
539
  el.style.setProperty("--money-expense", step?.well?.moneyExpense ?? t.moneyExpense);
509
540
  el.style.setProperty("--money-net", step?.well?.moneyNet ?? t.moneyNet);
510
541
  el.style.setProperty("--money-neutral", step?.well?.moneyNeutral ?? t.moneyNeutral);
511
- t.chart.forEach((c, i) => el.style.setProperty(`--chart-${i + 1}`, c));
542
+ // All nine `--chart-N`, a shorter ramp repeated: Imprint's eight write `--chart-9` as
543
+ // slot 0 — what `paletteFor(8)` names — so a host addressing `var(--chart-9)` directly
544
+ // follows the cycle, and switching away from Paul Tol's nine leaves no stale ninth hue.
545
+ // An empty ramp (a `derivePalette` set) leaves whatever was there, deliberately.
546
+ if (t.chart.length > 0) {
547
+ for (let i = 0; i < 9; i++) el.style.setProperty(`--chart-${i + 1}`, t.chart[i % t.chart.length]);
548
+ }
512
549
  }
@@ -0,0 +1,132 @@
1
+ import type { CSSProperties } from "react";
2
+ import { readKeyboardInset } from "../hooks/use-anchored-panel";
3
+
4
+ /*
5
+ * # The docked card (docs/tours-harmonization.md §11.7, contract round 054 §6)
6
+ *
7
+ * On a phone a field step's card is pinned full-width to the top or bottom edge of the
8
+ * USABLE BAND — the visual viewport less the keyboard, raised above the highest
9
+ * `[data-tour-obstruction]` (the number pad, a picker sheet) — on the edge away from the
10
+ * spotlighted field, so it covers neither the field nor the keyboard nor the pad.
11
+ */
12
+
13
+ export type DockEdge = "top" | "bottom";
14
+
15
+ export interface DockRect {
16
+ top: number;
17
+ left: number;
18
+ width: number;
19
+ height: number;
20
+ }
21
+
22
+ /** The band, in layout-viewport px (what `position: fixed` measures from). */
23
+ export interface Band {
24
+ top: number;
25
+ bottom: number;
26
+ }
27
+
28
+ /** Below this the docked layout applies (Tailwind's `sm`). */
29
+ export const DOCK_MAX_WIDTH = 640;
30
+ /** The gutter between the card and the band's edge, and between the card and the field. */
31
+ const GUTTER = 8;
32
+ /** An obstruction whose top is within this of the band's top covers the screen. */
33
+ const FULL_SCREEN_SLACK = 64;
34
+ /** Under this much free space on its edge the card flips (the hysteresis floor). */
35
+ const MIN_DOCK = 140;
36
+
37
+ /**
38
+ * The usable band now: the visual viewport while a keyboard is up (`readKeyboardInset`'s
39
+ * rules — pinch zoom is not a keyboard, 100 px minimum), else the layout viewport; its
40
+ * bottom raised to the top of the highest visible obstruction that holds neither the field
41
+ * nor the card. An obstruction that starts at the band's top (a full-screen picker, opened
42
+ * over everything) is not a bottom layer and is ignored; a tall one (the number pad at
43
+ * Large text is over half a phone) still counts.
44
+ */
45
+ export function readBand(target: Element | null, card: Element | null): Band {
46
+ const vh = window.innerHeight;
47
+ let top = 0;
48
+ let bottom = vh;
49
+ const vv = window.visualViewport;
50
+ if (vv && readKeyboardInset() > 0) {
51
+ top = vv.offsetTop;
52
+ bottom = vv.offsetTop + vv.height;
53
+ }
54
+ for (const el of Array.from(document.querySelectorAll("[data-tour-obstruction]"))) {
55
+ if ((target && el.contains(target)) || (card && el.contains(card))) continue;
56
+ const r = el.getBoundingClientRect();
57
+ if (r.width <= 0 || r.height <= 0 || r.top <= top + FULL_SCREEN_SLACK) continue;
58
+ if (r.top < bottom) bottom = r.top;
59
+ }
60
+ return { top, bottom };
61
+ }
62
+
63
+ /** The card's free space on `edge`, between the band's edge and the spotlight. */
64
+ function room(edge: DockEdge, spot: DockRect, band: Band): number {
65
+ return edge === "bottom" ? band.bottom - (spot.top + spot.height) - GUTTER * 2 : spot.top - band.top - GUTTER * 2;
66
+ }
67
+
68
+ /**
69
+ * The edge away from the field: the bottom when the spotlight's centre is in the band's
70
+ * upper half, else the top. With a `previous` edge, it stays unless the card would have to
71
+ * cover the spotlight there (no flip on every keyboard animation frame).
72
+ */
73
+ export function dockEdge(spot: DockRect, band: Band, previous: DockEdge | null, cardHeight: number): DockEdge {
74
+ const preferred: DockEdge = spot.top + spot.height / 2 < band.top + (band.bottom - band.top) / 2 ? "bottom" : "top";
75
+ if (!previous || previous === preferred) return preferred;
76
+ return room(previous, spot, band) >= Math.min(MIN_DOCK, cardHeight) ? previous : preferred;
77
+ }
78
+
79
+ /** The docked card's position: full-width with 8 px (safe-area aware) gutters, its height
80
+ * capped to the free space so its body scrolls and its title and buttons show. */
81
+ export function dockStyle(edge: DockEdge, spot: DockRect | null, band: Band): CSSProperties {
82
+ const vh = window.innerHeight;
83
+ const cap = spot ? Math.max(MIN_DOCK / 2, room(edge, spot, band)) : band.bottom - band.top - GUTTER * 2;
84
+ const base: CSSProperties = {
85
+ left: `max(${GUTTER}px, env(safe-area-inset-left))`,
86
+ right: `max(${GUTTER}px, env(safe-area-inset-right))`,
87
+ width: "auto",
88
+ maxWidth: "none",
89
+ maxHeight: cap,
90
+ };
91
+ if (edge === "top") {
92
+ return { ...base, top: band.top <= 0 ? `max(${GUTTER}px, env(safe-area-inset-top))` : band.top + GUTTER, bottom: "auto" };
93
+ }
94
+ const fromBottom = vh - band.bottom;
95
+ return {
96
+ ...base,
97
+ top: "auto",
98
+ bottom: fromBottom <= 0 ? `max(${GUTTER}px, env(safe-area-inset-bottom))` : fromBottom + GUTTER,
99
+ };
100
+ }
101
+
102
+ /** The target's nearest ancestor that scrolls vertically now (a sheet's body), or null. */
103
+ export function scrollParent(el: Element): HTMLElement | null {
104
+ for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
105
+ const { overflowY } = getComputedStyle(node);
106
+ if ((overflowY === "auto" || overflowY === "scroll") && node.scrollHeight > node.clientHeight) return node;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * Scroll the field into the half of the band away from the card, through its nearest
113
+ * scrollable ancestor (the window only when there is none — the page behind a sheet never
114
+ * moves). A field already inside that half is left where it is. Returns the edge the card
115
+ * takes.
116
+ */
117
+ export function scrollIntoFreeHalf(el: Element, card: Element | null, previous: DockEdge | null): DockEdge {
118
+ const r = el.getBoundingClientRect();
119
+ const band = readBand(el, card);
120
+ const edge = dockEdge(r, band, previous, card instanceof HTMLElement ? card.offsetHeight : MIN_DOCK);
121
+ const mid = band.top + (band.bottom - band.top) / 2;
122
+ const half = edge === "bottom" ? { top: band.top + GUTTER, bottom: mid } : { top: mid, bottom: band.bottom - GUTTER };
123
+ if (r.top >= half.top && r.top + r.height <= half.bottom) return edge;
124
+ // Centred in the half, or its top at the half's top when it is taller than the half.
125
+ const delta =
126
+ r.height > half.bottom - half.top ? r.top - half.top : r.top + r.height / 2 - (half.top + half.bottom) / 2;
127
+ const scroller = scrollParent(el);
128
+ if (scroller) scroller.scrollTop += delta;
129
+ // A field in a kit layer with nothing to scroll: the page behind it stays put.
130
+ else if (!el.closest("[data-kit-layer]")) window.scrollBy?.(0, delta);
131
+ return edge;
132
+ }