basalt-ui 1.30.2 → 1.31.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 (334) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +31 -0
  3. package/MIGRATING.md +172 -12
  4. package/agent/rules/basalt-controls.md +19 -19
  5. package/agent/rules/basalt-mantine.md +1 -1
  6. package/configs/oxlint-plugin.js +254 -4
  7. package/configs/oxlint-plugin.test.ts +145 -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 +54 -41
  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 +145 -131
  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 +83 -68
  47. package/dist/charts/kinds/donut-layout.d.ts +47 -0
  48. package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
  49. package/dist/charts/kinds/donut-layout.js +52 -0
  50. package/dist/charts/layout/auto-margin.d.ts +7 -2
  51. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  52. package/dist/charts/layout/auto-margin.js +12 -3
  53. package/dist/charts/layout/end-labels.d.ts +64 -0
  54. package/dist/charts/layout/end-labels.d.ts.map +1 -0
  55. package/dist/charts/layout/end-labels.js +76 -0
  56. package/dist/charts/primitives/Axes.d.ts +32 -4
  57. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  58. package/dist/charts/primitives/Axes.js +62 -16
  59. package/dist/charts/primitives/CartesianChart.d.ts +7 -6
  60. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  61. package/dist/charts/primitives/CartesianChart.js +271 -125
  62. package/dist/charts/primitives/ChartCard.d.ts +0 -14
  63. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  64. package/dist/charts/primitives/ChartCard.js +74 -21
  65. package/dist/charts/primitives/ChartFrame.d.ts +28 -22
  66. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartFrame.js +133 -34
  68. package/dist/charts/primitives/ChartLegend.d.ts +19 -15
  69. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  70. package/dist/charts/primitives/ChartLegend.js +212 -71
  71. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  72. package/dist/charts/primitives/ChartTooltip.js +4 -3
  73. package/dist/charts/primitives/Crosshair.js +2 -2
  74. package/dist/charts/primitives/HoverOverlay.d.ts +6 -1
  75. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  76. package/dist/charts/primitives/HoverOverlay.js +37 -26
  77. package/dist/charts/primitives/HoverOverlay.module.css +13 -0
  78. package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  79. package/dist/charts/primitives/chart-card-context.d.ts +29 -0
  80. package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
  81. package/dist/charts/primitives/chart-card-context.js +13 -0
  82. package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
  83. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  84. package/dist/charts/primitives/chart-frame-layout.js +76 -29
  85. package/dist/charts/primitives/chart-layout.d.ts +112 -0
  86. package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
  87. package/dist/charts/primitives/chart-layout.js +175 -0
  88. package/dist/charts/primitives/chart-tier.d.ts +13 -11
  89. package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
  90. package/dist/charts/primitives/chart-tier.js +42 -13
  91. package/dist/charts/series.d.ts +4 -1
  92. package/dist/charts/series.d.ts.map +1 -1
  93. package/dist/charts/utils/ticks.d.ts +27 -8
  94. package/dist/charts/utils/ticks.d.ts.map +1 -1
  95. package/dist/charts/utils/ticks.js +35 -11
  96. package/dist/common/errors.d.ts +11 -1
  97. package/dist/common/errors.d.ts.map +1 -1
  98. package/dist/common/errors.js +16 -2
  99. package/dist/common/hit-floor.d.ts +10 -0
  100. package/dist/common/hit-floor.d.ts.map +1 -0
  101. package/dist/common/hit-floor.js +6 -0
  102. package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
  103. package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
  104. package/dist/common/use-measured-widths.d.ts +37 -0
  105. package/dist/common/use-measured-widths.d.ts.map +1 -0
  106. package/dist/common/use-measured-widths.js +51 -0
  107. package/dist/common/use-media-query.d.ts +6 -0
  108. package/dist/common/use-media-query.d.ts.map +1 -0
  109. package/dist/common/use-media-query.js +40 -0
  110. package/dist/common/validate.d.ts +9 -0
  111. package/dist/common/validate.d.ts.map +1 -1
  112. package/dist/common/validate.js +9 -4
  113. package/dist/content/article-layout.d.ts +4 -2
  114. package/dist/content/article-layout.d.ts.map +1 -1
  115. package/dist/content/article-layout.js +43 -3
  116. package/dist/content/article-layout.module.css +45 -4
  117. package/dist/content/article-layout.module.css.d.ts +3 -0
  118. package/dist/content/code-block.d.ts.map +1 -1
  119. package/dist/content/code-block.js +40 -2
  120. package/dist/content/code-block.module.css +32 -2
  121. package/dist/content/prose.module.css +13 -8
  122. package/dist/controls/actions.d.ts +29 -6
  123. package/dist/controls/actions.d.ts.map +1 -1
  124. package/dist/controls/actions.js +158 -27
  125. package/dist/controls/actions.module.css +14 -1
  126. package/dist/controls/actions.module.css.d.ts +1 -0
  127. package/dist/controls/controls.module.css +8 -31
  128. package/dist/controls/enum-filter.js +2 -2
  129. package/dist/controls/filter-context.d.ts +10 -0
  130. package/dist/controls/filter-context.d.ts.map +1 -1
  131. package/dist/controls/filter-context.js +2 -0
  132. package/dist/controls/filter-pill.d.ts.map +1 -1
  133. package/dist/controls/filter-pill.js +1 -0
  134. package/dist/controls/index.d.ts +1 -1
  135. package/dist/controls/index.d.ts.map +1 -1
  136. package/dist/controls/index.js +0 -2
  137. package/dist/controls/multi-select-filter.js +2 -2
  138. package/dist/controls/sync-button.module.css +1 -1
  139. package/dist/dashboard/section.d.ts.map +1 -1
  140. package/dist/dashboard/section.js +1 -0
  141. package/dist/dashboard/section.module.css +1 -26
  142. package/dist/dashboard/stat-card.d.ts +1 -1
  143. package/dist/dashboard/stat-card.d.ts.map +1 -1
  144. package/dist/dashboard/stat-card.js +4 -1
  145. package/dist/dashboard/stat-card.module.css +6 -9
  146. package/dist/dashboard/stat-group.module.css +16 -4
  147. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  148. package/dist/dashboard/widget-header.d.ts +14 -4
  149. package/dist/dashboard/widget-header.d.ts.map +1 -1
  150. package/dist/dashboard/widget-header.js +23 -6
  151. package/dist/dashboard/widget-header.module.css +145 -133
  152. package/dist/dashboard/widget-header.module.css.d.ts +2 -0
  153. package/dist/data/data-table.d.ts +23 -0
  154. package/dist/data/data-table.d.ts.map +1 -1
  155. package/dist/data/data-table.js +339 -141
  156. package/dist/data/data-table.module.css +94 -0
  157. package/dist/data/data-table.module.css.d.ts +11 -0
  158. package/dist/data/virtual-list.d.ts.map +1 -1
  159. package/dist/data/virtual-list.js +10 -0
  160. package/dist/forms/form-layout.d.ts.map +1 -1
  161. package/dist/forms/form-layout.js +3 -3
  162. package/dist/forms/form-layout.module.css +30 -5
  163. package/dist/forms/form-layout.module.css.d.ts +1 -0
  164. package/dist/guard/index.d.ts +8 -2
  165. package/dist/guard/index.d.ts.map +1 -1
  166. package/dist/guard/index.js +67 -2
  167. package/dist/guard/types.d.ts +3 -3
  168. package/dist/guard/types.d.ts.map +1 -1
  169. package/dist/index.d.ts +1 -1
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +3 -1
  172. package/dist/provider/index.d.ts +20 -1
  173. package/dist/provider/index.d.ts.map +1 -1
  174. package/dist/provider/index.js +22 -2
  175. package/dist/shell/app-brand.d.ts.map +1 -1
  176. package/dist/shell/app-brand.js +1 -0
  177. package/dist/shell/app-brand.module.css +2 -2
  178. package/dist/shell/app-header.module.css +18 -8
  179. package/dist/shell/app-main.module.css +54 -3
  180. package/dist/shell/app-main.module.css.d.ts +1 -0
  181. package/dist/shell/app-mobile-nav.module.css +16 -4
  182. package/dist/shell/app-sidebar.module.css +4 -6
  183. package/dist/shell/index.d.ts +12 -0
  184. package/dist/shell/index.d.ts.map +1 -1
  185. package/dist/shell/index.js +57 -16
  186. package/dist/shell/mobile-nav-model.d.ts +1 -1
  187. package/dist/shell/page-aside.d.ts +22 -3
  188. package/dist/shell/page-aside.d.ts.map +1 -1
  189. package/dist/shell/page-aside.js +27 -13
  190. package/dist/shell/page-bar.d.ts.map +1 -1
  191. package/dist/shell/page-bar.js +10 -5
  192. package/dist/shell/page-bar.module.css +10 -34
  193. package/dist/shell/use-breakpoint.d.ts +3 -4
  194. package/dist/shell/use-breakpoint.d.ts.map +1 -1
  195. package/dist/shell/use-breakpoint.js +3 -25
  196. package/dist/shell/use-size-class.d.ts +6 -0
  197. package/dist/shell/use-size-class.d.ts.map +1 -0
  198. package/dist/shell/use-size-class.js +19 -0
  199. package/dist/state/persisted-or-local.d.ts +4 -1
  200. package/dist/state/persisted-or-local.d.ts.map +1 -1
  201. package/dist/state/persisted-or-local.js +4 -3
  202. package/dist/state/persisted.d.ts +10 -0
  203. package/dist/state/persisted.d.ts.map +1 -1
  204. package/dist/state/persisted.js +44 -14
  205. package/dist/styles.css +41 -0
  206. package/dist/surfaces.d.ts +4 -4
  207. package/dist/surfaces.d.ts.map +1 -1
  208. package/dist/surfaces.js +11 -3
  209. package/dist/theme/controls.module.css +7 -0
  210. package/dist/theme/ctl-theme.d.ts +1 -0
  211. package/dist/theme/ctl-theme.d.ts.map +1 -1
  212. package/dist/theme/ctl-theme.js +12 -4
  213. package/dist/theme/floating.module.css +36 -0
  214. package/dist/theme/floating.module.css.d.ts +3 -0
  215. package/dist/theme/index.d.ts +2 -2
  216. package/dist/theme/index.d.ts.map +1 -1
  217. package/dist/theme/index.js +34 -5
  218. package/dist/theme/nav-link.module.css +7 -0
  219. package/dist/tokens/index.d.ts +7 -16
  220. package/dist/tokens/index.d.ts.map +1 -1
  221. package/dist/tokens/index.js +23 -28
  222. package/dist/tokens/palette.d.ts +5 -27
  223. package/dist/tokens/palette.d.ts.map +1 -1
  224. package/dist/tokens/palette.js +8 -30
  225. package/dist/tokens/size-classes.d.ts +40 -0
  226. package/dist/tokens/size-classes.d.ts.map +1 -0
  227. package/dist/tokens/size-classes.js +29 -0
  228. package/dist/tokens.css +8 -4
  229. package/llms.txt +3 -3
  230. package/package.json +17 -17
  231. package/src/agent-chat/composer.tsx +74 -47
  232. package/src/agent-chat/thread-workspace.tsx +8 -11
  233. package/src/agent-chat/use-keyboard-inset.ts +68 -0
  234. package/src/charts/cursor/touch-pin.ts +138 -0
  235. package/src/charts/hooks/use-discrete-cursor.ts +335 -0
  236. package/src/charts/hooks/use-hysteresis.ts +26 -0
  237. package/src/charts/hooks/useBandPlot.ts +90 -27
  238. package/src/charts/hooks/useChartCursor.ts +211 -26
  239. package/src/charts/index.ts +0 -8
  240. package/src/charts/kinds/BandStrip.tsx +11 -1
  241. package/src/charts/kinds/Donut.tsx +153 -62
  242. package/src/charts/kinds/DualPanel.tsx +19 -4
  243. package/src/charts/kinds/Heatmap.tsx +61 -22
  244. package/src/charts/kinds/MirroredBars.tsx +15 -3
  245. package/src/charts/kinds/donut-layout.ts +82 -0
  246. package/src/charts/layout/auto-margin.ts +24 -5
  247. package/src/charts/layout/end-labels.ts +135 -0
  248. package/src/charts/primitives/Axes.tsx +112 -21
  249. package/src/charts/primitives/CartesianChart.tsx +282 -68
  250. package/src/charts/primitives/ChartCard.tsx +141 -48
  251. package/src/charts/primitives/ChartFrame.tsx +243 -60
  252. package/src/charts/primitives/ChartLegend.tsx +257 -73
  253. package/src/charts/primitives/ChartTooltip.tsx +8 -3
  254. package/src/charts/primitives/Crosshair.tsx +2 -2
  255. package/src/charts/primitives/HoverOverlay.module.css +13 -0
  256. package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  257. package/src/charts/primitives/HoverOverlay.tsx +15 -4
  258. package/src/charts/primitives/chart-card-context.ts +37 -0
  259. package/src/charts/primitives/chart-frame-layout.ts +214 -177
  260. package/src/charts/primitives/chart-layout.ts +318 -0
  261. package/src/charts/primitives/chart-tier.tsx +66 -21
  262. package/src/charts/series.ts +4 -1
  263. package/src/charts/utils/ticks.ts +95 -19
  264. package/src/common/errors.ts +25 -1
  265. package/src/common/hit-floor.ts +9 -0
  266. package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
  267. package/src/common/use-measured-widths.ts +88 -0
  268. package/src/common/use-media-query.ts +56 -0
  269. package/src/common/validate.ts +17 -4
  270. package/src/content/article-layout.module.css +45 -4
  271. package/src/content/article-layout.module.css.d.ts +3 -0
  272. package/src/content/article-layout.tsx +60 -5
  273. package/src/content/code-block.module.css +32 -2
  274. package/src/content/code-block.tsx +47 -3
  275. package/src/content/prose.module.css +13 -8
  276. package/src/controls/actions.module.css +14 -1
  277. package/src/controls/actions.module.css.d.ts +1 -0
  278. package/src/controls/actions.tsx +223 -75
  279. package/src/controls/controls.module.css +8 -31
  280. package/src/controls/enum-filter.tsx +2 -2
  281. package/src/controls/filter-context.tsx +11 -0
  282. package/src/controls/filter-pill.tsx +1 -0
  283. package/src/controls/index.ts +0 -1
  284. package/src/controls/multi-select-filter.tsx +2 -2
  285. package/src/controls/sync-button.module.css +1 -1
  286. package/src/dashboard/section.module.css +1 -26
  287. package/src/dashboard/section.tsx +1 -0
  288. package/src/dashboard/stat-card.module.css +6 -9
  289. package/src/dashboard/stat-card.tsx +6 -3
  290. package/src/dashboard/stat-group.module.css +16 -4
  291. package/src/dashboard/widget-grid.tsx +7 -6
  292. package/src/dashboard/widget-header.module.css +145 -133
  293. package/src/dashboard/widget-header.module.css.d.ts +2 -0
  294. package/src/dashboard/widget-header.tsx +45 -11
  295. package/src/data/data-table.module.css +94 -0
  296. package/src/data/data-table.module.css.d.ts +11 -0
  297. package/src/data/data-table.tsx +503 -164
  298. package/src/data/virtual-list.tsx +31 -0
  299. package/src/forms/form-layout.module.css +30 -5
  300. package/src/forms/form-layout.module.css.d.ts +1 -0
  301. package/src/forms/form-layout.tsx +12 -10
  302. package/src/guard/index.ts +183 -3
  303. package/src/guard/types.ts +3 -2
  304. package/src/index.ts +2 -0
  305. package/src/provider/index.tsx +53 -1
  306. package/src/shell/app-brand.module.css +2 -2
  307. package/src/shell/app-brand.tsx +1 -0
  308. package/src/shell/app-header.module.css +18 -8
  309. package/src/shell/app-main.module.css +54 -3
  310. package/src/shell/app-main.module.css.d.ts +1 -0
  311. package/src/shell/app-mobile-nav.module.css +16 -4
  312. package/src/shell/app-sidebar.module.css +4 -6
  313. package/src/shell/index.tsx +200 -129
  314. package/src/shell/mobile-nav-model.ts +1 -1
  315. package/src/shell/page-aside.tsx +70 -21
  316. package/src/shell/page-bar.module.css +10 -34
  317. package/src/shell/page-bar.tsx +10 -5
  318. package/src/shell/use-breakpoint.ts +9 -45
  319. package/src/shell/use-size-class.ts +31 -0
  320. package/src/state/persisted-or-local.ts +10 -4
  321. package/src/state/persisted.ts +76 -17
  322. package/src/styles.css +41 -0
  323. package/src/surfaces.ts +9 -1
  324. package/src/theme/controls.module.css +7 -0
  325. package/src/theme/ctl-theme.tsx +15 -3
  326. package/src/theme/floating.module.css +36 -0
  327. package/src/theme/floating.module.css.d.ts +3 -0
  328. package/src/theme/index.ts +46 -8
  329. package/src/theme/nav-link.module.css +7 -0
  330. package/src/tokens/index.ts +20 -30
  331. package/src/tokens/palette.ts +6 -28
  332. package/src/tokens/size-classes.ts +46 -0
  333. package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
  334. /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
@@ -1,7 +1,10 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
3
4
  import { alpha, VX } from "../../tokens/index.js";
4
- import { useChartTierMetrics } from "./chart-tier.js";
5
+ import { useDismissOnOutside } from "../cursor/touch-pin.js";
6
+ import { DOTS_HIT_GAP, LEGEND_DOT_SIZE, LEGEND_ROW_GAP, orderEntries } from "./chart-frame-layout.js";
7
+ import { useChartMetrics, useCoarsePointer } from "./chart-tier.js";
5
8
  const LEGEND_ITEM_BASE = {
6
9
  display: "flex",
7
10
  alignItems: "center",
@@ -20,33 +23,68 @@ const LEGEND_ITEM_BUTTON = {
20
23
  color: "inherit",
21
24
  textAlign: "inherit"
22
25
  };
23
- const GROUP_ORDER = ["series", "overlay", "reference"];
24
26
  const NO_HIDDEN = /* @__PURE__ */ new Set();
25
- const roleOf = (item) => item.role ?? "series";
26
- function orderEntries(items, groups) {
27
- if (!groups) return { entries: items, dividerAfter: /* @__PURE__ */ new Set() };
28
- const dividerAfter = /* @__PURE__ */ new Set();
29
- const entries = [];
30
- const nonEmptyGroups = GROUP_ORDER.map(
31
- (role) => items.filter((item) => roleOf(item) === role)
32
- ).filter((group) => group.length > 0);
33
- nonEmptyGroups.forEach((group, i) => {
34
- entries.push(...group);
35
- if (i < nonEmptyGroups.length - 1) dividerAfter.add(entries.length - 1);
36
- });
37
- return { entries, dividerAfter };
27
+ const DISCLOSURE_PANEL = {
28
+ position: "fixed",
29
+ zIndex: VX.zIndexFloating,
30
+ display: "flex",
31
+ flexDirection: "column",
32
+ gap: LEGEND_ROW_GAP,
33
+ // theme-allow raw-scroll-container — a fixed floating panel of one legend, not app chrome
34
+ overflowY: "auto",
35
+ // `<dialog open>` brings UA chrome (centring margin, border, fit-content box) this resets.
36
+ margin: 0,
37
+ border: "none",
38
+ width: "auto",
39
+ height: "auto",
40
+ padding: 10,
41
+ boxSizing: "border-box",
42
+ backgroundColor: VX.surface.overlay,
43
+ boxShadow: VX.shadowOverlay,
44
+ color: VX.muted,
45
+ borderRadius: VX.radiusCard
46
+ };
47
+ const DISCLOSURE_MIN_HEIGHT = 96;
48
+ function disclosurePlacement(chip, coarse) {
49
+ if (coarse) {
50
+ return {
51
+ left: 0,
52
+ right: 0,
53
+ bottom: 0,
54
+ maxHeight: "60dvh",
55
+ borderBottomLeftRadius: 0,
56
+ borderBottomRightRadius: 0,
57
+ padding: "12px 16px calc(12px + env(safe-area-inset-bottom, 0px))"
58
+ };
59
+ }
60
+ const below = window.innerHeight - chip.bottom;
61
+ const left = Math.max(8, Math.min(chip.left, window.innerWidth - 248));
62
+ const base = { left, right: "auto", minWidth: 200, maxWidth: "calc(100vw - 16px)" };
63
+ return below >= 200 || below >= chip.top ? { ...base, top: chip.bottom + 4, maxHeight: Math.max(below - 12, DISCLOSURE_MIN_HEIGHT) } : {
64
+ ...base,
65
+ bottom: window.innerHeight - chip.top + 4,
66
+ maxHeight: Math.max(chip.top - 12, DISCLOSURE_MIN_HEIGHT)
67
+ };
38
68
  }
39
- function wrapperStyle(placement, fontSize) {
69
+ function wrapperStyle(placement, fontSize, dots) {
40
70
  const vertical = placement === "left" || placement === "right";
71
+ const gap = dots ? DOTS_HIT_GAP : VX.legendGap;
41
72
  return {
42
73
  display: "flex",
43
74
  flexDirection: vertical ? "column" : "row",
44
75
  flexWrap: vertical ? "nowrap" : "wrap",
45
76
  alignItems: vertical ? "flex-start" : "center",
46
- justifyContent: vertical ? "flex-start" : "center",
47
- columnGap: VX.legendGap,
48
- rowGap: VX.legendGap,
49
- padding: "8px 0 2px",
77
+ justifyContent: "flex-start",
78
+ columnGap: gap,
79
+ // Always the tight row gap (R2C-9): the wider dots COLUMN gap is a hit-overlap cap, not
80
+ // breathing room between wrapped rows — a dots-mode header legend is 1 row by law
81
+ // (`LEGEND_ROWS.header` in `chart-layout.ts`) anyway, so this only ever shows in the band.
82
+ rowGap: LEGEND_ROW_GAP,
83
+ // Caps each entry's `[data-basalt-hit]` overlay at the real inter-entry gap (styles.css).
84
+ ...{ "--vx-hit-gap": `${gap}px` },
85
+ // A dots-mode row has no text baseline to breathe around — the header-fold law (wave 8) is
86
+ // the reason this row exists at all, so it gets the same tight treatment.
87
+ padding: dots ? "4px 0 2px" : "8px 0 2px",
50
88
  fontSize,
51
89
  color: VX.muted
52
90
  };
@@ -145,6 +183,21 @@ const LEGEND_CHILD_STYLE = {
145
183
  fontSize: "0.85em",
146
184
  opacity: 0.75
147
185
  };
186
+ function LegendDot({ item }) {
187
+ return /* @__PURE__ */ jsx(
188
+ "span",
189
+ {
190
+ style: {
191
+ width: LEGEND_DOT_SIZE,
192
+ height: LEGEND_DOT_SIZE,
193
+ borderRadius: "50%",
194
+ // theme-allow raw-surface — a circle shape, not a surface corner
195
+ backgroundColor: item.color,
196
+ flexShrink: 0
197
+ }
198
+ }
199
+ );
200
+ }
148
201
  function LegendChildSwatch({ item }) {
149
202
  if (item.shape === "bar") {
150
203
  return /* @__PURE__ */ jsx(
@@ -193,59 +246,105 @@ function ChartLegend({
193
246
  placement = "bottom",
194
247
  groups = false,
195
248
  maxRows,
249
+ mode = "chips",
196
250
  className,
197
251
  style
198
252
  }) {
199
- const tier = useChartTierMetrics();
200
- const [expanded, setExpanded] = useState(false);
201
- const handleEnter = (e) => {
202
- const key = e.currentTarget.dataset["legendKey"];
203
- if (key !== void 0) onHighlight?.(key);
253
+ const tier = useChartMetrics();
254
+ const coarse = useCoarsePointer();
255
+ const [disclosure, setDisclosure] = useState(null);
256
+ const triggerRef = useRef(null);
257
+ const panelRef = useRef(null);
258
+ const open = disclosure !== null;
259
+ const panelId = useId();
260
+ const wasOpen = useRef(false);
261
+ const openDisclosure = (trigger) => {
262
+ triggerRef.current = trigger;
263
+ setDisclosure(disclosurePlacement(trigger.getBoundingClientRect(), coarse));
204
264
  };
265
+ useEffect(() => {
266
+ if (open) panelRef.current?.focus();
267
+ else if (wasOpen.current) triggerRef.current?.focus();
268
+ wasOpen.current = open;
269
+ }, [open]);
270
+ const isInsidePanel = useCallback(
271
+ (target) => panelRef.current?.contains(target) === true || triggerRef.current?.contains(target) === true,
272
+ []
273
+ );
274
+ useDismissOnOutside({
275
+ enabled: open,
276
+ isInside: isInsidePanel,
277
+ onDismiss: () => setDisclosure(null),
278
+ dismissOnResize: true,
279
+ scrollExemptInside: true
280
+ });
281
+ useEffect(() => {
282
+ if (!open || !coarse) return;
283
+ const panel = panelRef.current;
284
+ if (panel === null) return;
285
+ const onKeyDown = (e) => {
286
+ if (e.key !== "Tab") return;
287
+ const focusable = panel.querySelectorAll(
288
+ 'button, [tabindex]:not([tabindex="-1"])'
289
+ );
290
+ const first = focusable[0];
291
+ const last = focusable[focusable.length - 1];
292
+ if (first === void 0 || last === void 0) return;
293
+ if (e.shiftKey && document.activeElement === first) {
294
+ e.preventDefault();
295
+ last.focus();
296
+ } else if (!e.shiftKey && document.activeElement === last) {
297
+ e.preventDefault();
298
+ first.focus();
299
+ }
300
+ };
301
+ panel.addEventListener("keydown", onKeyDown);
302
+ return () => panel.removeEventListener("keydown", onKeyDown);
303
+ }, [open, coarse]);
304
+ const handleEnter = (key) => onHighlight?.(key);
205
305
  const handleLeave = () => onHighlight?.(null);
206
- const handleToggle = (e) => {
207
- const key = e.currentTarget.dataset["legendKey"];
208
- if (key !== void 0) onToggle?.(key);
209
- };
210
306
  const idPrefix = chartId ? `${chartId}-` : "";
211
307
  const vertical = placement === "left" || placement === "right";
212
308
  const { entries, dividerAfter } = orderEntries(items, groups);
213
- const cap = maxRows ?? tier.legendMaxRows;
214
- const rolledUp = cap === void 0 ? 0 : Math.max(entries.length - cap, 0);
215
- const visible = rolledUp > 0 && !expanded ? entries.slice(0, cap) : entries;
216
- const nodes = [];
217
- visible.forEach((item, i) => {
218
- nodes.push(
219
- /* @__PURE__ */ jsxs(
220
- "button",
221
- {
222
- type: "button",
223
- "data-legend-key": item.key,
224
- "aria-label": item.note ? `${item.label} \u2014 ${item.note}` : item.label,
225
- ...onToggle !== void 0 && { "aria-pressed": !hidden.has(item.key) },
226
- style: {
227
- ...LEGEND_ITEM_BUTTON,
228
- cursor: onToggle === void 0 ? "default" : "pointer",
229
- // Toggled-off wins over hover-dimming: a hidden series must read as hidden even while
230
- // it is the one being hovered.
231
- opacity: hidden.has(item.key) ? 0.35 : highlighted === null || highlighted === item.key ? 1 : 0.3,
232
- textDecoration: hidden.has(item.key) ? "line-through" : "none"
233
- },
234
- ...onToggle !== void 0 && { onClick: handleToggle },
235
- onMouseEnter: handleEnter,
236
- onMouseLeave: handleLeave,
237
- onFocus: handleEnter,
238
- onBlur: handleLeave,
239
- children: [
240
- /* @__PURE__ */ jsx(LegendSwatch, { item, idPrefix }),
241
- /* @__PURE__ */ jsx("span", { children: item.label }),
242
- item.note ? /* @__PURE__ */ jsx("span", { style: LEGEND_NOTE_STYLE, children: item.note }) : null,
243
- item.children?.map((child) => /* @__PURE__ */ jsx(LegendChild, { item: child }, child.key))
244
- ]
309
+ const rolledUp = maxRows === void 0 ? 0 : Math.max(entries.length - maxRows, 0);
310
+ const visible = rolledUp > 0 ? entries.slice(0, maxRows) : entries;
311
+ const entryButton = (item, inPanel = false) => {
312
+ const isDotTrigger = !inPanel && mode === "dots";
313
+ return /* @__PURE__ */ jsx(
314
+ "button",
315
+ {
316
+ type: "button",
317
+ "data-basalt-hit": true,
318
+ ...!inPanel && { "data-legend-key": item.key },
319
+ "aria-label": item.note ? `${item.label} \u2014 ${item.note}` : item.label,
320
+ ...isDotTrigger && { title: item.label, "aria-haspopup": "dialog" },
321
+ ...isDotTrigger ? { "aria-expanded": open, "aria-controls": panelId } : onToggle !== void 0 && { "aria-pressed": !hidden.has(item.key) },
322
+ style: {
323
+ ...LEGEND_ITEM_BUTTON,
324
+ cursor: onToggle === void 0 && !isDotTrigger ? "default" : "pointer",
325
+ // Toggled-off wins over hover-dimming: a hidden series must read as hidden even while
326
+ // it is the one being hovered.
327
+ opacity: hidden.has(item.key) ? 0.35 : highlighted === null || highlighted === item.key ? 1 : 0.3,
328
+ textDecoration: hidden.has(item.key) ? "line-through" : "none"
245
329
  },
246
- item.key
247
- )
330
+ ...isDotTrigger ? { onClick: (e) => openDisclosure(e.currentTarget) } : onToggle !== void 0 && { onClick: () => onToggle(item.key) },
331
+ onMouseEnter: () => handleEnter(item.key),
332
+ onMouseLeave: handleLeave,
333
+ onFocus: () => handleEnter(item.key),
334
+ onBlur: handleLeave,
335
+ children: !inPanel && mode === "dots" ? /* @__PURE__ */ jsx(LegendDot, { item }) : /* @__PURE__ */ jsxs(Fragment, { children: [
336
+ /* @__PURE__ */ jsx(LegendSwatch, { item, idPrefix: inPanel ? `${idPrefix}disclosure-` : idPrefix }),
337
+ /* @__PURE__ */ jsx("span", { children: item.label }),
338
+ item.note ? /* @__PURE__ */ jsx("span", { style: LEGEND_NOTE_STYLE, children: item.note }) : null,
339
+ item.children?.map((child) => /* @__PURE__ */ jsx(LegendChild, { item: child }, child.key))
340
+ ] })
341
+ },
342
+ item.key
248
343
  );
344
+ };
345
+ const nodes = [];
346
+ visible.forEach((item, i) => {
347
+ nodes.push(entryButton(item));
249
348
  if (dividerAfter.has(i))
250
349
  nodes.push(/* @__PURE__ */ jsx(LegendDivider, { columnLayout: vertical }, `divider-${item.key}`));
251
350
  });
@@ -255,21 +354,63 @@ function ChartLegend({
255
354
  "button",
256
355
  {
257
356
  type: "button",
258
- "aria-expanded": expanded,
259
- onClick: () => setExpanded((prev) => !prev),
357
+ "data-basalt-hit": true,
358
+ "aria-expanded": open,
359
+ "aria-haspopup": "dialog",
360
+ "aria-controls": panelId,
361
+ onClick: (e) => open ? setDisclosure(null) : openDisclosure(e.currentTarget),
260
362
  style: { ...LEGEND_ITEM_BUTTON, cursor: "pointer", textDecoration: "underline" },
261
- children: expanded ? "Show less" : `+${rolledUp} more`
363
+ children: `All ${entries.length}`
262
364
  },
263
365
  "legend-more"
264
366
  )
265
367
  );
266
368
  }
267
- return /* @__PURE__ */ jsx(
369
+ return /* @__PURE__ */ jsxs(
268
370
  "div",
269
371
  {
270
372
  ...className !== void 0 && { className },
271
- style: { ...wrapperStyle(placement, tier.legendFontSize), ...style },
272
- children: nodes
373
+ style: { ...wrapperStyle(placement, tier.legendFontSize, mode === "dots"), ...style },
374
+ children: [
375
+ nodes,
376
+ open && createPortal(
377
+ /* @__PURE__ */ jsxs(Fragment, { children: [
378
+ coarse && /* @__PURE__ */ jsx(
379
+ "div",
380
+ {
381
+ "aria-hidden": true,
382
+ onPointerDown: (e) => {
383
+ e.stopPropagation();
384
+ setDisclosure(null);
385
+ },
386
+ style: {
387
+ position: "fixed",
388
+ inset: 0,
389
+ // One below the panel it sits behind — still `VX.zIndexFloating`-derived, never a
390
+ // second unrelated literal (B14).
391
+ zIndex: VX.zIndexFloating - 1,
392
+ backgroundColor: alpha(VX.neutral, 0.35)
393
+ }
394
+ }
395
+ ),
396
+ /* @__PURE__ */ jsx(
397
+ "dialog",
398
+ {
399
+ ref: panelRef,
400
+ id: panelId,
401
+ open: true,
402
+ tabIndex: -1,
403
+ "aria-label": "Legend",
404
+ ...coarse && { "aria-modal": true },
405
+ "data-basalt-legend-disclosure": coarse ? "sheet" : "popover",
406
+ style: { ...DISCLOSURE_PANEL, fontSize: tier.legendFontSize, ...disclosure },
407
+ children: entries.map((item) => entryButton(item, true))
408
+ }
409
+ )
410
+ ] }),
411
+ document.body
412
+ )
413
+ ]
273
414
  }
274
415
  );
275
416
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartTooltip.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAuBrD,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,EACV,MAAuB,GACxB,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;sGAGkG;IAClG,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC,+BAoCA;AAED,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,UAAU,EACV,KAAK,EACL,WAAW,EACX,MAAM,GACP,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,+BA0CA;AAED,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAEhE;AAsBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,QAAe,EACf,QAAQ,GACT,EAAE;IACD,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACvC;;;8CAG0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,SAAS,CAAA;CACpB,sCAsDA"}
1
+ {"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartTooltip.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAuBrD,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,EACV,MAAuB,GACxB,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;sGAGkG;IAClG,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC,+BAoCA;AAED,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,UAAU,EACV,KAAK,EACL,WAAW,EACX,MAAM,GACP,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,+BA0CA;AAED,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAEhE;AAsBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,QAAe,EACf,QAAQ,GACT,EAAE;IACD,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACvC;;;8CAG0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,SAAS,CAAA;CACpB,sCA2DA"}
@@ -3,11 +3,11 @@ import { useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { VX } from "../../tokens/index.js";
5
5
  import { fmtTooltipDate } from "../utils/format.js";
6
- import { useChartTierMetrics } from "./chart-tier.js";
6
+ import { useChartMetrics } from "./chart-tier.js";
7
7
  const TOOLTIP_STYLES = {
8
8
  position: "fixed",
9
9
  pointerEvents: "none",
10
- zIndex: 9999,
10
+ zIndex: VX.zIndexFloating,
11
11
  backgroundColor: VX.surface.panel,
12
12
  borderRadius: "var(--vx-radius-floating)",
13
13
  padding: "0",
@@ -132,7 +132,7 @@ function ChartTooltipFloat({
132
132
  const ref = useRef(null);
133
133
  const [box, setBox] = useState(null);
134
134
  const isClient = useIsClient();
135
- const { tooltipMinWidth } = useChartTierMetrics();
135
+ const { tooltipMinWidth } = useChartMetrics();
136
136
  useLayoutEffect(() => {
137
137
  const el = ref.current;
138
138
  if (el === null) return;
@@ -147,6 +147,7 @@ function ChartTooltipFloat({
147
147
  const height = box?.height ?? 0;
148
148
  const vw = window.innerWidth;
149
149
  const vh = window.innerHeight;
150
+ if (anchor.y < 0 || anchor.y > vh) return null;
150
151
  const flipsLeft = anchor.x + TOOLTIP_GAP + width + VIEWPORT_MARGIN > vw;
151
152
  const left = flipsLeft ? Math.max(VIEWPORT_MARGIN, anchor.x - TOOLTIP_GAP - width) : anchor.x + TOOLTIP_GAP;
152
153
  const top = Math.min(
@@ -1,6 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { VX } from "../../tokens/index.js";
3
- import { useChartTierMetrics } from "./chart-tier.js";
3
+ import { useChartMetrics } from "./chart-tier.js";
4
4
  function Crosshair({
5
5
  x,
6
6
  top,
@@ -14,7 +14,7 @@ function SeriesDot({
14
14
  color,
15
15
  r
16
16
  }) {
17
- const { dotR } = useChartTierMetrics();
17
+ const { dotR } = useChartMetrics();
18
18
  return /* @__PURE__ */ jsx("circle", { cx, cy, r: r ?? dotR, fill: color, stroke: VX.dotStroke, strokeWidth: 2 });
19
19
  }
20
20
  export {
@@ -6,10 +6,15 @@ import type { FocusEventHandler, KeyboardEventHandler, PointerEventHandler } fro
6
6
  * With `onKeyDown` it also becomes the chart's keyboard affordance: the rect takes focus and
7
7
  * ←/→ scrub the cursor, so a chart's values are reachable without a pointer.
8
8
  */
9
- export declare function HoverOverlay({ width, height, onMove, onLeave, onKeyDown, onBlur, ariaLabel, valueNow, valueMax, valueText, }: {
9
+ export declare function HoverOverlay({ width, height, onMove, onDown, onUp, onLeave, onKeyDown, onBlur, ariaLabel, valueNow, valueMax, valueText, }: {
10
10
  width: number;
11
11
  height: number;
12
12
  onMove: PointerEventHandler<SVGRectElement>;
13
+ /** Coarse-pointer tap: resolves + shows the tooltip immediately, with no preceding `pointermove`
14
+ * (`docs/CHARTS-SPEC.md` §4, Tooltip). The press is provisional; `onUp` commits it. */
15
+ onDown?: PointerEventHandler<SVGRectElement>;
16
+ /** Commits the provisional touch press so the pin survives the lift. */
17
+ onUp?: PointerEventHandler<SVGRectElement>;
13
18
  onLeave: PointerEventHandler<SVGRectElement>;
14
19
  /** Present = the overlay is focusable and scrubs on ←/→. */
15
20
  onKeyDown?: KeyboardEventHandler<SVGRectElement>;
@@ -1 +1 @@
1
- {"version":3,"file":"HoverOverlay.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/HoverOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAEzF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,MAAM,EACN,MAAM,EACN,OAAO,EACP,SAAS,EACT,MAAM,EACN,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,SAAS,GACV,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAA;IAC3C,OAAO,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAA;IAC5C,4DAA4D;IAC5D,SAAS,CAAC,EAAE,oBAAoB,CAAC,cAAc,CAAC,CAAA;IAChD,MAAM,CAAC,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAA;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,+BA8BA"}
1
+ {"version":3,"file":"HoverOverlay.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/HoverOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAGzF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,MAAM,EACN,MAAM,EACN,MAAM,EACN,IAAI,EACJ,OAAO,EACP,SAAS,EACT,MAAM,EACN,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,SAAS,GACV,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAA;IAC3C;2FACuF;IACvF,MAAM,CAAC,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAA;IAC5C,wEAAwE;IACxE,IAAI,CAAC,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAA;IAC1C,OAAO,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAA;IAC5C,4DAA4D;IAC5D,SAAS,CAAC,EAAE,oBAAoB,CAAC,cAAc,CAAC,CAAA;IAChD,MAAM,CAAC,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAA;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,+BAiCA"}
@@ -1,8 +1,11 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
+ import styles from "./HoverOverlay.module.css";
2
3
  function HoverOverlay({
3
4
  width,
4
5
  height,
5
6
  onMove,
7
+ onDown,
8
+ onUp,
6
9
  onLeave,
7
10
  onKeyDown,
8
11
  onBlur,
@@ -11,32 +14,40 @@ function HoverOverlay({
11
14
  valueMax,
12
15
  valueText
13
16
  }) {
14
- return /* @__PURE__ */ jsx(
15
- "rect",
16
- {
17
- width,
18
- height,
19
- fill: "transparent",
20
- ...onKeyDown !== void 0 && {
21
- tabIndex: 0,
22
- // `slider`, not `application`: `application` would drop this node out of the screen
23
- // reader's normal browse mode entirely, which is a far bigger hammer than an arrow-key
24
- // scrub needs. A slider announces both the affordance and the focused point.
25
- role: "slider",
26
- "aria-label": ariaLabel ?? "Chart data \u2014 use arrow keys to scrub",
27
- "aria-orientation": "horizontal",
28
- "aria-valuemin": 0,
29
- ...valueMax !== void 0 && { "aria-valuemax": valueMax },
30
- ...valueNow !== void 0 && { "aria-valuenow": valueNow },
31
- ...valueText !== void 0 && { "aria-valuetext": valueText },
32
- onKeyDown,
33
- ...onBlur !== void 0 && { onBlur }
34
- },
35
- style: { touchAction: "pan-y" },
36
- onPointerMove: onMove,
37
- onPointerLeave: onLeave,
38
- onPointerCancel: onLeave
39
- }
17
+ return (
18
+ // `touch-action: pan-y` lives on the plot's `<svg>` (each kind's own), NOT here: Chrome ignores
19
+ // `touch-action` on an SVG child `<rect>`, so the browser claims a horizontal pan and cancels
20
+ // the pointer (`R2C-2`). The `overlay` class owns the focus ring: no UA outline on tap, a VX
21
+ // accent inset under `:focus-visible` (`R2C-15`).
22
+ /* @__PURE__ */ jsx(
23
+ "rect",
24
+ {
25
+ className: styles.overlay,
26
+ width,
27
+ height,
28
+ fill: "transparent",
29
+ ...onKeyDown !== void 0 && {
30
+ tabIndex: 0,
31
+ // `slider`, not `application`: `application` would drop this node out of the screen
32
+ // reader's normal browse mode entirely, which is a far bigger hammer than an arrow-key
33
+ // scrub needs. A slider announces both the affordance and the focused point.
34
+ role: "slider",
35
+ "aria-label": ariaLabel ?? "Chart data \u2014 use arrow keys to scrub",
36
+ "aria-orientation": "horizontal",
37
+ "aria-valuemin": 0,
38
+ ...valueMax !== void 0 && { "aria-valuemax": valueMax },
39
+ ...valueNow !== void 0 && { "aria-valuenow": valueNow },
40
+ ...valueText !== void 0 && { "aria-valuetext": valueText },
41
+ onKeyDown,
42
+ ...onBlur !== void 0 && { onBlur }
43
+ },
44
+ onPointerMove: onMove,
45
+ ...onDown !== void 0 && { onPointerDown: onDown },
46
+ ...onUp !== void 0 && { onPointerUp: onUp },
47
+ onPointerLeave: onLeave,
48
+ onPointerCancel: onLeave
49
+ }
50
+ )
40
51
  );
41
52
  }
42
53
  export {
@@ -0,0 +1,13 @@
1
+ /*
2
+ * `HoverOverlay` — the transparent plot rect every cartesian kind scrubs through. A tap focuses it
3
+ * on pointerdown, so the UA's own outline would ring the whole plot on every phone tap (`R2C-15`);
4
+ * keyboard focus keeps a VX accent inset instead.
5
+ */
6
+ .overlay {
7
+ outline: none;
8
+ }
9
+
10
+ .overlay:focus-visible {
11
+ outline: 1px solid var(--vx-accent);
12
+ outline-offset: -1px;
13
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Typed CSS-module declaration — named string props, so dot access stays verbatim under the
3
+ * package's strict flags.
4
+ */
5
+ declare const classes: {
6
+ readonly overlay: string
7
+ }
8
+ export default classes
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `ChartCard`'s internal context: `inCard` (a frame inside a card treats its computed height as a
3
+ * MINIMUM and grows into the card body), the legend slot — the empty header-row box a chart will portal its legend into
4
+ * (`docs/CHARTS-SPEC.md` §5, Legend), so a card has one chrome band, not two — and `short`, the same
5
+ * `measuredHeight < CARD_SHORT_HEIGHT` flag `ChartCard` already computes for the header-fold law
6
+ * (`data-basalt-card-short`), threaded through so `ChartFrame` can fold its header-slot legend to
7
+ * 'dots' before it eats the plot floor (`docs/CHARTS-SPEC.md` §5, wave 11). `null`/`false` until the slot
8
+ * mounts, and outside any card. Deliberately not exported from any barrel.
9
+ *
10
+ * `claimLegendSlot`/`releaseLegendSlot` arbitrate the slot when a card hosts more than one
11
+ * `ChartFrame` (`docs/waves/PLAN.md` wave 3, P2-9): the first frame to claim an id keeps it for as
12
+ * long as it holds it, every later claimant is told no and falls back to its own band legend
13
+ * instead of a second frame silently portalling into the same node. Outside any card there is no
14
+ * slot to contend for, so the default always grants the claim.
15
+ *
16
+ * `legendSlotVersion` bumps every time the owner releases the slot (round 3's carried gap): a
17
+ * denied frame's claim attempt runs once, in its own mount effect, so with no signal that the slot
18
+ * freed up it would sit denied forever even after the owning frame unmounts. Frames read it only to
19
+ * put it in a dependency array — the effect re-running is the entire point, not the number itself.
20
+ */
21
+ export declare const ChartCardContext: import("react").Context<{
22
+ legendSlot: HTMLDivElement | null;
23
+ inCard: boolean;
24
+ short: boolean;
25
+ claimLegendSlot: (id: string) => boolean;
26
+ releaseLegendSlot: (id: string) => void;
27
+ legendSlotVersion: number;
28
+ }>;
29
+ //# sourceMappingURL=chart-card-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chart-card-context.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-card-context.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gBAAgB;gBACf,cAAc,GAAG,IAAI;YACzB,OAAO;WACR,OAAO;qBACG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO;uBACrB,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI;uBACpB,MAAM;EAQzB,CAAA"}
@@ -0,0 +1,13 @@
1
+ import { createContext } from "react";
2
+ const ChartCardContext = createContext({
3
+ legendSlot: null,
4
+ inCard: false,
5
+ short: false,
6
+ claimLegendSlot: () => true,
7
+ releaseLegendSlot: () => {
8
+ },
9
+ legendSlotVersion: 0
10
+ });
11
+ export {
12
+ ChartCardContext
13
+ };