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
@@ -2,66 +2,68 @@
2
2
  * Pure, DOM-free layout math behind `ChartFrame`'s plot/legend split — extracted so the arithmetic
3
3
  * is testable without a `ResizeObserver` and `ChartFrame.tsx` stays the measuring/rendering shell.
4
4
  */
5
+ import type { ContainerClass } from '../../tokens/size-classes.js';
5
6
  import type { ChartMargin } from '../../tokens/index.js';
6
7
  import type { LegendEntry } from './ChartLegend.js';
8
+ /** Gap between two wrapped legend rows — tight (4-6px); the wider `VX.legendGap` separates entries. */
9
+ export declare const LEGEND_ROW_GAP = 6;
7
10
  /**
8
- * The two size tiers a chart resolves to. There are exactly two, and there is no `tablet`: the
9
- * tier exists to answer "is there room for full-size chrome around the plot", which is a yes/no
10
- * question, and a third rung would need a third calibrated metric set nothing has asked for.
11
+ * The real gap a dots-mode entry needs so its `[data-basalt-hit]::after` overlay can reach the
12
+ * full 44px coarse floor with NO overlap into its neighbour's overlay (`hit-floor.layout.test.ts` +
13
+ * `hit-overlap.layout.test.ts`, wave 4's cap: `styles.css`'s `::after` grows to
14
+ * `min(max(size,44), size + 2 + gap)`). Two 44px-wide overlays centred on same-size hosts touch
15
+ * with zero overlap only once their PITCH (real gap + host size) is itself >= 44 — below that, the
16
+ * cap can only shrink the overlay below 44 (honouring WCAG 2.5.8's spacing route) or let it overlap;
17
+ * there is no gap value that lets it hit 44 exactly while staying flush against a same-size sibling.
18
+ * `LEGEND_DOT_SIZE` is 8px, so the floor is 44 − 8 − 2 = 34; a few px of headroom against rounding.
11
19
  */
12
- export type ChartTier = 'phone' | 'desktop';
20
+ export declare const LEGEND_DOT_SIZE = 8;
21
+ export declare const DOTS_HIT_GAP: number;
13
22
  /**
14
- * The tier a chart's chrome resolves to, from the width of the box it was MEASURED in — never
15
- * from a media query.
16
- *
17
- * A viewport breakpoint answers the wrong question. A chart in a 2-column grid cell on a 1440px
18
- * desktop is exactly as narrow as one filling a phone, and `@media` cannot see that; the
19
- * `ResizeObserver` `ChartFrame` already runs can. It also keeps this file (and the whole chart
20
- * layer) Mantine-free — `theme.breakpoints` is on the coupled side of the boundary.
21
- *
22
- * An UNMEASURED box (`containerW <= 0` — SSR, or before the observer's first callback) resolves
23
- * to `'desktop'`: the first frame must not paint phone chrome that then re-lays-out one frame
24
- * later, and `resolvePlotRect` already treats an unmeasured width as the first-frame case.
23
+ * Order entries series → overlay → reference and record where a divider belongs — the ONE ordering
24
+ * both `ChartLegend`'s render and the resolver's fit math (`chart-layout.ts`) use, so a dots-mode
25
+ * fit never disagrees with what actually wraps (a grouped legend renders dividers as flex items).
26
+ */
27
+ export declare function orderEntries(items: readonly LegendEntry[], groups: boolean): {
28
+ entries: LegendEntry[];
29
+ dividerAfter: ReadonlySet<number>;
30
+ };
31
+ /**
32
+ * The two metric sets a chart's chrome resolves to. Internally the container class
33
+ * (`micro`/`compact` → `'phone'`, `regular`/`wide` → `'desktop'`) picks one; there is no `tablet`.
34
+ * Internal only — not on any public barrel.
25
35
  */
26
- export declare function resolveChartTier(containerW: number): ChartTier;
36
+ export type ChartTier = 'phone' | 'desktop';
27
37
  /**
28
- * A chart height stated per size step instead of as one number — the declarative escape from "every
29
- * chart on this page is 260px tall on a 375px phone too".
38
+ * A chart height stated per container step instead of as one number — the declarative escape from
39
+ * "every chart on this page is 260px tall on a 375px phone too".
30
40
  *
31
- * `base` is required and is the height a box narrower than every named step gets; each further key
32
- * is the height from that step UP. Keys are Mantine's breakpoint names so a consumer writes the
33
- * vocabulary they already write, and nothing more than `sm`/`md`/`lg` is offered: `xs`/`xl` would
34
- * be two more rungs nobody has asked a chart to have.
41
+ * `base` is the height below the `regular` class; `regular` and `wide` take effect from
42
+ * `CONTAINER_CLASSES.regular` / `.wide` of the frame's MEASURED width (never the viewport).
35
43
  *
36
44
  * ```tsx
37
- * <ChartFrame series={series} height={{ base: 180, md: 260 }}>{…}</ChartFrame>
45
+ * <ChartFrame series={series} height={{ base: 180, wide: 260 }}>{…}</ChartFrame>
38
46
  * ```
39
47
  */
40
48
  export type ResponsiveChartHeight = {
41
- /** Below the narrowest STATED step — and NOT the first-frame value; see {@link resolveFrameHeight}. */
42
49
  base: number;
43
- /** From 768px of MEASURED container width up. */
44
- sm?: number;
45
- /** From 992px up. */
46
- md?: number;
47
- /** From 1200px up. */
48
- lg?: number;
50
+ /** From the `regular` container class (480px) up. */
51
+ regular?: number;
52
+ /** From the `wide` container class (800px) up. */
53
+ wide?: number;
49
54
  };
50
55
  /**
51
- * Resolve a `height` prop — a plain number, or a {@link ResponsiveChartHeight} — against the
52
- * measured container width.
53
- *
54
- * A plain number passes through untouched: this is a WIDENING of the prop, not a replacement, and
55
- * every existing `height={240}` must keep meaning exactly 240.
56
- *
57
- * An UNMEASURED box (`containerW <= 0` — SSR, or before the `ResizeObserver`'s first callback)
58
- * resolves to the LARGEST stated step, matching {@link resolveChartTier}'s first-frame rule: the
59
- * first paint must not be phone-shaped chrome that grows one frame later. Falling to `base` there
60
- * would make every SSR'd chart short and then jump.
56
+ * Resolve a `height` prop — a plain number, or a {@link ResponsiveChartHeight} — for a container
57
+ * class. A plain number passes through untouched. The class is the measured one, or, before the
58
+ * first measurement, the one implied by the viewport ({@link resolveChartLayout}), so the first
59
+ * frame is not a different height from the second.
60
+ */
61
+ export declare function resolveFrameHeight(height: number | ResponsiveChartHeight, containerClass: ContainerClass): number;
62
+ /**
63
+ * Every size a chart's chrome resolves per tier. One object so a new tier-sensitive size is added
64
+ * in one place and read by name, rather than each primitive branching on the tier itself. Internal
65
+ * only — not on any public barrel.
61
66
  */
62
- export declare function resolveFrameHeight(height: number | ResponsiveChartHeight, containerW: number): number;
63
- /** Every size a chart's chrome resolves per tier. One object so a new tier-sensitive size is added
64
- * in one place and read by name, rather than each primitive branching on the tier itself. */
65
67
  export type ChartTierMetrics = {
66
68
  /** Which tier these are, so a consumer of the metrics never needs both values threaded. */
67
69
  tier: ChartTier;
@@ -74,58 +76,28 @@ export type ChartTierMetrics = {
74
76
  dotR: number;
75
77
  /** Floating tooltip `minWidth`, px. */
76
78
  tooltipMinWidth: number;
77
- /** Default entry cap on the legend, or `undefined` for no default cap. An explicit
78
- * `legend.maxRows` replaces it outright ({@link resolveLegendMaxRows}). */
79
- legendMaxRows: number | undefined;
80
79
  /** Per-side margin FLOORS (`VX.margin` is a floor, never a ceiling — §1). */
81
80
  margin: ChartMargin;
82
81
  };
83
- /** The resolved sizes for one tier. Frozen module constants — never a new object per render. */
84
- export declare function chartTierMetrics(tier: ChartTier): ChartTierMetrics;
85
- /**
86
- * Which legend cap applies: the caller's, or the tier's own default.
87
- *
88
- * **An explicit `callerMaxRows` wins OUTRIGHT.** This used to be `Math.min(caller, tier)`, which
89
- * made the documented per-chart opt-out ("opt back out with an explicit `legend.maxRows`") false in
90
- * the one direction anybody needs it. The tier's `PHONE_LEGEND_MAX_ROWS` is a DEFAULT for charts
91
- * that said nothing — a chart that names a number has already answered the question the default
92
- * exists to answer, and it matters because the tier keys on the measured box: a 380px inspector
93
- * panel on a 1440px desktop is "a phone", so a six-series legend rolled up to two rows + `+4 more`
94
- * with no way to say no. `margin` and `xLabelRotate` always had that escape; the legend did not.
95
- *
96
- * The measured {@link legendEntryCap} a `fill` frame applies is NOT a second ceiling on top of
97
- * this: it resolves an ABSENT cap only. 1.30.0 ran it over the caller's number as well
98
- * (`Math.min(fitted, caller)`), which made "wins outright" false again in the one place it was
99
- * needed — meteo measured a 7-entry legend pinned to 2 entries at `maxRows` 3, 6 AND 99, with 5
100
- * series drawn in colours the legend refused to name. The fit is not physics either: it derives
101
- * from `VX.minPlotHeight`, which is a framework DEFAULT about the plot. Two defaults do not
102
- * outvote the one number a caller stated, so the PLOT yields the height instead
103
- * ({@link resolvePlotRect}'s `legendWins`) — visibly, in the frame the caller sized.
104
- */
105
- export declare function resolveLegendMaxRows(input: {
106
- callerMaxRows?: number | undefined;
107
- tier: ChartTier;
108
- }): number | undefined;
82
+ /** The metric set a container class resolves to (`micro`/`compact` → phone, else desktop). */
83
+ export declare function chartMetrics(containerClass: ContainerClass): ChartTierMetrics;
109
84
  /**
110
85
  * The whole legend-cap decision in one place: which cap `ChartLegend` gets, and whether the plot
111
- * pays for it.
112
- *
113
- * It lives here rather than inline in `ChartFrame` because the COMPOSITION is what broke. 1.30.0
114
- * had both halves right on their own — {@link resolveLegendMaxRows} honoured a stated cap,
115
- * {@link legendEntryCap} measured a fit — and then ran the second over the first, which no test
116
- * could see because neither pure function was wrong and the frame that combined them needs a
117
- * `ResizeObserver` to test. Now the combination is a pure function too.
86
+ * pays for it. Pure, because the COMPOSITION is what once broke (1.30.0 ran a measured fit over a
87
+ * caller's stated number) and the frame that combines them needs a `ResizeObserver` to test.
118
88
  *
119
89
  * Three cases, in order:
120
- * 1. The caller STATED a number → it is the cap, and on a `fill` band the plot yields
121
- * (`legendWins`). Nothing measured or defaulted trims it.
122
- * 2. No `fill` band → the tier's default (or none), because the frame simply grows.
123
- * 3. A `fill` band with nothing stated → the measured fit, bounded by the tier's default.
90
+ * 1. The caller STATED `legend.maxRows` (deprecated, still honoured) → it is the cap, and on a
91
+ * `fill` band the plot yields (`legendWins`). Nothing measured trims it.
92
+ * 2. No `fill` band → the resolver's width-measured fit (`fittedMaxRows`), because the frame grows.
93
+ * 3. A `fill` band with nothing stated → the tighter of that fit and the height-measured
94
+ * {@link legendEntryCap}, because a `fill` box cannot grow.
124
95
  */
125
96
  export declare function resolveLegendRollup(input: {
126
97
  /** The caller's own `legend.maxRows`. */
127
98
  statedMaxRows?: number | undefined;
128
- tier: ChartTier;
99
+ /** `resolveChartLayout().legend.visible` when it overflows, else `undefined`. */
100
+ fittedMaxRows?: number | undefined;
129
101
  /** A visible top/bottom legend on a `fill` frame — the one shape whose box cannot grow. */
130
102
  fillBand: boolean;
131
103
  items: readonly LegendEntry[];
@@ -140,22 +112,13 @@ export declare function resolveLegendRollup(input: {
140
112
  * Resolve the plot rect from the measured box and the measured legend band. Pure and exported so
141
113
  * the two floors below are testable without a `ResizeObserver`.
142
114
  *
143
- * Two floors, and each is only a floor while it is honest:
144
- * - **width** — `minWidth` is a FIRST-FRAME guard, applied only while `containerW` is still 0
145
- * (unmeasured, or SSR, where the observer never fires). Once measured the plot tracks the box
146
- * exactly, floored at 1 so no scale divides by zero: applying a 200px floor forever drew an SVG
147
- * wider than its own container inside any narrower grid cell.
148
- * - **height** — the legend band is subtracted first (it always was), but the remainder can no
149
- * longer collapse: eight entries wrapping to five rows at phone width ate a fixed
150
- * `height={240}` toward zero and the body stopped rendering entirely. The plot stops at
151
- * `VX.minPlotHeight` and the frame's own box grows by the difference instead — it is a flex
152
- * column with `height: auto`, so that growth is automatic. Under `fill` the box CANNOT grow, so
153
- * the legend is capped instead ({@link legendEntryCap}) — UNLESS the caller stated
154
- * `legend.maxRows`, which is `legendWins`: there the floor itself yields, because a floor is a
155
- * default and the caller's number is not, but only as far as the box HAS room
156
- * ({@link SELF_MEASURED_SLACK}). An unmeasured box (`resolvedHeight <= 0`, i.e. a `fill` frame
157
- * before its first measurement) stays at 0 and renders nothing, exactly as before — the floor
158
- * must never invent a height for a box nobody has measured.
115
+ * `minWidth` is a FIRST-FRAME guard, applied only while `containerW` is still 0 (unmeasured, or
116
+ * SSR); once measured the plot tracks the box exactly, floored at 1 so no scale divides by zero.
117
+ * The legend band is subtracted first, and the remainder stops at `VX.minPlotHeight` — the frame's
118
+ * flex box grows by the difference under a fixed height, and under `fill` (which cannot grow) the
119
+ * legend is capped instead ({@link legendEntryCap}). A caller-stated `legend.maxRows` yields the
120
+ * floor itself (`legendWins`) as far as the box has room ({@link SELF_MEASURED_SLACK}); an
121
+ * unmeasured box (`resolvedHeight <= 0`) stays 0 and renders nothing.
159
122
  */
160
123
  export declare function resolvePlotRect(input: {
161
124
  containerW: number;
@@ -176,20 +139,37 @@ export declare function resolvePlotRect(input: {
176
139
  width: number;
177
140
  height: number;
178
141
  };
142
+ /** How many of `items` fit in `rows` wrapped rows of `width` — the same greedy wrap the flex
143
+ * container performs, measured rather than assumed (`docs/CHARTS-SPEC.md` §1). */
144
+ export declare function entriesWithinRows(items: readonly LegendEntry[], width: number, rows: number): number;
145
+ /**
146
+ * Chips-mode counterpart of {@link entriesWithinDotRows}: once the un-reserved fit overflows, a
147
+ * second pass measures against the width LESS the `All N` chip's own footprint, so the count this
148
+ * returns still leaves the chip room to sit beside it in the same row — same reserve law the dots
149
+ * fit already applied, now applied to chip width instead of a dot's pitch. Returns 0 when not even
150
+ * the first chip fits once the chip's width is reserved, so the row renders the disclosure alone
151
+ * rather than one crowded chip plus a chip that doesn't actually fit next to it.
152
+ */
153
+ export declare function entriesWithinChipRows(items: readonly LegendEntry[], width: number, rows: number): number;
179
154
  /**
180
- * The entry cap a `fill` frame's legend must respect so the plot keeps `VX.minPlotHeight`.
155
+ * How many of `items` fit as DOTS in `rows` wrapped rows of `width` — the dots-mode counterpart of
156
+ * {@link entriesWithinRows}, measured at the dot's own pitch rather than a chip's width.
157
+ * `dividerAfter` (from {@link orderEntries}) counts a rendered group divider's own gap.
181
158
  *
182
- * A fixed-height frame grows to fit its legend; a `fill` frame is pinned to its cell, so the only
183
- * remaining lever is `ChartLegend`'s `maxRows` rollup (an ENTRY cap — see its JSDoc). The rows the
184
- * legend may take follow from the height left over; how many entries that is follows from
185
- * MEASURING the labels, not from assuming one entry per row — otherwise a five-entry legend that
186
- * fits on one line would roll up to `+2 more` in every 240px cell, moving rendering for charts
187
- * that never had the bug. Returns `undefined` when the whole legend already fits.
159
+ * Two passes: every entry visible with no `All N` chip (if that fits, no chip is drawn); otherwise
160
+ * reserve the chip's measured width up front so it can never wrap to a row of its own — and, once
161
+ * reserved, allow the answer to be 0: a chip wide enough to leave no room for even the first dot
162
+ * means the row draws the disclosure alone, not one dot crowding a chip that no longer fits beside it.
163
+ */
164
+ export declare function entriesWithinDotRows(items: readonly LegendEntry[], width: number, rows: number, dividerAfter?: ReadonlySet<number>): number;
165
+ /**
166
+ * The entry cap a `fill` frame's legend must respect so the plot keeps `VX.minPlotHeight`. A
167
+ * `fill` frame is pinned to its cell, so the only lever is `ChartLegend`'s `maxRows` rollup; how
168
+ * many entries fit follows from MEASURING the labels, not assuming one per row. Returns
169
+ * `undefined` when the whole legend already fits.
188
170
  *
189
- * This resolves an ABSENT cap. A caller who stated `legend.maxRows` never reaches here at all —
190
- * `ChartFrame` routes around it — because `Math.min`-ing a stated number against this one made the
191
- * documented escape inert in exactly the narrow `fill` panel it exists for
192
- * ({@link resolveLegendMaxRows}).
171
+ * Resolves an ABSENT cap only: a caller who stated `legend.maxRows` never reaches here
172
+ * ({@link resolveLegendRollup} routes around it), so the documented escape is never made inert.
193
173
  */
194
174
  export declare function legendEntryCap(input: {
195
175
  items: readonly LegendEntry[];
@@ -197,8 +177,8 @@ export declare function legendEntryCap(input: {
197
177
  /** Frame height the legend may consume — `resolvedHeight - VX.minPlotHeight`. */
198
178
  available: number;
199
179
  /**
200
- * The cap that applies when nothing explicit was stated — today only the tier's own default
201
- * ({@link chartTierMetrics}). Two defaults contending: the smaller wins.
180
+ * The cap that applies when nothing explicit was stated — the resolver's width fit. Two
181
+ * defaults contending: the smaller wins.
202
182
  */
203
183
  defaultMaxRows?: number;
204
184
  }): number | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"chart-frame-layout.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-frame-layout.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chart-frame-layout.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-frame-layout.ts"],"names":[],"mappings":"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"}
@@ -1,25 +1,36 @@
1
+ import { HIT_COARSE } from "../../common/hit-floor.js";
1
2
  import { VX } from "../../tokens/index.js";
2
3
  import { measureText } from "../utils/measure-text.js";
3
4
  const LEGEND_LINE_H = Math.ceil(VX.legendFontSize * 1.35);
4
5
  const LEGEND_PAD_Y = 10;
6
+ const LEGEND_ROW_GAP = 6;
5
7
  const LEGEND_SWATCH_W = 24;
6
- function resolveChartTier(containerW) {
7
- return containerW > 0 && containerW < VX.phoneChartWidth ? "phone" : "desktop";
8
+ const LEGEND_DOT_SIZE = 8;
9
+ const DOTS_HIT_GAP = HIT_COARSE - LEGEND_DOT_SIZE - 2 + 4;
10
+ const DOT_PITCH = LEGEND_DOT_SIZE + DOTS_HIT_GAP;
11
+ const GROUP_ORDER = ["series", "overlay", "reference"];
12
+ const roleOf = (item) => item.role ?? "series";
13
+ function orderEntries(items, groups) {
14
+ if (!groups) return { entries: [...items], dividerAfter: /* @__PURE__ */ new Set() };
15
+ const dividerAfter = /* @__PURE__ */ new Set();
16
+ const entries = [];
17
+ const nonEmptyGroups = GROUP_ORDER.map(
18
+ (role) => items.filter((item) => roleOf(item) === role)
19
+ ).filter((group) => group.length > 0);
20
+ nonEmptyGroups.forEach((group, i) => {
21
+ entries.push(...group);
22
+ if (i < nonEmptyGroups.length - 1) dividerAfter.add(entries.length - 1);
23
+ });
24
+ return { entries, dividerAfter };
8
25
  }
9
- const HEIGHT_STEP_WIDTH = { sm: 768, md: 992, lg: 1200 };
10
- const HEIGHT_STEPS = ["lg", "md", "sm"];
11
- function resolveFrameHeight(height, containerW) {
26
+ function resolveFrameHeight(height, containerClass) {
12
27
  if (typeof height === "number") return height;
13
- const widest = HEIGHT_STEPS.find((step2) => height[step2] !== void 0);
14
- if (containerW <= 0) return widest === void 0 ? height.base : height[widest] ?? height.base;
15
- const step = HEIGHT_STEPS.find(
16
- (name) => containerW >= HEIGHT_STEP_WIDTH[name] && height[name] !== void 0
17
- );
18
- return step === void 0 ? height.base : height[step] ?? height.base;
28
+ if (containerClass === "wide") return height.wide ?? height.regular ?? height.base;
29
+ if (containerClass === "regular") return height.regular ?? height.base;
30
+ return height.base;
19
31
  }
20
32
  const PHONE_MARGIN_SCALE = 0.75;
21
33
  const PHONE_DOT_R = Math.max(VX.dotR - 1, 2);
22
- const PHONE_LEGEND_MAX_ROWS = 2;
23
34
  const TOOLTIP_MIN_WIDTH = { desktop: 140, phone: 110 };
24
35
  const DESKTOP_METRICS = {
25
36
  tier: "desktop",
@@ -27,7 +38,6 @@ const DESKTOP_METRICS = {
27
38
  legendFontSize: VX.legendFontSize,
28
39
  dotR: VX.dotR,
29
40
  tooltipMinWidth: TOOLTIP_MIN_WIDTH.desktop,
30
- legendMaxRows: void 0,
31
41
  margin: VX.margin
32
42
  };
33
43
  const PHONE_METRICS = {
@@ -38,7 +48,6 @@ const PHONE_METRICS = {
38
48
  legendFontSize: VX.text.xs,
39
49
  dotR: PHONE_DOT_R,
40
50
  tooltipMinWidth: TOOLTIP_MIN_WIDTH.phone,
41
- legendMaxRows: PHONE_LEGEND_MAX_ROWS,
42
51
  margin: {
43
52
  top: Math.round(VX.margin.top * PHONE_MARGIN_SCALE),
44
53
  right: Math.round(VX.margin.right * PHONE_MARGIN_SCALE),
@@ -46,23 +55,19 @@ const PHONE_METRICS = {
46
55
  left: Math.round(VX.margin.left * PHONE_MARGIN_SCALE)
47
56
  }
48
57
  };
49
- function chartTierMetrics(tier) {
50
- return tier === "phone" ? PHONE_METRICS : DESKTOP_METRICS;
51
- }
52
- function resolveLegendMaxRows(input) {
53
- return input.callerMaxRows ?? chartTierMetrics(input.tier).legendMaxRows;
58
+ function chartMetrics(containerClass) {
59
+ return containerClass === "micro" || containerClass === "compact" ? PHONE_METRICS : DESKTOP_METRICS;
54
60
  }
55
61
  function resolveLegendRollup(input) {
56
- const { statedMaxRows, tier, fillBand, items, containerW, available } = input;
62
+ const { statedMaxRows, fittedMaxRows, fillBand, items, containerW, available } = input;
57
63
  if (statedMaxRows !== void 0) return { maxRows: statedMaxRows, legendWins: fillBand };
58
- const tierMaxRows = resolveLegendMaxRows({ callerMaxRows: void 0, tier });
59
- if (!fillBand) return { maxRows: tierMaxRows, legendWins: false };
64
+ if (!fillBand) return { maxRows: fittedMaxRows, legendWins: false };
60
65
  return {
61
66
  maxRows: legendEntryCap({
62
67
  items,
63
68
  containerW,
64
69
  available,
65
- ...tierMaxRows !== void 0 && { defaultMaxRows: tierMaxRows }
70
+ ...fittedMaxRows !== void 0 && { defaultMaxRows: fittedMaxRows }
66
71
  }),
67
72
  legendWins: false
68
73
  };
@@ -87,18 +92,19 @@ const SELF_MEASURED_SLACK = 0.5;
87
92
  function isSelfMeasured(room) {
88
93
  return room >= 0 && room < SELF_MEASURED_SLACK;
89
94
  }
90
- const legendBandHeight = (rows) => LEGEND_PAD_Y + rows * LEGEND_LINE_H + Math.max(rows - 1, 0) * VX.legendGap;
95
+ const legendBandHeight = (rows) => LEGEND_PAD_Y + rows * LEGEND_LINE_H + Math.max(rows - 1, 0) * LEGEND_ROW_GAP;
91
96
  const legendEntryWidth = (item) => LEGEND_SWATCH_W + measureText(
92
97
  item.note === void 0 ? item.label : `${item.label} ${item.note}`,
93
98
  VX.legendFontSize
94
99
  );
95
- function entriesWithinRows(items, width, rows) {
100
+ function fitChipRows(items, width, rows, allowZero = false) {
96
101
  let row = 1;
97
102
  let x = 0;
98
103
  let fitted = 0;
99
104
  for (const item of items) {
100
105
  const w = legendEntryWidth(item);
101
106
  const next = x === 0 ? w : x + VX.legendGap + w;
107
+ if (x === 0 && allowZero && next > width) return 0;
102
108
  if (next > width && x > 0) {
103
109
  row += 1;
104
110
  if (row > rows) return fitted;
@@ -110,12 +116,48 @@ function entriesWithinRows(items, width, rows) {
110
116
  }
111
117
  return fitted;
112
118
  }
119
+ function entriesWithinRows(items, width, rows) {
120
+ return fitChipRows(items, width, rows);
121
+ }
122
+ function entriesWithinChipRows(items, width, rows) {
123
+ const total = items.length;
124
+ const fittedNoReserve = fitChipRows(items, width, rows);
125
+ if (fittedNoReserve >= total) return fittedNoReserve;
126
+ const reserve = measureText(`All ${total}`, VX.legendFontSize) + VX.legendGap;
127
+ return fitChipRows(items, Math.max(width - reserve, 0), rows, true);
128
+ }
129
+ function fitDotRows(items, width, rows, dividerAfter, allowZero = false) {
130
+ let row = 1;
131
+ let x = 0;
132
+ let fitted = 0;
133
+ for (let i = 0; i < items.length; i += 1) {
134
+ const next = x === 0 ? LEGEND_DOT_SIZE : x + DOT_PITCH;
135
+ if (x === 0 && allowZero && next > width) return 0;
136
+ if (next > width && x > 0) {
137
+ row += 1;
138
+ if (row > rows) return fitted;
139
+ x = LEGEND_DOT_SIZE;
140
+ } else {
141
+ x = next;
142
+ }
143
+ fitted += 1;
144
+ if (dividerAfter.has(i)) x += DOT_PITCH;
145
+ }
146
+ return fitted;
147
+ }
148
+ function entriesWithinDotRows(items, width, rows, dividerAfter = /* @__PURE__ */ new Set()) {
149
+ const total = items.length;
150
+ const fittedNoReserve = fitDotRows(items, width, rows, dividerAfter);
151
+ if (fittedNoReserve >= total) return fittedNoReserve;
152
+ const reserve = measureText(`All ${total}`, VX.legendFontSize) + DOT_PITCH;
153
+ return fitDotRows(items, Math.max(width - reserve, 0), rows, dividerAfter, true);
154
+ }
113
155
  function legendEntryCap(input) {
114
156
  const { items, containerW, available, defaultMaxRows } = input;
115
157
  if (containerW <= 0 || items.length === 0) return defaultMaxRows;
116
158
  const rows = Math.max(
117
159
  1,
118
- Math.floor((available - LEGEND_PAD_Y + VX.legendGap) / (LEGEND_LINE_H + VX.legendGap))
160
+ Math.floor((available - LEGEND_PAD_Y + LEGEND_ROW_GAP) / (LEGEND_LINE_H + LEGEND_ROW_GAP))
119
161
  );
120
162
  if (legendBandHeight(rows) <= available && entriesWithinRows(items, containerW, rows) >= items.length) {
121
163
  return defaultMaxRows;
@@ -124,11 +166,16 @@ function legendEntryCap(input) {
124
166
  return defaultMaxRows === void 0 ? fitted : Math.min(fitted, defaultMaxRows);
125
167
  }
126
168
  export {
127
- chartTierMetrics,
169
+ DOTS_HIT_GAP,
170
+ LEGEND_DOT_SIZE,
171
+ LEGEND_ROW_GAP,
172
+ chartMetrics,
173
+ entriesWithinChipRows,
174
+ entriesWithinDotRows,
175
+ entriesWithinRows,
128
176
  legendEntryCap,
129
- resolveChartTier,
177
+ orderEntries,
130
178
  resolveFrameHeight,
131
- resolveLegendMaxRows,
132
179
  resolveLegendRollup,
133
180
  resolvePlotRect
134
181
  };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `resolveChartLayout` — the single, pure decision point for a chart's space
3
+ * (`docs/CHARTS-SPEC.md` §8): container class, height and legend fit. Also hosts the axis-economy
4
+ * ladder (`resolveYPlacement`, `planXLabels`, `yTickCount`, `compactNumber`, …) that
5
+ * `CartesianChart`/`useBandPlot` call directly. DOM-free and Mantine-free; deliberately not on any
6
+ * public barrel.
7
+ */
8
+ import type { ContainerClass, SIZE_CLASSES } from '../../tokens/size-classes.js';
9
+ import type { ChartMargin } from '../../tokens/index.js';
10
+ import type { LegendEntry } from './ChartLegend.js';
11
+ export type ChartLayout = {
12
+ containerClass: ContainerClass;
13
+ height: number;
14
+ legend: {
15
+ mode: 'none';
16
+ } | {
17
+ mode: 'dots' | 'chips';
18
+ where: 'header' | 'band';
19
+ visible: number;
20
+ overflow: number;
21
+ };
22
+ };
23
+ export type ChartLayoutInput = {
24
+ /** The chart frame's MEASURED width; `<= 0` is unmeasured (SSR, first paint). */
25
+ frameW: number;
26
+ /** Width of the card's header legend slot; `0` when there is none (the legend takes a band). */
27
+ slotW: number;
28
+ /** Viewport height in px (`0` unknown). Only read on a coarse pointer. */
29
+ viewportH: number;
30
+ legendItems: readonly LegendEntry[];
31
+ /** Viewport class — decides the container class until the frame is measured. */
32
+ sizeClass: keyof typeof SIZE_CLASSES;
33
+ coarse: boolean;
34
+ /**
35
+ * The card's own `short` flag (`ChartCard`'s `measuredHeight < CARD_SHORT_HEIGHT`) — the ONE
36
+ * source of truth for "this legend gets at most one row", not a second narrower threshold.
37
+ * Default `false` (no card, or an unmeasured/tall one).
38
+ */
39
+ cardShort?: boolean;
40
+ /** The legend's own `groups` config — whether role dividers render (and so count toward the
41
+ * dots-mode fit). Default `false`. */
42
+ groups?: boolean;
43
+ /** Consumer-stated values; each wins over the derived one. */
44
+ override?: {
45
+ height?: number;
46
+ };
47
+ };
48
+ /** Compact and micro plots are tight: y labels move inside and terminal x labels anchor inward. */
49
+ export declare function isTightClass(containerClass: ContainerClass): boolean;
50
+ /**
51
+ * At a tight container class the y labels move inside the plot (micro: no y axis at all), so the
52
+ * left gutter drops to its floor. Not-tight, no left axis, or an explicit `margin.left` all keep
53
+ * the axis outside — the single implementation `CartesianChart`/`useBandPlot` both call.
54
+ */
55
+ export declare function resolveYPlacement(containerClass: ContainerClass, opts?: {
56
+ hasLeftAxis?: boolean | undefined;
57
+ marginOverrideLeft?: number | undefined;
58
+ }): 'outside' | 'inside' | 'none';
59
+ /**
60
+ * Left-margin floor once `resolveYPlacement` moves the labels INSIDE the plot: just enough for the
61
+ * axis line and a hair of breathing room, not the outside axis's full label-width floor. Passing
62
+ * the tier's own `margin.left` here reserves a gutter for a label that no longer paints.
63
+ */
64
+ export declare const INSIDE_Y_FLOOR = 4;
65
+ /**
66
+ * The `autoMargin` FLOOR to measure against, once `resolveYPlacement` has decided where the left
67
+ * axis paints — an axis placed `'outside'` keeps the tier's ordinary margin floor; one placed
68
+ * `'inside'` (or dropped, `'none'`) paints IN the plot, so the outside label-width floor is dead
69
+ * space on that side and only {@link INSIDE_Y_FLOOR} applies. `CartesianChart` and `useBandPlot`
70
+ * used to each spell this same ternary out by hand.
71
+ */
72
+ export declare function resolveMarginFloor(yPlacement: 'outside' | 'inside' | 'none', tierMargin: ChartMargin): ChartMargin;
73
+ /**
74
+ * Ladder step 1: `12500` -> `12.5k`, `1_200_000` -> `1.2M`; `null` below 1000 or non-finite. Rounds
75
+ * BEFORE choosing the unit's suffix, so `999_950` reads `1M`, never `1000k`; the largest unit caps.
76
+ */
77
+ export declare function compactNumber(value: number): string | null;
78
+ /** Ladder step 1's rule: compact only when the longest y label is over 4 chars. */
79
+ export declare function shouldCompactYLabels(labels: readonly string[]): boolean;
80
+ /** Ladder step 2 + the `VX.minPlotHeight` guard: one tick per ~44px, 2 when the plot is under the floor. */
81
+ export declare function yTickCount(plotHeight: number): number;
82
+ /** Greedy word wrap by measured px; a single word wider than `maxPx` keeps its own line. */
83
+ export declare function wrapLabel(label: string, maxPx: number, fontPx: number): string[];
84
+ export type XLabelPlan = {
85
+ wrap: boolean;
86
+ rotate: 0 | 45;
87
+ /** Lines the tallest label takes once wrapped (1 when it does not). */
88
+ lines: number;
89
+ /** Px one tick label needs: measured width (wrapped width when wrapping) plus the gap. */
90
+ labelPx: number;
91
+ /** Width labels wrap to; `0` when they do not wrap. */
92
+ wrapPx: number;
93
+ };
94
+ /**
95
+ * Ladder step 4: x labels that do not all fit side by side wrap — categorical (band) domains
96
+ * only, and only when wrapping shows EVERY key. A wrap that would still drop keys buys nothing
97
+ * (it costs a line of bottom gutter to thin the same way flat already would), so it falls back to
98
+ * flat/thinned instead. Rotating is the last resort, reached only once even a full wrap can't fit.
99
+ */
100
+ export declare function planXLabels(input: {
101
+ labels: readonly string[];
102
+ plotWidth: number;
103
+ fontPx: number;
104
+ categorical: boolean;
105
+ }): XLabelPlan;
106
+ /** The container class: the measured width's, or the viewport-implied one while unmeasured. */
107
+ export declare function resolveFrameClass(input: {
108
+ frameW: number;
109
+ sizeClass: keyof typeof SIZE_CLASSES;
110
+ }): ContainerClass;
111
+ export declare function resolveChartLayout(input: ChartLayoutInput): ChartLayout;
112
+ //# sourceMappingURL=chart-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chart-layout.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-layout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAQhD,MAAM,MAAM,WAAW,GAAG;IACxB,cAAc,EAAE,cAAc,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EACF;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAChB;QAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAC5F,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAA;IACd,gGAAgG;IAChG,KAAK,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,SAAS,WAAW,EAAE,CAAA;IACnC,gFAAgF;IAChF,SAAS,EAAE,MAAM,OAAO,YAAY,CAAA;IACpC,MAAM,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;0CACsC;IACtC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC/B,CAAA;AAyFD,mGAAmG;AACnG,wBAAgB,YAAY,CAAC,cAAc,EAAE,cAAc,GAAG,OAAO,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,cAAc,EAAE,cAAc,EAC9B,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GACxF,SAAS,GAAG,QAAQ,GAAG,MAAM,CAK/B;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,IAAI,CAAA;AAE/B;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,EACzC,UAAU,EAAE,WAAW,GACtB,WAAW,CAEb;AAWD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU1D;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAEvE;AAED,4GAA4G;AAC5G,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED,4FAA4F;AAC5F,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAehF;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,CAAC,GAAG,EAAE,CAAA;IACd,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IACb,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAA;IACf,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,UAAU,CAyCb;AAED,+FAA+F;AAC/F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,OAAO,YAAY,CAAA;CACrC,GAAG,cAAc,CAIjB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAOvE"}