@eifi1/ui-kit 0.4.1 → 0.5.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 (410) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +498 -57
  3. package/dist/chart.d.ts +10 -0
  4. package/dist/chart.js +10 -0
  5. package/dist/chart.js.map +1 -0
  6. package/dist/components/account-settings.d.ts +11 -0
  7. package/dist/components/account-settings.js +19 -12
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/alert-banner.d.ts +10 -6
  10. package/dist/components/alert-banner.js +20 -16
  11. package/dist/components/alert-banner.js.map +1 -1
  12. package/dist/components/amount-input.d.ts +30 -4
  13. package/dist/components/amount-input.js +36 -14
  14. package/dist/components/amount-input.js.map +1 -1
  15. package/dist/components/calculator.d.ts +24 -7
  16. package/dist/components/calculator.js +37 -20
  17. package/dist/components/calculator.js.map +1 -1
  18. package/dist/components/chart-zoom.d.ts +156 -0
  19. package/dist/components/chart-zoom.js +262 -0
  20. package/dist/components/chart-zoom.js.map +1 -0
  21. package/dist/components/chart.d.ts +28 -3
  22. package/dist/components/chart.js +71 -32
  23. package/dist/components/chart.js.map +1 -1
  24. package/dist/components/checkbox.d.ts +51 -0
  25. package/dist/components/checkbox.js +123 -0
  26. package/dist/components/checkbox.js.map +1 -0
  27. package/dist/components/chip.d.ts +128 -0
  28. package/dist/components/chip.js +357 -0
  29. package/dist/components/chip.js.map +1 -0
  30. package/dist/components/choice-parts.d.ts +24 -0
  31. package/dist/components/choice-parts.js +18 -0
  32. package/dist/components/choice-parts.js.map +1 -0
  33. package/dist/components/combobox-core.d.ts +52 -9
  34. package/dist/components/combobox-core.js +89 -19
  35. package/dist/components/combobox-core.js.map +1 -1
  36. package/dist/components/combobox.d.ts +43 -33
  37. package/dist/components/combobox.js +409 -340
  38. package/dist/components/combobox.js.map +1 -1
  39. package/dist/components/currency-select.d.ts +48 -8
  40. package/dist/components/currency-select.js +107 -65
  41. package/dist/components/currency-select.js.map +1 -1
  42. package/dist/components/data-table-filter-popover.d.ts +15 -3
  43. package/dist/components/data-table-filter-popover.js +23 -17
  44. package/dist/components/data-table-filter-popover.js.map +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +3 -2
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +53 -2
  49. package/dist/components/data-table-labels.js +31 -2
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table-pagination.d.ts +3 -1
  52. package/dist/components/data-table-pagination.js +16 -10
  53. package/dist/components/data-table-pagination.js.map +1 -1
  54. package/dist/components/data-table-sort.js +1 -0
  55. package/dist/components/data-table-sort.js.map +1 -1
  56. package/dist/components/data-table.d.ts +1 -1
  57. package/dist/components/data-table.js +201 -85
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/date-picker.d.ts +25 -9
  60. package/dist/components/date-picker.js +232 -94
  61. package/dist/components/date-picker.js.map +1 -1
  62. package/dist/components/dropdown.d.ts +74 -19
  63. package/dist/components/dropdown.js +72 -22
  64. package/dist/components/dropdown.js.map +1 -1
  65. package/dist/components/entity-combobox.d.ts +5 -4
  66. package/dist/components/entity-combobox.js +107 -76
  67. package/dist/components/entity-combobox.js.map +1 -1
  68. package/dist/components/facing-pair.d.ts +36 -0
  69. package/dist/components/facing-pair.js +40 -0
  70. package/dist/components/facing-pair.js.map +1 -0
  71. package/dist/components/field-sync.d.ts +164 -0
  72. package/dist/components/field-sync.js +264 -0
  73. package/dist/components/field-sync.js.map +1 -0
  74. package/dist/components/file-dropzone.d.ts +21 -7
  75. package/dist/components/file-dropzone.js +28 -7
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/full-bleed-dialog.d.ts +37 -26
  78. package/dist/components/full-bleed-dialog.js +20 -5
  79. package/dist/components/full-bleed-dialog.js.map +1 -1
  80. package/dist/components/grouped-picker.d.ts +21 -10
  81. package/dist/components/grouped-picker.js +66 -52
  82. package/dist/components/grouped-picker.js.map +1 -1
  83. package/dist/components/hover-menu.d.ts +20 -4
  84. package/dist/components/hover-menu.js +15 -4
  85. package/dist/components/hover-menu.js.map +1 -1
  86. package/dist/components/mini-calendar.d.ts +102 -14
  87. package/dist/components/mini-calendar.js +256 -70
  88. package/dist/components/mini-calendar.js.map +1 -1
  89. package/dist/components/modal.d.ts +9 -3
  90. package/dist/components/modal.js +34 -45
  91. package/dist/components/modal.js.map +1 -1
  92. package/dist/components/month-picker.d.ts +79 -0
  93. package/dist/components/month-picker.js +342 -0
  94. package/dist/components/month-picker.js.map +1 -0
  95. package/dist/components/multi-entity-combobox.d.ts +7 -6
  96. package/dist/components/multi-entity-combobox.js +112 -72
  97. package/dist/components/multi-entity-combobox.js.map +1 -1
  98. package/dist/components/multi-select.d.ts +13 -11
  99. package/dist/components/multi-select.js +161 -84
  100. package/dist/components/multi-select.js.map +1 -1
  101. package/dist/components/number-field.d.ts +92 -0
  102. package/dist/components/number-field.js +111 -0
  103. package/dist/components/number-field.js.map +1 -0
  104. package/dist/components/number-input.d.ts +21 -2
  105. package/dist/components/number-input.js +2 -1
  106. package/dist/components/number-input.js.map +1 -1
  107. package/dist/components/numpad-sheet.d.ts +25 -4
  108. package/dist/components/numpad-sheet.js +30 -13
  109. package/dist/components/numpad-sheet.js.map +1 -1
  110. package/dist/components/page-contents.d.ts +90 -0
  111. package/dist/components/page-contents.js +170 -0
  112. package/dist/components/page-contents.js.map +1 -0
  113. package/dist/components/password-strength.d.ts +106 -0
  114. package/dist/components/password-strength.js +132 -0
  115. package/dist/components/password-strength.js.map +1 -0
  116. package/dist/components/picker-sheet.d.ts +23 -15
  117. package/dist/components/picker-sheet.js +34 -14
  118. package/dist/components/picker-sheet.js.map +1 -1
  119. package/dist/components/popover.d.ts +62 -4
  120. package/dist/components/popover.js +52 -2
  121. package/dist/components/popover.js.map +1 -1
  122. package/dist/components/search-field.d.ts +25 -7
  123. package/dist/components/search-field.js +7 -5
  124. package/dist/components/search-field.js.map +1 -1
  125. package/dist/components/series-chart-labels.d.ts +14 -0
  126. package/dist/components/series-chart-labels.js +11 -0
  127. package/dist/components/series-chart-labels.js.map +1 -0
  128. package/dist/components/series-chart.d.ts +207 -0
  129. package/dist/components/series-chart.js +223 -0
  130. package/dist/components/series-chart.js.map +1 -0
  131. package/dist/components/settings-fields.d.ts +14 -4
  132. package/dist/components/settings-fields.js +19 -8
  133. package/dist/components/settings-fields.js.map +1 -1
  134. package/dist/components/signature-pad.d.ts +119 -0
  135. package/dist/components/signature-pad.js +405 -0
  136. package/dist/components/signature-pad.js.map +1 -0
  137. package/dist/components/slider.d.ts +80 -0
  138. package/dist/components/slider.js +155 -0
  139. package/dist/components/slider.js.map +1 -0
  140. package/dist/components/sparkline.d.ts +93 -0
  141. package/dist/components/sparkline.js +205 -0
  142. package/dist/components/sparkline.js.map +1 -0
  143. package/dist/components/stat-tile.d.ts +151 -0
  144. package/dist/components/stat-tile.js +236 -0
  145. package/dist/components/stat-tile.js.map +1 -0
  146. package/dist/components/swipeable-row.d.ts +25 -2
  147. package/dist/components/swipeable-row.js +31 -0
  148. package/dist/components/swipeable-row.js.map +1 -1
  149. package/dist/components/switch.d.ts +60 -0
  150. package/dist/components/switch.js +81 -0
  151. package/dist/components/switch.js.map +1 -0
  152. package/dist/components/time-input.d.ts +58 -0
  153. package/dist/components/time-input.js +50 -0
  154. package/dist/components/time-input.js.map +1 -0
  155. package/dist/components/toggle-group.d.ts +18 -3
  156. package/dist/components/toggle-group.js +10 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +115 -0
  159. package/dist/components/toggle-legend.js +114 -0
  160. package/dist/components/toggle-legend.js.map +1 -0
  161. package/dist/components/tooltip.d.ts +40 -11
  162. package/dist/components/tooltip.js +77 -20
  163. package/dist/components/tooltip.js.map +1 -1
  164. package/dist/components/treemap.d.ts +132 -0
  165. package/dist/components/treemap.js +179 -0
  166. package/dist/components/treemap.js.map +1 -0
  167. package/dist/components/ui.d.ts +159 -46
  168. package/dist/components/ui.js +220 -115
  169. package/dist/components/ui.js.map +1 -1
  170. package/dist/components/use-mobile-reveal.js +1 -0
  171. package/dist/components/use-mobile-reveal.js.map +1 -1
  172. package/dist/components/use-table-state.d.ts +1 -1
  173. package/dist/components/use-table-state.js +38 -17
  174. package/dist/components/use-table-state.js.map +1 -1
  175. package/dist/components/user-avatar.d.ts +5 -3
  176. package/dist/components/user-avatar.js +4 -2
  177. package/dist/components/user-avatar.js.map +1 -1
  178. package/dist/components/wizard-stepper.js +3 -2
  179. package/dist/components/wizard-stepper.js.map +1 -1
  180. package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
  181. package/dist/data-table.d.ts +7 -0
  182. package/dist/data-table.js +12 -0
  183. package/dist/data-table.js.map +1 -0
  184. package/dist/feedback/feedback-attachment.js +12 -11
  185. package/dist/feedback/feedback-attachment.js.map +1 -1
  186. package/dist/feedback/feedback-dialog.js +2 -1
  187. package/dist/feedback/feedback-dialog.js.map +1 -1
  188. package/dist/feedback/feedback-inbox.d.ts +33 -7
  189. package/dist/feedback/feedback-inbox.js +49 -37
  190. package/dist/feedback/feedback-inbox.js.map +1 -1
  191. package/dist/feedback.d.ts +5 -0
  192. package/dist/feedback.js +5 -0
  193. package/dist/feedback.js.map +1 -0
  194. package/dist/hooks/use-anchored-panel.js +1 -0
  195. package/dist/hooks/use-anchored-panel.js.map +1 -1
  196. package/dist/hooks/use-anchored-rect.js +1 -0
  197. package/dist/hooks/use-anchored-rect.js.map +1 -1
  198. package/dist/hooks/use-announce.d.ts +46 -0
  199. package/dist/hooks/use-announce.js +37 -0
  200. package/dist/hooks/use-announce.js.map +1 -0
  201. package/dist/hooks/use-body-scroll-lock.js +1 -0
  202. package/dist/hooks/use-body-scroll-lock.js.map +1 -1
  203. package/dist/hooks/use-close-transition.js +1 -0
  204. package/dist/hooks/use-close-transition.js.map +1 -1
  205. package/dist/hooks/use-dismiss.js +1 -0
  206. package/dist/hooks/use-dismiss.js.map +1 -1
  207. package/dist/hooks/use-focus-trap.d.ts +16 -0
  208. package/dist/hooks/use-focus-trap.js +67 -0
  209. package/dist/hooks/use-focus-trap.js.map +1 -0
  210. package/dist/hooks/use-media-query.js +1 -0
  211. package/dist/hooks/use-media-query.js.map +1 -1
  212. package/dist/hooks/use-overlay-history.js +27 -1
  213. package/dist/hooks/use-overlay-history.js.map +1 -1
  214. package/dist/hooks/use-row-swipe.js +1 -0
  215. package/dist/hooks/use-row-swipe.js.map +1 -1
  216. package/dist/i18n/defaults.d.ts +31 -0
  217. package/dist/i18n/defaults.js +63 -0
  218. package/dist/i18n/defaults.js.map +1 -0
  219. package/dist/i18n/kit-labels.d.ts +250 -0
  220. package/dist/i18n/kit-labels.js +184 -0
  221. package/dist/i18n/kit-labels.js.map +1 -0
  222. package/dist/index.d.ts +47 -22
  223. package/dist/index.js +28 -0
  224. package/dist/index.js.map +1 -1
  225. package/dist/lib/calc.js +1 -0
  226. package/dist/lib/calc.js.map +1 -1
  227. package/dist/lib/cn.js +1 -0
  228. package/dist/lib/cn.js.map +1 -1
  229. package/dist/lib/dates.js +1 -0
  230. package/dist/lib/dates.js.map +1 -1
  231. package/dist/lib/logger.js +1 -0
  232. package/dist/lib/logger.js.map +1 -1
  233. package/dist/lib/safe-storage.js +1 -0
  234. package/dist/lib/safe-storage.js.map +1 -1
  235. package/dist/search/command-palette.d.ts +11 -1
  236. package/dist/search/command-palette.js +21 -15
  237. package/dist/search/command-palette.js.map +1 -1
  238. package/dist/search.d.ts +2 -0
  239. package/dist/search.js +3 -0
  240. package/dist/search.js.map +1 -0
  241. package/dist/shell/app-shell.d.ts +33 -4
  242. package/dist/shell/app-shell.js +275 -78
  243. package/dist/shell/app-shell.js.map +1 -1
  244. package/dist/shell/option-switcher-menu.js +3 -2
  245. package/dist/shell/option-switcher-menu.js.map +1 -1
  246. package/dist/shell/role-switcher.d.ts +6 -0
  247. package/dist/shell/role-switcher.js +7 -4
  248. package/dist/shell/role-switcher.js.map +1 -1
  249. package/dist/shell/top-bar.d.ts +15 -7
  250. package/dist/shell/top-bar.js +3 -5
  251. package/dist/shell/top-bar.js.map +1 -1
  252. package/dist/shell/topbar-action-menu.js +3 -2
  253. package/dist/shell/topbar-action-menu.js.map +1 -1
  254. package/dist/shell/topbar-controls.d.ts +5 -2
  255. package/dist/shell/topbar-controls.js +20 -15
  256. package/dist/shell/topbar-controls.js.map +1 -1
  257. package/dist/shell.d.ts +13 -0
  258. package/dist/shell.js +8 -0
  259. package/dist/shell.js.map +1 -0
  260. package/dist/theme/chart-palette.d.ts +19 -12
  261. package/dist/theme/chart-palette.js +24 -30
  262. package/dist/theme/chart-palette.js.map +1 -1
  263. package/dist/theme/color.d.ts +78 -0
  264. package/dist/theme/color.js +148 -0
  265. package/dist/theme/color.js.map +1 -0
  266. package/dist/theme/palette-derive.d.ts +140 -0
  267. package/dist/theme/palette-derive.js +376 -0
  268. package/dist/theme/palette-derive.js.map +1 -0
  269. package/dist/theme/palette-presets.d.ts +14 -1
  270. package/dist/theme/palette-presets.js +231 -3
  271. package/dist/theme/palette-presets.js.map +1 -1
  272. package/dist/theme/palette-store.js +1 -0
  273. package/dist/theme/palette-store.js.map +1 -1
  274. package/dist/theme/theme-store.js +1 -0
  275. package/dist/theme/theme-store.js.map +1 -1
  276. package/dist/tour/tour.d.ts +2 -1
  277. package/dist/tour/tour.js +15 -9
  278. package/dist/tour/tour.js.map +1 -1
  279. package/dist/tour.d.ts +2 -0
  280. package/dist/tour.js +3 -0
  281. package/dist/tour.js.map +1 -0
  282. package/dist/wizard/stepper-nav.js +10 -8
  283. package/dist/wizard/stepper-nav.js.map +1 -1
  284. package/dist/wizard/types.d.ts +18 -2
  285. package/dist/wizard/types.js +4 -1
  286. package/dist/wizard/types.js.map +1 -1
  287. package/dist/wizard/use-wizard.js +12 -4
  288. package/dist/wizard/use-wizard.js.map +1 -1
  289. package/dist/wizard/validation.js +1 -0
  290. package/dist/wizard/validation.js.map +1 -1
  291. package/dist/wizard/wizard-context.js +1 -0
  292. package/dist/wizard/wizard-context.js.map +1 -1
  293. package/dist/wizard/wizard-step.js +1 -0
  294. package/dist/wizard/wizard-step.js.map +1 -1
  295. package/dist/wizard/wizard-summary.js +5 -3
  296. package/dist/wizard/wizard-summary.js.map +1 -1
  297. package/dist/wizard.d.ts +8 -0
  298. package/dist/wizard.js +9 -0
  299. package/dist/wizard.js.map +1 -0
  300. package/package.json +66 -13
  301. package/src/chart.ts +22 -0
  302. package/src/components/account-settings.tsx +275 -0
  303. package/src/components/alert-banner.tsx +75 -0
  304. package/src/components/amount-input.tsx +471 -0
  305. package/src/components/calculator.tsx +252 -0
  306. package/src/components/chart-zoom.tsx +499 -0
  307. package/src/components/chart.tsx +363 -0
  308. package/src/components/checkbox.tsx +222 -0
  309. package/src/components/chip.tsx +570 -0
  310. package/src/components/choice-parts.ts +33 -0
  311. package/src/components/combobox-core.tsx +540 -0
  312. package/src/components/combobox.tsx +1026 -0
  313. package/src/components/currency-select.tsx +269 -0
  314. package/src/components/data-table-filter-popover.tsx +252 -0
  315. package/src/components/data-table-filters.ts +176 -0
  316. package/src/components/data-table-labels.ts +190 -0
  317. package/src/components/data-table-pagination.tsx +126 -0
  318. package/src/components/data-table-sort.ts +70 -0
  319. package/src/components/data-table.tsx +1740 -0
  320. package/src/components/date-picker.tsx +631 -0
  321. package/src/components/dropdown.tsx +341 -0
  322. package/src/components/entity-combobox.tsx +215 -0
  323. package/src/components/facing-pair.ts +86 -0
  324. package/src/components/field-sync.tsx +472 -0
  325. package/src/components/file-dropzone.tsx +181 -0
  326. package/src/components/full-bleed-dialog.tsx +179 -0
  327. package/src/components/grouped-picker.tsx +157 -0
  328. package/src/components/hover-menu.tsx +183 -0
  329. package/src/components/mini-calendar.tsx +554 -0
  330. package/src/components/modal.tsx +287 -0
  331. package/src/components/month-picker.tsx +549 -0
  332. package/src/components/multi-entity-combobox.tsx +226 -0
  333. package/src/components/multi-select.tsx +303 -0
  334. package/src/components/number-field.tsx +225 -0
  335. package/src/components/number-input.tsx +211 -0
  336. package/src/components/numpad-sheet.tsx +264 -0
  337. package/src/components/page-contents.tsx +289 -0
  338. package/src/components/password-strength.tsx +258 -0
  339. package/src/components/picker-sheet.tsx +251 -0
  340. package/src/components/popover.tsx +195 -0
  341. package/src/components/search-field.tsx +146 -0
  342. package/src/components/series-chart-labels.ts +26 -0
  343. package/src/components/series-chart.tsx +481 -0
  344. package/src/components/settings-fields.tsx +68 -0
  345. package/src/components/signature-pad.tsx +630 -0
  346. package/src/components/slider.tsx +313 -0
  347. package/src/components/sparkline.tsx +329 -0
  348. package/src/components/stat-tile.tsx +448 -0
  349. package/src/components/swipeable-row.tsx +271 -0
  350. package/src/components/switch.tsx +170 -0
  351. package/src/components/time-input.tsx +107 -0
  352. package/src/components/toggle-group.tsx +153 -0
  353. package/src/components/toggle-legend.tsx +229 -0
  354. package/src/components/tooltip.tsx +477 -0
  355. package/src/components/treemap.tsx +390 -0
  356. package/src/components/ui.tsx +1284 -0
  357. package/src/components/use-mobile-reveal.ts +54 -0
  358. package/src/components/use-table-state.ts +295 -0
  359. package/src/components/user-avatar.tsx +61 -0
  360. package/src/components/wizard-stepper.tsx +37 -0
  361. package/src/data-table.ts +19 -0
  362. package/src/feedback/feedback-attachment.tsx +246 -0
  363. package/src/feedback/feedback-dialog.tsx +209 -0
  364. package/src/feedback/feedback-inbox.tsx +615 -0
  365. package/src/feedback.ts +8 -0
  366. package/src/hooks/use-anchored-panel.ts +169 -0
  367. package/src/hooks/use-anchored-rect.ts +78 -0
  368. package/src/hooks/use-announce.ts +85 -0
  369. package/src/hooks/use-body-scroll-lock.ts +94 -0
  370. package/src/hooks/use-close-transition.ts +99 -0
  371. package/src/hooks/use-dismiss.ts +77 -0
  372. package/src/hooks/use-focus-trap.ts +160 -0
  373. package/src/hooks/use-media-query.ts +23 -0
  374. package/src/hooks/use-overlay-history.ts +345 -0
  375. package/src/hooks/use-row-swipe.ts +212 -0
  376. package/src/i18n/defaults.ts +68 -0
  377. package/src/i18n/kit-labels.tsx +447 -0
  378. package/src/index.ts +169 -0
  379. package/src/lib/calc.ts +309 -0
  380. package/src/lib/cn.ts +5 -0
  381. package/src/lib/dates.ts +187 -0
  382. package/src/lib/logger.ts +73 -0
  383. package/src/lib/safe-storage.ts +37 -0
  384. package/src/search/command-palette.tsx +342 -0
  385. package/src/search.ts +3 -0
  386. package/src/shell/app-shell.tsx +772 -0
  387. package/src/shell/option-switcher-menu.tsx +72 -0
  388. package/src/shell/role-switcher.tsx +46 -0
  389. package/src/shell/top-bar.tsx +39 -0
  390. package/src/shell/topbar-action-menu.tsx +91 -0
  391. package/src/shell/topbar-controls.tsx +229 -0
  392. package/src/shell.ts +10 -0
  393. package/src/theme/chart-palette.ts +145 -0
  394. package/src/theme/color.ts +266 -0
  395. package/src/theme/palette-derive.ts +681 -0
  396. package/src/theme/palette-presets.ts +760 -0
  397. package/src/theme/palette-store.ts +94 -0
  398. package/src/theme/theme-store.ts +145 -0
  399. package/src/tour/tour.tsx +586 -0
  400. package/src/tour.ts +3 -0
  401. package/src/vite-env.d.ts +26 -0
  402. package/src/wizard/stepper-nav.tsx +221 -0
  403. package/src/wizard/types.ts +178 -0
  404. package/src/wizard/use-wizard.ts +345 -0
  405. package/src/wizard/validation.ts +37 -0
  406. package/src/wizard/wizard-context.tsx +29 -0
  407. package/src/wizard/wizard-step.tsx +14 -0
  408. package/src/wizard/wizard-summary.tsx +69 -0
  409. package/src/wizard.ts +14 -0
  410. package/tokens.css +222 -20
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/option-switcher-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\nimport { Tooltip } from \"../components/tooltip\";\n\nexport interface OptionSwitcherOption<T extends string> {\n value: T;\n label: ReactNode;\n}\n\n/**\n * A top-bar hover menu that switches between a small set of options, with a\n * check on the active one and an optional heading. Generic glue over\n * {@link HoverMenu} — e.g. a dev role/impersonation switcher, an environment\n * picker. Controlled: the app owns `value` and `onSelect`.\n */\nexport function OptionSwitcherMenu<T extends string>({\n icon,\n ariaLabel,\n title,\n heading,\n options,\n value,\n onSelect,\n}: {\n icon: ReactNode;\n ariaLabel: string;\n title?: string;\n heading?: string;\n options: OptionSwitcherOption<T>[];\n value: T;\n onSelect: (value: T) => void;\n}) {\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <Tooltip label={title} portal>\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500\">\n {heading}\n </li>\n )}\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(opt.value);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span>{opt.label}</span>\n {value === opt.value && <Check className=\"size-4 text-slate-700 dark:text-slate-200\" />}\n </button>\n </li>\n ))}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"AAuCU,cAeI,YAfJ;AAtCV,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAC7D,SAAS,eAAe;AAajB,SAAS,mBAAqC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,WAAQ,OAAO,OAAO,QAAM,MAC3B,8BAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH,GACF;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAU,QACX;AAAA,mBACC,oBAAC,QAAG,WAAU,kGACX,mBACH;AAAA,QAED,QAAQ,IAAI,CAAC,QACZ,oBAAC,QACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM;AACb,uBAAS,IAAI,KAAK;AAClB,oBAAM;AAAA,YACR;AAAA,YACA,WAAW;AAAA,YAEX;AAAA,kCAAC,UAAM,cAAI,OAAM;AAAA,cAChB,UAAU,IAAI,SAAS,oBAAC,SAAM,WAAU,6CAA4C;AAAA;AAAA;AAAA,QACvF,KAXO,IAAI,KAYb,CACD;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/option-switcher-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\nimport { Tooltip } from \"../components/tooltip\";\n\nexport interface OptionSwitcherOption<T extends string> {\n value: T;\n label: ReactNode;\n}\n\n/**\n * A top-bar hover menu that switches between a small set of options, with a\n * check on the active one and an optional heading. Generic glue over\n * {@link HoverMenu} — e.g. a dev role/impersonation switcher, an environment\n * picker. Controlled: the app owns `value` and `onSelect`.\n */\nexport function OptionSwitcherMenu<T extends string>({\n icon,\n ariaLabel,\n title,\n heading,\n options,\n value,\n onSelect,\n}: {\n icon: ReactNode;\n ariaLabel: string;\n title?: string;\n heading?: string;\n options: OptionSwitcherOption<T>[];\n value: T;\n onSelect: (value: T) => void;\n}) {\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <Tooltip label={title} portal>\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\">\n {heading}\n </li>\n )}\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(opt.value);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span>{opt.label}</span>\n {value === opt.value && <Check className=\"size-4 text-[var(--text-secondary)]\" />}\n </button>\n </li>\n ))}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":";AAuCU,cAeI,YAfJ;AAtCV,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAC7D,SAAS,eAAe;AAajB,SAAS,mBAAqC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,WAAQ,OAAO,OAAO,QAAM,MAC3B,8BAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH,GACF;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAU,QACX;AAAA,mBACC,oBAAC,QAAG,WAAU,8FACX,mBACH;AAAA,QAED,QAAQ,IAAI,CAAC,QACZ,oBAAC,QACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM;AACb,uBAAS,IAAI,KAAK;AAClB,oBAAM;AAAA,YACR;AAAA,YACA,WAAW;AAAA,YAEX;AAAA,kCAAC,UAAM,cAAI,OAAM;AAAA,cAChB,UAAU,IAAI,SAAS,oBAAC,SAAM,WAAU,uCAAsC;AAAA;AAAA;AAAA,QACjF,KAXO,IAAI,KAYb,CACD;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -4,8 +4,10 @@ interface RoleSwitcherProps<R extends string> {
4
4
  roles: readonly R[];
5
5
  value: R;
6
6
  onChange: (role: R) => void;
7
+ /** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */
7
8
  ariaLabel?: string;
8
9
  heading?: string;
10
+ /** The trigger's tooltip. Default: `topBar.role(value)`. */
9
11
  title?: string;
10
12
  }
11
13
  /**
@@ -13,6 +15,10 @@ interface RoleSwitcherProps<R extends string> {
13
15
  * {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in
14
16
  * the top bar with just the role list + current value + a handler. Typically
15
17
  * dev-only / UI-only, but the app decides where and whether to render it.
18
+ *
19
+ * The flask wears `--warning` rather than a fixed `amber-500`: it is a caution
20
+ * marker — the app is not showing you your own role — and the token carries that
21
+ * amber across both themes instead of pinning one hue onto every palette.
16
22
  */
17
23
  declare function RoleSwitcher<R extends string>({ roles, value, onChange, ariaLabel, heading, title, }: RoleSwitcherProps<R>): react.JSX.Element;
18
24
 
@@ -1,21 +1,24 @@
1
+ "use client";
1
2
  import { jsx } from "react/jsx-runtime";
2
3
  import { FlaskConical } from "lucide-react";
3
4
  import { OptionSwitcherMenu } from "./option-switcher-menu.js";
5
+ import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels.js";
4
6
  function RoleSwitcher({
5
7
  roles,
6
8
  value,
7
9
  onChange,
8
- ariaLabel = "Switch role",
10
+ ariaLabel,
9
11
  heading,
10
12
  title
11
13
  }) {
14
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabel });
12
15
  return /* @__PURE__ */ jsx(
13
16
  OptionSwitcherMenu,
14
17
  {
15
- ariaLabel,
16
- title: title ?? `Role: ${value}`,
18
+ ariaLabel: labels.switchRole,
19
+ title: title ?? labels.role(value),
17
20
  heading,
18
- icon: /* @__PURE__ */ jsx(FlaskConical, { className: "size-5 text-amber-500" }),
21
+ icon: /* @__PURE__ */ jsx(FlaskConical, { className: "size-5 text-[var(--warning)]" }),
19
22
  options: roles.map((r) => ({ value: r, label: r })),
20
23
  value,
21
24
  onSelect: onChange
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/role-switcher.tsx"],"sourcesContent":["import { FlaskConical } from \"lucide-react\";\nimport { OptionSwitcherMenu } from \"./option-switcher-menu\";\n\nexport interface RoleSwitcherProps<R extends string> {\n roles: readonly R[];\n value: R;\n onChange: (role: R) => void;\n ariaLabel?: string;\n heading?: string;\n title?: string;\n}\n\n/**\n * A reusable user-role switcher (feedback #337): a thin wrapper over\n * {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in\n * the top bar with just the role list + current value + a handler. Typically\n * dev-only / UI-only, but the app decides where and whether to render it.\n */\nexport function RoleSwitcher<R extends string>({\n roles,\n value,\n onChange,\n ariaLabel = \"Switch role\",\n heading,\n title,\n}: RoleSwitcherProps<R>) {\n return (\n <OptionSwitcherMenu<R>\n ariaLabel={ariaLabel}\n title={title ?? `Role: ${value}`}\n heading={heading}\n icon={<FlaskConical className=\"size-5 text-amber-500\" />}\n options={roles.map((r) => ({ value: r, label: r }))}\n value={value}\n onSelect={onChange}\n />\n );\n}\n"],"mappings":"AA+BY;AA/BZ,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AAiB5B,SAAS,aAA+B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AACF,GAAyB;AACvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,SAAS,SAAS,KAAK;AAAA,MAC9B;AAAA,MACA,MAAM,oBAAC,gBAAa,WAAU,yBAAwB;AAAA,MACtD,SAAS,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAAA,MAClD;AAAA,MACA,UAAU;AAAA;AAAA,EACZ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/role-switcher.tsx"],"sourcesContent":["import { FlaskConical } from \"lucide-react\";\nimport { OptionSwitcherMenu } from \"./option-switcher-menu\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface RoleSwitcherProps<R extends string> {\n roles: readonly R[];\n value: R;\n onChange: (role: R) => void;\n /** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n heading?: string;\n /** The trigger's tooltip. Default: `topBar.role(value)`. */\n title?: string;\n}\n\n/**\n * A reusable user-role switcher (feedback #337): a thin wrapper over\n * {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in\n * the top bar with just the role list + current value + a handler. Typically\n * dev-only / UI-only, but the app decides where and whether to render it.\n *\n * The flask wears `--warning` rather than a fixed `amber-500`: it is a caution\n * marker — the app is not showing you your own role — and the token carries that\n * amber across both themes instead of pinning one hue onto every palette.\n */\nexport function RoleSwitcher<R extends string>({\n roles,\n value,\n onChange,\n ariaLabel,\n heading,\n title,\n}: RoleSwitcherProps<R>) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabel });\n return (\n <OptionSwitcherMenu<R>\n ariaLabel={labels.switchRole}\n title={title ?? labels.role(value)}\n heading={heading}\n icon={<FlaskConical className=\"size-5 text-[var(--warning)]\" />}\n options={roles.map((r) => ({ value: r, label: r }))}\n value={value}\n onSelect={onChange}\n />\n );\n}\n"],"mappings":";AAuCY;AAvCZ,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,wBAAwB,oBAAoB;AAuB9C,SAAS,aAA+B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,SAAS,aAAa,UAAU,wBAAwB,EAAE,YAAY,UAAU,CAAC;AACvF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,OAAO;AAAA,MAClB,OAAO,SAAS,OAAO,KAAK,KAAK;AAAA,MACjC;AAAA,MACA,MAAM,oBAAC,gBAAa,WAAU,gCAA+B;AAAA,MAC7D,SAAS,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAAA,MAClD;AAAA,MACA,UAAU;AAAA;AAAA,EACZ;AAEJ;","names":[]}
@@ -1,15 +1,23 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
 
4
4
  /**
5
- * The sticky app header frame: a `brand` slot on the left and an `actions` slot
6
- * on the right (theme/palette/language controls, account menu, etc.). Domain-free
7
- * — each app composes its own actions from the shared controls plus its own menus.
5
+ * `extends ComponentPropsWithoutRef<"header">` because this IS the `<header>`, and it is
6
+ * the `sticky top-0` element itself: a consumer cannot wrap it to add an attribute
7
+ * without the sticking moving to the wrapper. The one that matters most is `aria-label`
8
+ * — a `<header>` is a `banner` landmark, and a landmark with no name is how two of them
9
+ * become indistinguishable in a screen reader's landmark list.
8
10
  */
9
- declare function TopBar({ brand, actions, className, }: {
11
+ interface TopBarProps extends ComponentPropsWithoutRef<"header"> {
10
12
  brand: ReactNode;
11
13
  actions?: ReactNode;
12
14
  className?: string;
13
- }): react.JSX.Element;
15
+ }
16
+ /**
17
+ * The sticky app header frame: a `brand` slot on the left and an `actions` slot
18
+ * on the right (theme/palette/language controls, account menu, etc.). Domain-free
19
+ * — each app composes its own actions from the shared controls plus its own menus.
20
+ */
21
+ declare function TopBar({ brand, actions, className, ...rest }: TopBarProps): react.JSX.Element;
14
22
 
15
- export { TopBar };
23
+ export { TopBar, type TopBarProps };
@@ -1,13 +1,11 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../lib/cn.js";
3
- function TopBar({
4
- brand,
5
- actions,
6
- className
7
- }) {
4
+ function TopBar({ brand, actions, className, ...rest }) {
8
5
  return /* @__PURE__ */ jsxs(
9
6
  "header",
10
7
  {
8
+ ...rest,
11
9
  className: cn(
12
10
  // z-40 keeps the app header above sticky page content — notably the
13
11
  // expanded transaction editor (z-30), which on mobile shares the page
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/top-bar.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * The sticky app header frame: a `brand` slot on the left and an `actions` slot\n * on the right (theme/palette/language controls, account menu, etc.). Domain-free\n * — each app composes its own actions from the shared controls plus its own menus.\n */\nexport function TopBar({\n brand,\n actions,\n className,\n}: {\n brand: ReactNode;\n actions?: ReactNode;\n className?: string;\n}) {\n return (\n <header\n className={cn(\n // z-40 keeps the app header above sticky page content — notably the\n // expanded transaction editor (z-30), which on mobile shares the page\n // scroll and otherwise slid over the header while scrolling (feedback\n // #320). Modals/tour (z-50/60) still sit above it.\n \"sticky top-0 z-40 flex h-12 items-center justify-between gap-2 border-b border-[var(--border)] bg-[var(--bg-surface)] px-3 md:px-6\",\n className,\n )}\n >\n <div className=\"flex min-w-0 items-center gap-2\">{brand}</div>\n <div className=\"flex items-center gap-1\">{actions}</div>\n </header>\n );\n}\n"],"mappings":"AAkBI,SAUE,KAVF;AAjBJ,SAAS,UAAU;AAOZ,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,SAAI,WAAU,mCAAmC,iBAAM;AAAA,QACxD,oBAAC,SAAI,WAAU,2BAA2B,mBAAQ;AAAA;AAAA;AAAA,EACpD;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/top-bar.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * `extends ComponentPropsWithoutRef<\"header\">` because this IS the `<header>`, and it is\n * the `sticky top-0` element itself: a consumer cannot wrap it to add an attribute\n * without the sticking moving to the wrapper. The one that matters most is `aria-label`\n * — a `<header>` is a `banner` landmark, and a landmark with no name is how two of them\n * become indistinguishable in a screen reader's landmark list.\n */\nexport interface TopBarProps extends ComponentPropsWithoutRef<\"header\"> {\n brand: ReactNode;\n actions?: ReactNode;\n className?: string;\n}\n\n/**\n * The sticky app header frame: a `brand` slot on the left and an `actions` slot\n * on the right (theme/palette/language controls, account menu, etc.). Domain-free\n * — each app composes its own actions from the shared controls plus its own menus.\n */\nexport function TopBar({ brand, actions, className, ...rest }: TopBarProps) {\n return (\n <header\n {...rest}\n className={cn(\n // z-40 keeps the app header above sticky page content — notably the\n // expanded transaction editor (z-30), which on mobile shares the page\n // scroll and otherwise slid over the header while scrolling (feedback\n // #320). Modals/tour (z-50/60) still sit above it.\n \"sticky top-0 z-40 flex h-12 items-center justify-between gap-2 border-b border-[var(--border)] bg-[var(--bg-surface)] px-3 md:px-6\",\n className,\n )}\n >\n <div className=\"flex min-w-0 items-center gap-2\">{brand}</div>\n <div className=\"flex items-center gap-1\">{actions}</div>\n </header>\n );\n}\n"],"mappings":";AAuBI,SAWE,KAXF;AAtBJ,SAAS,UAAU;AAoBZ,SAAS,OAAO,EAAE,OAAO,SAAS,WAAW,GAAG,KAAK,GAAgB;AAC1E,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,SAAI,WAAU,mCAAmC,iBAAM;AAAA,QACxD,oBAAC,SAAI,WAAU,2BAA2B,mBAAQ;AAAA;AAAA;AAAA,EACpD;AAEJ;","names":[]}
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
2
3
  import { Link } from "react-router";
3
4
  import { cn } from "../lib/cn.js";
@@ -16,10 +17,10 @@ function TopBarActionMenu({
16
17
  ariaLabel,
17
18
  trigger: ({ toggle }) => /* @__PURE__ */ jsx("button", { type: "button", onClick: toggle, "aria-label": ariaLabel, className: TOPBAR_TRIGGER_CLASS, children: icon }),
18
19
  children: (close) => /* @__PURE__ */ jsxs("ul", { className: cn("py-1", panelClassName), children: [
19
- heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500", children: heading }),
20
+ heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]", children: heading }),
20
21
  entries.map((entry) => {
21
22
  if (entry.kind === "divider") {
22
- return /* @__PURE__ */ jsx("li", { className: "my-1 border-t border-slate-100 dark:border-slate-800" }, entry.key);
23
+ return /* @__PURE__ */ jsx("li", { className: "my-1 border-t border-[var(--border)]" }, entry.key);
23
24
  }
24
25
  const content = /* @__PURE__ */ jsxs("span", { className: "flex w-full items-center gap-2", children: [
25
26
  entry.icon,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/topbar-action-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\n\n/**\n * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a\n * `link` navigates via react-router, and a `divider` draws a rule. Non-divider\n * rows close the menu automatically when chosen. `kind` may be omitted for\n * actions, keeping the common case terse.\n */\nexport type TopBarMenuEntry =\n | { kind?: \"action\"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; onSelect: () => void }\n | { kind: \"link\"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; to: string }\n | { kind: \"divider\"; key: string };\n\n/**\n * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,\n * with an optional uppercase heading and divider rules. Generic glue over the\n * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —\n * so those call sites describe their rows as data instead of rebuilding the\n * trigger + list markup. For a checkmarked value-switcher use\n * {@link OptionSwitcherMenu} instead.\n */\nexport function TopBarActionMenu({\n icon,\n ariaLabel,\n heading,\n entries,\n panelClassName,\n}: {\n icon: ReactNode;\n ariaLabel: string;\n heading?: string;\n entries: TopBarMenuEntry[];\n panelClassName?: string;\n}) {\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n )}\n >\n {(close) => (\n <ul className={cn(\"py-1\", panelClassName)}>\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500\">\n {heading}\n </li>\n )}\n {entries.map((entry) => {\n if (entry.kind === \"divider\") {\n return <li key={entry.key} className=\"my-1 border-t border-slate-100 dark:border-slate-800\" />;\n }\n const content = (\n <span className=\"flex w-full items-center gap-2\">\n {entry.icon}\n {entry.label}\n {entry.trailing && <span className=\"ml-auto pl-2\">{entry.trailing}</span>}\n </span>\n );\n return (\n <li key={entry.key}>\n {entry.kind === \"link\" ? (\n <Link to={entry.to} onClick={close} className={TOPBAR_MENU_ITEM_CLASS}>\n {content}\n </Link>\n ) : (\n <button\n type=\"button\"\n onClick={() => {\n entry.onSelect();\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n {content}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"AA0CQ,cAiBM,YAjBN;AAzCR,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAqBtD,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAW,GAAG,QAAQ,cAAc,GACrC;AAAA,mBACC,oBAAC,QAAG,WAAU,kGACX,mBACH;AAAA,QAED,QAAQ,IAAI,CAAC,UAAU;AACtB,cAAI,MAAM,SAAS,WAAW;AAC5B,mBAAO,oBAAC,QAAmB,WAAU,0DAArB,MAAM,GAAsE;AAAA,UAC9F;AACA,gBAAM,UACJ,qBAAC,UAAK,WAAU,kCACb;AAAA,kBAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM,YAAY,oBAAC,UAAK,WAAU,gBAAgB,gBAAM,UAAS;AAAA,aACpE;AAEF,iBACE,oBAAC,QACE,gBAAM,SAAS,SACd,oBAAC,QAAK,IAAI,MAAM,IAAI,SAAS,OAAO,WAAW,wBAC5C,mBACH,IAEA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM;AACb,sBAAM,SAAS;AACf,sBAAM;AAAA,cACR;AAAA,cACA,WAAW;AAAA,cAEV;AAAA;AAAA,UACH,KAfK,MAAM,GAiBf;AAAA,QAEJ,CAAC;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/topbar-action-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\n\n/**\n * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a\n * `link` navigates via react-router, and a `divider` draws a rule. Non-divider\n * rows close the menu automatically when chosen. `kind` may be omitted for\n * actions, keeping the common case terse.\n */\nexport type TopBarMenuEntry =\n | { kind?: \"action\"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; onSelect: () => void }\n | { kind: \"link\"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; to: string }\n | { kind: \"divider\"; key: string };\n\n/**\n * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,\n * with an optional uppercase heading and divider rules. Generic glue over the\n * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —\n * so those call sites describe their rows as data instead of rebuilding the\n * trigger + list markup. For a checkmarked value-switcher use\n * {@link OptionSwitcherMenu} instead.\n */\nexport function TopBarActionMenu({\n icon,\n ariaLabel,\n heading,\n entries,\n panelClassName,\n}: {\n icon: ReactNode;\n ariaLabel: string;\n heading?: string;\n entries: TopBarMenuEntry[];\n panelClassName?: string;\n}) {\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n )}\n >\n {(close) => (\n <ul className={cn(\"py-1\", panelClassName)}>\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\">\n {heading}\n </li>\n )}\n {entries.map((entry) => {\n if (entry.kind === \"divider\") {\n return <li key={entry.key} className=\"my-1 border-t border-[var(--border)]\" />;\n }\n const content = (\n <span className=\"flex w-full items-center gap-2\">\n {entry.icon}\n {entry.label}\n {entry.trailing && <span className=\"ml-auto pl-2\">{entry.trailing}</span>}\n </span>\n );\n return (\n <li key={entry.key}>\n {entry.kind === \"link\" ? (\n <Link to={entry.to} onClick={close} className={TOPBAR_MENU_ITEM_CLASS}>\n {content}\n </Link>\n ) : (\n <button\n type=\"button\"\n onClick={() => {\n entry.onSelect();\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n {content}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":";AA0CQ,cAiBM,YAjBN;AAzCR,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAqBtD,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAW,GAAG,QAAQ,cAAc,GACrC;AAAA,mBACC,oBAAC,QAAG,WAAU,8FACX,mBACH;AAAA,QAED,QAAQ,IAAI,CAAC,UAAU;AACtB,cAAI,MAAM,SAAS,WAAW;AAC5B,mBAAO,oBAAC,QAAmB,WAAU,0CAArB,MAAM,GAAsD;AAAA,UAC9E;AACA,gBAAM,UACJ,qBAAC,UAAK,WAAU,kCACb;AAAA,kBAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM,YAAY,oBAAC,UAAK,WAAU,gBAAgB,gBAAM,UAAS;AAAA,aACpE;AAEF,iBACE,oBAAC,QACE,gBAAM,SAAS,SACd,oBAAC,QAAK,IAAI,MAAM,IAAI,SAAS,OAAO,WAAW,wBAC5C,mBACH,IAEA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM;AACb,sBAAM,SAAS;AACf,sBAAM;AAAA,cACR;AAAA,cACA,WAAW;AAAA,cAEV;AAAA;AAAA,UACH,KAfK,MAAM,GAiBf;AAAA,QAEJ,CAAC;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -7,13 +7,14 @@ import 'zustand';
7
7
 
8
8
  /** Shared square icon-button styling for top-bar triggers, so app-owned controls
9
9
  * (account/feedback menus) line up with the shared ones. */
10
- declare const TOPBAR_TRIGGER_CLASS = "size-9 rounded-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800";
10
+ declare const TOPBAR_TRIGGER_CLASS = "size-9 rounded-md flex items-center justify-center text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]";
11
11
  /** Shared row styling for the top-bar hover-menu items. */
12
- declare const TOPBAR_MENU_ITEM_CLASS = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-slate-700 hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-slate-800";
12
+ declare const TOPBAR_MENU_ITEM_CLASS = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]";
13
13
  /** Sun/Moon toggle for the light/dark theme. Controlled: the app owns the mode. */
14
14
  declare function ThemeToggle({ mode, onToggle, ariaLabel, title, className, }: {
15
15
  mode: ThemeMode;
16
16
  onToggle: () => void;
17
+ /** Default: `topBar.theme` from the {@link UiKitProvider}, else English. */
17
18
  ariaLabel?: string;
18
19
  title?: string;
19
20
  className?: string;
@@ -25,6 +26,7 @@ declare function PaletteMenu({ palettes, activeId, mode, onSelect, ariaLabel, he
25
26
  activeId: string;
26
27
  mode: ThemeMode;
27
28
  onSelect: (id: string) => void;
29
+ /** Default: `topBar.palette` from the {@link UiKitProvider}, else English. */
28
30
  ariaLabel?: string;
29
31
  /** Optional small heading row above the list. */
30
32
  heading?: string;
@@ -48,6 +50,7 @@ declare function LanguageMenu({ options, current, onChange, ariaLabel, }: {
48
50
  options: LanguageOption[];
49
51
  current: string | undefined;
50
52
  onChange: (code: string) => void;
53
+ /** Default: `topBar.language` from the {@link UiKitProvider}, else English. */
51
54
  ariaLabel?: string;
52
55
  }): react.JSX.Element;
53
56
 
@@ -1,10 +1,12 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
2
3
  import { Check, Globe, Moon, Palette, Sun } from "lucide-react";
3
4
  import { cn } from "../lib/cn.js";
4
5
  import { HoverMenu } from "../components/hover-menu.js";
5
6
  import { Tooltip } from "../components/tooltip.js";
6
- const TOPBAR_TRIGGER_CLASS = "size-9 rounded-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800";
7
- const TOPBAR_MENU_ITEM_CLASS = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-slate-700 hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-slate-800";
7
+ import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels.js";
8
+ const TOPBAR_TRIGGER_CLASS = "size-9 rounded-md flex items-center justify-center text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]";
9
+ const TOPBAR_MENU_ITEM_CLASS = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]";
8
10
  function ThemeToggle({
9
11
  mode,
10
12
  onToggle,
@@ -12,13 +14,14 @@ function ThemeToggle({
12
14
  title,
13
15
  className
14
16
  }) {
17
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });
15
18
  const Icon = mode === "dark" ? Moon : Sun;
16
19
  return /* @__PURE__ */ jsx(Tooltip, { label: title, portal: true, children: /* @__PURE__ */ jsx(
17
20
  "button",
18
21
  {
19
22
  type: "button",
20
23
  onClick: onToggle,
21
- "aria-label": ariaLabel,
24
+ "aria-label": labels.theme,
22
25
  className: cn(TOPBAR_TRIGGER_CLASS, className),
23
26
  children: /* @__PURE__ */ jsx(Icon, { className: "size-5" })
24
27
  }
@@ -29,26 +32,27 @@ function PaletteMenu({
29
32
  activeId,
30
33
  mode,
31
34
  onSelect,
32
- ariaLabel = "Appearance preset",
35
+ ariaLabel,
33
36
  heading
34
37
  }) {
38
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabel });
35
39
  const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;
36
40
  return /* @__PURE__ */ jsx(
37
41
  HoverMenu,
38
42
  {
39
- ariaLabel,
43
+ ariaLabel: labels.palette,
40
44
  trigger: ({ toggle }) => /* @__PURE__ */ jsx(Tooltip, { label: activeName, portal: true, children: /* @__PURE__ */ jsx(
41
45
  "button",
42
46
  {
43
47
  type: "button",
44
48
  onClick: toggle,
45
- "aria-label": ariaLabel,
49
+ "aria-label": labels.palette,
46
50
  className: TOPBAR_TRIGGER_CLASS,
47
- children: /* @__PURE__ */ jsx(Palette, { className: "size-5 text-violet-500" })
51
+ children: /* @__PURE__ */ jsx(Palette, { className: "size-5 text-[var(--chart-1)]" })
48
52
  }
49
53
  ) }),
50
54
  children: (close) => /* @__PURE__ */ jsxs("ul", { className: "w-60 py-1", children: [
51
- heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500", children: heading }),
55
+ heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]", children: heading }),
52
56
  palettes.map((p) => {
53
57
  const ts = p[mode];
54
58
  const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];
@@ -73,10 +77,10 @@ function PaletteMenu({
73
77
  )) }),
74
78
  /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col text-left", children: [
75
79
  /* @__PURE__ */ jsx("span", { className: "truncate", children: p.name }),
76
- /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-slate-400 dark:text-slate-500", children: p.blurb })
80
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-[var(--text-placeholder)]", children: p.blurb })
77
81
  ] })
78
82
  ] }),
79
- activeId === p.id && /* @__PURE__ */ jsx(Check, { className: "size-4 shrink-0 text-slate-700 dark:text-slate-200" })
83
+ activeId === p.id && /* @__PURE__ */ jsx(Check, { className: "size-4 shrink-0 text-[var(--text-secondary)]" })
80
84
  ]
81
85
  }
82
86
  ) }, p.id);
@@ -89,19 +93,20 @@ function LanguageMenu({
89
93
  options,
90
94
  current,
91
95
  onChange,
92
- ariaLabel = "Language"
96
+ ariaLabel
93
97
  }) {
98
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });
94
99
  const active = options.find((o) => o.code === current);
95
100
  return /* @__PURE__ */ jsx(
96
101
  HoverMenu,
97
102
  {
98
- ariaLabel,
99
- trigger: ({ toggle }) => /* @__PURE__ */ jsx(Tooltip, { label: active?.label ?? ariaLabel, portal: true, children: /* @__PURE__ */ jsx(
103
+ ariaLabel: labels.language,
104
+ trigger: ({ toggle }) => /* @__PURE__ */ jsx(Tooltip, { label: active?.label ?? labels.language, portal: true, children: /* @__PURE__ */ jsx(
100
105
  "button",
101
106
  {
102
107
  type: "button",
103
108
  onClick: toggle,
104
- "aria-label": ariaLabel,
109
+ "aria-label": labels.language,
105
110
  className: TOPBAR_TRIGGER_CLASS,
106
111
  children: active ? /* @__PURE__ */ jsx(
107
112
  "span",
@@ -134,7 +139,7 @@ function LanguageMenu({
134
139
  ),
135
140
  lang.label
136
141
  ] }),
137
- isActive && /* @__PURE__ */ jsx(Check, { className: "size-4 text-slate-700 dark:text-slate-200" })
142
+ isActive && /* @__PURE__ */ jsx(Check, { className: "size-4 text-[var(--text-secondary)]" })
138
143
  ]
139
144
  }
140
145
  ) }, lang.code);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/topbar-controls.tsx"],"sourcesContent":["import { Check, Globe, Moon, Palette, Sun } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport type { PalettePreset } from \"../theme/palette-presets\";\nimport type { ThemeMode } from \"../theme/theme-store\";\nimport { Tooltip } from \"../components/tooltip\";\n\n/** Shared square icon-button styling for top-bar triggers, so app-owned controls\n * (account/feedback menus) line up with the shared ones. */\nexport const TOPBAR_TRIGGER_CLASS =\n \"size-9 rounded-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800\";\n\n/** Shared row styling for the top-bar hover-menu items. */\nexport const TOPBAR_MENU_ITEM_CLASS =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-slate-700 hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-slate-800\";\n\n/** Sun/Moon toggle for the light/dark theme. Controlled: the app owns the mode. */\nexport function ThemeToggle({\n mode,\n onToggle,\n ariaLabel,\n title,\n className,\n}: {\n mode: ThemeMode;\n onToggle: () => void;\n ariaLabel?: string;\n title?: string;\n className?: string;\n}) {\n const Icon = mode === \"dark\" ? Moon : Sun;\n return (\n <Tooltip label={title} portal>\n <button\n type=\"button\"\n onClick={onToggle}\n aria-label={ariaLabel}\n className={cn(TOPBAR_TRIGGER_CLASS, className)}\n >\n <Icon className=\"size-5\" />\n </button>\n </Tooltip>\n );\n}\n\n/** Appearance-preset switcher: a hover menu of palette presets with per-theme\n * colour swatches. Controlled — the app owns the active id and applies it. */\nexport function PaletteMenu({\n palettes,\n activeId,\n mode,\n onSelect,\n ariaLabel = \"Appearance preset\",\n heading,\n}: {\n palettes: PalettePreset[];\n activeId: string;\n mode: ThemeMode;\n onSelect: (id: string) => void;\n ariaLabel?: string;\n /** Optional small heading row above the list. */\n heading?: string;\n}) {\n const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <Tooltip label={activeName} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={ariaLabel}\n className={TOPBAR_TRIGGER_CLASS}\n >\n <Palette className=\"size-5 text-violet-500\" />\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"w-60 py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500\">\n {heading}\n </li>\n )}\n {palettes.map((p) => {\n const ts = p[mode];\n const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];\n return (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(p.id);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex shrink-0 gap-0.5\">\n {swatches.map((c, i) => (\n <span\n key={i}\n className=\"size-3.5 rounded-sm ring-1 ring-inset ring-black/10 dark:ring-white/10\"\n style={{ background: c }}\n />\n ))}\n </span>\n <span className=\"flex min-w-0 flex-col text-left\">\n <span className=\"truncate\">{p.name}</span>\n <span className=\"truncate text-[11px] text-slate-400 dark:text-slate-500\">\n {p.blurb}\n </span>\n </span>\n </span>\n {activeId === p.id && (\n <Check className=\"size-4 shrink-0 text-slate-700 dark:text-slate-200\" />\n )}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n\nexport interface LanguageOption {\n code: string;\n label: string;\n /** ISO 3166-1 alpha-2 country code (lowercase) for the flag-icons flag. */\n country: string;\n}\n\n/** Language switcher: a hover menu of options with flags and a check on the\n * active one. Controlled — the app owns the current code and change handler.\n *\n * The trigger is the *active* option's flag, not an icon: lucide's `Languages`\n * glyph (文A) is the same mark Chrome puts in the omnibox for \"translate this\n * page\", so it read as machine-translation rather than \"pick the UI language\"\n * (Keksdose feedback #121). The flag also shows which language is currently on,\n * which no icon can, and costs the same width on a crowded mobile bar. A globe\n * covers the case where `current` matches nothing in `options`. */\nexport function LanguageMenu({\n options,\n current,\n onChange,\n ariaLabel = \"Language\",\n}: {\n options: LanguageOption[];\n current: string | undefined;\n onChange: (code: string) => void;\n ariaLabel?: string;\n}) {\n const active = options.find((o) => o.code === current);\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <Tooltip label={active?.label ?? ariaLabel} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={ariaLabel}\n className={TOPBAR_TRIGGER_CLASS}\n >\n {active ? (\n <span\n aria-hidden\n className={`fi fi-${active.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}\n />\n ) : (\n <Globe className=\"size-5\" />\n )}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {options.map((lang) => {\n const isActive = lang.code === current;\n return (\n <li key={lang.code}>\n <button\n type=\"button\"\n onClick={() => {\n onChange(lang.code);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex items-center gap-2\">\n <span\n aria-hidden\n className={`fi fi-${lang.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}\n />\n {lang.label}\n </span>\n {isActive && <Check className=\"size-4 text-slate-700 dark:text-slate-200\" />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/topbar-controls.tsx"],"sourcesContent":["import { Check, Globe, Moon, Palette, Sun } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport type { PalettePreset } from \"../theme/palette-presets\";\nimport type { ThemeMode } from \"../theme/theme-store\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/** Shared square icon-button styling for top-bar triggers, so app-owned controls\n * (account/feedback menus) line up with the shared ones. */\nexport const TOPBAR_TRIGGER_CLASS =\n \"size-9 rounded-md flex items-center justify-center text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\";\n\n/** Shared row styling for the top-bar hover-menu items. */\nexport const TOPBAR_MENU_ITEM_CLASS =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\";\n\n/** Sun/Moon toggle for the light/dark theme. Controlled: the app owns the mode. */\nexport function ThemeToggle({\n mode,\n onToggle,\n ariaLabel,\n title,\n className,\n}: {\n mode: ThemeMode;\n onToggle: () => void;\n /** Default: `topBar.theme` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n title?: string;\n className?: string;\n}) {\n // This had no default at all, so a toggle rendered without `ariaLabel` was an\n // icon button with no name — a reader heard \"button\" and nothing else.\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });\n const Icon = mode === \"dark\" ? Moon : Sun;\n return (\n <Tooltip label={title} portal>\n <button\n type=\"button\"\n onClick={onToggle}\n aria-label={labels.theme}\n className={cn(TOPBAR_TRIGGER_CLASS, className)}\n >\n <Icon className=\"size-5\" />\n </button>\n </Tooltip>\n );\n}\n\n/** Appearance-preset switcher: a hover menu of palette presets with per-theme\n * colour swatches. Controlled — the app owns the active id and applies it. */\nexport function PaletteMenu({\n palettes,\n activeId,\n mode,\n onSelect,\n ariaLabel,\n heading,\n}: {\n palettes: PalettePreset[];\n activeId: string;\n mode: ThemeMode;\n onSelect: (id: string) => void;\n /** Default: `topBar.palette` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n /** Optional small heading row above the list. */\n heading?: string;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabel });\n const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;\n return (\n <HoverMenu\n ariaLabel={labels.palette}\n trigger={({ toggle }) => (\n <Tooltip label={activeName} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={labels.palette}\n className={TOPBAR_TRIGGER_CLASS}\n >\n {/* A DATA colour, not a semantic one: the palette mark is the\n \"colour\" control, so it carries a colour, and which hue is\n arbitrary. `--chart-1` is the violet end of the CVD-safe\n categorical ramp and flips per theme — a fixed `violet-500` did\n not, and stayed a cool violet island on a warm page. */}\n <Palette className=\"size-5 text-[var(--chart-1)]\" />\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"w-60 py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\">\n {heading}\n </li>\n )}\n {palettes.map((p) => {\n const ts = p[mode];\n const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];\n return (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(p.id);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex shrink-0 gap-0.5\">\n {/* Deliberately a neutral at 10% and not a token: this hairline\n sits on an ARBITRARY preset colour, so it has to read against\n whatever that preset paints, which no themed border does. */}\n {swatches.map((c, i) => (\n <span\n key={i}\n className=\"size-3.5 rounded-sm ring-1 ring-inset ring-black/10 dark:ring-white/10\"\n style={{ background: c }}\n />\n ))}\n </span>\n <span className=\"flex min-w-0 flex-col text-left\">\n <span className=\"truncate\">{p.name}</span>\n <span className=\"truncate text-[11px] text-[var(--text-placeholder)]\">\n {p.blurb}\n </span>\n </span>\n </span>\n {activeId === p.id && (\n <Check className=\"size-4 shrink-0 text-[var(--text-secondary)]\" />\n )}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n\nexport interface LanguageOption {\n code: string;\n label: string;\n /** ISO 3166-1 alpha-2 country code (lowercase) for the flag-icons flag. */\n country: string;\n}\n\n/** Language switcher: a hover menu of options with flags and a check on the\n * active one. Controlled — the app owns the current code and change handler.\n *\n * The trigger is the *active* option's flag, not an icon: lucide's `Languages`\n * glyph (文A) is the same mark Chrome puts in the omnibox for \"translate this\n * page\", so it read as machine-translation rather than \"pick the UI language\"\n * (Keksdose feedback #121). The flag also shows which language is currently on,\n * which no icon can, and costs the same width on a crowded mobile bar. A globe\n * covers the case where `current` matches nothing in `options`. */\nexport function LanguageMenu({\n options,\n current,\n onChange,\n ariaLabel,\n}: {\n options: LanguageOption[];\n current: string | undefined;\n onChange: (code: string) => void;\n /** Default: `topBar.language` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });\n const active = options.find((o) => o.code === current);\n return (\n <HoverMenu\n ariaLabel={labels.language}\n trigger={({ toggle }) => (\n <Tooltip label={active?.label ?? labels.language} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={labels.language}\n className={TOPBAR_TRIGGER_CLASS}\n >\n {active ? (\n <span\n aria-hidden\n className={`fi fi-${active.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}\n />\n ) : (\n <Globe className=\"size-5\" />\n )}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {options.map((lang) => {\n const isActive = lang.code === current;\n return (\n <li key={lang.code}>\n <button\n type=\"button\"\n onClick={() => {\n onChange(lang.code);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex items-center gap-2\">\n <span\n aria-hidden\n className={`fi fi-${lang.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}\n />\n {lang.label}\n </span>\n {isActive && <Check className=\"size-4 text-[var(--text-secondary)]\" />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,13 @@
1
+ export { LanguageMenu, LanguageOption, PaletteMenu, TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS, ThemeToggle } from './shell/topbar-controls.js';
2
+ export { TopBar, TopBarProps } from './shell/top-bar.js';
3
+ export { AppShell, AppShellNavItem, AppShellProps, AppShellSubItem } from './shell/app-shell.js';
4
+ export { OptionSwitcherMenu, OptionSwitcherOption } from './shell/option-switcher-menu.js';
5
+ export { RoleSwitcher, RoleSwitcherProps } from './shell/role-switcher.js';
6
+ export { TopBarActionMenu, TopBarMenuEntry } from './shell/topbar-action-menu.js';
7
+ import 'react';
8
+ import './theme/palette-presets.js';
9
+ import './theme/chart-palette.js';
10
+ import './theme/theme-store.js';
11
+ import 'zustand/middleware';
12
+ import 'zustand';
13
+ import 'lucide-react';
package/dist/shell.js ADDED
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ export * from "./shell/topbar-controls.js";
3
+ export * from "./shell/top-bar.js";
4
+ export * from "./shell/app-shell.js";
5
+ export * from "./shell/option-switcher-menu.js";
6
+ export * from "./shell/role-switcher.js";
7
+ export * from "./shell/topbar-action-menu.js";
8
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
@@ -27,34 +27,41 @@ interface HeatStops {
27
27
  heatmap stops, per theme. Teal = under/good, amber = over/spend = bad. */
28
28
  declare const HEATMAP_HEX: {
29
29
  readonly light: {
30
- readonly neutral: "#f1f5f9";
31
- readonly under: "#0f766e";
32
- readonly over: "#b45309";
33
- readonly seqLow: "#fef3c7";
34
- readonly seqHigh: "#b45309";
30
+ readonly neutral: "#f1e7d2";
31
+ readonly under: "#0b6650";
32
+ readonly over: "#9c6418";
33
+ readonly seqLow: "#f6ecd0";
34
+ readonly seqHigh: "#9c6418";
35
35
  readonly empty: "rgba(148,163,184,0.14)";
36
36
  };
37
37
  readonly dark: {
38
- readonly neutral: "#334155";
39
- readonly under: "#2dd4bf";
38
+ readonly neutral: "#363a5a";
39
+ readonly under: "#22c3b6";
40
40
  readonly over: "#fbbf24";
41
- readonly seqLow: "#3f2d10";
41
+ readonly seqLow: "#3a2c14";
42
42
  readonly seqHigh: "#fbbf24";
43
43
  readonly empty: "rgba(148,163,184,0.10)";
44
44
  };
45
45
  };
46
46
  /** Linear interpolate two #rrggbb hexes; `t` in [0,1]. Returns #rrggbb. */
47
47
  declare function lerpHex(a: string, b: string, t: number): string;
48
- /** Pick a legible text colour (near-black / near-white) for a solid hex fill,
49
- flipping by perceived luminance so cell/treemap labels keep contrast.
48
+ /** Pick a legible text colour (near-black / near-white) for a solid hex fill, by
49
+ MEASURING both against it and keeping the one that wins.
50
+
51
+ This used to flip on Rec.601 luma > 0.6, which is a brightness heuristic and not a
52
+ contrast measure — the two disagree on six of the eighteen default chart hues, a
53
+ third of the ramp, and on each of those the label got the worse of the two inks.
54
+ `#44aa99` is the plainest case: luma calls it dark, so it took white, which measures
55
+ 2.69:1 on it; near-black measures 6.18:1. keksdose measured this too and forked the
56
+ function locally for its treemap, leaving its other two call sites on this one.
50
57
 
51
58
  A fill this cannot read gets `currentColor` — inherit, i.e. whatever the
52
59
  surrounding text already uses. That is the only answer that is right on both
53
60
  themes, because an unreadable fill is usually a translucent one (`HEATMAP_HEX.*
54
61
  .empty` is an `rgba()` string, and the variance heatmap does pass it here for any
55
62
  category with zero assigned and positive activity — a refund). Picking a constant
56
- instead is what produced the bug: `NaN > 0.6` is false, so the label came back
57
- near-white on a near-white cell and the figure was invisible. */
63
+ instead is what produced the earlier bug: the unparsed fill fell through to the
64
+ light ink, near-white on a near-white cell, and the figure was invisible. */
58
65
  declare function textOn(hex: string): string;
59
66
 
60
67
  export { CHART_COLORS, HEATMAP_HEX, type HeatStops, PALETTE_HEX, lerpHex, paletteFor, textOn };
@@ -1,3 +1,5 @@
1
+ "use client";
2
+ import { contrast, parseHex as parseRgbHex } from "./color.js";
1
3
  const PALETTE_SIZE = 9;
2
4
  function paletteFor(index) {
3
5
  return `var(--chart-${index % PALETTE_SIZE + 1})`;
@@ -15,29 +17,27 @@ const PALETTE_HEX = {
15
17
  };
16
18
  const HEATMAP_HEX = {
17
19
  light: {
18
- neutral: "#f1f5f9",
19
- // slate-100
20
- under: "#0f766e",
21
- // teal-700
22
- over: "#b45309",
23
- // amber-700
24
- seqLow: "#fef3c7",
25
- // amber-100
26
- seqHigh: "#b45309",
27
- // amber-700
20
+ neutral: "#f1e7d2",
21
+ // the cream surface itself, not a cool grey
22
+ under: "#0b6650",
23
+ // DEFAULT_PRESET.light.moneyIncome
24
+ over: "#9c6418",
25
+ // DEFAULT_PRESET.light.moneyExpense, warmed to the page
26
+ seqLow: "#f6ecd0",
27
+ seqHigh: "#9c6418",
28
28
  empty: "rgba(148,163,184,0.14)"
29
29
  },
30
30
  dark: {
31
- neutral: "#334155",
32
- // slate-700
33
- under: "#2dd4bf",
34
- // teal-400
31
+ neutral: "#363a5a",
32
+ // one step above --border on the indigo-noir surface
33
+ under: "#22c3b6",
34
+ // DEFAULT_PRESET.dark.moneyIncome
35
+ // Brighter than dark.moneyExpense (#e4b035) on purpose: a gradient endpoint is
36
+ // meant to be the loudest thing in its scale, and it is a fill, not a figure.
35
37
  over: "#fbbf24",
36
- // amber-400
37
- seqLow: "#3f2d10",
38
- // dim amber, sits just above slate-950
38
+ seqLow: "#3a2c14",
39
+ // dim amber, sits just above the page
39
40
  seqHigh: "#fbbf24",
40
- // amber-400
41
41
  empty: "rgba(148,163,184,0.10)"
42
42
  }
43
43
  };
@@ -47,23 +47,17 @@ function lerpHex(a, b, t) {
47
47
  const c = (i) => Math.round(pa[i] + (pb[i] - pa[i]) * clamp01(t));
48
48
  return `#${[c(0), c(1), c(2)].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
49
49
  }
50
+ const INK_DARK = "#1a1a1a";
51
+ const INK_LIGHT = "#f8fafc";
50
52
  function textOn(hex) {
51
- const rgb = parseHex(hex);
53
+ const rgb = parseRgbHex(hex);
52
54
  if (!rgb) return "currentColor";
53
- const [r, g, b] = rgb;
54
- const luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
55
- return luma > 0.6 ? "#1a1a1a" : "#f8fafc";
55
+ return contrast(rgb, INK_DARK) >= contrast(rgb, INK_LIGHT) ? INK_DARK : INK_LIGHT;
56
56
  }
57
57
  const UNREADABLE = [128, 128, 128];
58
58
  function parseHex(hex) {
59
- const h = hex.startsWith("#") ? hex.slice(1) : hex;
60
- const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
61
- if (!/^[0-9a-fA-F]{6}$/.test(full)) return null;
62
- return [
63
- parseInt(full.slice(0, 2), 16),
64
- parseInt(full.slice(2, 4), 16),
65
- parseInt(full.slice(4, 6), 16)
66
- ];
59
+ const rgb = parseRgbHex(hex);
60
+ return rgb ? [rgb.r, rgb.g, rgb.b] : null;
67
61
  }
68
62
  function clamp01(t) {
69
63
  return t < 0 ? 0 : t > 1 ? 1 : t;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/theme/chart-palette.ts"],"sourcesContent":["// Chart colour system. Single source of truth lives in the design tokens\n// (:root / .dark in tokens.css): brand = indigo, money = teal/amber/purple,\n// chart-1..9 = Paul Tol \"Muted\" (CVD-safe across protan/deutan/tritan).\n// Returning CSS vars here makes every Recharts series theme-aware for free — the\n// SVG fill/stroke resolves --chart-N against the current light/dark token set.\n\nconst PALETTE_SIZE = 9;\n\n/** CVD-safe categorical colour for series `index`, as a theme-aware CSS var. */\nexport function paletteFor(index: number): string {\n return `var(--chart-${(index % PALETTE_SIZE) + 1})`;\n}\n\n/** Semantic money colours, theme-aware via tokens. Pair with +/- and arrow\n cues at the call site so colour is never the only signal (CVD). */\nexport const CHART_COLORS = {\n income: \"var(--money-income)\",\n expense: \"var(--money-expense)\",\n net: \"var(--money-net)\",\n assigned: \"var(--brand)\",\n activity: \"var(--money-expense)\",\n} as const;\n\n// ── Raw hex stops, by theme ────────────────────────────────────────────────\n// The heatmaps interpolate colours in JS (CSS vars can't be lerped), so they\n// need concrete hex per theme. Keep these in sync with the tokens.\nexport const PALETTE_HEX = {\n light: [\"#332288\", \"#88ccee\", \"#44aa99\", \"#117733\", \"#999933\", \"#ddcc77\", \"#cc6677\", \"#882255\", \"#aa4499\"],\n dark: [\"#7e72d6\", \"#9fd8f2\", \"#5fc4b0\", \"#3fa45f\", \"#bfbf5e\", \"#e8dda0\", \"#e08c9a\", \"#c25a78\", \"#cc78be\"],\n} as const;\n\n/** A heatmap's interpolation stops for one theme. Open string fields so any\n palette preset (not just the built-in default) can supply its own stops. */\nexport interface HeatStops {\n neutral: string;\n under: string;\n over: string;\n seqLow: string;\n seqHigh: string;\n empty: string;\n}\n\n/** Diverging (variance: under←0→over) and sequential (spending intensity)\n heatmap stops, per theme. Teal = under/good, amber = over/spend = bad. */\nexport const HEATMAP_HEX = {\n light: {\n neutral: \"#f1f5f9\", // slate-100\n under: \"#0f766e\", // teal-700\n over: \"#b45309\", // amber-700\n seqLow: \"#fef3c7\", // amber-100\n seqHigh: \"#b45309\", // amber-700\n empty: \"rgba(148,163,184,0.14)\",\n },\n dark: {\n neutral: \"#334155\", // slate-700\n under: \"#2dd4bf\", // teal-400\n over: \"#fbbf24\", // amber-400\n seqLow: \"#3f2d10\", // dim amber, sits just above slate-950\n seqHigh: \"#fbbf24\", // amber-400\n empty: \"rgba(148,163,184,0.10)\",\n },\n} as const;\n\n/** Linear interpolate two #rrggbb hexes; `t` in [0,1]. Returns #rrggbb. */\nexport function lerpHex(a: string, b: string, t: number): string {\n const pa = parseHex(a) ?? UNREADABLE;\n const pb = parseHex(b) ?? UNREADABLE;\n const c = (i: number) => Math.round(pa[i] + (pb[i] - pa[i]) * clamp01(t));\n return `#${[c(0), c(1), c(2)].map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/** Pick a legible text colour (near-black / near-white) for a solid hex fill,\n flipping by perceived luminance so cell/treemap labels keep contrast.\n\n A fill this cannot read gets `currentColor` — inherit, i.e. whatever the\n surrounding text already uses. That is the only answer that is right on both\n themes, because an unreadable fill is usually a translucent one (`HEATMAP_HEX.*\n .empty` is an `rgba()` string, and the variance heatmap does pass it here for any\n category with zero assigned and positive activity — a refund). Picking a constant\n instead is what produced the bug: `NaN > 0.6` is false, so the label came back\n near-white on a near-white cell and the figure was invisible. */\nexport function textOn(hex: string): string {\n const rgb = parseHex(hex);\n if (!rgb) return \"currentColor\";\n const [r, g, b] = rgb;\n // Rec.601 luma; >0.6 → dark text, else light text.\n const luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n return luma > 0.6 ? \"#1a1a1a\" : \"#f8fafc\";\n}\n\n/** Mid-grey, for a stop {@link lerpHex} has to interpolate but cannot read. It has\n * to return SOME valid colour, and a grey is the one that misleads least. */\nconst UNREADABLE: [number, number, number] = [0x80, 0x80, 0x80];\n\n/**\n * `#rgb` or `#rrggbb` to components, or **null** when it is neither.\n *\n * This used to `parseInt` whatever it was given and hand the NaNs on, and both\n * callers propagated them silently: {@link lerpHex} produced the literal string\n * `\"#nannannan\"`, and {@link textOn} computed `NaN > 0.6 === false` and returned the\n * LIGHT label colour.\n *\n * That is not hypothetical — a non-hex value ships in this very module.\n * `HEATMAP_HEX.*.empty` is an `rgba(…)` string, and the variance heatmap paints a\n * figure on an `empty` cell whenever a category has zero assigned and positive\n * activity (a refund). On the light theme the cell is near-white and the label came\n * back near-white: the number was there, and unreadable.\n */\nfunction parseHex(hex: string): [number, number, number] | null {\n const h = hex.startsWith(\"#\") ? hex.slice(1) : hex;\n const full =\n h.length === 3\n ? h\n .split(\"\")\n .map((c) => c + c)\n .join(\"\")\n : h;\n if (!/^[0-9a-fA-F]{6}$/.test(full)) return null;\n return [\n parseInt(full.slice(0, 2), 16),\n parseInt(full.slice(2, 4), 16),\n parseInt(full.slice(4, 6), 16),\n ];\n}\n\nfunction clamp01(t: number): number {\n return t < 0 ? 0 : t > 1 ? 1 : t;\n}\n"],"mappings":"AAMA,MAAM,eAAe;AAGd,SAAS,WAAW,OAAuB;AAChD,SAAO,eAAgB,QAAQ,eAAgB,CAAC;AAClD;AAIO,MAAM,eAAe;AAAA,EAC1B,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AAKO,MAAM,cAAc;AAAA,EACzB,OAAO,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAAA,EACzG,MAAM,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAC1G;AAeO,MAAM,cAAc;AAAA,EACzB,OAAO;AAAA,IACL,SAAS;AAAA;AAAA,IACT,OAAO;AAAA;AAAA,IACP,MAAM;AAAA;AAAA,IACN,QAAQ;AAAA;AAAA,IACR,SAAS;AAAA;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,SAAS;AAAA;AAAA,IACT,OAAO;AAAA;AAAA,IACP,MAAM;AAAA;AAAA,IACN,QAAQ;AAAA;AAAA,IACR,SAAS;AAAA;AAAA,IACT,OAAO;AAAA,EACT;AACF;AAGO,SAAS,QAAQ,GAAW,GAAW,GAAmB;AAC/D,QAAM,KAAK,SAAS,CAAC,KAAK;AAC1B,QAAM,KAAK,SAAS,CAAC,KAAK;AAC1B,QAAM,IAAI,CAAC,MAAc,KAAK,MAAM,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC;AACxE,SAAO,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;AACpF;AAYO,SAAS,OAAO,KAAqB;AAC1C,QAAM,MAAM,SAAS,GAAG;AACxB,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAElB,QAAM,QAAQ,QAAQ,IAAI,QAAQ,IAAI,QAAQ,KAAK;AACnD,SAAO,OAAO,MAAM,YAAY;AAClC;AAIA,MAAM,aAAuC,CAAC,KAAM,KAAM,GAAI;AAgB9D,SAAS,SAAS,KAA8C;AAC9D,QAAM,IAAI,IAAI,WAAW,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI;AAC/C,QAAM,OACJ,EAAE,WAAW,IACT,EACG,MAAM,EAAE,EACR,IAAI,CAAC,MAAM,IAAI,CAAC,EAChB,KAAK,EAAE,IACV;AACN,MAAI,CAAC,mBAAmB,KAAK,IAAI,EAAG,QAAO;AAC3C,SAAO;AAAA,IACL,SAAS,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE;AAAA,IAC7B,SAAS,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE;AAAA,IAC7B,SAAS,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE;AAAA,EAC/B;AACF;AAEA,SAAS,QAAQ,GAAmB;AAClC,SAAO,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;AACjC;","names":[]}
1
+ {"version":3,"sources":["../../src/theme/chart-palette.ts"],"sourcesContent":["// Chart colour system. Single source of truth lives in the design tokens\n// (:root / .dark in tokens.css): brand = indigo, money = teal/amber/purple,\n// chart-1..9 = Paul Tol \"Muted\" (CVD-safe across protan/deutan/tritan).\n// Returning CSS vars here makes every Recharts series theme-aware for free — the\n// SVG fill/stroke resolves --chart-N against the current light/dark token set.\n\nimport { contrast, parseHex as parseRgbHex } from \"./color\";\n\nconst PALETTE_SIZE = 9;\n\n/** CVD-safe categorical colour for series `index`, as a theme-aware CSS var. */\nexport function paletteFor(index: number): string {\n return `var(--chart-${(index % PALETTE_SIZE) + 1})`;\n}\n\n/** Semantic money colours, theme-aware via tokens. Pair with +/- and arrow\n cues at the call site so colour is never the only signal (CVD). */\nexport const CHART_COLORS = {\n income: \"var(--money-income)\",\n expense: \"var(--money-expense)\",\n net: \"var(--money-net)\",\n assigned: \"var(--brand)\",\n activity: \"var(--money-expense)\",\n} as const;\n\n// ── Raw hex stops, by theme ────────────────────────────────────────────────\n// The heatmaps interpolate colours in JS (CSS vars can't be lerped), so they\n// need concrete hex per theme — i.e. the same colours DEFAULT_PRESET already\n// holds, written down a second time.\n//\n// They are duplicated rather than imported because these two objects are the whole\n// of the `@eifi1/ui-kit/chart` subpath's colour layer, and importing the preset bank\n// would pull every preset's full token set into that chunk to read two of them. What\n// keeps the copies together is `chart-palette.test.ts` — \"has not drifted from\n// DEFAULT_PRESET\" — which is not optional: they HAD drifted, five of the six light\n// heat stops, and the light `neutral` was still Tailwind's slate-100, a cool grey on\n// the warm cream page this theme has used since #328.\nexport const PALETTE_HEX = {\n light: [\"#332288\", \"#88ccee\", \"#44aa99\", \"#117733\", \"#999933\", \"#ddcc77\", \"#cc6677\", \"#882255\", \"#aa4499\"],\n dark: [\"#7e72d6\", \"#9fd8f2\", \"#5fc4b0\", \"#3fa45f\", \"#bfbf5e\", \"#e8dda0\", \"#e08c9a\", \"#c25a78\", \"#cc78be\"],\n} as const;\n\n/** A heatmap's interpolation stops for one theme. Open string fields so any\n palette preset (not just the built-in default) can supply its own stops. */\nexport interface HeatStops {\n neutral: string;\n under: string;\n over: string;\n seqLow: string;\n seqHigh: string;\n empty: string;\n}\n\n/** Diverging (variance: under←0→over) and sequential (spending intensity)\n heatmap stops, per theme. Teal = under/good, amber = over/spend = bad. */\nexport const HEATMAP_HEX = {\n light: {\n neutral: \"#f1e7d2\", // the cream surface itself, not a cool grey\n under: \"#0b6650\", // DEFAULT_PRESET.light.moneyIncome\n over: \"#9c6418\", // DEFAULT_PRESET.light.moneyExpense, warmed to the page\n seqLow: \"#f6ecd0\",\n seqHigh: \"#9c6418\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n dark: {\n neutral: \"#363a5a\", // one step above --border on the indigo-noir surface\n under: \"#22c3b6\", // DEFAULT_PRESET.dark.moneyIncome\n // Brighter than dark.moneyExpense (#e4b035) on purpose: a gradient endpoint is\n // meant to be the loudest thing in its scale, and it is a fill, not a figure.\n over: \"#fbbf24\",\n seqLow: \"#3a2c14\", // dim amber, sits just above the page\n seqHigh: \"#fbbf24\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n} as const;\n\n/** Linear interpolate two #rrggbb hexes; `t` in [0,1]. Returns #rrggbb. */\nexport function lerpHex(a: string, b: string, t: number): string {\n const pa = parseHex(a) ?? UNREADABLE;\n const pb = parseHex(b) ?? UNREADABLE;\n const c = (i: number) => Math.round(pa[i] + (pb[i] - pa[i]) * clamp01(t));\n return `#${[c(0), c(1), c(2)].map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/** The two inks a cell/treemap label can be set in. Near-black and near-white rather\n than #000/#fff: at full black-on-full-white the label stops looking like part of the\n same type system as the rest of the page. */\nconst INK_DARK = \"#1a1a1a\";\nconst INK_LIGHT = \"#f8fafc\";\n\n/** Pick a legible text colour (near-black / near-white) for a solid hex fill, by\n MEASURING both against it and keeping the one that wins.\n\n This used to flip on Rec.601 luma > 0.6, which is a brightness heuristic and not a\n contrast measure — the two disagree on six of the eighteen default chart hues, a\n third of the ramp, and on each of those the label got the worse of the two inks.\n `#44aa99` is the plainest case: luma calls it dark, so it took white, which measures\n 2.69:1 on it; near-black measures 6.18:1. keksdose measured this too and forked the\n function locally for its treemap, leaving its other two call sites on this one.\n\n A fill this cannot read gets `currentColor` — inherit, i.e. whatever the\n surrounding text already uses. That is the only answer that is right on both\n themes, because an unreadable fill is usually a translucent one (`HEATMAP_HEX.*\n .empty` is an `rgba()` string, and the variance heatmap does pass it here for any\n category with zero assigned and positive activity — a refund). Picking a constant\n instead is what produced the earlier bug: the unparsed fill fell through to the\n light ink, near-white on a near-white cell, and the figure was invisible. */\nexport function textOn(hex: string): string {\n const rgb = parseRgbHex(hex);\n if (!rgb) return \"currentColor\";\n // Ties go to the dark ink: at equal measured contrast it is the one that keeps a\n // small label looking like type rather than like a glow.\n return contrast(rgb, INK_DARK) >= contrast(rgb, INK_LIGHT) ? INK_DARK : INK_LIGHT;\n}\n\n/** Mid-grey, for a stop {@link lerpHex} has to interpolate but cannot read. It has\n * to return SOME valid colour, and a grey is the one that misleads least. */\nconst UNREADABLE: [number, number, number] = [0x80, 0x80, 0x80];\n\n/**\n * `#rgb` or `#rrggbb` to components, or **null** when it is neither.\n *\n * A tuple-shaped shim over `color.ts`'s parser, which is the same function and is\n * already the one `contrast` validates with — so {@link lerpHex} and {@link textOn}\n * cannot disagree about what counts as a colour.\n *\n * The **null** is the whole point of it. This used to `parseInt` whatever it was given\n * and hand the NaNs on, and both callers propagated them silently: {@link lerpHex}\n * produced the literal string `\"#nannannan\"`, and {@link textOn} fell through to the\n * LIGHT label colour.\n *\n * That is not hypothetical — a non-hex value ships in this very module.\n * `HEATMAP_HEX.*.empty` is an `rgba(…)` string, and the variance heatmap paints a\n * figure on an `empty` cell whenever a category has zero assigned and positive\n * activity (a refund). On the light theme the cell is near-white and the label came\n * back near-white: the number was there, and unreadable.\n */\nfunction parseHex(hex: string): [number, number, number] | null {\n const rgb = parseRgbHex(hex);\n return rgb ? [rgb.r, rgb.g, rgb.b] : null;\n}\n\nfunction clamp01(t: number): number {\n return t < 0 ? 0 : t > 1 ? 1 : t;\n}\n"],"mappings":";AAMA,SAAS,UAAU,YAAY,mBAAmB;AAElD,MAAM,eAAe;AAGd,SAAS,WAAW,OAAuB;AAChD,SAAO,eAAgB,QAAQ,eAAgB,CAAC;AAClD;AAIO,MAAM,eAAe;AAAA,EAC1B,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AAcO,MAAM,cAAc;AAAA,EACzB,OAAO,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAAA,EACzG,MAAM,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAC1G;AAeO,MAAM,cAAc;AAAA,EACzB,OAAO;AAAA,IACL,SAAS;AAAA;AAAA,IACT,OAAO;AAAA;AAAA,IACP,MAAM;AAAA;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,SAAS;AAAA;AAAA,IACT,OAAO;AAAA;AAAA;AAAA;AAAA,IAGP,MAAM;AAAA,IACN,QAAQ;AAAA;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AACF;AAGO,SAAS,QAAQ,GAAW,GAAW,GAAmB;AAC/D,QAAM,KAAK,SAAS,CAAC,KAAK;AAC1B,QAAM,KAAK,SAAS,CAAC,KAAK;AAC1B,QAAM,IAAI,CAAC,MAAc,KAAK,MAAM,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC;AACxE,SAAO,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;AACpF;AAKA,MAAM,WAAW;AACjB,MAAM,YAAY;AAmBX,SAAS,OAAO,KAAqB;AAC1C,QAAM,MAAM,YAAY,GAAG;AAC3B,MAAI,CAAC,IAAK,QAAO;AAGjB,SAAO,SAAS,KAAK,QAAQ,KAAK,SAAS,KAAK,SAAS,IAAI,WAAW;AAC1E;AAIA,MAAM,aAAuC,CAAC,KAAM,KAAM,GAAI;AAoB9D,SAAS,SAAS,KAA8C;AAC9D,QAAM,MAAM,YAAY,GAAG;AAC3B,SAAO,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,IAAI;AACvC;AAEA,SAAS,QAAQ,GAAmB;AAClC,SAAO,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;AACjC;","names":[]}