basalt-ui 1.30.1 → 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 +38 -0
  3. package/MIGRATING.md +218 -14
  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 +31 -17
  66. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartFrame.js +149 -32
  68. package/dist/charts/primitives/ChartLegend.d.ts +20 -14
  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 -112
  83. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  84. package/dist/charts/primitives/chart-frame-layout.js +83 -31
  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 +22 -1
  97. package/dist/common/errors.d.ts.map +1 -1
  98. package/dist/common/errors.js +20 -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 +272 -53
  252. package/src/charts/primitives/ChartLegend.tsx +258 -72
  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 +227 -156
  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 +39 -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
@@ -12,15 +12,43 @@
12
12
  * The header renders only when at least one of title/info/value/actions/icon/count is set — ending
13
13
  * the `''`-as-hidden-header sentinel a consumer used to reach for otherwise.
14
14
  */
15
+ import { useCallback, useMemo, useRef, useState } from 'react'
15
16
  import type { CSSProperties, ReactNode } from 'react'
16
17
  import { cx } from '../../common/props'
17
18
  import type { BasaltProps, SlotStylesProps } from '../../common/props'
18
19
  import { VX } from '../../tokens'
19
20
  import { WidgetHeader } from '../../dashboard/widget-header'
20
21
  import type { DeltaPolarity } from '../../dashboard/delta-badge'
22
+ import { useChartSize } from '../hooks/useChartSize'
23
+ import { useHysteresis } from '../hooks/use-hysteresis'
24
+ import { ChartCardContext } from './chart-card-context'
21
25
  import { ChartEmpty, ChartError, ChartPending, resolveChartState } from './ChartPending'
22
26
  import type { ChartState } from './ChartPending'
23
27
 
28
+ /**
29
+ * Below this rendered height the header folds its subtitle regardless of width (wave 8's
30
+ * header-economy law, `docs/CHARTS-SPEC.md` §5) — a short-but-wide card (a KPI-style
31
+ * chart card) still needs the vertical space back. A `@container basalt-card` SIZE query would be
32
+ * the natural expression, but `container-type: size` on a box with no explicit height (the common,
33
+ * non-grid-stretched case) collapses it to 0 — CSS containment requires a definite size in the
34
+ * contained axis, and a ChartCard's height is ordinarily derived from its content (VERIFIED: making
35
+ * the root a size container drops `card-chrome.layout.test.ts`'s unstretched short-card fixture to
36
+ * a 0px box). `useChartSize` (the same measuring primitive `ChartFrame` uses) reads the rendered
37
+ * height instead, and `data-basalt-card-short` is the CSS hook `widget-header.module.css` reacts to
38
+ * with the same subtitle-fold declarations as the width query.
39
+ */
40
+ const CARD_SHORT_HEIGHT = 280
41
+ /**
42
+ * Hysteresis band on the flag above (`docs/waves/PLAN.md` wave 3, P0-1). `short` folds the header,
43
+ * which changes the card's OWN measured height by the fold delta — a card whose unfolded height
44
+ * lands in `[280, 280 + Δ)` is bistable, and the async RO → rAF → debounce → setState → commit path
45
+ * can sit one reading behind, which turned the bistability into a perpetual flip-flop on the
46
+ * "Sales by channel" dashboard card (27-52 times per 2s, every viewport). Entering short still
47
+ * happens below `CARD_SHORT_HEIGHT`; LEAVING it needs `CARD_SHORT_HEIGHT + SHORT_EXIT_SLACK`, sized
48
+ * to clear the largest fold delta (subtitle line + inset + value row) with headroom.
49
+ */
50
+ const SHORT_EXIT_SLACK = 24
51
+
24
52
  // Surfaces resolve per theme via CSS vars, so the styles are static (no useMemo/isDark).
25
53
  // Depth = `shadow-card` (a whisper shadow + a 1px ring baked into the same value), never a
26
54
  // `border` property (docs/DESIGN-SPEC.md §5) — so a ChartCard reads IDENTICALLY to a Mantine
@@ -31,7 +59,16 @@ import type { ChartState } from './ChartPending'
31
59
  // `WidgetHeader`'s info bubble (`widget-header.module.css`'s `.infoBubble`, which is where the
32
60
  // tooltip this file used to own now lives) opens downward past the header's own box, and must be
33
61
  // able to overhang the card edge without being invisibly cut off.
62
+ //
63
+ // The root is a flex column and a `basalt-card` inline-size container (the card's own width drives
64
+ // its header chrome, `docs/CHARTS-SPEC.md` §8): the body takes the row's spare height when a grid stretches
65
+ // the card.
34
66
  const cardStyle: CSSProperties = {
67
+ display: 'flex',
68
+ flexDirection: 'column',
69
+ minHeight: 0,
70
+ containerType: 'inline-size',
71
+ containerName: 'basalt-card',
35
72
  borderRadius: VX.radiusCard,
36
73
  boxShadow: VX.shadowCard,
37
74
  backgroundColor: VX.surface.panel,
@@ -45,6 +82,11 @@ const bodyClipStyle: CSSProperties = {
45
82
  borderBottomLeftRadius: VX.radiusCard,
46
83
  borderBottomRightRadius: VX.radiusCard,
47
84
  overflow: 'hidden',
85
+ flex: '1 1 auto',
86
+ minHeight: 0,
87
+ // A column, so a `ChartFrame` inside can grow into the body a stretched grid row hands the card.
88
+ display: 'flex',
89
+ flexDirection: 'column',
48
90
  padding: '2px var(--vx-space-card-inset-x, 0.8125rem) var(--vx-space-card-inset-y, 0.6875rem)',
49
91
  }
50
92
  // Card inset = the `cardInsetY`/`cardInsetX` role tokens (vertical / horizontal), the SAME two
@@ -60,6 +102,13 @@ const bodyClipStyle: CSSProperties = {
60
102
  const headerWrapStyle: CSSProperties = {
61
103
  padding: 'var(--vx-space-card-inset-y, 0.6875rem) var(--vx-space-card-inset-x, 0.8125rem) 4px',
62
104
  }
105
+ // A short card (wave 8) trims the header's own top inset — the subtitle line and the wide-tier
106
+ // value/delta row are both already gone by the time `short` is true, so the full card inset reads
107
+ // as dead air rather than rhythm. Bottom stays 4px (unchanged) so the header-to-body seam matches
108
+ // every other ChartCard.
109
+ const headerWrapShortStyle: CSSProperties = {
110
+ padding: '4px var(--vx-space-card-inset-x, 0.8125rem) 4px',
111
+ }
63
112
 
64
113
  /**
65
114
  * The three boxes a `ChartCard` paints, and therefore the slot set a consumer may class
@@ -143,6 +192,8 @@ export function ChartCard({
143
192
  style,
144
193
  children,
145
194
  }: ChartCardProps) {
195
+ const [legendSlot, setLegendSlot] = useState<HTMLDivElement | null>(null)
196
+ const { ref: sizeRef, height: measuredHeight } = useChartSize()
146
197
  const resolvedState = resolveChartState({ ...(state !== undefined && { state }) })
147
198
  const hasHeader =
148
199
  title !== undefined ||
@@ -151,59 +202,101 @@ export function ChartCard({
151
202
  actions !== undefined ||
152
203
  icon !== undefined ||
153
204
  count !== undefined
205
+ // Hysteresis (P0-1, `SHORT_EXIT_SLACK`'s own doc): which threshold applies depends on the
206
+ // PREVIOUS committed flag, so a card whose unfolded height sits in the bistable band settles
207
+ // instead of flip-flopping forever.
208
+ const short = useHysteresis(
209
+ (wasShort) =>
210
+ measuredHeight > 0 &&
211
+ measuredHeight < (wasShort ? CARD_SHORT_HEIGHT + SHORT_EXIT_SLACK : CARD_SHORT_HEIGHT),
212
+ false,
213
+ )
214
+
215
+ const legendOwnerRef = useRef<string | null>(null)
216
+ // Bumped on every release so a previously-denied sibling's claim effect (keyed on the context
217
+ // value it reads, see `chart-card-context.ts`) re-runs and gets a chance to re-claim the slot the
218
+ // owner just gave up — without it a frame told "no" once stayed denied even after the owner
219
+ // unmounted.
220
+ const [legendSlotVersion, setLegendSlotVersion] = useState(0)
221
+ const claimLegendSlot = useCallback((id: string) => {
222
+ if (legendOwnerRef.current === null) legendOwnerRef.current = id
223
+ return legendOwnerRef.current === id
224
+ }, [])
225
+ const releaseLegendSlot = useCallback((id: string) => {
226
+ if (legendOwnerRef.current === id) {
227
+ legendOwnerRef.current = null
228
+ setLegendSlotVersion((v) => v + 1)
229
+ }
230
+ }, [])
231
+ const context = useMemo(
232
+ () => ({
233
+ legendSlot,
234
+ inCard: true,
235
+ short,
236
+ claimLegendSlot,
237
+ releaseLegendSlot,
238
+ legendSlotVersion,
239
+ }),
240
+ [legendSlot, short, claimLegendSlot, releaseLegendSlot, legendSlotVersion],
241
+ )
154
242
 
155
243
  return (
156
- <div
157
- className={cx(classNames?.root, className)}
158
- style={{ ...cardStyle, ...style }}
159
- {...(resolvedState === 'pending' && { 'aria-busy': 'true' })}
160
- >
161
- {hasHeader && (
162
- <div
163
- {...(classNames?.header !== undefined && { className: classNames.header })}
164
- style={headerWrapStyle}
165
- >
166
- <WidgetHeader
167
- tier="widget"
168
- title={title ?? ''}
169
- {...(icon !== undefined && { icon })}
170
- {...(subtitle !== undefined && { subtitle })}
171
- {...(info !== undefined && { info })}
172
- {...(value !== undefined && { value })}
173
- {...(delta !== undefined && { delta })}
174
- {...(deltaPeriod !== undefined && { deltaPeriod })}
175
- {...(deltaPolarity !== undefined && { deltaPolarity })}
176
- {...(count !== undefined && { count })}
177
- {...(actions !== undefined && {
178
- actions: <span data-basalt-tier="widget">{actions}</span>,
179
- })}
180
- />
181
- </div>
182
- )}
244
+ <ChartCardContext.Provider value={context}>
183
245
  <div
184
- {...(classNames?.body !== undefined && { className: classNames.body })}
185
- style={bodyClipStyle}
246
+ ref={sizeRef}
247
+ className={cx(classNames?.root, className)}
248
+ style={{ ...cardStyle, ...style }}
249
+ {...(resolvedState === 'pending' && { 'aria-busy': 'true' })}
250
+ {...(short && { 'data-basalt-card-short': '' })}
186
251
  >
187
- {resolvedState === 'pending' ? (
188
- <ChartPending width="100%" height={placeholderHeight} />
189
- ) : resolvedState === 'error' ? (
190
- <ChartError
191
- width="100%"
192
- height={placeholderHeight}
193
- error={state?.error}
194
- action={stateAction}
195
- />
196
- ) : resolvedState === 'empty' ? (
197
- <ChartEmpty
198
- width="100%"
199
- height={placeholderHeight}
200
- {...(typeof state?.empty === 'string' && { label: state.empty })}
201
- action={stateAction}
202
- />
203
- ) : (
204
- children
252
+ {hasHeader && (
253
+ <div
254
+ {...(classNames?.header !== undefined && { className: classNames.header })}
255
+ style={short ? headerWrapShortStyle : headerWrapStyle}
256
+ >
257
+ <WidgetHeader
258
+ tier="widget"
259
+ title={title ?? ''}
260
+ {...(icon !== undefined && { icon })}
261
+ {...(subtitle !== undefined && { subtitle })}
262
+ {...(info !== undefined && { info })}
263
+ {...(value !== undefined && { value })}
264
+ {...(delta !== undefined && { delta })}
265
+ {...(deltaPeriod !== undefined && { deltaPeriod })}
266
+ {...(deltaPolarity !== undefined && { deltaPolarity })}
267
+ {...(count !== undefined && { count })}
268
+ {...(actions !== undefined && {
269
+ actions: <span data-basalt-tier="widget">{actions}</span>,
270
+ })}
271
+ />
272
+ <div ref={setLegendSlot} data-basalt-legend-slot="" />
273
+ </div>
205
274
  )}
275
+ <div
276
+ {...(classNames?.body !== undefined && { className: classNames.body })}
277
+ style={bodyClipStyle}
278
+ >
279
+ {resolvedState === 'pending' ? (
280
+ <ChartPending width="100%" height={placeholderHeight} />
281
+ ) : resolvedState === 'error' ? (
282
+ <ChartError
283
+ width="100%"
284
+ height={placeholderHeight}
285
+ error={state?.error}
286
+ action={stateAction}
287
+ />
288
+ ) : resolvedState === 'empty' ? (
289
+ <ChartEmpty
290
+ width="100%"
291
+ height={placeholderHeight}
292
+ {...(typeof state?.empty === 'string' && { label: state.empty })}
293
+ action={stateAction}
294
+ />
295
+ ) : (
296
+ children
297
+ )}
298
+ </div>
206
299
  </div>
207
- </div>
300
+ </ChartCardContext.Provider>
208
301
  )
209
302
  }