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,186 +0,0 @@
1
- ---
2
- source: basalt-ui
3
- description: Mantine form conventions for basalt-ui apps — useBasaltForm, field, FormErrorSummary, useFormDraft from the shipped ./forms battery. @mantine/form is an optional peer.
4
- paths:
5
- - 'src/**'
6
- - 'apps/**/src/**'
7
- ---
8
-
9
- # Basalt Forms
10
-
11
- basalt-ui ships `./forms` — a Mantine form adapter battery providing `useBasaltForm`, `field`,
12
- `FormErrorSummary`, and `useFormDraft` on top of `@mantine/form`. `@mantine/form` is an **optional
13
- peer** — install it explicitly before using this battery:
14
-
15
- ```bash
16
- bun add @mantine/form
17
- ```
18
-
19
- Valibot is the default validation library (install it too: `bun add valibot`). Zod 4 works if
20
- already standardised in the project.
21
-
22
- ## useBasaltForm
23
-
24
- `useBasaltForm` is a typed `useForm` wrapper that applies basalt defaults — `mode: 'uncontrolled'`
25
- and `schemaResolver(schema, { sync: true })` when a schema is provided.
26
-
27
- ```ts
28
- import * as v from 'valibot'
29
- import { useBasaltForm, field } from 'basalt-ui/forms'
30
- import { TextInput, NumberInput, Button } from '@mantine/core'
31
-
32
- const Schema = v.object({
33
- name: v.pipe(v.string(), v.minLength(2, 'Name must be at least 2 characters')),
34
- email: v.pipe(v.string(), v.email('Enter a valid email')),
35
- amount: v.pipe(v.number(), v.minValue(0, 'Amount must be non-negative')),
36
- })
37
- type Values = v.InferOutput<typeof Schema>
38
- const INITIAL: Values = { name: '', email: '', amount: 0 }
39
-
40
- function MyForm() {
41
- const form = useBasaltForm({ initialValues: INITIAL, schema: Schema })
42
-
43
- return (
44
- <form onSubmit={form.onSubmit((values) => console.log(values))}>
45
- <TextInput {...field(form, 'name')} label="Name" />
46
- <TextInput {...field(form, 'email')} label="Email" />
47
- <NumberInput {...field(form, 'amount')} label="Amount" />
48
- <Button type="submit">Submit</Button>
49
- </form>
50
- )
51
- }
52
- ```
53
-
54
- - **`mode: 'uncontrolled'`** is the default — avoids per-keystroke re-renders. Pass
55
- `mode: 'controlled'` if real-time derived state is needed.
56
- - **Schema param** types against `StandardSchemaV1` from `basalt-ui` (vendored spec, not a package).
57
- A real Valibot or Zod 4 schema is structurally assignable — no wrapper needed.
58
- - **`{ sync: true }`** keeps `form.validate()` / `form.isValid()` synchronous for sync schemas.
59
- Omit only when the schema has async refinements.
60
-
61
- ## field
62
-
63
- `field(form, path)` bundles `getInputProps(path)` + `key(path)` into one spread — the two things
64
- every Mantine v9 uncontrolled field requires:
65
-
66
- ```ts
67
- <TextInput {...field(form, 'email')} label="Email" />
68
- // equivalent to:
69
- <TextInput {...form.getInputProps('email')} key={form.key('email')} label="Email" />
70
- ```
71
-
72
- ## schemaResolver options
73
-
74
- - **`{ sync: true }`** — required for sync schemas (Zod 4, Valibot, ArkType). Keeps
75
- `form.validate()` and `form.isValid()` synchronous.
76
- - **Zod 4** — `import { z } from 'zod'`. Zod 4's root export implements Standard Schema natively.
77
- - **Valibot** — `import * as v from 'valibot'`. No separate resolver needed; the object schema
78
- is directly assignable to `StandardSchemaV1`.
79
- - **ArkType**, **Effect/Schema** — also implement Standard Schema natively.
80
- - Type schema params against `StandardSchemaV1` from `basalt-ui` (never `ZodSchema`).
81
-
82
- ## FormErrorSummary
83
-
84
- An accessible error-summary component for long forms and screen readers. Renders a Mantine `Alert`
85
- with `role="alert"` listing all field errors. Returns `null` when the form is clean.
86
-
87
- ```tsx
88
- import { FormErrorSummary } from 'basalt-ui/forms'
89
-
90
- // Place at the top of the form so screen readers announce errors on submit:
91
- ;<form onSubmit={form.onSubmit(handleSubmit)}>
92
- <FormErrorSummary form={form} title="Fix these before submitting" />
93
- {/* fields */}
94
- </form>
95
- ```
96
-
97
- - Place at the **top of the form** so AT focus moves to the summary on submit failure.
98
- - `title` defaults to `"Please fix the following errors"`.
99
- - Renders `null` (no DOM node) when `form.errors` is empty — safe to include unconditionally.
100
-
101
- ## useFormDraft
102
-
103
- `useFormDraft` persists form values to `localStorage` via `createPersistedState` (versioned,
104
- SSR-safe, cross-tab). It restores the draft on mount and exposes `clearDraft` + `saveDraft`.
105
-
106
- ```ts
107
- import { useBasaltForm, field, FormErrorSummary, useFormDraft } from 'basalt-ui/forms'
108
- import { Button, TextInput } from '@mantine/core'
109
-
110
- const INITIAL: Values = { name: '', email: '' }
111
-
112
- function EditForm() {
113
- const { clearDraft, saveDraft } = useFormDraft(form, { key: 'edit-form', version: 1 })
114
-
115
- // Wire saveDraft into useBasaltForm's onValuesChange for automatic autosave:
116
- const form = useBasaltForm({
117
- initialValues: INITIAL,
118
- schema: Schema,
119
- onValuesChange: saveDraft, // ← automatic autosave on every field change
120
- })
121
-
122
- return (
123
- <form
124
- onSubmit={form.onSubmit((values) => {
125
- submit(values)
126
- clearDraft() // ← clear on success
127
- })}
128
- >
129
- <FormErrorSummary form={form} />
130
- <TextInput {...field(form, 'name')} label="Name" />
131
- <Button type="submit">Submit</Button>
132
- <Button variant="subtle" onClick={clearDraft}>Discard draft</Button>
133
- </form>
134
- )
135
- }
136
- ```
137
-
138
- ### Autosave mechanism
139
-
140
- `@mantine/form` v9 has no built-in whole-form change subscription on an existing form instance —
141
- `form.watch` is per-field only. The correct autosave paths are:
142
-
143
- 1. **`useBasaltForm({ onValuesChange: saveDraft })`** (recommended) — `onValuesChange` fires on every
144
- value change with `(values, previous)`. Pass `saveDraft` directly (it snapshots `form.getValues()`
145
- internally).
146
- 2. **`<TextInput {...field(form, 'name')} onBlur={saveDraft} />`** — explicit blur-based autosave.
147
- Use this for externally-created forms or when keystroke-level persistence is too aggressive.
148
-
149
- ### Key must be stable
150
-
151
- The `key` option must not change between renders. Changing it recreates the storage instance and
152
- loses the current draft. Use a module-level constant or a prop passed down from the route.
153
-
154
- ### Version bump
155
-
156
- Increment `version` when the form values shape changes (added/removed fields, renamed keys). The
157
- draft is automatically discarded and falls back to `initialValues` — no migration needed for simple
158
- shape changes.
159
-
160
- ### clearDraft on success
161
-
162
- Always call `clearDraft()` in `onSubmit`'s success path so stale form data doesn't reappear after
163
- a successful save:
164
-
165
- ```ts
166
- form.onSubmit(async (values) => {
167
- await mutation.mutateAsync(values)
168
- clearDraft()
169
- })
170
- ```
171
-
172
- ## List helpers
173
-
174
- For dynamic lists: `form.insertListItem('items', {...})` / `form.removeListItem('items', index)`.
175
- Access via `form.getValues().items[index]` (uncontrolled) and bind with
176
- `form.getInputProps('items.0.field')` / `field(form, 'items.0.field')`.
177
-
178
- ## Submission
179
-
180
- ```ts
181
- form.onSubmit((values) => mutation.mutate(values))
182
- ```
183
-
184
- Reset after success with `form.reset()` — never mutate `form.values` directly. Surface validation
185
- as inline field errors via `getInputProps`; toast only on submit-level failures.
186
- See basalt-query.md for the mutation/invalidation pattern.
@@ -1,264 +0,0 @@
1
- ---
2
- source: basalt-ui
3
- description: Notification conventions for basalt-ui apps — notify helpers, typed registry, persisted history store, bell/center, and BasaltNotifications overlay from the shipped ./notifications battery. @mantine/notifications is an optional peer.
4
- paths:
5
- - 'src/**'
6
- - 'apps/**/src/**'
7
- ---
8
-
9
- # Basalt Notifications
10
-
11
- basalt-ui ships `./notifications` — a Mantine notification adapter battery providing `notify`,
12
- `notifySuccess/Error/Warning/Info`, `notifyPromise`, `defineNotifications`, `emit`,
13
- `useNotificationHistory`, `NotificationBell`, `NotificationCenter`, and `BasaltNotifications`
14
- on top of `@mantine/notifications`. `@mantine/notifications` is an **optional peer** — install it
15
- before using this battery:
16
-
17
- ```bash
18
- bun add @mantine/notifications
19
- ```
20
-
21
- ## Mount BasaltNotifications
22
-
23
- Render **one** `<BasaltNotifications />` inside `BasaltProvider`, before the router. The overlay is
24
- opt-in — it is NOT auto-mounted from `BasaltProvider` to keep the peer out of the root barrel.
25
-
26
- ```tsx
27
- import { BasaltNotifications } from 'basalt-ui/notifications'
28
- import { BasaltProvider } from 'basalt-ui'
29
-
30
- createRoot(root).render(
31
- <BasaltProvider>
32
- <BasaltNotifications />
33
- <App />
34
- </BasaltProvider>,
35
- )
36
- ```
37
-
38
- `BasaltNotifications` wraps Mantine's `<Notifications />` with basalt defaults:
39
- `position="bottom-right"`, `autoClose=4000`, `limit=5`. Pass props to override:
40
-
41
- ```tsx
42
- <BasaltNotifications position="top-right" autoClose={3000} limit={3} />
43
- ```
44
-
45
- NOTE: `BasaltOverlays` (from `basalt-ui/commands`) is the shipped composable mount that wires
46
- notifications together with spotlight/modals in one place. A standalone `<BasaltNotifications />`
47
- remains available for apps that don't use the overlays layer.
48
-
49
- ## notify
50
-
51
- `notify(opts)` is the core helper. It maps intent → Mantine color name, sets the correct aria
52
- `role` for screen reader announcement, applies per-intent `autoClose` defaults, and records the
53
- toast to the persisted history store. Returns the notification id string.
54
-
55
- ```ts
56
- import { notify } from 'basalt-ui/notifications'
57
-
58
- notify({ intent: 'success', message: 'Profile saved' })
59
- notify({ intent: 'error', message: 'Upload failed', title: 'Error', autoClose: false })
60
- notify({ intent: 'warning', message: 'Session expires soon', dedupKey: 'session-warn' })
61
- ```
62
-
63
- ### Intent → Mantine color + aria role + autoClose
64
-
65
- | Intent | `color=` | `role=` | Default autoClose |
66
- | ------- | ---------- | --------------------- | ------------------------- |
67
- | success | `'green'` | `'status'` (polite) | 2000ms |
68
- | info | `'blue'` | `'status'` (polite) | 3000ms |
69
- | warning | `'yellow'` | `'alert'` (assertive) | 5000ms |
70
- | error | `'red'` | `'alert'` (assertive) | `false` (until dismissed) |
71
-
72
- **Never pass raw Mantine colors directly.** Always use `notify`'s `intent` field — the mapping is
73
- the canonical source so screen-reader announcement and autoClose stay consistent.
74
-
75
- ### Two-layer dedup
76
-
77
- 1. **Mantine id-dedup**: passing the same `id` to `notifications.show` is a no-op on screen
78
- (Mantine's built-in dedup). Pass `id` to prevent a toast from appearing twice.
79
- 2. **dedupKey window**: a module-level guard skips any `notify` call using the same `dedupKey`
80
- within 1500ms — useful for event-driven triggers that may fire rapidly.
81
-
82
- ```ts
83
- // Fire rapidly — only the first call within 1500ms shows:
84
- notify({ intent: 'warning', message: 'Rate limit', dedupKey: 'rate-limit-warn' })
85
- ```
86
-
87
- ## Convenience wrappers
88
-
89
- ```ts
90
- import { notifySuccess, notifyError, notifyWarning, notifyInfo } from 'basalt-ui/notifications'
91
-
92
- notifySuccess('Changes saved')
93
- notifyError('Upload failed', { title: 'Storage error' })
94
- notifyWarning('Session expires in 5 minutes')
95
- notifyInfo('New version available', { autoClose: 10000 })
96
- ```
97
-
98
- Each wrapper calls `notify` with the matching intent. The second arg takes all `NotifyOptions`
99
- fields except `intent` and `message`.
100
-
101
- ## notifyPromise
102
-
103
- For async operations — shows a loading toast, then updates to success or error on settle.
104
- Uses `notifications.show` for loading, then `notifications.update` for the final state (required
105
- by Mantine to replace an existing id — `show` with the same id is a no-op).
106
-
107
- ```ts
108
- import { notifyPromise } from 'basalt-ui/notifications'
109
-
110
- await notifyPromise(uploadFile(file), {
111
- loading: 'Uploading…',
112
- success: 'Upload complete',
113
- error: 'Upload failed',
114
- })
115
- ```
116
-
117
- The promise is passed through — `await notifyPromise(...)` resolves/rejects as normal. The final
118
- state is recorded to the notification history store (loading state is not recorded).
119
-
120
- ## defineNotifications + emit
121
-
122
- `defineNotifications` is a const-generic factory (mirrors `defineSeries` from `./tokens`) for
123
- typed notification kind registries. Augment `BasaltRegister.notifications` once with the spec map
124
- type, then use `emit` — unknown kinds are tsc errors.
125
-
126
- ```ts
127
- // notifications.ts — define and augment (app-side, once):
128
- import { defineNotifications } from 'basalt-ui/notifications'
129
-
130
- export const NOTIFICATIONS = defineNotifications({
131
- 'upload:success': { intent: 'success', toMessage: (p: { name: string }) => `Uploaded ${p.name}` },
132
- 'upload:error': { intent: 'error', toMessage: (_: unknown) => 'Upload failed' },
133
- 'session:warn': { intent: 'warning', toMessage: () => 'Session expires soon' },
134
- })
135
-
136
- declare module 'basalt-ui' {
137
- interface BasaltRegister {
138
- notifications: typeof NOTIFICATIONS
139
- }
140
- }
141
- ```
142
-
143
- ```ts
144
- // usage.ts — typed emit, unknown kinds fail tsc:
145
- import { emit } from 'basalt-ui/notifications'
146
-
147
- emit('upload:success', { name: 'photo.jpg' }) // ✓ typed payload
148
- emit('upload:error', null) // ✓
149
- emit('nonexistent', null) // ✗ tsc error
150
- ```
151
-
152
- `emit` resolves `intent` + calls `toMessage(payload)` from the registered spec, then calls
153
- `notify()`. Pass extra `NotifyOptions` as a third argument to override spec defaults.
154
-
155
- ## defineNotification (single-spec helper)
156
-
157
- Mirror of `defineOverlay` from `./commands` — types a single notification spec without registering
158
- it. Use when splitting notification definitions across files before merging into `defineNotifications`.
159
-
160
- ```ts
161
- import { defineNotification, defineNotifications } from 'basalt-ui/notifications'
162
-
163
- // Type an isolated spec constant — useful when splitting across files:
164
- const uploadSuccess = defineNotification({
165
- intent: 'success',
166
- toMessage: (p: { name: string }) => `Uploaded ${p.name}`,
167
- })
168
-
169
- // Merge into the full registry (defineNotifications does the actual registration):
170
- export const NOTIFICATIONS = defineNotifications({ 'upload:success': uploadSuccess })
171
- ```
172
-
173
- WARNING: `defineNotification` only TYPES — it does NOT register. Only `defineNotifications(map)`
174
- registers. Calling `emit` without a prior `defineNotifications` is a no-op at runtime.
175
-
176
- ### `NotificationSpec` has no `title`/`icon` — by design
177
-
178
- `NotificationSpec` is `{ intent, toMessage? }` — deliberately no `title`/`icon` field. Those stay
179
- per-call `NotifyOptions` (`emit(kind, payload, { title: '…' })` or `notify({ ..., title: '…' })`),
180
- so a title that varies per call site isn't forced into the registry. If every call to a given kind
181
- repeats the same title, pass it at each call site — there's no registry-level default to fall back
182
- on.
183
-
184
- ### Colors beyond the 4 intents live outside the registry
185
-
186
- `notify`/`emit` only resolve `intent` to one of the four Mantine colors (§ Intent → Mantine color
187
- above) — there's no `intent` override and no raw-color escape in `NotifyOptions`. A feature needing
188
- per-category colored toasts beyond `success`/`info`/`warning`/`error` (e.g. achievement toasts keyed
189
- by achievement type) cannot ride the typed registry. The documented stance: call Mantine's raw
190
- `notifications.show({ color, ... })` directly, outside `notify`/`emit`. The cost is explicit — a
191
- raw `notifications.show` toast is invisible to the persisted history store, the bell, and the
192
- notification center, since only `notify`/`notifyPromise` record to history.
193
-
194
- ## Notification history store
195
-
196
- Every `notify()` / `notifyPromise()` call records to a module-level persisted external store
197
- (own `useSyncExternalStore` + versioned `{ v, value }` envelope under `basalt:notifications` in
198
- localStorage). The store is **not** hook-scoped — `add`, `markRead`, `markAllRead`, `clear` are
199
- module-level and callable outside React.
200
-
201
- Ring-buffer cap: 50 items. Items store `{ id, intent, title?, message, createdAt, readAt? }` —
202
- **message and title are strings** (ReactNode is not serializable). Cross-tab sync via the
203
- `storage` event.
204
-
205
- ### useNotificationHistory
206
-
207
- ```ts
208
- import { useNotificationHistory } from 'basalt-ui/notifications'
209
-
210
- const { items, unreadCount, markRead, markAllRead, clear } = useNotificationHistory()
211
- ```
212
-
213
- | Field | Type | Description |
214
- | --------------- | --------------------------- | ------------------------------------------ |
215
- | `items` | `NotificationHistoryItem[]` | All history (newest first, ring-buffer 50) |
216
- | `unreadCount` | `number` | Items with no `readAt` |
217
- | `markRead(id)` | `(id: string) => void` | Mark one item read |
218
- | `markAllRead()` | `() => void` | Mark all items read |
219
- | `clear()` | `() => void` | Clear all history |
220
-
221
- ## NotificationBell
222
-
223
- A Mantine `ActionIcon` + `Indicator` showing unread count. Opens a `NotificationCenter` in a
224
- `Popover`. Marks all items read when the center opens. Designed for the shell `globalActions` slot.
225
-
226
- ```tsx
227
- import { NotificationBell } from 'basalt-ui/notifications'
228
- import { BasaltShell } from 'basalt-ui'
229
- ;<BasaltShell globalActions={<NotificationBell />} {...rest} />
230
- ```
231
-
232
- The bell renders a bell SVG icon (no `@tabler/icons` dependency — icons are ReactNode per house
233
- rules). The unread indicator is capped at `9+` for counts above 9.
234
-
235
- ## NotificationCenter
236
-
237
- Lists history items (title, message, relative time, unread dot) with "Mark all read" and "Clear"
238
- actions. Consumes `useNotificationHistory`. Composable — mount it inside any Popover, Drawer, or
239
- panel:
240
-
241
- ```tsx
242
- import { NotificationCenter } from 'basalt-ui/notifications'
243
- import { Drawer } from '@mantine/core'
244
- ;<Drawer opened={opened} onClose={close} title="Notifications">
245
- <NotificationCenter maxHeight={500} />
246
- </Drawer>
247
- ```
248
-
249
- `NotificationBell` uses `NotificationCenter` internally. Mount `NotificationCenter` directly only
250
- when you need a custom container (e.g. a full-page notifications view or a custom Drawer).
251
-
252
- ## Deferred (not yet shipped)
253
-
254
- The following features are advisory notes for future work — they are NOT part of the current battery:
255
-
256
- - **Web Push** (`usePushSubscription` + `sw.js` + Elysia push route): server-side push delivery
257
- for background notifications. Requires a service worker, a push endpoint, and a vapid key.
258
- - **Visibility-aware delivery buffering**: buffer toasts while the tab is hidden (`document.hidden`)
259
- and flush on `visibilitychange` — prevents lost toasts when the tab is backgrounded.
260
- - **IntersectionObserver per-item mark-seen**: replace the mark-read-on-open approach with
261
- per-item IntersectionObserver so only viewed items are marked read (higher fidelity).
262
- - **Cross-device read-sync**: propagate `readAt` via an Elysia endpoint so read state syncs
263
- across browser sessions (requires auth and a backend store).
264
- - **Quiet hours**: suppress non-error toasts during a configurable time window.
@@ -1,187 +0,0 @@
1
- ---
2
- source: basalt-ui
3
- description: TanStack Query conventions for basalt-ui apps — query factories, Suspense reads, and mutation/invalidation. basalt-ui ships ./query (createBasaltQueryClient + unwrap + devtools); this rule covers the advisory consumer patterns on top.
4
- paths:
5
- - 'src/lib/queries/**'
6
- - 'apps/**/src/lib/queries/**'
7
- - 'src/routes/**'
8
- - 'apps/**/src/routes/**'
9
- ---
10
-
11
- # Basalt Query — TanStack Query Conventions
12
-
13
- basalt-ui ships `./query` — a thin TanStack Query adapter providing `createBasaltQueryClient`,
14
- `unwrap`, `BasaltQueryDevtools`, and convenience re-exports (`QueryClientProvider`,
15
- `QueryErrorResetBoundary`, `useQueryErrorResetBoundary`). @tanstack/react-query is a **required**
16
- peer — `BasaltProvider` hard-requires it at build time.
17
-
18
- `./query` also re-exports the primary fetch hooks so consumers never need to dual-import from both
19
- `basalt-ui/query` and `@tanstack/react-query`:
20
-
21
- ```ts
22
- import {
23
- useQuery,
24
- useSuspenseQuery,
25
- useMutation,
26
- useQueryClient,
27
- useInfiniteQuery,
28
- queryOptions,
29
- } from 'basalt-ui/query'
30
- ```
31
-
32
- This rule covers the advisory consumer patterns on top of that adapter. Nothing in the framework
33
- enforces these conventions beyond the `unwrap` seam.
34
-
35
- ## Query factories
36
-
37
- Each resource has a factory in `src/lib/queries/<resource>.ts`:
38
-
39
- ```ts
40
- import { queryOptions, useSuspenseQuery } from 'basalt-ui/query'
41
- import { unwrap } from 'basalt-ui/query'
42
- import { api } from '../api-client'
43
-
44
- export const resourceQueries = {
45
- list: (params: ListParams) =>
46
- queryOptions({
47
- queryKey: ['resource', 'list', params],
48
- queryFn: () => unwrap(api.resource.get({ query: params })),
49
- }),
50
- summary: () =>
51
- queryOptions({
52
- queryKey: ['resource', 'summary'],
53
- queryFn: () => unwrap(api.resource.summary.get()),
54
- }),
55
- }
56
- ```
57
-
58
- - Key hierarchy: `[resource, action, ...params]`.
59
- - `queryOptions` and all hooks import from `basalt-ui/query` — do not dual-import from
60
- `@tanstack/react-query` directly.
61
- - `queryFn` unwraps typed responses via `unwrap()` — throws on the error branch so failures surface
62
- to the nearest error boundary or TanStack Query's error state. Also throws when `data` is `null`
63
- with no error (e.g. a 204 No Content or a silent transport failure that returns
64
- `{ data: null, error: null }`). Both cases are bugs the caller must address.
65
- - Never use raw strings as query keys in components — always import from the factory.
66
-
67
- ## Reading data in components
68
-
69
- ```ts
70
- import { useSuspenseQuery } from 'basalt-ui/query'
71
-
72
- const { data } = useSuspenseQuery(resourceQueries.summary())
73
- ```
74
-
75
- - Always `useSuspenseQuery` inside loader-prefetched routes (never `useQuery`) — pairs with the
76
- router's `ensureQueryData` (see basalt-router.md).
77
- - Wrap pages or panels in `<Suspense fallback={…}>` at the route level.
78
-
79
- ## Rendering a query — use `QueryState`, don't write the switch
80
-
81
- ```tsx
82
- import { QueryState } from 'basalt-ui' // NOT basalt-ui/query — it renders Mantine
83
-
84
- function Library() {
85
- const q = useQuery(imageQueries.list())
86
- return (
87
- <QueryState
88
- query={q}
89
- variant="page" // or "section" for a panel inside a page
90
- errorTitle="Could not load images"
91
- empty={{ title: 'No images yet', description: 'Upload one to get started.' }}
92
- isEmpty={(d) => d.items.length === 0}
93
- >
94
- {(data) => <ImageGrid items={data.items} />}
95
- </QueryState>
96
- )
97
- }
98
- ```
99
-
100
- **Rendering only the empty branch is the bug this retires.** A consumer's library showed
101
- `No images` on a **500**, and a share detail showed `Share not found` on a dropped connection —
102
- "nothing here" is what an app renders when nobody wrote the other three branches. One import
103
- replaced 204 hand-rolled lines across 10 call sites there.
104
-
105
- Precedence, in evaluation order:
106
-
107
- | Condition | Renders |
108
- | ----------------------------------- | ------------------------------------------------------------------------- |
109
- | `isError`, no `data` | `ErrorState` — `errorTitle` / `errorFallback` / `errorAction`, with Retry |
110
- | no `data`, `fetchStatus === 'idle'` | the `empty` copy (nothing, if `empty` is omitted) |
111
- | no `data` | `loading`, else `LoadingState` |
112
- | `data`, `isEmpty(data)` | the `empty` copy |
113
- | `data` | `children` — a node, or `(data) => node` |
114
- | `data` **and** `isError` | a fixed _Showing cached data_ banner above `children` |
115
-
116
- `errorTitle` / `errorFallback` / `errorAction` reach the no-data error branch only; the stale-data
117
- banner carries its own copy. `empty` is `{ title, description?, icon?, action? }` — `description`
118
- is optional, so a compact panel needs no invented second sentence.
119
-
120
- **`useQuery`, not `useSuspenseQuery`.** A suspense read never hands you an undefined `data` or an
121
- error branch — it throws to the nearest boundary — so `QueryState` has nothing to decide there.
122
- Suspense + an error boundary and `QueryState` are the two ways to render a query; pick one per read.
123
-
124
- `LoadingState` and `ErrorState` ship beside it as the escape hatch for a page that must place its
125
- branches in different DOM positions.
126
-
127
- **`query` is a structural subset, checked at runtime.** It needs `{ data, isError, error,
128
- fetchStatus, refetch }` — a TanStack `UseQueryResult` satisfies it, and a hand-composed object is
129
- legal. Because the subset gives up the compiler's help, a missing `isError` **throws** naming the
130
- field rather than rendering a false claim about the data.
131
-
132
- Decoding an unknown error into copy is `toErrorMessage(err, fallback?)` from `basalt-ui/query`,
133
- with `errorStatus(err)` for the status; `toErrorMessage` folds the status into the fallback when the
134
- body decodes to nothing readable, so an opaque envelope never renders as `{}`.
135
-
136
- ## Mutations
137
-
138
- ```ts
139
- const mutation = useMutation({
140
- mutationFn: (body) => unwrap(api.resource.post({ body })),
141
- onSuccess: () => {
142
- queryClient.invalidateQueries({ queryKey: ['resource'] })
143
- notifications.show({ message: 'Saved', color: 'green' })
144
- },
145
- })
146
- ```
147
-
148
- - Invalidate by resource prefix (first key segment) — catches list + summary at once.
149
- - Show a notification on success; let errors bubble to the global error boundary.
150
- - For the in-button success flip (loading spinner → green `IconCheck`), drive it off `mutation.isPending`
151
- / `mutation.isSuccess` — see the interaction-feedback section in basalt-mantine.md.
152
-
153
- ## DevTools
154
-
155
- Use `BasaltQueryDevtools` from `basalt-ui/query` — it is lazy, production-excluded, and safe to
156
- import unconditionally:
157
-
158
- ```tsx
159
- import { BasaltQueryDevtools, QueryClientProvider } from 'basalt-ui/query'
160
-
161
- function Root() {
162
- return (
163
- <QueryClientProvider client={client}>
164
- {children}
165
- <BasaltQueryDevtools initialIsOpen={false} />
166
- </QueryClientProvider>
167
- )
168
- }
169
- ```
170
-
171
- Never import `@tanstack/react-query-devtools` directly in app code — it is an optional peer and
172
- the lazy wrapper handles the production guard for you.
173
-
174
- ## Eden footguns (hard authoring rules)
175
-
176
- Two patterns silently degrade Eden Treaty inference to `any` — both must be avoided:
177
-
178
- 1. **Non-chained route definitions**: Elysia routes MUST be method-chained on the `app` instance.
179
- A standalone `app.get(...)` (reassigned or called without chaining back to the root) drops the
180
- `App` type to `any`, which propagates into the Treaty client and loses all type safety.
181
-
182
- 2. **Mismatched tsconfig path aliases**: The client package and the server package MUST share or
183
- extend one root `tsconfig` so path aliases (`~/*`, `@/*`, etc.) resolve identically in both.
184
- A mismatch causes Eden's type extraction to fail silently and degrade to `any`.
185
-
186
- Both footguns produce no TypeScript error at the point of breakage — they only manifest as `any`
187
- types on the Treaty client, making them hard to spot. Check these first when Eden types regress.