@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
@@ -20,7 +20,7 @@ function ButtonLink({
20
20
  replace,
21
21
  reloadDocument,
22
22
  disabled,
23
- variant = "primary",
23
+ variant = "default",
24
24
  size = "md",
25
25
  stretch,
26
26
  tone = "default",
@@ -86,7 +86,7 @@ function ButtonLink({
86
86
  );
87
87
  }
88
88
  function ButtonElement({
89
- variant = "primary",
89
+ variant = "default",
90
90
  size = "md",
91
91
  stretch,
92
92
  tone = "default",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/button.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer, replacingClick, routerLinkNavigation } from \"./text-link\";\nimport { useCommitReason } from \"./write-lock\";\nimport type { CommitScope } from \"./write-lock\";\nimport { hasContent } from \"../lib/presence\";\nimport { LockedReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport { Spinner } from \"./spinner\";\nimport { BUTTON_LINK_PRESSED, ICON_BUTTON_PRESSED, RenderedButtonLink, buttonLook } from \"./button-look\";\nimport type { ButtonSize, ButtonTone, ButtonVariant } from \"./button-look\";\n\n// The kit's text button, and `<Button href>`, the link that looks like one. Its look —\n// the variant, size and tone maps, and `buttonClasses` — is ./button-look, which\n// IconButton shares (0.34, MOD-2, refactor audit 2026-10).\n\n/**\n * ⚠️ No default `type`: like a native `<button>`, a `Button` inside a `<form>` SUBMITS\n * it unless you pass `type=\"button\"`. Kept native on purpose — a form's own submit\n * button relies on it, and changing the default would silently stop those forms from\n * submitting. Pass `type=\"button\"` for every other action in a form (keksdose had six\n * link buttons submitting their form).\n */\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: ButtonVariant;\n /** `md` (default) is the page's action button. `sm` is the compact one — 12px text\n * and `px-2 py-1` — for the secondary actions in a toolbar, a card header or a\n * table's header row (keksdose writes `px-2 py-1 text-xs` over `secondary` by hand\n * there). Every variant takes either size. */\n size?: ButtonSize;\n /** In a flex row next to a taller labelled field, fill the field's height so the\n * two line up. No effect outside a flex row. */\n stretch?: boolean;\n /** The `<button>` element. React 19 passes `ref` to a function component as an\n * ordinary prop, so it rides `...rest` onto the element with no `forwardRef` —\n * declared here only because `ButtonHTMLAttributes` does not carry it. */\n ref?: Ref<HTMLButtonElement>;\n /** Text colour for `link` and `ghost`: `muted` (quiet, body colour on hover) or\n * `danger` (quiet, `--danger` on hover). `danger` also makes `secondary` an\n * outlined destructive button and `primary` a soft one; ignored by the other\n * variants. See {@link ButtonTone}. */\n tone?: ButtonTone;\n /**\n * Make it a toggle button, as {@link IconButton}'s `pressed` does. `true` sets\n * `aria-pressed=\"true\"` and draws the \"on\" look — on a `link`, the brand colour and a\n * medium weight; on the boxed variants, the quiet brand fill IconButton uses — and\n * `false` sets `aria-pressed=\"false\"` with the ordinary look. Left out, no\n * `aria-pressed` (or the caller's own). For lenkbank's \"All speeds\" text toggle\n * (gear/hysteresis-charts.tsx:482), which is a hand-rolled `<button aria-pressed>`\n * swapping two class strings; with `variant=\"link\" tone=\"muted\"` it is this.\n */\n pressed?: boolean;\n /**\n * Why the action is not available — the button's half of DangerConfirm's\n * `lockedReason` (kastlan handover-detail-page.tsx:197 locks a signed handover).\n *\n * A disabled button that cannot say why is a dead end: the native `disabled` takes it\n * out of the tab order, so a keyboard user never lands on it, and a pointer gets a\n * `not-allowed` cursor and nothing else. With a reason the button is `aria-disabled`\n * instead — still focusable, still hoverable — clicks (and the Enter/Space and form\n * submission they stand for) do nothing, and the reason is shown in the kit\n * {@link Tooltip} and attached with `aria-describedby`, so a screen reader hears it\n * on focus. It wins over `disabled`: passing both keeps the button reachable, which is\n * the point of giving a reason.\n *\n * The description is a `hidden` copy of the reason rather than the bubble itself: the\n * bubble exists only while hovered or focused once it is portalled, and a description\n * that comes and goes is read inconsistently. The bubble is kept visual, the way\n * DangerConfirm keeps its own.\n */\n disabledReason?: ReactNode;\n /**\n * Where `disabledReason` shows (0.32, §4 \"No fact only in a tooltip\"): by default a\n * line under the button at Large and on a touch screen, the tooltip otherwise. See\n * {@link DisabledReasonDisplay}.\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This button COMMITS — it saves, creates, deletes. Under a locked\n * {@link WriteLockProvider} it is disabled the `disabledReason` way, with the lock's\n * reason (which wins over a `disabledReason` of its own). No provider, or an\n * unlocked one: no effect. See {@link WriteLockProvider}; a {@link CommitScope} since\n * 0.33 (`COMMIT_EXCEPT_BILLING`: every lock but a lapsed plan's).\n */\n commit?: CommitScope;\n /**\n * Busy — the save is in flight: a spinner, `aria-busy`, and no second submit.\n *\n * kastlan's FormActions (form-actions.tsx:50) disables its submit and swaps in a\n * `submittingLabel`, so the button jumps width mid-click and a screen reader hears\n * the name change instead of \"busy\". Here the label STAYS — it is still the\n * accessible name and still holds the button's width — and the spinner is drawn over\n * it in the text colour, so nothing beside the button moves.\n *\n * The same prop {@link FileButton} has (`pending`), with one difference: this one is\n * `aria-disabled`, not `disabled`. The button being pressed is the one that has focus,\n * and a native `disabled` drops that focus to `<body>` the moment the save starts;\n * clicks, and the Enter that submits a form through it, are swallowed instead.\n */\n pending?: boolean;\n /** Only on the link form — see {@link ButtonLinkProps}. */\n href?: never;\n renderLink?: never;\n external?: never;\n replace?: never;\n reloadDocument?: never;\n}\n\n/**\n * `<Button href>`: a link that looks like a button — for an action that NAVIGATES.\n *\n * kastlan paints `buttonClasses(…)` onto router Links by hand in six places (the\n * invoice / lease / unit preview dialogs' \"Open\" at invoice-preview-dialog.tsx:36,\n * platform-companies-page.tsx:111, verify-email-page.tsx:86's \"Log in\",\n * billing-history-page.tsx:85's PDF download), and has 26 `<Button onClick={() =>\n * navigate(…)}>` that are links in all but markup: no middle-click, no \"open in new\n * tab\", no URL on hover, and a screen reader announces a button that then changes the\n * page. With `href` it is an `<a>`: the provider's router link\n * ({@link UiKitProvider}'s `linkComponent`), or `renderLink` if given, else a plain one.\n *\n * `type` and `form` are typed out: they are a `<button>`'s, and a link never submits.\n */\nexport interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"type\" | \"href\"> {\n href: string;\n variant?: ButtonVariant;\n size?: ButtonSize;\n stretch?: boolean;\n tone?: ButtonTone;\n /** The in-app router link, over the provider's `linkComponent` (see\n * {@link KitLinkComponent}). Ignored when `external`. */\n renderLink?: KitLinkComponent;\n /** Leaves the app: a plain `<a target=\"_blank\" rel=\"noopener noreferrer\">`, never the\n * router link, and \"(opens in a new tab)\" read after the label — billing-history's\n * Stripe PDF. A `target=\"_blank\"` of your own is announced the same way. */\n external?: boolean;\n /**\n * Navigate by replacing the current history entry, so Back does not return here —\n * keksdose F1: the \"Continue\" out of a one-shot page (an emailed confirmation link, a\n * payment return URL) that must not be landed on again. Handed to the router link as\n * `replace` (see `KitLinkProps.replace` for mapping it); on a plain `<a>` a plain click\n * becomes `location.replace(href)`. Ignored when `external`.\n */\n replace?: boolean;\n /**\n * A plain `<a>` for an in-app `href`, so the browser loads the whole document instead\n * of the router swapping the view — keksdose F1: a route the SPA does not own (a\n * server-rendered export, a logout that must drop every in-memory cache). Wins over\n * `renderLink` and the provider's `linkComponent`.\n */\n reloadDocument?: boolean;\n /** A link cannot be `disabled`, so this renders an `<a>` with NO `href` —\n * `role=\"link\"` and `aria-disabled`, out of the tab order like a disabled button,\n * with the disabled look. The router link is not used: it needs somewhere to go. */\n disabled?: boolean;\n ref?: Ref<HTMLAnchorElement>;\n type?: never;\n form?: never;\n pending?: never;\n pressed?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n}\n\n// Declared link-first so `<Button href>` picks the link props; the LAST signature is\n// what `ComponentProps<typeof Button>` reads, so wrappers (FileButton) still see the\n// `<button>` props they always did.\nexport function Button(props: ButtonLinkProps): ReactElement;\nexport function Button(props: ButtonProps): ReactElement;\nexport function Button(props: ButtonProps | ButtonLinkProps): ReactElement {\n if (props.href !== undefined) return <ButtonLink {...(props as ButtonLinkProps)} />;\n return <ButtonElement {...(props as ButtonProps)} />;\n}\n\nfunction ButtonLink({\n href,\n renderLink,\n external,\n replace,\n reloadDocument,\n disabled,\n variant = \"primary\",\n size = \"md\",\n stretch,\n tone = \"default\",\n className,\n children,\n target,\n rel,\n onClick,\n ...rest\n}: ButtonLinkProps) {\n const kitLink = useKitLink();\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n // The same rule as every kit link (`pickLinkRenderer`): an external `href` or an\n // in-page `#anchor` is never handed to the provider's router link, even without\n // `external`.\n // `reloadDocument` is the caller saying \"not the router\" for this one link.\n const Link = reloadDocument ? undefined : pickLinkRenderer<KitLinkProps>(renderLink, kitLink, href);\n const newTab = !disabled && (external || target === \"_blank\");\n const cls = cn(\n buttonLook(variant, size, stretch, tone),\n // `relative` holds the sr-only note (see sr-only-containment.test).\n newTab && \"relative\",\n // BUTTON_BASE's `disabled:` classes never match an `<a>`; no pointer events also\n // keeps the hover fill from lighting up on something that refuses the click.\n disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n className,\n );\n const body = (\n <>\n {children}\n {/* The space OUTSIDE the sr-only span: a name computation trims each child's\n text, so one inside it was lost (\"Download(opens…\"). In a flex box a bare\n space draws nothing. */}\n {newTab && \" \"}\n {newTab && <span className=\"sr-only\">({common.opensInNewTab})</span>}\n </>\n );\n if (disabled) {\n return (\n <a {...rest} role=\"link\" aria-disabled=\"true\" className={cls}>\n {body}\n </a>\n );\n }\n if (external || !Link) {\n return (\n <a\n {...rest}\n href={href}\n target={external ? \"_blank\" : target}\n rel={external ? (rel ?? \"noopener noreferrer\") : rel}\n onClick={external ? onClick : replacingClick(onClick, href, replace, target)}\n className={cls}\n >\n {body}\n </a>\n );\n }\n return (\n <RenderedButtonLink\n render={Link}\n {...rest}\n {...routerLinkNavigation(replace)}\n href={href}\n target={target}\n rel={rel}\n onClick={onClick}\n className={cls}\n >\n {body}\n </RenderedButtonLink>\n );\n}\n\nfunction ButtonElement({\n variant = \"primary\",\n size = \"md\",\n stretch,\n tone = \"default\",\n pressed,\n disabledReason: ownDisabledReason,\n disabledReasonDisplay,\n commit,\n pending,\n className,\n onClick,\n children,\n href: _href,\n renderLink: _renderLink,\n external: _external,\n replace: _replace,\n reloadDocument: _reloadDocument,\n ...rest\n}: ButtonProps) {\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n const inert = locked || Boolean(pending);\n const ownDescribedBy = rest[\"aria-describedby\"];\n const button = (\n <button\n {...rest}\n disabled={inert ? undefined : rest.disabled}\n aria-disabled={inert || rest[\"aria-disabled\"]}\n aria-busy={pending || rest[\"aria-busy\"]}\n aria-describedby={locked ? (ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId) : ownDescribedBy}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n onClick={(e) => {\n // `preventDefault` as well as not calling through: a locked submit button must\n // not submit its form, and the browser does that after this handler returns.\n if (inert) {\n e.preventDefault();\n return;\n }\n onClick?.(e);\n }}\n className={cn(\n buttonLook(variant, size, stretch, tone),\n pressed && (variant === \"link\" ? BUTTON_LINK_PRESSED : ICON_BUTTON_PRESSED),\n // The disabled look for the focusable kind of disabled.\n locked && \"cursor-not-allowed opacity-50\",\n // No dimming while pending: the spinner IS the state, and at half opacity it\n // would be the faintest thing on the button. `relative` anchors it.\n pending && \"relative cursor-progress\",\n className,\n )}\n >\n {pending ? (\n <>\n {/* Transparent, not hidden: it keeps the width AND stays the accessible\n name (`visibility: hidden` would drop it from the name). `gap-[inherit]`\n keeps an icon + text spaced as the button spaces them. */}\n <span className=\"inline-flex items-center justify-center gap-[inherit] opacity-0\">{children}</span>\n {/* Decorative: `aria-busy` says it, as on FileButton. */}\n <Spinner\n label={null}\n className=\"absolute inset-0 m-auto size-4 border-current/30 border-t-current\"\n />\n </>\n ) : (\n children\n )}\n </button>\n );\n if (!locked) return button;\n return (\n <LockedReason\n lock={{ reason: disabledReason, reasonId }}\n display={disabledReasonDisplay}\n className={stretch ? \"self-stretch\" : undefined}\n >\n {button}\n </LockedReason>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/button.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer, replacingClick, routerLinkNavigation } from \"./text-link\";\nimport { useCommitReason } from \"./write-lock\";\nimport type { CommitScope } from \"./write-lock\";\nimport { hasContent } from \"../lib/presence\";\nimport { LockedReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport { Spinner } from \"./spinner\";\nimport { BUTTON_LINK_PRESSED, ICON_BUTTON_PRESSED, RenderedButtonLink, buttonLook } from \"./button-look\";\nimport type { ButtonSize, ButtonTone, ButtonVariant } from \"./button-look\";\n\n// The kit's text button, and `<Button href>`, the link that looks like one. Its look —\n// the variant, size and tone maps, and `buttonClasses` — is ./button-look, which\n// IconButton shares (0.34, MOD-2, refactor audit 2026-10).\n\n/**\n * ⚠️ No default `type`: like a native `<button>`, a `Button` inside a `<form>` SUBMITS\n * it unless you pass `type=\"button\"`. Kept native on purpose — a form's own submit\n * button relies on it, and changing the default would silently stop those forms from\n * submitting. Pass `type=\"button\"` for every other action in a form (keksdose had six\n * link buttons submitting their form).\n */\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** `default` (the quiet action, what a Button without `variant` draws), `accent` (the\n * strong call to action, solid brand), `secondary`, `ghost`, `danger` or `link`.\n * `primary` / `brand` are deprecated aliases of `default` / `accent`. See {@link ButtonVariant}. */\n variant?: ButtonVariant;\n /** `md` (default) is the page's action button. `sm` is the compact one — 12px text\n * and `px-2 py-1` — for the secondary actions in a toolbar, a card header or a\n * table's header row (keksdose writes `px-2 py-1 text-xs` over `secondary` by hand\n * there). Every variant takes either size. */\n size?: ButtonSize;\n /** In a flex row next to a taller labelled field, fill the field's height so the\n * two line up. No effect outside a flex row. */\n stretch?: boolean;\n /** The `<button>` element. React 19 passes `ref` to a function component as an\n * ordinary prop, so it rides `...rest` onto the element with no `forwardRef` —\n * declared here only because `ButtonHTMLAttributes` does not carry it. */\n ref?: Ref<HTMLButtonElement>;\n /** Text colour for `link` and `ghost`: `muted` (quiet, body colour on hover) or\n * `danger` (quiet, `--danger` on hover). `danger` also makes `secondary` an\n * outlined destructive button and `default` a soft one; ignored by the other\n * variants. See {@link ButtonTone}. */\n tone?: ButtonTone;\n /**\n * Make it a toggle button, as {@link IconButton}'s `pressed` does. `true` sets\n * `aria-pressed=\"true\"` and draws the \"on\" look — on a `link`, the brand colour and a\n * medium weight; on the boxed variants, the quiet brand fill IconButton uses — and\n * `false` sets `aria-pressed=\"false\"` with the ordinary look. Left out, no\n * `aria-pressed` (or the caller's own). For lenkbank's \"All speeds\" text toggle\n * (gear/hysteresis-charts.tsx:482), which is a hand-rolled `<button aria-pressed>`\n * swapping two class strings; with `variant=\"link\" tone=\"muted\"` it is this.\n */\n pressed?: boolean;\n /**\n * Why the action is not available — the button's half of DangerConfirm's\n * `lockedReason` (kastlan handover-detail-page.tsx:197 locks a signed handover).\n *\n * A disabled button that cannot say why is a dead end: the native `disabled` takes it\n * out of the tab order, so a keyboard user never lands on it, and a pointer gets a\n * `not-allowed` cursor and nothing else. With a reason the button is `aria-disabled`\n * instead — still focusable, still hoverable — clicks (and the Enter/Space and form\n * submission they stand for) do nothing, and the reason is shown in the kit\n * {@link Tooltip} and attached with `aria-describedby`, so a screen reader hears it\n * on focus. It wins over `disabled`: passing both keeps the button reachable, which is\n * the point of giving a reason.\n *\n * The description is a `hidden` copy of the reason rather than the bubble itself: the\n * bubble exists only while hovered or focused once it is portalled, and a description\n * that comes and goes is read inconsistently. The bubble is kept visual, the way\n * DangerConfirm keeps its own.\n */\n disabledReason?: ReactNode;\n /**\n * Where `disabledReason` shows (0.32, §4 \"No fact only in a tooltip\"): by default a\n * line under the button at Large and on a touch screen, the tooltip otherwise. See\n * {@link DisabledReasonDisplay}.\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This button COMMITS — it saves, creates, deletes. Under a locked\n * {@link WriteLockProvider} it is disabled the `disabledReason` way, with the lock's\n * reason (which wins over a `disabledReason` of its own). No provider, or an\n * unlocked one: no effect. See {@link WriteLockProvider}; a {@link CommitScope} since\n * 0.33 (`COMMIT_EXCEPT_BILLING`: every lock but a lapsed plan's).\n */\n commit?: CommitScope;\n /**\n * Busy — the save is in flight: a spinner, `aria-busy`, and no second submit.\n *\n * kastlan's FormActions (form-actions.tsx:50) disables its submit and swaps in a\n * `submittingLabel`, so the button jumps width mid-click and a screen reader hears\n * the name change instead of \"busy\". Here the label STAYS — it is still the\n * accessible name and still holds the button's width — and the spinner is drawn over\n * it in the text colour, so nothing beside the button moves.\n *\n * The same prop {@link FileButton} has (`pending`), with one difference: this one is\n * `aria-disabled`, not `disabled`. The button being pressed is the one that has focus,\n * and a native `disabled` drops that focus to `<body>` the moment the save starts;\n * clicks, and the Enter that submits a form through it, are swallowed instead.\n */\n pending?: boolean;\n /** Only on the link form — see {@link ButtonLinkProps}. */\n href?: never;\n renderLink?: never;\n external?: never;\n replace?: never;\n reloadDocument?: never;\n}\n\n/**\n * `<Button href>`: a link that looks like a button — for an action that NAVIGATES.\n *\n * kastlan paints `buttonClasses(…)` onto router Links by hand in six places (the\n * invoice / lease / unit preview dialogs' \"Open\" at invoice-preview-dialog.tsx:36,\n * platform-companies-page.tsx:111, verify-email-page.tsx:86's \"Log in\",\n * billing-history-page.tsx:85's PDF download), and has 26 `<Button onClick={() =>\n * navigate(…)}>` that are links in all but markup: no middle-click, no \"open in new\n * tab\", no URL on hover, and a screen reader announces a button that then changes the\n * page. With `href` it is an `<a>`: the provider's router link\n * ({@link UiKitProvider}'s `linkComponent`), or `renderLink` if given, else a plain one.\n *\n * `type` and `form` are typed out: they are a `<button>`'s, and a link never submits.\n */\nexport interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"type\" | \"href\"> {\n href: string;\n variant?: ButtonVariant;\n size?: ButtonSize;\n stretch?: boolean;\n tone?: ButtonTone;\n /** The in-app router link, over the provider's `linkComponent` (see\n * {@link KitLinkComponent}). Ignored when `external`. */\n renderLink?: KitLinkComponent;\n /** Leaves the app: a plain `<a target=\"_blank\" rel=\"noopener noreferrer\">`, never the\n * router link, and \"(opens in a new tab)\" read after the label — billing-history's\n * Stripe PDF. A `target=\"_blank\"` of your own is announced the same way. */\n external?: boolean;\n /**\n * Navigate by replacing the current history entry, so Back does not return here —\n * keksdose F1: the \"Continue\" out of a one-shot page (an emailed confirmation link, a\n * payment return URL) that must not be landed on again. Handed to the router link as\n * `replace` (see `KitLinkProps.replace` for mapping it); on a plain `<a>` a plain click\n * becomes `location.replace(href)`. Ignored when `external`.\n */\n replace?: boolean;\n /**\n * A plain `<a>` for an in-app `href`, so the browser loads the whole document instead\n * of the router swapping the view — keksdose F1: a route the SPA does not own (a\n * server-rendered export, a logout that must drop every in-memory cache). Wins over\n * `renderLink` and the provider's `linkComponent`.\n */\n reloadDocument?: boolean;\n /** A link cannot be `disabled`, so this renders an `<a>` with NO `href` —\n * `role=\"link\"` and `aria-disabled`, out of the tab order like a disabled button,\n * with the disabled look. The router link is not used: it needs somewhere to go. */\n disabled?: boolean;\n ref?: Ref<HTMLAnchorElement>;\n type?: never;\n form?: never;\n pending?: never;\n pressed?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n}\n\n// Declared link-first so `<Button href>` picks the link props; the LAST signature is\n// what `ComponentProps<typeof Button>` reads, so wrappers (FileButton) still see the\n// `<button>` props they always did.\nexport function Button(props: ButtonLinkProps): ReactElement;\nexport function Button(props: ButtonProps): ReactElement;\nexport function Button(props: ButtonProps | ButtonLinkProps): ReactElement {\n if (props.href !== undefined) return <ButtonLink {...(props as ButtonLinkProps)} />;\n return <ButtonElement {...(props as ButtonProps)} />;\n}\n\nfunction ButtonLink({\n href,\n renderLink,\n external,\n replace,\n reloadDocument,\n disabled,\n variant = \"default\",\n size = \"md\",\n stretch,\n tone = \"default\",\n className,\n children,\n target,\n rel,\n onClick,\n ...rest\n}: ButtonLinkProps) {\n const kitLink = useKitLink();\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n // The same rule as every kit link (`pickLinkRenderer`): an external `href` or an\n // in-page `#anchor` is never handed to the provider's router link, even without\n // `external`.\n // `reloadDocument` is the caller saying \"not the router\" for this one link.\n const Link = reloadDocument ? undefined : pickLinkRenderer<KitLinkProps>(renderLink, kitLink, href);\n const newTab = !disabled && (external || target === \"_blank\");\n const cls = cn(\n buttonLook(variant, size, stretch, tone),\n // `relative` holds the sr-only note (see sr-only-containment.test).\n newTab && \"relative\",\n // BUTTON_BASE's `disabled:` classes never match an `<a>`; no pointer events also\n // keeps the hover fill from lighting up on something that refuses the click.\n disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n className,\n );\n const body = (\n <>\n {children}\n {/* The space OUTSIDE the sr-only span: a name computation trims each child's\n text, so one inside it was lost (\"Download(opens…\"). In a flex box a bare\n space draws nothing. */}\n {newTab && \" \"}\n {newTab && <span className=\"sr-only\">({common.opensInNewTab})</span>}\n </>\n );\n if (disabled) {\n return (\n <a {...rest} role=\"link\" aria-disabled=\"true\" className={cls}>\n {body}\n </a>\n );\n }\n if (external || !Link) {\n return (\n <a\n {...rest}\n href={href}\n target={external ? \"_blank\" : target}\n rel={external ? (rel ?? \"noopener noreferrer\") : rel}\n onClick={external ? onClick : replacingClick(onClick, href, replace, target)}\n className={cls}\n >\n {body}\n </a>\n );\n }\n return (\n <RenderedButtonLink\n render={Link}\n {...rest}\n {...routerLinkNavigation(replace)}\n href={href}\n target={target}\n rel={rel}\n onClick={onClick}\n className={cls}\n >\n {body}\n </RenderedButtonLink>\n );\n}\n\nfunction ButtonElement({\n variant = \"default\",\n size = \"md\",\n stretch,\n tone = \"default\",\n pressed,\n disabledReason: ownDisabledReason,\n disabledReasonDisplay,\n commit,\n pending,\n className,\n onClick,\n children,\n href: _href,\n renderLink: _renderLink,\n external: _external,\n replace: _replace,\n reloadDocument: _reloadDocument,\n ...rest\n}: ButtonProps) {\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n const inert = locked || Boolean(pending);\n const ownDescribedBy = rest[\"aria-describedby\"];\n const button = (\n <button\n {...rest}\n disabled={inert ? undefined : rest.disabled}\n aria-disabled={inert || rest[\"aria-disabled\"]}\n aria-busy={pending || rest[\"aria-busy\"]}\n aria-describedby={locked ? (ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId) : ownDescribedBy}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n onClick={(e) => {\n // `preventDefault` as well as not calling through: a locked submit button must\n // not submit its form, and the browser does that after this handler returns.\n if (inert) {\n e.preventDefault();\n return;\n }\n onClick?.(e);\n }}\n className={cn(\n buttonLook(variant, size, stretch, tone),\n pressed && (variant === \"link\" ? BUTTON_LINK_PRESSED : ICON_BUTTON_PRESSED),\n // The disabled look for the focusable kind of disabled.\n locked && \"cursor-not-allowed opacity-50\",\n // No dimming while pending: the spinner IS the state, and at half opacity it\n // would be the faintest thing on the button. `relative` anchors it.\n pending && \"relative cursor-progress\",\n className,\n )}\n >\n {pending ? (\n <>\n {/* Transparent, not hidden: it keeps the width AND stays the accessible\n name (`visibility: hidden` would drop it from the name). `gap-[inherit]`\n keeps an icon + text spaced as the button spaces them. */}\n <span className=\"inline-flex items-center justify-center gap-[inherit] opacity-0\">{children}</span>\n {/* Decorative: `aria-busy` says it, as on FileButton. */}\n <Spinner\n label={null}\n className=\"absolute inset-0 m-auto size-4 border-current/30 border-t-current\"\n />\n </>\n ) : (\n children\n )}\n </button>\n );\n if (!locked) return button;\n return (\n <LockedReason\n lock={{ reason: disabledReason, reasonId }}\n display={disabledReasonDisplay}\n className={stretch ? \"self-stretch\" : undefined}\n >\n {button}\n </LockedReason>\n );\n}\n"],"mappings":"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","names":[]}
@@ -9,6 +9,7 @@ import { lerpHex, textOn } from "../theme/chart-palette.js";
9
9
  import { parseHex } from "../theme/color.js";
10
10
  import { Tooltip } from "./tooltip.js";
11
11
  import { DEFAULT_CALENDAR_HEATMAP_LABELS } from "./calendar-heatmap-labels.js";
12
+ import { formatWithMinus } from "../lib/minus.js";
12
13
  const DAY_NAME_FORMAT = {
13
14
  weekday: "long",
14
15
  day: "numeric",
@@ -71,7 +72,7 @@ function CalendarHeatmap({
71
72
  const providerWeekStart = useKitWeekStart();
72
73
  const weekStart = weekStartsOn ?? providerWeekStart ?? localeWeekStart(locale);
73
74
  const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
74
- const formatValue = formatValueProp ?? ((v) => numberFormat.format(v));
75
+ const formatValue = formatValueProp ?? ((v) => formatWithMinus(numberFormat, v));
75
76
  const values = useMemo(() => {
76
77
  const m = /* @__PURE__ */ new Map();
77
78
  for (const p of data) m.set(p.date, (m.get(p.date) ?? 0) + p.value);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/calendar-heatmap.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, horizontalStep } from \"../lib/direction\";\nimport { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from \"../lib/dates\";\nimport { useKitLabels, useKitLocale, useKitWeekStart, type LabelOverride } from \"../i18n/kit-labels\";\nimport { lerpHex, textOn } from \"../theme/chart-palette\";\nimport { parseHex } from \"../theme/color\";\nimport { Tooltip } from \"./tooltip\";\nimport type { WeekDay } from \"./mini-calendar\";\nimport { DEFAULT_CALENDAR_HEATMAP_LABELS } from \"./calendar-heatmap-labels\";\n\n/**\n * Every string the heatmap speaks — the `calendarHeatmap` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. Dates and\n * values arrive already formatted (in `locale`, and through `formatValue`), so the\n * functions only arrange them.\n */\nexport interface CalendarHeatmapLabels {\n /** Accessible name of the grid. */\n grid: string;\n /**\n * One day: its accessible name, and the tooltip unless `tooltip` is given. `date`\n * is the whole date in `locale` (\"Monday, 14 September 2026\"), `value` the day's\n * figure through `formatValue`.\n */\n day: (date: string, value: string) => string;\n /** The two ends of the legend's scale. */\n less: string;\n more: string;\n /** `maxDays` cut the window: how many of its earliest days are not drawn. */\n truncated: (count: number) => string;\n}\n\n// Defined in ./calendar-heatmap-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_CALENDAR_HEATMAP_LABELS };\n\n/** One day's figure. Several entries for one date are added up. */\nexport interface CalendarHeatmapDatum {\n /** \"YYYY-MM-DD\". */\n date: string;\n value: number;\n}\n\n/** What `tooltip` is told about the day under the pointer. */\nexport interface CalendarHeatmapDay {\n iso: string;\n date: Date;\n /** The day's figure; `0` when `data` has no entry for it (see `hasData`). */\n value: number;\n hasData: boolean;\n /** `0` = empty … `levels` = the busiest. */\n level: number;\n /** The whole date in `locale`. */\n formattedDate: string;\n /** `value` through `formatValue`. */\n formattedValue: string;\n}\n\n/**\n * `onSelect` is omitted from the `<div>` attributes for the reason `MiniCalendar` gives:\n * the DOM event of that name is a text-selection event, and this one answers with a date.\n */\nexport interface CalendarHeatmapProps extends Omit<ComponentPropsWithRef<\"div\">, \"onSelect\"> {\n data: readonly CalendarHeatmapDatum[];\n /** The window, inclusive, as \"YYYY-MM-DD\". */\n from: string;\n to: string;\n /**\n * `\"weeks\"` (default): the contribution-graph shape — one COLUMN per week, the\n * weekdays down the side, month names along the top; a year fits in ~53 small\n * squares and scrolls sideways where it does not. `\"month\"`: a 7-column calendar\n * with day numbers in large square cells, for a single month on a phone (switch on\n * `useMediaQuery` and narrow `from`/`to` to the month).\n */\n layout?: \"weeks\" | \"month\";\n /** The day shown as selected (\"YYYY-MM-DD\"). */\n selected?: string;\n /** Makes each day a button: a click (or Enter/Space) selects it — a drill-down into\n * that day's rows, a day panel. Without it the days are focusable but inert. */\n onSelect?: (iso: string) => void;\n /** How a value is written in the tooltip and the day's name. Default: `Intl.NumberFormat`\n * in `locale`. Pass a currency formatter for money. */\n formatValue?: (value: number) => string;\n /** Tooltip content for a day. Default: `labels.day(date, value)`. */\n tooltip?: (day: CalendarHeatmapDay) => ReactNode;\n /**\n * Tag each tooltip `data-private`, so the host's demo-mode rule blurs it (the\n * attribute `Tooltip redact` and `StatTile sensitive` carry). ON by default: a heatmap\n * is almost always the user's own figures, and a missing tag is silent — the grid\n * looks anonymous and the bubble spells the amount out on hover.\n */\n sensitive?: boolean;\n /** The value that reaches the top of the scale. Default: the largest value ON SCREEN,\n * so a peak outside the window does not wash the drawn days out to empty. */\n max?: number;\n /** How many non-empty intensity steps. Default 4. */\n levels?: number;\n /** The scale's colour, as any CSS colour. Default `var(--brand)`; each step is this\n * mixed into `--bg-surface`, so the ramp follows the theme. */\n color?: string;\n /**\n * A CONTINUOUS scale from `colorFrom` (the smallest value) to `colorTo` (the busiest\n * day), instead of `levels` steps of `color` — keksdose's palette-store ramp,\n * `seqLow` → `seqHigh`. Both are needed; `color` is then unused. Two `#rrggbb`\n * colours are interpolated in JS, so the fill is a concrete hex and the day number's\n * ink (in `\"month\"`) is MEASURED against it; anything else (a `var()`) is mixed with\n * `color-mix()`. `levels` still sets `data-level` and the legend's swatch count.\n */\n colorFrom?: string;\n colorTo?: string;\n /**\n * A fill of the caller's own for a day with a value: handed the value and the\n * scale's `max`, returns any CSS colour. Wins over `color` and `colorFrom`/`colorTo`.\n * Also paints the legend, at `max × level / levels`.\n */\n fill?: (value: number, max: number) => string;\n /** The fill of a day with no value (or a value of 0). Default: a wash of the text\n * colour over the surface, which reads as a cell on both themes. */\n emptyColor?: string;\n /**\n * Where the window ENDS — the day `maxDays` counts back from, and the last one drawn:\n *\n * - `\"to\"` (default): on `to`.\n * - `\"today\"`: on today.\n * - `\"latest\"`: on the last day that can carry a figure — today, or a later day that\n * actually has a value. A window running into the future (a report's \"all\" preset\n * ends a year ahead) then spends its cells on the days a person is looking for,\n * not on a year of empty ones.\n * - a `\"YYYY-MM-DD\"`: on that day.\n *\n * Always kept inside `from`…`to`.\n */\n anchor?: \"to\" | \"today\" | \"latest\" | (string & {});\n /**\n * The most days to draw. A longer window keeps its LATEST `maxDays` and says how many\n * it left out (`labels.truncated`) rather than silently ending. Default: no cap.\n */\n maxDays?: number;\n /** Draw the Less … More legend. Default `true`. */\n legend?: boolean;\n /** BCP 47 tag for the dates, month and weekday names and numbers. */\n locale?: string;\n /** `0` = Sunday … `6` = Saturday. Default: the provider's, else the locale's. */\n weekStartsOn?: WeekDay;\n labels?: LabelOverride<CalendarHeatmapLabels>;\n className?: string;\n}\n\nconst DAY_NAME_FORMAT: Intl.DateTimeFormatOptions = {\n weekday: \"long\",\n day: \"numeric\",\n month: \"long\",\n year: \"numeric\",\n};\n\n/** The empty day: a wash of the text colour, like `--bg-hover`, so it reads as a cell\n * on both themes without claiming any value. */\nconst EMPTY_FILL = \"color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface))\";\n\nconst weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;\n\n/** Where the window ends for an {@link CalendarHeatmapProps.anchor}, kept in `[from, to]`.\n * `latestData` is the latest ISO date carrying a value, or `\"\"`. */\nexport function heatmapWindowEnd(\n from: string,\n to: string,\n anchor: string,\n latestData: string,\n today: string = toLocalIso(new Date()),\n): string {\n let end: string;\n if (anchor === \"today\") end = today;\n else if (anchor === \"latest\") end = latestData > today ? latestData : today;\n else if (anchor === \"to\" || !parseIsoDate(anchor)) end = to;\n else end = anchor;\n // ISO dates sort as strings, so the clamp is plain comparison.\n if (end > to) end = to;\n if (end < from) end = from;\n return end;\n}\n\n/** The step a value falls on: 0 for nothing, else 1…levels by its share of `max`. */\nexport function heatmapLevel(value: number, max: number, levels: number): number {\n if (!(value > 0) || !(max > 0) || levels < 1) return 0;\n return Math.min(levels, Math.max(1, Math.ceil((value / max) * levels)));\n}\n\n/** How much of `color` a step mixes into the surface: 20% for the first step, 100% for\n * the last — the floor keeps the smallest value visibly apart from an empty day. */\nfunction levelPercent(level: number, levels: number): number {\n if (levels <= 1) return 100;\n return Math.round(20 + (80 * (level - 1)) / (levels - 1));\n}\n\nfunction levelFill(level: number, levels: number, color: string): string {\n if (level === 0) return EMPTY_FILL;\n return `color-mix(in oklab, ${color} ${levelPercent(level, levels)}%, var(--bg-surface))`;\n}\n\n/**\n * Days as a grid of shaded squares — how much happened on each, at a glance\n * (a contribution graph, a spending calendar).\n *\n * **Every day is a cell you can reach.** The grid is a `role=\"grid\"` with ONE tab\n * stop (the roving tabindex, as in `MiniCalendar`); the arrows move by the grid's own\n * geometry — in `\"weeks\"` ↑/↓ are the previous/next day and ←/→ a week, in `\"month\"`\n * the other way round, mirrored in RTL — PageUp/PageDown move a month and Home/End go\n * to the window's first/last day. Each cell is named with its whole date AND its value,\n * because the colour is not something a screen reader can say, and carries\n * `data-day=\"YYYY-MM-DD\"` as its identity for tests and for the host. Note that in\n * `\"weeks\"` the cells are in DOM order by WEEKDAY ROW (all Mondays, then all\n * Tuesdays…), not by date — query by `data-day`, not by position.\n *\n * The tooltip is portalled (a year of squares always sits in a sideways scroller) and\n * `data-private` unless `sensitive={false}`. Cell size in `\"weeks\"` is the CSS variable\n * `--heatmap-cell` (default `0.75rem`): `style={{ \"--heatmap-cell\": \"1rem\" }}`.\n */\nexport function CalendarHeatmap({\n data,\n from,\n to,\n layout = \"weeks\",\n selected,\n onSelect,\n formatValue: formatValueProp,\n tooltip,\n sensitive = true,\n max: maxProp,\n levels = 4,\n color = \"var(--brand)\",\n colorFrom,\n colorTo,\n fill: fillProp,\n emptyColor,\n anchor = \"to\",\n maxDays,\n legend = true,\n locale: localeProp,\n weekStartsOn,\n labels: labelsProp,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: CalendarHeatmapProps) {\n const labels = useKitLabels(\"calendarHeatmap\", DEFAULT_CALENDAR_HEATMAP_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const providerWeekStart = useKitWeekStart();\n const weekStart: WeekDay = weekStartsOn ?? providerWeekStart ?? localeWeekStart(locale);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const formatValue = formatValueProp ?? ((v: number) => numberFormat.format(v));\n\n const values = useMemo(() => {\n const m = new Map<string, number>();\n for (const p of data) m.set(p.date, (m.get(p.date) ?? 0) + p.value);\n return m;\n }, [data]);\n\n // The window, trimmed from the FRONT: the days a person looks for are the recent\n // ones, and a cap that kept the oldest would end the calendar before today.\n const latestData = useMemo(() => {\n let latest = \"\";\n for (const [iso, v] of values) if (v !== 0 && iso > latest) latest = iso;\n return latest;\n }, [values]);\n const windowEnd = heatmapWindowEnd(from, to, anchor, latestData);\n\n const { days, dropped } = useMemo(() => {\n const start = parseIsoDate(from);\n const end = parseIsoDate(windowEnd);\n const list: Date[] = [];\n if (!start || !end || end < start) return { days: list, dropped: 0 };\n const cursor = new Date(end);\n const cap = maxDays && maxDays > 0 ? maxDays : Infinity;\n while (cursor >= start && list.length < cap) {\n list.unshift(new Date(cursor));\n cursor.setDate(cursor.getDate() - 1);\n }\n const first = list[0];\n const trimmed = first ? Math.round((first.getTime() - start.getTime()) / 86_400_000) : 0;\n return { days: list, dropped: Math.max(0, trimmed) };\n }, [from, windowEnd, maxDays]);\n\n const firstIso = days.length ? toLocalIso(days[0]) : \"\";\n const lastIso = days.length ? toLocalIso(days[days.length - 1]) : \"\";\n\n const max = useMemo(() => {\n if (maxProp !== undefined) return maxProp;\n let m = 0;\n for (const d of days) m = Math.max(m, values.get(toLocalIso(d)) ?? 0);\n return m;\n }, [maxProp, days, values]);\n\n const clampIso = (iso: string) => (iso < firstIso ? firstIso : iso > lastIso ? lastIso : iso);\n\n // The roving tab stop: the selected day, else the latest one.\n const [activeState, setActiveIso] = useState(() => selected || lastIso);\n const [lastSelected, setLastSelected] = useState(selected);\n if (lastSelected !== selected) {\n setLastSelected(selected);\n if (selected) setActiveIso(selected);\n }\n const activeIso = days.length ? clampIso(activeState || lastIso) : \"\";\n\n const gridRef = useRef<HTMLDivElement>(null);\n const scrollerRef = useRef<HTMLDivElement>(null);\n const pendingFocus = useRef<string | null>(null);\n useEffect(() => {\n const iso = pendingFocus.current;\n if (!iso) return;\n pendingFocus.current = null;\n gridRef.current?.querySelector<HTMLElement>(`[data-day=\"${iso}\"]`)?.focus();\n });\n\n // Open the sideways scroller on the latest weeks, not the oldest. In RTL the end is\n // on the left, where browsers count scrollLeft down from 0.\n //\n // And KEEP it there while the box changes size — a sidebar or a contents rail that\n // appears after mount narrows the scroller, which leaves scrollLeft where it was and\n // so shows older weeks than it opened on. The pin holds only while the view sits at\n // the end: a user who scrolls back through the year unpins it, and a resize then\n // leaves their position alone. (Scrolling back to the end pins it again.)\n useLayoutEffect(() => {\n const el = scrollerRef.current;\n if (!el || layout !== \"weeks\") return;\n const toEnd = () => {\n el.scrollLeft = dirOf(el) === \"rtl\" ? -el.scrollWidth : el.scrollWidth;\n };\n toEnd();\n if (typeof ResizeObserver === \"undefined\") return;\n // |scrollLeft| covers both directions: 0 → max in LTR, 0 → -max in RTL.\n let pinned = true;\n const onScroll = () => {\n pinned = Math.abs(el.scrollLeft) >= el.scrollWidth - el.clientWidth - 1;\n };\n const ro = new ResizeObserver(() => {\n if (pinned) toEnd();\n });\n el.addEventListener(\"scroll\", onScroll, { passive: true });\n ro.observe(el);\n return () => {\n ro.disconnect();\n el.removeEventListener(\"scroll\", onScroll);\n };\n }, [layout, firstIso, lastIso]);\n\n const formatDay = (d: Date) => d.toLocaleDateString(locale, DAY_NAME_FORMAT);\n\n const weekdayLabels = useMemo(() => {\n const ref = new Date(2024, 0, 7 + weekStart); // 7 January 2024 was a Sunday\n const week = Array.from({ length: 7 }, (_, i) => {\n const d = new Date(ref);\n d.setDate(ref.getDate() + i);\n return d;\n });\n const name = (d: Date, weekday: \"short\" | \"narrow\" | \"long\") => d.toLocaleDateString(locale, { weekday });\n // The label column is 2rem at 10px type, room for about four characters. Most\n // locales' \"short\" is three or four (\"Thu\", \"jeu.\", \"czw.\"), but some have no\n // abbreviation at all — Arabic's short Thursday IS \"الخميس\", which overran the box\n // and was cut. Such a locale takes the \"narrow\" form (\"خ\") for ALL seven, so the\n // column never mixes two forms; the full name is on the aria-label either way.\n const tooLong = week.some((d) => [...name(d, \"short\")].length > 4);\n return week.map((d) => ({ short: name(d, tooLong ? \"narrow\" : \"short\"), long: name(d, \"long\") }));\n }, [locale, weekStart]);\n\n const lead = days.length ? weekdayIndex(days[0], weekStart) : 0;\n const weekCount = Math.ceil((lead + days.length) / 7);\n /** The day at position `i` of the week-aligned sequence (blanks before the first). */\n const dayAt = (i: number): Date | null => (i >= lead && i - lead < days.length ? days[i - lead] : null);\n\n const move = (e: KeyboardEvent<HTMLElement>) => {\n const current = parseIsoDate(activeIso);\n if (!current) return;\n const next = new Date(current);\n const step = horizontalStep(e.key, e.currentTarget);\n // In \"weeks\" a row is a weekday, so sideways is a week and up/down is a day.\n const across = layout === \"weeks\" ? 7 : 1;\n const down = layout === \"weeks\" ? 1 : 7;\n switch (step ? \"horizontal\" : e.key) {\n case \"horizontal\":\n next.setDate(next.getDate() + step * across);\n break;\n case \"ArrowUp\":\n next.setDate(next.getDate() - down);\n break;\n case \"ArrowDown\":\n next.setDate(next.getDate() + down);\n break;\n case \"PageUp\":\n next.setTime(addMonthsClamped(next, -1).getTime());\n break;\n case \"PageDown\":\n next.setTime(addMonthsClamped(next, 1).getTime());\n break;\n case \"Home\":\n next.setTime(days[0].getTime());\n break;\n case \"End\":\n next.setTime(days[days.length - 1].getTime());\n break;\n default:\n return;\n }\n e.preventDefault();\n const iso = clampIso(toLocalIso(next));\n if (iso === activeIso) return;\n pendingFocus.current = iso;\n setActiveIso(iso);\n };\n\n const legendLevels = Array.from({ length: levels + 1 }, (_, l) => l);\n const empty = emptyColor ?? EMPTY_FILL;\n const continuous = colorFrom !== undefined && colorTo !== undefined;\n const hexRamp = continuous && parseHex(colorFrom) != null && parseHex(colorTo) != null;\n\n /** A day's fill, and the ink its number is set in (`\"month\"`). */\n const paint = (value: number, level: number): { fill: string; ink: string } => {\n if (level === 0) return { fill: empty, ink: \"var(--text-primary)\" };\n if (fillProp || continuous) {\n const t = max > 0 ? Math.min(1, Math.max(0, value / max)) : 1;\n const fill = fillProp\n ? fillProp(value, max)\n : hexRamp\n ? lerpHex(colorFrom!, colorTo!, t)\n : `color-mix(in oklab, ${colorTo} ${Math.round(t * 100)}%, ${colorFrom})`;\n // Measured where the fill is a colour; a mix of references cannot be measured,\n // and the page's own text colour is the safe answer there.\n const measured = textOn(fill);\n return { fill, ink: measured === \"currentColor\" ? \"var(--text-primary)\" : measured };\n }\n // Light text once the fill is the stronger half of the mix — `--bg-surface` is the\n // colour the scale is mixed INTO, so it is the one that contrasts with its far end\n // on either theme.\n const strong = levelPercent(level, levels) >= 45;\n return {\n fill: levelFill(level, levels, color),\n ink: strong ? \"var(--bg-surface)\" : \"var(--text-primary)\",\n };\n };\n // The legend's swatches: a value that lands on each step (the top one on `max`).\n const legendFill = (l: number) => paint(((max > 0 ? max : 1) * l) / levels, l).fill;\n\n const cell = (d: Date, wrapClass: string, cellClass: string, withNumber: boolean) => {\n const iso = toLocalIso(d);\n const raw = values.get(iso);\n const value = raw ?? 0;\n const level = heatmapLevel(value, max, levels);\n const { fill, ink } = paint(value, level);\n const formattedDate = formatDay(d);\n const formattedValue = formatValue(value);\n const name = labels.day(formattedDate, formattedValue);\n const label = tooltip\n ? tooltip({ iso, date: d, value, hasData: raw !== undefined, level, formattedDate, formattedValue })\n : name;\n const isSelected = selected === iso;\n const shared = {\n role: \"gridcell\",\n \"data-day\": iso,\n \"data-level\": level,\n tabIndex: iso === activeIso ? 0 : -1,\n \"aria-label\": name,\n \"aria-selected\": onSelect ? isSelected : undefined,\n onKeyDown: move,\n onFocus: () => {\n if (iso !== activeIso) setActiveIso(iso);\n },\n style: { background: fill, color: ink },\n className: cn(\n cellClass,\n isSelected && \"ring-2 ring-[var(--text-primary)]\",\n onSelect && \"cursor-pointer hover:ring-1 hover:ring-brand\",\n ),\n } as const;\n const inner = withNumber ? (\n <span aria-hidden className=\"text-caption leading-none tabular-nums\">\n {numberFormat.format(d.getDate())}\n </span>\n ) : null;\n return (\n <Tooltip key={iso} label={label} portal redact={sensitive} role=\"none\" className={wrapClass}>\n {onSelect ? (\n <button type=\"button\" {...shared} onClick={() => onSelect(iso)}>\n {inner}\n </button>\n ) : (\n <div {...shared}>{inner}</div>\n )}\n </Tooltip>\n );\n };\n\n const gridName = ariaLabel ?? labels.grid;\n let body: ReactNode;\n if (layout === \"weeks\") {\n const columns = { gridTemplateColumns: `var(--heatmap-label, 2rem) repeat(${weekCount}, var(--heatmap-cell, 0.75rem))` } as CSSProperties;\n const cellSize = \"size-[var(--heatmap-cell,0.75rem)]\";\n // A month's name over the first column that holds its 1st — or over the first\n // column, when the window opens far enough before the next month for it to fit.\n const monthLabels = Array.from({ length: weekCount }, (_, w) => {\n for (let r = 0; r < 7; r++) {\n const d = dayAt(w * 7 + r);\n if (d && (d.getDate() === 1 || (w === 0 && r === lead && d.getDate() <= 14))) {\n return d.toLocaleDateString(locale, { month: \"short\" });\n }\n }\n return \"\";\n });\n body = (\n <div ref={scrollerRef} className=\"overflow-x-auto pb-1\">\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"inline-grid gap-0.5\">\n {/* Decoration: every cell's name already carries its month. */}\n <div aria-hidden className=\"grid gap-0.5 text-micro leading-3 text-muted\" style={columns}>\n <span />\n {monthLabels.map((m, w) => (\n <span key={w} className=\"overflow-visible whitespace-nowrap\">\n {m}\n </span>\n ))}\n </div>\n {Array.from({ length: 7 }, (_, r) => (\n <div key={r} role=\"row\" className=\"grid gap-0.5\" style={columns}>\n <div\n role=\"rowheader\"\n aria-label={weekdayLabels[r].long}\n className=\"overflow-hidden pe-1 text-micro leading-3 text-muted\"\n >\n {/* Every other weekday, as a contribution graph does: seven labels\n at this size are a smear. The name is there for all seven. */}\n {r % 2 === 1 ? weekdayLabels[r].short : \"\"}\n </div>\n {Array.from({ length: weekCount }, (_, w) => {\n const d = dayAt(w * 7 + r);\n return d ? (\n cell(d, cellSize, cn(cellSize, \"block rounded-sm\"), false)\n ) : (\n <div key={`pad-${w}`} role=\"gridcell\" className={cellSize} />\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n } else {\n const rows = Array.from({ length: weekCount }, (_, w) => w);\n body = (\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"grid gap-1\">\n <div role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {weekdayLabels.map((w, i) => (\n <div\n key={i}\n role=\"columnheader\"\n aria-label={w.long}\n className=\"pb-0.5 text-center text-micro uppercase tracking-wide text-muted\"\n >\n {w.short}\n </div>\n ))}\n </div>\n {rows.map((w) => (\n <div key={w} role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {Array.from({ length: 7 }, (_, c) => {\n const d = dayAt(w * 7 + c);\n return d ? (\n cell(d, \"aspect-square w-full\", \"flex h-full w-full items-center justify-center rounded\", true)\n ) : (\n <div key={`pad-${c}`} role=\"gridcell\" className=\"aspect-square\" />\n );\n })}\n </div>\n ))}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"min-w-0\", className)}>\n {dropped > 0 && (\n <div className=\"mb-2 text-xs text-muted\">{labels.truncated(dropped)}</div>\n )}\n {body}\n {legend && (\n <div aria-hidden className=\"mt-2 flex items-center justify-end gap-1 text-micro text-muted\">\n <span className=\"me-0.5\">{labels.less}</span>\n {legendLevels.map((l) => (\n <span\n key={l}\n data-legend-level={l}\n className=\"size-2.5 rounded-sm\"\n style={{ background: legendFill(l) }}\n />\n ))}\n <span className=\"ms-0.5\">{labels.more}</span>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["_"]}
1
+ {"version":3,"sources":["../../src/components/calendar-heatmap.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, horizontalStep } from \"../lib/direction\";\nimport { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from \"../lib/dates\";\nimport { useKitLabels, useKitLocale, useKitWeekStart, type LabelOverride } from \"../i18n/kit-labels\";\nimport { lerpHex, textOn } from \"../theme/chart-palette\";\nimport { parseHex } from \"../theme/color\";\nimport { Tooltip } from \"./tooltip\";\nimport type { WeekDay } from \"./mini-calendar\";\nimport { DEFAULT_CALENDAR_HEATMAP_LABELS } from \"./calendar-heatmap-labels\";\nimport { formatWithMinus } from \"../lib/minus\";\n\n/**\n * Every string the heatmap speaks — the `calendarHeatmap` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. Dates and\n * values arrive already formatted (in `locale`, and through `formatValue`), so the\n * functions only arrange them.\n */\nexport interface CalendarHeatmapLabels {\n /** Accessible name of the grid. */\n grid: string;\n /**\n * One day: its accessible name, and the tooltip unless `tooltip` is given. `date`\n * is the whole date in `locale` (\"Monday, 14 September 2026\"), `value` the day's\n * figure through `formatValue`.\n */\n day: (date: string, value: string) => string;\n /** The two ends of the legend's scale. */\n less: string;\n more: string;\n /** `maxDays` cut the window: how many of its earliest days are not drawn. */\n truncated: (count: number) => string;\n}\n\n// Defined in ./calendar-heatmap-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_CALENDAR_HEATMAP_LABELS };\n\n/** One day's figure. Several entries for one date are added up. */\nexport interface CalendarHeatmapDatum {\n /** \"YYYY-MM-DD\". */\n date: string;\n value: number;\n}\n\n/** What `tooltip` is told about the day under the pointer. */\nexport interface CalendarHeatmapDay {\n iso: string;\n date: Date;\n /** The day's figure; `0` when `data` has no entry for it (see `hasData`). */\n value: number;\n hasData: boolean;\n /** `0` = empty … `levels` = the busiest. */\n level: number;\n /** The whole date in `locale`. */\n formattedDate: string;\n /** `value` through `formatValue`. */\n formattedValue: string;\n}\n\n/**\n * `onSelect` is omitted from the `<div>` attributes for the reason `MiniCalendar` gives:\n * the DOM event of that name is a text-selection event, and this one answers with a date.\n */\nexport interface CalendarHeatmapProps extends Omit<ComponentPropsWithRef<\"div\">, \"onSelect\"> {\n data: readonly CalendarHeatmapDatum[];\n /** The window, inclusive, as \"YYYY-MM-DD\". */\n from: string;\n to: string;\n /**\n * `\"weeks\"` (default): the contribution-graph shape — one COLUMN per week, the\n * weekdays down the side, month names along the top; a year fits in ~53 small\n * squares and scrolls sideways where it does not. `\"month\"`: a 7-column calendar\n * with day numbers in large square cells, for a single month on a phone (switch on\n * `useMediaQuery` and narrow `from`/`to` to the month).\n */\n layout?: \"weeks\" | \"month\";\n /** The day shown as selected (\"YYYY-MM-DD\"). */\n selected?: string;\n /** Makes each day a button: a click (or Enter/Space) selects it — a drill-down into\n * that day's rows, a day panel. Without it the days are focusable but inert. */\n onSelect?: (iso: string) => void;\n /** How a value is written in the tooltip and the day's name. Default: `Intl.NumberFormat`\n * in `locale`. Pass a currency formatter for money. */\n formatValue?: (value: number) => string;\n /** Tooltip content for a day. Default: `labels.day(date, value)`. */\n tooltip?: (day: CalendarHeatmapDay) => ReactNode;\n /**\n * Tag each tooltip `data-private`, so the host's demo-mode rule blurs it (the\n * attribute `Tooltip redact` and `StatTile sensitive` carry). ON by default: a heatmap\n * is almost always the user's own figures, and a missing tag is silent — the grid\n * looks anonymous and the bubble spells the amount out on hover.\n */\n sensitive?: boolean;\n /** The value that reaches the top of the scale. Default: the largest value ON SCREEN,\n * so a peak outside the window does not wash the drawn days out to empty. */\n max?: number;\n /** How many non-empty intensity steps. Default 4. */\n levels?: number;\n /** The scale's colour, as any CSS colour. Default `var(--brand)`; each step is this\n * mixed into `--bg-surface`, so the ramp follows the theme. */\n color?: string;\n /**\n * A CONTINUOUS scale from `colorFrom` (the smallest value) to `colorTo` (the busiest\n * day), instead of `levels` steps of `color` — keksdose's palette-store ramp,\n * `seqLow` → `seqHigh`. Both are needed; `color` is then unused. Two `#rrggbb`\n * colours are interpolated in JS, so the fill is a concrete hex and the day number's\n * ink (in `\"month\"`) is MEASURED against it; anything else (a `var()`) is mixed with\n * `color-mix()`. `levels` still sets `data-level` and the legend's swatch count.\n */\n colorFrom?: string;\n colorTo?: string;\n /**\n * A fill of the caller's own for a day with a value: handed the value and the\n * scale's `max`, returns any CSS colour. Wins over `color` and `colorFrom`/`colorTo`.\n * Also paints the legend, at `max × level / levels`.\n */\n fill?: (value: number, max: number) => string;\n /** The fill of a day with no value (or a value of 0). Default: a wash of the text\n * colour over the surface, which reads as a cell on both themes. */\n emptyColor?: string;\n /**\n * Where the window ENDS — the day `maxDays` counts back from, and the last one drawn:\n *\n * - `\"to\"` (default): on `to`.\n * - `\"today\"`: on today.\n * - `\"latest\"`: on the last day that can carry a figure — today, or a later day that\n * actually has a value. A window running into the future (a report's \"all\" preset\n * ends a year ahead) then spends its cells on the days a person is looking for,\n * not on a year of empty ones.\n * - a `\"YYYY-MM-DD\"`: on that day.\n *\n * Always kept inside `from`…`to`.\n */\n anchor?: \"to\" | \"today\" | \"latest\" | (string & {});\n /**\n * The most days to draw. A longer window keeps its LATEST `maxDays` and says how many\n * it left out (`labels.truncated`) rather than silently ending. Default: no cap.\n */\n maxDays?: number;\n /** Draw the Less … More legend. Default `true`. */\n legend?: boolean;\n /** BCP 47 tag for the dates, month and weekday names and numbers. */\n locale?: string;\n /** `0` = Sunday … `6` = Saturday. Default: the provider's, else the locale's. */\n weekStartsOn?: WeekDay;\n labels?: LabelOverride<CalendarHeatmapLabels>;\n className?: string;\n}\n\nconst DAY_NAME_FORMAT: Intl.DateTimeFormatOptions = {\n weekday: \"long\",\n day: \"numeric\",\n month: \"long\",\n year: \"numeric\",\n};\n\n/** The empty day: a wash of the text colour, like `--bg-hover`, so it reads as a cell\n * on both themes without claiming any value. */\nconst EMPTY_FILL = \"color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface))\";\n\nconst weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;\n\n/** Where the window ends for an {@link CalendarHeatmapProps.anchor}, kept in `[from, to]`.\n * `latestData` is the latest ISO date carrying a value, or `\"\"`. */\nexport function heatmapWindowEnd(\n from: string,\n to: string,\n anchor: string,\n latestData: string,\n today: string = toLocalIso(new Date()),\n): string {\n let end: string;\n if (anchor === \"today\") end = today;\n else if (anchor === \"latest\") end = latestData > today ? latestData : today;\n else if (anchor === \"to\" || !parseIsoDate(anchor)) end = to;\n else end = anchor;\n // ISO dates sort as strings, so the clamp is plain comparison.\n if (end > to) end = to;\n if (end < from) end = from;\n return end;\n}\n\n/** The step a value falls on: 0 for nothing, else 1…levels by its share of `max`. */\nexport function heatmapLevel(value: number, max: number, levels: number): number {\n if (!(value > 0) || !(max > 0) || levels < 1) return 0;\n return Math.min(levels, Math.max(1, Math.ceil((value / max) * levels)));\n}\n\n/** How much of `color` a step mixes into the surface: 20% for the first step, 100% for\n * the last — the floor keeps the smallest value visibly apart from an empty day. */\nfunction levelPercent(level: number, levels: number): number {\n if (levels <= 1) return 100;\n return Math.round(20 + (80 * (level - 1)) / (levels - 1));\n}\n\nfunction levelFill(level: number, levels: number, color: string): string {\n if (level === 0) return EMPTY_FILL;\n return `color-mix(in oklab, ${color} ${levelPercent(level, levels)}%, var(--bg-surface))`;\n}\n\n/**\n * Days as a grid of shaded squares — how much happened on each, at a glance\n * (a contribution graph, a spending calendar).\n *\n * **Every day is a cell you can reach.** The grid is a `role=\"grid\"` with ONE tab\n * stop (the roving tabindex, as in `MiniCalendar`); the arrows move by the grid's own\n * geometry — in `\"weeks\"` ↑/↓ are the previous/next day and ←/→ a week, in `\"month\"`\n * the other way round, mirrored in RTL — PageUp/PageDown move a month and Home/End go\n * to the window's first/last day. Each cell is named with its whole date AND its value,\n * because the colour is not something a screen reader can say, and carries\n * `data-day=\"YYYY-MM-DD\"` as its identity for tests and for the host. Note that in\n * `\"weeks\"` the cells are in DOM order by WEEKDAY ROW (all Mondays, then all\n * Tuesdays…), not by date — query by `data-day`, not by position.\n *\n * The tooltip is portalled (a year of squares always sits in a sideways scroller) and\n * `data-private` unless `sensitive={false}`. Cell size in `\"weeks\"` is the CSS variable\n * `--heatmap-cell` (default `0.75rem`): `style={{ \"--heatmap-cell\": \"1rem\" }}`.\n */\nexport function CalendarHeatmap({\n data,\n from,\n to,\n layout = \"weeks\",\n selected,\n onSelect,\n formatValue: formatValueProp,\n tooltip,\n sensitive = true,\n max: maxProp,\n levels = 4,\n color = \"var(--brand)\",\n colorFrom,\n colorTo,\n fill: fillProp,\n emptyColor,\n anchor = \"to\",\n maxDays,\n legend = true,\n locale: localeProp,\n weekStartsOn,\n labels: labelsProp,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: CalendarHeatmapProps) {\n const labels = useKitLabels(\"calendarHeatmap\", DEFAULT_CALENDAR_HEATMAP_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const providerWeekStart = useKitWeekStart();\n const weekStart: WeekDay = weekStartsOn ?? providerWeekStart ?? localeWeekStart(locale);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const formatValue = formatValueProp ?? ((v: number) => formatWithMinus(numberFormat, v));\n\n const values = useMemo(() => {\n const m = new Map<string, number>();\n for (const p of data) m.set(p.date, (m.get(p.date) ?? 0) + p.value);\n return m;\n }, [data]);\n\n // The window, trimmed from the FRONT: the days a person looks for are the recent\n // ones, and a cap that kept the oldest would end the calendar before today.\n const latestData = useMemo(() => {\n let latest = \"\";\n for (const [iso, v] of values) if (v !== 0 && iso > latest) latest = iso;\n return latest;\n }, [values]);\n const windowEnd = heatmapWindowEnd(from, to, anchor, latestData);\n\n const { days, dropped } = useMemo(() => {\n const start = parseIsoDate(from);\n const end = parseIsoDate(windowEnd);\n const list: Date[] = [];\n if (!start || !end || end < start) return { days: list, dropped: 0 };\n const cursor = new Date(end);\n const cap = maxDays && maxDays > 0 ? maxDays : Infinity;\n while (cursor >= start && list.length < cap) {\n list.unshift(new Date(cursor));\n cursor.setDate(cursor.getDate() - 1);\n }\n const first = list[0];\n const trimmed = first ? Math.round((first.getTime() - start.getTime()) / 86_400_000) : 0;\n return { days: list, dropped: Math.max(0, trimmed) };\n }, [from, windowEnd, maxDays]);\n\n const firstIso = days.length ? toLocalIso(days[0]) : \"\";\n const lastIso = days.length ? toLocalIso(days[days.length - 1]) : \"\";\n\n const max = useMemo(() => {\n if (maxProp !== undefined) return maxProp;\n let m = 0;\n for (const d of days) m = Math.max(m, values.get(toLocalIso(d)) ?? 0);\n return m;\n }, [maxProp, days, values]);\n\n const clampIso = (iso: string) => (iso < firstIso ? firstIso : iso > lastIso ? lastIso : iso);\n\n // The roving tab stop: the selected day, else the latest one.\n const [activeState, setActiveIso] = useState(() => selected || lastIso);\n const [lastSelected, setLastSelected] = useState(selected);\n if (lastSelected !== selected) {\n setLastSelected(selected);\n if (selected) setActiveIso(selected);\n }\n const activeIso = days.length ? clampIso(activeState || lastIso) : \"\";\n\n const gridRef = useRef<HTMLDivElement>(null);\n const scrollerRef = useRef<HTMLDivElement>(null);\n const pendingFocus = useRef<string | null>(null);\n useEffect(() => {\n const iso = pendingFocus.current;\n if (!iso) return;\n pendingFocus.current = null;\n gridRef.current?.querySelector<HTMLElement>(`[data-day=\"${iso}\"]`)?.focus();\n });\n\n // Open the sideways scroller on the latest weeks, not the oldest. In RTL the end is\n // on the left, where browsers count scrollLeft down from 0.\n //\n // And KEEP it there while the box changes size — a sidebar or a contents rail that\n // appears after mount narrows the scroller, which leaves scrollLeft where it was and\n // so shows older weeks than it opened on. The pin holds only while the view sits at\n // the end: a user who scrolls back through the year unpins it, and a resize then\n // leaves their position alone. (Scrolling back to the end pins it again.)\n useLayoutEffect(() => {\n const el = scrollerRef.current;\n if (!el || layout !== \"weeks\") return;\n const toEnd = () => {\n el.scrollLeft = dirOf(el) === \"rtl\" ? -el.scrollWidth : el.scrollWidth;\n };\n toEnd();\n if (typeof ResizeObserver === \"undefined\") return;\n // |scrollLeft| covers both directions: 0 → max in LTR, 0 → -max in RTL.\n let pinned = true;\n const onScroll = () => {\n pinned = Math.abs(el.scrollLeft) >= el.scrollWidth - el.clientWidth - 1;\n };\n const ro = new ResizeObserver(() => {\n if (pinned) toEnd();\n });\n el.addEventListener(\"scroll\", onScroll, { passive: true });\n ro.observe(el);\n return () => {\n ro.disconnect();\n el.removeEventListener(\"scroll\", onScroll);\n };\n }, [layout, firstIso, lastIso]);\n\n const formatDay = (d: Date) => d.toLocaleDateString(locale, DAY_NAME_FORMAT);\n\n const weekdayLabels = useMemo(() => {\n const ref = new Date(2024, 0, 7 + weekStart); // 7 January 2024 was a Sunday\n const week = Array.from({ length: 7 }, (_, i) => {\n const d = new Date(ref);\n d.setDate(ref.getDate() + i);\n return d;\n });\n const name = (d: Date, weekday: \"short\" | \"narrow\" | \"long\") => d.toLocaleDateString(locale, { weekday });\n // The label column is 2rem at 10px type, room for about four characters. Most\n // locales' \"short\" is three or four (\"Thu\", \"jeu.\", \"czw.\"), but some have no\n // abbreviation at all — Arabic's short Thursday IS \"الخميس\", which overran the box\n // and was cut. Such a locale takes the \"narrow\" form (\"خ\") for ALL seven, so the\n // column never mixes two forms; the full name is on the aria-label either way.\n const tooLong = week.some((d) => [...name(d, \"short\")].length > 4);\n return week.map((d) => ({ short: name(d, tooLong ? \"narrow\" : \"short\"), long: name(d, \"long\") }));\n }, [locale, weekStart]);\n\n const lead = days.length ? weekdayIndex(days[0], weekStart) : 0;\n const weekCount = Math.ceil((lead + days.length) / 7);\n /** The day at position `i` of the week-aligned sequence (blanks before the first). */\n const dayAt = (i: number): Date | null => (i >= lead && i - lead < days.length ? days[i - lead] : null);\n\n const move = (e: KeyboardEvent<HTMLElement>) => {\n const current = parseIsoDate(activeIso);\n if (!current) return;\n const next = new Date(current);\n const step = horizontalStep(e.key, e.currentTarget);\n // In \"weeks\" a row is a weekday, so sideways is a week and up/down is a day.\n const across = layout === \"weeks\" ? 7 : 1;\n const down = layout === \"weeks\" ? 1 : 7;\n switch (step ? \"horizontal\" : e.key) {\n case \"horizontal\":\n next.setDate(next.getDate() + step * across);\n break;\n case \"ArrowUp\":\n next.setDate(next.getDate() - down);\n break;\n case \"ArrowDown\":\n next.setDate(next.getDate() + down);\n break;\n case \"PageUp\":\n next.setTime(addMonthsClamped(next, -1).getTime());\n break;\n case \"PageDown\":\n next.setTime(addMonthsClamped(next, 1).getTime());\n break;\n case \"Home\":\n next.setTime(days[0].getTime());\n break;\n case \"End\":\n next.setTime(days[days.length - 1].getTime());\n break;\n default:\n return;\n }\n e.preventDefault();\n const iso = clampIso(toLocalIso(next));\n if (iso === activeIso) return;\n pendingFocus.current = iso;\n setActiveIso(iso);\n };\n\n const legendLevels = Array.from({ length: levels + 1 }, (_, l) => l);\n const empty = emptyColor ?? EMPTY_FILL;\n const continuous = colorFrom !== undefined && colorTo !== undefined;\n const hexRamp = continuous && parseHex(colorFrom) != null && parseHex(colorTo) != null;\n\n /** A day's fill, and the ink its number is set in (`\"month\"`). */\n const paint = (value: number, level: number): { fill: string; ink: string } => {\n if (level === 0) return { fill: empty, ink: \"var(--text-primary)\" };\n if (fillProp || continuous) {\n const t = max > 0 ? Math.min(1, Math.max(0, value / max)) : 1;\n const fill = fillProp\n ? fillProp(value, max)\n : hexRamp\n ? lerpHex(colorFrom!, colorTo!, t)\n : `color-mix(in oklab, ${colorTo} ${Math.round(t * 100)}%, ${colorFrom})`;\n // Measured where the fill is a colour; a mix of references cannot be measured,\n // and the page's own text colour is the safe answer there.\n const measured = textOn(fill);\n return { fill, ink: measured === \"currentColor\" ? \"var(--text-primary)\" : measured };\n }\n // Light text once the fill is the stronger half of the mix — `--bg-surface` is the\n // colour the scale is mixed INTO, so it is the one that contrasts with its far end\n // on either theme.\n const strong = levelPercent(level, levels) >= 45;\n return {\n fill: levelFill(level, levels, color),\n ink: strong ? \"var(--bg-surface)\" : \"var(--text-primary)\",\n };\n };\n // The legend's swatches: a value that lands on each step (the top one on `max`).\n const legendFill = (l: number) => paint(((max > 0 ? max : 1) * l) / levels, l).fill;\n\n const cell = (d: Date, wrapClass: string, cellClass: string, withNumber: boolean) => {\n const iso = toLocalIso(d);\n const raw = values.get(iso);\n const value = raw ?? 0;\n const level = heatmapLevel(value, max, levels);\n const { fill, ink } = paint(value, level);\n const formattedDate = formatDay(d);\n const formattedValue = formatValue(value);\n const name = labels.day(formattedDate, formattedValue);\n const label = tooltip\n ? tooltip({ iso, date: d, value, hasData: raw !== undefined, level, formattedDate, formattedValue })\n : name;\n const isSelected = selected === iso;\n const shared = {\n role: \"gridcell\",\n \"data-day\": iso,\n \"data-level\": level,\n tabIndex: iso === activeIso ? 0 : -1,\n \"aria-label\": name,\n \"aria-selected\": onSelect ? isSelected : undefined,\n onKeyDown: move,\n onFocus: () => {\n if (iso !== activeIso) setActiveIso(iso);\n },\n style: { background: fill, color: ink },\n className: cn(\n cellClass,\n isSelected && \"ring-2 ring-[var(--text-primary)]\",\n onSelect && \"cursor-pointer hover:ring-1 hover:ring-brand\",\n ),\n } as const;\n const inner = withNumber ? (\n <span aria-hidden className=\"text-caption leading-none tabular-nums\">\n {numberFormat.format(d.getDate())}\n </span>\n ) : null;\n return (\n <Tooltip key={iso} label={label} portal redact={sensitive} role=\"none\" className={wrapClass}>\n {onSelect ? (\n <button type=\"button\" {...shared} onClick={() => onSelect(iso)}>\n {inner}\n </button>\n ) : (\n <div {...shared}>{inner}</div>\n )}\n </Tooltip>\n );\n };\n\n const gridName = ariaLabel ?? labels.grid;\n let body: ReactNode;\n if (layout === \"weeks\") {\n const columns = { gridTemplateColumns: `var(--heatmap-label, 2rem) repeat(${weekCount}, var(--heatmap-cell, 0.75rem))` } as CSSProperties;\n const cellSize = \"size-[var(--heatmap-cell,0.75rem)]\";\n // A month's name over the first column that holds its 1st — or over the first\n // column, when the window opens far enough before the next month for it to fit.\n const monthLabels = Array.from({ length: weekCount }, (_, w) => {\n for (let r = 0; r < 7; r++) {\n const d = dayAt(w * 7 + r);\n if (d && (d.getDate() === 1 || (w === 0 && r === lead && d.getDate() <= 14))) {\n return d.toLocaleDateString(locale, { month: \"short\" });\n }\n }\n return \"\";\n });\n body = (\n <div ref={scrollerRef} className=\"overflow-x-auto pb-1\">\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"inline-grid gap-0.5\">\n {/* Decoration: every cell's name already carries its month. */}\n <div aria-hidden className=\"grid gap-0.5 text-micro leading-3 text-muted\" style={columns}>\n <span />\n {monthLabels.map((m, w) => (\n <span key={w} className=\"overflow-visible whitespace-nowrap\">\n {m}\n </span>\n ))}\n </div>\n {Array.from({ length: 7 }, (_, r) => (\n <div key={r} role=\"row\" className=\"grid gap-0.5\" style={columns}>\n <div\n role=\"rowheader\"\n aria-label={weekdayLabels[r].long}\n className=\"overflow-hidden pe-1 text-micro leading-3 text-muted\"\n >\n {/* Every other weekday, as a contribution graph does: seven labels\n at this size are a smear. The name is there for all seven. */}\n {r % 2 === 1 ? weekdayLabels[r].short : \"\"}\n </div>\n {Array.from({ length: weekCount }, (_, w) => {\n const d = dayAt(w * 7 + r);\n return d ? (\n cell(d, cellSize, cn(cellSize, \"block rounded-sm\"), false)\n ) : (\n <div key={`pad-${w}`} role=\"gridcell\" className={cellSize} />\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n } else {\n const rows = Array.from({ length: weekCount }, (_, w) => w);\n body = (\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"grid gap-1\">\n <div role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {weekdayLabels.map((w, i) => (\n <div\n key={i}\n role=\"columnheader\"\n aria-label={w.long}\n className=\"pb-0.5 text-center text-micro uppercase tracking-wide text-muted\"\n >\n {w.short}\n </div>\n ))}\n </div>\n {rows.map((w) => (\n <div key={w} role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {Array.from({ length: 7 }, (_, c) => {\n const d = dayAt(w * 7 + c);\n return d ? (\n cell(d, \"aspect-square w-full\", \"flex h-full w-full items-center justify-center rounded\", true)\n ) : (\n <div key={`pad-${c}`} role=\"gridcell\" className=\"aspect-square\" />\n );\n })}\n </div>\n ))}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"min-w-0\", className)}>\n {dropped > 0 && (\n <div className=\"mb-2 text-xs text-muted\">{labels.truncated(dropped)}</div>\n )}\n {body}\n {legend && (\n <div aria-hidden className=\"mt-2 flex items-center justify-end gap-1 text-micro text-muted\">\n <span className=\"me-0.5\">{labels.less}</span>\n {legendLevels.map((l) => (\n <span\n key={l}\n data-legend-level={l}\n className=\"size-2.5 rounded-sm\"\n style={{ background: legendFill(l) }}\n />\n ))}\n <span className=\"ms-0.5\">{labels.more}</span>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["_"]}
@@ -0,0 +1,10 @@
1
+ /** A colour as the page resolves it: a hex as it is, a bare `var(--token)` read off
2
+ * <html>, anything else (a `color-mix()`, a var with a fallback) null. */
3
+ export declare function resolveChartColor(color: string, style: Pick<CSSStyleDeclaration, "getPropertyValue">): string | null;
4
+ /**
5
+ * `(fill) => edge`: the ink stroke a filled mark in `fill` needs on this page, or
6
+ * `undefined` — only for a colour under 3:1 against the card or the page (WCAG 1.4.11).
7
+ * `fill` may be a hex or a bare `var(--chart-N)` / `var(--money-…)`; anything it cannot
8
+ * measure gets no edge.
9
+ */
10
+ export declare function useChartEdge(): (fill: string | undefined) => string | undefined;
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { useCallback, useSyncExternalStore } from "react";
3
+ import { chartEdge } from "../theme/chart-palette.js";
4
+ import { parseHex } from "../theme/color.js";
5
+ const WATCHED = [
6
+ "--bg-surface",
7
+ "--bg-page",
8
+ ...Array.from({ length: 9 }, (_, i) => `--chart-${i + 1}`),
9
+ "--chart-ink",
10
+ "--chart-muted",
11
+ "--money-income",
12
+ "--money-expense",
13
+ "--money-net",
14
+ "--money-neutral",
15
+ "--brand"
16
+ ];
17
+ function readWatched() {
18
+ const style = getComputedStyle(document.documentElement);
19
+ return WATCHED.map((name) => style.getPropertyValue(name).trim()).join(",");
20
+ }
21
+ function subscribe(onChange) {
22
+ const observer = new MutationObserver(onChange);
23
+ observer.observe(document.documentElement, {
24
+ attributes: true,
25
+ attributeFilter: ["class", "style", "data-theme", "data-contrast"]
26
+ });
27
+ return () => observer.disconnect();
28
+ }
29
+ const SERVER = "";
30
+ const BARE_VAR = /^var\(\s*(--[A-Za-z0-9_-]+)\s*\)$/;
31
+ function resolveChartColor(color, style) {
32
+ const trimmed = color.trim();
33
+ if (parseHex(trimmed)) return trimmed;
34
+ const m = BARE_VAR.exec(trimmed);
35
+ if (!m) return null;
36
+ const value = style.getPropertyValue(m[1]).trim();
37
+ return parseHex(value) ? value : null;
38
+ }
39
+ function useChartEdge() {
40
+ const snapshot = useSyncExternalStore(subscribe, readWatched, () => SERVER);
41
+ return useCallback(
42
+ (fill) => {
43
+ if (!fill || snapshot === SERVER || typeof document === "undefined") return void 0;
44
+ const style = getComputedStyle(document.documentElement);
45
+ const hex = resolveChartColor(fill, style);
46
+ if (!hex) return void 0;
47
+ const surfaces = ["--bg-surface", "--bg-page"].map((n) => style.getPropertyValue(n).trim());
48
+ return chartEdge(hex, surfaces);
49
+ },
50
+ [snapshot]
51
+ );
52
+ }
53
+ export {
54
+ resolveChartColor,
55
+ useChartEdge
56
+ };
57
+ //# sourceMappingURL=chart-edge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/chart-edge.ts"],"sourcesContent":["// The live side of `chartEdge` (theme/chart-palette.ts): which colours a FILLED mark is\n// painted with and which grounds it sits on, read off <html> as the page resolves them —\n// so the 1px ink edge follows a theme toggle and a palette switch without the chart\n// knowing either store. Shared by SeriesChart (bars, areas) and Treemap (tiles).\nimport { useCallback, useSyncExternalStore } from \"react\";\nimport { chartEdge } from \"../theme/chart-palette\";\nimport { parseHex } from \"../theme/color\";\n\n/** The tokens a theme or palette change can move, read as ONE string so\n * `useSyncExternalStore` compares a primitive (a fresh array would never be equal). */\nconst WATCHED = [\n \"--bg-surface\",\n \"--bg-page\",\n ...Array.from({ length: 9 }, (_, i) => `--chart-${i + 1}`),\n \"--chart-ink\",\n \"--chart-muted\",\n \"--money-income\",\n \"--money-expense\",\n \"--money-net\",\n \"--money-neutral\",\n \"--brand\",\n];\n\nfunction readWatched(): string {\n const style = getComputedStyle(document.documentElement);\n return WATCHED.map((name) => style.getPropertyValue(name).trim()).join(\",\");\n}\n\nfunction subscribe(onChange: () => void): () => void {\n const observer = new MutationObserver(onChange);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\", \"style\", \"data-theme\", \"data-contrast\"],\n });\n return () => observer.disconnect();\n}\n\n/** Server render: nothing measured, so no edge — the client adds it on hydration. */\nconst SERVER = \"\";\n\nconst BARE_VAR = /^var\\(\\s*(--[A-Za-z0-9_-]+)\\s*\\)$/;\n\n/** A colour as the page resolves it: a hex as it is, a bare `var(--token)` read off\n * <html>, anything else (a `color-mix()`, a var with a fallback) null. */\nexport function resolveChartColor(color: string, style: Pick<CSSStyleDeclaration, \"getPropertyValue\">): string | null {\n const trimmed = color.trim();\n if (parseHex(trimmed)) return trimmed;\n const m = BARE_VAR.exec(trimmed);\n if (!m) return null;\n const value = style.getPropertyValue(m[1]).trim();\n return parseHex(value) ? value : null;\n}\n\n/**\n * `(fill) => edge`: the ink stroke a filled mark in `fill` needs on this page, or\n * `undefined` — only for a colour under 3:1 against the card or the page (WCAG 1.4.11).\n * `fill` may be a hex or a bare `var(--chart-N)` / `var(--money-…)`; anything it cannot\n * measure gets no edge.\n */\nexport function useChartEdge(): (fill: string | undefined) => string | undefined {\n const snapshot = useSyncExternalStore(subscribe, readWatched, () => SERVER);\n return useCallback(\n (fill: string | undefined) => {\n if (!fill || snapshot === SERVER || typeof document === \"undefined\") return undefined;\n const style = getComputedStyle(document.documentElement);\n const hex = resolveChartColor(fill, style);\n if (!hex) return undefined;\n const surfaces = [\"--bg-surface\", \"--bg-page\"].map((n) => style.getPropertyValue(n).trim());\n return chartEdge(hex, surfaces);\n },\n [snapshot],\n );\n}\n"],"mappings":";AAIA,SAAS,aAAa,4BAA4B;AAClD,SAAS,iBAAiB;AAC1B,SAAS,gBAAgB;AAIzB,MAAM,UAAU;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,MAAM,WAAW,IAAI,CAAC,EAAE;AAAA,EACzD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,cAAsB;AAC7B,QAAM,QAAQ,iBAAiB,SAAS,eAAe;AACvD,SAAO,QAAQ,IAAI,CAAC,SAAS,MAAM,iBAAiB,IAAI,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG;AAC5E;AAEA,SAAS,UAAU,UAAkC;AACnD,QAAM,WAAW,IAAI,iBAAiB,QAAQ;AAC9C,WAAS,QAAQ,SAAS,iBAAiB;AAAA,IACzC,YAAY;AAAA,IACZ,iBAAiB,CAAC,SAAS,SAAS,cAAc,eAAe;AAAA,EACnE,CAAC;AACD,SAAO,MAAM,SAAS,WAAW;AACnC;AAGA,MAAM,SAAS;AAEf,MAAM,WAAW;AAIV,SAAS,kBAAkB,OAAe,OAAqE;AACpH,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,SAAS,OAAO,EAAG,QAAO;AAC9B,QAAM,IAAI,SAAS,KAAK,OAAO;AAC/B,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,QAAQ,MAAM,iBAAiB,EAAE,CAAC,CAAC,EAAE,KAAK;AAChD,SAAO,SAAS,KAAK,IAAI,QAAQ;AACnC;AAQO,SAAS,eAAiE;AAC/E,QAAM,WAAW,qBAAqB,WAAW,aAAa,MAAM,MAAM;AAC1E,SAAO;AAAA,IACL,CAAC,SAA6B;AAC5B,UAAI,CAAC,QAAQ,aAAa,UAAU,OAAO,aAAa,YAAa,QAAO;AAC5E,YAAM,QAAQ,iBAAiB,SAAS,eAAe;AACvD,YAAM,MAAM,kBAAkB,MAAM,KAAK;AACzC,UAAI,CAAC,IAAK,QAAO;AACjB,YAAM,WAAW,CAAC,gBAAgB,WAAW,EAAE,IAAI,CAAC,MAAM,MAAM,iBAAiB,CAAC,EAAE,KAAK,CAAC;AAC1F,aAAO,UAAU,KAAK,QAAQ;AAAA,IAChC;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AACF;","names":[]}
@@ -8,6 +8,7 @@ import {
8
8
  } from "recharts";
9
9
  import { cn } from "../lib/cn.js";
10
10
  import { isRtl } from "../lib/direction.js";
11
+ import { TYPOGRAPHIC_MINUS } from "../lib/minus.js";
11
12
  const ChartContext = createContext(null);
12
13
  function useChart() {
13
14
  const ctx = useContext(ChartContext);
@@ -162,7 +163,7 @@ function ChartTooltipContent({
162
163
  if (inRow && !shown) text = NO_VALUE;
163
164
  else if (formatValue) text = formatValue(raw);
164
165
  else if (raw == null) text = NO_VALUE;
165
- else text = valueFormatter ? valueFormatter(raw) : raw;
166
+ else text = valueFormatter ? valueFormatter(raw) : String(raw).replace(/-/g, TYPOGRAPHIC_MINUS);
166
167
  return /* @__PURE__ */ jsxs(
167
168
  "div",
168
169
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/chart.tsx"],"sourcesContent":["// shadcn/ui chart kit — vendored (MIT) and adapted for this app:\n// - Recharts 3 instead of 2\n// - the original keys its styling off shadcn theme tokens (--muted-foreground,\n// --border, bg-background …). This app has no shadcn token layer, so those\n// are mapped onto this kit's own tokens (--text-muted, --border, --bg-surface\n// …), which follow the active palette preset and flip with the theme by\n// themselves — hence no `dark:` variants anywhere below.\n// The value on show is the *structure*: config-driven colors injected as CSS\n// vars, muted axis/grid styling, and a polished tooltip/legend — the things\n// that make shadcn charts read as \"designed\" rather than \"default Recharts\".\nimport { createContext, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentProps, CSSProperties, ReactNode, RefObject } from \"react\";\nimport {\n Legend as RechartsLegend,\n ResponsiveContainer,\n Tooltip as RechartsTooltip,\n} from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { isRtl } from \"../lib/direction\";\n\nexport interface ChartSeriesConfig {\n label: ReactNode;\n color: string;\n}\n\nexport type ChartConfig = Record<string, ChartSeriesConfig>;\n\nconst ChartContext = createContext<ChartConfig | null>(null);\n\nexport function useChart(): ChartConfig {\n const ctx = useContext(ChartContext);\n if (!ctx) throw new Error(\"useChart must be used within a <ChartContainer />\");\n return ctx;\n}\n\ninterface ChartContainerProps extends ComponentProps<\"div\"> {\n config: ChartConfig;\n children: ComponentProps<typeof ResponsiveContainer>[\"children\"];\n}\n\n/**\n * The shell every chart in the kit is drawn inside.\n *\n * **The plot is not mirrored in RTL.** An x axis is a number line or a timeline, and\n * both run left-to-right in Hebrew and Arabic charts as much as in English ones; a\n * time axis flipped to run right-to-left is the surprise, not the courtesy. So no\n * `reversed` on any axis, and the SVG is pinned to `direction: ltr` — without that it\n * inherits the page's `rtl`, which flips what `text-anchor: end` means, and the tick\n * labels of a left-hand y axis are drawn INTO the plot instead of beside it. Text\n * inside the SVG still shapes by its own script; only the anchoring is physical.\n * The HTML around the plot — legend, tooltip, the zoom reset button — follows the\n * page's direction like any other markup. A consumer who does want a mirrored\n * category axis passes `reversed` on its own `XAxis`.\n */\nexport function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {\n const uid = useId();\n // The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be\n // a CSS identifier: one stray space or `#` and the browser discards the whole rule,\n // taking every series colour with it — silently, because an invalid rule is not an\n // error anywhere. `useId()` gives `:r0:`, and a consumer-supplied id gives whatever\n // they typed, so both are filtered rather than just the colons.\n const chartId = `chart-${(id ?? uid).replace(/[^A-Za-z0-9_-]/g, \"\")}`;\n return (\n <ChartContext.Provider value={config}>\n <div\n data-chart={chartId}\n className={cn(\n // touch-pan-y lets a vertical finger drag still scroll the page while\n // a horizontal drag is handed to recharts for tooltip scrubbing,\n // instead of the page scrolling underneath the gesture (feedback #292).\n \"flex w-full touch-pan-y justify-center text-xs\",\n // physical anchoring for the unmirrored plot (see the doc comment above)\n \"[&_.recharts-surface]:[direction:ltr]\",\n // muted axis labels + gridlines; one class each, since the tokens\n // already carry the light/dark values\n \"[&_.recharts-cartesian-axis-tick_text]:fill-muted\",\n \"[&_.recharts-cartesian-grid_line]:stroke-subtle/70\",\n // hairline hover cursor instead of the heavy default\n \"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-strong\",\n \"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--bg-hover)]\",\n // kill the focus outlines & white sector strokes Recharts adds\n \"[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent\",\n \"[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent\",\n className,\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {children}\n </ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\n/**\n * A config key, which becomes the tail of a custom-property NAME.\n *\n * `dangerouslySetInnerHTML` means what it says: nothing below is escaped by React, and\n * a `ChartConfig` is not necessarily written by hand — kastlan builds one from category\n * rows in its database, so these keys are user data. A key containing `}` closes this\n * rule, and whatever follows it in that key becomes a NEW rule — one that applies to\n * the whole document rather than to this one chart.\n */\nconst CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * A config colour, which becomes a declaration VALUE.\n *\n * Every form a palette actually produces: both hex spellings, the functional notations\n * — with the argument list restricted to the characters those functions can legally\n * contain, so no `;`, no `}` and no nested `url(` — and `var(--token)`, which is what\n * this kit's own `paletteFor`/`CHART_COLORS` return. `var()`'s optional FALLBACK is\n * deliberately not accepted: its second argument is an arbitrary value, i.e. the same\n * hole in a shape that looks safe.\n */\nconst CSS_COLOR =\n /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\\([0-9A-Za-z.,%/ -]*\\)|var\\(--[A-Za-z0-9_-]+\\))$/;\n\n/**\n * The colour an entry is painted with, or `undefined` when the style block refuses it.\n *\n * The one gate both sides go through: {@link ChartStyle} emits exactly these, and the\n * legend and tooltip swatches show exactly these. They used to read `config[key].color`\n * directly, so an entry the style block had skipped still showed its configured colour\n * in the key — a swatch promising a colour the series was never painted in.\n */\nfunction paintedColor(key: string, entry: ChartSeriesConfig | undefined): string | undefined {\n if (!entry?.color) return undefined;\n return CSS_IDENT.test(key) && CSS_COLOR.test(entry.color) ? entry.color : undefined;\n}\n\n// Inject each series colour as a `--color-<key>` custom property scoped to this\n// chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip\n// stay in sync from one source of truth.\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n const entries = Object.entries(config).filter(([key, c]) => {\n if (!c.color) return false;\n // Skipping the entry costs that one series its `var(--color-…)` and leaves the rest\n // of the chart painted; emitting it unchecked costs the page. The warning is so the\n // consumer finds out from their console rather than from a colourless bar.\n if (!CSS_IDENT.test(key)) {\n console.warn(`[ui-kit] chart config key is not a CSS identifier, skipped: ${key}`);\n return false;\n }\n if (!CSS_COLOR.test(c.color)) {\n console.warn(`[ui-kit] chart config colour for \"${key}\" is not a colour, skipped: ${c.color}`);\n return false;\n }\n return true;\n });\n if (!entries.length) return null;\n const css = `[data-chart=${id}] {\\n${entries\n .map(([key, c]) => ` --color-${key}: ${c.color};`)\n .join(\"\\n\")}\\n}`;\n return <style dangerouslySetInnerHTML={{ __html: css }} />;\n}\n\n/**\n * recharts' `Tooltip`, with `filterNull` defaulting to **false**.\n *\n * recharts drops every payload entry whose value is `null`/`undefined` before the\n * content sees it, so the missing-value path {@link ChartTooltipContent} documents —\n * the em dash, `formatValue(undefined)` — never ran under the default: a series with a\n * gap at the cursor simply vanished from the tooltip, which reads as \"this series does\n * not exist here\" rather than \"it has no value here\". Kept, the entry prints \"—\".\n *\n * The other half of that filter — dropping series drawn with `hide` — is redone by\n * `ChartTooltipContent` itself (honouring `includeHidden`), so a legend toggle still\n * takes a series out of the tooltip. Pass `filterNull` to get recharts' behaviour back.\n */\nexport function ChartTooltip(props: ComponentProps<typeof RechartsTooltip>) {\n return <RechartsTooltip filterNull={false} {...props} />;\n}\nexport const ChartLegend = RechartsLegend;\n\ninterface TooltipPayloadItem {\n dataKey?: string | number;\n name?: string | number;\n value?: number | string;\n color?: string;\n /** A series drawn with `hide` — dropped here, see {@link ChartTooltip}. */\n hide?: boolean;\n payload?: Record<string, unknown>;\n}\n\n/**\n * What a series has no value for at this point: a gap in a history, a projection that\n * has not happened yet, a field that did not exist at an earlier capture.\n *\n * An em dash rather than an empty cell, so the row still lines up with its siblings and\n * the reader can see that the series WAS asked and had nothing, rather than wondering\n * whether it was dropped. It is a mark, not a word, so there is nothing here to\n * translate and no label prop to resolve; a caller who wants words passes\n * `formatValue`, which is handed the `undefined` and decides.\n */\nconst NO_VALUE = \"—\";\n\n/**\n * The number this payload item carries, or `undefined` when it carries none.\n *\n * This was `item.value ?? 0`, so every one of the absences above printed a confident\n * \"0\" that appears nowhere in the data — a fabricated figure in a tooltip, which is the\n * one place a reader goes to check an exact number. keksdose found it and wrapped the\n * component rather than trust it. `\"\"` and a non-numeric string are the same absence\n * spelled differently (recharts hands a string value straight through from the data),\n * and are answered the same way.\n */\nfunction tooltipValue(value: number | string | undefined): number | undefined {\n if (value == null || value === \"\") return undefined;\n const n = typeof value === \"string\" ? Number(value) : value;\n return Number.isFinite(n) ? n : undefined;\n}\n\ninterface ChartTooltipContentProps {\n active?: boolean;\n payload?: TooltipPayloadItem[];\n label?: string | number;\n indicator?: \"dot\" | \"line\";\n hideLabel?: boolean;\n labelFormatter?: (label: string | number) => ReactNode;\n /** Formats one series' value for display. NOT called when the series has no value at\n * this point — {@link ChartTooltipContentProps.formatValue} is the one that is. */\n valueFormatter?: (value: number) => ReactNode;\n /** Formats one series' value INCLUDING its absence: handed `undefined` for a gap, a\n * projection that has not happened yet, or a field that did not exist at an earlier\n * capture, and whatever it returns is rendered as-is. Only the caller knows whether\n * that absence reads as a dash, a \"not yet\" or a genuine zero, so this is how it\n * says. Supplying it wins over `valueFormatter` for every value.\n *\n * It sits NEXT to `valueFormatter` rather than widening it because that prop's\n * parameter is contextually typed at every call site there is — `(v) => money(v)` —\n * so widening it to `number | undefined` is a compile error in all three consuming\n * apps, which is a breaking change this module does not get to make by itself. */\n formatValue?: (value: number | undefined) => ReactNode;\n /** Cursor position within the chart, injected by recharts. Used with\n * {@link boundaryRef} to decide whether to flip the tooltip. */\n coordinate?: { x?: number; y?: number };\n /** When set, the tooltip flips to the LEFT of the cursor if it would otherwise\n * spill past the right edge of this (usually horizontally-scrolling) container\n * — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */\n boundaryRef?: RefObject<HTMLElement | null>;\n /** Injected by recharts: keep series drawn with `hide` in the tooltip. */\n includeHidden?: boolean;\n /**\n * `\"floating\"` (the default): the box recharts moves with the cursor. `\"readout\"`: the\n * same heading and values as a flat row for a FIXED place outside the plot — a\n * `SeriesChart` whose `tooltip.placement` is `\"above\"`/`\"below\"` portals it there, so\n * a finger on a phone no longer sits on the box it is trying to read (keksdose #359).\n * No border, shadow, width cap or viewport nudge; the values flow in a grid whose\n * column count depends on the row's width alone, so the row is the same height\n * whatever is being pointed at — and, idle, it shows {@link idleKeys} with \"—\".\n */\n variant?: \"floating\" | \"readout\";\n /** Readout only: the series to list while nothing is active, so the row keeps the\n * height it will have once something is. */\n idleKeys?: readonly string[];\n /** Readout only: the heading while nothing is active. Default \"—\". */\n idleLabel?: ReactNode;\n}\n\nexport function ChartTooltipContent({\n active,\n payload,\n label,\n indicator = \"dot\",\n hideLabel = false,\n labelFormatter,\n valueFormatter,\n formatValue,\n coordinate,\n boundaryRef,\n includeHidden = false,\n variant = \"floating\",\n idleKeys,\n idleLabel,\n}: ChartTooltipContentProps) {\n const config = useChart();\n const tipRef = useRef<HTMLDivElement>(null);\n const readout = variant === \"readout\";\n // `ChartTooltip` keeps null values (so they can print \"—\"), which also keeps the\n // series a legend toggle switched off with `hide`; those go here instead.\n const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);\n const shown = Boolean(active && items?.length);\n // Everything below that measures and moves the box is about a FLOATING one.\n const visible = shown && !readout;\n // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart\n // width; subtract the container's scrollLeft to get its on-screen x, then flip\n // left if the tooltip's own width would run past the visible right edge.\n //\n // Both the cursor and the flip are PHYSICAL (the plot is never mirrored, see\n // `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —\n // 0 at the start, negative going left — so there the content's left edge sits\n // `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.\n //\n // Measured in a layout effect (the DOM is not readable during render), so it commits\n // before paint. `payload` and `label` are deps because they change the tooltip's own\n // width, and recharts hands a fresh `payload` on every tooltip render, so a scroll\n // followed by any re-render is re-measured just as it was when this ran in render.\n const [flip, setFlip] = useState(false);\n const x = coordinate?.x;\n useLayoutEffect(() => {\n const boundary = boundaryRef?.current;\n if (!visible || !boundary || x == null) {\n setFlip(false);\n return;\n }\n const offset = isRtl(boundary)\n ? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft\n : boundary.scrollLeft;\n const visibleX = x - offset;\n const tipWidth = tipRef.current?.offsetWidth ?? 160;\n setFlip(visibleX + tipWidth + 12 > boundary.clientWidth);\n }, [visible, x, boundaryRef, payload, label]);\n\n // Whatever placed it — recharts' own rule or the boundary flip above — the box then\n // clamps itself into the viewport, 8px off each edge. The width cap alone did not\n // keep it on screen: on a 390px phone recharts opened a near-full-width tooltip to\n // the RIGHT of a pointer 35% into the plot, and the values ran off the edge\n // (lenkbank, on 0.15.2). Measured after placement, minus the shift it already has,\n // so a re-measure never compounds; an unmeasured box (no layout) is left alone.\n const [nudge, setNudge] = useState(0);\n const nudgeRef = useRef(0);\n // recharts slides its wrapper to each new position with a CSS transition, so the box\n // measured on commit is still where it WAS; measure again once the slide ends.\n const [settled, setSettled] = useState(0);\n useEffect(() => {\n const wrapper = tipRef.current?.closest<HTMLElement>(\".recharts-tooltip-wrapper\");\n if (!visible || !wrapper) return;\n const onEnd = () => setSettled((n) => n + 1);\n wrapper.addEventListener(\"transitionend\", onEnd);\n return () => wrapper.removeEventListener(\"transitionend\", onEnd);\n }, [visible]);\n useLayoutEffect(() => {\n const el = tipRef.current;\n if (!visible || !el) {\n nudgeRef.current = 0;\n setNudge(0);\n return;\n }\n const rect = el.getBoundingClientRect();\n if (rect.width === 0) return;\n const margin = 8;\n const viewport = document.documentElement.clientWidth || window.innerWidth;\n const left = rect.left - nudgeRef.current;\n const right = left + rect.width;\n let next = 0;\n if (right > viewport - margin) next = viewport - margin - right;\n if (left + next < margin) next = margin - left;\n if (next !== nudgeRef.current) {\n nudgeRef.current = next;\n setNudge(next);\n }\n }, [visible, x, payload, label, flip, settled]);\n // One series' line — the floating box stacks them; the readout lays them in a grid,\n // where a long name is cut rather than wrapped so every cell stays one line high.\n const tooltipRow = (item: TooltipPayloadItem, i: number, inRow: boolean) => {\n // Bars/lines/areas identify a series by dataKey; pies/treemaps key off\n // `name` (the nameKey value). Prefer whichever the config knows.\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const nm = item.name != null ? String(item.name) : undefined;\n const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : (dk ?? nm ?? String(i));\n const series = config[cfgKey];\n const fill = typeof item.payload?.fill === \"string\" ? item.payload.fill : undefined;\n // The data point's own `fill` before recharts' `item.color`: a treemap tile,\n // or a bar with a per-cell colour, IS the colour the reader is pointing at,\n // while `item.color` is the series-wide one — every tile's swatch came out\n // `--chart-1` whatever colour the tile was.\n const color = paintedColor(cfgKey, series) ?? fill ?? item.color ?? \"var(--text-muted)\";\n const name = series?.label ?? nm ?? cfgKey;\n const raw = tooltipValue(item.value);\n let text: ReactNode;\n // An idle readout cell has asked nothing yet, so there is no absence for\n // `formatValue` to word — it is the bare mark.\n if (inRow && !shown) text = NO_VALUE;\n else if (formatValue) text = formatValue(raw);\n else if (raw == null) text = NO_VALUE;\n else text = valueFormatter ? valueFormatter(raw) : raw;\n return (\n <div\n key={`${cfgKey}-${i}`}\n // A readout cell is capped so a wide row does not push its value a column away\n // from its name.\n className={cn(\"flex items-center\", inRow ? \"min-w-0 max-w-[12rem] gap-1.5\" : \"gap-2\")}\n >\n <span\n className={cn(\n \"shrink-0 rounded-[2px]\",\n indicator === \"dot\" ? \"h-2.5 w-2.5 rounded-full\" : \"h-2.5 w-1\",\n )}\n style={{ backgroundColor: color }}\n />\n <span\n className={cn(\"min-w-0 text-muted\", inRow ? \"truncate\" : \"break-words\")}\n >\n {name}\n </span>\n <span\n data-private\n // Isolated, so an RTL page does not turn \"-26.6\" into \"26.6-\": a figure has no\n // strong direction of its own, and one formatted with an RTL mark keeps it.\n dir=\"auto\"\n className=\"ms-auto shrink-0 whitespace-nowrap font-mono font-medium tabular-nums text-primary\"\n >\n {text}\n </span>\n </div>\n );\n };\n\n if (readout) {\n // Idle, the row lists every series with no value — the same cells it will have, so\n // nothing under it moves on the first tap.\n const rows: TooltipPayloadItem[] = shown\n ? items!\n : (idleKeys ?? Object.keys(config)).map((key) => ({ dataKey: key }));\n return (\n <div className=\"text-xs\" data-chart-readout={shown ? \"active\" : \"idle\"}>\n {/* A full 24 px line: the zoom's reset button sits at the top corner of a chart\n whose row is above it, and has to fit beside the heading, not over a value. */}\n <div data-readout-heading className=\"truncate font-medium leading-6 text-primary\">\n {shown && !hideLabel && label != null\n ? labelFormatter\n ? labelFormatter(label)\n : label\n : (idleLabel ?? NO_VALUE)}\n </div>\n <div className=\"grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-x-4 gap-y-1\">\n {rows.map((item, i) => tooltipRow(item, i, true))}\n </div>\n </div>\n );\n }\n if (!visible || !items) return null;\n return (\n <div\n ref={tipRef}\n style={\n boundaryRef || nudge\n ? {\n transform: [\n boundaryRef ? (flip ? \"translateX(calc(-100% - 12px))\" : \"translateX(12px)\") : \"\",\n nudge ? `translateX(${nudge}px)` : \"\",\n ]\n .filter(Boolean)\n .join(\" \"),\n }\n : undefined\n }\n // Capped at the viewport: six long series names (lenkbank's German motion\n // labels) made the box wider than a 390px chart, and recharts, which keeps a box\n // inside the plot only when it fits, let it run off the edge with the values cut.\n // Names wrap inside the cap; the values never do.\n className=\"min-w-[9rem] max-w-[min(20rem,calc(100vw-2rem))] rounded-lg border border-subtle bg-surface px-2.5 py-2 text-xs shadow-xl\"\n >\n {!hideLabel && label != null && (\n <div className=\"mb-1.5 font-medium text-primary\">\n {labelFormatter ? labelFormatter(label) : label}\n </div>\n )}\n <div className=\"grid gap-1.5\">{items.map((item, i) => tooltipRow(item, i, false))}</div>\n </div>\n );\n}\n\ninterface LegendPayloadItem {\n value?: string;\n dataKey?: string | number;\n color?: string;\n}\n\ninterface ChartLegendContentProps {\n payload?: LegendPayloadItem[];\n onItemClick?: (key: string) => void;\n /**\n * TOGGLE mode (recommended): the series currently switched off. Each entry is a\n * toggle button — pressed while its series is shown — and a hidden one is drawn\n * faded with its label struck through. Several can be off at once; pair with\n * `toggleHidden(hidden, key)` in `onItemClick` and `hide` on the chart's series.\n */\n hiddenKeys?: ReadonlySet<string> | readonly string[];\n /**\n * ISOLATE mode: the one series shown in full, every other entry dimmed. Kept for\n * the charts built on it; a legend where each entry switches its own series is\n * what readers expect from a legend, so prefer `hiddenKeys`.\n */\n activeKey?: string | null;\n}\n\nexport function ChartLegendContent({\n payload,\n onItemClick,\n hiddenKeys,\n activeKey,\n}: ChartLegendContentProps) {\n const hidden = hiddenKeys ? new Set(hiddenKeys) : null;\n const config = useChart();\n if (!payload?.length) return null;\n return (\n <div className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3\">\n {payload.map((item, i) => {\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const vv = item.value != null ? String(item.value) : undefined;\n const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));\n const series = config[cfgKey];\n const color = paintedColor(cfgKey, series) ?? item.color ?? \"var(--text-muted)\";\n const label = series?.label ?? vv ?? cfgKey;\n const off = hidden?.has(cfgKey) ?? false;\n const dimmed = off || (activeKey != null && activeKey !== cfgKey);\n const className = cn(\n \"flex items-center gap-1.5 text-secondary transition-opacity\",\n onItemClick ? \"cursor-pointer hover:opacity-80\" : \"cursor-default\",\n dimmed && \"opacity-40\",\n );\n const swatch = (\n <span\n aria-hidden\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{ backgroundColor: color } as CSSProperties}\n />\n );\n // A legend with no `onItemClick` is a KEY, not a control — and that is every\n // legend in kastlan, where this rendered N focusable buttons per chart that did\n // nothing when activated. A keyboard user paid the tab stops for all of them,\n // and a screen reader announced each one as a button, promising an action there\n // was none of. The interactive branch is unchanged.\n return onItemClick ? (\n <button\n key={`${cfgKey}-${i}`}\n type=\"button\"\n onClick={() => onItemClick(cfgKey)}\n // Toggles report their state: in toggle mode pressed = the series is shown;\n // in isolate mode pressed = the isolated one. With neither supplied the\n // legend knows nothing about state, and a pressed=\"false\" it cannot vouch\n // for would lie — so it says nothing.\n aria-pressed={\n hidden ? !off : activeKey === undefined ? undefined : activeKey === cfgKey\n }\n className={className}\n >\n {swatch}\n <span className={cn(off && \"line-through\")}>{label}</span>\n </button>\n ) : (\n <span key={`${cfgKey}-${i}`} className={className}>\n {swatch}\n {label}\n </span>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AAgEM,SAuBE,KAvBF;AAtDN,SAAS,eAAe,YAAY,WAAW,OAAO,iBAAiB,QAAQ,gBAAgB;AAE/F;AAAA,EACE,UAAU;AAAA,EACV;AAAA,EACA,WAAW;AAAA,OACN;AACP,SAAS,UAAU;AACnB,SAAS,aAAa;AAStB,MAAM,eAAe,cAAkC,IAAI;AAEpD,SAAS,WAAwB;AACtC,QAAM,MAAM,WAAW,YAAY;AACnC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,mDAAmD;AAC7E,SAAO;AACT;AAqBO,SAAS,eAAe,EAAE,IAAI,WAAW,UAAU,QAAQ,GAAG,MAAM,GAAwB;AACjG,QAAM,MAAM,MAAM;AAMlB,QAAM,UAAU,UAAU,MAAM,KAAK,QAAQ,mBAAmB,EAAE,CAAC;AACnE,SACE,oBAAC,aAAa,UAAb,EAAsB,OAAO,QAC5B;AAAA,IAAC;AAAA;AAAA,MACC,cAAY;AAAA,MACZ,WAAW;AAAA;AAAA;AAAA;AAAA,QAIT;AAAA;AAAA,QAEA;AAAA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA;AAAA,QAEA;AAAA,QACA;AAAA;AAAA,QAEA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,4BAAC,cAAW,IAAI,SAAS,QAAgB;AAAA,QACzC,oBAAC,uBAAoB,OAAM,QAAO,QAAO,QACtC,UACH;AAAA;AAAA;AAAA,EACF,GACF;AAEJ;AAWA,MAAM,YAAY;AAYlB,MAAM,YACJ;AAUF,SAAS,aAAa,KAAa,OAA0D;AAC3F,MAAI,CAAC,OAAO,MAAO,QAAO;AAC1B,SAAO,UAAU,KAAK,GAAG,KAAK,UAAU,KAAK,MAAM,KAAK,IAAI,MAAM,QAAQ;AAC5E;AAKA,SAAS,WAAW,EAAE,IAAI,OAAO,GAAwC;AACvE,QAAM,UAAU,OAAO,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM;AAC1D,QAAI,CAAC,EAAE,MAAO,QAAO;AAIrB,QAAI,CAAC,UAAU,KAAK,GAAG,GAAG;AACxB,cAAQ,KAAK,+DAA+D,GAAG,EAAE;AACjF,aAAO;AAAA,IACT;AACA,QAAI,CAAC,UAAU,KAAK,EAAE,KAAK,GAAG;AAC5B,cAAQ,KAAK,qCAAqC,GAAG,+BAA+B,EAAE,KAAK,EAAE;AAC7F,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,CAAC;AACD,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,QAAM,MAAM,eAAe,EAAE;AAAA,EAAQ,QAClC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,KAAK,GAAG,EACjD,KAAK,IAAI,CAAC;AAAA;AACb,SAAO,oBAAC,WAAM,yBAAyB,EAAE,QAAQ,IAAI,GAAG;AAC1D;AAeO,SAAS,aAAa,OAA+C;AAC1E,SAAO,oBAAC,mBAAgB,YAAY,OAAQ,GAAG,OAAO;AACxD;AACO,MAAM,cAAc;AAsB3B,MAAM,WAAW;AAYjB,SAAS,aAAa,OAAwD;AAC5E,MAAI,SAAS,QAAQ,UAAU,GAAI,QAAO;AAC1C,QAAM,IAAI,OAAO,UAAU,WAAW,OAAO,KAAK,IAAI;AACtD,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAiDO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,SAAS,SAAS;AACxB,QAAM,SAAS,OAAuB,IAAI;AAC1C,QAAM,UAAU,YAAY;AAG5B,QAAM,QAAQ,gBAAgB,UAAU,SAAS,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI;AACpF,QAAM,QAAQ,QAAQ,UAAU,OAAO,MAAM;AAE7C,QAAM,UAAU,SAAS,CAAC;AAc1B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,IAAI,YAAY;AACtB,kBAAgB,MAAM;AACpB,UAAM,WAAW,aAAa;AAC9B,QAAI,CAAC,WAAW,CAAC,YAAY,KAAK,MAAM;AACtC,cAAQ,KAAK;AACb;AAAA,IACF;AACA,UAAM,SAAS,MAAM,QAAQ,IACzB,SAAS,cAAc,SAAS,cAAc,SAAS,aACvD,SAAS;AACb,UAAM,WAAW,IAAI;AACrB,UAAM,WAAW,OAAO,SAAS,eAAe;AAChD,YAAQ,WAAW,WAAW,KAAK,SAAS,WAAW;AAAA,EACzD,GAAG,CAAC,SAAS,GAAG,aAAa,SAAS,KAAK,CAAC;AAQ5C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,CAAC;AACpC,QAAM,WAAW,OAAO,CAAC;AAGzB,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,CAAC;AACxC,YAAU,MAAM;AACd,UAAM,UAAU,OAAO,SAAS,QAAqB,2BAA2B;AAChF,QAAI,CAAC,WAAW,CAAC,QAAS;AAC1B,UAAM,QAAQ,MAAM,WAAW,CAAC,MAAM,IAAI,CAAC;AAC3C,YAAQ,iBAAiB,iBAAiB,KAAK;AAC/C,WAAO,MAAM,QAAQ,oBAAoB,iBAAiB,KAAK;AAAA,EACjE,GAAG,CAAC,OAAO,CAAC;AACZ,kBAAgB,MAAM;AACpB,UAAM,KAAK,OAAO;AAClB,QAAI,CAAC,WAAW,CAAC,IAAI;AACnB,eAAS,UAAU;AACnB,eAAS,CAAC;AACV;AAAA,IACF;AACA,UAAM,OAAO,GAAG,sBAAsB;AACtC,QAAI,KAAK,UAAU,EAAG;AACtB,UAAM,SAAS;AACf,UAAM,WAAW,SAAS,gBAAgB,eAAe,OAAO;AAChE,UAAM,OAAO,KAAK,OAAO,SAAS;AAClC,UAAM,QAAQ,OAAO,KAAK;AAC1B,QAAI,OAAO;AACX,QAAI,QAAQ,WAAW,OAAQ,QAAO,WAAW,SAAS;AAC1D,QAAI,OAAO,OAAO,OAAQ,QAAO,SAAS;AAC1C,QAAI,SAAS,SAAS,SAAS;AAC7B,eAAS,UAAU;AACnB,eAAS,IAAI;AAAA,IACf;AAAA,EACF,GAAG,CAAC,SAAS,GAAG,SAAS,OAAO,MAAM,OAAO,CAAC;AAG9C,QAAM,aAAa,CAAC,MAA0B,GAAW,UAAmB;AAG1E,UAAM,KAAK,KAAK,WAAW,OAAO,OAAO,KAAK,OAAO,IAAI;AACzD,UAAM,KAAK,KAAK,QAAQ,OAAO,OAAO,KAAK,IAAI,IAAI;AACnD,UAAM,SAAS,MAAM,OAAO,EAAE,IAAI,KAAK,MAAM,OAAO,EAAE,IAAI,KAAM,MAAM,MAAM,OAAO,CAAC;AACpF,UAAM,SAAS,OAAO,MAAM;AAC5B,UAAM,OAAO,OAAO,KAAK,SAAS,SAAS,WAAW,KAAK,QAAQ,OAAO;AAK1E,UAAM,QAAQ,aAAa,QAAQ,MAAM,KAAK,QAAQ,KAAK,SAAS;AACpE,UAAM,OAAO,QAAQ,SAAS,MAAM;AACpC,UAAM,MAAM,aAAa,KAAK,KAAK;AACnC,QAAI;AAGJ,QAAI,SAAS,CAAC,MAAO,QAAO;AAAA,aACnB,YAAa,QAAO,YAAY,GAAG;AAAA,aACnC,OAAO,KAAM,QAAO;AAAA,QACxB,QAAO,iBAAiB,eAAe,GAAG,IAAI;AACnD,WACE;AAAA,MAAC;AAAA;AAAA,QAIC,WAAW,GAAG,qBAAqB,QAAQ,kCAAkC,OAAO;AAAA,QAEpF;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,cAAc,QAAQ,6BAA6B;AAAA,cACrD;AAAA,cACA,OAAO,EAAE,iBAAiB,MAAM;AAAA;AAAA,UAClC;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,GAAG,sBAAsB,QAAQ,aAAa,aAAa;AAAA,cAErE;AAAA;AAAA,UACH;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,gBAAY;AAAA,cAGZ,KAAI;AAAA,cACJ,WAAU;AAAA,cAET;AAAA;AAAA,UACH;AAAA;AAAA;AAAA,MAzBK,GAAG,MAAM,IAAI,CAAC;AAAA,IA0BrB;AAAA,EAEJ;AAEA,MAAI,SAAS;AAGX,UAAM,OAA6B,QAC/B,SACC,YAAY,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,SAAS,IAAI,EAAE;AACrE,WACE,qBAAC,SAAI,WAAU,WAAU,sBAAoB,QAAQ,WAAW,QAG9D;AAAA,0BAAC,SAAI,wBAAoB,MAAC,WAAU,+CACjC,mBAAS,CAAC,aAAa,SAAS,OAC7B,iBACE,eAAe,KAAK,IACpB,QACD,aAAa,UACpB;AAAA,MACA,oBAAC,SAAI,WAAU,uEACZ,eAAK,IAAI,CAAC,MAAM,MAAM,WAAW,MAAM,GAAG,IAAI,CAAC,GAClD;AAAA,OACF;AAAA,EAEJ;AACA,MAAI,CAAC,WAAW,CAAC,MAAO,QAAO;AAC/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,OACE,eAAe,QACX;AAAA,QACE,WAAW;AAAA,UACT,cAAe,OAAO,mCAAmC,qBAAsB;AAAA,UAC/E,QAAQ,cAAc,KAAK,QAAQ;AAAA,QACrC,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,MACb,IACA;AAAA,MAMN,WAAU;AAAA,MAET;AAAA,SAAC,aAAa,SAAS,QACtB,oBAAC,SAAI,WAAU,mCACZ,2BAAiB,eAAe,KAAK,IAAI,OAC5C;AAAA,QAEF,oBAAC,SAAI,WAAU,gBAAgB,gBAAM,IAAI,CAAC,MAAM,MAAM,WAAW,MAAM,GAAG,KAAK,CAAC,GAAE;AAAA;AAAA;AAAA,EACpF;AAEJ;AA0BO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,SAAS,aAAa,IAAI,IAAI,UAAU,IAAI;AAClD,QAAM,SAAS,SAAS;AACxB,MAAI,CAAC,SAAS,OAAQ,QAAO;AAC7B,SACE,oBAAC,SAAI,WAAU,mEACZ,kBAAQ,IAAI,CAAC,MAAM,MAAM;AACxB,UAAM,KAAK,KAAK,WAAW,OAAO,OAAO,KAAK,OAAO,IAAI;AACzD,UAAM,KAAK,KAAK,SAAS,OAAO,OAAO,KAAK,KAAK,IAAI;AACrD,UAAM,SAAS,MAAM,OAAO,EAAE,IAAI,KAAK,MAAM,OAAO,EAAE,IAAI,KAAM,MAAM,MAAM,OAAO,CAAC;AACpF,UAAM,SAAS,OAAO,MAAM;AAC5B,UAAM,QAAQ,aAAa,QAAQ,MAAM,KAAK,KAAK,SAAS;AAC5D,UAAM,QAAQ,QAAQ,SAAS,MAAM;AACrC,UAAM,MAAM,QAAQ,IAAI,MAAM,KAAK;AACnC,UAAM,SAAS,OAAQ,aAAa,QAAQ,cAAc;AAC1D,UAAM,YAAY;AAAA,MAChB;AAAA,MACA,cAAc,oCAAoC;AAAA,MAClD,UAAU;AAAA,IACZ;AACA,UAAM,SACJ;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAU;AAAA,QACV,OAAO,EAAE,iBAAiB,MAAM;AAAA;AAAA,IAClC;AAOF,WAAO,cACL;AAAA,MAAC;AAAA;AAAA,QAEC,MAAK;AAAA,QACL,SAAS,MAAM,YAAY,MAAM;AAAA,QAKjC,gBACE,SAAS,CAAC,MAAM,cAAc,SAAY,SAAY,cAAc;AAAA,QAEtE;AAAA,QAEC;AAAA;AAAA,UACD,oBAAC,UAAK,WAAW,GAAG,OAAO,cAAc,GAAI,iBAAM;AAAA;AAAA;AAAA,MAb9C,GAAG,MAAM,IAAI,CAAC;AAAA,IAcrB,IAEA,qBAAC,UAA4B,WAC1B;AAAA;AAAA,MACA;AAAA,SAFQ,GAAG,MAAM,IAAI,CAAC,EAGzB;AAAA,EAEJ,CAAC,GACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/chart.tsx"],"sourcesContent":["// shadcn/ui chart kit — vendored (MIT) and adapted for this app:\n// - Recharts 3 instead of 2\n// - the original keys its styling off shadcn theme tokens (--muted-foreground,\n// --border, bg-background …). This app has no shadcn token layer, so those\n// are mapped onto this kit's own tokens (--text-muted, --border, --bg-surface\n// …), which follow the active palette preset and flip with the theme by\n// themselves — hence no `dark:` variants anywhere below.\n// The value on show is the *structure*: config-driven colors injected as CSS\n// vars, muted axis/grid styling, and a polished tooltip/legend — the things\n// that make shadcn charts read as \"designed\" rather than \"default Recharts\".\nimport { createContext, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentProps, CSSProperties, ReactNode, RefObject } from \"react\";\nimport {\n Legend as RechartsLegend,\n ResponsiveContainer,\n Tooltip as RechartsTooltip,\n} from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { isRtl } from \"../lib/direction\";\nimport { TYPOGRAPHIC_MINUS } from \"../lib/minus\";\n\nexport interface ChartSeriesConfig {\n label: ReactNode;\n color: string;\n}\n\nexport type ChartConfig = Record<string, ChartSeriesConfig>;\n\nconst ChartContext = createContext<ChartConfig | null>(null);\n\nexport function useChart(): ChartConfig {\n const ctx = useContext(ChartContext);\n if (!ctx) throw new Error(\"useChart must be used within a <ChartContainer />\");\n return ctx;\n}\n\ninterface ChartContainerProps extends ComponentProps<\"div\"> {\n config: ChartConfig;\n children: ComponentProps<typeof ResponsiveContainer>[\"children\"];\n}\n\n/**\n * The shell every chart in the kit is drawn inside.\n *\n * **The plot is not mirrored in RTL.** An x axis is a number line or a timeline, and\n * both run left-to-right in Hebrew and Arabic charts as much as in English ones; a\n * time axis flipped to run right-to-left is the surprise, not the courtesy. So no\n * `reversed` on any axis, and the SVG is pinned to `direction: ltr` — without that it\n * inherits the page's `rtl`, which flips what `text-anchor: end` means, and the tick\n * labels of a left-hand y axis are drawn INTO the plot instead of beside it. Text\n * inside the SVG still shapes by its own script; only the anchoring is physical.\n * The HTML around the plot — legend, tooltip, the zoom reset button — follows the\n * page's direction like any other markup. A consumer who does want a mirrored\n * category axis passes `reversed` on its own `XAxis`.\n */\nexport function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {\n const uid = useId();\n // The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be\n // a CSS identifier: one stray space or `#` and the browser discards the whole rule,\n // taking every series colour with it — silently, because an invalid rule is not an\n // error anywhere. `useId()` gives `:r0:`, and a consumer-supplied id gives whatever\n // they typed, so both are filtered rather than just the colons.\n const chartId = `chart-${(id ?? uid).replace(/[^A-Za-z0-9_-]/g, \"\")}`;\n return (\n <ChartContext.Provider value={config}>\n <div\n data-chart={chartId}\n className={cn(\n // touch-pan-y lets a vertical finger drag still scroll the page while\n // a horizontal drag is handed to recharts for tooltip scrubbing,\n // instead of the page scrolling underneath the gesture (feedback #292).\n \"flex w-full touch-pan-y justify-center text-xs\",\n // physical anchoring for the unmirrored plot (see the doc comment above)\n \"[&_.recharts-surface]:[direction:ltr]\",\n // muted axis labels + gridlines; one class each, since the tokens\n // already carry the light/dark values\n \"[&_.recharts-cartesian-axis-tick_text]:fill-muted\",\n \"[&_.recharts-cartesian-grid_line]:stroke-subtle/70\",\n // hairline hover cursor instead of the heavy default\n \"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-strong\",\n \"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--bg-hover)]\",\n // kill the focus outlines & white sector strokes Recharts adds\n \"[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent\",\n \"[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent\",\n className,\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {children}\n </ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\n/**\n * A config key, which becomes the tail of a custom-property NAME.\n *\n * `dangerouslySetInnerHTML` means what it says: nothing below is escaped by React, and\n * a `ChartConfig` is not necessarily written by hand — kastlan builds one from category\n * rows in its database, so these keys are user data. A key containing `}` closes this\n * rule, and whatever follows it in that key becomes a NEW rule — one that applies to\n * the whole document rather than to this one chart.\n */\nconst CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * A config colour, which becomes a declaration VALUE.\n *\n * Every form a palette actually produces: both hex spellings, the functional notations\n * — with the argument list restricted to the characters those functions can legally\n * contain, so no `;`, no `}` and no nested `url(` — and `var(--token)`, which is what\n * this kit's own `paletteFor`/`CHART_COLORS` return. `var()`'s optional FALLBACK is\n * deliberately not accepted: its second argument is an arbitrary value, i.e. the same\n * hole in a shape that looks safe.\n */\nconst CSS_COLOR =\n /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\\([0-9A-Za-z.,%/ -]*\\)|var\\(--[A-Za-z0-9_-]+\\))$/;\n\n/**\n * The colour an entry is painted with, or `undefined` when the style block refuses it.\n *\n * The one gate both sides go through: {@link ChartStyle} emits exactly these, and the\n * legend and tooltip swatches show exactly these. They used to read `config[key].color`\n * directly, so an entry the style block had skipped still showed its configured colour\n * in the key — a swatch promising a colour the series was never painted in.\n */\nfunction paintedColor(key: string, entry: ChartSeriesConfig | undefined): string | undefined {\n if (!entry?.color) return undefined;\n return CSS_IDENT.test(key) && CSS_COLOR.test(entry.color) ? entry.color : undefined;\n}\n\n// Inject each series colour as a `--color-<key>` custom property scoped to this\n// chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip\n// stay in sync from one source of truth.\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n const entries = Object.entries(config).filter(([key, c]) => {\n if (!c.color) return false;\n // Skipping the entry costs that one series its `var(--color-…)` and leaves the rest\n // of the chart painted; emitting it unchecked costs the page. The warning is so the\n // consumer finds out from their console rather than from a colourless bar.\n if (!CSS_IDENT.test(key)) {\n console.warn(`[ui-kit] chart config key is not a CSS identifier, skipped: ${key}`);\n return false;\n }\n if (!CSS_COLOR.test(c.color)) {\n console.warn(`[ui-kit] chart config colour for \"${key}\" is not a colour, skipped: ${c.color}`);\n return false;\n }\n return true;\n });\n if (!entries.length) return null;\n const css = `[data-chart=${id}] {\\n${entries\n .map(([key, c]) => ` --color-${key}: ${c.color};`)\n .join(\"\\n\")}\\n}`;\n return <style dangerouslySetInnerHTML={{ __html: css }} />;\n}\n\n/**\n * recharts' `Tooltip`, with `filterNull` defaulting to **false**.\n *\n * recharts drops every payload entry whose value is `null`/`undefined` before the\n * content sees it, so the missing-value path {@link ChartTooltipContent} documents —\n * the em dash, `formatValue(undefined)` — never ran under the default: a series with a\n * gap at the cursor simply vanished from the tooltip, which reads as \"this series does\n * not exist here\" rather than \"it has no value here\". Kept, the entry prints \"—\".\n *\n * The other half of that filter — dropping series drawn with `hide` — is redone by\n * `ChartTooltipContent` itself (honouring `includeHidden`), so a legend toggle still\n * takes a series out of the tooltip. Pass `filterNull` to get recharts' behaviour back.\n */\nexport function ChartTooltip(props: ComponentProps<typeof RechartsTooltip>) {\n return <RechartsTooltip filterNull={false} {...props} />;\n}\nexport const ChartLegend = RechartsLegend;\n\ninterface TooltipPayloadItem {\n dataKey?: string | number;\n name?: string | number;\n value?: number | string;\n color?: string;\n /** A series drawn with `hide` — dropped here, see {@link ChartTooltip}. */\n hide?: boolean;\n payload?: Record<string, unknown>;\n}\n\n/**\n * What a series has no value for at this point: a gap in a history, a projection that\n * has not happened yet, a field that did not exist at an earlier capture.\n *\n * An em dash rather than an empty cell, so the row still lines up with its siblings and\n * the reader can see that the series WAS asked and had nothing, rather than wondering\n * whether it was dropped. It is a mark, not a word, so there is nothing here to\n * translate and no label prop to resolve; a caller who wants words passes\n * `formatValue`, which is handed the `undefined` and decides.\n */\nconst NO_VALUE = \"—\";\n\n/**\n * The number this payload item carries, or `undefined` when it carries none.\n *\n * This was `item.value ?? 0`, so every one of the absences above printed a confident\n * \"0\" that appears nowhere in the data — a fabricated figure in a tooltip, which is the\n * one place a reader goes to check an exact number. keksdose found it and wrapped the\n * component rather than trust it. `\"\"` and a non-numeric string are the same absence\n * spelled differently (recharts hands a string value straight through from the data),\n * and are answered the same way.\n */\nfunction tooltipValue(value: number | string | undefined): number | undefined {\n if (value == null || value === \"\") return undefined;\n const n = typeof value === \"string\" ? Number(value) : value;\n return Number.isFinite(n) ? n : undefined;\n}\n\ninterface ChartTooltipContentProps {\n active?: boolean;\n payload?: TooltipPayloadItem[];\n label?: string | number;\n indicator?: \"dot\" | \"line\";\n hideLabel?: boolean;\n labelFormatter?: (label: string | number) => ReactNode;\n /** Formats one series' value for display. NOT called when the series has no value at\n * this point — {@link ChartTooltipContentProps.formatValue} is the one that is. */\n valueFormatter?: (value: number) => ReactNode;\n /** Formats one series' value INCLUDING its absence: handed `undefined` for a gap, a\n * projection that has not happened yet, or a field that did not exist at an earlier\n * capture, and whatever it returns is rendered as-is. Only the caller knows whether\n * that absence reads as a dash, a \"not yet\" or a genuine zero, so this is how it\n * says. Supplying it wins over `valueFormatter` for every value.\n *\n * It sits NEXT to `valueFormatter` rather than widening it because that prop's\n * parameter is contextually typed at every call site there is — `(v) => money(v)` —\n * so widening it to `number | undefined` is a compile error in all three consuming\n * apps, which is a breaking change this module does not get to make by itself. */\n formatValue?: (value: number | undefined) => ReactNode;\n /** Cursor position within the chart, injected by recharts. Used with\n * {@link boundaryRef} to decide whether to flip the tooltip. */\n coordinate?: { x?: number; y?: number };\n /** When set, the tooltip flips to the LEFT of the cursor if it would otherwise\n * spill past the right edge of this (usually horizontally-scrolling) container\n * — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */\n boundaryRef?: RefObject<HTMLElement | null>;\n /** Injected by recharts: keep series drawn with `hide` in the tooltip. */\n includeHidden?: boolean;\n /**\n * `\"floating\"` (the default): the box recharts moves with the cursor. `\"readout\"`: the\n * same heading and values as a flat row for a FIXED place outside the plot — a\n * `SeriesChart` whose `tooltip.placement` is `\"above\"`/`\"below\"` portals it there, so\n * a finger on a phone no longer sits on the box it is trying to read (keksdose #359).\n * No border, shadow, width cap or viewport nudge; the values flow in a grid whose\n * column count depends on the row's width alone, so the row is the same height\n * whatever is being pointed at — and, idle, it shows {@link idleKeys} with \"—\".\n */\n variant?: \"floating\" | \"readout\";\n /** Readout only: the series to list while nothing is active, so the row keeps the\n * height it will have once something is. */\n idleKeys?: readonly string[];\n /** Readout only: the heading while nothing is active. Default \"—\". */\n idleLabel?: ReactNode;\n}\n\nexport function ChartTooltipContent({\n active,\n payload,\n label,\n indicator = \"dot\",\n hideLabel = false,\n labelFormatter,\n valueFormatter,\n formatValue,\n coordinate,\n boundaryRef,\n includeHidden = false,\n variant = \"floating\",\n idleKeys,\n idleLabel,\n}: ChartTooltipContentProps) {\n const config = useChart();\n const tipRef = useRef<HTMLDivElement>(null);\n const readout = variant === \"readout\";\n // `ChartTooltip` keeps null values (so they can print \"—\"), which also keeps the\n // series a legend toggle switched off with `hide`; those go here instead.\n const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);\n const shown = Boolean(active && items?.length);\n // Everything below that measures and moves the box is about a FLOATING one.\n const visible = shown && !readout;\n // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart\n // width; subtract the container's scrollLeft to get its on-screen x, then flip\n // left if the tooltip's own width would run past the visible right edge.\n //\n // Both the cursor and the flip are PHYSICAL (the plot is never mirrored, see\n // `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —\n // 0 at the start, negative going left — so there the content's left edge sits\n // `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.\n //\n // Measured in a layout effect (the DOM is not readable during render), so it commits\n // before paint. `payload` and `label` are deps because they change the tooltip's own\n // width, and recharts hands a fresh `payload` on every tooltip render, so a scroll\n // followed by any re-render is re-measured just as it was when this ran in render.\n const [flip, setFlip] = useState(false);\n const x = coordinate?.x;\n useLayoutEffect(() => {\n const boundary = boundaryRef?.current;\n if (!visible || !boundary || x == null) {\n setFlip(false);\n return;\n }\n const offset = isRtl(boundary)\n ? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft\n : boundary.scrollLeft;\n const visibleX = x - offset;\n const tipWidth = tipRef.current?.offsetWidth ?? 160;\n setFlip(visibleX + tipWidth + 12 > boundary.clientWidth);\n }, [visible, x, boundaryRef, payload, label]);\n\n // Whatever placed it — recharts' own rule or the boundary flip above — the box then\n // clamps itself into the viewport, 8px off each edge. The width cap alone did not\n // keep it on screen: on a 390px phone recharts opened a near-full-width tooltip to\n // the RIGHT of a pointer 35% into the plot, and the values ran off the edge\n // (lenkbank, on 0.15.2). Measured after placement, minus the shift it already has,\n // so a re-measure never compounds; an unmeasured box (no layout) is left alone.\n const [nudge, setNudge] = useState(0);\n const nudgeRef = useRef(0);\n // recharts slides its wrapper to each new position with a CSS transition, so the box\n // measured on commit is still where it WAS; measure again once the slide ends.\n const [settled, setSettled] = useState(0);\n useEffect(() => {\n const wrapper = tipRef.current?.closest<HTMLElement>(\".recharts-tooltip-wrapper\");\n if (!visible || !wrapper) return;\n const onEnd = () => setSettled((n) => n + 1);\n wrapper.addEventListener(\"transitionend\", onEnd);\n return () => wrapper.removeEventListener(\"transitionend\", onEnd);\n }, [visible]);\n useLayoutEffect(() => {\n const el = tipRef.current;\n if (!visible || !el) {\n nudgeRef.current = 0;\n setNudge(0);\n return;\n }\n const rect = el.getBoundingClientRect();\n if (rect.width === 0) return;\n const margin = 8;\n const viewport = document.documentElement.clientWidth || window.innerWidth;\n const left = rect.left - nudgeRef.current;\n const right = left + rect.width;\n let next = 0;\n if (right > viewport - margin) next = viewport - margin - right;\n if (left + next < margin) next = margin - left;\n if (next !== nudgeRef.current) {\n nudgeRef.current = next;\n setNudge(next);\n }\n }, [visible, x, payload, label, flip, settled]);\n // One series' line — the floating box stacks them; the readout lays them in a grid,\n // where a long name is cut rather than wrapped so every cell stays one line high.\n const tooltipRow = (item: TooltipPayloadItem, i: number, inRow: boolean) => {\n // Bars/lines/areas identify a series by dataKey; pies/treemaps key off\n // `name` (the nameKey value). Prefer whichever the config knows.\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const nm = item.name != null ? String(item.name) : undefined;\n const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : (dk ?? nm ?? String(i));\n const series = config[cfgKey];\n const fill = typeof item.payload?.fill === \"string\" ? item.payload.fill : undefined;\n // The data point's own `fill` before recharts' `item.color`: a treemap tile,\n // or a bar with a per-cell colour, IS the colour the reader is pointing at,\n // while `item.color` is the series-wide one — every tile's swatch came out\n // `--chart-1` whatever colour the tile was.\n const color = paintedColor(cfgKey, series) ?? fill ?? item.color ?? \"var(--text-muted)\";\n const name = series?.label ?? nm ?? cfgKey;\n const raw = tooltipValue(item.value);\n let text: ReactNode;\n // An idle readout cell has asked nothing yet, so there is no absence for\n // `formatValue` to word — it is the bare mark.\n if (inRow && !shown) text = NO_VALUE;\n else if (formatValue) text = formatValue(raw);\n else if (raw == null) text = NO_VALUE;\n // The bare number's minus is U+2212, as every kit figure's (0.49, KIT-1).\n else text = valueFormatter ? valueFormatter(raw) : String(raw).replace(/-/g, TYPOGRAPHIC_MINUS);\n return (\n <div\n key={`${cfgKey}-${i}`}\n // A readout cell is capped so a wide row does not push its value a column away\n // from its name.\n className={cn(\"flex items-center\", inRow ? \"min-w-0 max-w-[12rem] gap-1.5\" : \"gap-2\")}\n >\n <span\n className={cn(\n \"shrink-0 rounded-[2px]\",\n indicator === \"dot\" ? \"h-2.5 w-2.5 rounded-full\" : \"h-2.5 w-1\",\n )}\n style={{ backgroundColor: color }}\n />\n <span\n className={cn(\"min-w-0 text-muted\", inRow ? \"truncate\" : \"break-words\")}\n >\n {name}\n </span>\n <span\n data-private\n // Isolated, so an RTL page does not turn \"-26.6\" into \"26.6-\": a figure has no\n // strong direction of its own, and one formatted with an RTL mark keeps it.\n dir=\"auto\"\n className=\"ms-auto shrink-0 whitespace-nowrap font-mono font-medium tabular-nums text-primary\"\n >\n {text}\n </span>\n </div>\n );\n };\n\n if (readout) {\n // Idle, the row lists every series with no value — the same cells it will have, so\n // nothing under it moves on the first tap.\n const rows: TooltipPayloadItem[] = shown\n ? items!\n : (idleKeys ?? Object.keys(config)).map((key) => ({ dataKey: key }));\n return (\n <div className=\"text-xs\" data-chart-readout={shown ? \"active\" : \"idle\"}>\n {/* A full 24 px line: the zoom's reset button sits at the top corner of a chart\n whose row is above it, and has to fit beside the heading, not over a value. */}\n <div data-readout-heading className=\"truncate font-medium leading-6 text-primary\">\n {shown && !hideLabel && label != null\n ? labelFormatter\n ? labelFormatter(label)\n : label\n : (idleLabel ?? NO_VALUE)}\n </div>\n <div className=\"grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-x-4 gap-y-1\">\n {rows.map((item, i) => tooltipRow(item, i, true))}\n </div>\n </div>\n );\n }\n if (!visible || !items) return null;\n return (\n <div\n ref={tipRef}\n style={\n boundaryRef || nudge\n ? {\n transform: [\n boundaryRef ? (flip ? \"translateX(calc(-100% - 12px))\" : \"translateX(12px)\") : \"\",\n nudge ? `translateX(${nudge}px)` : \"\",\n ]\n .filter(Boolean)\n .join(\" \"),\n }\n : undefined\n }\n // Capped at the viewport: six long series names (lenkbank's German motion\n // labels) made the box wider than a 390px chart, and recharts, which keeps a box\n // inside the plot only when it fits, let it run off the edge with the values cut.\n // Names wrap inside the cap; the values never do.\n className=\"min-w-[9rem] max-w-[min(20rem,calc(100vw-2rem))] rounded-lg border border-subtle bg-surface px-2.5 py-2 text-xs shadow-xl\"\n >\n {!hideLabel && label != null && (\n <div className=\"mb-1.5 font-medium text-primary\">\n {labelFormatter ? labelFormatter(label) : label}\n </div>\n )}\n <div className=\"grid gap-1.5\">{items.map((item, i) => tooltipRow(item, i, false))}</div>\n </div>\n );\n}\n\ninterface LegendPayloadItem {\n value?: string;\n dataKey?: string | number;\n color?: string;\n}\n\ninterface ChartLegendContentProps {\n payload?: LegendPayloadItem[];\n onItemClick?: (key: string) => void;\n /**\n * TOGGLE mode (recommended): the series currently switched off. Each entry is a\n * toggle button — pressed while its series is shown — and a hidden one is drawn\n * faded with its label struck through. Several can be off at once; pair with\n * `toggleHidden(hidden, key)` in `onItemClick` and `hide` on the chart's series.\n */\n hiddenKeys?: ReadonlySet<string> | readonly string[];\n /**\n * ISOLATE mode: the one series shown in full, every other entry dimmed. Kept for\n * the charts built on it; a legend where each entry switches its own series is\n * what readers expect from a legend, so prefer `hiddenKeys`.\n */\n activeKey?: string | null;\n}\n\nexport function ChartLegendContent({\n payload,\n onItemClick,\n hiddenKeys,\n activeKey,\n}: ChartLegendContentProps) {\n const hidden = hiddenKeys ? new Set(hiddenKeys) : null;\n const config = useChart();\n if (!payload?.length) return null;\n return (\n <div className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3\">\n {payload.map((item, i) => {\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const vv = item.value != null ? String(item.value) : undefined;\n const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));\n const series = config[cfgKey];\n const color = paintedColor(cfgKey, series) ?? item.color ?? \"var(--text-muted)\";\n const label = series?.label ?? vv ?? cfgKey;\n const off = hidden?.has(cfgKey) ?? false;\n const dimmed = off || (activeKey != null && activeKey !== cfgKey);\n const className = cn(\n \"flex items-center gap-1.5 text-secondary transition-opacity\",\n onItemClick ? \"cursor-pointer hover:opacity-80\" : \"cursor-default\",\n dimmed && \"opacity-40\",\n );\n const swatch = (\n <span\n aria-hidden\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{ backgroundColor: color } as CSSProperties}\n />\n );\n // A legend with no `onItemClick` is a KEY, not a control — and that is every\n // legend in kastlan, where this rendered N focusable buttons per chart that did\n // nothing when activated. A keyboard user paid the tab stops for all of them,\n // and a screen reader announced each one as a button, promising an action there\n // was none of. The interactive branch is unchanged.\n return onItemClick ? (\n <button\n key={`${cfgKey}-${i}`}\n type=\"button\"\n onClick={() => onItemClick(cfgKey)}\n // Toggles report their state: in toggle mode pressed = the series is shown;\n // in isolate mode pressed = the isolated one. With neither supplied the\n // legend knows nothing about state, and a pressed=\"false\" it cannot vouch\n // for would lie — so it says nothing.\n aria-pressed={\n hidden ? !off : activeKey === undefined ? undefined : activeKey === cfgKey\n }\n className={className}\n >\n {swatch}\n <span className={cn(off && \"line-through\")}>{label}</span>\n </button>\n ) : (\n <span key={`${cfgKey}-${i}`} className={className}>\n {swatch}\n {label}\n </span>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,6 @@
1
1
  import type { KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref } from "react";
2
2
  import type { LucideIcon } from "lucide-react";
3
+ import { type StatusKind } from "../lib/status-tone.js";
3
4
  import { type DisabledReasonDisplay } from "./field-parts.js";
4
5
  import { type CommitScope } from "./write-lock.js";
5
6
  export { refreshChipEdges } from "../lib/chip-edge-snap.js";
@@ -79,6 +80,12 @@ export interface ChipLinkProps {
79
80
  interface ChipBaseProps {
80
81
  children: ReactNode;
81
82
  tone?: ChipTone;
83
+ /**
84
+ * What the value means as a status (0.49 KIT-8), instead of a `tone`: `good` (success),
85
+ * `attention` (warning), `problem` (danger), `active` (info), `inactive` (neutral) —
86
+ * {@link statusTone}. An explicit `tone` wins. Never `brand` for a status.
87
+ */
88
+ status?: StatusKind;
82
89
  size?: ChipSize;
83
90
  /** See {@link ChipVariant}. Default `soft`. */
84
91
  variant?: ChipVariant;
@@ -264,4 +271,4 @@ export type ChipProps = ChipBaseProps & ({
264
271
  * focus ring is drawn on the body alone, and `ref`, `id` and the ARIA props still reach
265
272
  * the body — the interactive element.
266
273
  */
267
- export declare function Chip({ ref, children, tone, size, variant, shape, caps, icon: Icon, selected, checkbox, href, renderLink, onClick, onRemove, removeLabel, removeDisabled, disabled, className, tabIndex, role, onKeyDown, onFocus, snapEdges, disabledReason: ownDisabledReason, disabledReasonDisplay, commit, ...rest }: ChipProps): import("react").JSX.Element;
274
+ export declare function Chip({ ref, children, tone: toneProp, status, size, variant, shape, caps, icon: Icon, selected, checkbox, href, renderLink, onClick, onRemove, removeLabel, removeDisabled, disabled, className, tabIndex, role, onKeyDown, onFocus, snapEdges, disabledReason: ownDisabledReason, disabledReasonDisplay, commit, ...rest }: ChipProps): import("react").JSX.Element;