basalt-ui 1.30.2 → 1.32.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 (357) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +54 -0
  3. package/MIGRATING.md +256 -12
  4. package/agent/rules/basalt-controls.md +19 -19
  5. package/agent/rules/basalt-mantine.md +5 -5
  6. package/configs/oxlint-plugin.js +371 -4
  7. package/configs/oxlint-plugin.test.ts +327 -2
  8. package/configs/oxlint.json +2 -1
  9. package/dist/agent-chat/composer.d.ts.map +1 -1
  10. package/dist/agent-chat/composer.js +41 -43
  11. package/dist/agent-chat/thread-workspace.d.ts +2 -2
  12. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  13. package/dist/agent-chat/thread-workspace.js +3 -4
  14. package/dist/agent-chat/use-keyboard-inset.d.ts +2 -0
  15. package/dist/agent-chat/use-keyboard-inset.d.ts.map +1 -0
  16. package/dist/agent-chat/use-keyboard-inset.js +34 -0
  17. package/dist/charts/cursor/touch-pin.d.ts +45 -0
  18. package/dist/charts/cursor/touch-pin.d.ts.map +1 -0
  19. package/dist/charts/cursor/touch-pin.js +79 -0
  20. package/dist/charts/hooks/use-discrete-cursor.d.ts +66 -0
  21. package/dist/charts/hooks/use-discrete-cursor.d.ts.map +1 -0
  22. package/dist/charts/hooks/use-discrete-cursor.js +198 -0
  23. package/dist/charts/hooks/use-hysteresis.d.ts +18 -0
  24. package/dist/charts/hooks/use-hysteresis.d.ts.map +1 -0
  25. package/dist/charts/hooks/use-hysteresis.js +12 -0
  26. package/dist/charts/hooks/useBandPlot.d.ts +7 -0
  27. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
  28. package/dist/charts/hooks/useBandPlot.js +62 -18
  29. package/dist/charts/hooks/useChartCursor.d.ts +40 -11
  30. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  31. package/dist/charts/hooks/useChartCursor.js +102 -11
  32. package/dist/charts/index.d.ts +0 -1
  33. package/dist/charts/index.d.ts.map +1 -1
  34. package/dist/charts/index.js +0 -10
  35. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  36. package/dist/charts/kinds/BandStrip.js +47 -82
  37. package/dist/charts/kinds/Donut.d.ts +9 -3
  38. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  39. package/dist/charts/kinds/Donut.js +110 -48
  40. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  41. package/dist/charts/kinds/DualPanel.js +138 -145
  42. package/dist/charts/kinds/Heatmap.d.ts +7 -0
  43. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  44. package/dist/charts/kinds/Heatmap.js +37 -16
  45. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  46. package/dist/charts/kinds/MirroredBars.js +68 -111
  47. package/dist/charts/kinds/band-chrome.d.ts +31 -0
  48. package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
  49. package/dist/charts/kinds/band-chrome.js +71 -0
  50. package/dist/charts/kinds/donut-layout.d.ts +47 -0
  51. package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
  52. package/dist/charts/kinds/donut-layout.js +52 -0
  53. package/dist/charts/layout/auto-margin.d.ts +7 -2
  54. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  55. package/dist/charts/layout/auto-margin.js +12 -3
  56. package/dist/charts/layout/end-labels.d.ts +64 -0
  57. package/dist/charts/layout/end-labels.d.ts.map +1 -0
  58. package/dist/charts/layout/end-labels.js +76 -0
  59. package/dist/charts/primitives/Axes.d.ts +32 -4
  60. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  61. package/dist/charts/primitives/Axes.js +62 -16
  62. package/dist/charts/primitives/CartesianChart.d.ts +7 -6
  63. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  64. package/dist/charts/primitives/CartesianChart.js +264 -145
  65. package/dist/charts/primitives/ChartCard.d.ts +0 -14
  66. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartCard.js +56 -21
  68. package/dist/charts/primitives/ChartFrame.d.ts +28 -22
  69. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  70. package/dist/charts/primitives/ChartFrame.js +116 -34
  71. package/dist/charts/primitives/ChartLegend.d.ts +19 -15
  72. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  73. package/dist/charts/primitives/ChartLegend.js +86 -73
  74. package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
  75. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  76. package/dist/charts/primitives/ChartTooltip.js +20 -4
  77. package/dist/charts/primitives/Crosshair.js +2 -2
  78. package/dist/charts/primitives/HoverOverlay.d.ts +29 -9
  79. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  80. package/dist/charts/primitives/HoverOverlay.js +54 -27
  81. package/dist/charts/primitives/HoverOverlay.module.css +13 -0
  82. package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  83. package/dist/charts/primitives/chart-card-context.d.ts +33 -0
  84. package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
  85. package/dist/charts/primitives/chart-card-context.js +18 -0
  86. package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
  87. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  88. package/dist/charts/primitives/chart-frame-layout.js +75 -34
  89. package/dist/charts/primitives/chart-layout.d.ts +112 -0
  90. package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
  91. package/dist/charts/primitives/chart-layout.js +175 -0
  92. package/dist/charts/primitives/chart-tier.d.ts +13 -11
  93. package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
  94. package/dist/charts/primitives/chart-tier.js +42 -13
  95. package/dist/charts/primitives/legend-disclosure.d.ts +27 -0
  96. package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
  97. package/dist/charts/primitives/legend-disclosure.js +153 -0
  98. package/dist/charts/series.d.ts +4 -1
  99. package/dist/charts/series.d.ts.map +1 -1
  100. package/dist/charts/utils/ticks.d.ts +27 -8
  101. package/dist/charts/utils/ticks.d.ts.map +1 -1
  102. package/dist/charts/utils/ticks.js +35 -11
  103. package/dist/common/errors.d.ts +11 -1
  104. package/dist/common/errors.d.ts.map +1 -1
  105. package/dist/common/errors.js +16 -2
  106. package/dist/common/hit-floor.d.ts +10 -0
  107. package/dist/common/hit-floor.d.ts.map +1 -0
  108. package/dist/common/hit-floor.js +6 -0
  109. package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
  110. package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
  111. package/dist/common/use-measured-widths.d.ts +37 -0
  112. package/dist/common/use-measured-widths.d.ts.map +1 -0
  113. package/dist/common/use-measured-widths.js +51 -0
  114. package/dist/common/use-media-query.d.ts +6 -0
  115. package/dist/common/use-media-query.d.ts.map +1 -0
  116. package/dist/common/use-media-query.js +40 -0
  117. package/dist/common/validate.d.ts +9 -0
  118. package/dist/common/validate.d.ts.map +1 -1
  119. package/dist/common/validate.js +9 -4
  120. package/dist/content/article-layout.d.ts +4 -2
  121. package/dist/content/article-layout.d.ts.map +1 -1
  122. package/dist/content/article-layout.js +43 -3
  123. package/dist/content/article-layout.module.css +45 -4
  124. package/dist/content/article-layout.module.css.d.ts +3 -0
  125. package/dist/content/code-block.d.ts.map +1 -1
  126. package/dist/content/code-block.js +40 -2
  127. package/dist/content/code-block.module.css +32 -2
  128. package/dist/content/prose.module.css +13 -8
  129. package/dist/controls/actions.d.ts +29 -6
  130. package/dist/controls/actions.d.ts.map +1 -1
  131. package/dist/controls/actions.js +158 -27
  132. package/dist/controls/actions.module.css +14 -1
  133. package/dist/controls/actions.module.css.d.ts +1 -0
  134. package/dist/controls/controls.module.css +13 -31
  135. package/dist/controls/enum-filter.js +2 -2
  136. package/dist/controls/filter-context.d.ts +10 -0
  137. package/dist/controls/filter-context.d.ts.map +1 -1
  138. package/dist/controls/filter-context.js +2 -0
  139. package/dist/controls/filter-pill.d.ts.map +1 -1
  140. package/dist/controls/filter-pill.js +1 -0
  141. package/dist/controls/index.d.ts +1 -1
  142. package/dist/controls/index.d.ts.map +1 -1
  143. package/dist/controls/index.js +0 -2
  144. package/dist/controls/multi-select-filter.js +2 -2
  145. package/dist/controls/sync-button.module.css +2 -1
  146. package/dist/dashboard/section.d.ts.map +1 -1
  147. package/dist/dashboard/section.js +1 -0
  148. package/dist/dashboard/section.module.css +1 -26
  149. package/dist/dashboard/stat-card.d.ts +1 -1
  150. package/dist/dashboard/stat-card.d.ts.map +1 -1
  151. package/dist/dashboard/stat-card.js +4 -1
  152. package/dist/dashboard/stat-card.module.css +6 -9
  153. package/dist/dashboard/stat-group.module.css +33 -66
  154. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  155. package/dist/dashboard/widget-grid.module.css +11 -38
  156. package/dist/dashboard/widget-header.d.ts +14 -4
  157. package/dist/dashboard/widget-header.d.ts.map +1 -1
  158. package/dist/dashboard/widget-header.js +23 -6
  159. package/dist/dashboard/widget-header.module.css +145 -133
  160. package/dist/dashboard/widget-header.module.css.d.ts +2 -0
  161. package/dist/data/data-table-state.d.ts +83 -0
  162. package/dist/data/data-table-state.d.ts.map +1 -0
  163. package/dist/data/data-table-state.js +139 -0
  164. package/dist/data/data-table.d.ts +56 -2
  165. package/dist/data/data-table.d.ts.map +1 -1
  166. package/dist/data/data-table.js +436 -336
  167. package/dist/data/data-table.module.css +94 -0
  168. package/dist/data/data-table.module.css.d.ts +11 -0
  169. package/dist/data/virtual-list.d.ts.map +1 -1
  170. package/dist/data/virtual-list.js +10 -0
  171. package/dist/forms/form-layout.d.ts.map +1 -1
  172. package/dist/forms/form-layout.js +3 -3
  173. package/dist/forms/form-layout.module.css +20 -5
  174. package/dist/forms/form-layout.module.css.d.ts +1 -0
  175. package/dist/guard/index.d.ts +8 -2
  176. package/dist/guard/index.d.ts.map +1 -1
  177. package/dist/guard/index.js +68 -2
  178. package/dist/guard/types.d.ts +3 -3
  179. package/dist/guard/types.d.ts.map +1 -1
  180. package/dist/index.d.ts +1 -1
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +3 -1
  183. package/dist/notifications/center.d.ts.map +1 -1
  184. package/dist/provider/index.d.ts +20 -1
  185. package/dist/provider/index.d.ts.map +1 -1
  186. package/dist/provider/index.js +30 -8
  187. package/dist/shell/app-brand.d.ts.map +1 -1
  188. package/dist/shell/app-brand.js +1 -0
  189. package/dist/shell/app-brand.module.css +2 -2
  190. package/dist/shell/app-header.module.css +18 -8
  191. package/dist/shell/app-main.module.css +54 -3
  192. package/dist/shell/app-main.module.css.d.ts +1 -0
  193. package/dist/shell/app-mobile-nav.module.css +16 -4
  194. package/dist/shell/app-sidebar.module.css +4 -6
  195. package/dist/shell/aside-docking.d.ts +31 -0
  196. package/dist/shell/aside-docking.d.ts.map +1 -0
  197. package/dist/shell/aside-docking.js +19 -0
  198. package/dist/shell/index.d.ts +1 -0
  199. package/dist/shell/index.d.ts.map +1 -1
  200. package/dist/shell/index.js +56 -16
  201. package/dist/shell/mobile-nav-model.d.ts +1 -1
  202. package/dist/shell/page-aside.d.ts +22 -3
  203. package/dist/shell/page-aside.d.ts.map +1 -1
  204. package/dist/shell/page-aside.js +27 -13
  205. package/dist/shell/page-bar.d.ts.map +1 -1
  206. package/dist/shell/page-bar.js +10 -5
  207. package/dist/shell/page-bar.module.css +10 -34
  208. package/dist/shell/use-breakpoint.d.ts +3 -4
  209. package/dist/shell/use-breakpoint.d.ts.map +1 -1
  210. package/dist/shell/use-breakpoint.js +3 -25
  211. package/dist/shell/use-size-class.d.ts +6 -0
  212. package/dist/shell/use-size-class.d.ts.map +1 -0
  213. package/dist/shell/use-size-class.js +19 -0
  214. package/dist/state/persisted-or-local.d.ts +4 -1
  215. package/dist/state/persisted-or-local.d.ts.map +1 -1
  216. package/dist/state/persisted-or-local.js +4 -3
  217. package/dist/state/persisted.d.ts +10 -0
  218. package/dist/state/persisted.d.ts.map +1 -1
  219. package/dist/state/persisted.js +44 -14
  220. package/dist/styles.css +41 -0
  221. package/dist/surfaces.d.ts +4 -4
  222. package/dist/surfaces.d.ts.map +1 -1
  223. package/dist/surfaces.js +11 -3
  224. package/dist/theme/controls.module.css +7 -0
  225. package/dist/theme/ctl-theme.d.ts +1 -0
  226. package/dist/theme/ctl-theme.d.ts.map +1 -1
  227. package/dist/theme/ctl-theme.js +12 -4
  228. package/dist/theme/floating.module.css +37 -0
  229. package/dist/theme/floating.module.css.d.ts +3 -0
  230. package/dist/theme/index.d.ts +2 -2
  231. package/dist/theme/index.d.ts.map +1 -1
  232. package/dist/theme/index.js +34 -5
  233. package/dist/theme/nav-link.module.css +7 -0
  234. package/dist/theme-lab/derive-controls.d.ts.map +1 -1
  235. package/dist/tokens/index.d.ts +7 -16
  236. package/dist/tokens/index.d.ts.map +1 -1
  237. package/dist/tokens/index.js +23 -28
  238. package/dist/tokens/palette.d.ts +5 -27
  239. package/dist/tokens/palette.d.ts.map +1 -1
  240. package/dist/tokens/palette.js +8 -30
  241. package/dist/tokens/size-classes.d.ts +40 -0
  242. package/dist/tokens/size-classes.d.ts.map +1 -0
  243. package/dist/tokens/size-classes.js +29 -0
  244. package/dist/tokens.css +8 -4
  245. package/llms.txt +3 -3
  246. package/package.json +18 -17
  247. package/src/agent-chat/composer.tsx +74 -47
  248. package/src/agent-chat/thread-workspace.tsx +8 -11
  249. package/src/agent-chat/use-keyboard-inset.ts +68 -0
  250. package/src/charts/cursor/touch-pin.ts +138 -0
  251. package/src/charts/hooks/use-discrete-cursor.ts +335 -0
  252. package/src/charts/hooks/use-hysteresis.ts +26 -0
  253. package/src/charts/hooks/useBandPlot.ts +90 -27
  254. package/src/charts/hooks/useChartCursor.ts +211 -26
  255. package/src/charts/index.ts +0 -8
  256. package/src/charts/kinds/BandStrip.tsx +25 -61
  257. package/src/charts/kinds/Donut.tsx +153 -62
  258. package/src/charts/kinds/DualPanel.tsx +21 -26
  259. package/src/charts/kinds/Heatmap.tsx +61 -22
  260. package/src/charts/kinds/MirroredBars.tsx +23 -65
  261. package/src/charts/kinds/band-chrome.tsx +91 -0
  262. package/src/charts/kinds/donut-layout.ts +82 -0
  263. package/src/charts/layout/auto-margin.ts +24 -5
  264. package/src/charts/layout/end-labels.ts +135 -0
  265. package/src/charts/primitives/Axes.tsx +112 -21
  266. package/src/charts/primitives/CartesianChart.tsx +282 -111
  267. package/src/charts/primitives/ChartCard.tsx +119 -48
  268. package/src/charts/primitives/ChartFrame.tsx +204 -60
  269. package/src/charts/primitives/ChartLegend.tsx +115 -78
  270. package/src/charts/primitives/ChartTooltip.tsx +24 -3
  271. package/src/charts/primitives/Crosshair.tsx +2 -2
  272. package/src/charts/primitives/HoverOverlay.module.css +13 -0
  273. package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  274. package/src/charts/primitives/HoverOverlay.tsx +62 -20
  275. package/src/charts/primitives/chart-card-context.ts +46 -0
  276. package/src/charts/primitives/chart-frame-layout.ts +207 -182
  277. package/src/charts/primitives/chart-layout.ts +318 -0
  278. package/src/charts/primitives/chart-tier.tsx +66 -21
  279. package/src/charts/primitives/legend-disclosure.tsx +177 -0
  280. package/src/charts/series.ts +4 -1
  281. package/src/charts/utils/ticks.ts +95 -19
  282. package/src/common/errors.ts +25 -1
  283. package/src/common/hit-floor.ts +9 -0
  284. package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
  285. package/src/common/use-measured-widths.ts +88 -0
  286. package/src/common/use-media-query.ts +56 -0
  287. package/src/common/validate.ts +17 -4
  288. package/src/content/article-layout.module.css +45 -4
  289. package/src/content/article-layout.module.css.d.ts +3 -0
  290. package/src/content/article-layout.tsx +60 -5
  291. package/src/content/code-block.module.css +32 -2
  292. package/src/content/code-block.tsx +47 -3
  293. package/src/content/prose.module.css +13 -8
  294. package/src/controls/actions.module.css +14 -1
  295. package/src/controls/actions.module.css.d.ts +1 -0
  296. package/src/controls/actions.tsx +223 -75
  297. package/src/controls/controls.module.css +13 -31
  298. package/src/controls/enum-filter.tsx +2 -2
  299. package/src/controls/filter-context.tsx +11 -0
  300. package/src/controls/filter-pill.tsx +1 -0
  301. package/src/controls/index.ts +0 -1
  302. package/src/controls/multi-select-filter.tsx +2 -2
  303. package/src/controls/sync-button.module.css +2 -1
  304. package/src/dashboard/section.module.css +1 -26
  305. package/src/dashboard/section.tsx +1 -0
  306. package/src/dashboard/stat-card.module.css +6 -9
  307. package/src/dashboard/stat-card.tsx +6 -3
  308. package/src/dashboard/stat-group.module.css +33 -66
  309. package/src/dashboard/widget-grid.module.css +11 -38
  310. package/src/dashboard/widget-grid.tsx +7 -6
  311. package/src/dashboard/widget-header.module.css +145 -133
  312. package/src/dashboard/widget-header.module.css.d.ts +2 -0
  313. package/src/dashboard/widget-header.tsx +45 -11
  314. package/src/data/data-table-state.ts +218 -0
  315. package/src/data/data-table.module.css +94 -0
  316. package/src/data/data-table.module.css.d.ts +11 -0
  317. package/src/data/data-table.tsx +547 -299
  318. package/src/data/virtual-list.tsx +31 -0
  319. package/src/forms/form-layout.module.css +20 -5
  320. package/src/forms/form-layout.module.css.d.ts +1 -0
  321. package/src/forms/form-layout.tsx +12 -10
  322. package/src/guard/index.ts +188 -3
  323. package/src/guard/types.ts +3 -2
  324. package/src/index.ts +2 -0
  325. package/src/notifications/center.tsx +3 -0
  326. package/src/provider/index.tsx +60 -7
  327. package/src/shell/app-brand.module.css +2 -2
  328. package/src/shell/app-brand.tsx +1 -0
  329. package/src/shell/app-header.module.css +18 -8
  330. package/src/shell/app-main.module.css +54 -3
  331. package/src/shell/app-main.module.css.d.ts +1 -0
  332. package/src/shell/app-mobile-nav.module.css +16 -4
  333. package/src/shell/app-sidebar.module.css +4 -6
  334. package/src/shell/aside-docking.ts +54 -0
  335. package/src/shell/index.tsx +178 -129
  336. package/src/shell/mobile-nav-model.ts +1 -1
  337. package/src/shell/page-aside.tsx +70 -21
  338. package/src/shell/page-bar.module.css +10 -34
  339. package/src/shell/page-bar.tsx +10 -5
  340. package/src/shell/use-breakpoint.ts +9 -45
  341. package/src/shell/use-size-class.ts +31 -0
  342. package/src/state/persisted-or-local.ts +10 -4
  343. package/src/state/persisted.ts +76 -17
  344. package/src/styles.css +41 -0
  345. package/src/surfaces.ts +9 -1
  346. package/src/theme/controls.module.css +7 -0
  347. package/src/theme/ctl-theme.tsx +15 -3
  348. package/src/theme/floating.module.css +37 -0
  349. package/src/theme/floating.module.css.d.ts +3 -0
  350. package/src/theme/index.ts +46 -8
  351. package/src/theme/nav-link.module.css +7 -0
  352. package/src/theme-lab/derive-controls.tsx +2 -0
  353. package/src/tokens/index.ts +20 -30
  354. package/src/tokens/palette.ts +6 -28
  355. package/src/tokens/size-classes.ts +46 -0
  356. package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
  357. /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
@@ -8,21 +8,15 @@ import type { ChartMargin } from '../../tokens'
8
8
  import type { CursorResolution } from '../cursor/resolve'
9
9
  import { useBandPlot } from '../hooks/useBandPlot'
10
10
  import type { BandFold, BandTooltipConfig } from '../hooks/useBandPlot'
11
- import { AxisBottomDate } from '../primitives/Axes'
12
11
  import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
13
12
  import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
14
- import {
15
- ChartTooltipFloat,
16
- TooltipBody,
17
- TooltipHeader,
18
- TooltipRow,
19
- } from '../primitives/ChartTooltip'
13
+ import { TooltipRow } from '../primitives/ChartTooltip'
20
14
  import { Crosshair } from '../primitives/Crosshair'
21
15
  import { HatchPattern, hatchFill, hatchSizeFor } from '../primitives/HatchPattern'
22
- import { HoverOverlay } from '../primitives/HoverOverlay'
23
16
  import type { ChartState } from '../primitives/ChartPending'
24
17
  import type { ChartLegendConfig, SeriesStyle } from '../series'
25
18
  import { fmtAxisDate } from '../utils/format'
19
+ import { BandAxisOverlay, BandTooltip, clampFraction } from './band-chrome'
26
20
  import { isDev } from '../../common/is-dev'
27
21
 
28
22
  /**
@@ -256,7 +250,7 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
256
250
  ...(tooltip !== undefined && { tooltip }),
257
251
  })
258
252
 
259
- const { bands, margin, plotWidth, scale, cursor, point, crosshairX, step, bandWidth } = band
253
+ const { bands, margin, plotWidth, point, crosshairX, step, bandWidth } = band
260
254
  const stripHeight = Math.max(plot.height - margin.top - margin.bottom, 1)
261
255
  const hatchId = `${chartId}-band-absent`
262
256
  if (absentState !== undefined && !styleByKey.has(absentState)) {
@@ -351,13 +345,17 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
351
345
 
352
346
  if (plotWidth <= 0 || bands.length === 0) return null
353
347
 
354
- const cfg = tooltip === false ? undefined : tooltip
355
- const badge = cfg?.label === undefined || point === null ? null : cfg.label(point)
356
348
  const row = point === null ? null : deriveBandRow(getBand(point), styleByKey, hidden, point)
357
349
 
358
350
  return (
359
351
  <>
360
- <svg ref={band.svgRef} width={plot.width} height={plot.height}>
352
+ <svg
353
+ ref={band.svgRef}
354
+ width={plot.width}
355
+ height={plot.height}
356
+ // `touch-action` on the plot `<svg>`: Chrome ignores it on the overlay `<rect>` (`R2C-2`).
357
+ style={{ touchAction: 'pan-y' }}
358
+ >
361
359
  <defs>
362
360
  <HatchPattern
363
361
  id={hatchId}
@@ -377,65 +375,31 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
377
375
 
378
376
  {crosshairX !== null && <Crosshair x={crosshairX} top={0} bottom={stripHeight} />}
379
377
 
380
- <AxisBottomDate
381
- top={stripHeight}
382
- scale={scale}
383
- tickValues={band.tickValues}
384
- tickFormat={(v) => formatX(String(v))}
385
- />
386
-
387
- <HoverOverlay
388
- width={plotWidth}
378
+ <BandAxisOverlay
379
+ band={band}
389
380
  height={stripHeight}
390
- onMove={cursor.onPointerMove}
391
- onLeave={cursor.onPointerLeave}
392
- onKeyDown={cursor.onKeyDown}
393
- onBlur={cursor.onBlur}
394
- valueMax={Math.max(bands.length - 1, 0)}
395
- {...(point !== null && {
396
- valueNow: bands.indexOf(point),
397
- valueText: formatX(getX(point)),
398
- })}
399
- {...(ariaLabel !== undefined && { ariaLabel })}
381
+ getX={getX}
382
+ formatX={formatX}
383
+ ariaLabel={ariaLabel}
400
384
  />
401
385
  </Group>
402
386
  </svg>
403
387
 
404
- {band.showTooltip && point !== null && (
405
- <ChartTooltipFloat anchor={band.tooltipAnchor} ariaLive={band.ariaLive}>
406
- <TooltipHeader
407
- date={getX(point)}
408
- {...(cfg?.formatHeader !== undefined && {
409
- format: (key: string) => cfg.formatHeader?.(key, point) ?? key,
410
- })}
411
- {...(badge !== null && { label: badge.text, labelColor: badge.color })}
388
+ <BandTooltip band={band} tooltip={tooltip} getX={getX} hidden={hidden}>
389
+ {row !== null && (
390
+ <TooltipRow
391
+ color={row.color}
392
+ label={row.label}
393
+ value={row.value}
394
+ shape={row.shape}
395
+ dashed={row.dashed}
412
396
  />
413
- <TooltipBody>
414
- {cfg?.prependRows?.(point, { hidden })}
415
- {row !== null && (
416
- <TooltipRow
417
- color={row.color}
418
- label={row.label}
419
- value={row.value}
420
- shape={row.shape}
421
- dashed={row.dashed}
422
- />
423
- )}
424
- {cfg?.extraRows?.(point, { hidden })}
425
- </TooltipBody>
426
- </ChartTooltipFloat>
427
- )}
397
+ )}
398
+ </BandTooltip>
428
399
  </>
429
400
  )
430
401
  }
431
402
 
432
- /** A 0..1 share. Non-finite (a 0/0 fold count) reads as "nothing is absent" — the conservative
433
- * end, since the alternative is a band that does not render at all. */
434
- function clampFraction(value: number | undefined): number {
435
- if (value === undefined || !Number.isFinite(value)) return 0
436
- return Math.min(Math.max(value, 0), 1)
437
- }
438
-
439
403
  /**
440
404
  * The strip's one derived row: the hovered band's STATE, named and formatted by its own `series`
441
405
  * entry. A strip cannot show a row for a state it does not draw, and cannot name a state the
@@ -1,7 +1,10 @@
1
1
  import { Group } from '@visx/group'
2
2
  import { Pie } from '@visx/shape'
3
- import { memo, useMemo, useState } from 'react'
4
- import type { PointerEvent, ReactNode } from 'react'
3
+ import { memo, useMemo } from 'react'
4
+ import type { ReactNode } from 'react'
5
+ import { useChartSize } from '../hooks/useChartSize'
6
+ import { useDiscreteCursor } from '../hooks/use-discrete-cursor'
7
+ import { useHysteresis } from '../hooks/use-hysteresis'
5
8
  import { assertRequiredProps } from '../../common/validate'
6
9
  import type { BasaltProps } from '../../common/props'
7
10
  import { ChartTooltipFloat, TooltipBody, TooltipRow } from '../primitives/ChartTooltip'
@@ -9,6 +12,15 @@ import { ChartFrame } from '../primitives/ChartFrame'
9
12
  import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
10
13
  import type { ChartState } from '../primitives/ChartPending'
11
14
  import { VX } from '../../tokens'
15
+ import { resolveContainerClass } from '../../tokens/size-classes'
16
+ import { resolveFrameHeight } from '../primitives/chart-frame-layout'
17
+ import {
18
+ foldOther,
19
+ resolveDonutLayout,
20
+ resolveDonutRadius,
21
+ resolveOtherKey,
22
+ sharePercent,
23
+ } from './donut-layout'
12
24
  import type { SeriesStyle } from '../series'
13
25
  import type { SeriesKey } from '../../register'
14
26
 
@@ -64,14 +76,22 @@ export type DonutProps<K extends string = SeriesKey> = BasaltProps & {
64
76
  state?: ChartState
65
77
  }
66
78
 
79
+ const identityLabel = (key: string): string => key
80
+
67
81
  /**
68
82
  * Radial slice-share chart with a punched-out center label. Composes `ChartFrame` for a
69
83
  * categorical legend derived from the slices (one `SeriesStyle` per slice, `mark: 'bar'`) so the
70
- * legend can never drift from what's plotted — legend-hidden slices drop out of the ring, the
71
- * center total, and the tooltip's "Share" row together (`docs/CHARTS-SPEC.md` §5). No crosshair —
72
- * meaningless for a radial layout. Hover stays local to the pie (dimming siblings on hover) rather
84
+ * legend can never drift from what's plotted — legend-hidden slices drop out of the ring and the
85
+ * tooltip together, while every share is taken of ALL slices so the legend and the tooltip never
86
+ * disagree (`docs/CHARTS-SPEC.md` §5). No crosshair — meaningless for a radial layout. Hover stays local to the pie (dimming siblings on hover) rather
73
87
  * than joining the shared cursor: a date-keyed cursor has no counterpart on a donut, and cross-kind
74
88
  * category sync (donut ↔ bar, via a generalized key) is a distinct, deliberately deferred feature.
89
+ * Tap-to-pin, keyboard stepping and focus over the slice ring come from `useDiscreteCursor`
90
+ * (`docs/CHARTS-SPEC.md` §4, Tooltip), the discrete counterpart of `HoverOverlay`.
91
+ *
92
+ * Layout law (`docs/CHARTS-SPEC.md` §5, Legend): frame W/H > 1.5 puts the legend beside the ring
93
+ * with value and %, the ring capped at `min(h, 0.55w)`; `micro`/`compact` stack the full legend
94
+ * under the ring, never rolled up. 7 or more slices fold the smallest into a neutral "Other".
75
95
  */
76
96
  function DonutInner<K extends string = SeriesKey>(props: DonutProps<K>) {
77
97
  // F-ERR-1: name the component and the prop. Without this a missing accessor surfaces
@@ -82,7 +102,8 @@ function DonutInner<K extends string = SeriesKey>(props: DonutProps<K>) {
82
102
  data,
83
103
  height,
84
104
  colorForKey,
85
- seriesLabel = (k) => k,
105
+ formatValue,
106
+ seriesLabel = identityLabel,
86
107
  ariaLabel,
87
108
  isPending,
88
109
  state,
@@ -90,44 +111,118 @@ function DonutInner<K extends string = SeriesKey>(props: DonutProps<K>) {
90
111
  style,
91
112
  } = props
92
113
 
93
- const series: SeriesStyle[] = data.map((d) => ({
94
- key: d.key,
95
- label: seriesLabel(d.key),
96
- color: colorForKey(d.key),
97
- mark: 'bar',
98
- }))
114
+ // The legend side depends on the frame's own measured box, which only exists inside
115
+ // `ChartFrame` — so measure the wrapper (same width) one level up. A stated height is used as
116
+ // is; the measured one moves with the legend's placement, so an unstated height gets hysteresis.
117
+ const { ref, width: frameW, height: measuredH } = useChartSize()
118
+ const containerClass = resolveContainerClass(frameW)
119
+ const frameH = height === undefined ? measuredH : resolveFrameHeight(height, containerClass)
120
+ const side = useHysteresis(
121
+ (wasSide) =>
122
+ resolveDonutLayout({
123
+ frameW,
124
+ frameH,
125
+ containerClass,
126
+ wasSide: height === undefined && wasSide,
127
+ }).side,
128
+ false,
129
+ )
130
+
131
+ const slices = useMemo(() => {
132
+ const { kept, other } = foldOther(data)
133
+ const out: Slice[] = kept.map((d) => ({
134
+ key: d.key,
135
+ label: seriesLabel(d.key),
136
+ color: colorForKey(d.key),
137
+ value: d.value,
138
+ }))
139
+ if (other !== null)
140
+ out.push({
141
+ key: resolveOtherKey(kept.map((d) => d.key)),
142
+ label: 'Other',
143
+ color: VX.neutral,
144
+ value: other,
145
+ })
146
+ return out
147
+ }, [data, seriesLabel, colorForKey])
148
+
149
+ // One denominator for the side legend and the tooltip: every slice, hidden or not.
150
+ const total = useMemo(() => slices.reduce((sum, d) => sum + d.value, 0), [slices])
151
+ const series = useMemo<SeriesStyle[]>(
152
+ () =>
153
+ slices.map((d) => ({
154
+ key: d.key,
155
+ label: side
156
+ ? `${d.label} · ${formatValue(d.value)} · ${sharePercent(d.value, total)}%`
157
+ : d.label,
158
+ color: d.color,
159
+ mark: 'bar',
160
+ })),
161
+ [slices, side, formatValue, total],
162
+ )
99
163
 
100
164
  return (
101
- <ChartFrame
102
- series={series}
103
- {...(height !== undefined && { height })}
104
- {...(ariaLabel !== undefined && { ariaLabel })}
105
- {...(isPending !== undefined && { isPending })}
106
- {...(state !== undefined && { state })}
165
+ <div
166
+ ref={ref}
107
167
  {...(className !== undefined && { className })}
108
- {...(style !== undefined && { style })}
168
+ style={{ display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: 0, ...style }}
109
169
  >
110
- {(plot) => <DonutPlot {...props} plot={plot} />}
111
- </ChartFrame>
170
+ <ChartFrame
171
+ series={series}
172
+ // Never roll the legend up: the full list is the donut's only key. Side legends read as a
173
+ // column. `headerSlot: false` (P1-4) keeps it under/beside the ring even inside a
174
+ // `ChartCard` with a header slot free — a Donut's legend IS its key, so it must never
175
+ // shrink to unlabelled header dots.
176
+ legend={{ placement: side ? 'right' : 'bottom', maxRows: series.length, headerSlot: false }}
177
+ {...(height !== undefined && { height })}
178
+ {...(ariaLabel !== undefined && { ariaLabel })}
179
+ {...(isPending !== undefined && { isPending })}
180
+ {...(state !== undefined && { state })}
181
+ >
182
+ {(plot) => (
183
+ <DonutPlot
184
+ slices={slices}
185
+ total={total}
186
+ side={side}
187
+ frameW={frameW}
188
+ plot={plot}
189
+ formatValue={formatValue}
190
+ {...(props.centerLabel !== undefined && { centerLabel: props.centerLabel })}
191
+ {...(props.centerSubLabel !== undefined && { centerSubLabel: props.centerSubLabel })}
192
+ {...(props.centerContent !== undefined && { centerContent: props.centerContent })}
193
+ {...(props.innerRatio !== undefined && { innerRatio: props.innerRatio })}
194
+ {...(props.padAngle !== undefined && { padAngle: props.padAngle })}
195
+ />
196
+ )}
197
+ </ChartFrame>
198
+ </div>
112
199
  )
113
200
  }
114
201
 
115
- type DonutPlotProps<K extends string = SeriesKey> = DonutProps<K> & {
202
+ type Slice = { key: string; label: string; color: string; value: number }
203
+
204
+ type DonutPlotProps = Pick<
205
+ DonutProps,
206
+ 'formatValue' | 'centerLabel' | 'centerSubLabel' | 'centerContent' | 'innerRatio' | 'padAngle'
207
+ > & {
116
208
  plot: { width: number; height: number; hidden: ReadonlySet<string> }
209
+ slices: Slice[]
210
+ /** Sum of ALL slices (hidden included) — the one denominator for every share. */
211
+ total: number
212
+ side: boolean
213
+ frameW: number
117
214
  }
118
215
 
119
- /** A hovered slice plus the viewport anchor `ChartTooltipFloat` positions against. */
120
- type DonutTip<K extends string> = { key: K; value: number; anchor: { x: number; y: number } }
121
-
122
216
  /** The measured plot — split from {@link DonutInner} so it only draws once `ChartFrame` has
123
217
  * resolved a non-empty plot rect (radius/center depend on the measured size). */
124
- function DonutPlot<K extends string = SeriesKey>(props: DonutPlotProps<K>) {
218
+ function DonutPlot(props: DonutPlotProps) {
125
219
  const {
126
- data,
220
+ slices,
221
+ total,
127
222
  plot,
128
- colorForKey,
223
+ side,
224
+ frameW,
129
225
  formatValue,
130
- seriesLabel = (k) => k,
131
226
  centerLabel,
132
227
  centerSubLabel,
133
228
  centerContent,
@@ -136,28 +231,28 @@ function DonutPlot<K extends string = SeriesKey>(props: DonutPlotProps<K>) {
136
231
  } = props
137
232
  const { width, height, hidden } = plot
138
233
 
139
- const [tip, setTip] = useState<DonutTip<K> | null>(null)
140
- const hoveredKey = tip?.key ?? null
234
+ const visibleData = useMemo(() => slices.filter((d) => !hidden.has(d.key)), [slices, hidden])
141
235
 
142
- const visibleData = useMemo(() => data.filter((d) => !hidden.has(d.key)), [data, hidden])
236
+ // One ring, one shared keyboard host: `useDiscreteCursor` gives arrow-key stepping through the
237
+ // VISIBLE slices (a hidden legend entry is not a reachable target either), plus tap-to-pin on
238
+ // touch. Left/Right and Up/Down are equivalent (no `columns` — a ring has no 2D shape).
239
+ const cursor = useDiscreteCursor<Slice>({
240
+ targets: visibleData,
241
+ getKey: (d) => d.key,
242
+ ariaLabel: 'Donut slices — use arrow keys to select, Escape to dismiss',
243
+ })
244
+ const activeKey = cursor.tip?.target.key ?? null
143
245
 
144
- const radius = Math.min(width, height) / 2 - 4
246
+ const radius = resolveDonutRadius({ plotW: width, plotH: height, frameW, side })
145
247
  const innerRadius = radius * innerRatio
146
248
  const centerX = width / 2
147
249
  const centerY = height / 2
148
250
 
149
- const total = useMemo(() => visibleData.reduce((sum, d) => sum + d.value, 0), [visibleData])
150
-
151
- const show = (d: DonutDatum<K>, event: PointerEvent<SVGGElement>) => {
152
- setTip({ key: d.key, value: d.value, anchor: { x: event.clientX, y: event.clientY } })
153
- }
154
- const hide = () => setTip(null)
155
-
156
251
  return (
157
252
  <div style={{ position: 'relative' }}>
158
- <svg width={width} height={height}>
253
+ <svg width={width} height={height} {...cursor.hostProps}>
159
254
  <Group left={centerX} top={centerY}>
160
- <Pie<DonutDatum<K>>
255
+ <Pie<Slice>
161
256
  data={visibleData}
162
257
  pieValue={(d) => d.value}
163
258
  pieSortValues={() => 0}
@@ -172,26 +267,22 @@ function DonutPlot<K extends string = SeriesKey>(props: DonutPlotProps<K>) {
172
267
  return (
173
268
  <g
174
269
  key={key}
175
- onPointerEnter={(event) => {
176
- show(arc.data, event)
177
- }}
178
- onPointerMove={(event) => {
179
- show(arc.data, event)
180
- }}
181
- onPointerLeave={() => {
182
- hide()
183
- }}
184
- onPointerCancel={() => {
185
- hide()
186
- }}
270
+ id={cursor.optionId(arc.data)}
271
+ // An SVG `<g>` has no `<option>` counterpart — `useDiscreteCursor`'s
272
+ // listbox/option pair (see its JSDoc) is a role over an arbitrary shape.
273
+ // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role
274
+ role="option"
275
+ aria-selected={activeKey === key}
276
+ aria-label={`${arc.data.label}: ${formatValue(arc.data.value)}, ${sharePercent(arc.data.value, total)}%`}
277
+ {...cursor.pointerProps(arc.data)}
187
278
  style={{ cursor: 'pointer' }}
188
279
  >
189
280
  <path
190
281
  d={pie.path(arc) || ''}
191
- fill={colorForKey(key)}
282
+ fill={arc.data.color}
192
283
  stroke={VX.surface.panel}
193
284
  strokeWidth={1.5}
194
- opacity={hoveredKey === null || hoveredKey === key ? 1 : 0.4}
285
+ opacity={activeKey === null || activeKey === key ? 1 : 0.4}
195
286
  />
196
287
  </g>
197
288
  )
@@ -244,19 +335,19 @@ function DonutPlot<K extends string = SeriesKey>(props: DonutPlotProps<K>) {
244
335
  </div>
245
336
  )}
246
337
 
247
- {tip !== null && (
248
- <ChartTooltipFloat anchor={tip.anchor}>
338
+ {cursor.tip !== null && (
339
+ <ChartTooltipFloat anchor={cursor.tip.anchor}>
249
340
  <TooltipBody>
250
341
  <TooltipRow
251
- color={colorForKey(tip.key)}
252
- label={seriesLabel(tip.key)}
253
- value={formatValue(tip.value)}
342
+ color={cursor.tip.target.color}
343
+ label={cursor.tip.target.label}
344
+ value={formatValue(cursor.tip.target.value)}
254
345
  shape="bar"
255
346
  />
256
347
  <TooltipRow
257
348
  color={VX.grid}
258
349
  label="Share"
259
- value={`${total > 0 ? Math.round((tip.value / total) * 100) : 0}%`}
350
+ value={`${sharePercent(cursor.tip.target.value, total)}%`}
260
351
  shape="bar"
261
352
  />
262
353
  </TooltipBody>
@@ -13,11 +13,12 @@ import {
13
13
  TooltipBody,
14
14
  TooltipHeader,
15
15
  TooltipRow,
16
+ TooltipRows,
16
17
  } from '../primitives/ChartTooltip'
17
18
  import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
18
19
  import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
19
20
  import { Crosshair, SeriesDot } from '../primitives/Crosshair'
20
- import { HoverOverlay } from '../primitives/HoverOverlay'
21
+ import { cursorSliderProps, HoverOverlay } from '../primitives/HoverOverlay'
21
22
  import { ZoneRects } from '../primitives/ZoneRects'
22
23
  import type { ZoneSpec } from '../primitives/ZoneRects'
23
24
  import type { ChartState } from '../primitives/ChartPending'
@@ -336,12 +337,16 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
336
337
  [bottomDomain, bottomH, useNiceBottom],
337
338
  )
338
339
 
340
+ // ONE `<svg>` wraps both panes below, so it doubles as the tap-outside boundary
341
+ // (`docs/CHARTS-SPEC.md` §4, Tooltip) for the single cursor they share.
342
+ const svgRef = useRef<SVGSVGElement>(null)
339
343
  const cursor = useChartCursor<T>({
340
344
  data,
341
345
  chartId,
342
346
  getKey: getX,
343
347
  xScale,
344
348
  marginLeft: margin.left,
349
+ boundaryRef: svgRef,
345
350
  ...(cursorResolution !== undefined && { resolution: cursorResolution }),
346
351
  })
347
352
 
@@ -477,7 +482,6 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
477
482
  [visibleSeries, lineValid, xScale, topYScale, getX],
478
483
  )
479
484
 
480
- const svgRef = useRef<SVGSVGElement>(null)
481
485
  const point = cursor.point
482
486
  const sx = point !== null ? (xScale(getX(point)) ?? 0) : 0
483
487
  const syncedBar = point !== null ? getBar(point) : null
@@ -485,10 +489,12 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
485
489
  // Same seam as `CartesianChart`: this chart renders its tooltip as the cursor SOURCE (always) or,
486
490
  // opted in via `onFollow`, as a FOLLOWER. A follower has no pointer under it, so it is always
487
491
  // positioned via the anchored (crosshair) path — `cursor.anchor` stays null on a chart nobody is
488
- // hovering, so following it here would render nothing.
492
+ // hovering, so following it here would render nothing. `DualPanel` has no `follow` prop to
493
+ // override the default, so touch (spec §5) anchors unconditionally, same as an unset `follow`
494
+ // elsewhere.
489
495
  const isFollowerRender = !cursor.isSource && onFollow === true
490
496
  const showTooltip = point !== null && (cursor.isSource || isFollowerRender)
491
- const anchorX = isFollowerRender && point !== null ? sx : null
497
+ const anchorX = (isFollowerRender || cursor.isTouch) && point !== null ? sx : null
492
498
  const svgRect = anchorX === null ? undefined : svgRef.current?.getBoundingClientRect()
493
499
  const tooltipAnchor =
494
500
  svgRect === undefined || anchorX === null
@@ -497,7 +503,13 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
497
503
 
498
504
  return (
499
505
  <>
500
- <svg ref={svgRef} width={plot.width} height={plot.height}>
506
+ <svg
507
+ ref={svgRef}
508
+ width={plot.width}
509
+ height={plot.height}
510
+ // `touch-action` on the plot `<svg>`: Chrome ignores it on the overlay `<rect>` (`R2C-2`).
511
+ style={{ touchAction: 'pan-y' }}
512
+ >
501
513
  {/* Top pane: line series + fill-between + zones + ref lines. */}
502
514
  <Group left={margin.left} top={margin.top}>
503
515
  <GridRows scale={topYScale} width={xMax} stroke={VX.grid} numTicks={4} />
@@ -584,16 +596,7 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
584
596
  <HoverOverlay
585
597
  width={xMax}
586
598
  height={topH + PANE_GAP}
587
- onMove={cursor.onPointerMove}
588
- onLeave={cursor.onPointerLeave}
589
- onKeyDown={cursor.onKeyDown}
590
- onBlur={cursor.onBlur}
591
- valueMax={Math.max(data.length - 1, 0)}
592
- {...(point !== null && {
593
- valueNow: data.indexOf(point),
594
- valueText: formatX(getX(point)),
595
- })}
596
- {...(ariaLabel !== undefined && { ariaLabel })}
599
+ {...cursorSliderProps({ cursor, data, getX, formatX, ariaLabel })}
597
600
  />
598
601
  </Group>
599
602
 
@@ -648,6 +651,8 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
648
651
  width={xMax}
649
652
  height={bottomH + margin.bottom}
650
653
  onMove={cursor.onPointerMove}
654
+ onDown={cursor.onPointerDown}
655
+ onUp={cursor.onPointerUp}
651
656
  onLeave={cursor.onPointerLeave}
652
657
  />
653
658
  </Group>
@@ -673,17 +678,7 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
673
678
  })()}
674
679
  />
675
680
  <TooltipBody>
676
- {deriveTooltipRows(visibleSeries, point, formatTop).map((row) => (
677
- <TooltipRow
678
- key={row.key}
679
- color={row.color}
680
- label={row.label}
681
- value={row.value}
682
- shape={row.shape}
683
- dashed={row.dashed}
684
- {...(row.strokeWidth !== undefined && { strokeWidth: row.strokeWidth })}
685
- />
686
- ))}
681
+ <TooltipRows rows={deriveTooltipRows(visibleSeries, point, formatTop)} />
687
682
  {barVisible &&
688
683
  (() => {
689
684
  const v = getBar(point)