basalt-ui 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (462) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +18 -0
  3. package/MIGRATING.md +546 -20
  4. package/README.md +45 -32
  5. package/agent/rules/basalt-batteries.md +205 -0
  6. package/agent/rules/basalt-charts.md +125 -555
  7. package/agent/rules/basalt-controls.md +155 -0
  8. package/agent/rules/basalt-mantine.md +146 -439
  9. package/agent/rules/basalt-state.md +113 -96
  10. package/agent/rules/basalt-tokens.md +126 -288
  11. package/agent/skills/basalt-app/SKILL.md +70 -195
  12. package/agent/skills/basalt-charts/SKILL.md +54 -234
  13. package/agent/skills/basalt-design/SKILL.md +56 -248
  14. package/agent/templates/CLAUDE-block.md.tpl +31 -40
  15. package/agent/templates/DESIGN.md.tpl +22 -51
  16. package/configs/oxlint-plugin.js +1275 -31
  17. package/configs/oxlint-plugin.test.ts +947 -11
  18. package/configs/oxlint.json +11 -3
  19. package/dist/charts/cursor/resolve.d.ts +18 -0
  20. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  21. package/dist/charts/cursor/resolve.js +15 -6
  22. package/dist/charts/cursor/scope.d.ts +6 -3
  23. package/dist/charts/cursor/scope.d.ts.map +1 -1
  24. package/dist/charts/cursor/store.d.ts +5 -1
  25. package/dist/charts/cursor/store.d.ts.map +1 -1
  26. package/dist/charts/cursor/store.js +4 -4
  27. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  28. package/dist/charts/hooks/useChartCursor.js +9 -8
  29. package/dist/charts/index.d.ts +2 -2
  30. package/dist/charts/index.d.ts.map +1 -1
  31. package/dist/charts/primitives/ChartCard.d.ts +34 -9
  32. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.js +29 -114
  34. package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
  35. package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
  36. package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
  37. package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
  38. package/dist/charts/sparklines/BarSparkline.js +18 -4
  39. package/dist/cli/check-coverage.test.d.ts +2 -0
  40. package/dist/cli/check-coverage.test.d.ts.map +1 -0
  41. package/dist/cli/index.d.ts +67 -4
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +225 -7
  44. package/dist/connectivity/connectivity-indicator.d.ts +4 -1
  45. package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
  46. package/dist/connectivity/connectivity-indicator.js +1 -1
  47. package/dist/content/index.d.ts +5 -3
  48. package/dist/content/index.d.ts.map +1 -1
  49. package/dist/content/index.js +4 -2
  50. package/dist/content/prose.module.css +7 -28
  51. package/dist/controls/actions.d.ts +169 -0
  52. package/dist/controls/actions.d.ts.map +1 -0
  53. package/dist/controls/actions.js +287 -0
  54. package/dist/controls/actions.module.css +62 -0
  55. package/dist/controls/actions.module.css.d.ts +13 -0
  56. package/dist/controls/actions.test.d.ts +2 -0
  57. package/dist/controls/actions.test.d.ts.map +1 -0
  58. package/dist/controls/actions.type-guard.test.d.ts +2 -0
  59. package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
  60. package/dist/controls/compare-filter.d.ts +41 -0
  61. package/dist/controls/compare-filter.d.ts.map +1 -0
  62. package/dist/controls/compare-filter.js +23 -0
  63. package/dist/controls/control-group.d.ts +19 -0
  64. package/dist/controls/control-group.d.ts.map +1 -0
  65. package/dist/controls/control-group.js +20 -0
  66. package/dist/controls/control-group.test.d.ts +2 -0
  67. package/dist/controls/control-group.test.d.ts.map +1 -0
  68. package/dist/controls/controls.module.css +542 -0
  69. package/dist/controls/controls.module.css.d.ts +35 -0
  70. package/dist/controls/controls.router.test.d.ts +2 -0
  71. package/dist/controls/controls.router.test.d.ts.map +1 -0
  72. package/dist/controls/enum-filter.d.ts +30 -0
  73. package/dist/controls/enum-filter.d.ts.map +1 -0
  74. package/dist/controls/enum-filter.js +91 -0
  75. package/dist/controls/filter-context.d.ts +55 -0
  76. package/dist/controls/filter-context.d.ts.map +1 -0
  77. package/dist/controls/filter-context.js +69 -0
  78. package/dist/controls/filter-pill.d.ts +28 -0
  79. package/dist/controls/filter-pill.d.ts.map +1 -0
  80. package/dist/controls/filter-pill.js +70 -0
  81. package/dist/controls/filter-set.d.ts +13 -0
  82. package/dist/controls/filter-set.d.ts.map +1 -0
  83. package/dist/controls/filter-set.js +125 -0
  84. package/dist/controls/filter-set.test.d.ts +2 -0
  85. package/dist/controls/filter-set.test.d.ts.map +1 -0
  86. package/dist/controls/filter-sheet.d.ts +117 -0
  87. package/dist/controls/filter-sheet.d.ts.map +1 -0
  88. package/dist/controls/filter-sheet.js +146 -0
  89. package/dist/controls/glyphs.d.ts +27 -0
  90. package/dist/controls/glyphs.d.ts.map +1 -0
  91. package/dist/controls/glyphs.js +49 -0
  92. package/dist/controls/index.d.ts +19 -0
  93. package/dist/controls/index.d.ts.map +1 -0
  94. package/dist/controls/index.js +44 -0
  95. package/dist/controls/multi-select-filter.d.ts +23 -0
  96. package/dist/controls/multi-select-filter.d.ts.map +1 -0
  97. package/dist/controls/multi-select-filter.js +89 -0
  98. package/dist/controls/peer-boundary.test.d.ts +2 -0
  99. package/dist/controls/peer-boundary.test.d.ts.map +1 -0
  100. package/dist/controls/props-contract.test.d.ts +2 -0
  101. package/dist/controls/props-contract.test.d.ts.map +1 -0
  102. package/dist/controls/range-filter.d.ts +29 -0
  103. package/dist/controls/range-filter.d.ts.map +1 -0
  104. package/dist/controls/range-filter.js +120 -0
  105. package/dist/controls/search-filter.d.ts +10 -0
  106. package/dist/controls/search-filter.d.ts.map +1 -0
  107. package/dist/controls/search-filter.js +81 -0
  108. package/dist/controls/select-filter.d.ts +74 -0
  109. package/dist/controls/select-filter.d.ts.map +1 -0
  110. package/dist/controls/select-filter.js +10 -0
  111. package/dist/controls/sync-button.d.ts +38 -0
  112. package/dist/controls/sync-button.d.ts.map +1 -0
  113. package/dist/controls/sync-button.js +119 -0
  114. package/dist/controls/sync-button.module.css +63 -0
  115. package/dist/controls/sync-button.module.css.d.ts +11 -0
  116. package/dist/controls/sync-button.test.d.ts +2 -0
  117. package/dist/controls/sync-button.test.d.ts.map +1 -0
  118. package/dist/controls/toggle-filter.d.ts +9 -0
  119. package/dist/controls/toggle-filter.d.ts.map +1 -0
  120. package/dist/controls/toggle-filter.js +42 -0
  121. package/dist/controls/view-tabs.d.ts +23 -0
  122. package/dist/controls/view-tabs.d.ts.map +1 -0
  123. package/dist/controls/view-tabs.js +59 -0
  124. package/dist/controls-dates/date-range-picker.d.ts +4 -0
  125. package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
  126. package/dist/controls-dates/date-range-picker.js +18 -0
  127. package/dist/controls-dates/index.d.ts +9 -0
  128. package/dist/controls-dates/index.d.ts.map +1 -0
  129. package/dist/controls-dates/index.js +4 -0
  130. package/dist/dashboard/delta-badge.d.ts +6 -12
  131. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  132. package/dist/dashboard/delta-badge.js +1 -50
  133. package/dist/dashboard/settings-section.d.ts +6 -4
  134. package/dist/dashboard/settings-section.d.ts.map +1 -1
  135. package/dist/dashboard/settings-section.js +23 -16
  136. package/dist/dashboard/settings-section.module.css +10 -0
  137. package/dist/dashboard/settings-section.module.css.d.ts +1 -0
  138. package/dist/dashboard/settings-section.test.d.ts +2 -0
  139. package/dist/dashboard/settings-section.test.d.ts.map +1 -0
  140. package/dist/dashboard/stat-card.d.ts +44 -9
  141. package/dist/dashboard/stat-card.d.ts.map +1 -1
  142. package/dist/dashboard/stat-card.js +80 -48
  143. package/dist/dashboard/stat-card.module.css +82 -0
  144. package/dist/dashboard/stat-card.module.css.d.ts +11 -0
  145. package/dist/data/data-table.d.ts +37 -15
  146. package/dist/data/data-table.d.ts.map +1 -1
  147. package/dist/data/data-table.js +88 -58
  148. package/dist/guard/grace.test.d.ts +20 -0
  149. package/dist/guard/grace.test.d.ts.map +1 -0
  150. package/dist/guard/index.d.ts +66 -3
  151. package/dist/guard/index.d.ts.map +1 -1
  152. package/dist/guard/index.js +82 -7
  153. package/dist/guard/types.d.ts +3 -3
  154. package/dist/guard/types.d.ts.map +1 -1
  155. package/dist/index.d.ts +12 -2
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +13 -10
  158. package/dist/nav/types.d.ts +78 -2
  159. package/dist/nav/types.d.ts.map +1 -1
  160. package/dist/notifications/bell.d.ts +4 -1
  161. package/dist/notifications/bell.d.ts.map +1 -1
  162. package/dist/notifications/index.d.ts +1 -1
  163. package/dist/router-tanstack/field.d.ts +14 -0
  164. package/dist/router-tanstack/field.d.ts.map +1 -0
  165. package/dist/router-tanstack/field.js +4 -0
  166. package/dist/router-tanstack/index.d.ts +6 -2
  167. package/dist/router-tanstack/index.d.ts.map +1 -1
  168. package/dist/router-tanstack/index.js +4 -0
  169. package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
  170. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  171. package/dist/router-tanstack/multi-search-param-store.js +19 -27
  172. package/dist/router-tanstack/search-param-store.d.ts +3 -87
  173. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  174. package/dist/router-tanstack/search-param-store.js +16 -45
  175. package/dist/router-tanstack/search-store.d.ts +81 -0
  176. package/dist/router-tanstack/search-store.d.ts.map +1 -0
  177. package/dist/router-tanstack/search-store.js +187 -0
  178. package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
  179. package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
  180. package/dist/router-tanstack/search-store.test.d.ts +2 -0
  181. package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
  182. package/dist/router-tanstack/use-nav.d.ts +0 -2
  183. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  184. package/dist/router-tanstack/use-nav.js +34 -12
  185. package/dist/router-tanstack/use-nav.test.d.ts +2 -0
  186. package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
  187. package/dist/section/index.d.ts +2 -0
  188. package/dist/section/index.d.ts.map +1 -0
  189. package/dist/section/index.js +4 -0
  190. package/dist/section/section.d.ts +24 -0
  191. package/dist/section/section.d.ts.map +1 -0
  192. package/dist/section/section.js +126 -0
  193. package/dist/section/section.module.css +104 -0
  194. package/dist/section/section.module.css.d.ts +11 -0
  195. package/dist/section/section.test.d.ts +2 -0
  196. package/dist/section/section.test.d.ts.map +1 -0
  197. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  198. package/dist/shell/app-breadcrumbs.js +18 -15
  199. package/dist/shell/app-header.module.css +77 -37
  200. package/dist/shell/app-header.module.css.d.ts +2 -1
  201. package/dist/shell/app-mobile-nav.d.ts +9 -2
  202. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  203. package/dist/shell/app-mobile-nav.js +98 -11
  204. package/dist/shell/app-mobile-nav.module.css +8 -0
  205. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  206. package/dist/shell/app-sidebar.d.ts +55 -28
  207. package/dist/shell/app-sidebar.d.ts.map +1 -1
  208. package/dist/shell/app-sidebar.js +116 -67
  209. package/dist/shell/app-sidebar.module.css +303 -20
  210. package/dist/shell/app-sidebar.module.css.d.ts +19 -1
  211. package/dist/shell/index.d.ts +55 -21
  212. package/dist/shell/index.d.ts.map +1 -1
  213. package/dist/shell/index.js +40 -18
  214. package/dist/shell/mobile-nav-model.d.ts +13 -2
  215. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  216. package/dist/shell/mobile-nav-model.js +15 -1
  217. package/dist/shell/nav-count-badge.d.ts +1 -1
  218. package/dist/shell/page-bar.d.ts +51 -0
  219. package/dist/shell/page-bar.d.ts.map +1 -0
  220. package/dist/shell/page-bar.js +154 -0
  221. package/dist/shell/page-bar.module.css +178 -0
  222. package/dist/shell/page-bar.module.css.d.ts +17 -0
  223. package/dist/shell/page-bar.test.d.ts +2 -0
  224. package/dist/shell/page-bar.test.d.ts.map +1 -0
  225. package/dist/shell/sidebar-block-model.d.ts +48 -0
  226. package/dist/shell/sidebar-block-model.d.ts.map +1 -0
  227. package/dist/shell/sidebar-block-model.js +39 -0
  228. package/dist/shell/sidebar-blocks.d.ts +32 -0
  229. package/dist/shell/sidebar-blocks.d.ts.map +1 -0
  230. package/dist/shell/sidebar-blocks.js +169 -0
  231. package/dist/shell/sidebar-search.d.ts +13 -1
  232. package/dist/shell/sidebar-search.d.ts.map +1 -1
  233. package/dist/shell/sidebar-search.js +34 -1
  234. package/dist/shell/sidebar-search.module.css +26 -0
  235. package/dist/shell/sidebar-search.module.css.d.ts +2 -0
  236. package/dist/state/fields.d.ts +392 -0
  237. package/dist/state/fields.d.ts.map +1 -0
  238. package/dist/state/fields.js +369 -0
  239. package/dist/state/fields.test.d.ts +2 -0
  240. package/dist/state/fields.test.d.ts.map +1 -0
  241. package/dist/state/persisted.d.ts +48 -0
  242. package/dist/state/persisted.d.ts.map +1 -0
  243. package/dist/state/persisted.js +105 -0
  244. package/dist/state.d.ts +11 -56
  245. package/dist/state.d.ts.map +1 -1
  246. package/dist/state.js +14 -120
  247. package/dist/styles.css +13 -0
  248. package/dist/surfaces.d.ts +106 -24
  249. package/dist/surfaces.d.ts.map +1 -1
  250. package/dist/surfaces.js +155 -19
  251. package/dist/theme/controls.module.css +16 -1
  252. package/dist/theme/ctl-theme.d.ts +32 -0
  253. package/dist/theme/ctl-theme.d.ts.map +1 -0
  254. package/dist/theme/ctl-theme.js +54 -0
  255. package/dist/theme/ctl-theme.test.d.ts +2 -0
  256. package/dist/theme/ctl-theme.test.d.ts.map +1 -0
  257. package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
  258. package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
  259. package/dist/theme/divider-law.test.d.ts +2 -0
  260. package/dist/theme/divider-law.test.d.ts.map +1 -0
  261. package/dist/theme/icon-slot.d.ts +44 -0
  262. package/dist/theme/icon-slot.d.ts.map +1 -0
  263. package/dist/theme/icon-slot.js +21 -0
  264. package/dist/theme/icon-slot.module.css +47 -0
  265. package/dist/theme/icon-slot.module.css.d.ts +8 -0
  266. package/dist/theme/icon-slot.test.d.ts +2 -0
  267. package/dist/theme/icon-slot.test.d.ts.map +1 -0
  268. package/dist/theme/index.d.ts +1 -0
  269. package/dist/theme/index.d.ts.map +1 -1
  270. package/dist/theme/index.js +82 -5
  271. package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
  272. package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
  273. package/dist/theme/segmented-control.module.css +20 -0
  274. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  275. package/dist/tokens/index.d.ts +12 -0
  276. package/dist/tokens/index.d.ts.map +1 -1
  277. package/dist/tokens/index.js +38 -8
  278. package/dist/tokens/palette.d.ts +97 -61
  279. package/dist/tokens/palette.d.ts.map +1 -1
  280. package/dist/tokens/palette.js +76 -38
  281. package/dist/tokens.css +10 -2
  282. package/dist/widget-header/delta-badge.d.ts +13 -0
  283. package/dist/widget-header/delta-badge.d.ts.map +1 -0
  284. package/dist/widget-header/delta-badge.js +29 -0
  285. package/dist/widget-header/delta-badge.test.d.ts +2 -0
  286. package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
  287. package/dist/widget-header/index.d.ts +3 -0
  288. package/dist/widget-header/index.d.ts.map +1 -0
  289. package/dist/widget-header/index.js +6 -0
  290. package/dist/widget-header/no-mantine.test.d.ts +2 -0
  291. package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
  292. package/dist/widget-header/widget-header.d.ts +37 -0
  293. package/dist/widget-header/widget-header.d.ts.map +1 -0
  294. package/dist/widget-header/widget-header.js +77 -0
  295. package/dist/widget-header/widget-header.module.css +335 -0
  296. package/dist/widget-header/widget-header.module.css.d.ts +24 -0
  297. package/dist/widget-header/widget-header.test.d.ts +2 -0
  298. package/dist/widget-header/widget-header.test.d.ts.map +1 -0
  299. package/llms.txt +30 -17
  300. package/package.json +14 -1
  301. package/src/charts/cursor/resolve.test.ts +29 -1
  302. package/src/charts/cursor/resolve.ts +38 -6
  303. package/src/charts/cursor/scope.tsx +6 -3
  304. package/src/charts/cursor/store.test.ts +19 -1
  305. package/src/charts/cursor/store.ts +10 -5
  306. package/src/charts/hooks/useChartCursor.ts +16 -8
  307. package/src/charts/index.ts +2 -2
  308. package/src/charts/primitives/CartesianChart.test.tsx +124 -0
  309. package/src/charts/primitives/ChartCard.test.tsx +66 -0
  310. package/src/charts/primitives/ChartCard.tsx +86 -146
  311. package/src/charts/sparklines/BarSparkline.tsx +43 -5
  312. package/src/cli/check-coverage.test.ts +297 -0
  313. package/src/cli/check-theme.test.ts +38 -14
  314. package/src/cli/index.ts +392 -16
  315. package/src/cli/init-sync.test.ts +97 -0
  316. package/src/connectivity/connectivity-indicator.tsx +16 -9
  317. package/src/content/index.ts +16 -3
  318. package/src/content/prose.module.css +7 -28
  319. package/src/content/prose.module.css.test.ts +8 -24
  320. package/src/controls/actions.module.css +62 -0
  321. package/src/controls/actions.module.css.d.ts +13 -0
  322. package/src/controls/actions.test.tsx +252 -0
  323. package/src/controls/actions.tsx +655 -0
  324. package/src/controls/actions.type-guard.test.ts +63 -0
  325. package/src/controls/compare-filter.tsx +64 -0
  326. package/src/controls/control-group.test.tsx +229 -0
  327. package/src/controls/control-group.tsx +63 -0
  328. package/src/controls/controls.module.css +542 -0
  329. package/src/controls/controls.module.css.d.ts +35 -0
  330. package/src/controls/controls.router.test.tsx +637 -0
  331. package/src/controls/enum-filter.tsx +159 -0
  332. package/src/controls/filter-context.tsx +153 -0
  333. package/src/controls/filter-pill.tsx +120 -0
  334. package/src/controls/filter-set.test.tsx +472 -0
  335. package/src/controls/filter-set.tsx +225 -0
  336. package/src/controls/filter-sheet.tsx +285 -0
  337. package/src/controls/glyphs.tsx +81 -0
  338. package/src/controls/index.ts +48 -0
  339. package/src/controls/multi-select-filter.tsx +140 -0
  340. package/src/controls/peer-boundary.test.ts +85 -0
  341. package/src/controls/props-contract.test.tsx +106 -0
  342. package/src/controls/range-filter.tsx +184 -0
  343. package/src/controls/search-filter.tsx +123 -0
  344. package/src/controls/select-filter.tsx +89 -0
  345. package/src/controls/sync-button.module.css +63 -0
  346. package/src/controls/sync-button.module.css.d.ts +11 -0
  347. package/src/controls/sync-button.test.tsx +183 -0
  348. package/src/controls/sync-button.tsx +197 -0
  349. package/src/controls/toggle-filter.tsx +60 -0
  350. package/src/controls/view-tabs.tsx +110 -0
  351. package/src/controls-dates/date-range-picker.tsx +42 -0
  352. package/src/controls-dates/index.ts +8 -0
  353. package/src/dashboard/delta-badge.tsx +4 -74
  354. package/src/dashboard/settings-section.module.css +10 -0
  355. package/src/dashboard/settings-section.module.css.d.ts +1 -0
  356. package/src/dashboard/settings-section.test.tsx +56 -0
  357. package/src/dashboard/settings-section.tsx +33 -30
  358. package/src/dashboard/stat-card.module.css +82 -0
  359. package/src/dashboard/stat-card.module.css.d.ts +11 -0
  360. package/src/dashboard/stat-card.test.tsx +67 -1
  361. package/src/dashboard/stat-card.tsx +209 -68
  362. package/src/data/data-table.test.tsx +96 -11
  363. package/src/data/data-table.tsx +220 -84
  364. package/src/guard/check-source.test.ts +126 -13
  365. package/src/guard/grace.test.ts +149 -0
  366. package/src/guard/index.ts +172 -29
  367. package/src/guard/types.ts +4 -2
  368. package/src/index.ts +46 -5
  369. package/src/nav/types.ts +84 -2
  370. package/src/notifications/bell.tsx +6 -3
  371. package/src/notifications/index.ts +1 -1
  372. package/src/router-tanstack/field.ts +31 -0
  373. package/src/router-tanstack/index.ts +24 -2
  374. package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
  375. package/src/router-tanstack/multi-search-param-store.ts +46 -77
  376. package/src/router-tanstack/search-param-store.ts +42 -159
  377. package/src/router-tanstack/search-store.router.test.tsx +661 -0
  378. package/src/router-tanstack/search-store.test.ts +418 -0
  379. package/src/router-tanstack/search-store.ts +380 -0
  380. package/src/router-tanstack/use-nav.test.tsx +187 -0
  381. package/src/router-tanstack/use-nav.ts +72 -14
  382. package/src/section/index.ts +1 -0
  383. package/src/section/section.module.css +104 -0
  384. package/src/section/section.module.css.d.ts +11 -0
  385. package/src/section/section.test.tsx +204 -0
  386. package/src/section/section.tsx +220 -0
  387. package/src/shell/app-breadcrumbs.tsx +44 -31
  388. package/src/shell/app-header.module.css +77 -37
  389. package/src/shell/app-header.module.css.d.ts +2 -1
  390. package/src/shell/app-mobile-nav.module.css +8 -0
  391. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  392. package/src/shell/app-mobile-nav.tsx +147 -8
  393. package/src/shell/app-sidebar-account.tsx +2 -2
  394. package/src/shell/app-sidebar.module.css +303 -20
  395. package/src/shell/app-sidebar.module.css.d.ts +19 -1
  396. package/src/shell/app-sidebar.test.tsx +456 -41
  397. package/src/shell/app-sidebar.tsx +276 -140
  398. package/src/shell/index.test.tsx +291 -5
  399. package/src/shell/index.tsx +137 -38
  400. package/src/shell/mobile-nav-model.test.ts +228 -7
  401. package/src/shell/mobile-nav-model.ts +53 -3
  402. package/src/shell/nav-count-badge.tsx +1 -1
  403. package/src/shell/page-bar.module.css +178 -0
  404. package/src/shell/page-bar.module.css.d.ts +17 -0
  405. package/src/shell/page-bar.test.tsx +316 -0
  406. package/src/shell/page-bar.tsx +315 -0
  407. package/src/shell/sidebar-block-model.ts +83 -0
  408. package/src/shell/sidebar-blocks.tsx +301 -0
  409. package/src/shell/sidebar-search.module.css +26 -0
  410. package/src/shell/sidebar-search.module.css.d.ts +2 -0
  411. package/src/shell/sidebar-search.tsx +71 -2
  412. package/src/state/fields.test.tsx +258 -0
  413. package/src/state/fields.ts +892 -0
  414. package/src/state/persisted.ts +211 -0
  415. package/src/state.ts +46 -240
  416. package/src/styles.css +13 -0
  417. package/src/surfaces.ts +203 -36
  418. package/src/theme/controls.module.css +16 -1
  419. package/src/theme/ctl-theme.test.tsx +275 -0
  420. package/src/theme/ctl-theme.tsx +138 -0
  421. package/src/theme/ctl-tier-coverage.test.ts +170 -0
  422. package/src/theme/density-relations.test.ts +106 -51
  423. package/src/theme/divider-law.test.ts +259 -0
  424. package/src/theme/icon-slot.module.css +47 -0
  425. package/src/theme/icon-slot.module.css.d.ts +8 -0
  426. package/src/theme/icon-slot.test.tsx +275 -0
  427. package/src/theme/icon-slot.tsx +59 -0
  428. package/src/theme/index.ts +165 -3
  429. package/src/theme/layout-rhythm-css.test.ts +72 -0
  430. package/src/theme/radius.test.ts +11 -1
  431. package/src/theme/segmented-control.module.css +20 -0
  432. package/src/theme/shadow-surfaces.test.ts +20 -7
  433. package/src/theme/spacing.test.ts +75 -27
  434. package/src/theme/use-basalt-spacing.ts +2 -2
  435. package/src/tokens/density.test.ts +7 -25
  436. package/src/tokens/index.ts +48 -8
  437. package/src/tokens/palette.ts +107 -73
  438. package/src/widget-header/delta-badge.test.tsx +60 -0
  439. package/src/widget-header/delta-badge.tsx +57 -0
  440. package/src/widget-header/index.ts +2 -0
  441. package/src/widget-header/no-mantine.test.ts +33 -0
  442. package/src/widget-header/widget-header.module.css +335 -0
  443. package/src/widget-header/widget-header.module.css.d.ts +24 -0
  444. package/src/widget-header/widget-header.test.tsx +187 -0
  445. package/src/widget-header/widget-header.tsx +168 -0
  446. package/agent/rules/basalt-agent.md +0 -452
  447. package/agent/rules/basalt-app.md +0 -246
  448. package/agent/rules/basalt-commands.md +0 -320
  449. package/agent/rules/basalt-content.md +0 -409
  450. package/agent/rules/basalt-data.md +0 -329
  451. package/agent/rules/basalt-forms.md +0 -186
  452. package/agent/rules/basalt-notifications.md +0 -264
  453. package/agent/rules/basalt-query.md +0 -187
  454. package/agent/rules/basalt-router.md +0 -336
  455. package/dist/content/article-filter-bar.d.ts +0 -40
  456. package/dist/content/article-filter-bar.d.ts.map +0 -1
  457. package/dist/content/article-filter-bar.js +0 -56
  458. package/dist/shell/page-header.d.ts +0 -13
  459. package/dist/shell/page-header.d.ts.map +0 -1
  460. package/dist/shell/page-header.js +0 -23
  461. package/src/content/article-filter-bar.tsx +0 -88
  462. package/src/shell/page-header.tsx +0 -35
@@ -1,329 +0,0 @@
1
- ---
2
- source: basalt-ui
3
- description: Data table and virtual list adapter battery from basalt-ui/data — BasaltDataTable (sortable TanStack Table + Mantine Table) and BasaltVirtualList (TanStack Virtual + Mantine Box). Both packages are optional peers.
4
- paths:
5
- - 'src/**'
6
- - 'apps/**/src/**'
7
- ---
8
-
9
- # Basalt Data
10
-
11
- basalt-ui ships `./data` — a Mantine-coupled data adapter battery providing `BasaltDataTable` and
12
- `BasaltVirtualList` backed by headless TanStack Table and TanStack Virtual respectively. Both
13
- optional peers must be installed before use:
14
-
15
- ```bash
16
- bun add @tanstack/react-table @tanstack/react-virtual
17
- ```
18
-
19
- ## BasaltDataTable
20
-
21
- A generic sortable data table over `@tanstack/react-table`, rendered with Mantine `Table`
22
- primitives (`Table.Thead/Tbody/Tr/Th/Td`). Client-side sorting is built in via `useState`; no
23
- server-side wiring required.
24
-
25
- ```tsx
26
- import { BasaltDataTable, createColumnHelper } from 'basalt-ui/data'
27
-
28
- type User = { name: string; email: string; age: number }
29
-
30
- const col = createColumnHelper<User>()
31
- const columns = [
32
- col.accessor('name', { header: 'Name' }),
33
- col.accessor('email', { header: 'Email' }),
34
- col.accessor('age', { header: 'Age' }),
35
- ]
36
-
37
- <BasaltDataTable
38
- data={users}
39
- columns={columns}
40
- enableSorting // default true — clickable sort headers with ↑/↓ indicators
41
- striped
42
- highlightOnHover
43
- emptyState={<Text c="dimmed">No users found.</Text>}
44
- />
45
- ```
46
-
47
- ### Props
48
-
49
- | Prop | Type | Default | Notes |
50
- | ------------------ | --------------------------- | -------- | ------------------------------------------------------ |
51
- | `data` | `T[]` | required | Row data array |
52
- | `columns` | `ColumnDef<T>[]` | required | TanStack column definitions |
53
- | `enableSorting` | `boolean` | `true` | Clickable headers, asc/desc toggle |
54
- | `striped` | `boolean` | — | Forwarded to Mantine `Table` |
55
- | `highlightOnHover` | `boolean` | — | Forwarded to Mantine `Table` |
56
- | `emptyState` | `ReactNode` | — | Shown when `data` is empty and not loading |
57
- | `isLoading` | `boolean` | `false` | Shows skeleton rows instead of empty-state/data |
58
- | `skeletonRows` | `number` | `5` | Number of skeleton rows when `isLoading` is true |
59
- | `initialSorting` | `SortingState` | `[]` | Initial sort state; drives `useState` initializer |
60
- | `onSortingChange` | `(s: SortingState) => void` | — | Called whenever sorting changes; omit for uncontrolled |
61
-
62
- Body chrome — all conditional pass-throughs to Mantine `Table` except `withTableBorder`, which
63
- basalt defaults to `true`:
64
-
65
- | Prop | Type | Notes |
66
- | --------------------------------------- | ------------------------------- | -------------------------------------------------------------------- |
67
- | `maxHeight` | `number \| string` | Caps the body height — renders `Table.ScrollContainer type="native"` |
68
- | `minWidth` | `number \| string` | Same container; `maxHeight` alone passes `minWidth={0}` |
69
- | `stickyHeader` / `stickyHeaderOffset` | `boolean` / `number \| string` | Forwarded to Mantine; pair with `maxHeight` |
70
- | `verticalSpacing` / `horizontalSpacing` | `MantineSpacing` | Cell padding |
71
- | `withRowBorders` / `withTableBorder` | `boolean` | `withTableBorder` defaults `true` here, overriding Mantine's `false` |
72
- | `meta: { align }` (per column) | `'left' \| 'center' \| 'right'` | Sets `textAlign` on BOTH the `th` and the `td` |
73
- | `meta: { numeral: false }` (per column) | `boolean` | Opts a numeric cell OUT of the mono-numeral style. Opt-out only |
74
-
75
- `meta.align` is a `ColumnMeta` module augmentation, so a typo'd key is a tsc error and a value
76
- outside the union **throws** naming the column — a money column never silently left-aligns. Set it
77
- once on the column def instead of repeating `textAlign: 'right'` on the header and the cell.
78
-
79
- **`meta.numeral: false` is needed less often than it looks.** The auto style sets `fontFamily`,
80
- `fontSize`, `fontWeight`, `fontVariantNumeric` and `color` on the `<td>` whenever the raw value is a
81
- number — but a child `Text` sets its own `color` and `fontWeight` and wins those, and never sets a
82
- `fontFamily`. **Only `fontFamily` leaks.** So `c="dimmed"` is not a reason to opt out (and on a
83
- percentage column the mono figures are what you want); `fw={600}` on an accent figure is, because
84
- the `<td>`'s monospace still overrides the family the `Text` inherited. One consumer measured this
85
- across three tables: **3 columns** genuinely needed the opt-out, not the 4 that were assumed. Reach
86
- for it when the cell renders its own chrome — a coloured `Text`, a `Badge`, a sparkline — and check
87
- what actually leaked before adding it. `null` is worth a thought too: `typeof null === 'object'`, so
88
- a `number | null` column goes mono only on its non-null rows, which is worse than uniformly wrong.
89
-
90
- ### Controlled sorting and URL sync
91
-
92
- `initialSorting` seeds the internal `useState` and `onSortingChange` is called on every sort
93
- change — the table stays internally managed (uncontrolled) when `onSortingChange` is omitted.
94
-
95
- To sync sort state with TanStack Router search params:
96
-
97
- ```tsx
98
- import { type SortingState } from 'basalt-ui/data'
99
-
100
- // In the route definition (validated search params):
101
- // sortBy: z.string().optional(), sortDir: z.enum(['asc','desc']).optional()
102
-
103
- function UsersTable() {
104
- const { sortBy, sortDir } = Route.useSearch()
105
- const navigate = Route.useNavigate()
106
-
107
- const initialSorting: SortingState = sortBy ? [{ id: sortBy, desc: sortDir === 'desc' }] : []
108
-
109
- return (
110
- <BasaltDataTable
111
- data={users}
112
- columns={columns}
113
- initialSorting={initialSorting}
114
- onSortingChange={(s) => {
115
- const col = s[0]
116
- navigate({
117
- search: (prev) => ({
118
- ...prev,
119
- sortBy: col?.id,
120
- sortDir: col?.desc ? 'desc' : 'asc',
121
- }),
122
- })
123
- }}
124
- />
125
- )
126
- }
127
- ```
128
-
129
- ### Column definitions
130
-
131
- Use `createColumnHelper<T>()` (re-exported from `basalt-ui/data`) for a typed accessor builder.
132
- `ColumnDef<T>` is generic over the row type only — `TValue` defaults to `unknown`, so no `any`
133
- is needed.
134
-
135
- ```ts
136
- import { createColumnHelper, type ColumnDef } from 'basalt-ui/data'
137
-
138
- type Row = { id: number; label: string }
139
- const col = createColumnHelper<Row>()
140
-
141
- // Accessor column — infers the value type from the key
142
- col.accessor('label', { header: 'Label' })
143
-
144
- // Display column — custom renderer, no accessor
145
- col.display({ id: 'actions', header: 'Actions', cell: (ctx) => <Button>Delete {ctx.row.original.id}</Button> })
146
- ```
147
-
148
- ### No `any`
149
-
150
- `ColumnDef<T>` defaults `TValue` to `unknown`, not `any`. Never cast column defs to `any` — use
151
- the correct `ColumnDef<T>` type or `ColumnDef<T, unknown>`.
152
-
153
- ### Also shipped, and easy to miss
154
-
155
- Global search (`enableGlobalFilter`, `globalFilterPlaceholder`, `searchIcon`,
156
- `initialGlobalFilter`, `onGlobalFilterChange`), faceted filters (`facets`), a toolbar slot
157
- (`toolbarActions`), pagination client- or server-side (`enablePagination`, `pageSizeOptions`,
158
- `initialPagination`, `onPaginationChange`, `manualPagination`, `rowCount`, `pageCount`) and column
159
- pinning (`enablePinning`, `initialColumnPinning`). **Don't hand-roll any of these** — read the props
160
- on the type, they are all there.
161
-
162
- ### `manualPagination` imposes a contract on the rest of the props
163
-
164
- Adopting it makes `data` **one server page** while the bar still reads `Showing 1–25 of 412`. Every
165
- other client-side control then becomes a claim about 412 rows it can only make about 25 — a sort
166
- that reorders one page under a header chevron that says it sorted all of them is a plausible, wrong
167
- answer with nothing on screen to give it away. So each control has to be resolved explicitly; there
168
- is no safe default, because only the call site knows whether the server does the work.
169
-
170
- | With `manualPagination` | Resolve it with |
171
- | ------------------------- | ----------------------------------------------------------------------------------- |
172
- | the pagination bar itself | `enablePagination` (without it `manualPagination` is inert) + `rowCount` |
173
- | sorting | `manualSorting` + sort in the `onSortingChange` request, or `enableSorting={false}` |
174
- | `enableGlobalFilter` | `manualFiltering` + filter in the `onGlobalFilterChange` request |
175
- | `facets` | `manualFiltering` + `onColumnFiltersChange` |
176
-
177
- ```tsx
178
- <BasaltDataTable
179
- data={page.rows}
180
- columns={columns}
181
- enablePagination
182
- manualPagination
183
- rowCount={page.total}
184
- manualSorting
185
- onSortingChange={(s) => refetch({ sort: s[0] })}
186
- onPaginationChange={(p) => refetch({ page: p })}
187
- />
188
- ```
189
-
190
- `manualSorting` / `manualFiltering` don't switch the controls off — the headers and inputs stay
191
- live and keep reporting through their `on*Change` callbacks. They stop the table from _recomputing_
192
- the answer locally, so what renders is the order and the selection the server sent.
193
-
194
- **Unresolved, it is not silent.** A contradiction throws in dev naming every breach at once, and a
195
- production bundle (where the throw is constant-folded away) degrades to the honest table instead:
196
- no sort headers, no filter controls, no `of N` it cannot stand behind, plus one `console.error`.
197
- You will never get the plausible wrong answer, but you also won't get sorting you thought you had —
198
- so read the throw, don't work around it.
199
-
200
- Still deferred: row selection, row expansion / sub-rows, and fully controlled (external) sorting
201
- STATE — `manualSorting` hands the _work_ to the server, but the table still owns the `SortingState`
202
- and reports it through `onSortingChange`. Add them when a concrete consumer need arises; keep the API
203
- additive.
204
-
205
- ### The blessed lane vs the raw escape hatch
206
-
207
- `BasaltDataTable` (from `basalt-ui/data/table`) is the **blessed, opinionated grid** — batteries
208
- included (sorting, loading skeletons, empty state, Mantine `Table` chrome) and the default choice
209
- for a data grid in a basalt-ui app.
210
-
211
- For a bespoke table shape `BasaltDataTable` doesn't cover, `basalt-ui/data/table` also re-exports
212
- the raw TanStack Table primitives as the documented **escape hatch**: `useReactTable`, `flexRender`,
213
- `getCoreRowModel`, `getSortedRowModel`, `createColumnHelper`, and the `ColumnDef` type — the full
214
- surface for constructing and rendering a fully custom table, with no direct `@tanstack/react-table`
215
- import in consumer code:
216
-
217
- ```tsx
218
- import {
219
- createColumnHelper,
220
- flexRender,
221
- getCoreRowModel,
222
- useReactTable,
223
- } from 'basalt-ui/data/table'
224
-
225
- type Row = { name: string; score: number }
226
- const col = createColumnHelper<Row>()
227
- const columns = [
228
- col.accessor('name', { header: 'Name' }),
229
- col.accessor('score', { header: 'Score' }),
230
- ]
231
-
232
- const table = useReactTable({ data: rows, columns, getCoreRowModel: getCoreRowModel() })
233
- // table.getHeaderGroups() / table.getRowModel().rows, rendered with flexRender(...) and your own markup
234
- ```
235
-
236
- Reach for the escape hatch only when `BasaltDataTable`'s props genuinely can't express the shape.
237
- Pagination, server-side data and filtering are **not** among those cases any more — see above.
238
-
239
- **Both lanes scroll through the same node.** An escape-hatch table that needs a capped body wraps
240
- it in `<Table.ScrollContainer type="native" minWidth={0} maxHeight={320}>` — and that is byte for
241
- byte what `BasaltDataTable`'s `maxHeight`/`minWidth` render. The blessed lane and the escape cannot
242
- contradict each other on scrolling, and neither should reach for `type="scrollarea"`: `ScrollArea`'s
243
- custom viewport is the positioning context a sticky `<thead>` resolves against, so the default type
244
- pins the header to the page viewport instead of the table's box. (`basalt/raw-scroll-container`
245
- takes no view here — it steers raw `overflow: auto`, and `Table.ScrollContainer` is a Mantine
246
- component, not a raw scroll box.)
247
-
248
- **Adopt the blessed lane for ownership, not for a line count.** Porting one consumer's three tables
249
- onto these props made them 341 → **370–379** lines, **29–38 longer**: a scratch port measured the
250
- low end, the consumer's own shipped port the high end, because it kept the comment blocks
251
- justifying each table's `numeral` opt-outs and its `enableSorting` decision. Accessor blocks at
252
- 4–6 lines each cost more than an eight-`<Table.Td>` row at ~3 when every cell is bespoke, and the
253
- opt-outs are lines the hand-rolled version never had to write. Budget the high end. What the port
254
- bought was the `type="native"` footgun, alignment stated once instead of on both `th` and `td` —
255
- **4 literals in one table alone, 6 across the two that had alignment to state** — and
256
- sorting/filtering/pagination no longer being yours to maintain. Expect that trade, not a shrink.
257
-
258
- That consumer deliberately left the third table hand-rolled, and the reason generalises: four of
259
- its six columns would have needed `enableSorting: false` (three sparkline cells over `number[]`
260
- accessors, one status enum), and a numeric column rendering its own coloured `Text` would have
261
- needed a `numeral: false` it did not need before. A table with no sorting, no pagination, no filter
262
- and no alignment to declare is a layout grid that happens to use `<table>` — adopting the component
263
- there buys nothing and taxes you to switch off things you never asked for.
264
-
265
- ## BasaltVirtualList
266
-
267
- A windowed virtual list backed by TanStack Virtual, rendered inside a Mantine `Box` scroll
268
- container. Only the visible rows plus `overscan` are in the DOM at any time — suitable for lists
269
- of 1 000+ items.
270
-
271
- ```tsx
272
- import { Box } from '@mantine/core'
273
- import { VX } from 'basalt-ui/tokens'
274
- import { BasaltVirtualList } from 'basalt-ui/data'
275
-
276
- const items = Array.from({ length: 10_000 }, (_, i) => ({ id: i, name: `Row ${i}` }))
277
-
278
- <BasaltVirtualList
279
- items={items}
280
- height={400}
281
- estimateSize={40}
282
- overscan={5}
283
- renderItem={(item, index) => (
284
- <Box px="sm" py={8} style={{ borderBottom: `1px solid ${VX.divider}` }}>
285
- {index + 1}. {item.name}
286
- </Box>
287
- )}
288
- getItemKey={(item) => item.id}
289
- />
290
- ```
291
-
292
- ### Props
293
-
294
- | Prop | Type | Default | Notes |
295
- | -------------- | ---------------------------------------------- | -------- | ------------------------------------------------- |
296
- | `items` | `T[]` | required | Full unsliced item list |
297
- | `height` | `number \| string` | required | Scroll container height (px number or CSS string) |
298
- | `estimateSize` | `number` | `40` | Estimated row height for layout (px) |
299
- | `overscan` | `number` | `5` | Extra rows rendered beyond visible viewport |
300
- | `renderItem` | `(item: T, index: number) => ReactNode` | required | Row render function |
301
- | `getItemKey` | `(item: T, index: number) => string \| number` | — | Stable key (defaults to index) |
302
- | `isLoading` | `boolean` | `false` | Shows skeleton rows at `estimateSize` height |
303
- | `skeletonRows` | `number` | `5` | Number of skeleton rows when `isLoading` is true |
304
-
305
- ### useFlushSync: false (React 19)
306
-
307
- Always pass `useFlushSync: false` to `useVirtualizer`. TanStack Virtual internally calls the
308
- deprecated `flushSync` on scroll events; this opt-out disables that path and is required for React
309
- 19+ apps (silences a runtime warning). `BasaltVirtualList` sets this automatically — do not
310
- override it.
311
-
312
- ### Absolute-position transform pattern
313
-
314
- The virtual list uses the canonical TanStack Virtual render pattern:
315
-
316
- 1. **Scroll container** — fixed height, `overflow: auto`, holds a `ref` passed to `getScrollElement`.
317
- 2. **Inner sizer div** — height equals `getTotalSize()`, position `relative` — defines the total
318
- scrollable area without rendering all rows.
319
- 3. **Virtual rows** — absolutely positioned (`position: absolute`, `top: 0`, `left: 0`) with
320
- `transform: translateY(vi.start)` — each row "jumps" to its virtual position without relayout.
321
-
322
- Never use `margin-top` or `top: vi.start` for positioning — the `transform` approach avoids
323
- layout reflow and is the canonical pattern for TanStack Virtual.
324
-
325
- ### When to virtualize
326
-
327
- Use `BasaltVirtualList` when the list exceeds ~200 rows. Below that, a plain Mantine `Stack` or
328
- `Table` is cheaper (no virtualizer overhead, simpler DOM, easier accessibility). Above ~1 000
329
- rows, virtualization is essential for scroll performance.
@@ -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.