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
@@ -13,9 +13,26 @@
13
13
  * - a `disabled` destination that is silently dropped instead of rendered disabled (rule 11).
14
14
  */
15
15
  import { describe, expect, test } from 'bun:test'
16
- import { MOBILE_MORE_KEY, projectMobileNav } from './mobile-nav-model'
16
+ import { MOBILE_MORE_KEY, blockRowCount, projectMobileNav } from './mobile-nav-model'
17
17
  import type { ProjectMobileNavOptions } from './mobile-nav-model'
18
- import type { MobileNavGroup, MobileNavModel, MobileNavSlot, SidebarItem } from '../nav/types'
18
+ import {
19
+ sidebarBlockFoldKey,
20
+ sidebarBlockMobile,
21
+ sidebarBlockPlacement,
22
+ sidebarBlockRail,
23
+ sidebarBlockVisibleCount,
24
+ sidebarSectionFoldKey,
25
+ slugifyLabel,
26
+ } from './sidebar-block-model'
27
+ import type {
28
+ MobileNavGroup,
29
+ MobileNavModel,
30
+ MobileNavSlot,
31
+ SidebarCustomBlock,
32
+ SidebarItem,
33
+ SidebarListBlock,
34
+ SidebarProgressBlock,
35
+ } from '../nav/types'
19
36
 
20
37
  // ── fixtures ─────────────────────────────────────────────────────────────────────────────────────
21
38
 
@@ -258,9 +275,34 @@ describe('projectMobileNav', () => {
258
275
  expect(rowKeys(more.groups)).toEqual(['reports', 'daily', 'weekly'])
259
276
  })
260
277
 
261
- /** Rule 12 — today's `items.some(i => i.active)` leaves the tab dark while a child route is
262
- * open, which reads as "you are nowhere". A slot is active when ANY destination under it is. */
263
- test('12. a slot is active when a NESTED child is the active destination', () => {
278
+ /**
279
+ * Rule 12 — a slot is active when the active destination is one it COVERS, and coverage and
280
+ * activeness read the SAME set. See `hasActiveDestination`'s doc for why they used to differ and
281
+ * what that cost (two `aria-current="page"` tabs at `/dashboard/sessions`, measured in Chrome).
282
+ *
283
+ * A SECTION tab covers its whole pruned tree, so the rollup rule 12 was written for still holds
284
+ * there — that is the case below.
285
+ */
286
+ test('12. a SECTION slot is active when a NESTED child is the active destination', () => {
287
+ const model = projectMobileNav([
288
+ {
289
+ label: 'Main',
290
+ mobile: { tab: true },
291
+ items: [item('reports', { children: [item('daily', { active: true })] })],
292
+ },
293
+ { label: 'Other', items: [item('other', { mobile: 'tab' })] },
294
+ ])
295
+
296
+ expect(slotAt(model, 0).active).toBe(true)
297
+ expect(slotAt(model, 1).active).toBe(false)
298
+ })
299
+
300
+ /**
301
+ * The exclusivity half, and the one that was missing: an ITEM tab covers only the destination its
302
+ * tap navigates to, so a nested child that lives in the overflow lights the OVERFLOW slot instead.
303
+ * Lighting both was the defect — a bar cannot tell a reader they are in two places.
304
+ */
305
+ test('12. an ITEM tab does NOT light for a child it cannot navigate to — that child’s slot does', () => {
264
306
  const model = projectMobileNav([
265
307
  {
266
308
  label: 'Main',
@@ -271,8 +313,67 @@ describe('projectMobileNav', () => {
271
313
  },
272
314
  ])
273
315
 
274
- expect(slotAt(model, 0).active).toBe(true)
275
- expect(slotAt(model, 1).active).toBe(false)
316
+ const active = model.slots.filter((slot) => slot.active)
317
+ expect(active.length).toBe(1)
318
+ expect(active[0]?.key).toBe('daily')
319
+ expect(slotAt(model, 0).active).toBe(false)
320
+ })
321
+
322
+ /**
323
+ * The invariant, stated once over every shape this model produces: AT MOST ONE slot is active.
324
+ * `useNav` already guarantees exactly one active DESTINATION (`use-nav.test.tsx`); this is the
325
+ * projection's half of the same law, and it is what a rendered bar actually shows.
326
+ */
327
+ test('12. at most ONE slot is active, for every destination in a mixed definition', () => {
328
+ const sections = (activeKey: string) => [
329
+ {
330
+ label: 'Overview',
331
+ items: [
332
+ item('dashboard', {
333
+ mobile: 'tab' as const,
334
+ active: activeKey === 'dashboard',
335
+ children: [
336
+ item('sessions', { active: activeKey === 'sessions' }),
337
+ item('traffic', { active: activeKey === 'traffic' }),
338
+ ],
339
+ }),
340
+ item('activity', { mobile: 'tab' as const, active: activeKey === 'activity' }),
341
+ ],
342
+ },
343
+ {
344
+ label: 'Batteries',
345
+ mobile: { tab: true as const },
346
+ items: [
347
+ item('query', { active: activeKey === 'query' }),
348
+ item('router', { active: activeKey === 'router' }),
349
+ item('forms', { active: activeKey === 'forms' }),
350
+ ],
351
+ },
352
+ {
353
+ label: 'System',
354
+ items: [
355
+ item('settings', { active: activeKey === 'settings' }),
356
+ item('user', { active: activeKey === 'user' }),
357
+ ],
358
+ },
359
+ ]
360
+
361
+ const keys = [
362
+ 'dashboard',
363
+ 'sessions',
364
+ 'traffic',
365
+ 'activity',
366
+ 'query',
367
+ 'router',
368
+ 'forms',
369
+ 'settings',
370
+ 'user',
371
+ ] as const
372
+ for (const key of keys) {
373
+ const model = projectMobileNav(sections(key))
374
+ const active = model.slots.filter((slot) => slot.active)
375
+ expect({ key, count: active.length }).toEqual({ key, count: 1 })
376
+ }
276
377
  })
277
378
 
278
379
  /** §2.4 — a five-slot bar is ~72px wide, so the bar label is `short` when the consumer supplied
@@ -294,3 +395,123 @@ describe('projectMobileNav', () => {
294
395
  expect(slotAt(model, 1).short).toBe('Activity')
295
396
  })
296
397
  })
398
+
399
+ /**
400
+ * `blockRowCount` — the `accountRowCount` sibling for sidebar blocks (`docs/CONTROLS-SPEC.md`
401
+ * §2.3). It exists for one reason: `BasaltShell` needs the More-surface row count BEFORE the
402
+ * projection runs, because that count is what picks `menu` vs `sheet` against `menuMax`. A second,
403
+ * independent estimate of the same rows is exactly how a "1 row" account shipped a 9-row menu into
404
+ * headroom sized for 6.
405
+ */
406
+ /** A three-item list block, the shape every row-count case counts against. */
407
+ const list = (extra: Partial<SidebarListBlock> = {}): SidebarListBlock => ({
408
+ kind: 'list',
409
+ key: extra.key ?? 'awaiting',
410
+ label: 'Awaiting action',
411
+ items: [
412
+ { key: 'a', label: 'A' },
413
+ { key: 'b', label: 'B' },
414
+ { key: 'c', label: 'C' },
415
+ ],
416
+ ...extra,
417
+ })
418
+
419
+ describe('blockRowCount', () => {
420
+ test('undefined and an empty list are both zero', () => {
421
+ expect(blockRowCount(undefined)).toBe(0)
422
+ expect(blockRowCount([])).toBe(0)
423
+ })
424
+
425
+ test('ONE row per list block, never one per item — the row opens a nested sheet', () => {
426
+ expect(blockRowCount([list(), list({ key: 'second' })])).toBe(2)
427
+ })
428
+
429
+ test("mobile:'hidden' drops a list; a progress block is hidden until it asks otherwise", () => {
430
+ expect(blockRowCount([list({ mobile: 'hidden' })])).toBe(0)
431
+ const progress: SidebarProgressBlock = {
432
+ kind: 'progress',
433
+ key: 'onboarding',
434
+ label: 'Getting started',
435
+ value: 1,
436
+ total: 5,
437
+ }
438
+ expect(blockRowCount([progress])).toBe(0)
439
+ expect(blockRowCount([{ ...progress, mobile: 'more' }])).toBe(1)
440
+ })
441
+
442
+ test('a custom block never reaches mobile — it is desktop-only by kind', () => {
443
+ expect(blockRowCount([{ kind: 'custom', key: 'tree', node: null }])).toBe(0)
444
+ })
445
+
446
+ /** Same rule the renderer applies: an empty list would open a sheet with nothing in it. */
447
+ test('an empty list block contributes no row', () => {
448
+ expect(blockRowCount([list({ items: [] })])).toBe(0)
449
+ })
450
+
451
+ /** The threshold this number exists to move, asserted end to end against the projection. */
452
+ test('block rows push a six-destination More over the threshold into a sheet', () => {
453
+ const sections = [{ label: 'Main', items: [item('a', { mobile: 'tab' }), ...items(6)] }]
454
+ expect(slotAt(projectMobileNav(sections), 1).kind).toBe('menu')
455
+ expect(
456
+ slotAt(projectMobileNav(sections, { extraMoreRows: blockRowCount([list()]) }), 1).kind,
457
+ ).toBe('sheet')
458
+ })
459
+ })
460
+
461
+ /**
462
+ * The block-default resolvers. Three call sites read them — the sidebar (placement + rail), the
463
+ * renderer (paint) and `blockRowCount` (the count above) — and a default re-derived per call site
464
+ * is how the count and the renderer drift apart.
465
+ */
466
+ describe('sidebar block defaults', () => {
467
+ const listBlock: SidebarListBlock = { kind: 'list', key: 'k', label: 'L', items: [] }
468
+ const progressBlock: SidebarProgressBlock = {
469
+ kind: 'progress',
470
+ key: 'p',
471
+ label: 'P',
472
+ value: 1,
473
+ total: 5,
474
+ }
475
+ const customBlock: SidebarCustomBlock = { kind: 'custom', key: 'c', node: null }
476
+
477
+ test('placement: list/custom default to nav, progress to bottom', () => {
478
+ expect(sidebarBlockPlacement(listBlock)).toBe('nav')
479
+ expect(sidebarBlockPlacement(customBlock)).toBe('nav')
480
+ expect(sidebarBlockPlacement(progressBlock)).toBe('bottom')
481
+ expect(sidebarBlockPlacement({ ...listBlock, placement: 'bottom' })).toBe('bottom')
482
+ })
483
+
484
+ test('rail: a dot only for a list with BOTH a count and an icon, a ring for progress', () => {
485
+ expect(sidebarBlockRail(listBlock)).toBe('hidden')
486
+ expect(sidebarBlockRail({ ...listBlock, count: 3, icon: null })).toBe('dot')
487
+ expect(sidebarBlockRail({ ...listBlock, count: 3, icon: null, rail: 'hidden' })).toBe('hidden')
488
+ // Neither half alone earns one — and an explicit `rail: 'dot'` cannot conjure the node either.
489
+ expect(sidebarBlockRail({ ...listBlock, count: 3 })).toBe('hidden')
490
+ expect(sidebarBlockRail({ ...listBlock, icon: null })).toBe('hidden')
491
+ expect(sidebarBlockRail({ ...listBlock, rail: 'dot' })).toBe('hidden')
492
+ expect(sidebarBlockRail(progressBlock)).toBe('ring')
493
+ expect(sidebarBlockRail({ ...progressBlock, rail: 'hidden' })).toBe('hidden')
494
+ expect(sidebarBlockRail(customBlock)).toBe('hidden')
495
+ })
496
+
497
+ test('mobile: a list is reachable from More by default, progress and custom are not', () => {
498
+ expect(sidebarBlockMobile(listBlock)).toBe('more')
499
+ expect(sidebarBlockMobile({ ...listBlock, mobile: 'hidden' })).toBe('hidden')
500
+ expect(sidebarBlockMobile(progressBlock)).toBe('hidden')
501
+ expect(sidebarBlockMobile({ ...progressBlock, mobile: 'more' })).toBe('more')
502
+ expect(sidebarBlockMobile(customBlock)).toBe('hidden')
503
+ })
504
+
505
+ test('visible count honours max, and a max past the count changes nothing', () => {
506
+ expect(sidebarBlockVisibleCount(6, 3)).toBe(3)
507
+ expect(sidebarBlockVisibleCount(6, undefined)).toBe(6)
508
+ expect(sidebarBlockVisibleCount(2, 9)).toBe(2)
509
+ })
510
+
511
+ /** The fold keys are a consumer-readable contract — a rename resets every user's sidebar. */
512
+ test('the fold keys namespace to basalt:sidebar-block:<key> / basalt:sidebar-section:<slug>', () => {
513
+ expect(sidebarBlockFoldKey('awaiting')).toBe('sidebar-block:awaiting')
514
+ expect(sidebarSectionFoldKey('Tools & More')).toBe('sidebar-section:tools-more')
515
+ expect(slugifyLabel(' Awaiting Action! ')).toBe('awaiting-action')
516
+ })
517
+ })
@@ -19,9 +19,11 @@ import type {
19
19
  MobileNavModel,
20
20
  MobileNavSlot,
21
21
  NavMobilePlacement,
22
+ SidebarBlock,
22
23
  SidebarItem,
23
24
  SidebarSection,
24
25
  } from '../nav/types'
26
+ import { sidebarBlockMobile } from './sidebar-block-model'
25
27
 
26
28
  /** Hard cap on bar slots, INCLUDING More. Five 72px slots is the widest a 360px viewport holds. */
27
29
  export const MOBILE_MAX_TABS_DEFAULT = 5
@@ -39,13 +41,33 @@ export const MOBILE_MORE_KEY = '__more'
39
41
  export type ProjectMobileNavOptions = {
40
42
  config?: MobileNavConfig | undefined
41
43
  /**
42
- * Extra rows the More surface will hold — account + settings + `moreExtra`. Drives both
44
+ * Extra rows the More surface will hold — account + settings + sidebar blocks. Drives both
43
45
  * `needsMore` and the menu-vs-sheet threshold. The model cannot see those rows (they are
44
46
  * Mantine-rendered chrome, not `SidebarItem`s), so the shell counts them and passes the total.
45
47
  */
46
48
  extraMoreRows?: number
47
49
  }
48
50
 
51
+ /**
52
+ * How many More-surface rows `blocks` contribute — the `accountRowCount` sibling for sidebar
53
+ * blocks, and for the same reason: `BasaltShell` needs the number BEFORE the projection runs (it
54
+ * feeds `extraMoreRows`, which picks `menu` vs `sheet` against `menuMax`) while the renderer is
55
+ * what decides how many rows a block actually becomes.
56
+ *
57
+ * ONE row per block, never one per item: a list block projects to a single `Awaiting action · 3`
58
+ * row that opens a nested sheet of its items. Counting the items instead would push a 12-item
59
+ * block's More surface to a sheet on the strength of rows that surface never paints.
60
+ */
61
+ export function blockRowCount(blocks: readonly SidebarBlock[] | undefined): number {
62
+ if (blocks === undefined) return 0
63
+ return blocks.filter((block) => {
64
+ if (sidebarBlockMobile(block) !== 'more') return false
65
+ // An empty list block would open a sheet with nothing in it — same failure as the `loading`
66
+ // account that used to conjure a More slot (`accountRowCount`).
67
+ return block.kind !== 'list' || block.items.length > 0
68
+ }).length
69
+ }
70
+
49
71
  const DEV = process.env['NODE_ENV'] !== 'production'
50
72
 
51
73
  /**
@@ -67,12 +89,38 @@ function isSectionHidden(section: SidebarSection): boolean {
67
89
  return section.mobile === false
68
90
  }
69
91
 
70
- /** §2.3 rule 12 — a slot is active when ANY of its destinations is, nested children included. */
92
+ /**
93
+ * §2.3 rule 12 — a slot is active when the active destination is one the slot COVERS.
94
+ *
95
+ * "Covers" is the load-bearing word, and it is what makes exclusivity structural: a slot's `covered`
96
+ * set is exactly the destinations it can navigate to, and activeness now reads that same set. Two
97
+ * slots therefore cannot both be lit for one location, because no destination is in two `covered`
98
+ * sets — the overflow is built by removing everything the bar already covers.
99
+ *
100
+ * It used to roll up unconditionally over `children`, and `itemCandidate.covered` was
101
+ * `new Set([item.key])` — the item's own key ONLY. The two disagreed, and the disagreement was the
102
+ * bug: at `/dashboard/sessions` the `Dashboard` link slot lit through the rollup while `Sessions`
103
+ * itself, uncovered, sat in the overflow and lit the `More` slot as well. Measured in Chrome: two
104
+ * `aria-current="page"` tabs in a five-slot bar.
105
+ *
106
+ * The rollup itself survives where coverage backs it — a SECTION slot covers its whole pruned tree
107
+ * (`collectKeys` recurses), so a nested destination inside a section tab still lights that tab, which
108
+ * is the "you are nowhere" case rule 12 was written for. What changed is an ITEM tab with children:
109
+ * it covers only itself, its children stay reachable in the overflow, and the slot that can actually
110
+ * reach the open route is the one that lights. That is strictly better than the alternative — folding
111
+ * the children into the parent's coverage would light the parent and make the child unreachable from
112
+ * the bar at all.
113
+ */
71
114
  function hasActiveDestination(item: SidebarItem): boolean {
72
115
  if (item.active) return true
73
116
  return item.children?.some(hasActiveDestination) ?? false
74
117
  }
75
118
 
119
+ /** The half of rule 12 an ITEM slot reads: only the destination the tap actually navigates to. */
120
+ function isActiveDestination(item: SidebarItem): boolean {
121
+ return item.active === true
122
+ }
123
+
76
124
  /** Rows a group tree will render. Nesting costs a row per node — children are indented rows. */
77
125
  function countRows(items: readonly SidebarItem[]): number {
78
126
  return items.reduce((total, item) => total + 1 + countRows(item.children ?? []), 0)
@@ -139,13 +187,15 @@ function itemCandidate(item: SidebarItem): Candidate {
139
187
  return {
140
188
  key: item.key,
141
189
  covered: new Set([item.key]),
190
+ // `isActiveDestination`, not the rollup: this slot COVERS one key, so it reads one key
191
+ // (`hasActiveDestination`'s doc has the whole accounting).
142
192
  toSlot: () => ({
143
193
  kind: 'link',
144
194
  key: item.key,
145
195
  label: item.label,
146
196
  short: item.short ?? item.label,
147
197
  icon: item.icon,
148
- active: hasActiveDestination(item),
198
+ active: isActiveDestination(item),
149
199
  item,
150
200
  }),
151
201
  }
@@ -8,7 +8,7 @@ import { Badge } from '@mantine/core'
8
8
  import { VX } from '../tokens'
9
9
 
10
10
  /**
11
- * Sidebar nav count badge (docs/DESIGN-SPEC.md §5): mono 10.5px, ink-8% bg, radius 5, height 16,
11
+ * Sidebar nav count badge (docs/DESIGN-SPEC.md §5): mono micro (11px), ink-8% bg, radius 5, height 16,
12
12
  * padding 0 5px, muted text; `marginLeft: auto` pins it to the row end on any render path.
13
13
  * `styles` (inline) rather than a token color prop, since none of Mantine's variant/color
14
14
  * combinations land on the ink-tint idiom. Returns `null` for a zero/empty count so the badge slot
@@ -0,0 +1,178 @@
1
+ /*
2
+ * `PageBar` layout (docs/CONTROLS-SPEC.md §2.1).
3
+ *
4
+ * Law C7 in CSS form: `flex-wrap: nowrap` on every row and NO `overflow-x` anywhere. A home that
5
+ * scrolls sideways hides its own controls; the fold is computed instead — `More` for actions
6
+ * (`ActionGroup`), a `Filters (n)` sheet for filters (`FilterSet`'s job, wave 4). The filter
7
+ * container below only supplies `min-width: 0` so that fold has a box to measure against.
8
+ */
9
+
10
+ /* The shell-less bar: both rows, sticky at the top of the document. One layer under the AppShell
11
+ * header (`--app-shell-header-z-index`, default 100) so it never wins a stacking fight with ordinary
12
+ * in-flow content — Mantine's Input section, Indicator, sticky table headers, and Slider thumbs all
13
+ * sit at `z-index: 1` or higher, which used to paint over a bare `z-index: 1` bar while scrolling. */
14
+ .bar {
15
+ position: sticky;
16
+ top: 0;
17
+ z-index: calc(var(--app-shell-header-z-index, 100) - 1);
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: var(--vx-space-stack-xs);
21
+ background-color: var(--vx-surface-bg);
22
+ }
23
+
24
+ /*
25
+ * Row 2 inside a shell: in the page flow, sticky under the 48px header. `--app-shell-header-height`
26
+ * is Mantine's own AppShell variable, so the offset tracks the header at any density with no React
27
+ * state and no measurement. `z-index` sits one layer below the header itself (see `.bar` above) —
28
+ * high enough to stay above ordinary page content, never above the AppShell header.
29
+ */
30
+ .row2Sticky {
31
+ position: sticky;
32
+ top: var(--app-shell-header-height, 0px);
33
+ z-index: calc(var(--app-shell-header-z-index, 100) - 1);
34
+ background-color: var(--vx-surface-bg);
35
+ }
36
+
37
+ .row1 {
38
+ display: flex;
39
+ flex-wrap: nowrap;
40
+ align-items: center;
41
+ gap: var(--vx-space-control-gap);
42
+ min-width: 0;
43
+ min-height: var(--vx-space-page-bar-row-height);
44
+ }
45
+
46
+ .lead {
47
+ display: flex;
48
+ flex-wrap: nowrap;
49
+ align-items: center;
50
+ gap: var(--vx-space-control-gap);
51
+ flex-shrink: 0;
52
+ min-width: 0;
53
+ }
54
+
55
+ .icon {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ color: var(--vx-muted);
59
+ }
60
+
61
+ /*
62
+ * The shell-less page title (law C8: the breadcrumb names the page inside a shell, this does it
63
+ * outside one). Head-font treatment, same 88%/550 as every `WidgetHeader` title.
64
+ */
65
+ .title {
66
+ margin: 0;
67
+ font-family: var(--basalt-font-head);
68
+ font-size: var(--vx-text-xl);
69
+ font-weight: 550;
70
+ font-stretch: 88%;
71
+ color: var(--vx-ink);
72
+ white-space: nowrap;
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
+ }
76
+
77
+ /*
78
+ * Icons inside a row-1 button are 16px and sit 6px from their label. Mantine spends
79
+ * `--mantine-spacing-xs` (11px) on a Button's section gap regardless of the size tier, which is the
80
+ * BODY tier's number — at 30px it detached the glyph from the word it belongs to. Scoped to the row
81
+ * rather than themed on `Button` globally: a `size="md"` button in a form still wants Mantine's gap.
82
+ */
83
+ .row1 :global(.mantine-Button-section[data-position='left']),
84
+ .row2 :global(.mantine-Button-section[data-position='left']) {
85
+ margin-inline-end: var(--vx-space-control-gap);
86
+ }
87
+
88
+ .row1 :global(.mantine-Button-section) > svg,
89
+ .row1 :global(.mantine-ActionIcon-root) > svg {
90
+ width: 1rem;
91
+ height: 1rem;
92
+ }
93
+
94
+ /* Actions and sync are right-aligned; the lead (or the breadcrumb, in a shell) holds the left. */
95
+ .row1End {
96
+ display: flex;
97
+ flex-wrap: nowrap;
98
+ align-items: center;
99
+ justify-content: flex-end;
100
+ gap: var(--vx-space-control-gap);
101
+ flex: 1 1 auto;
102
+ min-width: 0;
103
+ }
104
+
105
+ /*
106
+ * Row 2's rhythm is 8px · pills · 8px, NOT a 40px reserved row.
107
+ *
108
+ * `min-height: var(--vx-space-page-bar-row-height)` (40px) left a 30px pill floating inside a 40px
109
+ * box with the 5px above and below it belonging to nothing — and because the row sat in a `Stack`
110
+ * that already spaced it, the visible gap between the header and the pills read as ~19px while the
111
+ * gap from the pills to the first card read as ~19px too. Symmetric padding on the row itself is the
112
+ * same total height (46px vs 40px) with both halves owned by one declaration, so the sticky bar's
113
+ * published height covers exactly the band it paints.
114
+ */
115
+ .row2 {
116
+ display: flex;
117
+ flex-wrap: nowrap;
118
+ align-items: center;
119
+ gap: var(--vx-space-control-gap);
120
+ min-width: 0;
121
+ padding-block: var(--vx-space-stack-sm);
122
+ }
123
+
124
+ .filters {
125
+ display: flex;
126
+ flex-wrap: nowrap;
127
+ align-items: center;
128
+ gap: var(--vx-space-control-gap);
129
+ flex: 1 1 auto;
130
+ min-width: 0;
131
+ }
132
+
133
+ .filtersEnd {
134
+ display: flex;
135
+ flex-wrap: nowrap;
136
+ align-items: center;
137
+ gap: var(--vx-space-control-gap);
138
+ margin-inline-start: auto;
139
+ flex-shrink: 0;
140
+ }
141
+
142
+ /*
143
+ * Law C15 — every touch target inside a home is at least `touchControlHeight` (36px at level 0)
144
+ * below `sm`, while the VISIBLE box stays the 30px `ctl` tier. `::after` and not `::before`:
145
+ * Mantine's Button owns `::before` for its loading overlay (`@mantine/core/styles/Button.css`),
146
+ * so a hit area drawn there would fight it. `pointer-events` stays default so the overlay is the
147
+ * one receiving the tap.
148
+ *
149
+ * `overflow: visible` is LOAD-BEARING, not tidying. Mantine's Button and ActionIcon roots both set
150
+ * `overflow: hidden` (`Button.css`, `ActionIcon.css`), which clips an overlay that by definition
151
+ * extends past the 30px box — so without this the 36px target simply does not exist and a tap 3px
152
+ * above the control misses. What `overflow: hidden` buys those roots is clipping the loading
153
+ * overlay's blur, and that overlay is only painted under `[data-loading]`, which is excluded below.
154
+ */
155
+ @media (max-width: 47.99375em) {
156
+ .row1 :is(button, a),
157
+ .row2 :is(button, a) {
158
+ position: relative;
159
+ }
160
+
161
+ .row1 :is(button, a):not([data-loading]),
162
+ .row2 :is(button, a):not([data-loading]) {
163
+ overflow: visible;
164
+ }
165
+
166
+ .row1 :is(button, a)::after,
167
+ .row2 :is(button, a)::after {
168
+ content: '';
169
+ position: absolute;
170
+ top: 50%;
171
+ left: 50%;
172
+ translate: -50% -50%;
173
+ width: 100%;
174
+ height: 100%;
175
+ min-width: var(--vx-space-touch-control-height);
176
+ min-height: var(--vx-space-touch-control-height);
177
+ }
178
+ }
@@ -0,0 +1,17 @@
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 bar: string
7
+ readonly row2Sticky: string
8
+ readonly row1: string
9
+ readonly lead: string
10
+ readonly icon: string
11
+ readonly title: string
12
+ readonly row2: string
13
+ readonly row1End: string
14
+ readonly filters: string
15
+ readonly filtersEnd: string
16
+ }
17
+ export default classes