basalt-ui 1.24.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 (467) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +25 -0
  3. package/MIGRATING.md +547 -21
  4. package/README.md +46 -33
  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/query-state.d.ts +1 -1
  134. package/dist/dashboard/settings-section.d.ts +6 -4
  135. package/dist/dashboard/settings-section.d.ts.map +1 -1
  136. package/dist/dashboard/settings-section.js +23 -16
  137. package/dist/dashboard/settings-section.module.css +10 -0
  138. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  139. package/dist/dashboard/settings-section.test.d.ts +2 -0
  140. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  141. package/dist/dashboard/stat-card.d.ts +44 -9
  142. package/dist/dashboard/stat-card.d.ts.map +1 -1
  143. package/dist/dashboard/stat-card.js +80 -48
  144. package/dist/dashboard/stat-card.module.css +82 -0
  145. package/dist/dashboard/stat-card.module.css.d.ts +11 -0
  146. package/dist/data/data-table.d.ts +102 -20
  147. package/dist/data/data-table.d.ts.map +1 -1
  148. package/dist/data/data-table.js +166 -65
  149. package/dist/data/table.d.ts +1 -1
  150. package/dist/data/table.d.ts.map +1 -1
  151. package/dist/guard/grace.test.d.ts +20 -0
  152. package/dist/guard/grace.test.d.ts.map +1 -0
  153. package/dist/guard/index.d.ts +66 -3
  154. package/dist/guard/index.d.ts.map +1 -1
  155. package/dist/guard/index.js +82 -7
  156. package/dist/guard/types.d.ts +3 -3
  157. package/dist/guard/types.d.ts.map +1 -1
  158. package/dist/index.d.ts +12 -2
  159. package/dist/index.d.ts.map +1 -1
  160. package/dist/index.js +13 -10
  161. package/dist/nav/types.d.ts +78 -2
  162. package/dist/nav/types.d.ts.map +1 -1
  163. package/dist/notifications/bell.d.ts +4 -1
  164. package/dist/notifications/bell.d.ts.map +1 -1
  165. package/dist/notifications/index.d.ts +1 -1
  166. package/dist/router-tanstack/field.d.ts +14 -0
  167. package/dist/router-tanstack/field.d.ts.map +1 -0
  168. package/dist/router-tanstack/field.js +4 -0
  169. package/dist/router-tanstack/index.d.ts +6 -2
  170. package/dist/router-tanstack/index.d.ts.map +1 -1
  171. package/dist/router-tanstack/index.js +4 -0
  172. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  173. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  174. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  175. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  176. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  177. package/dist/router-tanstack/search-param-store.js +16 -45
  178. package/dist/router-tanstack/search-store.d.ts +81 -0
  179. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  180. package/dist/router-tanstack/search-store.js +187 -0
  181. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  182. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  183. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  184. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  185. package/dist/router-tanstack/use-nav.d.ts +0 -2
  186. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  187. package/dist/router-tanstack/use-nav.js +34 -12
  188. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  189. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  190. package/dist/section/index.d.ts +2 -0
  191. package/dist/section/index.d.ts.map +1 -0
  192. package/dist/section/index.js +4 -0
  193. package/dist/section/section.d.ts +24 -0
  194. package/dist/section/section.d.ts.map +1 -0
  195. package/dist/section/section.js +126 -0
  196. package/dist/section/section.module.css +104 -0
  197. package/dist/section/section.module.css.d.ts +11 -0
  198. package/dist/section/section.test.d.ts +2 -0
  199. package/dist/section/section.test.d.ts.map +1 -0
  200. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  201. package/dist/shell/app-breadcrumbs.js +18 -15
  202. package/dist/shell/app-header.module.css +77 -37
  203. package/dist/shell/app-header.module.css.d.ts +2 -1
  204. package/dist/shell/app-mobile-nav.d.ts +9 -2
  205. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  206. package/dist/shell/app-mobile-nav.js +98 -11
  207. package/dist/shell/app-mobile-nav.module.css +8 -0
  208. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  209. package/dist/shell/app-sidebar.d.ts +55 -28
  210. package/dist/shell/app-sidebar.d.ts.map +1 -1
  211. package/dist/shell/app-sidebar.js +116 -67
  212. package/dist/shell/app-sidebar.module.css +303 -20
  213. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  214. package/dist/shell/index.d.ts +55 -21
  215. package/dist/shell/index.d.ts.map +1 -1
  216. package/dist/shell/index.js +40 -18
  217. package/dist/shell/mobile-nav-model.d.ts +13 -2
  218. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  219. package/dist/shell/mobile-nav-model.js +15 -1
  220. package/dist/shell/nav-count-badge.d.ts +1 -1
  221. package/dist/shell/page-bar.d.ts +51 -0
  222. package/dist/shell/page-bar.d.ts.map +1 -0
  223. package/dist/shell/page-bar.js +154 -0
  224. package/dist/shell/page-bar.module.css +178 -0
  225. package/dist/shell/page-bar.module.css.d.ts +17 -0
  226. package/dist/shell/page-bar.test.d.ts +2 -0
  227. package/dist/shell/page-bar.test.d.ts.map +1 -0
  228. package/dist/shell/sidebar-block-model.d.ts +48 -0
  229. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  230. package/dist/shell/sidebar-block-model.js +39 -0
  231. package/dist/shell/sidebar-blocks.d.ts +32 -0
  232. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  233. package/dist/shell/sidebar-blocks.js +169 -0
  234. package/dist/shell/sidebar-search.d.ts +13 -1
  235. package/dist/shell/sidebar-search.d.ts.map +1 -1
  236. package/dist/shell/sidebar-search.js +34 -1
  237. package/dist/shell/sidebar-search.module.css +26 -0
  238. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  239. package/dist/state/fields.d.ts +392 -0
  240. package/dist/state/fields.d.ts.map +1 -0
  241. package/dist/state/fields.js +369 -0
  242. package/dist/state/fields.test.d.ts +2 -0
  243. package/dist/state/fields.test.d.ts.map +1 -0
  244. package/dist/state/persisted.d.ts +48 -0
  245. package/dist/state/persisted.d.ts.map +1 -0
  246. package/dist/state/persisted.js +105 -0
  247. package/dist/state.d.ts +11 -56
  248. package/dist/state.d.ts.map +1 -1
  249. package/dist/state.js +14 -120
  250. package/dist/styles.css +13 -0
  251. package/dist/surfaces.d.ts +106 -24
  252. package/dist/surfaces.d.ts.map +1 -1
  253. package/dist/surfaces.js +155 -19
  254. package/dist/theme/controls.module.css +16 -1
  255. package/dist/theme/ctl-theme.d.ts +32 -0
  256. package/dist/theme/ctl-theme.d.ts.map +1 -0
  257. package/dist/theme/ctl-theme.js +54 -0
  258. package/dist/theme/ctl-theme.test.d.ts +2 -0
  259. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  260. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  261. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  262. package/dist/theme/divider-law.test.d.ts +2 -0
  263. package/dist/theme/divider-law.test.d.ts.map +1 -0
  264. package/dist/theme/icon-slot.d.ts +44 -0
  265. package/dist/theme/icon-slot.d.ts.map +1 -0
  266. package/dist/theme/icon-slot.js +21 -0
  267. package/dist/theme/icon-slot.module.css +47 -0
  268. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  269. package/dist/theme/icon-slot.test.d.ts +2 -0
  270. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  271. package/dist/theme/index.d.ts +1 -0
  272. package/dist/theme/index.d.ts.map +1 -1
  273. package/dist/theme/index.js +82 -5
  274. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  275. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  276. package/dist/theme/segmented-control.module.css +20 -0
  277. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  278. package/dist/tokens/index.d.ts +12 -0
  279. package/dist/tokens/index.d.ts.map +1 -1
  280. package/dist/tokens/index.js +38 -8
  281. package/dist/tokens/palette.d.ts +97 -61
  282. package/dist/tokens/palette.d.ts.map +1 -1
  283. package/dist/tokens/palette.js +76 -38
  284. package/dist/tokens.css +10 -2
  285. package/dist/widget-header/delta-badge.d.ts +13 -0
  286. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  287. package/dist/widget-header/delta-badge.js +29 -0
  288. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  289. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  290. package/dist/widget-header/index.d.ts +3 -0
  291. package/dist/widget-header/index.d.ts.map +1 -0
  292. package/dist/widget-header/index.js +6 -0
  293. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  294. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  295. package/dist/widget-header/widget-header.d.ts +37 -0
  296. package/dist/widget-header/widget-header.d.ts.map +1 -0
  297. package/dist/widget-header/widget-header.js +77 -0
  298. package/dist/widget-header/widget-header.module.css +335 -0
  299. package/dist/widget-header/widget-header.module.css.d.ts +24 -0
  300. package/dist/widget-header/widget-header.test.d.ts +2 -0
  301. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  302. package/llms.txt +30 -17
  303. package/package.json +14 -1
  304. package/src/charts/cursor/resolve.test.ts +29 -1
  305. package/src/charts/cursor/resolve.ts +38 -6
  306. package/src/charts/cursor/scope.tsx +6 -3
  307. package/src/charts/cursor/store.test.ts +19 -1
  308. package/src/charts/cursor/store.ts +10 -5
  309. package/src/charts/hooks/useChartCursor.ts +16 -8
  310. package/src/charts/index.ts +2 -2
  311. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  312. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  313. package/src/charts/primitives/ChartCard.tsx +86 -146
  314. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  315. package/src/cli/check-coverage.test.ts +297 -0
  316. package/src/cli/check-theme.test.ts +38 -14
  317. package/src/cli/index.ts +392 -16
  318. package/src/cli/init-sync.test.ts +97 -0
  319. package/src/connectivity/connectivity-indicator.tsx +16 -9
  320. package/src/content/index.ts +16 -3
  321. package/src/content/prose.module.css +7 -28
  322. package/src/content/prose.module.css.test.ts +8 -24
  323. package/src/controls/actions.module.css +62 -0
  324. package/src/controls/actions.module.css.d.ts +13 -0
  325. package/src/controls/actions.test.tsx +252 -0
  326. package/src/controls/actions.tsx +655 -0
  327. package/src/controls/actions.type-guard.test.ts +63 -0
  328. package/src/controls/compare-filter.tsx +64 -0
  329. package/src/controls/control-group.test.tsx +229 -0
  330. package/src/controls/control-group.tsx +63 -0
  331. package/src/controls/controls.module.css +542 -0
  332. package/src/controls/controls.module.css.d.ts +35 -0
  333. package/src/controls/controls.router.test.tsx +637 -0
  334. package/src/controls/enum-filter.tsx +159 -0
  335. package/src/controls/filter-context.tsx +153 -0
  336. package/src/controls/filter-pill.tsx +120 -0
  337. package/src/controls/filter-set.test.tsx +472 -0
  338. package/src/controls/filter-set.tsx +225 -0
  339. package/src/controls/filter-sheet.tsx +285 -0
  340. package/src/controls/glyphs.tsx +81 -0
  341. package/src/controls/index.ts +48 -0
  342. package/src/controls/multi-select-filter.tsx +140 -0
  343. package/src/controls/peer-boundary.test.ts +85 -0
  344. package/src/controls/props-contract.test.tsx +106 -0
  345. package/src/controls/range-filter.tsx +184 -0
  346. package/src/controls/search-filter.tsx +123 -0
  347. package/src/controls/select-filter.tsx +89 -0
  348. package/src/controls/sync-button.module.css +63 -0
  349. package/src/controls/sync-button.module.css.d.ts +11 -0
  350. package/src/controls/sync-button.test.tsx +183 -0
  351. package/src/controls/sync-button.tsx +197 -0
  352. package/src/controls/toggle-filter.tsx +60 -0
  353. package/src/controls/view-tabs.tsx +110 -0
  354. package/src/controls-dates/date-range-picker.tsx +42 -0
  355. package/src/controls-dates/index.ts +8 -0
  356. package/src/dashboard/delta-badge.tsx +4 -74
  357. package/src/dashboard/query-state.tsx +1 -1
  358. package/src/dashboard/settings-section.module.css +10 -0
  359. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  360. package/src/dashboard/settings-section.test.tsx +56 -0
  361. package/src/dashboard/settings-section.tsx +33 -30
  362. package/src/dashboard/stat-card.module.css +82 -0
  363. package/src/dashboard/stat-card.module.css.d.ts +11 -0
  364. package/src/dashboard/stat-card.test.tsx +67 -1
  365. package/src/dashboard/stat-card.tsx +209 -68
  366. package/src/data/data-table.test.tsx +282 -6
  367. package/src/data/data-table.tsx +429 -94
  368. package/src/data/table.ts +2 -0
  369. package/src/guard/check-source.test.ts +126 -13
  370. package/src/guard/grace.test.ts +149 -0
  371. package/src/guard/index.ts +172 -29
  372. package/src/guard/types.ts +4 -2
  373. package/src/index.ts +46 -5
  374. package/src/nav/types.ts +84 -2
  375. package/src/notifications/bell.tsx +6 -3
  376. package/src/notifications/index.ts +1 -1
  377. package/src/router-tanstack/field.ts +31 -0
  378. package/src/router-tanstack/index.ts +24 -2
  379. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  380. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  381. package/src/router-tanstack/search-param-store.ts +42 -159
  382. package/src/router-tanstack/search-store.router.test.tsx +661 -0
  383. package/src/router-tanstack/search-store.test.ts +418 -0
  384. package/src/router-tanstack/search-store.ts +380 -0
  385. package/src/router-tanstack/use-nav.test.tsx +187 -0
  386. package/src/router-tanstack/use-nav.ts +72 -14
  387. package/src/section/index.ts +1 -0
  388. package/src/section/section.module.css +104 -0
  389. package/src/section/section.module.css.d.ts +11 -0
  390. package/src/section/section.test.tsx +204 -0
  391. package/src/section/section.tsx +220 -0
  392. package/src/shell/app-breadcrumbs.tsx +44 -31
  393. package/src/shell/app-header.module.css +77 -37
  394. package/src/shell/app-header.module.css.d.ts +2 -1
  395. package/src/shell/app-mobile-nav.module.css +8 -0
  396. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  397. package/src/shell/app-mobile-nav.tsx +147 -8
  398. package/src/shell/app-sidebar-account.tsx +2 -2
  399. package/src/shell/app-sidebar.module.css +303 -20
  400. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  401. package/src/shell/app-sidebar.test.tsx +456 -41
  402. package/src/shell/app-sidebar.tsx +276 -140
  403. package/src/shell/index.test.tsx +291 -5
  404. package/src/shell/index.tsx +137 -38
  405. package/src/shell/mobile-nav-model.test.ts +228 -7
  406. package/src/shell/mobile-nav-model.ts +53 -3
  407. package/src/shell/nav-count-badge.tsx +1 -1
  408. package/src/shell/page-bar.module.css +178 -0
  409. package/src/shell/page-bar.module.css.d.ts +17 -0
  410. package/src/shell/page-bar.test.tsx +316 -0
  411. package/src/shell/page-bar.tsx +315 -0
  412. package/src/shell/sidebar-block-model.ts +83 -0
  413. package/src/shell/sidebar-blocks.tsx +301 -0
  414. package/src/shell/sidebar-search.module.css +26 -0
  415. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  416. package/src/shell/sidebar-search.tsx +71 -2
  417. package/src/state/fields.test.tsx +258 -0
  418. package/src/state/fields.ts +892 -0
  419. package/src/state/persisted.ts +211 -0
  420. package/src/state.ts +46 -240
  421. package/src/styles.css +13 -0
  422. package/src/surfaces.ts +203 -36
  423. package/src/theme/controls.module.css +16 -1
  424. package/src/theme/ctl-theme.test.tsx +275 -0
  425. package/src/theme/ctl-theme.tsx +138 -0
  426. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  427. package/src/theme/density-relations.test.ts +106 -51
  428. package/src/theme/divider-law.test.ts +259 -0
  429. package/src/theme/icon-slot.module.css +47 -0
  430. package/src/theme/icon-slot.module.css.d.ts +8 -0
  431. package/src/theme/icon-slot.test.tsx +275 -0
  432. package/src/theme/icon-slot.tsx +59 -0
  433. package/src/theme/index.ts +165 -3
  434. package/src/theme/layout-rhythm-css.test.ts +72 -0
  435. package/src/theme/radius.test.ts +11 -1
  436. package/src/theme/segmented-control.module.css +20 -0
  437. package/src/theme/shadow-surfaces.test.ts +20 -7
  438. package/src/theme/spacing.test.ts +75 -27
  439. package/src/theme/use-basalt-spacing.ts +2 -2
  440. package/src/tokens/density.test.ts +7 -25
  441. package/src/tokens/index.ts +48 -8
  442. package/src/tokens/palette.ts +107 -73
  443. package/src/widget-header/delta-badge.test.tsx +60 -0
  444. package/src/widget-header/delta-badge.tsx +57 -0
  445. package/src/widget-header/index.ts +2 -0
  446. package/src/widget-header/no-mantine.test.ts +33 -0
  447. package/src/widget-header/widget-header.module.css +335 -0
  448. package/src/widget-header/widget-header.module.css.d.ts +24 -0
  449. package/src/widget-header/widget-header.test.tsx +187 -0
  450. package/src/widget-header/widget-header.tsx +168 -0
  451. package/agent/rules/basalt-agent.md +0 -452
  452. package/agent/rules/basalt-app.md +0 -246
  453. package/agent/rules/basalt-commands.md +0 -320
  454. package/agent/rules/basalt-content.md +0 -409
  455. package/agent/rules/basalt-data.md +0 -269
  456. package/agent/rules/basalt-forms.md +0 -186
  457. package/agent/rules/basalt-notifications.md +0 -264
  458. package/agent/rules/basalt-query.md +0 -187
  459. package/agent/rules/basalt-router.md +0 -336
  460. package/dist/content/article-filter-bar.d.ts +0 -40
  461. package/dist/content/article-filter-bar.d.ts.map +0 -1
  462. package/dist/content/article-filter-bar.js +0 -56
  463. package/dist/shell/page-header.d.ts +0 -13
  464. package/dist/shell/page-header.d.ts.map +0 -1
  465. package/dist/shell/page-header.js +0 -23
  466. package/src/content/article-filter-bar.tsx +0 -88
  467. package/src/shell/page-header.tsx +0 -35
@@ -1,246 +0,0 @@
1
- ---
2
- source: basalt-ui
3
- description: App bootstrapping for basalt-ui consumers — composing basaltViteConfig + basaltAppPlugin in vite.config.ts, plugin ordering, the required Mantine CSS layer order, and the PWA/favicon/manifest surface (bring-your-own icons, opt-in serviceWorker).
4
- paths:
5
- - 'vite.config.ts'
6
- - 'apps/**/vite.config.ts'
7
- - 'index.html'
8
- - 'apps/**/index.html'
9
- - 'src/main.tsx'
10
- - 'apps/**/src/main.tsx'
11
- ---
12
-
13
- # Basalt App — Bootstrapping & Vite Config
14
-
15
- `basalt-ui/vite` ships two entry points with a deliberate split: `basaltViteConfig` is **config
16
- only**, `basaltAppPlugin` is the **plugin** half. This rule covers composing them, where
17
- `basaltAppPlugin` sits relative to other plugins, and the head/PWA/manifest surface it generates.
18
-
19
- ## Composing the vite config
20
-
21
- ```ts
22
- // vite.config.ts
23
- import react from '@vitejs/plugin-react'
24
- import { basaltAppPlugin, basaltViteConfig } from 'basalt-ui/vite'
25
- import { defineConfig } from 'vite'
26
-
27
- export default defineConfig({
28
- ...basaltViteConfig({ port: 5173, apiTarget: 'http://localhost:3000' }),
29
- plugins: [react(), ...basaltAppPlugin({ name: 'MyApp', description: '…' })],
30
- })
31
- ```
32
-
33
- `basaltAppPlugin(opts)` returns `PluginOption[]` (a Vite plugin array, not a single plugin) — spread
34
- it into your own `plugins` array with `...basaltAppPlugin({...})`, same as any other Vite plugin
35
- that ships more than one plugin instance.
36
-
37
- `basaltViteConfig` **never returns `plugins`** and never will — it stays config-only by contract.
38
- Plugins carry app-specific concerns (router codegen, PWA, `react-compiler` babel options) that don't
39
- belong in a shared, app-agnostic spine; keeping them out of `basaltViteConfig` means adopting the
40
- preset never silently injects a plugin you didn't ask for, and `basaltAppPlugin` can be adopted (or
41
- skipped) independently of the rest of the preset.
42
-
43
- ## Plugin ordering
44
-
45
- `basaltAppPlugin`'s hooks (`transformIndexHtml`, `generateBundle`, `configureServer`) don't
46
- generate a virtual module another plugin resolves against — unlike
47
- `@content-collections/vite`, whose plugin-ordering rule (agent/rules/basalt-content.md) requires
48
- `contentCollections()` to run FIRST because it generates the virtual `content-collections` module
49
- other plugins/transforms may depend on. `basaltAppPlugin` has no equivalent requirement, so when
50
- composing both:
51
-
52
- ```ts
53
- plugins: [
54
- contentCollections(), // FIRST — see basalt-content.md
55
- react(), // or tanstackStart()
56
- ...basaltAppPlugin({ name: 'MyApp' }), // position is free — see below
57
- ]
58
- ```
59
-
60
- Position is a readability convention here, not a correctness requirement — including with
61
- `serviceWorker` enabled. `vite-plugin-pwa` expands to five plugins that **self-order** via
62
- `enforce` (`vite-plugin-pwa` is `enforce: 'pre'`; `:build`, `:info` and `:pwa-assets` are
63
- `enforce: 'post'`), and Vite sorts by `enforce` before array position. Its precache manifest is
64
- built in `closeBundle`, where `workbox-build` globs the **output directory on disk** — every other
65
- plugin's transforms and emits have already landed there. So no ordering relationship exists between
66
- `basaltAppPlugin` and `react()`/`tanstackStart()`.
67
-
68
- The one real caveat is a plugin that **emits files from its own `closeBundle`**: if it runs after
69
- `vite-plugin-pwa`'s, those files are written too late to be precached (the `rollup-plugin-copy`
70
- class of bug). `vite-plugin-pwa` exposes `integration.closeBundleOrder` to sequence against it.
71
-
72
- ## What `basalt-ui init` writes into `package.json`
73
-
74
- `init` is not only a file-scaffolder. It also patches two keys, and on an existing app it is a
75
- **lint-debt event, not a no-op** — the shipped preset switches on whole oxlint plugins the repo was
76
- never linted against, so previously-clean code lands with real findings on the first run. `init`
77
- prints the plugin list; run `oxlint .` and triage it before your next commit rather than
78
- blanket-disabling a plugin.
79
-
80
- - **`basalt.roots`** — inferred from the real layout (workspace packages depending on basalt or
81
- Mantine, else every workspace `src/`, else `src`). Everything derives from it: `check-theme`'s
82
- scan, the seeded CI `oxfmt` globs, the default scan exemption. Correct it if your sources live
83
- elsewhere; without it a workspace repo scaffolds a guard that scans zero files.
84
- - **`scripts["lint:basalt"]`** — `oxlint . && basalt-ui check-theme`. Previously the docs told you
85
- to wire this and nothing wrote it.
86
- - **`basalt.profile: 'tokens-only'`** — for a consumer on the `--vx-*` layer with no Mantine. It
87
- must be DECLARED (or passed as `--tokens-only`); `check-theme` deliberately never infers it,
88
- because inferring from a missing `@mantine/core` would silence 17 kinds on any repo that keeps
89
- Mantine in a different workspace package. `doctor` does infer it, because its profile only changes
90
- which ADVICE it prints and never what it enforces — and it names the key to write down.
91
-
92
- `init` keeps an existing `.oxlintrc.json` rather than clobbering it, and now names what keeping it
93
- costs (the whole basalt lint half stays off). `--merge-lint` splices the preset's `extends` in; it
94
- refuses on a commented config rather than deleting the comments.
95
-
96
- Run `basalt-ui doctor` afterwards to confirm the wiring took. A check that cannot RUN is reported as
97
- `SKIPPED` and exits non-zero on its own — "All checks passed" is only printable when every check ran.
98
-
99
- ### The lefthook preset overrides YOU, not the other way round
100
-
101
- `doctor`'s `lefthook-preset` check asks whether a pre-commit gate EXISTS — not whether your config
102
- contains the extends string. Since 1.22.0 it asks `lefthook dump`, which resolves `extends`,
103
- `include` and per-command `root:`. **Wiring the jobs yourself passes**: linewatch runs all three with
104
- `root: 'web/'` precisely because `extends` merges commands _without_ their working directory, and the
105
- old text match warned there and prescribed a change that would have broken it.
106
-
107
- A broken `extends` target is still a hard fail — lefthook merges a missing target into **zero**
108
- commands and exits 0, so `lefthook dump` looks clean and the repo has no gate with nothing saying so.
109
- A provably absent gate is a warn; a repo where `lefthook dump` could not run gets an advisory warning
110
- naming what it could not see. n/a with no lefthook at all. `sync` reports the same seam, since it is
111
- the command an upgrade actually runs and neither file is one basalt owns.
112
-
113
- **An `extends` target WINS on a colliding key.** Declare `pre-commit.commands.oxfmt.run` (or
114
- `glob:`) in your own file and the preset's value is what runs — yours is discarded silently. Only
115
- keys the preset does not define merge in. So `env:`, `exclude:`, `skip:` and a differently-named
116
- command of your own are yours; `run:` and `glob:` on a shipped command are not. The sanctioned seam
117
- is therefore `BASALT_BIN`: the guard job runs `${BASALT_BIN:-bunx --no-install basalt-ui}`, and the
118
- shell default is the whole reason the seam exists at all. `init` renders it with the resolved local
119
- bin. `--no-install` is deliberate — a bare `bunx basalt-ui` in a repo where basalt is not resolvable
120
- from the root silently downloads a different copy from npm and gates the commit with a version
121
- nobody pinned.
122
-
123
- ## `index.html` and `public/` are scanned
124
-
125
- `check-theme` resolves `.html` / `.webmanifest` / `.json` as markup (colour kinds only), and each
126
- `basalt.roots` entry's PARENT contributes its `index.html` and its `public/` tree — exactly the Vite
127
- layout above. So a raw hex in a `theme-color` meta or a webmanifest's `background_color` is a
128
- finding now; those are the two colours nothing re-derives on a scheme change. Nothing else in the
129
- parent is walked, and `.json` is never blanket-scanned — name one in `basalt.include`.
130
-
131
- ## `__APP_VERSION__`
132
-
133
- `basaltViteConfig` injects the `__APP_VERSION__` define, and since 1.20.0 ships the ambient
134
- declaration with it (`src/register.ts` → `dist/register.d.ts`, re-exported by the root barrel). An
135
- app that imports anything from `basalt-ui` has it in scope — delete your hand-written ambient block
136
- and its `oxlint-disable`. A subpath-only consumer does not get it, which is the same set as the
137
- consumers not on this Vite preset anyway.
138
-
139
- ## CSS layer order (Mantine)
140
-
141
- `basalt-ui/styles.css` must load AFTER every `@mantine/*/styles.layer.css` bundle — the layered
142
- bundles order `@layer mantine, basalt` so basalt's rules (including the iOS 16px input floor) win;
143
- an unlayered Mantine import outranks `@layer basalt` regardless of specificity. In `main.tsx`:
144
-
145
- ```ts
146
- import '@mantine/core/styles.layer.css'
147
- // ...other @mantine/*/styles.layer.css bundles for any batteries you install
148
- import 'basalt-ui/styles.css'
149
- ```
150
-
151
- ## Head, manifest, and icons — bring your own
152
-
153
- `basaltAppPlugin` injects dual `theme-color` tags (light/dark media queries) and an anti-FOUC inline
154
- `<style>` background, both resolved from the `SURFACE.bg` token — never hand-compute these hexes,
155
- they track the palette automatically. It also injects the `apple-mobile-web-app-*` /
156
- `mobile-web-app-capable` meta set, a `darkreader-lock` meta (default on), a viewport tag with
157
- `viewport-fit=cover` (skipped when the consumer's own `index.html` already declares one), and
158
- site-wide OG/Twitter defaults from `options.seo`.
159
-
160
- **Set `colorScheme` to whatever the app passes as `defaultColorScheme`** — `'dark'` (the default),
161
- `'light'`, `'auto'` (declares `color-scheme: light dark` and picks the pre-boot background off
162
- `prefers-color-scheme`), or `false` (paint the background, declare no `color-scheme`).
163
-
164
- ```ts
165
- plugins: [react(), ...basaltAppPlugin({ name: 'Argo', colorScheme: 'auto' })]
166
- ```
167
-
168
- Before 1.21.0 the anti-FOUC rule emitted `html{color-scheme:dark}` **unlayered**, which outranks
169
- Mantine's own `@layer mantine` declaration regardless of specificity — a light-scheme consumer got
170
- dark scrollbars, dark `<select>` popups and dark date pickers permanently, with no opt-out. The rule
171
- is now scoped to `html:not([data-mantine-color-scheme])`, so it expires the instant MantineProvider
172
- resolves the real scheme. A mismatched `colorScheme` is therefore a flash of the wrong surface, not
173
- a stuck one.
174
-
175
- **The plugin hoists your `<meta charset>`.** Every injected tag defaults to `head-prepend`, which
176
- landed ~20 tags ahead of the consumer's own encoding declaration — measured at byte 1653 on a
177
- realistic shell, past the 1024-byte window the HTML spec gives it, with no consumer-side fix
178
- available. The plugin now re-emits the declaration as its own first head-prepend tag (46 bytes after
179
- the fix) and runs `order: 'pre'` so nothing else can land ahead of it. The legacy `http-equiv` form
180
- is detected and left verbatim.
181
-
182
- Icons are **bring-your-own** — basalt-ui takes no `sharp`/image-processing dependency. The plugin
183
- expects these filenames under `public/` (or `icons.dir` if customized):
184
-
185
- ```text
186
- favicon.ico
187
- favicon.svg
188
- favicon-96x96.png
189
- apple-touch-icon.png
190
- web-app-manifest-192x192.png
191
- web-app-manifest-512x512.png
192
- ```
193
-
194
- Generate the set with `@vite-pwa/assets-generator` (or realfavicongenerator.net) from a single
195
- source image — it lives in the CONSUMER's own devDependencies, not basalt-ui's. `basalt-ui doctor`
196
- warns (does not fail) when `public/` exists but is missing one of these files — and it now
197
- parses `basaltAppPlugin`'s `icons` option out of your vite config first, so it checks the files you
198
- declared rather than the six it assumes. `icons: false` or an empty array is "nothing to check"; an
199
- unparseable or absent config falls back to the six defaults. Adopting the array is no longer a way
200
- to earn a warning.
201
-
202
- **If your `public/` does not match those six filenames, name your icons instead.** `icons` takes
203
- `false | { dir?: string } | readonly BasaltAppIcon[]`, and an array uses the manifest's own field
204
- names verbatim:
205
-
206
- ```ts
207
- basaltAppPlugin({
208
- name: 'rb',
209
- icons: [{ src: '/favicon.svg', sizes: 'any', type: 'image/svg+xml' }],
210
- })
211
- ```
212
-
213
- `BasaltAppIcon` is `{ src, sizes?, type?, purpose?, rel? }`, `rel` being one of `'icon'`,
214
- `'shortcut icon'`, `'apple-touch-icon'`, `'mask-icon'`. **Every entry becomes a manifest icon; an
215
- entry reaches the head only when it names a `rel`** — which is what lets an app whose `index.html`
216
- already links its favicon take the generated manifest without a duplicate tag. An empty array reads
217
- as `false` (no `icons` member, rather than an empty one). `{ dir }` and the default are byte-identical
218
- to what they always emitted.
219
-
220
- One SVG at `sizes: 'any'` is a complete, installable icon set for a single-page app — and it is the
221
- shape every non-scaffolded consumer already has. If you keep a hand-written `manifest.webmanifest`
222
- only because the plugin could not name your icon, delete it: the generated manifest reproduces it
223
- member for member, including the two hexes derived from `SURFACE.bg` rather than hand-copied, and it
224
- takes the permanent `theme-allow-file` on that file with it. **Delete that app's own
225
- `<link rel="manifest">` in the same change** — the plugin injects its own, so leaving both ships two
226
- manifest links pointing at different files, one of them a 404 the moment the hand-written file goes.
227
-
228
- `basaltAppPlugin` also emits `site.webmanifest` (served in dev too) with explicit `id`/`scope`/
229
- `start_url` — pass `manifest: false` to skip it, or `icons: false` to skip the icons. Since 1.22.0
230
- `icons: false` omits BOTH the head `<link>` tags and the manifest's `icons` member; before that it
231
- skipped only the head, so a manifest shipped naming two PNGs the app never builds.
232
-
233
- ## Service worker (opt-in)
234
-
235
- `serviceWorker` defaults to `false` — no service worker unless you ask for it. `true` composes the
236
- optional peer `vite-plugin-pwa` with argo-derived workbox defaults (`autoUpdate` registration,
237
- `cleanupOutdatedCaches`, a `/index.html` navigate fallback that denies `/api/*`); pass an object to
238
- deep-merge overrides on top. When the peer (plus its own peers `workbox-build`/`workbox-window`) is
239
- not installed, the plugin degrades to a one-line console warning — no service worker, no throw:
240
-
241
- ```bash
242
- bun add -D vite-plugin-pwa workbox-build workbox-window
243
- ```
244
-
245
- `basaltAppPlugin` always emits its own `site.webmanifest` — `serviceWorker`'s `manifest: false` is
246
- baked into the defaults so `vite-plugin-pwa` never emits a competing one.
@@ -1,320 +0,0 @@
1
- ---
2
- source: basalt-ui
3
- description: Command bus, overlay controller, Spotlight projection, shortcuts display, and composable overlay mount from the shipped ./commands battery. @mantine/modals + @mantine/spotlight are optional peers; @mantine/notifications is already a peer from ./notifications.
4
- paths:
5
- - 'src/**'
6
- - 'apps/**/src/**'
7
- ---
8
-
9
- # Basalt Commands
10
-
11
- basalt-ui ships `./commands` — a Mantine-coupled command bus + overlay controller battery providing
12
- `defineCommands`, `runCommand`, `defineOverlays`, `overlays`, `toSpotlightActions`,
13
- `toShortcutList`, `ShortcutsHelp`, and `BasaltOverlays` on top of `@mantine/modals` and
14
- `@mantine/spotlight`. Both are **optional peers** — install them before using this battery. The
15
- default `BasaltOverlays` also mounts `<Notifications>` (or pass `notifications={false}`), so
16
- `@mantine/notifications` is needed too:
17
-
18
- ```bash
19
- bun add @mantine/modals @mantine/spotlight @mantine/notifications
20
- ```
21
-
22
- Also import Spotlight styles in `main.tsx` — use the **layered** bundle, since the unlayered one
23
- outranks basalt's `@layer basalt` styles regardless of specificity:
24
-
25
- ```tsx
26
- import '@mantine/spotlight/styles.layer.css'
27
- ```
28
-
29
- ## Mount BasaltOverlays
30
-
31
- `BasaltOverlays` is the composable overlay mount — it bundles `ModalsProvider`, `Spotlight`, and
32
- `Notifications` into a single mount point. Put it inside `BasaltProvider`, before the router. It
33
- **replaces** `<BasaltNotifications />` from `basalt-ui/notifications` — do NOT mount both in the
34
- same tree (that would double-mount `<Notifications />`).
35
-
36
- ```tsx
37
- import { BasaltOverlays } from 'basalt-ui/commands'
38
- import { BasaltProvider } from 'basalt-ui'
39
- import '@mantine/spotlight/styles.layer.css'
40
-
41
- createRoot(root).render(
42
- <BasaltProvider>
43
- <BasaltOverlays>
44
- <App />
45
- </BasaltOverlays>
46
- </BasaltProvider>,
47
- )
48
- ```
49
-
50
- Each layer is enabled by default; pass `false` to disable:
51
-
52
- ```tsx
53
- <BasaltOverlays spotlight={false} notifications={false}>
54
- <App />
55
- </BasaltOverlays>
56
- ```
57
-
58
- ## defineCommands + runCommand
59
-
60
- `defineCommands` is a const-generic factory (mirrors `defineSeries`/`defineNotifications`) for
61
- typed command registries. Augment `BasaltRegister.commands` once, then use `runCommand` — unknown
62
- ids are tsc errors.
63
-
64
- ```ts
65
- // commands.ts (app-side) — define and augment once:
66
- import { defineCommands } from 'basalt-ui/commands'
67
-
68
- export const COMMANDS = defineCommands({
69
- 'file:save': { label: 'Save file', group: 'File', shortcut: 'Mod+S', run: () => save() },
70
- 'file:open': { label: 'Open file', group: 'File', shortcut: 'Mod+O', run: () => open() },
71
- 'data:sync': { label: 'Sync data', group: 'Data', run: async () => sync() },
72
- 'help:docs': { label: 'Open docs', group: 'Help', run: () => window.open('/docs') },
73
- 'edit:focus': {
74
- label: 'Focus editor',
75
- group: 'Edit',
76
- shortcut: 'Mod+E',
77
- when: () => isEditorPage(), // hides from palette when not on editor page
78
- run: () => focusEditor(),
79
- },
80
- })
81
-
82
- declare module 'basalt-ui' {
83
- interface BasaltRegister {
84
- commands: typeof COMMANDS
85
- }
86
- }
87
- ```
88
-
89
- ```ts
90
- // usage.ts — typed runCommand; unknown ids are tsc errors:
91
- import { runCommand } from 'basalt-ui/commands'
92
-
93
- runCommand('file:save') // ✓ sync
94
- await runCommand('data:sync') // ✓ async
95
- runCommand('nonexistent') // ✗ tsc error
96
- ```
97
-
98
- ### CommandId + CommandRunContext
99
-
100
- `CommandId` is `Extract<keyof Slot<'commands', CommandMap>, string>`.
101
- Un-augmented: `never` → `runCommand` is effectively uncallable.
102
- Augmented: the exact registered key literal union.
103
-
104
- `CommandRunContext` is minimal (`{ close?: () => void }`) — pass it to close the command palette
105
- before executing:
106
-
107
- ```ts
108
- runCommand('file:save', { close: () => spotlight.close() })
109
- ```
110
-
111
- ### CRITICAL — runtime stash pattern
112
-
113
- The `BasaltRegister` slot augmentation is **type-only and erased at runtime**. `defineCommands`
114
- stashes the spec map in a module-level `activeCommands` variable; `runCommand` reads from THAT
115
- stash — NOT from `{} as Commands`. This is the same pattern as `defineNotifications` / `emit`.
116
- Always call `defineCommands` once before any call to `runCommand` (typically imported at app boot).
117
-
118
- ### TS circular-inference footgun
119
-
120
- Calling `overlays.open(...)` inside a `run` handler, in the **same module that also augments**
121
- `BasaltRegister.commands` (i.e. the file with `declare module 'basalt-ui' { interface
122
- BasaltRegister { commands: typeof COMMANDS } }`), can trip TS into inferring `COMMANDS` as
123
- `implicitly has type 'any'` — the `run` return type and the `commands` slot type end up in a
124
- circular inference loop. Fix: give that `run` an explicit `: void` (or `: Promise<void>` for async)
125
- return annotation. Prefer splitting overlay-opening commands into a separate file from the
126
- `defineCommands`/`declare module` augmentation when practical — it sidesteps the cycle entirely.
127
-
128
- ## defineOverlays + overlays
129
-
130
- `defineOverlays` registers ephemeral modal overlays. `overlays.open` is the imperative controller.
131
-
132
- ```ts
133
- // overlays.ts (app-side) — define and augment once:
134
- import { defineOverlays } from 'basalt-ui/commands'
135
-
136
- export const OVERLAYS = defineOverlays({
137
- 'user:edit': {
138
- title: 'Edit user',
139
- render: (p: { id: string; name: string }) => <UserEditForm id={p.id} name={p.name} />,
140
- },
141
- 'confirm:delete': {
142
- title: 'Delete item?',
143
- render: (p: { name: string }) => <ConfirmBody name={p.name} />,
144
- },
145
- })
146
-
147
- declare module 'basalt-ui' {
148
- interface BasaltRegister { overlays: typeof OVERLAYS }
149
- }
150
- ```
151
-
152
- ```ts
153
- // usage.ts — typed overlays.open; unknown keys are tsc errors:
154
- import { overlays } from 'basalt-ui/commands'
155
-
156
- overlays.open('user:edit', { id: '42', name: 'Alice' }) // ✓ typed
157
- overlays.open('confirm:delete', { name: 'photo.jpg' }) // ✓
158
- overlays.open('nonexistent', {}) // ✗ tsc error
159
- overlays.close() // close all open modals
160
- ```
161
-
162
- `OverlayKey` is `Extract<keyof Slot<'overlays', OverlayMap>, string>`.
163
- The same runtime-stash pattern applies — `defineOverlays` stashes to `activeOverlays`.
164
-
165
- ### Imperative vs route (when to use each)
166
-
167
- | Use `overlays.open` | Use route mask (`./router-tanstack`) |
168
- | ------------------------------------- | ------------------------------------------ |
169
- | Ephemeral confirms, quick-edit panels | Shareable/back-button/refreshable overlays |
170
- | No URL needed | URL-addressable dialogs |
171
- | Triggered from commands or buttons | Triggered by navigation |
172
-
173
- ## Spotlight projection (toSpotlightActions)
174
-
175
- `toSpotlightActions()` projects the active command registry to `SpotlightActionData[]` for Mantine
176
- Spotlight. Commands where `when()` returns `false` are excluded. `BasaltOverlays` calls it
177
- automatically with `shortcut="mod + K"`.
178
-
179
- ```ts
180
- import { toSpotlightActions } from 'basalt-ui/commands'
181
- import { Spotlight, spotlight } from '@mantine/spotlight'
182
-
183
- // Manual Spotlight mount (if not using BasaltOverlays):
184
- <Spotlight actions={toSpotlightActions()} shortcut="mod + K" />
185
-
186
- // Programmatically open the palette:
187
- spotlight.open()
188
- ```
189
-
190
- ## Shortcut display (toShortcutList + ShortcutsHelp)
191
-
192
- `toShortcutList()` returns commands with a `shortcut` field as a flat list. `ShortcutsHelp`
193
- renders them in a grouped Mantine layout with platform-aware labels (⌘ on mac, Ctrl elsewhere).
194
-
195
- **Display + live binding.** `ShortcutsHelp` renders the shortcuts for reference, and the same
196
- `shortcut` field is bound to live keys by `useCommandHotkeys()` (via the `@tanstack/react-hotkeys`
197
- optional peer). Spotlight `mod + K` covers the palette trigger.
198
-
199
- ```tsx
200
- import { ShortcutsHelp } from 'basalt-ui/commands'
201
-
202
- // In a help modal or settings panel:
203
- ;<ShortcutsHelp title="Keyboard shortcuts" maw={480} />
204
- ```
205
-
206
- ```ts
207
- import { toShortcutList } from 'basalt-ui/commands'
208
- const list = toShortcutList()
209
- // [{ id: 'file:save', label: 'Save file', shortcut: 'Mod+S', group: 'File' }, ...]
210
- ```
211
-
212
- ## Live keybindings (useCommandHotkeys)
213
-
214
- `useCommandHotkeys()` binds all registered command shortcuts to their `run()` functions via
215
- `@tanstack/react-hotkeys`. It is an **optional-peer** hook — degrades to a no-op when the peer is
216
- absent; core resolves cleanly either way. No `HotkeysProvider` wrapper is required.
217
-
218
- Install the optional peer:
219
-
220
- ```bash
221
- bun add @tanstack/react-hotkeys
222
- ```
223
-
224
- ### Mount pattern
225
-
226
- **BasaltOverlays mounts it automatically** (via the internal `HotkeysMount` component) when
227
- `hotkeys={true}` (default). No extra setup needed if using `BasaltOverlays`.
228
-
229
- For manual mounting, call `useCommandHotkeys()` once in the component tree, after `defineCommands`:
230
-
231
- ```tsx
232
- import { useCommandHotkeys } from 'basalt-ui/commands'
233
-
234
- function App() {
235
- // Activate all registered command shortcuts — call once per tree, not per-command.
236
- useCommandHotkeys()
237
- return <RouterProvider router={router} />
238
- }
239
- ```
240
-
241
- If you want hotkeys active only on a specific page:
242
-
243
- ```tsx
244
- function EditorPage() {
245
- useCommandHotkeys()
246
- return <EditorLayout />
247
- }
248
- ```
249
-
250
- ### toHotkeyBindings
251
-
252
- `toHotkeyBindings()` projects the command registry into the `UseHotkeyDefinition[]` array that
253
- `useHotkeys` from `@tanstack/react-hotkeys` expects. Commands without a `shortcut` are skipped.
254
- Commands where `when()` returns false at press time are silently skipped.
255
-
256
- ```ts
257
- import { toHotkeyBindings } from 'basalt-ui/commands'
258
- import { useHotkeys } from '@tanstack/react-hotkeys'
259
-
260
- // Manual binding — equivalent to useCommandHotkeys() but you control the options:
261
- function MyHotkeys() {
262
- useHotkeys(toHotkeyBindings(), { preventDefault: true })
263
- }
264
- ```
265
-
266
- ### Shortcut string format
267
-
268
- Shortcut strings use the `Mod+Key` convention: `Mod` resolves to `⌘` on macOS and `Ctrl`
269
- elsewhere. The `@tanstack/hotkeys` package handles platform resolution at runtime.
270
-
271
- ```ts
272
- defineCommands({
273
- 'file:save': { label: 'Save', shortcut: 'Mod+S', run: () => save() },
274
- 'file:close': { label: 'Close', shortcut: 'Mod+W', run: () => close() },
275
- 'edit:find': { label: 'Find', shortcut: 'Mod+F', run: () => find() },
276
- 'view:zoom': { label: 'Zoom', shortcut: 'Mod+Shift+Z', run: () => zoom() },
277
- })
278
- ```
279
-
280
- ## Spotlight live store (basaltSpotlight)
281
-
282
- `BasaltOverlays` mounts Spotlight against a basalt-owned store (`createSpotlight()` singleton).
283
- Import `openSpotlight` / `closeSpotlight` from `basalt-ui/commands` instead of Mantine's global
284
- `spotlight` to target this instance:
285
-
286
- ```ts
287
- import { openSpotlight, closeSpotlight } from 'basalt-ui/commands'
288
-
289
- // Open the palette programmatically:
290
- openSpotlight()
291
-
292
- // From a command handler (close palette, then run):
293
- runCommand('palette:open', { close: () => closeSpotlight() })
294
- ```
295
-
296
- Spotlight actions are synced from the registry via `toSpotlightActions()` at mount time. Shortcuts
297
- displayed in the Spotlight action description are platform-formatted via `formatShortcutDisplay`
298
- (`⌘S` on mac, `Ctrl+S` on windows).
299
-
300
- ## defineOverlay (single-overlay helper)
301
-
302
- Mirror of `defineCommand` — types a single overlay without registering it. Use when splitting
303
- overlay definitions across files.
304
-
305
- ```ts
306
- import { defineOverlay } from 'basalt-ui/commands'
307
-
308
- const confirmDelete = defineOverlay<{ name: string }>({
309
- title: 'Confirm delete',
310
- render: ({ name }) => <Text>Delete "{name}"?</Text>,
311
- })
312
- // Then include in defineOverlays({ 'confirm:delete': confirmDelete })
313
- ```
314
-
315
- WARNING: `defineOverlay` only TYPES — it does NOT register. Only `defineOverlays(map)` registers.
316
-
317
- ## Deferred
318
-
319
- - **`SidebarItem.command?: CommandId`** — cross-ref between the shell sidebar and the command bus
320
- is a later integration step. Do not add it now.