basalt-ui 1.28.0 → 1.29.1

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 (457) hide show
  1. package/AGENTS.md +12 -19
  2. package/CHANGELOG.md +23 -0
  3. package/MIGRATING.md +171 -7
  4. package/README.md +66 -105
  5. package/agent/rules/basalt-batteries.md +60 -183
  6. package/agent/rules/basalt-charts.md +56 -75
  7. package/agent/rules/basalt-controls.md +38 -69
  8. package/agent/rules/basalt-mantine.md +58 -103
  9. package/agent/rules/basalt-state.md +42 -60
  10. package/agent/rules/basalt-tokens.md +46 -67
  11. package/agent/skills/basalt-app/SKILL.md +2 -0
  12. package/agent/templates/CLAUDE-block.md.tpl +14 -14
  13. package/agent/templates/query-client.ts.tpl +1 -1
  14. package/configs/oxlint-plugin.js +84 -254
  15. package/configs/oxlint-plugin.test.ts +23 -266
  16. package/configs/oxlint.json +0 -2
  17. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  18. package/dist/agent/ai-sdk-transport.js +7 -4
  19. package/dist/agent/coalesce.js +1 -1
  20. package/dist/agent/merge.js +1 -1
  21. package/dist/agent/transport.d.ts +13 -2
  22. package/dist/agent/transport.d.ts.map +1 -1
  23. package/dist/agent/use-agent-thread-runs.d.ts +12 -1
  24. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  25. package/dist/agent/use-agent-thread-runs.js +20 -6
  26. package/dist/agent-chat/index.d.ts +1 -1
  27. package/dist/agent-chat/index.d.ts.map +1 -1
  28. package/dist/agent-chat/relative-time.js +1 -1
  29. package/dist/agent-chat/thread-detail-panel.js +1 -1
  30. package/dist/agent-chat/thread-feed-row.d.ts +32 -7
  31. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
  32. package/dist/agent-chat/thread-feed-row.js +121 -86
  33. package/dist/agent-chat/thread-feed.d.ts +11 -1
  34. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  35. package/dist/agent-chat/thread-feed.js +10 -6
  36. package/dist/agent-chat/thread-message.js +1 -1
  37. package/dist/agent-chat/virtualize.d.ts +39 -0
  38. package/dist/agent-chat/virtualize.d.ts.map +1 -1
  39. package/dist/agent-chat/virtualize.js +15 -0
  40. package/dist/charts/index.d.ts +3 -3
  41. package/dist/charts/index.d.ts.map +1 -1
  42. package/dist/charts/index.js +8 -4
  43. package/dist/charts/kinds/BandStrip.js +1 -1
  44. package/dist/charts/kinds/Bars.d.ts +1 -3
  45. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  46. package/dist/charts/kinds/Bars.js +1 -1
  47. package/dist/charts/kinds/DualPanel.d.ts +3 -1
  48. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  49. package/dist/charts/kinds/DualPanel.js +1 -1
  50. package/dist/charts/kinds/StackedArea.js +1 -1
  51. package/dist/charts/kinds/ZonedLine.d.ts +1 -3
  52. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  53. package/dist/charts/primitives/Axes.d.ts +21 -2
  54. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  55. package/dist/charts/primitives/Axes.js +34 -2
  56. package/dist/charts/primitives/ChartCard.d.ts +20 -4
  57. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  58. package/dist/charts/primitives/ChartCard.js +62 -33
  59. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  60. package/dist/charts/primitives/ChartFrame.js +8 -1
  61. package/dist/charts/primitives/ChartPending.d.ts +12 -6
  62. package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
  63. package/dist/charts/primitives/ChartPending.js +1 -1
  64. package/dist/charts/utils/format.d.ts +5 -92
  65. package/dist/charts/utils/format.d.ts.map +1 -1
  66. package/dist/charts/utils/format.js +10 -80
  67. package/dist/cli/audit-allows.d.ts +24 -0
  68. package/dist/cli/audit-allows.d.ts.map +1 -0
  69. package/dist/cli/audit-allows.js +240 -0
  70. package/dist/cli/check-theme.d.ts +10 -0
  71. package/dist/cli/check-theme.d.ts.map +1 -0
  72. package/dist/cli/check-theme.js +139 -0
  73. package/dist/cli/doctor.d.ts +47 -0
  74. package/dist/cli/doctor.d.ts.map +1 -0
  75. package/dist/cli/doctor.js +197 -0
  76. package/dist/cli/guard-hook.d.ts +9 -0
  77. package/dist/cli/guard-hook.d.ts.map +1 -0
  78. package/dist/cli/guard-hook.js +93 -0
  79. package/dist/cli/help.d.ts +9 -0
  80. package/dist/cli/help.d.ts.map +1 -0
  81. package/dist/cli/help.js +53 -0
  82. package/dist/cli/index.d.ts +360 -270
  83. package/dist/cli/index.d.ts.map +1 -1
  84. package/dist/cli/index.js +85 -1901
  85. package/dist/cli/init.d.ts +4 -0
  86. package/dist/cli/init.d.ts.map +1 -0
  87. package/dist/cli/init.js +165 -0
  88. package/dist/cli/sync.d.ts +43 -0
  89. package/dist/cli/sync.d.ts.map +1 -0
  90. package/dist/cli/sync.js +206 -0
  91. package/dist/cli/tokens-css.d.ts +51 -0
  92. package/dist/cli/tokens-css.d.ts.map +1 -0
  93. package/dist/cli/tokens-css.js +165 -0
  94. package/dist/commands/define-commands.js +1 -1
  95. package/dist/commands/define-overlays.js +1 -1
  96. package/dist/commands/index.d.ts +2 -1
  97. package/dist/commands/index.d.ts.map +1 -1
  98. package/dist/commands/index.js +6 -0
  99. package/dist/commands/overlays-mount.d.ts +7 -9
  100. package/dist/commands/overlays-mount.d.ts.map +1 -1
  101. package/dist/commands/shell-bridge.d.ts +57 -0
  102. package/dist/commands/shell-bridge.d.ts.map +1 -0
  103. package/dist/commands/shell-bridge.js +32 -0
  104. package/dist/common/errors.d.ts +35 -0
  105. package/dist/common/errors.d.ts.map +1 -1
  106. package/dist/common/errors.js +29 -1
  107. package/dist/common/index.d.ts +2 -0
  108. package/dist/common/index.d.ts.map +1 -1
  109. package/dist/common/index.js +6 -0
  110. package/dist/common/is-dev.d.ts.map +1 -0
  111. package/dist/{motion/index.d.ts → common/motion.d.ts} +1 -1
  112. package/dist/common/motion.d.ts.map +1 -0
  113. package/dist/common/props.d.ts +1 -1
  114. package/dist/common/props.d.ts.map +1 -1
  115. package/dist/common/validate.js +1 -1
  116. package/dist/content/markdown.d.ts +3 -4
  117. package/dist/content/markdown.d.ts.map +1 -1
  118. package/dist/content/markdown.js +1 -4
  119. package/dist/controls/actions.d.ts +1 -1
  120. package/dist/controls/actions.d.ts.map +1 -1
  121. package/dist/controls/panel-row.js +1 -1
  122. package/dist/controls/range-filter.js +1 -1
  123. package/dist/dashboard/delta-badge.d.ts +18 -10
  124. package/dist/dashboard/delta-badge.d.ts.map +1 -1
  125. package/dist/dashboard/delta-badge.js +33 -1
  126. package/dist/dashboard/empty-state.d.ts +1 -4
  127. package/dist/dashboard/empty-state.d.ts.map +1 -1
  128. package/dist/dashboard/empty-state.js +1 -2
  129. package/dist/dashboard/index.d.ts +4 -2
  130. package/dist/dashboard/index.d.ts.map +1 -1
  131. package/dist/dashboard/index.js +7 -1
  132. package/dist/dashboard/query-state.d.ts +4 -13
  133. package/dist/dashboard/query-state.d.ts.map +1 -1
  134. package/dist/dashboard/query-state.js +4 -7
  135. package/dist/{section → dashboard}/section.d.ts +2 -2
  136. package/dist/{section → dashboard}/section.d.ts.map +1 -1
  137. package/dist/{section → dashboard}/section.js +2 -2
  138. package/dist/dashboard/settings-section.d.ts +1 -1
  139. package/dist/dashboard/settings-section.d.ts.map +1 -1
  140. package/dist/dashboard/settings-section.js +1 -1
  141. package/dist/dashboard/stat-card.d.ts +1 -1
  142. package/dist/dashboard/stat-card.d.ts.map +1 -1
  143. package/dist/dashboard/stat-card.js +1 -1
  144. package/dist/dashboard/widget-header.d.ts.map +1 -0
  145. package/dist/data/data-table.js +2 -2
  146. package/dist/data/table.d.ts +1 -1
  147. package/dist/data/table.d.ts.map +1 -1
  148. package/dist/data/table.js +2 -16
  149. package/dist/data/virtual.d.ts +3 -3
  150. package/dist/dev-dock.d.ts +26 -0
  151. package/dist/dev-dock.d.ts.map +1 -0
  152. package/dist/dev-dock.js +85 -0
  153. package/dist/format/date.d.ts +62 -0
  154. package/dist/format/date.d.ts.map +1 -0
  155. package/dist/format/date.js +106 -0
  156. package/dist/format/index.d.ts +39 -0
  157. package/dist/format/index.d.ts.map +1 -0
  158. package/dist/format/index.js +54 -0
  159. package/dist/format/measure.d.ts +15 -0
  160. package/dist/format/measure.d.ts.map +1 -0
  161. package/dist/format/measure.js +16 -0
  162. package/dist/format/number.d.ts +69 -0
  163. package/dist/format/number.d.ts.map +1 -0
  164. package/dist/format/number.js +62 -0
  165. package/dist/format/shared.d.ts +27 -0
  166. package/dist/format/shared.d.ts.map +1 -0
  167. package/dist/format/shared.js +29 -0
  168. package/dist/forms/field.d.ts +4 -20
  169. package/dist/forms/field.d.ts.map +1 -1
  170. package/dist/forms/field.js +0 -4
  171. package/dist/forms/form-layout.d.ts +1 -1
  172. package/dist/forms/form-layout.d.ts.map +1 -1
  173. package/dist/forms/form-layout.js +1 -1
  174. package/dist/forms/index.d.ts +1 -1
  175. package/dist/forms/index.d.ts.map +1 -1
  176. package/dist/forms/index.js +1 -2
  177. package/dist/forms/use-form-submit.d.ts +1 -1
  178. package/dist/forms/use-form-submit.js +1 -1
  179. package/dist/guard/index.d.ts +2 -9
  180. package/dist/guard/index.d.ts.map +1 -1
  181. package/dist/guard/index.js +53 -32
  182. package/dist/guard/types.d.ts +8 -10
  183. package/dist/guard/types.d.ts.map +1 -1
  184. package/dist/index.d.ts +16 -11
  185. package/dist/index.d.ts.map +1 -1
  186. package/dist/index.js +23 -29
  187. package/dist/notifications/index.d.ts +5 -5
  188. package/dist/notifications/index.d.ts.map +1 -1
  189. package/dist/notifications/index.js +0 -2
  190. package/dist/notifications/mount-guard.d.ts.map +1 -1
  191. package/dist/notifications/mount-guard.js +2 -2
  192. package/dist/provider/connectivity-indicator.d.ts.map +1 -0
  193. package/dist/{connectivity → provider}/connectivity-provider.d.ts +1 -1
  194. package/dist/provider/connectivity-provider.d.ts.map +1 -0
  195. package/dist/{connectivity/types.d.ts → provider/connectivity-types.d.ts} +1 -1
  196. package/dist/provider/connectivity-types.d.ts.map +1 -0
  197. package/dist/provider/error-fallback.module.css +21 -0
  198. package/dist/provider/error-fallback.module.css.d.ts +9 -0
  199. package/dist/provider/index.d.ts +20 -36
  200. package/dist/provider/index.d.ts.map +1 -1
  201. package/dist/provider/index.js +42 -32
  202. package/dist/provider/lab-theme.js +1 -1
  203. package/dist/provider/use-connectivity.d.ts +3 -0
  204. package/dist/provider/use-connectivity.d.ts.map +1 -0
  205. package/dist/query-client.d.ts +67 -0
  206. package/dist/query-client.d.ts.map +1 -0
  207. package/dist/query-client.js +35 -0
  208. package/dist/{query/devtools.d.ts → query-devtools.d.ts} +2 -2
  209. package/dist/query-devtools.d.ts.map +1 -0
  210. package/dist/register.d.ts +17 -3
  211. package/dist/register.d.ts.map +1 -1
  212. package/dist/router-tanstack/index.d.ts +4 -7
  213. package/dist/router-tanstack/index.d.ts.map +1 -1
  214. package/dist/router-tanstack/index.js +1 -9
  215. package/dist/router-tanstack/nav.d.ts +1 -1
  216. package/dist/router-tanstack/nav.d.ts.map +1 -1
  217. package/dist/router-tanstack/search-store.d.ts +3 -6
  218. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  219. package/dist/router-tanstack/use-nav.d.ts +1 -1
  220. package/dist/router-tanstack/use-nav.d.ts.map +1 -1
  221. package/dist/shell/app-breadcrumbs.d.ts +1 -1
  222. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  223. package/dist/shell/app-main.module.css +11 -0
  224. package/dist/shell/app-mobile-nav.d.ts +1 -1
  225. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  226. package/dist/shell/app-mobile-nav.js +24 -2
  227. package/dist/shell/app-sidebar.d.ts +1 -1
  228. package/dist/shell/app-sidebar.d.ts.map +1 -1
  229. package/dist/shell/app-sidebar.js +31 -2
  230. package/dist/shell/app-sidebar.module.css +10 -0
  231. package/dist/shell/index.d.ts +10 -4
  232. package/dist/shell/index.d.ts.map +1 -1
  233. package/dist/shell/index.js +13 -17
  234. package/dist/shell/mobile-nav-model.d.ts +1 -1
  235. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  236. package/dist/{nav/types.d.ts → shell/nav-types.d.ts} +1 -1
  237. package/dist/shell/nav-types.d.ts.map +1 -0
  238. package/dist/shell/page-aside.d.ts.map +1 -1
  239. package/dist/shell/page-aside.js +2 -29
  240. package/dist/shell/page-title.d.ts +14 -0
  241. package/dist/shell/page-title.d.ts.map +1 -0
  242. package/dist/shell/page-title.js +25 -0
  243. package/dist/shell/page-title.module.css +33 -0
  244. package/dist/shell/page-title.module.css.d.ts +11 -0
  245. package/dist/shell/sidebar-block-model.d.ts +1 -1
  246. package/dist/shell/sidebar-block-model.d.ts.map +1 -1
  247. package/dist/shell/sidebar-blocks.d.ts +1 -1
  248. package/dist/shell/sidebar-blocks.d.ts.map +1 -1
  249. package/dist/shell/use-breakpoint.d.ts +11 -0
  250. package/dist/shell/use-breakpoint.d.ts.map +1 -0
  251. package/dist/shell/use-breakpoint.js +33 -0
  252. package/dist/state/fields.d.ts +3 -19
  253. package/dist/state/fields.d.ts.map +1 -1
  254. package/dist/state/fields.js +6 -16
  255. package/dist/styles.css +36 -0
  256. package/dist/surfaces.d.ts +18 -41
  257. package/dist/surfaces.d.ts.map +1 -1
  258. package/dist/surfaces.js +48 -60
  259. package/dist/theme/index.d.ts.map +1 -1
  260. package/dist/{theme-toggle/index.d.ts → theme/theme-toggle.d.ts} +1 -1
  261. package/dist/theme/theme-toggle.d.ts.map +1 -0
  262. package/dist/{theme-toggle/index.js → theme/theme-toggle.js} +1 -1
  263. package/dist/tokens/index.d.ts +1 -0
  264. package/dist/tokens/index.d.ts.map +1 -1
  265. package/dist/tokens/index.js +10 -0
  266. package/dist/tokens/palette.d.ts +12 -0
  267. package/dist/tokens/palette.d.ts.map +1 -1
  268. package/dist/tokens/palette.js +13 -1
  269. package/dist/tokens.css +1 -0
  270. package/llms.txt +9 -24
  271. package/package.json +12 -16
  272. package/src/agent/ai-sdk-transport.ts +11 -4
  273. package/src/agent/coalesce.ts +1 -1
  274. package/src/agent/merge.ts +1 -1
  275. package/src/agent/transport.ts +15 -2
  276. package/src/agent/use-agent-thread-runs.ts +65 -15
  277. package/src/agent-chat/index.ts +6 -4
  278. package/src/agent-chat/relative-time.ts +1 -1
  279. package/src/agent-chat/thread-detail-panel.tsx +1 -1
  280. package/src/agent-chat/thread-feed-row.tsx +105 -30
  281. package/src/agent-chat/thread-feed.tsx +51 -25
  282. package/src/agent-chat/thread-message.tsx +1 -1
  283. package/src/agent-chat/virtualize.ts +48 -0
  284. package/src/charts/index.ts +7 -10
  285. package/src/charts/kinds/BandStrip.tsx +1 -1
  286. package/src/charts/kinds/Bars.tsx +2 -5
  287. package/src/charts/kinds/DualPanel.tsx +4 -3
  288. package/src/charts/kinds/StackedArea.tsx +1 -1
  289. package/src/charts/kinds/ZonedLine.tsx +1 -4
  290. package/src/charts/primitives/Axes.tsx +49 -0
  291. package/src/charts/primitives/ChartCard.tsx +49 -6
  292. package/src/charts/primitives/ChartFrame.tsx +5 -1
  293. package/src/charts/primitives/ChartPending.tsx +14 -8
  294. package/src/charts/utils/format.ts +17 -186
  295. package/src/cli/audit-allows.ts +371 -0
  296. package/src/cli/check-theme.ts +197 -0
  297. package/src/cli/doctor.ts +331 -0
  298. package/src/cli/guard-hook.ts +115 -0
  299. package/src/cli/help.ts +101 -0
  300. package/src/cli/index.ts +269 -3439
  301. package/src/cli/init.ts +247 -0
  302. package/src/cli/sync.ts +324 -0
  303. package/src/cli/tokens-css.ts +252 -0
  304. package/src/commands/define-commands.ts +1 -1
  305. package/src/commands/define-overlays.ts +1 -1
  306. package/src/commands/index.ts +10 -1
  307. package/src/commands/overlays-mount.tsx +8 -10
  308. package/src/commands/shell-bridge.ts +86 -0
  309. package/src/common/errors.ts +70 -0
  310. package/src/common/index.ts +2 -0
  311. package/src/common/props.ts +1 -1
  312. package/src/common/validate.ts +1 -1
  313. package/src/content/markdown.tsx +4 -8
  314. package/src/controls/actions.tsx +1 -1
  315. package/src/controls/panel-row.tsx +1 -1
  316. package/src/controls/range-filter.tsx +1 -1
  317. package/src/dashboard/delta-badge.tsx +74 -7
  318. package/src/dashboard/empty-state.tsx +2 -6
  319. package/src/dashboard/index.ts +11 -2
  320. package/src/dashboard/query-state.tsx +5 -18
  321. package/src/{section → dashboard}/section.tsx +4 -4
  322. package/src/dashboard/settings-section.tsx +2 -2
  323. package/src/dashboard/stat-card.tsx +2 -2
  324. package/src/data/data-table.tsx +3 -3
  325. package/src/data/table.ts +6 -25
  326. package/src/data/virtual-list.tsx +1 -1
  327. package/src/data/virtual.ts +3 -3
  328. package/src/dev-dock.tsx +162 -0
  329. package/src/format/date.ts +184 -0
  330. package/src/format/index.ts +69 -0
  331. package/src/format/measure.ts +31 -0
  332. package/src/format/number.ts +129 -0
  333. package/src/format/shared.ts +63 -0
  334. package/src/forms/field.ts +4 -24
  335. package/src/forms/form-layout.tsx +2 -2
  336. package/src/forms/index.ts +1 -2
  337. package/src/forms/use-form-submit.ts +2 -2
  338. package/src/guard/index.ts +80 -69
  339. package/src/guard/types.ts +7 -10
  340. package/src/index.ts +38 -29
  341. package/src/notifications/index.ts +5 -7
  342. package/src/notifications/mount-guard.ts +7 -9
  343. package/src/{connectivity → provider}/connectivity-indicator.tsx +1 -1
  344. package/src/{connectivity → provider}/connectivity-provider.tsx +1 -1
  345. package/src/provider/error-fallback.module.css +21 -0
  346. package/src/provider/error-fallback.module.css.d.ts +9 -0
  347. package/src/provider/index.tsx +71 -71
  348. package/src/provider/lab-theme.ts +1 -1
  349. package/src/{connectivity → provider}/use-connectivity.ts +3 -3
  350. package/src/query-client.ts +105 -0
  351. package/src/{query/devtools.tsx → query-devtools.tsx} +5 -4
  352. package/src/register.ts +20 -4
  353. package/src/router-tanstack/index.ts +4 -15
  354. package/src/router-tanstack/nav.ts +1 -1
  355. package/src/router-tanstack/search-store.ts +3 -4
  356. package/src/router-tanstack/use-nav.ts +1 -1
  357. package/src/shell/app-breadcrumbs.tsx +1 -1
  358. package/src/shell/app-main.module.css +11 -0
  359. package/src/shell/app-mobile-nav.tsx +28 -1
  360. package/src/shell/app-sidebar.module.css +10 -0
  361. package/src/shell/app-sidebar.tsx +31 -2
  362. package/src/shell/index.tsx +23 -28
  363. package/src/shell/mobile-nav-model.ts +1 -1
  364. package/src/shell/page-aside.tsx +2 -52
  365. package/src/shell/page-title.module.css +33 -0
  366. package/src/shell/page-title.module.css.d.ts +11 -0
  367. package/src/shell/page-title.tsx +57 -0
  368. package/src/shell/sidebar-block-model.ts +1 -1
  369. package/src/shell/sidebar-blocks.tsx +1 -1
  370. package/src/shell/use-breakpoint.ts +73 -0
  371. package/src/state/fields.ts +7 -43
  372. package/src/styles.css +36 -0
  373. package/src/surfaces.ts +47 -61
  374. package/src/theme/index.ts +7 -0
  375. package/src/{theme-toggle/index.tsx → theme/theme-toggle.tsx} +1 -1
  376. package/src/tokens/index.ts +10 -0
  377. package/src/tokens/palette.ts +12 -0
  378. package/dist/connectivity/connectivity-indicator.d.ts.map +0 -1
  379. package/dist/connectivity/connectivity-provider.d.ts.map +0 -1
  380. package/dist/connectivity/index.d.ts +0 -14
  381. package/dist/connectivity/index.d.ts.map +0 -1
  382. package/dist/connectivity/index.js +0 -8
  383. package/dist/connectivity/types.d.ts.map +0 -1
  384. package/dist/connectivity/use-connectivity.d.ts +0 -3
  385. package/dist/connectivity/use-connectivity.d.ts.map +0 -1
  386. package/dist/data/index.d.ts +0 -38
  387. package/dist/data/index.d.ts.map +0 -1
  388. package/dist/data/index.js +0 -24
  389. package/dist/motion/index.d.ts.map +0 -1
  390. package/dist/nav/types.d.ts.map +0 -1
  391. package/dist/notifications/overlay.d.ts +0 -20
  392. package/dist/notifications/overlay.d.ts.map +0 -1
  393. package/dist/notifications/overlay.js +0 -14
  394. package/dist/query/devtools.d.ts.map +0 -1
  395. package/dist/query/error-message.d.ts +0 -41
  396. package/dist/query/error-message.d.ts.map +0 -1
  397. package/dist/query/error-message.js +0 -30
  398. package/dist/query/index.d.ts +0 -47
  399. package/dist/query/index.d.ts.map +0 -1
  400. package/dist/query/index.js +0 -56
  401. package/dist/router-tanstack/field.d.ts +0 -14
  402. package/dist/router-tanstack/field.d.ts.map +0 -1
  403. package/dist/router-tanstack/field.js +0 -4
  404. package/dist/router-tanstack/multi-search-param-store.d.ts +0 -43
  405. package/dist/router-tanstack/multi-search-param-store.d.ts.map +0 -1
  406. package/dist/router-tanstack/multi-search-param-store.js +0 -30
  407. package/dist/router-tanstack/search-param-store.d.ts +0 -42
  408. package/dist/router-tanstack/search-param-store.d.ts.map +0 -1
  409. package/dist/router-tanstack/search-param-store.js +0 -25
  410. package/dist/section/index.d.ts +0 -2
  411. package/dist/section/index.d.ts.map +0 -1
  412. package/dist/section/index.js +0 -4
  413. package/dist/theme-toggle/index.d.ts.map +0 -1
  414. package/dist/utils/is-dev.d.ts.map +0 -1
  415. package/dist/widget-header/delta-badge.d.ts +0 -19
  416. package/dist/widget-header/delta-badge.d.ts.map +0 -1
  417. package/dist/widget-header/delta-badge.js +0 -36
  418. package/dist/widget-header/index.d.ts +0 -3
  419. package/dist/widget-header/index.d.ts.map +0 -1
  420. package/dist/widget-header/index.js +0 -8
  421. package/dist/widget-header/widget-header.d.ts.map +0 -1
  422. package/src/connectivity/index.ts +0 -19
  423. package/src/data/index.ts +0 -69
  424. package/src/notifications/overlay.tsx +0 -66
  425. package/src/query/error-message.ts +0 -70
  426. package/src/query/index.ts +0 -97
  427. package/src/router-tanstack/field.ts +0 -36
  428. package/src/router-tanstack/multi-search-param-store.ts +0 -106
  429. package/src/router-tanstack/search-param-store.ts +0 -100
  430. package/src/section/index.ts +0 -1
  431. package/src/widget-header/delta-badge.tsx +0 -77
  432. package/src/widget-header/index.ts +0 -10
  433. /package/dist/{utils → common}/is-dev.d.ts +0 -0
  434. /package/dist/{utils → common}/is-dev.js +0 -0
  435. /package/dist/{motion/index.js → common/motion.js} +0 -0
  436. /package/dist/{section → dashboard}/section.module.css +0 -0
  437. /package/dist/{section → dashboard}/section.module.css.d.ts +0 -0
  438. /package/dist/{widget-header → dashboard}/widget-header.d.ts +0 -0
  439. /package/dist/{widget-header → dashboard}/widget-header.js +0 -0
  440. /package/dist/{widget-header → dashboard}/widget-header.module.css +0 -0
  441. /package/dist/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
  442. /package/dist/{connectivity → provider}/connectivity-indicator.d.ts +0 -0
  443. /package/dist/{connectivity → provider}/connectivity-indicator.js +0 -0
  444. /package/dist/{connectivity → provider}/connectivity-provider.js +0 -0
  445. /package/dist/{connectivity/types.js → provider/connectivity-types.js} +0 -0
  446. /package/dist/{connectivity → provider}/use-connectivity.js +0 -0
  447. /package/dist/{query/devtools.js → query-devtools.js} +0 -0
  448. /package/dist/{nav/types.js → shell/nav-types.js} +0 -0
  449. /package/src/{utils → common}/is-dev.ts +0 -0
  450. /package/src/{motion/index.ts → common/motion.ts} +0 -0
  451. /package/src/{section → dashboard}/section.module.css +0 -0
  452. /package/src/{section → dashboard}/section.module.css.d.ts +0 -0
  453. /package/src/{widget-header → dashboard}/widget-header.module.css +0 -0
  454. /package/src/{widget-header → dashboard}/widget-header.module.css.d.ts +0 -0
  455. /package/src/{widget-header → dashboard}/widget-header.tsx +0 -0
  456. /package/src/{connectivity/types.ts → provider/connectivity-types.ts} +0 -0
  457. /package/src/{nav/types.ts → shell/nav-types.ts} +0 -0
@@ -37,7 +37,7 @@ import { cx } from '../common/props'
37
37
  import type { BasaltProps } from '../common/props'
38
38
  import { assertRequiredProps } from '../common/validate'
39
39
  import type { BrandConfig, SettingsMenuItem } from './index'
40
- import type { SidebarBlock, SidebarItem, SidebarSection } from '../nav/types'
40
+ import type { SidebarBlock, SidebarItem, SidebarSection } from './nav-types'
41
41
  import { NavCountBadge } from './nav-count-badge'
42
42
  import { SidebarAccount } from './app-sidebar-account'
43
43
  import type { AccountMenuItem, BasaltAccountProps } from './account-types'
@@ -149,6 +149,27 @@ function IconGear() {
149
149
  )
150
150
  }
151
151
 
152
+ /** The `SettingsMenuItem.active` glyph — no icon package, so a plain inline mark like `IconGear`
153
+ * above. Shared shape with `app-mobile-nav.tsx`'s own copy (that module's More sheet is a
154
+ * different render tree and basalt ships no icon dependency to import between them). */
155
+ function IconCheck() {
156
+ return (
157
+ <svg
158
+ width={14}
159
+ height={14}
160
+ viewBox="0 0 24 24"
161
+ fill="none"
162
+ stroke="currentColor"
163
+ strokeWidth={2.5}
164
+ strokeLinecap="round"
165
+ strokeLinejoin="round"
166
+ aria-hidden
167
+ >
168
+ <path d="M5 12l5 5l10 -10" />
169
+ </svg>
170
+ )
171
+ }
172
+
152
173
  const HOVER_OPEN_DELAY = 150
153
174
  const HOVER_CLOSE_DELAY = 200
154
175
 
@@ -465,6 +486,7 @@ export function AppSidebar(props: AppSidebarProps) {
465
486
  className={classes.footerBtn}
466
487
  onClick={entry.onClick}
467
488
  aria-label={entry.label}
489
+ aria-current={entry.active ? 'true' : undefined}
468
490
  >
469
491
  {/* A FIXED slot, so rows align on one icon column whether or not each ships an icon —
470
492
  and the gear fallback is functional, not decorative: in the collapsed rail the label
@@ -477,6 +499,7 @@ export function AppSidebar(props: AppSidebarProps) {
477
499
  </Text>
478
500
  {/* The rail ring rides the FIRST row only — one progress block is one mark. */}
479
501
  {index === 0 ? ringMark : null}
502
+ {entry.active ? <IconCheck /> : null}
480
503
  </UnstyledButton>
481
504
  ))}
482
505
  {versionLabel !== undefined && (
@@ -499,7 +522,13 @@ export function AppSidebar(props: AppSidebarProps) {
499
522
  </Menu.Target>
500
523
  <Menu.Dropdown>
501
524
  {settingsItems.map((entry) => (
502
- <Menu.Item key={entry.key} leftSection={entry.icon} onClick={entry.onClick}>
525
+ <Menu.Item
526
+ key={entry.key}
527
+ leftSection={entry.icon}
528
+ rightSection={entry.active ? <IconCheck /> : undefined}
529
+ onClick={entry.onClick}
530
+ aria-current={entry.active ? 'true' : undefined}
531
+ >
503
532
  {entry.label}
504
533
  </Menu.Item>
505
534
  ))}
@@ -18,8 +18,9 @@
18
18
  * `createPersistedState` (`../state`) keyed by `storageKey` — see `collapseStore`.
19
19
  */
20
20
  import { AppShell, Box } from '@mantine/core'
21
- import { Fragment, useMemo } from 'react'
21
+ import { Fragment, useCallback, useEffect, useMemo } from 'react'
22
22
  import type { MouseEvent, ReactNode } from 'react'
23
+ import { registerSidebarToggle } from '../commands/shell-bridge'
23
24
  import { cx } from '../common/props'
24
25
  import type { BasaltProps } from '../common/props'
25
26
  import { assertRequiredProps } from '../common/validate'
@@ -40,7 +41,7 @@ import type {
40
41
  SidebarBlock,
41
42
  SidebarItem,
42
43
  SidebarSection,
43
- } from '../nav/types'
44
+ } from './nav-types'
44
45
  import { CtlSlot, useBasaltSpacing } from '../theme'
45
46
  import { createPersistedState } from '../state'
46
47
  import brandClasses from './app-brand.module.css'
@@ -78,10 +79,12 @@ export {
78
79
  export { AppBreadcrumbs, type AppBreadcrumbsProps } from './app-breadcrumbs'
79
80
  export { PageBar, type PageBarProps, type PageBarSlot } from './page-bar'
80
81
  export { PageAside, type PageAsideProps, type PageAsideSlot } from './page-aside'
82
+ export { useBreakpoint, type BreakpointName } from './use-breakpoint'
83
+ export { PageTitle, type PageTitleProps, type PageTitleSlot } from './page-title'
81
84
 
82
85
  /**
83
- * The shared nav vocabulary lives in `src/nav/types.ts` so the headless router bridge can `import
84
- * type` it without reaching into the Mantine layer. Re-exported here so `SidebarItem` /
86
+ * The shared nav vocabulary lives in `src/shell/nav-types.ts` so the headless router bridge can
87
+ * `import type` it without reaching into the Mantine layer. Re-exported here so `SidebarItem` /
85
88
  * `SidebarSection` keep resolving straight from `basalt-ui`, exactly as before.
86
89
  */
87
90
  export type {
@@ -102,7 +105,7 @@ export type {
102
105
  MobileNavModel,
103
106
  MobileNavSlot,
104
107
  MobileNavSurface,
105
- } from '../nav/types'
108
+ } from './nav-types'
106
109
 
107
110
  /** Brand identity shown in the sidebar header (logo + name). */
108
111
  export type BrandConfig = {
@@ -122,6 +125,10 @@ export type SettingsMenuItem = {
122
125
  label: string
123
126
  icon?: ReactNode
124
127
  onClick?: (e: MouseEvent) => void
128
+ /** This entry is the current selection (e.g. the active theme radio, the open dev tool) — a
129
+ * trailing check glyph renders in every form (flat footer row, gear `Menu.Item`, the mobile
130
+ * More-sheet row) and the row carries `aria-current="true"`. */
131
+ active?: boolean
125
132
  }
126
133
 
127
134
  export type BasaltShellProps = BasaltProps & {
@@ -276,30 +283,9 @@ const COLLAPSE_VERSION = 1
276
283
  */
277
284
  const collapseStores = new Map<string, () => readonly [boolean, (next: boolean) => void]>()
278
285
 
279
- /**
280
- * Seeds `basalt:<key>` from the raw pre-1.20.1 key once, so the switch doesn't silently re-expand
281
- * every consumer's sidebar. Mantine's `useLocalStorage` wrote a bare `JSON.stringify(value)` at the
282
- * un-namespaced key; only `'true'`/`'false'` are accepted, and only when the house key is empty.
283
- * A one-upgrade bridge, not a supported format — delete it once no consumer predates 1.20.1.
284
- */
285
- function migrateLegacyCollapse(key: string): void {
286
- try {
287
- if (window.localStorage.getItem(`basalt:${key}`) !== null) return
288
- const legacy = window.localStorage.getItem(key)
289
- if (legacy !== 'true' && legacy !== 'false') return
290
- window.localStorage.setItem(
291
- `basalt:${key}`,
292
- JSON.stringify({ v: COLLAPSE_VERSION, value: legacy === 'true' }),
293
- )
294
- } catch {
295
- // Storage blocked (private browsing, quota) — defaulting to expanded is a fine outcome.
296
- }
297
- }
298
-
299
286
  function collapseStore(key: string): () => readonly [boolean, (next: boolean) => void] {
300
287
  const cached = collapseStores.get(key)
301
288
  if (cached) return cached
302
- if (typeof window !== 'undefined') migrateLegacyCollapse(key)
303
289
  const store = createPersistedState<boolean>({ key, version: COLLAPSE_VERSION, initial: false })
304
290
  collapseStores.set(key, store)
305
291
  return store
@@ -354,11 +340,20 @@ function ShellFrame({
354
340
  // localStorage-persisted state entirely — the consumer becomes the source of truth.
355
341
  const isCollapseControlled = collapsedProp !== undefined
356
342
  const collapsed = isCollapseControlled ? collapsedProp : storedCollapsed
357
- const toggleCollapse = () => {
343
+ const toggleCollapse = useCallback(() => {
358
344
  const next = !collapsed
359
345
  if (!isCollapseControlled) setStoredCollapsed(next)
360
346
  onCollapsedChange?.(next)
361
- }
347
+ }, [collapsed, isCollapseControlled, setStoredCollapsed, onCollapsedChange])
348
+ // `commands/shell-bridge.ts`'s `toggleSidebar` handle — registered here so a consumer's
349
+ // `commands.tsx` calls it with ZERO module-scope bridge of its own, unlike argo's hand-rolled
350
+ // `sidebar-bridge.ts` this seeds from (see that file's own doc). Re-registered whenever
351
+ // `toggleCollapse` itself changes identity, so the handle always flips the CURRENT state rather
352
+ // than a stale closure's.
353
+ useEffect(() => {
354
+ registerSidebarToggle(toggleCollapse)
355
+ return () => registerSidebarToggle(null)
356
+ }, [toggleCollapse])
362
357
 
363
358
  const activeCrumb = findActiveCrumb(sections)
364
359
  // The account row, every settings entry and every mobile-reachable sidebar block render as flat
@@ -22,7 +22,7 @@ import type {
22
22
  SidebarBlock,
23
23
  SidebarItem,
24
24
  SidebarSection,
25
- } from '../nav/types'
25
+ } from './nav-types'
26
26
  import { sidebarBlockMobile } from './sidebar-block-model'
27
27
 
28
28
  /** Hard cap on bar slots, INCLUDING More. Five 72px slots is the widest a 360px viewport holds. */
@@ -43,14 +43,7 @@
43
43
  * read stays in JS — through `useSyncExternalStore`, so SSR, hydration and the first paint agree.
44
44
  * Recorded in `docs/CONTROLS-SPEC.md` §1 (C9) and `docs/ASIDE-SPEC.md` §0.
45
45
  */
46
- import {
47
- createContext,
48
- useCallback,
49
- useContext,
50
- useMemo,
51
- useState,
52
- useSyncExternalStore,
53
- } from 'react'
46
+ import { createContext, useCallback, useContext, useMemo, useState } from 'react'
54
47
  import type { ReactNode } from 'react'
55
48
  import { createPortal } from 'react-dom'
56
49
  import { useMantineTheme } from '@mantine/core'
@@ -61,6 +54,7 @@ import { usePersistedOrLocal } from '../state/persisted-or-local'
61
54
  import { FilterSetScope } from '../controls/filter-context'
62
55
  import { useAsidePanelSlot } from './page-bar'
63
56
  import { useIsomorphicLayoutEffect } from './isomorphic-layout-effect'
57
+ import { useMediaQueryMatches } from './use-breakpoint'
64
58
  import classes from './page-aside.module.css'
65
59
 
66
60
  type AsideRegion = {
@@ -133,50 +127,6 @@ export function AsideOutlet({ className }: { className?: string }) {
133
127
  return <div ref={setTarget} className={className} />
134
128
  }
135
129
 
136
- /** `window.matchMedia`, or `null` where there is no window and on a shim that does not ship it. */
137
- function mediaQueryList(query: string): MediaQueryList | null {
138
- if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return null
139
- return window.matchMedia(query)
140
- }
141
-
142
- /**
143
- * Internal — a media query read through `useSyncExternalStore`, the ONE hook whose server snapshot
144
- * React honours during BOTH `renderToString` and the hydration pass.
145
- *
146
- * That is the whole reason it is not `@mantine/hooks`' `useMediaQuery`, which reads `matchMedia`
147
- * inside a `useState` INITIALIZER: on the server it returns `initialValue`, on the client's very
148
- * first render it returns the real match, and when that render is a HYDRATION the two disagree
149
- * silently — a server-rendered desktop panel hydrating on a phone against a client tree that has
150
- * already moved the children into the page bar's sheet. `getServerSnapshot` makes React render the
151
- * server's answer during hydration and re-render with the real one immediately after, which is the
152
- * documented, warning-free version of the same correction.
153
- *
154
- * `fallback` is what the server (and a shim with no `matchMedia`) sees. It stays the DESKTOP
155
- * answer, because the portalled branch is the one a shell-less or pre-hydration page can always
156
- * fall back to rendering.
157
- */
158
- function useMediaQueryMatches(query: string, fallback: boolean): boolean {
159
- // ONE `MediaQueryList` per query, held across renders. `matchMedia()` allocates a fresh live
160
- // object on every call and `getSnapshot` runs on every render AND after every store
161
- // notification — so calling it in there churned an object per read, and the list `subscribe`
162
- // listened on was never the one `getSnapshot` measured. Reading `.matches` off the SAME instance
163
- // it subscribed to is what makes the snapshot and the notification describe one thing.
164
- const list = useMemo(() => mediaQueryList(query), [query])
165
- const subscribe = useCallback(
166
- (onStoreChange: () => void) => {
167
- if (list === null) return () => {}
168
- list.addEventListener('change', onStoreChange)
169
- return () => {
170
- list.removeEventListener('change', onStoreChange)
171
- }
172
- },
173
- [list],
174
- )
175
- const getSnapshot = useCallback(() => list?.matches ?? fallback, [list, fallback])
176
- const getServerSnapshot = useCallback(() => fallback, [fallback])
177
- return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
178
- }
179
-
180
130
  /** The three boxes `PageAside` paints, in every projection (`common/props.ts`). */
181
131
  export type PageAsideSlot = 'root' | 'header' | 'body'
182
132
 
@@ -0,0 +1,33 @@
1
+ /*
2
+ * `PageTitle` — the shell-less page-title primitive (C5 consolidation, `docs/CONTROLS-SPEC.md`
3
+ * law C8's third named spot alongside the breadcrumb and `PageBar.title`). Same treatment as
4
+ * `page-bar.module.css`'s `.title` (head font, 88% stretch, weight 550) — a page is named once,
5
+ * and every named spot reads identically regardless of which one a given surface uses.
6
+ */
7
+
8
+ .root {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 4px;
12
+ }
13
+
14
+ .titleRow {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: var(--vx-space-nav-icon-gap, 8px);
18
+ }
19
+
20
+ .title {
21
+ margin: 0;
22
+ font-family: var(--basalt-font-head);
23
+ font-size: var(--vx-text-xl);
24
+ font-weight: 550;
25
+ font-stretch: 88%;
26
+ color: var(--vx-ink);
27
+ }
28
+
29
+ .subtitle {
30
+ margin: 0;
31
+ font-size: var(--vx-text-sm);
32
+ color: var(--vx-faint);
33
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Typed CSS-module declaration (the S4 CSS-modules path). Named string props — not an index
3
+ * signature — so dot access stays verbatim under the package's strict flags.
4
+ */
5
+ declare const classes: {
6
+ readonly root: string
7
+ readonly titleRow: string
8
+ readonly title: string
9
+ readonly subtitle: string
10
+ }
11
+ export default classes
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `PageTitle` — the shell-less page-title primitive (C5 consolidation). Law C8
3
+ * (`docs/CONTROLS-SPEC.md` §2.2) names two sanctioned spots for a page's ONE name: the breadcrumb
4
+ * inside `BasaltShell`, or `PageBar.title` in a shell-less app — but neither fits a surface that
5
+ * has no `PageBar` at all, which is exactly what `BasaltErrorBoundary`'s default fallback and an
6
+ * auth gate ARE. Both were the two named `in-body-page-title` waivers this primitive closes
7
+ * (argo's `lib/error-boundary.tsx:36`, `lib/auth-gate.tsx:30` — each a bare `<Title order={2}>` +
8
+ * a muted subtitle line, hand-copied).
9
+ *
10
+ * A plain `<h1>` + CSS module, NOT Mantine's `<Title>` component — same reason `page-bar.tsx`'s
11
+ * own `title` row does (`page-bar.module.css`'s `.title`): `basalt/in-body-page-title` and its
12
+ * text-lane twin both match the JSX tag `<Title`, so a THIRD sanctioned spot built out of that
13
+ * same tag would need a waiver on itself. A raw, styled `<h1>` needs none — it IS the remedy, not
14
+ * a case of it.
15
+ *
16
+ * @example
17
+ * import { PageTitle } from 'basalt-ui'
18
+ *
19
+ * <PageTitle title="Something went wrong" subtitle="The page hit an unexpected error." />
20
+ */
21
+ import { cx } from '../common/props'
22
+ import { assertRequiredProps } from '../common/validate'
23
+ import type { BasaltProps, SlotStylesProps } from '../common/props'
24
+ import type { ReactNode } from 'react'
25
+ import classes from './page-title.module.css'
26
+
27
+ export type PageTitleSlot = 'root' | 'title' | 'subtitle'
28
+
29
+ export type PageTitleProps = BasaltProps &
30
+ SlotStylesProps<PageTitleSlot> & {
31
+ /** The page's one name. */
32
+ title: ReactNode
33
+ /** Optional muted line below the title. */
34
+ subtitle?: ReactNode
35
+ /** Optional leading icon, rendered before the title. Decorative — hidden from assistive tech,
36
+ * same convention as `WidgetHeader.icon`. */
37
+ icon?: ReactNode
38
+ }
39
+
40
+ export function PageTitle(props: PageTitleProps): ReactNode {
41
+ assertRequiredProps('PageTitle', props, ['title'])
42
+ const { title, subtitle, icon, className, classNames, style } = props
43
+ return (
44
+ <div
45
+ className={cx(classes.root, classNames?.root, className)}
46
+ {...(style !== undefined && { style })}
47
+ >
48
+ <div className={cx(classes.titleRow, classNames?.title)}>
49
+ {icon !== undefined && <span aria-hidden="true">{icon}</span>}
50
+ <h1 className={classes.title}>{title}</h1>
51
+ </div>
52
+ {subtitle !== undefined && (
53
+ <p className={cx(classes.subtitle, classNames?.subtitle)}>{subtitle}</p>
54
+ )}
55
+ </div>
56
+ )
57
+ }
@@ -9,7 +9,7 @@
9
9
  * A default re-derived per call site is how the count and the renderer drift apart (see
10
10
  * `accountRowCount`'s doc for the same failure with account rows).
11
11
  */
12
- import type { SidebarBlock } from '../nav/types'
12
+ import type { SidebarBlock } from './nav-types'
13
13
 
14
14
  /** Envelope version for a persisted fold flag. Bump only if the value stops being a boolean. */
15
15
  export const FOLD_VERSION = 1
@@ -21,7 +21,7 @@ import type {
21
21
  SidebarCustomBlock,
22
22
  SidebarListBlock,
23
23
  SidebarProgressBlock,
24
- } from '../nav/types'
24
+ } from './nav-types'
25
25
  import { NavCountBadge } from './nav-count-badge'
26
26
  import { createPersistedState } from '../state'
27
27
  import {
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `useBreakpoint` — one media-query hook over the theme's own breakpoints, SSR/hydration-safe.
3
+ *
4
+ * Built on `useSyncExternalStore`, NOT `@mantine/hooks`' `useMediaQuery` — that hook reads
5
+ * `matchMedia` inside a `useState` initializer, so on the server it returns its `fallback` and on
6
+ * the client's very first render it returns the REAL match; when that render is a hydration pass
7
+ * the two disagree silently (a server-rendered desktop layout hydrating on a phone against a
8
+ * client tree that already moved). `useSyncExternalStore`'s `getServerSnapshot` makes React render
9
+ * the server's answer during hydration and correct itself immediately after — the documented,
10
+ * warning-free version of the same fix. `page-aside.tsx`'s own former private
11
+ * `useMediaQueryMatches` carried this exact rationale; it now lives here and page-aside composes
12
+ * this hook instead (C5 consolidation — one media-query hook, not two).
13
+ *
14
+ * `edge` defaults to `'min'` (`"at least this wide"`); pass `'max'` for `"narrower than"`. The
15
+ * `fallback` (what the server, and a shim with no `matchMedia`, sees) defaults to the answer that
16
+ * assumes the NARROW side of the query — `false` for `min`, `true` for `max` — since a component
17
+ * library cannot know a consumer's real device mix; a call site with its own rationale (e.g.
18
+ * `page-aside.tsx`'s desktop-first portal fallback) overrides it via `options.fallback`.
19
+ *
20
+ * @example
21
+ * import { useBreakpoint } from 'basalt-ui'
22
+ *
23
+ * const isDesktop = useBreakpoint('sm') // >= theme.breakpoints.sm
24
+ * const isNarrow = !useBreakpoint('sm') // < theme.breakpoints.sm
25
+ * const isWide = useBreakpoint('lg') // >= theme.breakpoints.lg
26
+ */
27
+ import { useCallback, useMemo, useSyncExternalStore } from 'react'
28
+ import { useMantineTheme } from '@mantine/core'
29
+
30
+ export type BreakpointName = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
31
+
32
+ /** `window.matchMedia`, or `null` where there is no window and on a shim that does not ship it. */
33
+ function mediaQueryList(query: string): MediaQueryList | null {
34
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return null
35
+ return window.matchMedia(query)
36
+ }
37
+
38
+ /**
39
+ * Internal — the raw media-query read. Exported (not `default`) so `page-aside.tsx` can compose
40
+ * it directly for its `(min-width: theme.breakpoints.sm)` desktop check without re-deriving the
41
+ * query string `useBreakpoint` builds for the fixed `BreakpointName` union.
42
+ */
43
+ export function useMediaQueryMatches(query: string, fallback: boolean): boolean {
44
+ // ONE `MediaQueryList` per query, held across renders — `matchMedia()` allocates a fresh live
45
+ // object on every call, and `getSnapshot` runs on every render AND after every store
46
+ // notification, so calling it there would churn an object per read while `subscribe` listened
47
+ // on a different instance than `getSnapshot` measured.
48
+ const list = useMemo(() => mediaQueryList(query), [query])
49
+ const subscribe = useCallback(
50
+ (onStoreChange: () => void) => {
51
+ if (list === null) return () => {}
52
+ list.addEventListener('change', onStoreChange)
53
+ return () => {
54
+ list.removeEventListener('change', onStoreChange)
55
+ }
56
+ },
57
+ [list],
58
+ )
59
+ const getSnapshot = useCallback(() => list?.matches ?? fallback, [list, fallback])
60
+ const getServerSnapshot = useCallback(() => fallback, [fallback])
61
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
62
+ }
63
+
64
+ export function useBreakpoint(
65
+ name: BreakpointName,
66
+ edge: 'min' | 'max' = 'min',
67
+ options?: { fallback?: boolean },
68
+ ): boolean {
69
+ const theme = useMantineTheme()
70
+ const bp = theme.breakpoints[name]
71
+ const fallback = options?.fallback ?? edge === 'max'
72
+ return useMediaQueryMatches(`(${edge}-width: ${bp})`, fallback)
73
+ }
@@ -561,13 +561,7 @@ export type FieldCodec = {
561
561
  readonly int: boolean
562
562
  readonly step: number | undefined
563
563
  }
564
- /**
565
- * Validate a STORED (or caller-supplied) value. `null` = unusable.
566
- *
567
- * Deliberately a different function from `fromSearch` for one kind: the deprecated multi store
568
- * reads an empty URL array as "absent", and reusing that rule on the storage side would turn a
569
- * deliberately CLEARED filter into a miss and resurrect the fallback on upgrade.
570
- */
564
+ /** Validate a STORED (or caller-supplied) value. `null` = unusable. */
571
565
  decode(raw: unknown): unknown
572
566
  /** Pull the field out of a router search object. `null` = absent or invalid. */
573
567
  fromSearch(search: Record<string, unknown>): unknown
@@ -614,17 +608,11 @@ function sameList(a: unknown, b: unknown): boolean {
614
608
  }
615
609
 
616
610
  /**
617
- * Build the codec for one field. `legacyMultiEmpty` restores the DEPRECATED multi-store's rule that
618
- * an EMPTY url array means "absent, fall through to storage" — the deprecated wrapper's contract,
619
- * never the new store's, where an empty selection is a value like any other.
611
+ * Build the codec for one field.
620
612
  *
621
613
  * @internal — see `FieldCodec`.
622
614
  */
623
- export function resolveFieldCodec(
624
- name: string,
625
- f: AnyField,
626
- opts?: { legacyMultiEmpty?: boolean },
627
- ): FieldCodec {
615
+ export function resolveFieldCodec(name: string, f: AnyField): FieldCodec {
628
616
  switch (f.kind) {
629
617
  case 'enum': {
630
618
  const allowed = new Set<string>(f.values)
@@ -642,19 +630,9 @@ export function resolveFieldCodec(
642
630
  }
643
631
  return f.values.filter((value) => picked.has(value))
644
632
  }
645
- // The legacy empty-array rule is a URL rule ONLY. A stored `[]` is what "I cleared every
646
- // filter" looks like, and the deprecated store has always read it back as `[]`.
647
- const decodeUrl =
648
- opts?.legacyMultiEmpty === true
649
- ? (raw: unknown): unknown => {
650
- const value = decode(raw)
651
- return Array.isArray(value) && value.length === 0 ? null : value
652
- }
653
- : decode
654
633
  return single(name, f, decode, {
655
634
  optionValues: f.values,
656
635
  equals: sameList,
657
- decodeUrl,
658
636
  })
659
637
  }
660
638
  case 'range': {
@@ -759,13 +737,10 @@ function single(
759
737
  extra?: {
760
738
  optionValues?: readonly string[]
761
739
  equals?: (a: unknown, b: unknown) => boolean
762
- /** URL-side decoder when it differs from the storage-side one (multi, legacy layout only). */
763
- decodeUrl?: (raw: unknown) => unknown
764
740
  /** Number only: the declared bounds and grain, republished onto the handle. */
765
741
  bounds?: FieldCodec['bounds']
766
742
  },
767
743
  ): FieldCodec {
768
- const decodeUrl = extra?.decodeUrl ?? decode
769
744
  return {
770
745
  kind: f.kind,
771
746
  lane: f.lane,
@@ -776,7 +751,7 @@ function single(
776
751
  },
777
752
  ...(extra?.bounds !== undefined && { bounds: extra.bounds }),
778
753
  decode,
779
- fromSearch: (search) => decodeUrl(search[name]),
754
+ fromSearch: (search) => decode(search[name]),
780
755
  toSearch: (value) => ({ [name]: value }),
781
756
  equals: extra?.equals ?? ((a, b) => a === b),
782
757
  optionValues: extra?.optionValues ?? [],
@@ -805,12 +780,6 @@ export type StoreCoreOptions = {
805
780
  readonly key: string
806
781
  readonly fields: Record<string, AnyField>
807
782
  readonly version?: number | undefined
808
- /**
809
- * Legacy single-value storage layout: the whole envelope value IS this field's value, the shape
810
- * `createSearchParamStore` has written since 1.0.0. Set ONLY by the deprecated wrappers — a
811
- * consumer's already-persisted selection has to keep resolving byte-for-byte.
812
- */
813
- readonly legacyValueField?: string
814
783
  }
815
784
 
816
785
  /**
@@ -967,10 +936,9 @@ function assertNoParamCollision(key: string, entries: readonly StoreEntry[]): vo
967
936
  /** @internal — the seam behind `createSearchStore` and `createLocalStore`. */
968
937
  export function createStoreCore(o: StoreCoreOptions): StoreCore {
969
938
  const version = o.version ?? 1
970
- const legacy = o.legacyValueField
971
939
  const entries: StoreEntry[] = Object.entries(o.fields).map(([name, f]) => ({
972
940
  name,
973
- codec: resolveFieldCodec(name, f, { legacyMultiEmpty: legacy !== undefined }),
941
+ codec: resolveFieldCodec(name, f),
974
942
  }))
975
943
  assertNoParamCollision(o.key, entries)
976
944
  const urlEntries = entries.filter((entry) => entry.codec.lane.url)
@@ -983,12 +951,8 @@ export function createStoreCore(o: StoreCoreOptions): StoreCore {
983
951
  const usePersistedRaw = createPersistedState<unknown>({ key: o.key, version, initial: null })
984
952
  const memory = createMemoryLane()
985
953
 
986
- const toRecord = (raw: unknown): StoreRecord => {
987
- if (legacy !== undefined) return raw === null || raw === undefined ? {} : { [legacy]: raw }
988
- return isRecord(raw) ? raw : {}
989
- }
990
- const fromRecord = (record: StoreRecord): unknown =>
991
- legacy !== undefined ? (record[legacy] ?? null) : record
954
+ const toRecord = (raw: unknown): StoreRecord => (isRecord(raw) ? raw : {})
955
+ const fromRecord = (record: StoreRecord): unknown => record
992
956
 
993
957
  const readRecord = (): StoreRecord => toRecord(readPersistedValue(o.key, version))
994
958
 
package/src/styles.css CHANGED
@@ -40,6 +40,42 @@
40
40
  }
41
41
 
42
42
  @layer basalt {
43
+ /* Native/PWA chrome (C5 consolidation) — three of the five rules argo's own `styles/native.css`
44
+ carried, moved here so every consumer gets them for free instead of hand-copying a stylesheet.
45
+ The other two (safe-area padding on the sidebar/main) are CSS-module rules on those specific
46
+ regions, not global — see `shell/app-sidebar.module.css` and `shell/app-main.module.css`.
47
+ Overscroll containment and the footer's safe-area-bottom inset already ship
48
+ (`shell/app-main.module.css`, `shell/app-mobile-nav.module.css`) and are not repeated here. */
49
+
50
+ /* Stops iOS from inflating font sizes on a rotation into landscape — `text-size-adjust` governs
51
+ text INFLATION, a different failure mode than the input-zoom floor just below. */
52
+ html {
53
+ -webkit-text-size-adjust: 100%;
54
+ text-size-adjust: 100%;
55
+ }
56
+
57
+ /* Removes the grey tap-highlight flash on iOS/Android for anything actually tappable. Scoped to
58
+ the interactive roles/tags rather than `*` (argo's own rule): a highlight on non-interactive
59
+ content is not the flash this exists to remove, and `*` would also numb a consumer's own
60
+ custom interactive elements that don't match one of these. */
61
+ button,
62
+ a,
63
+ [role='button'],
64
+ [role='tab'] {
65
+ -webkit-tap-highlight-color: transparent;
66
+ }
67
+
68
+ /* A long-press on a control must not select its label or pop the iOS callout menu — inputs and
69
+ prose content stay selectable. Same Mantine class-hook targets argo's own rule used
70
+ (`.mantine-Button-root`/`.mantine-NavLink-root`/`.mantine-Burger-root`): the theme layer is
71
+ the one place allowed to reach for a Mantine class hook directly, per `basalt-mantine.md`. */
72
+ .mantine-Button-root,
73
+ .mantine-NavLink-root,
74
+ .mantine-Burger-root {
75
+ -webkit-touch-callout: none;
76
+ user-select: none;
77
+ }
78
+
43
79
  /* iOS input-zoom FLOOR. Safari zooms the viewport whenever a focused input computes below 16px,
44
80
  and never zooms back out on blur — the single worst mobile-form defect. 16px is the only clean
45
81
  fix: `maximum-scale=1` / `user-scalable=no` block pinch-zoom on Android (WCAG 1.4.4),