basalt-ui 1.25.0 → 1.26.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 (462) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +18 -0
  3. package/MIGRATING.md +546 -20
  4. package/README.md +45 -32
  5. package/agent/rules/basalt-batteries.md +205 -0
  6. package/agent/rules/basalt-charts.md +125 -555
  7. package/agent/rules/basalt-controls.md +155 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +113 -96
  10. package/agent/rules/basalt-tokens.md +126 -288
  11. package/agent/skills/basalt-app/SKILL.md +70 -195
  12. package/agent/skills/basalt-charts/SKILL.md +54 -234
  13. package/agent/skills/basalt-design/SKILL.md +56 -248
  14. package/agent/templates/CLAUDE-block.md.tpl +31 -40
  15. package/agent/templates/DESIGN.md.tpl +22 -51
  16. package/configs/oxlint-plugin.js +1275 -31
  17. package/configs/oxlint-plugin.test.ts +947 -11
  18. package/configs/oxlint.json +11 -3
  19. package/dist/charts/cursor/resolve.d.ts +18 -0
  20. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  21. package/dist/charts/cursor/resolve.js +15 -6
  22. package/dist/charts/cursor/scope.d.ts +6 -3
  23. package/dist/charts/cursor/scope.d.ts.map +1 -1
  24. package/dist/charts/cursor/store.d.ts +5 -1
  25. package/dist/charts/cursor/store.d.ts.map +1 -1
  26. package/dist/charts/cursor/store.js +4 -4
  27. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  28. package/dist/charts/hooks/useChartCursor.js +9 -8
  29. package/dist/charts/index.d.ts +2 -2
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/primitives/ChartCard.d.ts +34 -9
  32. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.js +29 -114
  34. package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
  35. package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
  36. package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
  37. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  38. package/dist/charts/sparklines/BarSparkline.js +18 -4
  39. package/dist/cli/check-coverage.test.d.ts +2 -0
  40. package/dist/cli/check-coverage.test.d.ts.map +1 -0
  41. package/dist/cli/index.d.ts +67 -4
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +225 -7
  44. package/dist/connectivity/connectivity-indicator.d.ts +4 -1
  45. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  46. package/dist/connectivity/connectivity-indicator.js +1 -1
  47. package/dist/content/index.d.ts +5 -3
  48. package/dist/content/index.d.ts.map +1 -1
  49. package/dist/content/index.js +4 -2
  50. package/dist/content/prose.module.css +7 -28
  51. package/dist/controls/actions.d.ts +169 -0
  52. package/dist/controls/actions.d.ts.map +1 -0
  53. package/dist/controls/actions.js +287 -0
  54. package/dist/controls/actions.module.css +62 -0
  55. package/dist/controls/actions.module.css.d.ts +13 -0
  56. package/dist/controls/actions.test.d.ts +2 -0
  57. package/dist/controls/actions.test.d.ts.map +1 -0
  58. package/dist/controls/actions.type-guard.test.d.ts +2 -0
  59. package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
  60. package/dist/controls/compare-filter.d.ts +41 -0
  61. package/dist/controls/compare-filter.d.ts.map +1 -0
  62. package/dist/controls/compare-filter.js +23 -0
  63. package/dist/controls/control-group.d.ts +19 -0
  64. package/dist/controls/control-group.d.ts.map +1 -0
  65. package/dist/controls/control-group.js +20 -0
  66. package/dist/controls/control-group.test.d.ts +2 -0
  67. package/dist/controls/control-group.test.d.ts.map +1 -0
  68. package/dist/controls/controls.module.css +542 -0
  69. package/dist/controls/controls.module.css.d.ts +35 -0
  70. package/dist/controls/controls.router.test.d.ts +2 -0
  71. package/dist/controls/controls.router.test.d.ts.map +1 -0
  72. package/dist/controls/enum-filter.d.ts +30 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +91 -0
  75. package/dist/controls/filter-context.d.ts +55 -0
  76. package/dist/controls/filter-context.d.ts.map +1 -0
  77. package/dist/controls/filter-context.js +69 -0
  78. package/dist/controls/filter-pill.d.ts +28 -0
  79. package/dist/controls/filter-pill.d.ts.map +1 -0
  80. package/dist/controls/filter-pill.js +70 -0
  81. package/dist/controls/filter-set.d.ts +13 -0
  82. package/dist/controls/filter-set.d.ts.map +1 -0
  83. package/dist/controls/filter-set.js +125 -0
  84. package/dist/controls/filter-set.test.d.ts +2 -0
  85. package/dist/controls/filter-set.test.d.ts.map +1 -0
  86. package/dist/controls/filter-sheet.d.ts +117 -0
  87. package/dist/controls/filter-sheet.d.ts.map +1 -0
  88. package/dist/controls/filter-sheet.js +146 -0
  89. package/dist/controls/glyphs.d.ts +27 -0
  90. package/dist/controls/glyphs.d.ts.map +1 -0
  91. package/dist/controls/glyphs.js +49 -0
  92. package/dist/controls/index.d.ts +19 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +44 -0
  95. package/dist/controls/multi-select-filter.d.ts +23 -0
  96. package/dist/controls/multi-select-filter.d.ts.map +1 -0
  97. package/dist/controls/multi-select-filter.js +89 -0
  98. package/dist/controls/peer-boundary.test.d.ts +2 -0
  99. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  100. package/dist/controls/props-contract.test.d.ts +2 -0
  101. package/dist/controls/props-contract.test.d.ts.map +1 -0
  102. package/dist/controls/range-filter.d.ts +29 -0
  103. package/dist/controls/range-filter.d.ts.map +1 -0
  104. package/dist/controls/range-filter.js +120 -0
  105. package/dist/controls/search-filter.d.ts +10 -0
  106. package/dist/controls/search-filter.d.ts.map +1 -0
  107. package/dist/controls/search-filter.js +81 -0
  108. package/dist/controls/select-filter.d.ts +74 -0
  109. package/dist/controls/select-filter.d.ts.map +1 -0
  110. package/dist/controls/select-filter.js +10 -0
  111. package/dist/controls/sync-button.d.ts +38 -0
  112. package/dist/controls/sync-button.d.ts.map +1 -0
  113. package/dist/controls/sync-button.js +119 -0
  114. package/dist/controls/sync-button.module.css +63 -0
  115. package/dist/controls/sync-button.module.css.d.ts +11 -0
  116. package/dist/controls/sync-button.test.d.ts +2 -0
  117. package/dist/controls/sync-button.test.d.ts.map +1 -0
  118. package/dist/controls/toggle-filter.d.ts +9 -0
  119. package/dist/controls/toggle-filter.d.ts.map +1 -0
  120. package/dist/controls/toggle-filter.js +42 -0
  121. package/dist/controls/view-tabs.d.ts +23 -0
  122. package/dist/controls/view-tabs.d.ts.map +1 -0
  123. package/dist/controls/view-tabs.js +59 -0
  124. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  125. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  126. package/dist/controls-dates/date-range-picker.js +18 -0
  127. package/dist/controls-dates/index.d.ts +9 -0
  128. package/dist/controls-dates/index.d.ts.map +1 -0
  129. package/dist/controls-dates/index.js +4 -0
  130. package/dist/dashboard/delta-badge.d.ts +6 -12
  131. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  132. package/dist/dashboard/delta-badge.js +1 -50
  133. package/dist/dashboard/settings-section.d.ts +6 -4
  134. package/dist/dashboard/settings-section.d.ts.map +1 -1
  135. package/dist/dashboard/settings-section.js +23 -16
  136. package/dist/dashboard/settings-section.module.css +10 -0
  137. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  138. package/dist/dashboard/settings-section.test.d.ts +2 -0
  139. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  140. package/dist/dashboard/stat-card.d.ts +44 -9
  141. package/dist/dashboard/stat-card.d.ts.map +1 -1
  142. package/dist/dashboard/stat-card.js +80 -48
  143. package/dist/dashboard/stat-card.module.css +82 -0
  144. package/dist/dashboard/stat-card.module.css.d.ts +11 -0
  145. package/dist/data/data-table.d.ts +37 -15
  146. package/dist/data/data-table.d.ts.map +1 -1
  147. package/dist/data/data-table.js +88 -58
  148. package/dist/guard/grace.test.d.ts +20 -0
  149. package/dist/guard/grace.test.d.ts.map +1 -0
  150. package/dist/guard/index.d.ts +66 -3
  151. package/dist/guard/index.d.ts.map +1 -1
  152. package/dist/guard/index.js +82 -7
  153. package/dist/guard/types.d.ts +3 -3
  154. package/dist/guard/types.d.ts.map +1 -1
  155. package/dist/index.d.ts +12 -2
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +13 -10
  158. package/dist/nav/types.d.ts +78 -2
  159. package/dist/nav/types.d.ts.map +1 -1
  160. package/dist/notifications/bell.d.ts +4 -1
  161. package/dist/notifications/bell.d.ts.map +1 -1
  162. package/dist/notifications/index.d.ts +1 -1
  163. package/dist/router-tanstack/field.d.ts +14 -0
  164. package/dist/router-tanstack/field.d.ts.map +1 -0
  165. package/dist/router-tanstack/field.js +4 -0
  166. package/dist/router-tanstack/index.d.ts +6 -2
  167. package/dist/router-tanstack/index.d.ts.map +1 -1
  168. package/dist/router-tanstack/index.js +4 -0
  169. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  170. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  171. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  172. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  173. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  174. package/dist/router-tanstack/search-param-store.js +16 -45
  175. package/dist/router-tanstack/search-store.d.ts +81 -0
  176. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  177. package/dist/router-tanstack/search-store.js +187 -0
  178. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  179. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  180. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  181. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  182. package/dist/router-tanstack/use-nav.d.ts +0 -2
  183. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  184. package/dist/router-tanstack/use-nav.js +34 -12
  185. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  186. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  187. package/dist/section/index.d.ts +2 -0
  188. package/dist/section/index.d.ts.map +1 -0
  189. package/dist/section/index.js +4 -0
  190. package/dist/section/section.d.ts +24 -0
  191. package/dist/section/section.d.ts.map +1 -0
  192. package/dist/section/section.js +126 -0
  193. package/dist/section/section.module.css +104 -0
  194. package/dist/section/section.module.css.d.ts +11 -0
  195. package/dist/section/section.test.d.ts +2 -0
  196. package/dist/section/section.test.d.ts.map +1 -0
  197. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  198. package/dist/shell/app-breadcrumbs.js +18 -15
  199. package/dist/shell/app-header.module.css +77 -37
  200. package/dist/shell/app-header.module.css.d.ts +2 -1
  201. package/dist/shell/app-mobile-nav.d.ts +9 -2
  202. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  203. package/dist/shell/app-mobile-nav.js +98 -11
  204. package/dist/shell/app-mobile-nav.module.css +8 -0
  205. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  206. package/dist/shell/app-sidebar.d.ts +55 -28
  207. package/dist/shell/app-sidebar.d.ts.map +1 -1
  208. package/dist/shell/app-sidebar.js +116 -67
  209. package/dist/shell/app-sidebar.module.css +303 -20
  210. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  211. package/dist/shell/index.d.ts +55 -21
  212. package/dist/shell/index.d.ts.map +1 -1
  213. package/dist/shell/index.js +40 -18
  214. package/dist/shell/mobile-nav-model.d.ts +13 -2
  215. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  216. package/dist/shell/mobile-nav-model.js +15 -1
  217. package/dist/shell/nav-count-badge.d.ts +1 -1
  218. package/dist/shell/page-bar.d.ts +51 -0
  219. package/dist/shell/page-bar.d.ts.map +1 -0
  220. package/dist/shell/page-bar.js +154 -0
  221. package/dist/shell/page-bar.module.css +178 -0
  222. package/dist/shell/page-bar.module.css.d.ts +17 -0
  223. package/dist/shell/page-bar.test.d.ts +2 -0
  224. package/dist/shell/page-bar.test.d.ts.map +1 -0
  225. package/dist/shell/sidebar-block-model.d.ts +48 -0
  226. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  227. package/dist/shell/sidebar-block-model.js +39 -0
  228. package/dist/shell/sidebar-blocks.d.ts +32 -0
  229. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  230. package/dist/shell/sidebar-blocks.js +169 -0
  231. package/dist/shell/sidebar-search.d.ts +13 -1
  232. package/dist/shell/sidebar-search.d.ts.map +1 -1
  233. package/dist/shell/sidebar-search.js +34 -1
  234. package/dist/shell/sidebar-search.module.css +26 -0
  235. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  236. package/dist/state/fields.d.ts +392 -0
  237. package/dist/state/fields.d.ts.map +1 -0
  238. package/dist/state/fields.js +369 -0
  239. package/dist/state/fields.test.d.ts +2 -0
  240. package/dist/state/fields.test.d.ts.map +1 -0
  241. package/dist/state/persisted.d.ts +48 -0
  242. package/dist/state/persisted.d.ts.map +1 -0
  243. package/dist/state/persisted.js +105 -0
  244. package/dist/state.d.ts +11 -56
  245. package/dist/state.d.ts.map +1 -1
  246. package/dist/state.js +14 -120
  247. package/dist/styles.css +13 -0
  248. package/dist/surfaces.d.ts +106 -24
  249. package/dist/surfaces.d.ts.map +1 -1
  250. package/dist/surfaces.js +155 -19
  251. package/dist/theme/controls.module.css +16 -1
  252. package/dist/theme/ctl-theme.d.ts +32 -0
  253. package/dist/theme/ctl-theme.d.ts.map +1 -0
  254. package/dist/theme/ctl-theme.js +54 -0
  255. package/dist/theme/ctl-theme.test.d.ts +2 -0
  256. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  257. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  258. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  259. package/dist/theme/divider-law.test.d.ts +2 -0
  260. package/dist/theme/divider-law.test.d.ts.map +1 -0
  261. package/dist/theme/icon-slot.d.ts +44 -0
  262. package/dist/theme/icon-slot.d.ts.map +1 -0
  263. package/dist/theme/icon-slot.js +21 -0
  264. package/dist/theme/icon-slot.module.css +47 -0
  265. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  266. package/dist/theme/icon-slot.test.d.ts +2 -0
  267. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  268. package/dist/theme/index.d.ts +1 -0
  269. package/dist/theme/index.d.ts.map +1 -1
  270. package/dist/theme/index.js +82 -5
  271. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  272. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  273. package/dist/theme/segmented-control.module.css +20 -0
  274. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  275. package/dist/tokens/index.d.ts +12 -0
  276. package/dist/tokens/index.d.ts.map +1 -1
  277. package/dist/tokens/index.js +38 -8
  278. package/dist/tokens/palette.d.ts +97 -61
  279. package/dist/tokens/palette.d.ts.map +1 -1
  280. package/dist/tokens/palette.js +76 -38
  281. package/dist/tokens.css +10 -2
  282. package/dist/widget-header/delta-badge.d.ts +13 -0
  283. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  284. package/dist/widget-header/delta-badge.js +29 -0
  285. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  286. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  287. package/dist/widget-header/index.d.ts +3 -0
  288. package/dist/widget-header/index.d.ts.map +1 -0
  289. package/dist/widget-header/index.js +6 -0
  290. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  291. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  292. package/dist/widget-header/widget-header.d.ts +37 -0
  293. package/dist/widget-header/widget-header.d.ts.map +1 -0
  294. package/dist/widget-header/widget-header.js +77 -0
  295. package/dist/widget-header/widget-header.module.css +335 -0
  296. package/dist/widget-header/widget-header.module.css.d.ts +24 -0
  297. package/dist/widget-header/widget-header.test.d.ts +2 -0
  298. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  299. package/llms.txt +30 -17
  300. package/package.json +14 -1
  301. package/src/charts/cursor/resolve.test.ts +29 -1
  302. package/src/charts/cursor/resolve.ts +38 -6
  303. package/src/charts/cursor/scope.tsx +6 -3
  304. package/src/charts/cursor/store.test.ts +19 -1
  305. package/src/charts/cursor/store.ts +10 -5
  306. package/src/charts/hooks/useChartCursor.ts +16 -8
  307. package/src/charts/index.ts +2 -2
  308. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  309. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  310. package/src/charts/primitives/ChartCard.tsx +86 -146
  311. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  312. package/src/cli/check-coverage.test.ts +297 -0
  313. package/src/cli/check-theme.test.ts +38 -14
  314. package/src/cli/index.ts +392 -16
  315. package/src/cli/init-sync.test.ts +97 -0
  316. package/src/connectivity/connectivity-indicator.tsx +16 -9
  317. package/src/content/index.ts +16 -3
  318. package/src/content/prose.module.css +7 -28
  319. package/src/content/prose.module.css.test.ts +8 -24
  320. package/src/controls/actions.module.css +62 -0
  321. package/src/controls/actions.module.css.d.ts +13 -0
  322. package/src/controls/actions.test.tsx +252 -0
  323. package/src/controls/actions.tsx +655 -0
  324. package/src/controls/actions.type-guard.test.ts +63 -0
  325. package/src/controls/compare-filter.tsx +64 -0
  326. package/src/controls/control-group.test.tsx +229 -0
  327. package/src/controls/control-group.tsx +63 -0
  328. package/src/controls/controls.module.css +542 -0
  329. package/src/controls/controls.module.css.d.ts +35 -0
  330. package/src/controls/controls.router.test.tsx +637 -0
  331. package/src/controls/enum-filter.tsx +159 -0
  332. package/src/controls/filter-context.tsx +153 -0
  333. package/src/controls/filter-pill.tsx +120 -0
  334. package/src/controls/filter-set.test.tsx +472 -0
  335. package/src/controls/filter-set.tsx +225 -0
  336. package/src/controls/filter-sheet.tsx +285 -0
  337. package/src/controls/glyphs.tsx +81 -0
  338. package/src/controls/index.ts +48 -0
  339. package/src/controls/multi-select-filter.tsx +140 -0
  340. package/src/controls/peer-boundary.test.ts +85 -0
  341. package/src/controls/props-contract.test.tsx +106 -0
  342. package/src/controls/range-filter.tsx +184 -0
  343. package/src/controls/search-filter.tsx +123 -0
  344. package/src/controls/select-filter.tsx +89 -0
  345. package/src/controls/sync-button.module.css +63 -0
  346. package/src/controls/sync-button.module.css.d.ts +11 -0
  347. package/src/controls/sync-button.test.tsx +183 -0
  348. package/src/controls/sync-button.tsx +197 -0
  349. package/src/controls/toggle-filter.tsx +60 -0
  350. package/src/controls/view-tabs.tsx +110 -0
  351. package/src/controls-dates/date-range-picker.tsx +42 -0
  352. package/src/controls-dates/index.ts +8 -0
  353. package/src/dashboard/delta-badge.tsx +4 -74
  354. package/src/dashboard/settings-section.module.css +10 -0
  355. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  356. package/src/dashboard/settings-section.test.tsx +56 -0
  357. package/src/dashboard/settings-section.tsx +33 -30
  358. package/src/dashboard/stat-card.module.css +82 -0
  359. package/src/dashboard/stat-card.module.css.d.ts +11 -0
  360. package/src/dashboard/stat-card.test.tsx +67 -1
  361. package/src/dashboard/stat-card.tsx +209 -68
  362. package/src/data/data-table.test.tsx +96 -11
  363. package/src/data/data-table.tsx +220 -84
  364. package/src/guard/check-source.test.ts +126 -13
  365. package/src/guard/grace.test.ts +149 -0
  366. package/src/guard/index.ts +172 -29
  367. package/src/guard/types.ts +4 -2
  368. package/src/index.ts +46 -5
  369. package/src/nav/types.ts +84 -2
  370. package/src/notifications/bell.tsx +6 -3
  371. package/src/notifications/index.ts +1 -1
  372. package/src/router-tanstack/field.ts +31 -0
  373. package/src/router-tanstack/index.ts +24 -2
  374. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  375. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  376. package/src/router-tanstack/search-param-store.ts +42 -159
  377. package/src/router-tanstack/search-store.router.test.tsx +661 -0
  378. package/src/router-tanstack/search-store.test.ts +418 -0
  379. package/src/router-tanstack/search-store.ts +380 -0
  380. package/src/router-tanstack/use-nav.test.tsx +187 -0
  381. package/src/router-tanstack/use-nav.ts +72 -14
  382. package/src/section/index.ts +1 -0
  383. package/src/section/section.module.css +104 -0
  384. package/src/section/section.module.css.d.ts +11 -0
  385. package/src/section/section.test.tsx +204 -0
  386. package/src/section/section.tsx +220 -0
  387. package/src/shell/app-breadcrumbs.tsx +44 -31
  388. package/src/shell/app-header.module.css +77 -37
  389. package/src/shell/app-header.module.css.d.ts +2 -1
  390. package/src/shell/app-mobile-nav.module.css +8 -0
  391. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  392. package/src/shell/app-mobile-nav.tsx +147 -8
  393. package/src/shell/app-sidebar-account.tsx +2 -2
  394. package/src/shell/app-sidebar.module.css +303 -20
  395. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  396. package/src/shell/app-sidebar.test.tsx +456 -41
  397. package/src/shell/app-sidebar.tsx +276 -140
  398. package/src/shell/index.test.tsx +291 -5
  399. package/src/shell/index.tsx +137 -38
  400. package/src/shell/mobile-nav-model.test.ts +228 -7
  401. package/src/shell/mobile-nav-model.ts +53 -3
  402. package/src/shell/nav-count-badge.tsx +1 -1
  403. package/src/shell/page-bar.module.css +178 -0
  404. package/src/shell/page-bar.module.css.d.ts +17 -0
  405. package/src/shell/page-bar.test.tsx +316 -0
  406. package/src/shell/page-bar.tsx +315 -0
  407. package/src/shell/sidebar-block-model.ts +83 -0
  408. package/src/shell/sidebar-blocks.tsx +301 -0
  409. package/src/shell/sidebar-search.module.css +26 -0
  410. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  411. package/src/shell/sidebar-search.tsx +71 -2
  412. package/src/state/fields.test.tsx +258 -0
  413. package/src/state/fields.ts +892 -0
  414. package/src/state/persisted.ts +211 -0
  415. package/src/state.ts +46 -240
  416. package/src/styles.css +13 -0
  417. package/src/surfaces.ts +203 -36
  418. package/src/theme/controls.module.css +16 -1
  419. package/src/theme/ctl-theme.test.tsx +275 -0
  420. package/src/theme/ctl-theme.tsx +138 -0
  421. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  422. package/src/theme/density-relations.test.ts +106 -51
  423. package/src/theme/divider-law.test.ts +259 -0
  424. package/src/theme/icon-slot.module.css +47 -0
  425. package/src/theme/icon-slot.module.css.d.ts +8 -0
  426. package/src/theme/icon-slot.test.tsx +275 -0
  427. package/src/theme/icon-slot.tsx +59 -0
  428. package/src/theme/index.ts +165 -3
  429. package/src/theme/layout-rhythm-css.test.ts +72 -0
  430. package/src/theme/radius.test.ts +11 -1
  431. package/src/theme/segmented-control.module.css +20 -0
  432. package/src/theme/shadow-surfaces.test.ts +20 -7
  433. package/src/theme/spacing.test.ts +75 -27
  434. package/src/theme/use-basalt-spacing.ts +2 -2
  435. package/src/tokens/density.test.ts +7 -25
  436. package/src/tokens/index.ts +48 -8
  437. package/src/tokens/palette.ts +107 -73
  438. package/src/widget-header/delta-badge.test.tsx +60 -0
  439. package/src/widget-header/delta-badge.tsx +57 -0
  440. package/src/widget-header/index.ts +2 -0
  441. package/src/widget-header/no-mantine.test.ts +33 -0
  442. package/src/widget-header/widget-header.module.css +335 -0
  443. package/src/widget-header/widget-header.module.css.d.ts +24 -0
  444. package/src/widget-header/widget-header.test.tsx +187 -0
  445. package/src/widget-header/widget-header.tsx +168 -0
  446. package/agent/rules/basalt-agent.md +0 -452
  447. package/agent/rules/basalt-app.md +0 -246
  448. package/agent/rules/basalt-commands.md +0 -320
  449. package/agent/rules/basalt-content.md +0 -409
  450. package/agent/rules/basalt-data.md +0 -329
  451. package/agent/rules/basalt-forms.md +0 -186
  452. package/agent/rules/basalt-notifications.md +0 -264
  453. package/agent/rules/basalt-query.md +0 -187
  454. package/agent/rules/basalt-router.md +0 -336
  455. package/dist/content/article-filter-bar.d.ts +0 -40
  456. package/dist/content/article-filter-bar.d.ts.map +0 -1
  457. package/dist/content/article-filter-bar.js +0 -56
  458. package/dist/shell/page-header.d.ts +0 -13
  459. package/dist/shell/page-header.d.ts.map +0 -1
  460. package/dist/shell/page-header.js +0 -23
  461. package/src/content/article-filter-bar.tsx +0 -88
  462. package/src/shell/page-header.tsx +0 -35
@@ -1,76 +1,6 @@
1
1
  /**
2
- * DeltaBadge — the KPI trend chip (docs/DESIGN-SPEC.md §5): mono 11.5px weight 600, status-color
3
- * text on a status-13% tint, radius 6, 2px/7px padding, optional ▲/▼ glyph at 9px. Sign alone
4
- * drives the tone — positive reads as success, negative as danger, zero as a neutral faint tint
5
- * with no glyph. The label never carries a leading "+"; pass `format` to control the magnitude
6
- * string (defaults to a one-decimal percentage of the absolute value).
7
- *
8
- * @example
9
- * import { DeltaBadge } from 'basalt-ui'
10
- *
11
- * <DeltaBadge value={12.4} /> // ▲ 12.4%, success tint
12
- * <DeltaBadge value={-3.1} /> // ▼ 3.1%, danger tint
13
- * <DeltaBadge value={0} /> // 0.0%, neutral faint tint, no glyph
14
- * <DeltaBadge value={182} format={(v) => `${Math.abs(v)}ms`} withGlyph={false} />
2
+ * `DeltaBadge` moved to `src/widget-header/` (docs/CONTROLS-SPEC.md §2.2) so `WidgetHeader` can
3
+ * compose it Mantine-free. Re-exported here unchanged so this deep import path — and `StatCard`'s
4
+ * own `./delta-badge` import — keep working without a wave-3 edit.
15
5
  */
16
- import { Badge } from '@mantine/core'
17
- import { alpha, VX } from '../tokens'
18
-
19
- export type DeltaBadgeProps = {
20
- /** Signed delta driving tone (positive/negative/zero) and, by default, the label magnitude. */
21
- value: number
22
- /** Formats the value into the label text. Defaults to `${Math.abs(value).toFixed(1)}%`. */
23
- format?: (value: number) => string
24
- /** Render the ▲/▼ direction glyph. Defaults to `true`; a zero value never shows a glyph. */
25
- withGlyph?: boolean
26
- /** Optional comparison timeframe rendered directly after the value in a dimmer shade of the same
27
- * tone (e.g. `MoM`, `WoW`, `YTD`) — states what the delta is measured against without a hover. */
28
- period?: string | undefined
29
- }
30
-
31
- const defaultFormat = (value: number): string => `${Math.abs(value).toFixed(1)}%`
32
-
33
- export function DeltaBadge({
34
- value,
35
- format = defaultFormat,
36
- withGlyph = true,
37
- period,
38
- }: DeltaBadgeProps) {
39
- const tone = value > 0 ? VX.status.good : value < 0 ? VX.status.bad : VX.faint
40
- const glyph = value > 0 ? '▲' : value < 0 ? '▼' : undefined
41
- const showGlyph = withGlyph && glyph !== undefined
42
-
43
- return (
44
- <Badge
45
- styles={{
46
- root: {
47
- backgroundColor: alpha(tone, 0.13),
48
- padding: 'var(--vx-space-badge-inset-y) var(--vx-space-badge-inset-x)',
49
- height: 'auto',
50
- },
51
- label: {
52
- display: 'inline-flex', // theme-allow inline-display — a Badge `styles.label` part, not a layout node Flex could replace
53
- alignItems: 'center',
54
- gap: 3, // theme-allow inline-spacing — sub-scale glyph-to-label gap, below the smallest scale stop
55
- fontFamily: 'var(--basalt-font-mono)',
56
- fontSize: VX.text.xs,
57
- fontWeight: 600,
58
- textTransform: 'none',
59
- letterSpacing: 'normal',
60
- color: tone,
61
- },
62
- }}
63
- >
64
- {/* optical glyph ratio relative to the label, not a scale step */}
65
- {showGlyph && <span style={{ fontSize: '0.8em' }}>{glyph}</span>}
66
- {format(value)}
67
- {period && (
68
- <span
69
- style={{ marginLeft: 4, fontWeight: 500, opacity: 0.6 }} // theme-allow inline-spacing — sub-scale optical period offset, below the smallest scale stop
70
- >
71
- {period}
72
- </span>
73
- )}
74
- </Badge>
75
- )
76
- }
6
+ export { DeltaBadge, type DeltaBadgeProps } from '../widget-header/delta-badge'
@@ -3,6 +3,16 @@
3
3
  * vertical padding and a 1px `--vx-divider` rule below it, EXCEPT the last row in the section —
4
4
  * `:not(:last-child)` strips the divider there so the section doesn't end on a trailing line.
5
5
  */
6
+
7
+ /* The WidgetHeader block (plus DangerZone's eyebrow, when present) sits above `.rows` with the
8
+ * same gap the old inline `Stack gap={2} mb="sm"` produced. */
9
+ .header {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: 2px;
13
+ margin-bottom: var(--mantine-spacing-sm);
14
+ }
15
+
6
16
  .rows > * {
7
17
  padding: var(--vx-space-settings-row-inset-y) 0;
8
18
  }
@@ -3,6 +3,7 @@
3
3
  * signature — so dot access stays verbatim under the package's strict flags.
4
4
  */
5
5
  declare const classes: {
6
+ readonly header: string
6
7
  readonly rows: string
7
8
  readonly row: string
8
9
  readonly control: string
@@ -0,0 +1,56 @@
1
+ /**
2
+ * SettingsSection / DangerZone — the WidgetHeader composition (docs/CONTROLS-SPEC.md §2.2):
3
+ * `description` renamed to `subtitle`, both now render an h2 (`tier="section"`) via `WidgetHeader`,
4
+ * and `actions` reaches the header's actions slot.
5
+ */
6
+ import { MantineProvider } from '@mantine/core'
7
+ import { render, screen } from '@testing-library/react'
8
+ import { describe, expect, test } from 'bun:test'
9
+ import type { ReactElement } from 'react'
10
+ import { DangerZone, SettingsSection } from './settings-section'
11
+
12
+ function renderWith(node: ReactElement) {
13
+ return render(<MantineProvider>{node}</MantineProvider>)
14
+ }
15
+
16
+ describe('SettingsSection composes WidgetHeader at the section tier', () => {
17
+ test('renders an h2 carrying the title', () => {
18
+ renderWith(
19
+ <SettingsSection title="Profile">
20
+ <div>rows</div>
21
+ </SettingsSection>,
22
+ )
23
+ expect(screen.getByRole('heading', { level: 2, name: 'Profile' })).toBeDefined()
24
+ })
25
+
26
+ test('subtitle (renamed from description) renders below the title', () => {
27
+ renderWith(
28
+ <SettingsSection title="Profile" subtitle="Your public identity.">
29
+ <div>rows</div>
30
+ </SettingsSection>,
31
+ )
32
+ expect(screen.getByText('Your public identity.')).toBeDefined()
33
+ })
34
+
35
+ test('actions reach the header actions slot', () => {
36
+ renderWith(
37
+ <SettingsSection title="Profile" actions={<button type="button">Export</button>}>
38
+ <div>rows</div>
39
+ </SettingsSection>,
40
+ )
41
+ expect(screen.getByRole('button', { name: 'Export' })).toBeDefined()
42
+ })
43
+ })
44
+
45
+ describe('DangerZone keeps its eyebrow alongside the WidgetHeader', () => {
46
+ test('renders the eyebrow and an h2 title', () => {
47
+ renderWith(
48
+ <DangerZone title="Delete workspace" subtitle="This action cannot be undone.">
49
+ <div>rows</div>
50
+ </DangerZone>,
51
+ )
52
+ expect(screen.getByText('Danger Zone')).toBeDefined()
53
+ expect(screen.getByRole('heading', { level: 2, name: 'Delete workspace' })).toBeDefined()
54
+ expect(screen.getByText('This action cannot be undone.')).toBeDefined()
55
+ })
56
+ })
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * SettingsSection / SettingsRow / DangerZone — the settings-page building blocks
3
- * (docs/DESIGN-SPEC.md §5). A `SettingsSection` is a titled card grouping a stack of
4
- * `SettingsRow`s, each separated by a 1px `--vx-divider` rule (no border on the last row —
5
- * handled by the shared `.rows` container in `settings-section.module.css`). `DangerZone` is a
6
- * `SettingsSection` variant for irreversible actions: a mono "DANGER ZONE" eyebrow in
3
+ * (docs/DESIGN-SPEC.md §5, docs/CONTROLS-SPEC.md §2.2). A `SettingsSection` is a titled card
4
+ * grouping a stack of `SettingsRow`s, each separated by a 1px `--vx-divider` rule (no border on the
5
+ * last row — handled by the shared `.rows` container in `settings-section.module.css`). `DangerZone`
6
+ * is a `SettingsSection` variant for irreversible actions: a mono "DANGER ZONE" eyebrow in
7
7
  * status-danger, and a danger-tinted ring layered atop the card's shadow-card depth.
8
8
  *
9
+ * Both compose `WidgetHeader tier="section"` for the title/subtitle/actions row — `actions` is
10
+ * wrapped in `CtlSlot` (C1/C5).
11
+ *
9
12
  * @example
10
13
  * import { SettingsSection, SettingsRow, DangerZone } from 'basalt-ui'
11
14
  *
12
- * <SettingsSection title="Profile" description="Your public identity.">
15
+ * <SettingsSection title="Profile" subtitle="Your public identity.">
13
16
  * <SettingsRow label="Display name" control={<TextInput value={name} onChange={...} />} />
14
17
  * <SettingsRow
15
18
  * label="Email"
@@ -18,7 +21,7 @@
18
21
  * />
19
22
  * </SettingsSection>
20
23
  *
21
- * <DangerZone title="Delete workspace" description="This action cannot be undone.">
24
+ * <DangerZone title="Delete workspace" subtitle="This action cannot be undone.">
22
25
  * <SettingsRow
23
26
  * label="Delete this workspace"
24
27
  * control={<Button color="red" variant="outline">Delete</Button>}
@@ -27,17 +30,11 @@
27
30
  */
28
31
  import { Card, Stack } from '@mantine/core'
29
32
  import type { ReactNode } from 'react'
33
+ import { WidgetHeader } from '../widget-header'
34
+ import { CtlSlot } from '../theme'
30
35
  import { alpha, VX } from '../tokens'
31
36
  import classes from './settings-section.module.css'
32
37
 
33
- const titleStyle = {
34
- fontFamily: 'var(--basalt-font-head)',
35
- fontStretch: '88%' as const,
36
- fontSize: VX.text.md,
37
- fontWeight: 550,
38
- color: VX.ink,
39
- }
40
-
41
38
  const eyebrowStyle = {
42
39
  fontFamily: 'var(--basalt-font-mono)',
43
40
  fontSize: VX.text.micro,
@@ -48,23 +45,27 @@ const eyebrowStyle = {
48
45
  }
49
46
 
50
47
  export type SettingsSectionProps = {
51
- /** Head-font section title (15px, weight 550, ink). */
48
+ /** Head-font section title, rendered via `WidgetHeader tier="section"`. */
52
49
  title: string
53
50
  /** Optional 13px muted description below the title. */
54
- description?: string
51
+ subtitle?: string
52
+ /** Header-right slot — wrapped in `CtlSlot` (C1/C5). */
53
+ actions?: ReactNode
55
54
  /** Section body — typically a stack of `SettingsRow`s. */
56
55
  children: ReactNode
57
56
  }
58
57
 
59
- export function SettingsSection({ title, description, children }: SettingsSectionProps) {
58
+ export function SettingsSection({ title, subtitle, actions, children }: SettingsSectionProps) {
60
59
  return (
61
60
  <Card style={{ padding: 'var(--mantine-spacing-xs) var(--mantine-spacing-sm)' }}>
62
- <Stack gap={2} mb="sm">
63
- <span style={titleStyle}>{title}</span>
64
- {description && (
65
- <span style={{ fontSize: VX.text.sm, color: VX.muted }}>{description}</span>
66
- )}
67
- </Stack>
61
+ <div className={classes.header}>
62
+ <WidgetHeader
63
+ tier="section"
64
+ title={title}
65
+ {...(subtitle !== undefined && { subtitle })}
66
+ {...(actions !== undefined && { actions: <CtlSlot>{actions}</CtlSlot> })}
67
+ />
68
+ </div>
68
69
  <div className={classes.rows}>{children}</div>
69
70
  </Card>
70
71
  )
@@ -97,7 +98,7 @@ export function SettingsRow({ label, description, control, children }: SettingsR
97
98
 
98
99
  export type DangerZoneProps = SettingsSectionProps
99
100
 
100
- export function DangerZone({ title, description, children }: DangerZoneProps) {
101
+ export function DangerZone({ title, subtitle, actions, children }: DangerZoneProps) {
101
102
  return (
102
103
  <Card
103
104
  style={{
@@ -105,13 +106,15 @@ export function DangerZone({ title, description, children }: DangerZoneProps) {
105
106
  boxShadow: `${VX.shadowCard}, 0 0 0 1px ${alpha(VX.status.bad, 0.25)}`,
106
107
  }}
107
108
  >
108
- <Stack gap={2} mb="sm">
109
+ <div className={classes.header}>
109
110
  <span style={eyebrowStyle}>Danger Zone</span>
110
- <span style={titleStyle}>{title}</span>
111
- {description && (
112
- <span style={{ fontSize: VX.text.sm, color: VX.muted }}>{description}</span>
113
- )}
114
- </Stack>
111
+ <WidgetHeader
112
+ tier="section"
113
+ title={title}
114
+ {...(subtitle !== undefined && { subtitle })}
115
+ {...(actions !== undefined && { actions: <CtlSlot>{actions}</CtlSlot> })}
116
+ />
117
+ </div>
115
118
  <div className={classes.rows}>{children}</div>
116
119
  </Card>
117
120
  )
@@ -0,0 +1,82 @@
1
+ /*
2
+ * StatCard's sparkline placement (docs/CONTROLS-SPEC.md §2.2). `bleed` (default) keeps the historic
3
+ * full-width row beneath the hero value, bled past the card's own inset padding — same negative
4
+ * margins the pre-WidgetHeader version drew inline. `right` sits the sparkline beside the
5
+ * `WidgetHeader`'s hero-value row instead, the reference-design look, and collapses back to
6
+ * `bleed`'s stacked layout below `sm` — CSS only, no JS branch (`47.99375em` is Mantine's own
7
+ * complement of `min-width: 48em`, same value `content/prose.module.css` derives it from).
8
+ */
9
+
10
+ .body {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: var(--vx-space-stat-card-gap);
14
+ height: 100%;
15
+ }
16
+
17
+ /*
18
+ * `flex-end`, not `center`: the sparkline belongs to the HERO ROW, and the hero row is the header
19
+ * block's last line. Centred against the whole body it floated between the title and the value,
20
+ * which reads as a third, unrelated element; bottom-aligned to the header block it sits beside the
21
+ * number it describes, the reference layout. `gap` is the card's own control gap so the trend never
22
+ * touches a long value.
23
+ */
24
+ .body[data-placement='right'] {
25
+ flex-direction: row;
26
+ align-items: flex-end;
27
+ gap: var(--vx-space-control-gap);
28
+ }
29
+
30
+ .header {
31
+ min-width: 0;
32
+ }
33
+
34
+ /*
35
+ * `flex-shrink: 0` on the header, `1` on the sparkline slot below: the hero value and its delta keep
36
+ * their width and the TREND gives ground. The other way round, a 4-up KPI grid on a 1440 viewport
37
+ * ellipsized `$1,294,900` to `$1294.…` in order to keep 72px of bars intact, which is the wrong
38
+ * thing to protect. The value still carries `overflow: hidden` as the last resort, and `Card` clips.
39
+ */
40
+ .body[data-placement='right'] .header {
41
+ flex: 1 0 auto;
42
+ min-width: 0;
43
+ }
44
+
45
+ .sparklineBleed {
46
+ margin-top: auto;
47
+ margin-inline: calc(-1 * var(--mantine-spacing-sm));
48
+ margin-bottom: calc(-1 * var(--mantine-spacing-xs));
49
+ }
50
+
51
+ /* Asks for `StatCard`'s `SPARKLINE_RIGHT_WIDTH`/`_HEIGHT` (handed down as custom properties, so this
52
+ * rule and the `< sm` reset below can both reach them) and shrinks below the width before the hero
53
+ * row does — see the `.header` note above. `min-width: 0` is what lets it. */
54
+ .sparklineRight {
55
+ display: flex;
56
+ flex: 0 1 var(--basalt-stat-sparkline-w, 4.5rem);
57
+ min-width: 0;
58
+ height: var(--basalt-stat-sparkline-h, 1.625rem);
59
+ align-items: flex-end;
60
+ overflow: hidden;
61
+ }
62
+
63
+ @media (max-width: 47.99375em) {
64
+ .body[data-placement='right'] {
65
+ flex-direction: column;
66
+ align-items: stretch;
67
+ }
68
+
69
+ .body[data-placement='right'] .header {
70
+ flex: initial;
71
+ }
72
+
73
+ /* The row became a COLUMN, so `flex-basis` is now the main-axis HEIGHT — the 72px width would
74
+ * become a 72px-tall box around 26px of bars. `flex: none` + the height keeps the bled trend the
75
+ * same 26px it is beside a hero row. */
76
+ .body[data-placement='right'] .sparklineRight {
77
+ flex: none;
78
+ margin-top: auto;
79
+ margin-inline: calc(-1 * var(--mantine-spacing-sm));
80
+ margin-bottom: calc(-1 * var(--mantine-spacing-xs));
81
+ }
82
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Typed CSS-module declaration (the S4 CSS-modules path). Named string props — not an index
3
+ * signature — so dot access stays verbatim under the package's strict flags.
4
+ */
5
+ declare const classes: {
6
+ readonly body: string
7
+ readonly header: string
8
+ readonly sparklineBleed: string
9
+ readonly sparklineRight: string
10
+ }
11
+ export default classes
@@ -30,7 +30,7 @@ function render(tone?: StatCardTone) {
30
30
  <MantineProvider>
31
31
  {/* exactOptionalPropertyTypes forbids an explicit tone={undefined} — spread only when set */}
32
32
  <StatCard
33
- label="Downtime · last 24h"
33
+ title="Downtime · last 24h"
34
34
  value="0 min"
35
35
  {...(tone !== undefined ? { tone } : {})}
36
36
  />
@@ -86,3 +86,69 @@ describe('omitting tone is not "good" — it is untinted, and says nothing', ()
86
86
  expect(markup).toContain('0 min')
87
87
  })
88
88
  })
89
+
90
+ describe('the header composes WidgetHeader at the widget tier', () => {
91
+ test('renders an h3 carrying the title', () => {
92
+ const markup = renderToStaticMarkup(
93
+ <MantineProvider>
94
+ <StatCard title="Active Users" value="12,483" />
95
+ </MantineProvider>,
96
+ )
97
+ expect(markup).toContain('<h3')
98
+ expect(markup).toContain('Active Users')
99
+ })
100
+ })
101
+
102
+ describe('info and subtitle reach the composed WidgetHeader', () => {
103
+ function renderHeader(props: { info?: string; subtitle?: string }) {
104
+ return renderToStaticMarkup(
105
+ <MantineProvider>
106
+ <StatCard title="Training load" value="412" {...props} />
107
+ </MantineProvider>,
108
+ )
109
+ }
110
+
111
+ test('the subtitle renders as its own line under the hero row', () => {
112
+ expect(renderHeader({ subtitle: 'TSS · 7-day rolling' })).toContain('TSS · 7-day rolling')
113
+ })
114
+
115
+ test('the info text ships with the glyph, and never inside the heading', () => {
116
+ const markup = renderHeader({ info: 'Sum of per-session TSS over 7 days.' })
117
+ // `WidgetHeader` renders the bubble only while open, so the closed markup carries the named
118
+ // trigger — what matters here is that the card FORWARDED the prop and that the text stayed OUT
119
+ // of the `<h3>`, which is what an `info` rendered inside the heading would silently do.
120
+ expect(markup).toContain('aria-label="More information"')
121
+ const heading = /<h3[^>]*>(.*?)<\/h3>/.exec(markup)?.[1] ?? ''
122
+ expect(heading).toContain('Training load')
123
+ expect(heading).not.toContain('Sum of')
124
+ })
125
+
126
+ test('neither prop renders anything when omitted — no glyph, no empty line', () => {
127
+ expect(renderHeader({})).not.toContain('More information')
128
+ })
129
+ })
130
+
131
+ describe('sparklinePlacement', () => {
132
+ function renderWithSparkline(placement?: 'bleed' | 'right') {
133
+ return renderToStaticMarkup(
134
+ <MantineProvider>
135
+ <StatCard
136
+ title="Active Users"
137
+ value="12,483"
138
+ sparkline={<span data-testid="spark">spark</span>}
139
+ {...(placement !== undefined ? { sparklinePlacement: placement } : {})}
140
+ />
141
+ </MantineProvider>,
142
+ )
143
+ }
144
+
145
+ test('defaults to bleed — the full-width row bled to the card edges', () => {
146
+ const markup = renderWithSparkline()
147
+ expect(markup).toContain('data-placement="bleed"')
148
+ })
149
+
150
+ test('right sits the sparkline beside the hero-value row', () => {
151
+ const markup = renderWithSparkline('right')
152
+ expect(markup).toContain('data-placement="right"')
153
+ })
154
+ })