@eifi1/ui-kit 0.51.7 → 0.52.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 (746) hide show
  1. package/README.md +141 -23
  2. package/dist/account/account-parts.d.ts +10 -8
  3. package/dist/account/account-parts.js +1 -18
  4. package/dist/account/account-parts.js.map +1 -1
  5. package/dist/account/data-export-setting-labels.js +1 -1
  6. package/dist/account/data-export-setting-labels.js.map +1 -1
  7. package/dist/account/delete-account-setting-labels.js +1 -1
  8. package/dist/account/delete-account-setting-labels.js.map +1 -1
  9. package/dist/account/email-change-setting-labels.js +1 -1
  10. package/dist/account/email-change-setting-labels.js.map +1 -1
  11. package/dist/account/passkeys-setting.d.ts +9 -2
  12. package/dist/account/passkeys-setting.js +41 -29
  13. package/dist/account/passkeys-setting.js.map +1 -1
  14. package/dist/account/sessions-setting.d.ts +3 -2
  15. package/dist/account/sessions-setting.js +10 -8
  16. package/dist/account/sessions-setting.js.map +1 -1
  17. package/dist/admin/admin-action-confirm.js +1 -3
  18. package/dist/admin/admin-action-confirm.js.map +1 -1
  19. package/dist/admin/admin-parts.d.ts +6 -3
  20. package/dist/admin/admin-parts.js +6 -21
  21. package/dist/admin/admin-parts.js.map +1 -1
  22. package/dist/admin/mail-test-card-labels.js +1 -1
  23. package/dist/admin/mail-test-card-labels.js.map +1 -1
  24. package/dist/admin/plan-change-labels.d.ts +2 -2
  25. package/dist/admin/plan-change-labels.js +1 -17
  26. package/dist/admin/plan-change-labels.js.map +1 -1
  27. package/dist/admin/plan-change.d.ts +3 -42
  28. package/dist/admin/plan-change.js +1 -1
  29. package/dist/admin/plan-change.js.map +1 -1
  30. package/dist/admin/reviewer-scope-editor.js +3 -3
  31. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  32. package/dist/auth/auth-errors-labels.d.ts +11 -0
  33. package/dist/auth/auth-errors-labels.js +13 -0
  34. package/dist/auth/auth-errors-labels.js.map +1 -0
  35. package/dist/auth/auth-errors.d.ts +7 -23
  36. package/dist/auth/auth-errors.js +2 -15
  37. package/dist/auth/auth-errors.js.map +1 -1
  38. package/dist/auth/complete-name-dialog.js +2 -1
  39. package/dist/auth/complete-name-dialog.js.map +1 -1
  40. package/dist/auth/forgot-password-form-labels.js +1 -1
  41. package/dist/auth/forgot-password-form-labels.js.map +1 -1
  42. package/dist/auth/forgot-password-form.js +7 -2
  43. package/dist/auth/forgot-password-form.js.map +1 -1
  44. package/dist/auth/register-form-labels.js +1 -1
  45. package/dist/auth/register-form-labels.js.map +1 -1
  46. package/dist/auth/reset-password-form-labels.js +1 -1
  47. package/dist/auth/reset-password-form-labels.js.map +1 -1
  48. package/dist/auth/reset-password-form.js +5 -1
  49. package/dist/auth/reset-password-form.js.map +1 -1
  50. package/dist/auth/sign-in-form-labels.js +1 -1
  51. package/dist/auth/sign-in-form-labels.js.map +1 -1
  52. package/dist/auth/status-parts.d.ts +0 -3
  53. package/dist/auth/status-parts.js +0 -4
  54. package/dist/auth/status-parts.js.map +1 -1
  55. package/dist/auth/verify-email-labels.js +1 -1
  56. package/dist/auth/verify-email-labels.js.map +1 -1
  57. package/dist/auth/verify-email.js +7 -2
  58. package/dist/auth/verify-email.js.map +1 -1
  59. package/dist/billing/cancel-subscription.d.ts +20 -8
  60. package/dist/billing/cancel-subscription.js +7 -5
  61. package/dist/billing/cancel-subscription.js.map +1 -1
  62. package/dist/billing/dialog-close.d.ts +29 -0
  63. package/dist/billing/dialog-close.js +8 -0
  64. package/dist/billing/dialog-close.js.map +1 -0
  65. package/dist/billing/plan-card.d.ts +14 -4
  66. package/dist/billing/plan-card.js +15 -8
  67. package/dist/billing/plan-card.js.map +1 -1
  68. package/dist/billing/plan-change-labels.d.ts +42 -0
  69. package/dist/billing/plan-change-labels.js +22 -0
  70. package/dist/billing/plan-change-labels.js.map +1 -0
  71. package/dist/billing/plan-switch.d.ts +19 -7
  72. package/dist/billing/plan-switch.js +8 -4
  73. package/dist/billing/plan-switch.js.map +1 -1
  74. package/dist/chart.d.ts +12 -0
  75. package/dist/chart.js +16 -1
  76. package/dist/chart.js.map +1 -1
  77. package/dist/components/account-chips.d.ts +5 -5
  78. package/dist/components/account-chips.js +11 -13
  79. package/dist/components/account-chips.js.map +1 -1
  80. package/dist/components/alert-banner.d.ts +16 -5
  81. package/dist/components/alert-banner.js +46 -1
  82. package/dist/components/alert-banner.js.map +1 -1
  83. package/dist/components/amount-input.d.ts +13 -0
  84. package/dist/components/amount-input.js +1 -0
  85. package/dist/components/amount-input.js.map +1 -1
  86. package/dist/components/autocomplete.js +57 -65
  87. package/dist/components/autocomplete.js.map +1 -1
  88. package/dist/components/bulk-action-bar.d.ts +12 -2
  89. package/dist/components/bulk-action-bar.js +65 -21
  90. package/dist/components/bulk-action-bar.js.map +1 -1
  91. package/dist/components/button-look.d.ts +5 -5
  92. package/dist/components/button-look.js.map +1 -1
  93. package/dist/components/button.d.ts +2 -2
  94. package/dist/components/button.js.map +1 -1
  95. package/dist/components/chart-edge.d.ts +16 -0
  96. package/dist/components/chart-edge.js +14 -1
  97. package/dist/components/chart-edge.js.map +1 -1
  98. package/dist/components/chart-figure.d.ts +13 -0
  99. package/dist/components/chart-figure.js +15 -0
  100. package/dist/components/chart-figure.js.map +1 -0
  101. package/dist/components/chart-zoom-core.js +1 -4
  102. package/dist/components/chart-zoom-core.js.map +1 -1
  103. package/dist/components/chart.d.ts +8 -1
  104. package/dist/components/chart.js +19 -10
  105. package/dist/components/chart.js.map +1 -1
  106. package/dist/components/combobox-core.d.ts +111 -2
  107. package/dist/components/combobox-core.js +148 -42
  108. package/dist/components/combobox-core.js.map +1 -1
  109. package/dist/components/combobox.d.ts +1 -1
  110. package/dist/components/combobox.js +141 -130
  111. package/dist/components/combobox.js.map +1 -1
  112. package/dist/components/copy-button.d.ts +6 -5
  113. package/dist/components/copy-button.js +5 -3
  114. package/dist/components/copy-button.js.map +1 -1
  115. package/dist/components/country-select.js +2 -2
  116. package/dist/components/country-select.js.map +1 -1
  117. package/dist/components/data-table-column-settings.d.ts +1 -1
  118. package/dist/components/data-table-column-settings.js +33 -7
  119. package/dist/components/data-table-column-settings.js.map +1 -1
  120. package/dist/components/data-table-filters.d.ts +4 -2
  121. package/dist/components/data-table-filters.js +2 -2
  122. package/dist/components/data-table-filters.js.map +1 -1
  123. package/dist/components/data-table-fit.d.ts +6 -4
  124. package/dist/components/data-table-fit.js +4 -1
  125. package/dist/components/data-table-fit.js.map +1 -1
  126. package/dist/components/data-table-groups.d.ts +1 -1
  127. package/dist/components/data-table-groups.js.map +1 -1
  128. package/dist/components/data-table-head.d.ts +1 -1
  129. package/dist/components/data-table-head.js +15 -12
  130. package/dist/components/data-table-head.js.map +1 -1
  131. package/dist/components/data-table-mobile.d.ts +12 -7
  132. package/dist/components/data-table-mobile.js +43 -30
  133. package/dist/components/data-table-mobile.js.map +1 -1
  134. package/dist/components/data-table-pagination.js +11 -6
  135. package/dist/components/data-table-pagination.js.map +1 -1
  136. package/dist/components/data-table-resize.d.ts +1 -1
  137. package/dist/components/data-table-resize.js.map +1 -1
  138. package/dist/components/data-table-row-actions.d.ts +2 -2
  139. package/dist/components/data-table-row-actions.js.map +1 -1
  140. package/dist/components/data-table-row.d.ts +6 -2
  141. package/dist/components/data-table-row.js +17 -10
  142. package/dist/components/data-table-row.js.map +1 -1
  143. package/dist/components/data-table-selection.d.ts +1 -1
  144. package/dist/components/data-table-selection.js.map +1 -1
  145. package/dist/components/data-table-shared.d.ts +10 -2
  146. package/dist/components/data-table-shared.js +18 -0
  147. package/dist/components/data-table-shared.js.map +1 -1
  148. package/dist/components/data-table-sort.d.ts +21 -2
  149. package/dist/components/data-table-sort.js +24 -0
  150. package/dist/components/data-table-sort.js.map +1 -1
  151. package/dist/components/data-table-virtual.d.ts +1 -1
  152. package/dist/components/data-table-virtual.js +18 -6
  153. package/dist/components/data-table-virtual.js.map +1 -1
  154. package/dist/components/data-table.d.ts +13 -4
  155. package/dist/components/data-table.js +139 -36
  156. package/dist/components/data-table.js.map +1 -1
  157. package/dist/components/date-field.d.ts +24 -2
  158. package/dist/components/date-field.js +33 -8
  159. package/dist/components/date-field.js.map +1 -1
  160. package/dist/components/date-picker.d.ts +15 -2
  161. package/dist/components/date-picker.js +55 -26
  162. package/dist/components/date-picker.js.map +1 -1
  163. package/dist/components/date-range-picker.d.ts +47 -20
  164. package/dist/components/date-range-picker.js +23 -7
  165. package/dist/components/date-range-picker.js.map +1 -1
  166. package/dist/components/error-boundary.js +37 -30
  167. package/dist/components/error-boundary.js.map +1 -1
  168. package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
  169. package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
  170. package/dist/components/field-base-labels.js.map +1 -0
  171. package/dist/components/field-base.d.ts +4 -3
  172. package/dist/components/field-base.js +3 -3
  173. package/dist/components/field-base.js.map +1 -1
  174. package/dist/components/field-sync-labels.d.ts +1 -0
  175. package/dist/components/field-sync-labels.js.map +1 -1
  176. package/dist/components/field-sync.d.ts +23 -1
  177. package/dist/components/field-sync.js.map +1 -1
  178. package/dist/components/form-actions.js +11 -5
  179. package/dist/components/form-actions.js.map +1 -1
  180. package/dist/components/hover-menu.d.ts +3 -0
  181. package/dist/components/hover-menu.js +5 -3
  182. package/dist/components/hover-menu.js.map +1 -1
  183. package/dist/components/icon-button.js +14 -9
  184. package/dist/components/icon-button.js.map +1 -1
  185. package/dist/components/inline-confirm.d.ts +43 -0
  186. package/dist/components/inline-confirm.js +67 -0
  187. package/dist/components/inline-confirm.js.map +1 -0
  188. package/dist/components/inline-entity-combobox.d.ts +4 -3
  189. package/dist/components/inline-entity-combobox.js +148 -140
  190. package/dist/components/inline-entity-combobox.js.map +1 -1
  191. package/dist/components/input.js +1 -1
  192. package/dist/components/input.js.map +1 -1
  193. package/dist/components/jump-calendar.js +28 -112
  194. package/dist/components/jump-calendar.js.map +1 -1
  195. package/dist/components/legal-page.d.ts +3 -1
  196. package/dist/components/legal-page.js +3 -2
  197. package/dist/components/legal-page.js.map +1 -1
  198. package/dist/components/legal.d.ts +4 -2
  199. package/dist/components/legal.js.map +1 -1
  200. package/dist/components/lightbox.d.ts +2 -2
  201. package/dist/components/lightbox.js +17 -6
  202. package/dist/components/lightbox.js.map +1 -1
  203. package/dist/components/list.d.ts +6 -2
  204. package/dist/components/list.js +3 -2
  205. package/dist/components/list.js.map +1 -1
  206. package/dist/components/measured-grid.d.ts +1 -1
  207. package/dist/components/measured-grid.js +2 -1
  208. package/dist/components/measured-grid.js.map +1 -1
  209. package/dist/components/mini-calendar.js +3 -5
  210. package/dist/components/mini-calendar.js.map +1 -1
  211. package/dist/components/modal.js +3 -1
  212. package/dist/components/modal.js.map +1 -1
  213. package/dist/components/month-grid.d.ts +55 -0
  214. package/dist/components/month-grid.js +148 -0
  215. package/dist/components/month-grid.js.map +1 -0
  216. package/dist/components/month-picker.d.ts +36 -4
  217. package/dist/components/month-picker.js +123 -146
  218. package/dist/components/month-picker.js.map +1 -1
  219. package/dist/components/multi-select.d.ts +2 -2
  220. package/dist/components/multi-select.js +11 -6
  221. package/dist/components/multi-select.js.map +1 -1
  222. package/dist/components/picker-sheet.js +2 -1
  223. package/dist/components/picker-sheet.js.map +1 -1
  224. package/dist/components/pie-chart-labels.d.ts +7 -0
  225. package/dist/components/pie-chart-labels.js +1 -0
  226. package/dist/components/pie-chart-labels.js.map +1 -1
  227. package/dist/components/pie-chart.d.ts +5 -3
  228. package/dist/components/pie-chart.js +7 -22
  229. package/dist/components/pie-chart.js.map +1 -1
  230. package/dist/components/popover.d.ts +3 -0
  231. package/dist/components/popover.js +7 -1
  232. package/dist/components/popover.js.map +1 -1
  233. package/dist/components/row-actions.js +2 -1
  234. package/dist/components/row-actions.js.map +1 -1
  235. package/dist/components/section-nav.d.ts +7 -0
  236. package/dist/components/section-nav.js.map +1 -1
  237. package/dist/components/series-chart-axes.d.ts +3 -3
  238. package/dist/components/series-chart-axes.js.map +1 -1
  239. package/dist/components/series-chart-labels.d.ts +7 -4
  240. package/dist/components/series-chart-labels.js.map +1 -1
  241. package/dist/components/series-chart.d.ts +31 -7
  242. package/dist/components/series-chart.js +72 -29
  243. package/dist/components/series-chart.js.map +1 -1
  244. package/dist/components/share-card.js +3 -3
  245. package/dist/components/share-card.js.map +1 -1
  246. package/dist/components/sign-chip-labels.d.ts +1 -0
  247. package/dist/components/sign-chip-labels.js.map +1 -1
  248. package/dist/components/sign-chip.d.ts +10 -1
  249. package/dist/components/sign-chip.js.map +1 -1
  250. package/dist/components/tabs-labels.d.ts +2 -0
  251. package/dist/components/tabs-labels.js +11 -0
  252. package/dist/components/tabs-labels.js.map +1 -0
  253. package/dist/components/tabs.d.ts +1 -1
  254. package/dist/components/tabs.js +1 -1
  255. package/dist/components/tabs.js.map +1 -1
  256. package/dist/components/toggle-legend.d.ts +2 -2
  257. package/dist/components/toggle-legend.js +6 -6
  258. package/dist/components/toggle-legend.js.map +1 -1
  259. package/dist/components/translation-review-editor.d.ts +4 -2
  260. package/dist/components/translation-review-editor.js +2 -3
  261. package/dist/components/translation-review-editor.js.map +1 -1
  262. package/dist/components/translation-review.d.ts +2 -1
  263. package/dist/components/translation-review.js +2 -2
  264. package/dist/components/translation-review.js.map +1 -1
  265. package/dist/components/treemap-labels.d.ts +26 -0
  266. package/dist/components/treemap-labels.js +10 -0
  267. package/dist/components/treemap-labels.js.map +1 -0
  268. package/dist/components/treemap-layout.d.ts +62 -0
  269. package/dist/components/treemap-layout.js +126 -0
  270. package/dist/components/treemap-layout.js.map +1 -0
  271. package/dist/components/treemap.d.ts +140 -17
  272. package/dist/components/treemap.js +324 -97
  273. package/dist/components/treemap.js.map +1 -1
  274. package/dist/components/use-table-state.d.ts +5 -5
  275. package/dist/components/use-table-state.js +4 -1
  276. package/dist/components/use-table-state.js.map +1 -1
  277. package/dist/data-table.d.ts +6 -2
  278. package/dist/data-table.js +7 -2
  279. package/dist/data-table.js.map +1 -1
  280. package/dist/eslint/index.js +1 -1
  281. package/dist/eslint/role-utilities.d.ts +36 -7
  282. package/dist/eslint/role-utilities.js +42 -9
  283. package/dist/eslint/role-utilities.js.map +1 -1
  284. package/dist/feedback/feedback-dialog.d.ts +25 -16
  285. package/dist/feedback/feedback-dialog.js +6 -2
  286. package/dist/feedback/feedback-dialog.js.map +1 -1
  287. package/dist/feedback/feedback-mutation.d.ts +30 -0
  288. package/dist/feedback/feedback-mutation.js +16 -0
  289. package/dist/feedback/feedback-mutation.js.map +1 -0
  290. package/dist/feedback/feedback-page-link.d.ts +53 -0
  291. package/dist/feedback/feedback-page-link.js +37 -0
  292. package/dist/feedback/feedback-page-link.js.map +1 -0
  293. package/dist/feedback/feedback-public-list.d.ts +12 -3
  294. package/dist/feedback/feedback-public-list.js +2 -1
  295. package/dist/feedback/feedback-public-list.js.map +1 -1
  296. package/dist/feedback/feedback-replies.d.ts +3 -1
  297. package/dist/feedback/feedback-replies.js +5 -4
  298. package/dist/feedback/feedback-replies.js.map +1 -1
  299. package/dist/feedback/feedback-row-detail-labels.js +5 -0
  300. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  301. package/dist/feedback/feedback-row-detail.d.ts +64 -28
  302. package/dist/feedback/feedback-row-detail.js +30 -28
  303. package/dist/feedback/feedback-row-detail.js.map +1 -1
  304. package/dist/feedback/feedback-status-undo.d.ts +30 -11
  305. package/dist/feedback/feedback-status-undo.js +10 -15
  306. package/dist/feedback/feedback-status-undo.js.map +1 -1
  307. package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
  308. package/dist/feedback/feedback-submit-dialog.js +1 -1
  309. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  310. package/dist/feedback/feedback-submit.js +9 -3
  311. package/dist/feedback/feedback-submit.js.map +1 -1
  312. package/dist/feedback/feedback-swipe.d.ts +1 -1
  313. package/dist/feedback/feedback-swipe.js.map +1 -1
  314. package/dist/feedback/feedback-table.d.ts +21 -9
  315. package/dist/feedback/feedback-table.js +45 -25
  316. package/dist/feedback/feedback-table.js.map +1 -1
  317. package/dist/feedback/feedback-thread-labels.d.ts +2 -2
  318. package/dist/feedback/feedback-thread-labels.js.map +1 -1
  319. package/dist/feedback/feedback-thread.d.ts +24 -23
  320. package/dist/feedback/feedback-thread.js +2 -1
  321. package/dist/feedback/feedback-thread.js.map +1 -1
  322. package/dist/feedback.d.ts +34 -0
  323. package/dist/feedback.js +42 -0
  324. package/dist/feedback.js.map +1 -1
  325. package/dist/hooks/use-focus-trap.d.ts +35 -0
  326. package/dist/hooks/use-focus-trap.js +6 -4
  327. package/dist/hooks/use-focus-trap.js.map +1 -1
  328. package/dist/hooks/use-hotkey.d.ts +5 -2
  329. package/dist/hooks/use-hotkey.js +5 -4
  330. package/dist/hooks/use-hotkey.js.map +1 -1
  331. package/dist/hooks/use-long-press.d.ts +7 -4
  332. package/dist/hooks/use-long-press.js +3 -0
  333. package/dist/hooks/use-long-press.js.map +1 -1
  334. package/dist/hooks/use-media-query.js +12 -4
  335. package/dist/hooks/use-media-query.js.map +1 -1
  336. package/dist/hooks/use-mounted.d.ts +22 -0
  337. package/dist/hooks/use-mounted.js +24 -0
  338. package/dist/hooks/use-mounted.js.map +1 -0
  339. package/dist/hooks/use-overlay-history.js +19 -3
  340. package/dist/hooks/use-overlay-history.js.map +1 -1
  341. package/dist/i18n/defaults.js +4 -5
  342. package/dist/i18n/defaults.js.map +1 -1
  343. package/dist/i18n/german.js +7 -1
  344. package/dist/i18n/german.js.map +1 -1
  345. package/dist/i18n/kit-labels.d.ts +9 -2
  346. package/dist/i18n/kit-labels.js.map +1 -1
  347. package/dist/i18n/locales/en.js +20 -41
  348. package/dist/i18n/locales/en.js.map +3 -3
  349. package/dist/i18n/locales/es.js +7 -1
  350. package/dist/i18n/locales/es.js.map +1 -1
  351. package/dist/i18n/locales/fr.js +20 -10
  352. package/dist/i18n/locales/fr.js.map +1 -1
  353. package/dist/i18n/locales/hu.js +9 -1
  354. package/dist/i18n/locales/hu.js.map +1 -1
  355. package/dist/i18n/locales/it.js +7 -1
  356. package/dist/i18n/locales/it.js.map +1 -1
  357. package/dist/i18n/locales/zh.js +8 -1
  358. package/dist/i18n/locales/zh.js.map +1 -1
  359. package/dist/i18n/review.js +5 -0
  360. package/dist/i18n/review.js.map +1 -1
  361. package/dist/index.d.ts +64 -17
  362. package/dist/index.js +77 -12
  363. package/dist/index.js.map +1 -1
  364. package/dist/landing/routing.d.ts +3 -1
  365. package/dist/landing/routing.js +4 -7
  366. package/dist/landing/routing.js.map +1 -1
  367. package/dist/lib/broadcast.d.ts +17 -0
  368. package/dist/lib/broadcast.js +30 -0
  369. package/dist/lib/broadcast.js.map +1 -0
  370. package/dist/lib/date-format.d.ts +37 -2
  371. package/dist/lib/date-format.js +34 -8
  372. package/dist/lib/date-format.js.map +1 -1
  373. package/dist/lib/day-format.d.ts +46 -0
  374. package/dist/lib/day-format.js +46 -0
  375. package/dist/lib/day-format.js.map +1 -0
  376. package/dist/lib/describe-failure.d.ts +14 -0
  377. package/dist/lib/describe-failure.js +9 -0
  378. package/dist/lib/describe-failure.js.map +1 -0
  379. package/dist/lib/direction.d.ts +16 -0
  380. package/dist/lib/direction.js +9 -1
  381. package/dist/lib/direction.js.map +1 -1
  382. package/dist/lib/focus-left.d.ts +63 -0
  383. package/dist/lib/focus-left.js +67 -0
  384. package/dist/lib/focus-left.js.map +1 -0
  385. package/dist/lib/format.js +5 -14
  386. package/dist/lib/format.js.map +1 -1
  387. package/dist/lib/http-status.d.ts +24 -0
  388. package/dist/lib/http-status.js +14 -0
  389. package/dist/lib/http-status.js.map +1 -0
  390. package/dist/lib/intl-cache.d.ts +14 -0
  391. package/dist/lib/intl-cache.js +15 -0
  392. package/dist/lib/intl-cache.js.map +1 -0
  393. package/dist/lib/isin.d.ts +10 -0
  394. package/dist/lib/isin.js.map +1 -1
  395. package/dist/lib/iso-day.d.ts +11 -0
  396. package/dist/lib/iso-day.js +8 -0
  397. package/dist/lib/iso-day.js.map +1 -0
  398. package/dist/lib/lazy-with-fallback.d.ts +31 -0
  399. package/dist/lib/lazy-with-fallback.js +40 -0
  400. package/dist/lib/lazy-with-fallback.js.map +1 -0
  401. package/dist/lib/path-pattern.d.ts +34 -0
  402. package/dist/lib/path-pattern.js +24 -0
  403. package/dist/lib/path-pattern.js.map +1 -0
  404. package/dist/lib/reduced-motion.d.ts +2 -1
  405. package/dist/lib/reduced-motion.js.map +1 -1
  406. package/dist/lib/round.d.ts +2 -1
  407. package/dist/lib/round.js +2 -1
  408. package/dist/lib/round.js.map +1 -1
  409. package/dist/lib/safe-storage.d.ts +24 -5
  410. package/dist/lib/safe-storage.js +27 -2
  411. package/dist/lib/safe-storage.js.map +1 -1
  412. package/dist/lib/session-refresh.js +1 -1
  413. package/dist/lib/session-refresh.js.map +1 -1
  414. package/dist/lib/share-format.d.ts +13 -0
  415. package/dist/lib/share-format.js +10 -0
  416. package/dist/lib/share-format.js.map +1 -0
  417. package/dist/lib/text-case.d.ts +17 -0
  418. package/dist/lib/text-case.js +8 -0
  419. package/dist/lib/text-case.js.map +1 -0
  420. package/dist/lib/text-match.d.ts +29 -0
  421. package/dist/lib/text-match.js +20 -2
  422. package/dist/lib/text-match.js.map +1 -1
  423. package/dist/lib/thenable.d.ts +13 -0
  424. package/dist/lib/thenable.js +8 -0
  425. package/dist/lib/thenable.js.map +1 -0
  426. package/dist/lib/translation-review.d.ts +7 -2
  427. package/dist/lib/translation-review.js +4 -4
  428. package/dist/lib/translation-review.js.map +1 -1
  429. package/dist/notifications/device-zone-report.js +1 -1
  430. package/dist/notifications/device-zone-report.js.map +1 -1
  431. package/dist/notifications/notification-bell.d.ts +2 -1
  432. package/dist/notifications/notification-bell.js +3 -3
  433. package/dist/notifications/notification-bell.js.map +1 -1
  434. package/dist/notifications/notification-inbox.d.ts +10 -6
  435. package/dist/notifications/notification-inbox.js +26 -48
  436. package/dist/notifications/notification-inbox.js.map +1 -1
  437. package/dist/notifications/notification-parts.d.ts +3 -2
  438. package/dist/notifications/notification-parts.js +1 -3
  439. package/dist/notifications/notification-parts.js.map +1 -1
  440. package/dist/notifications/notification-types-setting.js +1 -1
  441. package/dist/notifications/notification-types-setting.js.map +1 -1
  442. package/dist/notifications/notifications-labels.d.ts +6 -0
  443. package/dist/notifications/notifications-labels.js +1 -0
  444. package/dist/notifications/notifications-labels.js.map +1 -1
  445. package/dist/notifications/push-device-setting.js +1 -1
  446. package/dist/notifications/push-device-setting.js.map +1 -1
  447. package/dist/notifications/push-devices-setting.d.ts +2 -1
  448. package/dist/notifications/push-devices-setting.js +27 -49
  449. package/dist/notifications/push-devices-setting.js.map +1 -1
  450. package/dist/notifications/quiet-hours-setting.d.ts +10 -3
  451. package/dist/notifications/quiet-hours-setting.js +65 -26
  452. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  453. package/dist/notifications/unsubscribe-panel.js +4 -7
  454. package/dist/notifications/unsubscribe-panel.js.map +1 -1
  455. package/dist/notifications/use-push-device.d.ts +39 -1
  456. package/dist/notifications/use-push-device.js +28 -7
  457. package/dist/notifications/use-push-device.js.map +1 -1
  458. package/dist/offline/query-persistence.d.ts +3 -1
  459. package/dist/offline/query-persistence.js +19 -2
  460. package/dist/offline/query-persistence.js.map +1 -1
  461. package/dist/rhf/fields.d.ts +11 -4
  462. package/dist/rhf/fields.js.map +1 -1
  463. package/dist/search/command-palette.js +1 -1
  464. package/dist/search/command-palette.js.map +1 -1
  465. package/dist/search/global-search.js +19 -9
  466. package/dist/search/global-search.js.map +1 -1
  467. package/dist/search/use-command-key.d.ts +12 -1
  468. package/dist/search/use-command-key.js +3 -12
  469. package/dist/search/use-command-key.js.map +1 -1
  470. package/dist/search.d.ts +7 -0
  471. package/dist/search.js +10 -0
  472. package/dist/search.js.map +1 -1
  473. package/dist/settings/use-settings-focus.js +1 -0
  474. package/dist/settings/use-settings-focus.js.map +1 -1
  475. package/dist/shell/account-menu-language.d.ts +21 -5
  476. package/dist/shell/account-menu-language.js +3 -1
  477. package/dist/shell/account-menu-language.js.map +1 -1
  478. package/dist/shell/account-menu-text-size.d.ts +26 -9
  479. package/dist/shell/account-menu-text-size.js +3 -2
  480. package/dist/shell/account-menu-text-size.js.map +1 -1
  481. package/dist/shell/dev-menu.js +5 -18
  482. package/dist/shell/dev-menu.js.map +1 -1
  483. package/dist/shell/role-switcher.d.ts +7 -0
  484. package/dist/shell/role-switcher.js.map +1 -1
  485. package/dist/shell/top-bar-actions.d.ts +1 -1
  486. package/dist/shell/top-bar-actions.js.map +1 -1
  487. package/dist/shell/top-bar-parts-labels.d.ts +0 -2
  488. package/dist/shell/top-bar-parts-labels.js +0 -1
  489. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  490. package/dist/shell.d.ts +27 -8
  491. package/dist/shell.js +28 -7
  492. package/dist/shell.js.map +1 -1
  493. package/dist/sync/engine.d.ts +30 -4
  494. package/dist/sync/engine.js +172 -52
  495. package/dist/sync/engine.js.map +1 -1
  496. package/dist/sync.d.ts +6 -2
  497. package/dist/sync.js +6 -1
  498. package/dist/sync.js.map +1 -1
  499. package/dist/testing/css-variables.js +3 -8
  500. package/dist/testing/css-variables.js.map +1 -1
  501. package/dist/theme/contrast.d.ts +2 -1
  502. package/dist/theme/contrast.js +3 -8
  503. package/dist/theme/contrast.js.map +1 -1
  504. package/dist/theme/palette-store.js +6 -6
  505. package/dist/theme/palette-store.js.map +1 -1
  506. package/dist/theme/text-size.js +3 -8
  507. package/dist/theme/text-size.js.map +1 -1
  508. package/dist/theme/theme-store.js +3 -6
  509. package/dist/theme/theme-store.js.map +1 -1
  510. package/dist/tour/tour-catalogue.d.ts +7 -4
  511. package/dist/tour/tour-catalogue.js +12 -10
  512. package/dist/tour/tour-catalogue.js.map +1 -1
  513. package/dist/tour/tour-dock.d.ts +0 -2
  514. package/dist/tour/tour-dock.js +0 -2
  515. package/dist/tour/tour-dock.js.map +1 -1
  516. package/dist/tour/tour-gates.d.ts +4 -0
  517. package/dist/tour/tour-gates.js +14 -0
  518. package/dist/tour/tour-gates.js.map +1 -1
  519. package/dist/tour/tour-overlay.d.ts +27 -0
  520. package/dist/tour/tour-overlay.js +741 -0
  521. package/dist/tour/tour-overlay.js.map +1 -0
  522. package/dist/tour/tour.d.ts +13 -3
  523. package/dist/tour/tour.js +59 -781
  524. package/dist/tour/tour.js.map +1 -1
  525. package/dist/tour/use-tour-progress.js +1 -1
  526. package/dist/tour/use-tour-progress.js.map +1 -1
  527. package/dist/wizard/stepper-nav.d.ts +5 -1
  528. package/dist/wizard/stepper-nav.js.map +1 -1
  529. package/dist/wizard/types.d.ts +6 -2
  530. package/dist/wizard/types.js.map +1 -1
  531. package/dist/wizard/use-wizard.d.ts +3 -1
  532. package/dist/wizard/use-wizard.js.map +1 -1
  533. package/dist/wizard/validation.d.ts +3 -1
  534. package/dist/wizard/validation.js.map +1 -1
  535. package/package.json +10 -6
  536. package/src/account/account-parts.ts +5 -25
  537. package/src/account/data-export-setting-labels.ts +1 -1
  538. package/src/account/delete-account-setting-labels.ts +1 -1
  539. package/src/account/email-change-setting-labels.ts +1 -1
  540. package/src/account/passkeys-setting.tsx +60 -38
  541. package/src/account/sessions-setting.tsx +18 -12
  542. package/src/admin/admin-action-confirm.tsx +1 -5
  543. package/src/admin/admin-parts.ts +11 -25
  544. package/src/admin/mail-test-card-labels.ts +1 -1
  545. package/src/admin/plan-change-labels.ts +5 -23
  546. package/src/admin/plan-change.tsx +7 -44
  547. package/src/admin/reviewer-scope-editor.tsx +3 -3
  548. package/src/auth/auth-errors-labels.ts +23 -0
  549. package/src/auth/auth-errors.ts +11 -38
  550. package/src/auth/complete-name-dialog.tsx +4 -1
  551. package/src/auth/forgot-password-form-labels.ts +1 -1
  552. package/src/auth/forgot-password-form.tsx +11 -6
  553. package/src/auth/register-form-labels.ts +1 -1
  554. package/src/auth/reset-password-form-labels.ts +1 -1
  555. package/src/auth/reset-password-form.tsx +8 -4
  556. package/src/auth/sign-in-form-labels.ts +1 -1
  557. package/src/auth/status-parts.tsx +0 -6
  558. package/src/auth/verify-email-labels.ts +1 -1
  559. package/src/auth/verify-email.tsx +11 -6
  560. package/src/billing/cancel-subscription.tsx +26 -12
  561. package/src/billing/dialog-close.ts +34 -0
  562. package/src/billing/plan-card.tsx +36 -11
  563. package/src/billing/plan-change-labels.ts +66 -0
  564. package/src/billing/plan-switch.tsx +26 -12
  565. package/src/chart.ts +21 -3
  566. package/src/components/account-chips.tsx +20 -23
  567. package/src/components/alert-banner.tsx +82 -5
  568. package/src/components/amount-input.tsx +18 -7
  569. package/src/components/autocomplete.tsx +64 -70
  570. package/src/components/bulk-action-bar.tsx +70 -2
  571. package/src/components/button-look.ts +5 -5
  572. package/src/components/button.tsx +2 -2
  573. package/src/components/chart-edge.ts +34 -1
  574. package/src/components/chart-figure.tsx +35 -0
  575. package/src/components/chart-zoom-core.tsx +6 -5
  576. package/src/components/chart.tsx +26 -5
  577. package/src/components/combobox-core.tsx +293 -62
  578. package/src/components/combobox.tsx +183 -153
  579. package/src/components/copy-button.tsx +34 -23
  580. package/src/components/country-select.tsx +16 -11
  581. package/src/components/data-table-column-settings.tsx +52 -10
  582. package/src/components/data-table-filters.ts +6 -4
  583. package/src/components/data-table-fit.ts +19 -5
  584. package/src/components/data-table-groups.tsx +1 -1
  585. package/src/components/data-table-head.tsx +32 -22
  586. package/src/components/data-table-mobile.tsx +61 -26
  587. package/src/components/data-table-pagination.tsx +21 -6
  588. package/src/components/data-table-resize.ts +1 -1
  589. package/src/components/data-table-row-actions.tsx +3 -3
  590. package/src/components/data-table-row.tsx +25 -14
  591. package/src/components/data-table-selection.ts +1 -1
  592. package/src/components/data-table-shared.tsx +33 -1
  593. package/src/components/data-table-sort.ts +57 -2
  594. package/src/components/data-table-virtual.ts +32 -8
  595. package/src/components/data-table.tsx +239 -68
  596. package/src/components/date-field.tsx +65 -8
  597. package/src/components/date-picker.tsx +125 -48
  598. package/src/components/date-range-picker.tsx +82 -31
  599. package/src/components/error-boundary.tsx +53 -39
  600. package/src/components/field-base-labels.ts +20 -0
  601. package/src/components/field-base.tsx +13 -7
  602. package/src/components/field-sync-labels.ts +1 -0
  603. package/src/components/field-sync.tsx +23 -1
  604. package/src/components/form-actions.tsx +20 -5
  605. package/src/components/hover-menu.tsx +18 -8
  606. package/src/components/icon-button.tsx +14 -9
  607. package/src/components/inline-confirm.tsx +116 -0
  608. package/src/components/inline-entity-combobox.tsx +197 -153
  609. package/src/components/input.tsx +1 -1
  610. package/src/components/jump-calendar.tsx +30 -146
  611. package/src/components/legal-page.tsx +10 -7
  612. package/src/components/legal.tsx +4 -2
  613. package/src/components/lightbox.tsx +21 -8
  614. package/src/components/list.tsx +10 -4
  615. package/src/components/measured-grid.tsx +8 -4
  616. package/src/components/mini-calendar.tsx +6 -5
  617. package/src/components/modal.tsx +6 -1
  618. package/src/components/month-grid.tsx +229 -0
  619. package/src/components/month-picker.tsx +192 -200
  620. package/src/components/multi-select.tsx +21 -9
  621. package/src/components/picker-sheet.tsx +12 -9
  622. package/src/components/pie-chart-labels.ts +8 -0
  623. package/src/components/pie-chart.tsx +22 -28
  624. package/src/components/popover.tsx +18 -1
  625. package/src/components/row-actions.tsx +2 -1
  626. package/src/components/section-nav.tsx +7 -0
  627. package/src/components/series-chart-axes.ts +3 -3
  628. package/src/components/series-chart-labels.ts +7 -4
  629. package/src/components/series-chart.tsx +178 -69
  630. package/src/components/share-card.tsx +6 -6
  631. package/src/components/sign-chip-labels.ts +1 -0
  632. package/src/components/sign-chip.tsx +10 -1
  633. package/src/components/tabs-labels.ts +13 -0
  634. package/src/components/tabs.tsx +2 -2
  635. package/src/components/toggle-legend.tsx +16 -11
  636. package/src/components/translation-review-editor.tsx +6 -5
  637. package/src/components/translation-review.tsx +4 -3
  638. package/src/components/treemap-labels.ts +39 -0
  639. package/src/components/treemap-layout.ts +210 -0
  640. package/src/components/treemap.tsx +722 -133
  641. package/src/components/use-table-state.ts +12 -9
  642. package/src/data-table.ts +9 -2
  643. package/src/eslint/role-utilities.ts +97 -19
  644. package/src/feedback/feedback-dialog.tsx +30 -19
  645. package/src/feedback/feedback-mutation.ts +41 -0
  646. package/src/feedback/feedback-page-link.ts +90 -0
  647. package/src/feedback/feedback-public-list.tsx +17 -5
  648. package/src/feedback/feedback-replies.tsx +13 -6
  649. package/src/feedback/feedback-row-detail-labels.ts +5 -0
  650. package/src/feedback/feedback-row-detail.tsx +103 -67
  651. package/src/feedback/feedback-status-undo.ts +41 -27
  652. package/src/feedback/feedback-submit-dialog.tsx +2 -2
  653. package/src/feedback/feedback-submit.tsx +21 -7
  654. package/src/feedback/feedback-swipe.tsx +1 -1
  655. package/src/feedback/feedback-table.tsx +86 -38
  656. package/src/feedback/feedback-thread-labels.ts +2 -2
  657. package/src/feedback/feedback-thread.tsx +27 -25
  658. package/src/feedback.ts +42 -0
  659. package/src/hooks/use-focus-trap.ts +8 -24
  660. package/src/hooks/use-hotkey.ts +24 -7
  661. package/src/hooks/use-long-press.ts +16 -2
  662. package/src/hooks/use-media-query.ts +20 -4
  663. package/src/hooks/use-mounted.ts +39 -0
  664. package/src/hooks/use-overlay-history.ts +32 -3
  665. package/src/i18n/defaults.ts +4 -5
  666. package/src/i18n/german.ts +8 -2
  667. package/src/i18n/kit-labels.tsx +9 -2
  668. package/src/i18n/locales/es.ts +8 -2
  669. package/src/i18n/locales/fr.ts +21 -13
  670. package/src/i18n/locales/hu.ts +10 -2
  671. package/src/i18n/locales/it.ts +8 -2
  672. package/src/i18n/locales/zh.ts +9 -2
  673. package/src/i18n/review.ts +5 -0
  674. package/src/index.ts +103 -13
  675. package/src/landing/routing.tsx +14 -10
  676. package/src/lib/broadcast.ts +52 -0
  677. package/src/lib/date-format.ts +89 -13
  678. package/src/lib/day-format.ts +96 -0
  679. package/src/lib/describe-failure.ts +18 -0
  680. package/src/lib/direction.ts +39 -0
  681. package/src/lib/focus-left.ts +156 -0
  682. package/src/lib/format.ts +5 -17
  683. package/src/lib/http-status.ts +38 -0
  684. package/src/lib/intl-cache.ts +25 -0
  685. package/src/lib/isin.ts +10 -0
  686. package/src/lib/iso-day.ts +13 -0
  687. package/src/lib/lazy-with-fallback.ts +82 -0
  688. package/src/lib/path-pattern.ts +55 -0
  689. package/src/lib/reduced-motion.ts +2 -1
  690. package/src/lib/round.ts +7 -2
  691. package/src/lib/safe-storage.ts +48 -6
  692. package/src/lib/session-refresh.ts +1 -1
  693. package/src/lib/share-format.ts +18 -0
  694. package/src/lib/text-case.ts +19 -0
  695. package/src/lib/text-match.ts +57 -2
  696. package/src/lib/thenable.ts +19 -0
  697. package/src/lib/translation-review.ts +13 -5
  698. package/src/notifications/device-zone-report.ts +1 -1
  699. package/src/notifications/notification-bell.tsx +7 -4
  700. package/src/notifications/notification-inbox.tsx +40 -58
  701. package/src/notifications/notification-parts.tsx +3 -4
  702. package/src/notifications/notification-types-setting.tsx +1 -1
  703. package/src/notifications/notifications-labels.ts +7 -0
  704. package/src/notifications/push-device-setting.tsx +1 -1
  705. package/src/notifications/push-devices-setting.tsx +30 -55
  706. package/src/notifications/quiet-hours-setting.tsx +93 -31
  707. package/src/notifications/unsubscribe-panel.tsx +4 -7
  708. package/src/notifications/use-push-device.ts +76 -10
  709. package/src/offline/query-persistence.ts +35 -3
  710. package/src/rhf/fields.tsx +11 -4
  711. package/src/search/command-palette.tsx +2 -4
  712. package/src/search/global-search.tsx +36 -15
  713. package/src/search/use-command-key.ts +13 -12
  714. package/src/search.ts +9 -0
  715. package/src/settings/use-settings-focus.ts +3 -0
  716. package/src/shell/account-menu-language.tsx +27 -5
  717. package/src/shell/account-menu-text-size.tsx +32 -10
  718. package/src/shell/dev-menu.tsx +7 -21
  719. package/src/shell/role-switcher.tsx +7 -0
  720. package/src/shell/top-bar-actions.tsx +1 -1
  721. package/src/shell/top-bar-parts-labels.ts +0 -3
  722. package/src/shell.ts +40 -9
  723. package/src/sync/engine.ts +339 -61
  724. package/src/sync.ts +8 -1
  725. package/src/testing/css-variables.ts +8 -9
  726. package/src/theme/contrast.ts +8 -9
  727. package/src/theme/palette-store.ts +9 -9
  728. package/src/theme/text-size.ts +6 -8
  729. package/src/theme/theme-store.ts +7 -9
  730. package/src/tour/tour-catalogue.tsx +26 -15
  731. package/src/tour/tour-dock.ts +0 -2
  732. package/src/tour/tour-gates.ts +23 -0
  733. package/src/tour/tour-overlay.tsx +1170 -0
  734. package/src/tour/tour.tsx +108 -1202
  735. package/src/tour/use-tour-progress.ts +1 -1
  736. package/src/wizard/stepper-nav.tsx +3 -1
  737. package/src/wizard/types.ts +11 -2
  738. package/src/wizard/use-wizard.ts +2 -1
  739. package/src/wizard/validation.ts +8 -2
  740. package/tokens.css +2 -1
  741. package/dist/components/ui-labels.js.map +0 -1
  742. package/dist/shell/dev-switchers.d.ts +0 -6
  743. package/dist/shell/dev-switchers.js +0 -12
  744. package/dist/shell/dev-switchers.js.map +0 -1
  745. package/src/components/ui-labels.ts +0 -24
  746. package/src/shell/dev-switchers.tsx +0 -18
@@ -8,18 +8,35 @@
8
8
  // chart. The one thing it cannot take from a CSS var is the tile colour: the label ink
9
9
  // is MEASURED against the fill, and a `var(--chart-3)` string measures as nothing — see
10
10
  // `useChartTokenHex` below for how the concrete hex is found instead.
11
- import { useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
12
- import type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from "react";
13
- import { Treemap as RechartsTreemap } from "recharts";
11
+ //
12
+ // Since 0.52 a node can hold `children`: a group, drawn as a labelled frame round its
13
+ // tiles (keksdose dev #642, shops by chain). Where every node is drawn is the map's own
14
+ // layout (treemap-layout.ts), not recharts': see "Groups: a frame round their tiles".
15
+ import {
16
+ createContext,
17
+ useCallback,
18
+ useContext,
19
+ useLayoutEffect,
20
+ useMemo,
21
+ useRef,
22
+ useState,
23
+ useSyncExternalStore,
24
+ } from "react";
25
+ import type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactElement, ReactNode } from "react";
26
+ import { Treemap as RechartsTreemap, useChartHeight, useChartWidth } from "recharts";
14
27
  import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "./chart";
15
- import { PALETTE_HEX, PALETTE_SIZE, textOn } from "../theme/chart-palette";
16
- import { resolveChartColor, useChartEdge } from "./chart-edge";
28
+ import { CHART_ROLES, PALETTE_HEX, PALETTE_SIZE, lerpHex, textOn } from "../theme/chart-palette";
29
+ import { resolveChartColor, useChartEdge, useEdgesInForce, type ChartEdges } from "./chart-edge";
17
30
  import { parseHex } from "../theme/color";
18
- import { dirOf, type Direction } from "../lib/direction";
31
+ import { dirOf, rovingTarget, type Direction } from "../lib/direction";
32
+ import { shareFormat } from "../lib/share-format";
19
33
  import { cn } from "../lib/cn";
20
34
  import { useTextSize } from "../theme/text-size";
21
35
  import { chartHeightProps, type ChartHeight } from "./chart-height";
22
36
  import { formatWithMinus } from "../lib/minus";
37
+ import { useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
38
+ import { DEFAULT_TREEMAP_LABELS, type TreemapLabels } from "./treemap-labels";
39
+ import { groupBody, layoutTreemap, type Box, type Placed } from "./treemap-layout";
23
40
 
24
41
  /**
25
42
  * One tile.
@@ -31,13 +48,19 @@ import { formatWithMinus } from "../lib/minus";
31
48
  */
32
49
  export interface TreemapNode {
33
50
  /** Stable key, handed back by {@link TreemapProps.onNodeClick} and
34
- * {@link TreemapProps.nodeNote}. Names can collide; ids must not. */
51
+ * {@link TreemapProps.nodeNote}. Names can collide; ids must not among a group's own
52
+ * nodes (or the top's). A group and a tile inside it may share one — chain 12 and shop
53
+ * 12 from two tables — and `onNodeClick`'s `groups` tells them apart (0.52). */
35
54
  id: string;
36
55
  name: string;
37
56
  /** The tile's area. A node whose value is not a positive finite number is NOT
38
57
  * drawn — an area cannot be negative, and recharts lays a negative size out as
39
58
  * nonsense rather than refusing it. Saying so is the caller's job: a group that
40
- * vanishes silently between two views of the same data is a bug report. */
59
+ * vanishes silently between two views of the same data is a bug report.
60
+ *
61
+ * A GROUP's own `value` is not read: its area is its drawn children's together. Pass
62
+ * that sum — the field stays required so that every reader of a `TreemapNode[]` keeps
63
+ * a `number`. */
41
64
  value: number;
42
65
  /** Overrides the by-index palette colour for this tile — e.g. to paint every child
43
66
  * in its parent group's colour, so the groups stay legible in one picture. A
@@ -45,43 +68,123 @@ export interface TreemapNode {
45
68
  * does `CHART_ROLES.other` (`var(--chart-muted)`), the colour for a "rest" bucket,
46
69
  * which should not wrap round to a hue the reader matches to a real node;
47
70
  * anything else (another var, a `color-mix()`) gets `currentColor` ink, because
48
- * there is nothing to measure — set `labelColor` then. */
71
+ * there is nothing to measure — set `labelColor` then.
72
+ *
73
+ * On a group, the colour of every tile inside it that sets none (0.52): keksdose's
74
+ * "hue = chain" is a `fill` per chain. A group left without one takes the ramp by its
75
+ * index, as a tile does, and hands that down; its frame is drawn in the page's own
76
+ * colours whatever it is. */
49
77
  fill?: string;
50
78
  /** The label's ink, for a `fill` the kit cannot measure (a `var()` or `color-mix()`):
51
79
  * keksdose's recency plot fills with `color-mix(in srgb, var(--chart-1) N%,
52
80
  * transparent)`, where `currentColor` put dark page text on full-strength indigo
53
- * (~1.5:1). Wins over the measured ink when given. */
81
+ * (~1.5:1). Wins over the measured ink when given. A tile that takes its group's
82
+ * `fill` takes the group's `labelColor` with it; one with a `fill` of its own, or one
83
+ * that `shade` turns into another colour, is measured again. */
54
84
  labelColor?: string;
85
+ /**
86
+ * The tile's colour strength, 0–1 (0.52): its `fill` mixed into the card's colour
87
+ * (`--bg-surface`) as a concrete hex, `1` the full hue. For a second, ordered quantity
88
+ * on top of the hue — keksdose's recency steps (1, 0.78, 0.55) over the chain's hue —
89
+ * without a `color-mix()`: the result is measured like any hex, so the label ink stays
90
+ * automatic and no `labelColor` is needed. keksdose found a tile under ~0.55 stops
91
+ * reading as its hue and passes for the muted "other" (#642).
92
+ *
93
+ * Only a fill the kit can measure is shaded (a hex, a bare `var(--chart-N)` or another
94
+ * bare token), and only on a page that declares `--bg-surface` as a hex; anything else
95
+ * keeps its full hue. On a group, the default for its tiles, as `fill` is.
96
+ */
97
+ shade?: number;
98
+ /**
99
+ * Makes this node a GROUP (0.52, keksdose dev #642: "Is there already a distinction
100
+ * between shop and chain?"): a labelled frame, its name in a band along the top, its
101
+ * tiles laid out inside it by the map's own layout, each at its share of the whole
102
+ * picture (see "Groups: a frame round their tiles"). Groups nest. A group with nothing
103
+ * drawable inside is not drawn; an empty array is a tile.
104
+ *
105
+ * The frame is in the page's colours (the well, `--bg-surface-2`, under
106
+ * `--text-primary`, at least 7.4:1 in every preset and contrast), and under More
107
+ * contrast it is outlined in `--chart-ink` as a pale tile is. Its band is drawn only
108
+ * where the group has the room for it — see `TreemapCell`.
109
+ */
110
+ children?: readonly TreemapNode[];
55
111
  }
56
112
 
57
113
  /**
58
114
  * Everything a `<div>` takes reaches the chart's root (`ChartContainer`), so a test id
59
115
  * or a `data-tour` anchor can find it. `children` is the chart's own.
116
+ *
117
+ * The root is a `role="figure"` (0.52, refactor audit 2026-10-30, DSP-5): name it with
118
+ * `aria-label` ("Spending by category"), and say what it shows with {@link
119
+ * TreemapProps.description}.
60
120
  */
61
121
  export interface TreemapProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
62
- data: TreemapNode[];
122
+ data: readonly TreemapNode[];
63
123
  /**
64
124
  * Formats a node's value in the tooltip. Handed the node's SHARE of what is drawn
65
125
  * (0–1) as a second argument, because a tile is read for how big it is relative to
66
126
  * the rest, and "€412.30" alone makes the reader estimate that ratio off the areas.
67
127
  * The share is `undefined` when the drawn total is not positive.
68
128
  *
69
- * Default: the bare number through `Intl.NumberFormat` in the runtime's locale. A
70
- * host that has a currency or a locale passes this — the kit knows neither.
129
+ * A tile's and a group's accessible name (`labels.tile`, `labels.group`, 0.52) says the
130
+ * value too, asked of the formatter as the tooltip asks it, share and all, and its text
131
+ * is used; markup is said as the bare number. The name says the share itself, so a
132
+ * formatter that writes it into its text has it said twice.
133
+ *
134
+ * Default: the bare number through `Intl.NumberFormat` in the provider's locale (0.52,
135
+ * DOC-10: before, the runtime's). A host with a currency passes this — the kit knows
136
+ * none.
71
137
  */
72
138
  valueFormatter?: (value: number, share: number | undefined) => ReactNode;
139
+ /**
140
+ * What the map SHOWS, in a sentence — "Rent is three fifths of the month." — the
141
+ * figure's accessible description (0.52, DSP-5). Not drawn; a caption the eye should
142
+ * read too belongs in the page.
143
+ */
144
+ description?: string;
145
+ /** Per-map strings over `<UiKitProvider labels={{ treemap }}>`. */
146
+ labels?: LabelOverride<TreemapLabels>;
73
147
  /** Chart height: a CSS length (`"20rem"`, `"min(20rem, 50dvh)"`), a Tailwind class,
74
148
  * or px. Default `"20rem"` — 320 px at Normal, growing with the text size like the
75
149
  * labels in the tiles (0.32, docs/text-size-harmonization.md §10.10). */
76
150
  height?: ChartHeight;
77
- /** A tile was activated — by click, or by Enter/Space on a focused tile. Setting
78
- * this makes every tile a focusable `role="button"` named by its node name. */
79
- onNodeClick?: (id: string, name: string) => void;
151
+ /**
152
+ * A tile was activated — by click, or by Enter/Space on a focused tile. Setting this
153
+ * makes every tile a `role="button"`.
154
+ *
155
+ * Either way every tile is reachable (0.52, DSP-5): ONE tab stop for the map, the
156
+ * arrows (←/→ along the reading direction, ↓/↑), Home and End between tiles in the
157
+ * order given, each named "name: value (share)" (`labels.tile`) — a `role="img"` to read
158
+ * when nothing listens, as PieChart's slices. Before, a map nobody clicked was nothing
159
+ * to a screen reader, and a clickable tile said its name alone, one tab stop each.
160
+ *
161
+ * In a map with groups (`TreemapNode.children`, 0.52) the walk is the data's own order,
162
+ * depth first: a group's frame, then its tiles, then the next group. A frame is named
163
+ * by `labels.group` ("Interspar, group: €1,240 (40%)"), a tile inside one by
164
+ * `labels.inGroup` round its own name ("Rankweil: €412 (12%), in Interspar"). Only
165
+ * tiles reach this callback (a frame reaches `onGroupClick`); a tile inside a group is
166
+ * handed a third argument, the ids of its groups, outermost first. Optional in the type,
167
+ * so a forwarder calling this with two arguments still type-checks — and absent for a
168
+ * tile at the top, so a flat map calls it exactly as before.
169
+ */
170
+ onNodeClick?: (id: string, name: string, groups?: readonly string[]) => void;
171
+ /**
172
+ * A group's frame was activated — by click on its band or margin, or by Enter/Space on
173
+ * the focused frame (0.52). Setting this makes every frame a `role="button"`; without it
174
+ * a frame is a `role="img"`, still a stop of the map's keyboard so its total is heard.
175
+ * `groups` as for {@link TreemapProps.onNodeClick}: the groups round this one, passed
176
+ * only for a group inside another.
177
+ */
178
+ onGroupClick?: (id: string, name: string, groups?: readonly string[]) => void;
80
179
  /**
81
180
  * The node names are the user's own data (payees, not categories): the tile labels
82
181
  * and the whole tooltip bubble get `data-private`, the same attribute the kit's
83
182
  * `Tooltip redact` and `ChartTooltipContent`'s value cell carry, so the host's one
84
183
  * privacy rule reaches them. Off by default — a category name is a label of the app.
184
+ *
185
+ * Every tile is tagged as a whole too, clickable or not (0.52): its accessible name
186
+ * (`labels.tile`) says the name and the value, in an attribute a host's redaction
187
+ * reaches only through the tag.
85
188
  */
86
189
  redactNames?: boolean;
87
190
  /**
@@ -93,6 +196,8 @@ export interface TreemapProps extends Omit<ComponentPropsWithoutRef<"div">, "chi
93
196
  * node by name or by raw value, so a per-node note would be keyed on an amount (two
94
197
  * nodes of equal size would swap notes). And a tooltip is a pointer affordance: a
95
198
  * phone never hovers, so a figure that only exists on hover does not exist there.
199
+ *
200
+ * Asked for tiles only: a group's band holds its name and nothing else.
96
201
  */
97
202
  nodeNote?: (id: string) => string | undefined;
98
203
  /**
@@ -101,6 +206,9 @@ export interface TreemapProps extends Omit<ComponentPropsWithoutRef<"div">, "chi
101
206
  * and a map made of unlabelled rectangles answers nothing; around 24 is where the
102
207
  * smallest tile of a skewed series reaches that floor on a phone-width card. The
103
208
  * caller owns the "N more not shown" line, since it knows how many it passed.
209
+ *
210
+ * With groups, the tiles are counted across them in the order given, depth first; a
211
+ * group left with none is not drawn, and a group's frame is not a tile.
104
212
  */
105
213
  maxTiles?: number;
106
214
  /**
@@ -131,25 +239,9 @@ export interface TreemapProps extends Omit<ComponentPropsWithoutRef<"div">, "chi
131
239
  edges?: TreemapEdges;
132
240
  }
133
241
 
134
- /** {@link TreemapProps.edges}. */
135
- export type TreemapEdges = "auto" | "uniform" | "contrast";
136
-
137
- // `<html data-contrast>`, followed live: what `edges="auto"` keys on. Read here rather
138
- // than through theme/contrast, whose store would come into the chart entry with it.
139
- function subscribeContrast(onChange: () => void): () => void {
140
- if (typeof MutationObserver === "undefined" || typeof document === "undefined") return () => {};
141
- const observer = new MutationObserver(onChange);
142
- observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-contrast"] });
143
- return () => observer.disconnect();
144
- }
145
- const readMoreContrast = () =>
146
- typeof document !== "undefined" && document.documentElement.getAttribute("data-contrast") === "more";
147
-
148
- /** The edges a tile draws: `auto` resolved against the contrast in force. */
149
- function useTileEdges(edges: TreemapEdges): "uniform" | "contrast" {
150
- const more = useSyncExternalStore(subscribeContrast, readMoreContrast, () => false);
151
- return edges === "auto" ? (more ? "contrast" : "uniform") : edges;
152
- }
242
+ /** {@link TreemapProps.edges}. The switch itself is chart-edge's `useEdgesInForce`,
243
+ * which SeriesChart's bars and areas follow too since 0.52 (DSP-18). */
244
+ export type TreemapEdges = ChartEdges;
153
245
 
154
246
  // ── The categorical ramp, as concrete hex ──────────────────────────────────
155
247
  //
@@ -164,19 +256,23 @@ function useTileEdges(edges: TreemapEdges): "uniform" | "contrast" {
164
256
 
165
257
  const CHART_TOKENS = PALETTE_SIZE;
166
258
 
167
- /** The resolved ramp, joined into ONE string so `useSyncExternalStore` compares a
168
- * primitive: a fresh array per read would never be `Object.is`-equal to the last and
169
- * would re-render forever. */
259
+ /** The resolved ramp, and after a `|` the card's colour, joined into ONE string so
260
+ * `useSyncExternalStore` compares a primitive: a fresh array per read would never be
261
+ * `Object.is`-equal to the last and would re-render forever. */
170
262
  function readChartTokens(): string {
171
263
  const root = document.documentElement;
172
264
  const fallback = root.classList.contains("dark") ? PALETTE_HEX.dark : PALETTE_HEX.light;
173
265
  const style = getComputedStyle(root);
174
- return Array.from({ length: CHART_TOKENS }, (_, i) => {
266
+ const ramp = Array.from({ length: CHART_TOKENS }, (_, i) => {
175
267
  const v = style.getPropertyValue(`--chart-${i + 1}`).trim();
176
268
  // An unresolved token (a host that never loaded tokens.css, a test DOM) falls
177
269
  // back to the built-in ramp for the current theme rather than to no colour.
178
270
  return parseHex(v) ? v : fallback[i];
179
271
  }).join(",");
272
+ // The card, which `TreemapNode.shade` mixes a hue into (0.52). No fallback: a page that
273
+ // declares no `--bg-surface` hex leaves nothing to mix into, and the tile keeps its hue.
274
+ const card = style.getPropertyValue("--bg-surface").trim();
275
+ return `${ramp}|${parseHex(card) ? card : ""}`;
180
276
  }
181
277
 
182
278
  function subscribeChartTokens(onChange: () => void): () => void {
@@ -188,11 +284,21 @@ function subscribeChartTokens(onChange: () => void): () => void {
188
284
  return () => observer.disconnect();
189
285
  }
190
286
 
191
- const SERVER_TOKENS = PALETTE_HEX.light.join(",");
287
+ const SERVER_TOKENS = `${PALETTE_HEX.light.join(",")}|`;
192
288
 
193
- function useChartTokenHex(): string[] {
289
+ interface ChartTokens {
290
+ ramp: readonly string[];
291
+ /** `--bg-surface` as hex, or null when the page declares none. */
292
+ card: string | null;
293
+ }
294
+
295
+ function useChartTokenHex(): ChartTokens {
194
296
  const joined = useSyncExternalStore(subscribeChartTokens, readChartTokens, () => SERVER_TOKENS);
195
- return joined.split(",");
297
+ // One object per reading, not per render: the map's nodes are memoised on it.
298
+ return useMemo(() => {
299
+ const [ramp = "", card = ""] = joined.split("|");
300
+ return { ramp: ramp.split(","), card: card || null };
301
+ }, [joined]);
196
302
  }
197
303
 
198
304
  const CHART_TOKEN_REF = /^var\(\s*--chart-(\d+)\s*\)$/;
@@ -214,6 +320,60 @@ function measurableFill(fill: string, tokens: readonly string[]): string | null
214
320
  return resolveChartColor(fill, getComputedStyle(document.documentElement));
215
321
  }
216
322
 
323
+ // ── What a tile reads from its map ───────────────────────────────────────
324
+ //
325
+ // 0.52 (refactor audit 2026-10-30, DSP-22): every tile resolved the token ramp, the ink
326
+ // edge and the edge rule itself — three stores, each with a MutationObserver on <html>, so
327
+ // 72 on keksdose's 24-tile maps, and up to 29 custom properties re-read on every render of
328
+ // every tile. `Treemap` resolves them once and hands them down here; a TreemapCell drawn
329
+ // in a caller's own recharts `<Treemap>` has no map above it and resolves its own.
330
+
331
+ interface TileContext {
332
+ tokens: readonly string[];
333
+ edgeFor: (fill: string | undefined) => string | undefined;
334
+ edges: "uniform" | "contrast";
335
+ /** The map's keyboard (DSP-5): absent for a standalone cell. */
336
+ keys?: TileKeys;
337
+ /** Where every node is drawn in a chart of this size (see "Groups"), by path: absent
338
+ * for a standalone cell, which draws where recharts laid it. */
339
+ layout?: (width: number, height: number, scale: number) => ReadonlyMap<string, Placed>;
340
+ /** The pointer is on the node drawn at `box`: where the map's tooltip stands. */
341
+ hover?: (box: Box) => void;
342
+ }
343
+
344
+ /**
345
+ * Keyed by a node's PATH (`MapNode.path`), not its id: a group and a tile may share an id
346
+ * — chain 12 and shop 12 come from two tables — and keyed by id the map had two tab stops,
347
+ * the frame said the tile's name and the arrows stuck (0.52 code review of treemap.tsx,
348
+ * finding 3).
349
+ */
350
+ interface TileKeys {
351
+ /** A tile's or a group's accessible name (`labels.tile`, `labels.group`). */
352
+ name: (path: string) => string | undefined;
353
+ onFocus: (path: string) => void;
354
+ /** A node's element as it mounts (and `null` as it goes): the map sets its `tabindex` and
355
+ * gives it back the focus its previous element held (see the map's keyboard). */
356
+ register: (path: string, element: SVGGElement | null) => void;
357
+ /** Moves focus for an arrow, Home or End pressed on node `path`; `false` for any other key. */
358
+ move: (path: string, event: KeyboardEvent<SVGGElement>) => boolean;
359
+ }
360
+
361
+ const TreemapTiles = createContext<TileContext | null>(null);
362
+
363
+ // ── Groups: a frame round their tiles ─────────────────────────────────────
364
+ //
365
+ // 0.52 (keksdose dev #642). A group is a frame with its name in a band along the top, its
366
+ // tiles inside it. recharts lays nested data out edge to edge, with no room for a band, so
367
+ // the map lays it out ITSELF (treemap-layout.ts): squarified as recharts does it, each
368
+ // group given its frame's area on top of its tiles' — so a tile's area stays its share of
369
+ // the picture in any group (0.52 code review of treemap.tsx, finding 1) — and computed up
370
+ // front from the drawn nodes and the chart's size, which every cell reads by its path.
371
+ // Before, a group's cell made the room during render and left it in a module cache for its
372
+ // children's cells, which held only while recharts rendered a group before its children
373
+ // (finding 8). recharts still renders each node's cell, and its tooltip; the map stands the
374
+ // tooltip at the tile as drawn (`TreemapTooltip`), since recharts' own anchor is the centre
375
+ // of the rectangle IT laid out.
376
+
217
377
  // ── Tile ──────────────────────────────────────────────────────────────────
218
378
 
219
379
  /** The name's size in px at Normal; a tile draws it × the text size's scale (§10.10), and
@@ -235,6 +395,19 @@ export function fitLabel(name: string, boxWidth: number, fontSize: number): stri
235
395
  return name.length <= max ? name : `${name.slice(0, max - 1).trimEnd()}…`;
236
396
  }
237
397
 
398
+ /** A tile's text, painted over a halo in its tile's colour (see `TreemapCell`). */
399
+ const haloOf = (halo: string) => ({
400
+ stroke: halo,
401
+ strokeWidth: 2.5,
402
+ strokeLinejoin: "round" as const,
403
+ paintOrder: "stroke",
404
+ });
405
+
406
+ /** A band's name: the page's text on the well, 8.2:1 at the least in every preset, 7.4:1
407
+ * under More contrast (measured 0.52) — no halo, nothing to stand out from; `none`
408
+ * against the inherited chart stroke. */
409
+ const BAND_INK = { fill: "var(--text-primary)", stroke: "none" };
410
+
238
411
  /** What recharts hands a `content` element for each node, plus the three options the
239
412
  * chart forwards. Everything optional because recharts clones the element it was
240
413
  * given and fills the geometry in — the element as written carries only the options. */
@@ -249,14 +422,28 @@ export interface TreemapCellProps {
249
422
  /** See {@link TreemapNode.labelColor}; recharts hands each node's fields to its cell. */
250
423
  labelColor?: string;
251
424
  id?: string;
252
- onNodeClick?: (id: string, name: string) => void;
425
+ /** Called with `groups` as its third argument when the cell has any; see
426
+ * {@link TreemapProps.onNodeClick}. */
427
+ onNodeClick?: (id: string, name: string, groups?: readonly string[]) => void;
428
+ /** A group's cell's click; see {@link TreemapProps.onGroupClick}. */
429
+ onGroupClick?: (id: string, name: string, groups?: readonly string[]) => void;
430
+ /** The ids of the groups round the node, outermost first. `Treemap` puts it on every
431
+ * node it hands recharts; a cell drawn in a caller's own map has it if the data does. */
432
+ groups?: readonly string[];
433
+ /** The node's place in the tree, which `Treemap` puts on every node it hands recharts:
434
+ * its key in the map's keyboard, where an id may repeat between a group and a tile. */
435
+ path?: string;
436
+ /** Filled in by recharts for a GROUP: the nodes laid out inside it. A cell with
437
+ * children draws a frame, not a tile (0.52). */
438
+ children?: readonly object[] | null;
253
439
  redactNames?: boolean;
254
440
  nodeNote?: (id: string) => string | undefined;
255
441
  /** The page's reading direction, which `Treemap` reads off its own root. In `rtl`
256
442
  * the label stands in the tile's top RIGHT corner and is shaped right-to-left, so a
257
443
  * clipped name ends in its ellipsis on the reading end. Default `ltr`. */
258
444
  dir?: Direction;
259
- /** See {@link TreemapProps.edges}. Default `auto`. */
445
+ /** See {@link TreemapProps.edges}. Default `auto`. Read by a cell drawn on its own; a
446
+ * `Treemap` hands its tiles its own `edges`. */
260
447
  edges?: TreemapEdges;
261
448
  }
262
449
 
@@ -270,8 +457,43 @@ export interface TreemapCellProps {
270
457
  * draws its own label with a halo in the TILE colour painted BEHIND the fill
271
458
  * (`paint-order: stroke`), which defeats any inherited stroke and keeps the text solid
272
459
  * where it overlaps a lighter or darker part of its own tile.
460
+ *
461
+ * Inside a `Treemap` it is one stop of the map's keyboard, named with its value and
462
+ * share (0.52); drawn on its own it is a stop only as a button (`onNodeClick`), named by
463
+ * `name`.
464
+ *
465
+ * A node with `children` is a GROUP (0.52): the cell draws a frame in the well's colour
466
+ * (`--bg-surface-2`), with the group's name in a band along its top where the group has
467
+ * the room (61px at Normal, treemap-layout's `BODY_SHARE`). Inside a `Treemap` its tiles are laid out
468
+ * in the frame's body; in a caller's own nested recharts `<Treemap>` they are drawn where
469
+ * recharts lays them, over the frame — only `Treemap` makes the room.
273
470
  */
274
- export function TreemapCell({
471
+ export function TreemapCell(props: TreemapCellProps) {
472
+ const map = useContext(TreemapTiles);
473
+ return map ? <MapTile {...props} map={map} /> : <StandaloneTile {...props} />;
474
+ }
475
+
476
+ /** A cell inside a `Treemap`: drawn where the map's layout puts its node, read by path
477
+ * for the chart's size as recharts measured it. */
478
+ function MapTile(props: TreemapCellProps & { map: TileContext }) {
479
+ const width = useChartWidth();
480
+ const height = useChartHeight();
481
+ const { scale } = useTextSize();
482
+ const place =
483
+ props.path !== undefined && width && height ? props.map.layout?.(width, height, scale).get(props.path) : undefined;
484
+ return <Tile {...props} place={place} />;
485
+ }
486
+
487
+ /** A cell with no `Treemap` above it: it resolves what the map would have handed it. */
488
+ function StandaloneTile(props: TreemapCellProps) {
489
+ const tokens = useChartTokenHex().ramp;
490
+ const edgeFor = useChartEdge();
491
+ const edges = useEdgesInForce(props.edges ?? "auto");
492
+ const map = useMemo(() => ({ tokens, edgeFor, edges }), [tokens, edgeFor, edges]);
493
+ return <Tile {...props} map={map} />;
494
+ }
495
+
496
+ function Tile({
275
497
  depth,
276
498
  x = 0,
277
499
  y = 0,
@@ -282,85 +504,126 @@ export function TreemapCell({
282
504
  labelColor,
283
505
  id,
284
506
  onNodeClick,
507
+ onGroupClick,
508
+ groups,
509
+ path,
510
+ children,
285
511
  redactNames,
286
512
  nodeNote,
287
513
  dir = "ltr",
288
- edges: edgesProp = "auto",
289
- }: TreemapCellProps) {
514
+ map,
515
+ place,
516
+ }: TreemapCellProps & { map: TileContext; place?: Placed }) {
290
517
  // Before the early return: a hook has to run on every render of the cell.
291
- const tokenHex = useChartTokenHex();
292
518
  const { scale } = useTextSize();
293
- const edgeFor = useChartEdge();
294
- const edges = useTileEdges(edgesProp);
519
+ const { tokens: tokenHex, edgeFor, edges, keys } = map;
295
520
  // Depth 0 is recharts' synthetic root, which spans the whole chart.
296
521
  if (!depth) return null;
522
+ // Inside a Treemap, where the map's layout draws the node; on its own, where recharts
523
+ // laid it out.
524
+ const box = place?.box ?? { x, y, width, height };
297
525
  // Whole pixels: a rect on a half pixel renders its edge across two half-lit
298
526
  // columns, and the text baseline inherits that blur.
299
- const px = Math.round(x);
300
- const py = Math.round(y);
301
- const pw = Math.round(width);
302
- const ph = Math.round(height);
527
+ const px = Math.round(box.x);
528
+ const py = Math.round(box.y);
529
+ const pw = Math.round(box.width);
530
+ const ph = Math.round(box.height);
531
+ const isGroup = children != null && children.length > 0;
532
+ const group = !isGroup
533
+ ? undefined
534
+ : place?.body
535
+ ? { banded: place.banded ?? false, body: place.body }
536
+ : groupBody({ x: px, y: py, width: pw, height: ph }, scale);
303
537
  const tileFill = fill ?? "var(--chart-1)";
304
538
  const measured = measurableFill(tileFill, tokenHex);
305
539
  const ink = labelColor ?? (measured ? textOn(measured) : "currentColor");
306
540
  // `contrast`: a tile in a hue under 3:1 against the card gets a 1px ink edge inside
307
541
  // the gutter (WCAG 1.4.11, anyplot's rule); a hue that clears it stays bare.
308
542
  // `uniform`: no per-tile edge; every tile is parted by the same gutter.
309
- const edge = edges === "contrast" && measured ? edgeFor(measured) : undefined;
543
+ // A group's frame is the well, 1.09–1.24:1 from the card in every preset and contrast
544
+ // (measured 0.52): under `contrast` it always takes the edge.
545
+ const edge =
546
+ edges !== "contrast" ? undefined : group ? CHART_ROLES.outline : measured ? edgeFor(measured) : undefined;
310
547
  // The halo only on an OPAQUE fill we could measure. A translucent fill (a
311
548
  // `color-mix(… transparent)`) composited twice — once as the tile, once as the halo
312
549
  // over it — draws a darker ring round every glyph, which read as doubled text on the
313
550
  // faded tiles. `none` still overrides the inherited chart `stroke`.
314
551
  const halo = measured ? tileFill : "none";
315
552
  // Below ~44x20 there is no room for a readable word, so no label at all rather than
316
- // a one-letter stub bleeding over the tile edge.
553
+ // a one-letter stub bleeding over the tile edge. A group's name only in its band.
317
554
  const labelSize = LABEL_FONT_SIZE * scale;
318
555
  const noteSize = NOTE_FONT_SIZE * scale;
319
- const label = pw >= 44 * scale && ph >= 20 * scale ? fitLabel(name ?? "", pw, labelSize) : null;
556
+ const roomForName = group ? group.banded : pw >= 44 * scale && ph >= 20 * scale;
557
+ const label = roomForName ? fitLabel(name ?? "", pw, labelSize) : null;
320
558
  // Only under a label that is itself drawn — a stray "+12%" on an unnamed rectangle
321
559
  // names nothing — and only where a second baseline fits: the note's baseline sits
322
560
  // 29px down and an 11px glyph hangs ~2px below it, so 36 leaves the same ~5px under
323
- // the note that the label keeps under itself at the 20px floor.
324
- const rawNote = id != null && nodeNote ? nodeNote(id) : undefined;
561
+ // the note that the label keeps under itself at the 20px floor. Never in a band.
562
+ const rawNote = !group && id != null && nodeNote ? nodeNote(id) : undefined;
325
563
  const note = label != null && rawNote != null && ph >= 36 * scale ? fitLabel(rawNote, pw, noteSize) : null;
326
564
  // The label's START edge, 6px in from the tile's. The tiles themselves are laid out
327
565
  // physically (the plot is not mirrored, see `ChartContainer`) and the chart's SVG is
328
566
  // pinned `ltr`, so the direction is set on each <text> explicitly: `start` is then
329
567
  // the right end in RTL, and the label grows leftwards from the tile's right edge.
330
568
  const textX = dir === "rtl" ? px + pw - 6 : px + 6;
331
- const activate = id != null && onNodeClick ? () => onNodeClick(id, name ?? "") : undefined;
332
- const onKeyDown = activate
569
+ // A tile's click is `onNodeClick`, a frame's `onGroupClick`. The groups go along only
570
+ // when there are any, so a flat map calls back exactly as it did before 0.52.
571
+ const click = group ? onGroupClick : onNodeClick;
572
+ const activate =
573
+ id != null && click
574
+ ? () => (groups && groups.length > 0 ? click(id, name ?? "", groups) : click(id, name ?? ""))
575
+ : undefined;
576
+ // Inside a Treemap every tile and frame is a stop of the map's one roving tab stop,
577
+ // named with its value and share; a standalone cell is a stop of its own only when it
578
+ // is a button.
579
+ const roving = path != null && keys ? keys : undefined;
580
+ const focusable = roving !== undefined || activate !== undefined;
581
+ const onKeyDown = focusable
333
582
  ? (e: KeyboardEvent<SVGGElement>) => {
334
- if (e.key !== "Enter" && e.key !== " ") return;
583
+ if (roving?.move(path!, e)) {
584
+ e.preventDefault();
585
+ return;
586
+ }
587
+ if (!activate || (e.key !== "Enter" && e.key !== " ")) return;
335
588
  e.preventDefault();
336
589
  activate();
337
590
  }
338
591
  : undefined;
339
592
  return (
340
593
  <g
594
+ ref={roving ? refFor(roving.register, path!) : undefined}
341
595
  onClick={activate}
342
596
  onKeyDown={onKeyDown}
597
+ onFocus={roving ? () => roving.onFocus(path!) : undefined}
598
+ onMouseEnter={map.hover ? () => map.hover!({ x: px, y: py, width: pw, height: ph }) : undefined}
343
599
  // A clickable tile is a button to a keyboard and a screen reader too; the
344
- // drilldown it opens was mouse-only in the app this came from. Named by the node
345
- // name, which is also why the group carries `data-private` under `redactNames`.
346
- role={activate ? "button" : undefined}
347
- tabIndex={activate ? 0 : undefined}
348
- aria-label={activate ? name : undefined}
349
- data-private={activate && redactNames ? "" : undefined}
600
+ // drilldown it opens was mouse-only in the app this came from. Under `redactNames`
601
+ // the group carries `data-private` whenever it carries a name, button or not: the
602
+ // name says the node's name and value, and an untagged `aria-label` is read by a
603
+ // session replay or a redaction keyed on `[data-private]` (0.52, code review of
604
+ // feat/0.52.0 — DSP-5 named every tile, and this tagged only the buttons).
605
+ role={activate ? "button" : roving ? "img" : undefined}
606
+ // In a map, -1 as drawn: the map moves its one stop on the elements in place.
607
+ tabIndex={roving ? -1 : activate ? 0 : undefined}
608
+ aria-label={focusable ? (roving?.name(path!) ?? name) : undefined}
609
+ data-private={focusable && redactNames ? "" : undefined}
610
+ data-group={group ? "" : undefined}
350
611
  cursor={activate ? "pointer" : undefined}
351
612
  // CSS beats a presentation attribute, so this recolours the rect's own stroke
352
613
  // for as long as the tile has keyboard focus.
353
- className={activate ? "outline-none [&:focus-visible>rect]:stroke-[var(--brand)]" : undefined}
614
+ className={focusable ? "outline-none [&:focus-visible>rect]:stroke-[var(--brand)]" : undefined}
354
615
  >
355
616
  {/* The 2px gutter: the page's ground (`uniform`, the same dark line round every
356
617
  tile in the dark theme) or the card showing through (`contrast`). Never a white
357
- grid: on a dark surface white lines glare harder than the tiles. */}
618
+ grid: on a dark surface white lines glare harder than the tiles. A group's frame
619
+ is the well, the inset surface: the page's own colour in light, a step up from
620
+ it in dark, and never one of the hues its tiles are told apart by. */}
358
621
  <rect
359
622
  x={px}
360
623
  y={py}
361
624
  width={pw}
362
625
  height={ph}
363
- fill={tileFill}
626
+ fill={group ? "var(--bg-surface-2)" : tileFill}
364
627
  stroke={edges === "contrast" ? "var(--bg-surface)" : "var(--bg-page)"}
365
628
  strokeWidth={2}
366
629
  rx={3}
@@ -385,13 +648,10 @@ export function TreemapCell({
385
648
  // <text> exactly as it reaches a table cell.
386
649
  data-private={redactNames ? "" : undefined}
387
650
  x={textX}
388
- y={py + 15 * scale}
651
+ // A band's name centred in its 20px; a tile's where its floor leaves room below.
652
+ y={py + (group ? 14 : 15) * scale}
389
653
  direction={dir}
390
- fill={ink}
391
- stroke={halo}
392
- strokeWidth={2.5}
393
- strokeLinejoin="round"
394
- paintOrder="stroke"
654
+ {...(group ? BAND_INK : { fill: ink, ...haloOf(halo) })}
395
655
  fontSize={labelSize}
396
656
  fontWeight={600}
397
657
  style={{ pointerEvents: "none" }}
@@ -406,10 +666,7 @@ export function TreemapCell({
406
666
  direction={dir}
407
667
  fill={ink}
408
668
  fillOpacity={0.85}
409
- stroke={halo}
410
- strokeWidth={2.5}
411
- strokeLinejoin="round"
412
- paintOrder="stroke"
669
+ {...haloOf(halo)}
413
670
  fontSize={noteSize}
414
671
  fontWeight={500}
415
672
  style={{ pointerEvents: "none" }}
@@ -449,13 +706,182 @@ function RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent
449
706
  );
450
707
  }
451
708
 
452
- const DEFAULT_NUMBER = new Intl.NumberFormat();
709
+ /** Where the pointer last entered a node, as drawn: a store of one box, so that only the
710
+ * tooltip re-renders when it moves — not the map, nor (through recharts) every tile. */
711
+ interface Anchor {
712
+ read: () => Box | null;
713
+ subscribe: (onChange: () => void) => () => void;
714
+ set: (box: Box) => void;
715
+ }
716
+
717
+ function createAnchor(): Anchor {
718
+ let at: Box | null = null;
719
+ const listeners = new Set<() => void>();
720
+ return {
721
+ read: () => at,
722
+ subscribe: (onChange) => {
723
+ listeners.add(onChange);
724
+ return () => listeners.delete(onChange);
725
+ },
726
+ set: (box) => {
727
+ if (at && at.x === box.x && at.y === box.y && at.width === box.width && at.height === box.height) return;
728
+ at = box;
729
+ listeners.forEach((onChange) => onChange());
730
+ },
731
+ };
732
+ }
733
+
734
+ /**
735
+ * The map's tooltip, stood at the tile the pointer is on AS DRAWN. recharts anchors its
736
+ * tooltip at the centre of the rectangle it laid the node out in, and since 0.52 the map
737
+ * draws its nodes where its own layout puts them (see "Groups") — a tooltip anchored by
738
+ * recharts could stand over a neighbour. It stands 10px off the tile's centre, towards the
739
+ * chart's middle, so it does not run off the side the tile is on. `content` is the bubble,
740
+ * as recharts' own `content`.
741
+ */
742
+ function TreemapTooltip({ content, anchor }: { content: ReactElement; anchor: Anchor }) {
743
+ const at = useSyncExternalStore(anchor.subscribe, anchor.read, anchor.read);
744
+ const width = useChartWidth() ?? 0;
745
+ const height = useChartHeight() ?? 0;
746
+ if (!at) return <ChartTooltip content={content} />;
747
+ const cx = at.x + at.width / 2;
748
+ const cy = at.y + at.height / 2;
749
+ const left = cx > width / 2;
750
+ const up = cy > height / 2;
751
+ return (
752
+ <ChartTooltip
753
+ content={content}
754
+ position={{ x: cx + (left ? -10 : 10), y: cy + (up ? -10 : 10) }}
755
+ // `translate` composes with the `transform` recharts sets for `position`.
756
+ wrapperStyle={{ translate: `${left ? "-100%" : "0"} ${up ? "-100%" : "0"}` }}
757
+ />
758
+ );
759
+ }
760
+
761
+ /** What `Treemap` hands recharts per node: a tile, or a group with `children`. recharts
762
+ * hands each node's fields to its cell; a field left out of this never reaches
763
+ * `TreemapCell` (0.8.0 shipped `labelColor` without it — keksdose). A type, not an
764
+ * interface: recharts' `data` wants an index signature, which only an alias meets. */
765
+ type MapNode = {
766
+ id: string;
767
+ name: string;
768
+ /** A tile's value; a group's, its drawn tiles' together. recharts sums a group again
769
+ * for its layout, and its tooltip reads this field. */
770
+ size: number;
771
+ fill: string;
772
+ labelColor?: string;
773
+ /** The ids of the groups round the node, outermost first. */
774
+ groups: readonly string[];
775
+ /** The node's key in the map's keyboard and names: its id and its groups' (see TileKeys). */
776
+ path: string;
777
+ children?: MapNode[];
778
+ };
779
+
780
+ /** What a group hands down to the tiles inside it that set none of their own. */
781
+ interface Paint {
782
+ fill: string;
783
+ labelColor?: string;
784
+ shade?: number;
785
+ }
786
+
787
+ /**
788
+ * `data` as drawn: tiles with a positive value, at most `maxTiles` of them in the order
789
+ * given (depth first), and the groups that still hold any. `shaded` turns a fill and a
790
+ * `shade` into the fill drawn.
791
+ *
792
+ * Coloured by index into `data` AS GIVEN, before anything is dropped: a caller painting a
793
+ * legend or a second map from the same list must land on the same hue for the same row,
794
+ * and filtering first would shift every colour after a gap. Below the top a node takes
795
+ * its group's colour, so a group's tiles share one hue unless they set their own.
796
+ */
797
+ function mapNodes(
798
+ data: readonly TreemapNode[],
799
+ ramp: readonly string[],
800
+ maxTiles: number | null | undefined,
801
+ shaded: (fill: string, shade: number | undefined) => string,
802
+ ): MapNode[] {
803
+ // Whole tiles, as `slice(0, maxTiles ?? undefined)` counted them up to 0.51: 2.5 draws 2,
804
+ // NaN none and null every one (0.52 code review of treemap.tsx, finding 4 — a bare
805
+ // counter drew 3, and every tile; deep review, charts, finding 2 — Math.floor(null) is 0,
806
+ // and a JS caller's or a `number | null` setting's null drew an empty card).
807
+ let left = maxTiles == null ? Infinity : Number.isNaN(maxTiles) ? 0 : Math.floor(maxTiles);
808
+ const walk = (list: readonly TreemapNode[], groups: readonly string[], paint: Paint | undefined): MapNode[] =>
809
+ list.flatMap((d, idx): MapNode[] => {
810
+ const fill = d.fill ?? paint?.fill ?? ramp[idx % ramp.length]!;
811
+ // An inherited fill brings its ink along (a `color-mix()` group needs its
812
+ // `labelColor`); a fill of the tile's own is measured again.
813
+ const labelColor = d.labelColor ?? (d.fill === undefined ? paint?.labelColor : undefined);
814
+ const shade = d.shade ?? paint?.shade;
815
+ if (d.children && d.children.length > 0) {
816
+ const children = walk(d.children, [...groups, d.id], { fill, labelColor, shade });
817
+ if (children.length === 0) return [];
818
+ const size = children.reduce((acc, c) => acc + c.size, 0);
819
+ return [{ id: d.id, name: d.name, size, fill, groups, path: pathOf(groups, d.id), children }];
820
+ }
821
+ if (!(Number.isFinite(d.value) && d.value > 0) || left <= 0) return [];
822
+ left -= 1;
823
+ const drawn = shaded(fill, shade);
824
+ // An ink the GROUP chose was chosen for the group's colour: once `shade` makes the
825
+ // tile another hex, that hex is measured instead (0.52 code review of treemap.tsx,
826
+ // finding 5 — white ink on a tile shaded almost to the card, ~1.3:1). The tile's
827
+ // own `labelColor` still wins: the caller set it for this tile.
828
+ const ink = d.labelColor ?? (drawn === fill ? labelColor : undefined);
829
+ return [
830
+ { id: d.id, name: d.name, size: d.value, fill: drawn, labelColor: ink, groups, path: pathOf(groups, d.id) },
831
+ ];
832
+ });
833
+ return walk(data, [], undefined);
834
+ }
835
+
836
+ /** The last layout made for each drawn tree, by size and text size: a memo, not state — it
837
+ * is a pure function of its key, so whichever cell asks first, every cell gets the same. */
838
+ const LAYOUTS = new WeakMap<readonly MapNode[], { key: string; placed: ReadonlyMap<string, Placed> }>();
839
+
840
+ function layoutFor(nodes: readonly MapNode[], width: number, height: number, scale: number) {
841
+ const key = `${width}x${height}@${scale}`;
842
+ const last = LAYOUTS.get(nodes);
843
+ if (last?.key === key) return last.placed;
844
+ const placed = layoutTreemap(nodes, width, height, scale);
845
+ LAYOUTS.set(nodes, { key, placed });
846
+ return placed;
847
+ }
848
+
849
+ /** One ref callback per map and path, the same on every render: a new function each time
850
+ * makes React detach and attach the element on every render of the tile. A path no longer
851
+ * drawn is let go as the map lays out its new data (see the map's keyboard). */
852
+ const REFS = new WeakMap<TileKeys["register"], Map<string, (element: SVGGElement | null) => void>>();
853
+
854
+ function refFor(register: TileKeys["register"], path: string) {
855
+ let byPath = REFS.get(register);
856
+ if (!byPath) {
857
+ byPath = new Map();
858
+ REFS.set(register, byPath);
859
+ }
860
+ let ref = byPath.get(path);
861
+ if (!ref) {
862
+ ref = (element) => register(path, element);
863
+ byPath.set(path, ref);
864
+ }
865
+ return ref;
866
+ }
867
+
868
+ /** A node's path: its groups' ids and its own, as JSON so that no id can fake a separator. */
869
+ const pathOf = (groups: readonly string[], id: string) => JSON.stringify([...groups, id]);
870
+
871
+ /** `nodes` in the order the keyboard walks them: each group, then what is inside it. */
872
+ function depthFirst(nodes: readonly MapNode[]): MapNode[] {
873
+ return nodes.flatMap((n) => [n, ...(n.children ? depthFirst(n.children) : [])]);
874
+ }
453
875
 
454
876
  /**
455
877
  * Tile chart: one rectangle per {@link TreemapNode}, sized by `value`, coloured by
456
878
  * index from the chart ramp (or by the node's own `fill`), labelled in whichever of the
457
879
  * kit's two inks measures better against it. Hover shows the name and the formatted
458
- * value; `onNodeClick` turns the tiles into buttons.
880
+ * value; every tile is a keyboard stop named with its value and share (see
881
+ * `onNodeClick`), and `onNodeClick` turns the tiles into buttons.
882
+ *
883
+ * A node with `children` is a group (0.52): a labelled frame with its tiles inside, in
884
+ * its colour unless they set their own — see {@link TreemapNode.children}.
459
885
  *
460
886
  * Renders nothing when no node has a positive value — an empty box is a hole in the
461
887
  * card, and the caller's own sentence is the better answer there.
@@ -467,15 +893,29 @@ export function Treemap({
467
893
  valueFormatter,
468
894
  height = "20rem",
469
895
  onNodeClick,
896
+ onGroupClick,
470
897
  redactNames = false,
471
898
  nodeNote,
472
899
  maxTiles,
473
900
  colors,
474
901
  edges = "auto",
902
+ description,
903
+ labels: labelsProp,
475
904
  style,
476
905
  ...rest
477
906
  }: TreemapProps) {
478
- const tokenHex = useChartTokenHex();
907
+ // Resolved once for every tile, which reads them from `TreemapTiles` (DSP-22).
908
+ const { ramp: tokenHex, card } = useChartTokenHex();
909
+ const edgeFor = useChartEdge();
910
+ const edgesInForce = useEdgesInForce(edges);
911
+ const labels = useKitLabels("treemap", DEFAULT_TREEMAP_LABELS, labelsProp);
912
+ // The provider's locale (0.52, refactor audit 2026-10-30, DOC-10): this was the one
913
+ // formatter default in the kit that took the runtime's.
914
+ const locale = useKitLocale();
915
+ const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
916
+ // One decimal under 10 %, the rule PieChart says its shares by too (lib/share-format):
917
+ // a tile of 0.4 % named "0%" says it is not there.
918
+ const shareText = useMemo(() => shareFormat(locale), [locale]);
479
919
  // Read off the DOM after mount: the `dir` a treemap sits under is an ancestor's
480
920
  // attribute, and SVG text anchoring cannot follow it by CSS (see TreemapCell). Again
481
921
  // on new data, because a map that had nothing to draw rendered no root to read.
@@ -485,58 +925,207 @@ export function Treemap({
485
925
  setDir(dirOf(rootRef.current));
486
926
  }, [data]);
487
927
  const ramp = colors && colors.length > 0 ? colors : tokenHex;
488
- // Coloured by index into `data` AS GIVEN, before anything is dropped: a caller
489
- // painting a legend or a second map from the same list must land on the same hue
490
- // for the same row, and filtering first would shift every colour after a gap.
491
- const nodes = data
492
- .map((d, idx) => ({
493
- id: d.id,
494
- name: d.name,
495
- size: d.value,
496
- fill: d.fill ?? ramp[idx % ramp.length],
497
- // recharts hands each node's fields to its cell; a field left out of this map
498
- // never reaches `TreemapCell` (0.8.0 shipped `labelColor` without it — keksdose).
499
- labelColor: d.labelColor,
500
- }))
501
- .filter((n) => Number.isFinite(n.size) && n.size > 0)
502
- .slice(0, maxTiles ?? undefined);
503
- if (nodes.length === 0) return null;
504
- const box = chartHeightProps(height);
928
+ // Memoised: moving the tab stop re-renders the map, and fresh nodes would re-lay it.
929
+ const nodes = useMemo(
930
+ () =>
931
+ mapNodes(data, ramp, maxTiles, (fill, shade) => {
932
+ // `shade` (0.52): the hue mixed into the card as a hex, so it is measured like
933
+ // any other. A fill or a card it cannot read keeps the full hue.
934
+ if (shade === undefined || !Number.isFinite(shade) || shade >= 1 || !card) return fill;
935
+ const hex = measurableFill(fill, tokenHex);
936
+ return hex ? lerpHex(card, hex, shade) : fill;
937
+ }),
938
+ [data, ramp, maxTiles, tokenHex, card],
939
+ );
940
+ // The keyboard walks groups and tiles alike, in the data's order (see `onNodeClick`).
941
+ const order = useMemo(() => depthFirst(nodes), [nodes]);
942
+ // The map's layout (see "Groups"), for the size recharts measured: each cell asks for it
943
+ // by that size, and `layoutFor` makes it once per size and text size.
944
+ const layout = useMemo(
945
+ () => (width: number, height: number, scale: number) => layoutFor(nodes, width, height, scale),
946
+ [nodes],
947
+ );
948
+ // Where the pointer is, for the tooltip to stand at (`TreemapTooltip`).
949
+ const [anchor] = useState(createAnchor);
505
950
 
506
- // The share is of what is DRAWN, so it always reads as "of this picture".
507
- const total = nodes.reduce((acc, n) => acc + n.size, 0);
508
- const format = (v: number): ReactNode => {
509
- const share = total > 0 ? v / total : undefined;
510
- return valueFormatter ? valueFormatter(v, share) : formatWithMinus(DEFAULT_NUMBER, v);
511
- };
951
+ // The keyboard (DSP-5): one tab stop for the map, moved between tiles in `order` as
952
+ // PieChart's is between slices. The tiles are drawn by recharts, so they register
953
+ // their element here rather than being looked up.
954
+ //
955
+ // The stop is moved on the elements IN PLACE, as PieChart moves its own, and never
956
+ // reaches the tiles' context: kept there, every arrow-key step was a new context and
957
+ // every tile drew again — layout, fitted labels, notes (0.52 code review of treemap.tsx,
958
+ // finding 6). And recharts keys a node by where IT lays it out (`x-y-name`), so new data
959
+ // or a resize remounts the focused node: its element leaves holding the focus, which
960
+ // fell to <body> (finding 7). The path it held is kept, and its new element takes the
961
+ // focus back as it mounts — or, if the node is gone, the stop does.
962
+ const tiles = useRef(new Map<string, SVGGElement>());
963
+ const orderRef = useRef(order);
964
+ const stopRef = useRef<string | undefined>(undefined);
965
+ const restoreRef = useRef<string | null>(null);
966
+ /** The path holding the stop: the last one focused while it is drawn, else the first. */
967
+ const stopOf = useCallback(() => {
968
+ const drawn = orderRef.current;
969
+ return drawn.some((n) => n.path === stopRef.current) ? stopRef.current : drawn[0]?.path;
970
+ }, []);
971
+ const applyStop = useCallback(() => {
972
+ const at = stopOf();
973
+ tiles.current.forEach((el, path) => el.setAttribute("tabindex", path === at ? "0" : "-1"));
974
+ return at;
975
+ }, [stopOf]);
976
+ const register = useCallback(
977
+ (path: string, element: SVGGElement | null) => {
978
+ if (element) {
979
+ tiles.current.set(path, element);
980
+ element.setAttribute("tabindex", path === stopOf() ? "0" : "-1");
981
+ if (restoreRef.current === path) {
982
+ restoreRef.current = null;
983
+ element.focus();
984
+ }
985
+ return;
986
+ }
987
+ // Detached before it is removed: still the focused element if it held the focus.
988
+ // Kept for this commit only — the node's new element mounts in it, or the effect below
989
+ // hands the focus on. A node that left with no new data (`<Activity mode="hidden">`, a
990
+ // panel 0 wide) took the focus back whenever it was shown again, from wherever the
991
+ // reader was by then (0.52 code review, deep, charts, finding 3).
992
+ if (tiles.current.get(path) === document.activeElement) {
993
+ restoreRef.current = path;
994
+ queueMicrotask(() => {
995
+ if (restoreRef.current === path) restoreRef.current = null;
996
+ });
997
+ }
998
+ tiles.current.delete(path);
999
+ },
1000
+ [stopOf],
1001
+ );
1002
+ const onFocus = useCallback(
1003
+ (path: string) => {
1004
+ stopRef.current = path;
1005
+ applyStop();
1006
+ },
1007
+ [applyStop],
1008
+ );
1009
+ // New data: the order the stop is read against, the stop set again on what is drawn now,
1010
+ // the focus of a node that is gone handed to the stop — and the ref callbacks of the paths
1011
+ // no longer drawn let go: a map that switched data (a month picker over payees) kept one
1012
+ // for every node it had ever drawn (0.52 code review, deep, charts, finding 11).
1013
+ useLayoutEffect(() => {
1014
+ orderRef.current = order;
1015
+ const refs = REFS.get(register);
1016
+ if (refs) {
1017
+ const drawn = new Set(order.map((n) => n.path));
1018
+ for (const path of refs.keys()) if (!drawn.has(path)) refs.delete(path);
1019
+ }
1020
+ const at = applyStop();
1021
+ if (restoreRef.current === null) return;
1022
+ restoreRef.current = null;
1023
+ if (at) tiles.current.get(at)?.focus();
1024
+ }, [order, applyStop, register]);
1025
+ const move = useCallback((path: string, event: KeyboardEvent<SVGGElement>) => {
1026
+ const drawn = orderRef.current;
1027
+ const at = drawn.findIndex((n) => n.path === path);
1028
+ // The ends hold: a map is a plane, not PieChart's ring. → is the next tile in any page:
1029
+ // the tiles are laid out left to right whatever the reading direction (the plot is not
1030
+ // mirrored, see `ChartContainer`), and → read off the page walked an RTL page's map
1031
+ // backwards, against the picture (0.52 code review, deep, charts, finding 1).
1032
+ const to = at < 0 ? undefined : rovingTarget(event.key, "ltr", at, drawn.length, false);
1033
+ if (to === undefined) return false;
1034
+ const target = drawn[to];
1035
+ if (target) tiles.current.get(target.path)?.focus();
1036
+ return true;
1037
+ }, []);
1038
+
1039
+ // Everything below is memoised over what it reads (0.52, code review of feat/0.52.0). A
1040
+ // tile re-renders when its context or its recharts element changes, and recharts' map is
1041
+ // a PureComponent that redraws every node when `content` or its tooltip child is a new
1042
+ // element: built afresh per render, as they were, a page re-rendering the card or one
1043
+ // arrow-key step redrew every tile and ran `labels.tile` and two `Intl` formats per node
1044
+ // again — the cost DSP-22 had just taken out of the tiles.
1045
+ //
1046
+ // The share is of what is DRAWN, so it always reads as "of this picture" — a group's
1047
+ // and a tile's inside it alike.
1048
+ const total = useMemo(() => nodes.reduce((acc, n) => acc + n.size, 0), [nodes]);
1049
+ const format = useCallback(
1050
+ (v: number): ReactNode =>
1051
+ valueFormatter ? valueFormatter(v, total > 0 ? v / total : undefined) : formatWithMinus(numberFormat, v),
1052
+ [valueFormatter, total, numberFormat],
1053
+ );
1054
+ const names = useMemo(() => {
1055
+ // The value as the tooltip says it: the caller's formatter, handed the node's share as
1056
+ // it is there, when it gives text; a value it formats as markup is said as a number.
1057
+ //
1058
+ // It was asked with the share `undefined` — which, with anything drawn, it had never
1059
+ // been before 0.52 — so a formatter leaning on the share printed "NaN%" into every name
1060
+ // or threw and took the chart down (0.52 code review of treemap.tsx, finding 2). A guard
1061
+ // caught both; every named node has its share, so it is handed over instead (deep
1062
+ // review, charts, finding 13).
1063
+ const valueText = (v: number) => {
1064
+ const out = format(v);
1065
+ return typeof out === "string" || typeof out === "number" ? String(out) : formatWithMinus(numberFormat, v);
1066
+ };
1067
+ // A group's name by its path; a node's groups by theirs, its own path's prefixes.
1068
+ const groupName = new Map(order.filter((n) => n.children).map((n) => [n.path, n.name]));
1069
+ return new Map(
1070
+ order.map((n) => {
1071
+ const say = n.children ? labels.group : labels.tile;
1072
+ const own = say(n.name, valueText(n.size), shareText(total > 0 ? n.size / total : 0));
1073
+ // The innermost group first: "Rent: …, in Housing, in Fixed costs".
1074
+ const named = n.groups
1075
+ .map((g, i) => groupName.get(pathOf(n.groups.slice(0, i), g)) ?? g)
1076
+ .reduceRight((acc, group) => labels.inGroup(acc, group), own);
1077
+ return [n.path, named] as const;
1078
+ }),
1079
+ );
1080
+ }, [order, labels, format, numberFormat, shareText, total]);
1081
+ const keys = useMemo<TileKeys>(
1082
+ () => ({ name: (path) => names.get(path), onFocus, register, move }),
1083
+ [names, onFocus, register, move],
1084
+ );
1085
+ const context = useMemo<TileContext>(
1086
+ () => ({ tokens: tokenHex, edgeFor, edges: edgesInForce, keys, layout, hover: anchor.set }),
1087
+ [tokenHex, edgeFor, edgesInForce, keys, layout, anchor],
1088
+ );
512
1089
  const Content = redactNames ? RedactedTooltipContent : ChartTooltipContent;
1090
+ const cell = useMemo(
1091
+ () => (
1092
+ <TreemapCell
1093
+ onNodeClick={onNodeClick}
1094
+ onGroupClick={onGroupClick}
1095
+ redactNames={redactNames}
1096
+ nodeNote={nodeNote}
1097
+ dir={dir}
1098
+ />
1099
+ ),
1100
+ [onNodeClick, onGroupClick, redactNames, nodeNote, dir],
1101
+ );
1102
+ const tooltip = useMemo(
1103
+ () => <TreemapTooltip content={<Content hideLabel valueFormatter={format} />} anchor={anchor} />,
1104
+ [Content, format, anchor],
1105
+ );
1106
+
1107
+ if (nodes.length === 0) return null;
1108
+ const box = chartHeightProps(height);
513
1109
  return (
514
- <ChartContainer
515
- {...rest}
516
- ref={rootRef}
517
- config={TREEMAP_CONFIG}
518
- className={cn(box.className, rest.className)}
519
- style={{ ...style, ...box.style }}
520
- >
521
- {/* No chart-level `stroke`: recharts spreads it onto the <svg> root, from where
522
- it inherits into every label (see TreemapCell). Each cell strokes its own rect. */}
523
- <RechartsTreemap
524
- data={nodes}
525
- dataKey="size"
526
- fill="var(--chart-1)"
527
- isAnimationActive={false}
528
- content={
529
- <TreemapCell
530
- onNodeClick={onNodeClick}
531
- redactNames={redactNames}
532
- nodeNote={nodeNote}
533
- dir={dir}
534
- edges={edges}
535
- />
536
- }
1110
+ // Round the container, not inside it: ResponsiveContainer sizes its ONE child, the chart.
1111
+ <TreemapTiles.Provider value={context}>
1112
+ <ChartContainer
1113
+ role="figure"
1114
+ {...rest}
1115
+ // Read through `aria-describedby`, after a caller's own ids; `hidden`, inside the
1116
+ // figure (0.52 code review, deep, charts, finding 7).
1117
+ description={description}
1118
+ ref={rootRef}
1119
+ config={TREEMAP_CONFIG}
1120
+ className={cn(box.className, rest.className)}
1121
+ style={{ ...style, ...box.style }}
537
1122
  >
538
- <ChartTooltip content={<Content hideLabel valueFormatter={format} />} />
539
- </RechartsTreemap>
540
- </ChartContainer>
1123
+ {/* No chart-level `stroke`: recharts spreads it onto the <svg> root, from where
1124
+ it inherits into every label (see TreemapCell). Each cell strokes its own rect. */}
1125
+ <RechartsTreemap data={nodes} dataKey="size" fill="var(--chart-1)" isAnimationActive={false} content={cell}>
1126
+ {tooltip}
1127
+ </RechartsTreemap>
1128
+ </ChartContainer>
1129
+ </TreemapTiles.Provider>
541
1130
  );
542
1131
  }