@nocoo/basalt 2.1.8 → 3.0.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 (352) hide show
  1. package/README.md +2 -0
  2. package/ai/COMPATIBILITY.md +11 -1
  3. package/ai/DESIGN.md +342 -0
  4. package/ai/INTEGRATION.md +239 -152
  5. package/ai/RECIPES.md +96 -26
  6. package/ai/USAGE.md +5 -5
  7. package/ai/registry.json +2883 -275
  8. package/dist/assets/brand-mark.js.map +1 -1
  9. package/dist/charts/area.js.map +1 -1
  10. package/dist/charts/bar.js.map +1 -1
  11. package/dist/charts/bullet.js.map +1 -1
  12. package/dist/charts/charts.js.map +1 -1
  13. package/dist/charts/config.d.ts +11 -11
  14. package/dist/charts/config.js +4 -4
  15. package/dist/charts/config.js.map +1 -1
  16. package/dist/charts/custom-chart.js.map +1 -1
  17. package/dist/charts/date-navigation.js +10 -10
  18. package/dist/charts/date-navigation.js.map +1 -1
  19. package/dist/charts/donut.js +1 -1
  20. package/dist/charts/donut.js.map +1 -1
  21. package/dist/charts/frame.d.ts +1 -1
  22. package/dist/charts/frame.js +4 -4
  23. package/dist/charts/frame.js.map +1 -1
  24. package/dist/charts/funnel.js.map +1 -1
  25. package/dist/charts/gauge.js +5 -5
  26. package/dist/charts/gauge.js.map +1 -1
  27. package/dist/charts/grouped-bar.js.map +1 -1
  28. package/dist/charts/heatmap-calendar.js +11 -11
  29. package/dist/charts/heatmap-calendar.js.map +1 -1
  30. package/dist/charts/heatmap-matrix.js +8 -8
  31. package/dist/charts/heatmap-matrix.js.map +1 -1
  32. package/dist/charts/item-list.js +1 -1
  33. package/dist/charts/item-list.js.map +1 -1
  34. package/dist/charts/legend.js +19 -19
  35. package/dist/charts/legend.js.map +1 -1
  36. package/dist/charts/line.js.map +1 -1
  37. package/dist/charts/palette.js +2 -2
  38. package/dist/charts/palette.js.map +1 -1
  39. package/dist/charts/radar.js.map +1 -1
  40. package/dist/charts/sample.js.map +1 -1
  41. package/dist/charts/sankey.js.map +1 -1
  42. package/dist/charts/series.js.map +1 -1
  43. package/dist/charts/slot-bar.js +2 -2
  44. package/dist/charts/slot-bar.js.map +1 -1
  45. package/dist/charts/sparkline.js +38 -23
  46. package/dist/charts/sparkline.js.map +1 -1
  47. package/dist/charts/stacked-bar.js.map +1 -1
  48. package/dist/charts/stat-card.d.ts +2 -2
  49. package/dist/charts/stat-card.js +61 -58
  50. package/dist/charts/stat-card.js.map +1 -1
  51. package/dist/charts/timeline.js +18 -9
  52. package/dist/charts/timeline.js.map +1 -1
  53. package/dist/charts/timeseries.js.map +1 -1
  54. package/dist/charts/tooltip.js +16 -16
  55. package/dist/charts/tooltip.js.map +1 -1
  56. package/dist/components/accordion.js +1 -1
  57. package/dist/components/accordion.js.map +1 -1
  58. package/dist/components/alert-dialog.js +4 -4
  59. package/dist/components/alert-dialog.js.map +1 -1
  60. package/dist/components/app-header.d.ts +5 -1
  61. package/dist/components/app-header.js +19 -11
  62. package/dist/components/app-header.js.map +1 -1
  63. package/dist/components/app-shell.d.ts +4 -1
  64. package/dist/components/app-shell.js +8 -5
  65. package/dist/components/app-shell.js.map +1 -1
  66. package/dist/components/approval-card.d.ts +13 -0
  67. package/dist/components/approval-card.js +161 -0
  68. package/dist/components/approval-card.js.map +1 -0
  69. package/dist/components/autocomplete.js.map +1 -1
  70. package/dist/components/avatar.d.ts +12 -0
  71. package/dist/components/avatar.js +39 -18
  72. package/dist/components/avatar.js.map +1 -1
  73. package/dist/components/badge.js +8 -8
  74. package/dist/components/badge.js.map +1 -1
  75. package/dist/components/banner.d.ts +2 -2
  76. package/dist/components/banner.js +9 -8
  77. package/dist/components/banner.js.map +1 -1
  78. package/dist/components/basalt-mark.js +1 -1
  79. package/dist/components/basalt-mark.js.map +1 -1
  80. package/dist/components/battery-meter.js +5 -5
  81. package/dist/components/battery-meter.js.map +1 -1
  82. package/dist/components/breadcrumbs.js +6 -6
  83. package/dist/components/breadcrumbs.js.map +1 -1
  84. package/dist/components/button.js +10 -10
  85. package/dist/components/button.js.map +1 -1
  86. package/dist/components/chat-bubble.js +4 -3
  87. package/dist/components/chat-bubble.js.map +1 -1
  88. package/dist/components/chat-composer.d.ts +21 -3
  89. package/dist/components/chat-composer.js +126 -60
  90. package/dist/components/chat-composer.js.map +1 -1
  91. package/dist/components/chat-header.js +6 -6
  92. package/dist/components/chat-header.js.map +1 -1
  93. package/dist/components/chat-inbox.js +34 -30
  94. package/dist/components/chat-inbox.js.map +1 -1
  95. package/dist/components/chat-markdown.d.ts +8 -0
  96. package/dist/components/chat-markdown.js +122 -0
  97. package/dist/components/chat-markdown.js.map +1 -0
  98. package/dist/components/chat-message.d.ts +26 -0
  99. package/dist/components/chat-message.js +145 -0
  100. package/dist/components/chat-message.js.map +1 -0
  101. package/dist/components/checkbox.js +11 -11
  102. package/dist/components/checkbox.js.map +1 -1
  103. package/dist/components/clipboard-text.js +3 -3
  104. package/dist/components/clipboard-text.js.map +1 -1
  105. package/dist/components/code.d.ts +22 -20
  106. package/dist/components/code.js +94 -32
  107. package/dist/components/code.js.map +1 -1
  108. package/dist/components/collapsible.js +8 -4
  109. package/dist/components/collapsible.js.map +1 -1
  110. package/dist/components/combobox.js.map +1 -1
  111. package/dist/components/command-palette.js +73 -47
  112. package/dist/components/command-palette.js.map +1 -1
  113. package/dist/components/confirm-dialog.d.ts +4 -1
  114. package/dist/components/confirm-dialog.js +20 -18
  115. package/dist/components/confirm-dialog.js.map +1 -1
  116. package/dist/components/context-cards.d.ts +15 -0
  117. package/dist/components/context-cards.js +148 -0
  118. package/dist/components/context-cards.js.map +1 -0
  119. package/dist/components/context-menu.d.ts +1 -1
  120. package/dist/components/context-menu.js +18 -14
  121. package/dist/components/context-menu.js.map +1 -1
  122. package/dist/components/data-table.d.ts +8 -2
  123. package/dist/components/data-table.js +161 -106
  124. package/dist/components/data-table.js.map +1 -1
  125. package/dist/components/date-picker.js +13 -13
  126. package/dist/components/date-picker.js.map +1 -1
  127. package/dist/components/delete-resource.js +1 -1
  128. package/dist/components/delete-resource.js.map +1 -1
  129. package/dist/components/description-list.js +3 -3
  130. package/dist/components/description-list.js.map +1 -1
  131. package/dist/components/dialog.d.ts +2 -2
  132. package/dist/components/dialog.js +8 -8
  133. package/dist/components/dialog.js.map +1 -1
  134. package/dist/components/diff-table.d.ts +15 -0
  135. package/dist/components/diff-table.js +146 -0
  136. package/dist/components/diff-table.js.map +1 -0
  137. package/dist/components/dock.js +2 -2
  138. package/dist/components/dock.js.map +1 -1
  139. package/dist/components/dropdown-menu.js +23 -18
  140. package/dist/components/dropdown-menu.js.map +1 -1
  141. package/dist/components/editable-nav-item.js +63 -58
  142. package/dist/components/editable-nav-item.js.map +1 -1
  143. package/dist/components/empty.js +6 -6
  144. package/dist/components/empty.js.map +1 -1
  145. package/dist/components/fab.js +1 -1
  146. package/dist/components/fab.js.map +1 -1
  147. package/dist/components/field.js +3 -3
  148. package/dist/components/field.js.map +1 -1
  149. package/dist/components/file-dropzone.js +7 -7
  150. package/dist/components/file-dropzone.js.map +1 -1
  151. package/dist/components/filter-bar.js +6 -6
  152. package/dist/components/filter-bar.js.map +1 -1
  153. package/dist/components/flow.js +4 -3
  154. package/dist/components/flow.js.map +1 -1
  155. package/dist/components/grid.d.ts +3 -1
  156. package/dist/components/grid.js +15 -8
  157. package/dist/components/grid.js.map +1 -1
  158. package/dist/components/hover-card.js +1 -1
  159. package/dist/components/hover-card.js.map +1 -1
  160. package/dist/components/icon-picker.d.ts +8 -2
  161. package/dist/components/icon-picker.js +75 -47
  162. package/dist/components/icon-picker.js.map +1 -1
  163. package/dist/components/inline-editable.js +6 -6
  164. package/dist/components/inline-editable.js.map +1 -1
  165. package/dist/components/input-area.js +3 -3
  166. package/dist/components/input-area.js.map +1 -1
  167. package/dist/components/input-group.js +5 -5
  168. package/dist/components/input-group.js.map +1 -1
  169. package/dist/components/input.js +4 -4
  170. package/dist/components/input.js.map +1 -1
  171. package/dist/components/label.js +3 -3
  172. package/dist/components/label.js.map +1 -1
  173. package/dist/components/layer-card.d.ts +9 -4
  174. package/dist/components/layer-card.js +72 -67
  175. package/dist/components/layer-card.js.map +1 -1
  176. package/dist/components/link.js.map +1 -1
  177. package/dist/components/loader.d.ts +16 -10
  178. package/dist/components/loader.js +46 -53
  179. package/dist/components/loader.js.map +1 -1
  180. package/dist/components/loading-screen.js +3 -3
  181. package/dist/components/loading-screen.js.map +1 -1
  182. package/dist/components/menu-bar.js +32 -27
  183. package/dist/components/menu-bar.js.map +1 -1
  184. package/dist/components/meter.d.ts +5 -5
  185. package/dist/components/meter.js +25 -12
  186. package/dist/components/meter.js.map +1 -1
  187. package/dist/components/multi-select.js +66 -62
  188. package/dist/components/multi-select.js.map +1 -1
  189. package/dist/components/navigation-menu.d.ts +2 -2
  190. package/dist/components/navigation-menu.js +22 -8
  191. package/dist/components/navigation-menu.js.map +1 -1
  192. package/dist/components/overlay.js +2 -2
  193. package/dist/components/overlay.js.map +1 -1
  194. package/dist/components/page-header.d.ts +3 -1
  195. package/dist/components/page-header.js +12 -12
  196. package/dist/components/page-header.js.map +1 -1
  197. package/dist/components/pagination.js +3 -3
  198. package/dist/components/pagination.js.map +1 -1
  199. package/dist/components/popover.js +18 -23
  200. package/dist/components/popover.js.map +1 -1
  201. package/dist/components/prompt-bar.d.ts +16 -0
  202. package/dist/components/prompt-bar.js +49 -0
  203. package/dist/components/prompt-bar.js.map +1 -0
  204. package/dist/components/radio.js +9 -9
  205. package/dist/components/radio.js.map +1 -1
  206. package/dist/components/recommendation-card.d.ts +13 -0
  207. package/dist/components/recommendation-card.js +123 -0
  208. package/dist/components/recommendation-card.js.map +1 -0
  209. package/dist/components/resource-list.js +1 -1
  210. package/dist/components/resource-list.js.map +1 -1
  211. package/dist/components/responsive-master-detail.js +7 -7
  212. package/dist/components/responsive-master-detail.js.map +1 -1
  213. package/dist/components/scroll-area.js +3 -3
  214. package/dist/components/scroll-area.js.map +1 -1
  215. package/dist/components/section-rule.d.ts +5 -3
  216. package/dist/components/section-rule.js +12 -12
  217. package/dist/components/section-rule.js.map +1 -1
  218. package/dist/components/segment-control.js +3 -3
  219. package/dist/components/segment-control.js.map +1 -1
  220. package/dist/components/select.js +47 -32
  221. package/dist/components/select.js.map +1 -1
  222. package/dist/components/sensitive-input.js +17 -19
  223. package/dist/components/sensitive-input.js.map +1 -1
  224. package/dist/components/separator.js.map +1 -1
  225. package/dist/components/sheet.d.ts +4 -4
  226. package/dist/components/sheet.js +12 -10
  227. package/dist/components/sheet.js.map +1 -1
  228. package/dist/components/sidebar.d.ts +3 -1
  229. package/dist/components/sidebar.js +150 -138
  230. package/dist/components/sidebar.js.map +1 -1
  231. package/dist/components/skeleton-line.d.ts +1 -1
  232. package/dist/components/skeleton-line.js +2 -2
  233. package/dist/components/skeleton-line.js.map +1 -1
  234. package/dist/components/slider.js +2 -2
  235. package/dist/components/slider.js.map +1 -1
  236. package/dist/components/stat-strip.js +5 -4
  237. package/dist/components/stat-strip.js.map +1 -1
  238. package/dist/components/switch.js +6 -6
  239. package/dist/components/switch.js.map +1 -1
  240. package/dist/components/table-of-contents.js +5 -5
  241. package/dist/components/table-of-contents.js.map +1 -1
  242. package/dist/components/table-pager.js +2 -2
  243. package/dist/components/table-pager.js.map +1 -1
  244. package/dist/components/table.js +4 -4
  245. package/dist/components/table.js.map +1 -1
  246. package/dist/components/tabs.js +7 -6
  247. package/dist/components/tabs.js.map +1 -1
  248. package/dist/components/tag-badge.d.ts +10 -10
  249. package/dist/components/tag-badge.js +11 -11
  250. package/dist/components/tag-badge.js.map +1 -1
  251. package/dist/components/tag-color-picker.d.ts +8 -1
  252. package/dist/components/tag-color-picker.js +33 -25
  253. package/dist/components/tag-color-picker.js.map +1 -1
  254. package/dist/components/text.js +5 -5
  255. package/dist/components/text.js.map +1 -1
  256. package/dist/components/theme-toggle.js +1 -1
  257. package/dist/components/theme-toggle.js.map +1 -1
  258. package/dist/components/thinking.d.ts +16 -0
  259. package/dist/components/thinking.js +85 -0
  260. package/dist/components/thinking.js.map +1 -0
  261. package/dist/components/toast.js +25 -20
  262. package/dist/components/toast.js.map +1 -1
  263. package/dist/components/toggle-group.js +6 -5
  264. package/dist/components/toggle-group.js.map +1 -1
  265. package/dist/components/toggle.js +6 -6
  266. package/dist/components/toggle.js.map +1 -1
  267. package/dist/components/tool-chips.d.ts +12 -0
  268. package/dist/components/tool-chips.js +138 -0
  269. package/dist/components/tool-chips.js.map +1 -0
  270. package/dist/components/toolbar.js +3 -3
  271. package/dist/components/toolbar.js.map +1 -1
  272. package/dist/components/tooltip.js +1 -1
  273. package/dist/components/tooltip.js.map +1 -1
  274. package/dist/components/typeahead-field.js +59 -56
  275. package/dist/components/typeahead-field.js.map +1 -1
  276. package/dist/components/upload-queue.js +15 -14
  277. package/dist/components/upload-queue.js.map +1 -1
  278. package/dist/models/agent-feedback.d.ts +44 -0
  279. package/dist/models/agent-feedback.js +28 -0
  280. package/dist/models/agent-feedback.js.map +1 -0
  281. package/dist/models/avatar.d.ts +2 -0
  282. package/dist/models/avatar.js +15 -0
  283. package/dist/models/avatar.js.map +1 -0
  284. package/dist/models/code.d.ts +6 -0
  285. package/dist/models/code.js +24 -0
  286. package/dist/models/code.js.map +1 -0
  287. package/dist/models/context-cards.d.ts +13 -0
  288. package/dist/models/context-cards.js +17 -0
  289. package/dist/models/context-cards.js.map +1 -0
  290. package/dist/models/diff-table.d.ts +12 -0
  291. package/dist/models/diff-table.js +9 -0
  292. package/dist/models/diff-table.js.map +1 -0
  293. package/dist/models/recommendation.d.ts +26 -0
  294. package/dist/models/recommendation.js +23 -0
  295. package/dist/models/recommendation.js.map +1 -0
  296. package/dist/providers/accent.js +1 -1
  297. package/dist/providers/accent.js.map +1 -1
  298. package/dist/providers/link.js.map +1 -1
  299. package/dist/providers/theme.js +1 -1
  300. package/dist/providers/theme.js.map +1 -1
  301. package/dist/styles/base.css +128 -0
  302. package/dist/styles/standalone.css +1994 -937
  303. package/dist/styles/tailwind.css +179 -13
  304. package/dist/styles/tokens.css +679 -20
  305. package/dist/utils/cn.js +25 -4
  306. package/dist/utils/cn.js.map +1 -1
  307. package/dist/utils/control-surface.d.ts +1 -1
  308. package/dist/utils/control-surface.js +1 -1
  309. package/dist/utils/control-surface.js.map +1 -1
  310. package/dist/utils/navigation.d.ts +3 -0
  311. package/dist/utils/navigation.js +8 -0
  312. package/dist/utils/navigation.js.map +1 -0
  313. package/dist/utils/preference-store.js +1 -1
  314. package/dist/utils/preference-store.js.map +1 -1
  315. package/dist/utils/selection-indicator.d.ts +1 -1
  316. package/dist/utils/selection-indicator.js +49 -72
  317. package/dist/utils/selection-indicator.js.map +1 -1
  318. package/dist/utils/use-checkable-group.js.map +1 -1
  319. package/dist/utils/use-hover-highlight.d.ts +4 -0
  320. package/dist/utils/use-hover-highlight.js +80 -0
  321. package/dist/utils/use-hover-highlight.js.map +1 -0
  322. package/dist/viewmodels/use-approval-card.d.ts +20 -0
  323. package/dist/viewmodels/use-approval-card.js +83 -0
  324. package/dist/viewmodels/use-approval-card.js.map +1 -0
  325. package/dist/viewmodels/use-chat-composer.d.ts +16 -0
  326. package/dist/viewmodels/use-chat-composer.js +34 -0
  327. package/dist/viewmodels/use-chat-composer.js.map +1 -0
  328. package/dist/viewmodels/use-chat-message.d.ts +16 -0
  329. package/dist/viewmodels/use-chat-message.js +26 -0
  330. package/dist/viewmodels/use-chat-message.js.map +1 -0
  331. package/dist/viewmodels/use-code-block.d.ts +8 -0
  332. package/dist/viewmodels/use-code-block.js +38 -0
  333. package/dist/viewmodels/use-code-block.js.map +1 -0
  334. package/dist/viewmodels/use-context-cards.d.ts +17 -0
  335. package/dist/viewmodels/use-context-cards.js +18 -0
  336. package/dist/viewmodels/use-context-cards.js.map +1 -0
  337. package/dist/viewmodels/use-diff-table.d.ts +16 -0
  338. package/dist/viewmodels/use-diff-table.js +42 -0
  339. package/dist/viewmodels/use-diff-table.js.map +1 -0
  340. package/dist/viewmodels/use-loader.d.ts +4 -0
  341. package/dist/viewmodels/use-loader.js +22 -0
  342. package/dist/viewmodels/use-loader.js.map +1 -0
  343. package/dist/viewmodels/use-recommendation-card.d.ts +17 -0
  344. package/dist/viewmodels/use-recommendation-card.js +41 -0
  345. package/dist/viewmodels/use-recommendation-card.js.map +1 -0
  346. package/dist/viewmodels/use-thinking.d.ts +13 -0
  347. package/dist/viewmodels/use-thinking.js +18 -0
  348. package/dist/viewmodels/use-thinking.js.map +1 -0
  349. package/dist/viewmodels/use-tool-chips.d.ts +10 -0
  350. package/dist/viewmodels/use-tool-chips.js +20 -0
  351. package/dist/viewmodels/use-tool-chips.js.map +1 -0
  352. package/package.json +3 -1
package/ai/INTEGRATION.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Basalt application chrome
2
2
 
3
+ Design tokens, component categories, intrinsic sizing and motion rules are authoritative in [DESIGN.md](DESIGN.md). Follow that contract for library code and examples.
4
+
3
5
  This is the one-pass setup for a Basalt app. An MVP is **login + shell + one page**. After that, product work is arranging `LayerCard` on the island. Do not invent a second header, a second rail, or a second title above the content heading.
4
6
 
5
7
  The library owns the rail, the main column, collapse motion, the island, the page heading, and region rules. The app owns navigation data, brand, version, identity, and page bodies.
@@ -43,7 +45,7 @@ AppShell ← dashboard and catalog routes
43
45
  ├── Sidebar ← product title + version + nav
44
46
  └── AppMain ← id="main-content"
45
47
  ├── AppHeader ← crumbs + current page | top-right
46
- └── island wrap ← px-2 pb-2 md:px-3 md:pb-3
48
+ └── island wrap ← px-basalt-space-lg pb-basalt-space-lg md:px-basalt-space-lg md:pb-basalt-space-lg
47
49
  └── ContentIsland
48
50
  ├── PageHeader ← title + subtitle | create / short filters
49
51
  │ ← optional own-row complex filters
@@ -51,7 +53,7 @@ AppShell ← dashboard and catalog routes
51
53
  └── LayerCard… ← all later product UI
52
54
  ```
53
55
 
54
- `AppHeader.title` is the current page in the top bar (`h1`, `text-sm`). `PageHeader.title` is the content heading (`h1`, `text-2xl`). They may use the same words. They are different roles. Do not put an icon + page name above `PageHeader` — that duplicates the bar.
56
+ `AppHeader.title` is the current page in the top bar (`h1`, `text-sm`). `PageHeader.title` is the content heading (`h1`, `text-2xl`). They may use the same words in regular workspaces. Mobile immersive layouts instead use one compact AppHeader and a desktop-only PageHeader wrapper; see §6. They are different roles. Do not put an icon + page name above `PageHeader` — that duplicates the bar.
55
57
 
56
58
  The landing page and its prerendered HTML share `LandingContent`. Load the site stylesheet from `index.html` so it also works without JavaScript. Scope marketing CSS to `.landing` and crawl summaries to `[data-crawl-page]`; do not set global `header`, `main`, or `footer` dimensions. Keep document scrolling available on `html`, `body`, and `#root`. `AppShell` contains the dashboard viewport, and `ContentIsland` owns its independent scroll. Give both a positioning context (`relative`) so absolutely positioned content, including screen-reader labels, stays inside the correct scroll boundary.
57
59
 
@@ -291,7 +293,7 @@ function PreferencesControls() {
291
293
  const { accent, setAccent } = useAccent();
292
294
 
293
295
  return (
294
- <div className="flex flex-wrap items-center gap-2">
296
+ <div className="flex flex-wrap items-center gap-basalt-space-lg">
295
297
  <span className="text-sm font-medium">Theme: {theme}</span>
296
298
  <Button
297
299
  variant="outline"
@@ -300,7 +302,7 @@ function PreferencesControls() {
300
302
  >
301
303
  Toggle {theme === "dark" ? "Light" : "Dark"}
302
304
  </Button>
303
- <span className="text-sm font-medium ml-2">Accent: {accent}</span>
305
+ <span className="text-sm font-medium ml-basalt-space-lg">Accent: {accent}</span>
304
306
  <Button
305
307
  variant="outline"
306
308
  size="sm"
@@ -332,7 +334,7 @@ export function HostPreferencesApp() {
332
334
  accent={accent}
333
335
  onAccentChange={setAccent}
334
336
  >
335
- <div className="p-4 border rounded-lg space-y-3">
337
+ <div className="p-basalt-space-lg border rounded-lg space-y-basalt-space-lg">
336
338
  <p className="text-xs text-basalt-muted-foreground">
337
339
  Host owns document root DOM and persistence. Basalt contexts operate safely without side-effects.
338
340
  </p>
@@ -365,25 +367,49 @@ Icons: `lucide-react`, `strokeWidth={1.5}`, nav size `h-4 w-4 shrink-0`.
365
367
  <a id="root-geometry"></a>
366
368
  ## 6. Root geometry
367
369
 
368
- `AppShell` is a full-viewport flex row: `h-screen w-full overflow-hidden bg-basalt-background`.
370
+ `AppShell` owns the viewport and safe area; `AppMain` and `ContentIsland` derive their scroll behavior from it. Choose deliberately:
369
371
 
370
- ```
371
- AppShell ← flex row, h-screen, overflow hidden
372
- ├── AppSkipLink ← first child
373
- ├── Sidebar ← the rail (in-flow, not fixed)
374
- └── AppMain ← flex-1 min-w-0 column, overflow hidden
375
- ├── AppHeader ← h-14
376
- └── island wrap ← flex-1 min-h-0, px-2 pb-2 md:px-3 md:pb-3
377
- └── ContentIsland ← page outlet
378
- ```
372
+ | `layout` | Below 768px | 768px and above |
373
+ | --- | --- | --- |
374
+ | `workspace` (default) | Bounded panes | Bounded panes |
375
+ | `responsive` | Natural document scrolling | Bounded panes |
376
+ | `document` | Natural document scrolling | Natural document scrolling |
377
+
378
+ Bounded shells use border-box `height/max-height: 100dvh`. Document shells use `min-height: 100dvh`, automatic height, visible overflow and ordinary root scrolling. Main and island release their height/overflow constraints together; no UA detection, innerHeight workaround, fixed reader viewport or nested mobile scroller is needed. Desktop workspaces keep the island as the vertical scroll owner.
379
379
 
380
380
  ### AppShell, AppMain, AppSkipLink API & Contracts
381
381
 
382
- - **`AppShell`**: Viewport root container. Inherits standard `HTMLAttributes<HTMLDivElement>`. Renders a full-viewport flex row (`h-screen w-full overflow-hidden bg-basalt-background`). Does not forward ref.
383
- - **`AppMain`**: Primary content container. Inherits standard `HTMLAttributes<HTMLElement>`. Defaults `id="main-content"` as the skip-link landmark target, which can be overridden by props via `{...props}`. Recommended to keep `"main-content"` matching `AppSkipLink`. Renders a vertical column with `h-full min-w-0 flex-1 flex-col overflow-hidden`. Does not forward ref.
384
- - **`AppSkipLink`**: Accessibility skip target. Inherits standard `AnchorHTMLAttributes<HTMLAnchorElement>`. Defaults `href="#main-content"`. Renders screen-reader-only element that transitions into absolute focus overlay on keyboard navigation (`sr-only focus:not-sr-only focus:absolute ...`). Does not forward ref.
382
+ - **`AppShell`** accepts `layout?: "workspace" | "responsive" | "document"` and native div attributes. It is the sole safe-area owner of the normal document tree: CSS `env(safe-area-inset-*)` padding is included in its border-box. In document mode bottom padding is trailing content, never a fixed reserved strip. Do not subtract these insets again from main/island height.
383
+ - **`AppMain`** renders the main landmark with `id="main-content"` and `tabIndex={-1}` by default. Native main attributes may override these. It has `min-width: 0` and a flexible column layout; its height and overflow follow the shell.
384
+ - **`AppSkipLink`** points to `#main-content` unless `href` is supplied. Provide visible link text; keep it first in the shell.
385
+ - **`ContentIsland`** (from `components/sidebar`) accepts `mobileSurface?: "inset" | "edge-to-edge"` (default `inset`). Both are L1 surface roots. Edge-to-edge removes mobile padding, radius, ring and shadow; desktop padding/radius remain. The application supplies readable inner content padding, not another painted card.
386
+ - All four accept native attributes and `className`; they do not forward refs. Avoid overriding their height/overflow contract. Put specialized bounded desktop panes inside the island with `md:` constraints only.
387
+
388
+ ```tsx excerpt:responsive-shell
389
+ <AppShell layout="responsive">
390
+ <AppSkipLink>Skip to content</AppSkipLink>
391
+ <AppMain>
392
+ <AppHeader title="Field notes" actions={actions} />
393
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col md:px-basalt-space-lg md:pb-basalt-space-lg">
394
+ <ContentIsland mobileSurface="edge-to-edge">
395
+ <div className="mx-auto max-w-prose px-basalt-space-lg py-basalt-space-lg md:p-0">{content}</div>
396
+ </ContentIsland>
397
+ </div>
398
+ </AppMain>
399
+ </AppShell>
400
+ ```
401
+
402
+ The wrapper intentionally has **no mobile `px-basalt-space-lg` / `pb-basalt-space-lg`**. ContentIsland cannot remove spacing owned by its parent. Remove consumer `h-screen`, `h-full`, `overflow-hidden`, fixed heights and body scroll locks from the document path; all intermediate wrappers must be natural-height columns. Do not target private descendants to undo the library.
403
+
404
+ The consumer owns `viewport` metadata: `width=device-width, initial-scale=1, viewport-fit=cover`. Do not disable zoom. The library never edits document metadata or body styles for layout. Floating modal navigation has a separate portal tree: `Sheet` or overlay `Sidebar` owns modal scroll locking/focus; the Sidebar within it remains bounded and applies that portal's safe-area padding once. Do not add a manual body overflow effect.
405
+
406
+ A desktop Sidebar stays bounded even beside a document: its own navigation may scroll, but the long main content belongs to the document. Hide the desktop rail on mobile and use the same navigation inside an expanded Sidebar in a Sheet. Do not mount duplicate focusable navigation. Single-column reading needs no Sidebar at all.
407
+
408
+ `AppHeader` adds `sticky?: boolean` (default false) and `density?: "comfortable" | "compact"` (default comfortable). Compact is 52px, with at least 44px controls inside the leading/actions slots (not breadcrumb/title links); comfortable remains 56px. `sticky` uses the current scroll owner and an opaque L1 background. Place it directly inside AppMain or as the first child of ContentIsland; do not put it in a short wrapper that limits its sticky travel. The shell exposes `--basalt-safe-top` and resolves `--basalt-sticky-top` to that inset for document scrolling, or zero for bounded panes. The sticky row offsets below the notch and paints the gap with a pseudo-element. Initial inset is still paid only once by the shell, not added again to header height. Do not add safe-area padding to the header. A bounded header outside the island is already stationary and does not need `sticky`.
409
+
410
+ **Single-header immersion is an explicit application layout.** On mobile, use one compact `AppHeader` with title, back/navigation and essential actions; omit the duplicated `PageHeader` (or put it in a `hidden md:block` wrapper). Keep the normal desktop PageHeader hierarchy for workspaces. Content section headings still belong in the document. Use existing DropdownMenu/Popover for secondary actions and 44px minimum touch targets; never invent reader-specific routing or actions in the library.
385
411
 
386
- The island wrap is the only extra layout div in the main column. Pages render **inside** `ContentIsland`. Pages do not set `h-screen`, side padding, or a second card around the island.
412
+ Copyable complete recipes: [mobile layouts](packages/basalt/ai/RECIPES.md#mobile-layouts). Live full-page examples: `/examples/reader`, `/examples/list-detail`, `/examples/workspace`. Desktop bounded panes, mobile root scrolling, form focus and modal restoration are separate checks. Automated WebKit checks CSS, scrolling and focus, **not physical iPhone Safari toolbar collapse or the software keyboard**; verify those manually on device.
387
413
 
388
414
  ---
389
415
 
@@ -391,48 +417,33 @@ The island wrap is the only extra layout div in the main column. Pages render **
391
417
 
392
418
  `Sidebar` **is** the column. It already has:
393
419
 
394
- - `h-screen`, `flex-col`, `shrink-0`, `sticky top-0`
420
+ - shell-derived bounded height (standalone `100dvh`), `flex-col`, `shrink-0`, sticky positioning at the shell-owned safe top
395
421
  - expanded width **260px** (inline `width`)
396
422
  - collapsed width **68px**
397
- - collapse animation `transition-all duration-300 ease-in-out`
423
+ - collapse animation `transition-[width] duration-basalt-normal ease-basalt` (disabled for reduced motion)
398
424
 
399
425
  Children of `Sidebar` are **regions only**: header, search, nav, footer. They fill that column. `SidebarNav` is `flex-1 min-h-0 overflow-y-auto`, so the footer stays at the bottom.
400
426
 
401
427
  Do not wrap those regions in another full-viewport column. Do not set `h-screen`, `w-[260px]`, or `w-[68px]` on an inner element. Width and height stay on `Sidebar`.
402
428
 
403
- ### Expanded tree
429
+ ### Shared navigation geometry
404
430
 
405
- ```
406
- Sidebar ← collapsed={false}; owns 260px and h-screen
407
- ├── SidebarHeader ← h-14 px-3 already
408
- │ └── brand row ← flex, items-center, justify-between, w-full
409
- │ (no extra horizontal padding)
410
- ├── search wrap ← px-3 pb-1 only
411
- │ └── SidebarSearch
412
- ├── SidebarNav ← pt-1; flex-1
413
- │ ├── SidebarPartition ← label; already px-6
414
- │ └── item stack ← flex flex-col gap-0.5 px-3
415
- │ └── SidebarItem ← already px-3 py-2.5
416
- └── SidebarFooter ← px-4 py-3 already
417
- └── SidebarUser
418
- ```
419
-
420
- `SidebarHeader` already pads horizontally. Brand, version pill, and collapse control go **directly** in it. The version string is read from the app `package.json` at build time. Never hardcode it.
431
+ `SidebarNav` and `NavigationMenuList` own one `--basalt-space-nav-inset` (8px) boundary and `--basalt-space-nav-gap` (2px) between rows. Sidebar groups do not add another horizontal inset. Top-level items, group items, editable/folder rows and horizontal navigation links reuse the existing row roles: 8px horizontal, 6px vertical, 8px icon gap, 22px line box and a 34px reference height. Header, search wrapper and footer share the same navigation inset. Do not add caller-owned `px-basalt-space-lg`, `pt-basalt-space-sm` or item-stack padding to compensate for missing package geometry.
421
432
 
422
- Nav labels use `SidebarPartition`. The item stack is **one** `px-3` column (the same gutter `SidebarGroup` uses). `SidebarItem` already has its own `px-3`. That is the whole horizontal rhythm: partition at 24px, item content at 24px.
433
+ Use `SidebarNav` for vertical lists, including standalone lists of `EditableNavItem` / `FolderNavItem`; `NavigationMenuList` owns horizontal/vertical Radix navigation. Both compose the existing list-local hover adapter. One background moves with transform between enabled rows, follows keyboard focus and returns to selection on pointer leave. Nested lists own separate highlights. Hidden, disabled and actively renamed rows are excluded. Geometry reads are batched per animation frame, same-row pointer motion causes no updates, and reduced motion disables transitions. Navigation state, routes and rename operations remain application-owned; the adapter is View-only.
423
434
 
424
- Collapsible sections use `SidebarGroup` instead of Partition + stack. `SidebarGroup` already includes the item gutter. Do not add another `px-3` around it.
435
+ `SidebarHeader` and `SidebarFooter` already pad horizontally. Brand, version pill, and collapse control go directly in the header. The version string is read from the app `package.json` at build time. Never hardcode it. `SidebarPartition` aligns with row text. `SidebarGroup` adds only section spacing, never a second item gutter.
425
436
 
426
437
  ```tsx excerpt:sidebar-expanded-structure
427
438
  <Sidebar collapsed={collapsed}>
428
439
  <SidebarHeader>
429
440
  <div className="flex w-full items-center justify-between">
430
- <div className="flex min-w-0 items-center gap-3">
441
+ <div className="flex min-w-0 items-center gap-basalt-space-lg">
431
442
  <BasaltMark className="h-5 w-5 shrink-0" />
432
443
  <span className="truncate text-lg font-semibold text-basalt-foreground md:text-xl">
433
444
  Acme
434
445
  </span>
435
- <span className="shrink-0 rounded-md bg-basalt-secondary px-1.5 py-0.5 text-[10px] leading-none font-medium text-basalt-muted-foreground">
446
+ <span className="shrink-0 rounded-md bg-basalt-secondary px-basalt-space-md py-basalt-space-xs text-[10px] leading-none font-medium text-basalt-muted-foreground">
436
447
  v{version}
437
448
  </span>
438
449
  </div>
@@ -447,14 +458,14 @@ Collapsible sections use `SidebarGroup` instead of Partition + stack. `SidebarGr
447
458
  </Button>
448
459
  </div>
449
460
  </SidebarHeader>
450
- <div className="px-3 pb-1">
461
+ <div className="px-basalt-nav-inset">
451
462
  <SidebarSearch onClick={() => setSearchOpen(true)}>Search</SidebarSearch>
452
463
  </div>
453
- <SidebarNav className="pt-1">
464
+ <SidebarNav>
454
465
  {groups.map((group) => (
455
466
  <div key={group.label}>
456
467
  <SidebarPartition>{group.label}</SidebarPartition>
457
- <div className="flex flex-col gap-0.5 px-3">
468
+ <div className="flex flex-col gap-basalt-nav-gap">
458
469
  {group.items.map((item) => (
459
470
  <SidebarItem
460
471
  key={item.href}
@@ -475,56 +486,48 @@ Collapsible sections use `SidebarGroup` instead of Partition + stack. `SidebarGr
475
486
  </Sidebar>
476
487
  ```
477
488
 
489
+ SidebarPartition marks a major section; SidebarGroup is a smaller uppercase, semibold disclosure label with additional group spacing. Group labels do not participate in the destination hover layer. Do not restyle them as ordinary SidebarItem rows or add per-page indentation repairs.
490
+
478
491
  ### Collapsed tree
479
492
 
480
493
  Pass `collapsed` on `Sidebar`. The element becomes 68px. Children are icon-sized and centered. Still no inner width/height wrapper.
481
494
 
482
- ```
483
- Sidebar ← collapsed; owns 68px
484
- ├── SidebarHeader ← justify-center px-0
485
- ├── collapse Button ← ghost icon, mb-1
486
- ├── search SidebarIconItem ← mb-2; tooltip
487
- ├── SidebarNav ← w-full items-center gap-1 pt-1
488
- │ └── SidebarIconItem ← h-10 w-10; tooltip
489
- └── SidebarFooter ← flex w-full justify-center px-0
490
- ```
495
+ The 68px rail uses `--basalt-size-rail`. Header and footer center their content automatically; SidebarIconItem uses the shared 34px control size. The navigation region centers its icon rows on that same rail axis without caller margin overrides. Render icon-only content with an accessible name and tooltip when collapsed; the library does not hide arbitrary application text. Keep a brand mark's expanded inset consistent with the collapsed axis instead of adding collapse-only offsets.
491
496
 
492
497
  ```tsx excerpt:sidebar-collapsed-structure
493
498
  <Sidebar collapsed={collapsed}>
494
- <SidebarHeader className="justify-center px-0">
499
+ <SidebarHeader>
495
500
  <BasaltMark className="h-5 w-5" />
496
501
  </SidebarHeader>
497
502
  <Button
498
503
  variant="ghost"
499
504
  size="icon"
500
- className="mb-1 self-center"
505
+ className="mb-basalt-space-sm self-center"
501
506
  onClick={onToggle}
502
507
  aria-label="Expand sidebar"
503
508
  >
504
509
  <PanelLeft aria-hidden="true" />
505
510
  </Button>
506
- <Tooltip delayDuration={0}>
507
- <TooltipTrigger asChild>
508
- <SidebarIconItem
509
- className="mb-2 self-center"
510
- onClick={() => setSearchOpen(true)}
511
- aria-label="Search (⌘K)"
512
- >
513
- <Search className="h-4 w-4" strokeWidth={1.5} aria-hidden="true" />
514
- </SidebarIconItem>
515
- </TooltipTrigger>
516
- <TooltipContent side="right" sideOffset={8}>
517
- Search (⌘K)
518
- </TooltipContent>
519
- </Tooltip>
520
- <SidebarNav className="w-full items-center gap-1 pt-1">
511
+ <SidebarNav>
512
+ <Tooltip delayDuration={0}>
513
+ <TooltipTrigger asChild>
514
+ <SidebarIconItem
515
+ onClick={() => setSearchOpen(true)}
516
+ aria-label="Search (⌘K)"
517
+ >
518
+ <Search className="h-4 w-4" strokeWidth={1.5} aria-hidden="true" />
519
+ </SidebarIconItem>
520
+ </TooltipTrigger>
521
+ <TooltipContent side="right" sideOffset={8}>
522
+ Search (⌘K)
523
+ </TooltipContent>
524
+ </Tooltip>
521
525
  {items.map((item) => (
522
526
  <Tooltip key={item.href} delayDuration={0}>
523
527
  <TooltipTrigger asChild>
524
528
  <SidebarIconItem
525
529
  active={active(item.href)}
526
530
  aria-label={item.label}
527
- className="self-center"
528
531
  onClick={() => navigate(item.href)}
529
532
  >
530
533
  <item.icon className="h-4 w-4" strokeWidth={1.5} />
@@ -536,7 +539,7 @@ Sidebar ← collapsed; owns 68px
536
539
  </Tooltip>
537
540
  ))}
538
541
  </SidebarNav>
539
- <SidebarFooter className="flex w-full justify-center px-0">
542
+ <SidebarFooter>
540
543
  <Tooltip delayDuration={0}>
541
544
  <TooltipTrigger asChild>
542
545
  <span className="inline-flex">{avatar}</span>
@@ -579,9 +582,11 @@ Breakpoint: `768px`. Below that, the in-flow rail is omitted. The same sidebar c
579
582
  </Sheet>
580
583
  ```
581
584
 
582
- `SheetContent` is already `h-full`. `Sidebar` still owns the 260px column inside it. Close the sheet on pathname change. While open, set `document.body.style.overflow = "hidden"` and clear it on close.
585
+ `SheetContent` bounds its Sidebar to the available height. Sidebar owns the 260px column and the portal safe area. Close the sheet on pathname change. Radix Sheet owns modal scroll locking and focus restoration; do not set body overflow separately.
586
+
587
+ Sheets slide from their declared edge in 350ms and return in 250ms, while the backdrop fades and blurs to 6px. DropdownMenu, Select, ContextMenu, MenuBar, Popover, HoverCard and Tooltip scale from their trigger origin and fade in/out in 200/150ms. Shared motion tokens live in the package stylesheet; both CSS entrypoints include them. Reduced motion disables these animations. Keep content mounted through Radix's closing state; do not conditionally remove the root before its exit completes.
583
588
 
584
- `AppHeader` `leading` is the menu button on mobile only (`Button variant="ghost" size="icon" className="h-8 w-8"`).
589
+ `AppHeader` `leading` is the menu button on mobile only. Use `density="compact"` with `Button variant="ghost" size="icon"`; compact header controls have a 44px minimum target.
585
590
 
586
591
  Header actions use a stable hand cursor and compact 12px `TooltipContent`. `Button`, `LinkButton`, `Toggle`, and `ToggleGroupItem` keep their full rectangular pointer target with a transparent `::before`, including rounded corners. Wrap the actual button or link in `TooltipTrigger asChild` so the trigger retains one focusable hit target. `Toggle` and `ToggleGroupItem` selection styling follows `aria-pressed` / `aria-checked`, independently of the tooltip's `data-state`. Icon-only links must include a screen-reader label inside the link as well as an accessible name.
587
592
 
@@ -621,12 +626,6 @@ export function AppFrame() {
621
626
  setMobileOpen(false);
622
627
  }, [location.pathname]);
623
628
 
624
- useEffect(() => {
625
- document.body.style.overflow = mobileOpen ? "hidden" : "";
626
- return () => {
627
- document.body.style.overflow = "";
628
- };
629
- }, [mobileOpen]);
630
629
 
631
630
  return (
632
631
  <AppShell>
@@ -663,7 +662,7 @@ export function AppFrame() {
663
662
  title={title}
664
663
  actions={<ThemeToggle aria-label={`Toggle theme (now ${theme})`} />}
665
664
  />
666
- <div className="flex min-h-0 flex-1 flex-col px-2 pb-2 md:px-3 md:pb-3">
665
+ <div className="flex min-h-0 flex-1 flex-col px-basalt-space-lg pb-basalt-space-lg md:px-basalt-space-lg md:pb-basalt-space-lg">
667
666
  <ContentIsland>
668
667
  <Outlet />
669
668
  </ContentIsland>
@@ -676,14 +675,14 @@ export function AppFrame() {
676
675
 
677
676
  <a id="appheader-and-breadcrumbs"></a>
678
677
 
679
- `AppHeader` is `h-14`, matching `SidebarHeader`.
678
+ `AppHeader` defaults to `h-14`, matching `SidebarHeader`; `density="compact"` uses a 52px row with 44px minimum targets. `sticky` keeps it visible within the current scroll owner; see §6 for safe-area positioning.
680
679
 
681
680
  ### AppHeader API & Contracts
682
681
 
683
682
  `AppHeader` inherits root `header` attributes (`HTMLAttributes<HTMLElement>`).
684
683
 
685
684
  - **`title?: string`**: Current page title rendered inside an `h1` (`truncate text-sm font-normal text-basalt-foreground`). (Source type intersection is `HTMLAttributes<HTMLElement> & { title?: ReactNode }`, which narrows valid `title` values to `string | undefined`). Do not also put the current page title in `breadcrumbs`.
686
- - **`breadcrumbs?: { href?: string; label: ReactNode }[]`**: Ancestor breadcrumb hierarchy displayed before the current page title (ancestors only). Separated by ChevronRight.
685
+ - **`breadcrumbs?: { href?: string; label: ReactNode }[]`**: Breadcrumb hierarchy separated by ChevronRight. Supply ancestors with `title`, or a complete path ending in a nonlinked current item and omit `title`. No trailing separator is rendered without a title.
687
686
  - **`leading?: ReactNode`**: Optional slot before breadcrumbs, typically the mobile hamburger drawer trigger.
688
687
  - **`actions?: ReactNode`**: Top-right framework controls (`ThemeToggle`, account dropdown, etc.). This slot is reserved for framework controls, **not** the page-level create button.
689
688
  - **Native & Ref Boundary**: Inherits native `HTMLAttributes<HTMLElement>`, but does not forward ref.
@@ -696,56 +695,29 @@ Product title and version live in `SidebarHeader`, not in `AppHeader`. Read the
696
695
 
697
696
  Login is **not** inside `AppShell`. It is a centered badge on the full viewport.
698
697
 
699
- Shape: ISO ID card, `aspect-[54/86]`, `w-72`, `rounded-2xl`, `bg-basalt-card`, layered shadow, hairline ring. Primary strip on top, mark and actions in the body, status strip pinned to the bottom.
698
+ Shape: a fixed badge width, `rounded-basalt-lg`, `bg-basalt-card` and a hairline ring. Three bands: the primary strip on top, the body holding the mark and actions, and the status footer at the bottom. The body grows with its content, so a sign-in form fits without a fixed aspect ratio.
700
699
 
701
700
  ```tsx excerpt:login-badge-page
702
701
  export function LoginPage() {
703
702
  return (
704
- <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-basalt-background p-4">
705
- <div className="flex flex-col items-center">
706
- <div
707
- data-basalt-surface-root=""
708
- className="relative flex aspect-[54/86] w-72 flex-col overflow-hidden rounded-2xl bg-basalt-card ring-1 ring-black/[0.08] dark:ring-white/[0.06]"
709
- style={{
710
- boxShadow: [
711
- "0 1px 2px rgba(0,0,0,0.06)",
712
- "0 4px 8px rgba(0,0,0,0.04)",
713
- "0 12px 24px rgba(0,0,0,0.06)",
714
- "0 24px 48px rgba(0,0,0,0.04)",
715
- "0 0 0 0.5px rgba(0,0,0,0.02)",
716
- "0 0 60px rgba(0,0,0,0.03)",
717
- ].join(", "),
718
- }}
719
- >
720
- <div className="bg-basalt-primary px-5 py-4">
721
- <div className="flex items-center justify-between">
722
- <div className="h-4 w-8 rounded-full bg-basalt-background/80" />
723
- <div className="flex items-center gap-2">
724
- <BasaltMark className="h-4 w-4 text-basalt-primary-foreground" />
725
- <span className="text-sm font-semibold text-basalt-primary-foreground">
726
- Acme
727
- </span>
728
- </div>
729
- <span className="text-[10px] font-medium tracking-widest text-basalt-primary-foreground/60 uppercase">
730
- Visitor
731
- </span>
732
- </div>
733
- </div>
734
- <div className="flex flex-1 flex-col items-center px-6 pt-6 pb-14">
735
- <div className="flex h-24 w-24 items-center justify-center overflow-hidden rounded-full bg-basalt-secondary p-2.5 ring-1 ring-basalt-border">
736
- <BasaltMark className="h-10 w-10 text-basalt-muted-foreground" />
737
- </div>
738
- <p className="mt-5 text-lg font-semibold text-basalt-foreground">Welcome</p>
739
- <p className="mt-1 text-xs text-basalt-muted-foreground">Sign in to continue</p>
740
- <div className="mt-5 h-px w-full bg-basalt-border" />
741
- <div className="flex-1" />
742
- <Button variant="secondary" className="w-full rounded-xl py-3">
743
- Continue
744
- </Button>
745
- </div>
746
- <div className="absolute right-0 bottom-0 left-0 flex items-center justify-center border-t border-basalt-border bg-basalt-secondary/50 py-2.5">
747
- <span className="text-[10px] text-basalt-muted-foreground">Secure sign-in</span>
748
- </div>
703
+ <div className="basalt-ui relative flex min-h-dvh items-center justify-center bg-basalt-background p-basalt-space-lg">
704
+ <div
705
+ data-basalt-surface-root=""
706
+ className="flex w-72 max-w-full flex-col overflow-hidden rounded-basalt-lg bg-basalt-card ring-1 ring-basalt-border"
707
+ >
708
+ <div className="flex items-center justify-between gap-basalt-space-lg bg-basalt-primary px-basalt-space-lg py-basalt-space-lg">
709
+ <span className="text-basalt-base font-semibold text-basalt-primary-foreground">Acme</span>
710
+ <span className="text-basalt-xs font-medium uppercase tracking-widest text-basalt-primary-foreground/80">
711
+ Visitor
712
+ </span>
713
+ </div>
714
+ <div className="flex flex-1 flex-col items-center gap-basalt-space-lg px-basalt-space-lg py-basalt-space-lg">
715
+ <p className="text-basalt-xl font-semibold text-basalt-foreground">Welcome</p>
716
+ <p className="text-basalt-sm text-basalt-muted-foreground">Sign in to continue</p>
717
+ <Button className="w-full">Continue</Button>
718
+ </div>
719
+ <div className="border-t border-basalt-border px-basalt-space-lg py-basalt-space-lg">
720
+ <p className="text-basalt-xs text-basalt-muted-foreground">Secure sign-in</p>
749
721
  </div>
750
722
  </div>
751
723
  </div>
@@ -777,15 +749,17 @@ Boot and route gates use `LoadingScreen` — a centered mark and a 6rem shimmer
777
749
 
778
750
  ## 13. First page
779
751
 
780
- When skip link, rail (260 / 68, 300ms), header `h-14`, and island are in place, add routes as `Outlet` pages. Application pages inside the island start with `PageHeader`. The shell file does not grow with page UI.
752
+ When skip link, rail (260 / 68, 300ms), header `h-14`, and island are in place, add routes as `Outlet` pages. Regular application pages inside the island start with `PageHeader`. Mobile immersive layouts may use a single `AppHeader` instead, with `PageHeader` shown only on desktop; see the root geometry contract. The shell file does not grow with page UI.
781
753
 
782
- Standalone login, loading, error, and landing pages use their own first-screen structure: `/login` preserves the visitor-badge composition, `/loading` is a named loading status, and `/404` and `/static-page` have independent headings. Library reference pages use their document heading and section navigation. These are deliberate layout exceptions, not alternate application-page templates.
754
+ Standalone login, loading, error, and landing pages use their own first-screen structure: `/login` preserves the visitor-badge composition, `/loading` is a named loading status, and `/404` and `/static-page` have independent headings. These are deliberate layout exceptions, not alternate application-page templates. Library reference pages, category overviews and source viewers are normal island pages: they use the same heading template, with document sections below it.
783
755
 
784
- The showcase site's library index and dashboard examples use the app-local `ShowcaseHeader` with the landing page's paired day/night mountain imagery fading into the content island. Headings and actions align with the page's content grid; the landscape reaches the island's top edge and scrolls with it. The library has a more spacious opening, dashboards use the compact variant, and reference headings carry a quieter version. The original logo remains in the navigation. This site-specific treatment does not change the package's `PageHeader` or application layout recipe.
756
+ The showcase's routes under `DashboardLayout` use the app-local `ShowcasePage`: one PageHeader and a shared section gap, with no repeated island padding, painted shell or nested main/viewport. Every catalog route keeps that shared scale: 30px titles and 24px page and document-section separation, whether it is an application page, a library index, a category guide, documentation or a source viewer. Catalog pages do not override the shared scale or its document rhythm; `PageHeader` keeps its `size` choices for consumers, where dashboards select `size="lg"` for 30px titles and ordinary application pages keep the `md` default. Header height follows content, never a scenic banner or negative inset. The original logo remains in navigation; mountain artwork belongs only to the independent landing. See [DESIGN.md](DESIGN.md#page-composition).
757
+
758
+ The catalog's app-local `RouteScrollReset` wraps each leaf page inside its Suspense boundary. Once the route content is committed, a changed pathname resets ContentIsland and document scrolling immediately, including Back/Forward navigation. Valid URL fragments instead use the chapter anchor with the measured sticky-directory inset. Same-page query changes and local controls preserve scroll. The shared sidebar is not remounted or scrolled. Routing policy belongs to the app, never to the Basalt package.
785
759
 
786
760
  The `/forms`, `/settings`, `/data`, and `/chat` examples demonstrate local state and simulated requests, including failure/retry and cancellation. Their viewmodels own data and timers; Views own native FormData, focus, and responsive layout. Replace the local service adapter when integrating a backend. Theme selection uses the shared provider; local profile, uploads, and chat changes do not update a real account.
787
761
 
788
- `PageHeader` is flush on `ContentIsland`. Do not wrap it in another card. Do not put an icon row above the heading.
762
+ `PageHeader` is flush on `ContentIsland`. The island owns 16px mobile insets, 24px tablet insets, and 32px horizontal / 24px vertical desktop insets. Do not wrap the header in another card or add page padding. `PageHeader.size` is `"md" | "lg" | "xl"` (default `"md"`); larger headings use 16/24px descriptions, all title/description pairs use an 8px gap. `SectionRule variant="heading"` gives reading sections a 20px sentence-case title without the compact dashed rule. Controls and card slots keep their compact geometry.
789
763
 
790
764
  ### Create and filters
791
765
 
@@ -806,7 +780,7 @@ import { PageHeader } from "@nocoo/basalt/components/page-header";
806
780
 
807
781
  export default function ProjectsPage() {
808
782
  return (
809
- <div className="space-y-8">
783
+ <div className="space-y-basalt-layout-lg">
810
784
  <PageHeader
811
785
  title="Projects"
812
786
  description="Active work in this workspace."
@@ -836,7 +810,7 @@ import { SectionRule } from "@nocoo/basalt/components/section-rule";
836
810
 
837
811
  export default function ProjectsPage() {
838
812
  return (
839
- <div className="space-y-8">
813
+ <div className="space-y-basalt-layout-lg">
840
814
  <PageHeader
841
815
  title="Projects"
842
816
  description="Active work in this workspace."
@@ -855,7 +829,7 @@ export default function ProjectsPage() {
855
829
  }
856
830
  />
857
831
  <SectionRule title="Overview" hint="Live totals for the current workspace.">
858
- <div className="grid grid-cols-2 gap-4">
832
+ <div className="grid grid-cols-2 gap-basalt-layout">
859
833
  <LayerCard>
860
834
  <p className="text-xs text-basalt-muted-foreground">Projects</p>
861
835
  <p className="text-2xl font-semibold">24</p>
@@ -954,7 +928,9 @@ Place cards with Grid or flex. Those classes do not paint. A grid of `LayerCard`
954
928
 
955
929
  Input, Select, secondary / outline Button use `bg-basalt-control` (`--basalt-control-fill` from the current surface) plus border. Do not override with `bg-card`, `bg-basalt-card`, or `bg-basalt-background`.
956
930
 
957
- `Table` sets `data-basalt-table`. Zebra, hover, and selected paint on `td`, from `--basalt-zebra-fill` / `--basalt-control-fill`.
931
+ `Table` sets `data-basalt-table`. Zebra, hover, and selected paint on `td`, from `--basalt-zebra-fill`, `--basalt-hover` and `--basalt-selected`. Selection adds no decorative side bar or frame; semantic add/remove diff tints are preserved.
932
+
933
+ Neutral selected states use the brighter `--basalt-selected` surface, readable `--basalt-selected-foreground` and navigation font weight, without decorative side bars or selected borders. Never use `accent` or `muted` to signal selection. Existing semantic checkmarks and tab underlines remain; do not add a decorative frame at the white ceiling. A moving hover layer must not erase the selected row. Sidebar, navigation menus, editable navigation and ChatInbox share this behavior and inset keyboard focus; neutral Button variants also honor `aria-pressed` and `aria-current`. Segments and tabs retain their existing measured indicator motion. Primary/destructive actions, check/radio/switch glyphs and semantic diffs keep their semantic colors. See [DESIGN.md](DESIGN.md#selection-and-navigation-hierarchy).
958
934
 
959
935
  `DescriptionList` is layout and type only. It is not a surface.
960
936
 
@@ -984,7 +960,7 @@ import { LayerCard } from "@nocoo/basalt/components/layer-card";
984
960
 
985
961
  ## 15. PageHeader and SectionRule
986
962
 
987
- `PageHeader` and `SectionRule` are the only page-level chrome inside the island. Import them from granular paths. They are not on the root barrel. Do not wrap the title in an island.
963
+ `PageHeader` and `SectionRule` provide regular page-level chrome inside the island. A mobile immersive composition may use a single sticky AppHeader inside the island instead (§6). Import them from granular paths. They are not on the root barrel. Do not wrap the title in an island.
988
964
 
989
965
  ### PageHeader
990
966
 
@@ -1072,7 +1048,7 @@ export function ProfileForm() {
1072
1048
  <Switch name="notifications" defaultChecked />
1073
1049
  </Field>
1074
1050
 
1075
- <div className="flex gap-2 mt-4">
1051
+ <div className="flex gap-basalt-space-lg mt-basalt-space-lg">
1076
1052
  <Button type="reset" variant="secondary">Reset</Button>
1077
1053
  <Button type="submit" variant="default">Save Changes</Button>
1078
1054
  </div>
@@ -1161,7 +1137,7 @@ export function EmptyActionExample() {
1161
1137
  const [created, setCreated] = useState(false);
1162
1138
 
1163
1139
  return (
1164
- <LayerCard className="max-w-md p-6">
1140
+ <LayerCard className="max-w-md">
1165
1141
  {created ? (
1166
1142
  <p className="text-sm text-basalt-foreground">Project created successfully.</p>
1167
1143
  ) : (
@@ -1227,7 +1203,7 @@ export function ProjectSettingsForm() {
1227
1203
  };
1228
1204
 
1229
1205
  return (
1230
- <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
1206
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-basalt-space-lg">
1231
1207
  <Field
1232
1208
  label="Project Name"
1233
1209
  hint="Unique name for your project workspace"
@@ -1265,12 +1241,12 @@ export function ProjectSettingsForm() {
1265
1241
  />
1266
1242
 
1267
1243
  {submittedData && (
1268
- <div data-testid="submission-output" className="text-sm font-mono p-2 border rounded">
1244
+ <div data-testid="submission-output" className="text-sm font-mono p-basalt-space-lg border rounded">
1269
1245
  Submitted: {JSON.stringify(submittedData)}
1270
1246
  </div>
1271
1247
  )}
1272
1248
 
1273
- <div className="flex gap-2">
1249
+ <div className="flex gap-basalt-space-lg">
1274
1250
  <Button
1275
1251
  type="button"
1276
1252
  variant="secondary"
@@ -1469,6 +1445,7 @@ Basalt charts are composed of responsive frame, legend, and tooltip subsystem pr
1469
1445
  ### StatCard & StatGrid (`@nocoo/basalt/charts/stat-card`)
1470
1446
 
1471
1447
  - **`StatCard`**: High-level KPI and telemetry presentation card supporting incremental interactive slots and status state transitions.
1448
+ - **Composition**: One `LayerCard` surface with shared card insets, an unboxed title icon, tabular display value, comparison text and supporting content in that order. It has no header divider or fixed content height. Long labels and values wrap; actions remain accessible. Use a compact `LineChart`/`BarChart` child with `className="h-basalt-16 w-full"` and a visible time-window caption for trend cards. Keep bar baselines at zero and disclose nonzero line scales. Do not stretch plots with `flex-1`, repeat card padding or recreate the header in application CSS.
1472
1449
  - **Props (`StatCardProps`)**:
1473
1450
  - `value: string | number` (required): Core metric value. Formatted via `toLocaleString()` if numeric. Overridden when `status` is provided.
1474
1451
  - `title?: string` / `label?: string` (optional): Heading text (`title` takes precedence over `label`).
@@ -1495,6 +1472,8 @@ Basalt charts are composed of responsive frame, legend, and tooltip subsystem pr
1495
1472
 
1496
1473
  Basalt cartesian charts (`LineChart`, `AreaChart`, `BarChart`, `StackedBarChart`, `GroupedBarChart`, `Charts`, `Timeseries`, `CustomChart`, `Sparkline`, and `SlotBarChart`) support arbitrary, type-safe data keys beyond the legacy `y`, `y2`, and `y3` series convention:
1497
1474
 
1475
+ `Sparkline` renders compact, rounded bars with alternating full/muted intensity (decorative, not another data dimension). A single series defaults to chart green; explicit series colors and multi-series palette order are preserved. Its default plot is 112 by 20px, without axes, legends, tooltips or entrance animation. Null values remain gaps and negative values retain their baseline. Pair it with `Meter` for table cells: Meter uses 17 slots, redistributes red/yellow/green across the filled portion, and shows the numeric value inline. Colors do not imply domain-specific risk thresholds. Values clamp to 0..100 and non-finite readings announce unavailable. `BatteryMeter` remains the separate device-charge control.
1476
+
1498
1477
  - **Generic Data Binding**:
1499
1478
  - `data: TData[]`: Generic record where `TData extends { x: string | number }`. Caller datasets can contain any number of arbitrary metric keys (e.g., `edgeCache`, `p95US`, `workersCPU`), nullable numbers (`number | null`), and optional numeric properties (`number | undefined`).
1500
1479
  - `series?: Array<ChartSeriesDescriptor<K>>`: Strongly-typed array of series descriptors. Series keys `K` are automatically constrained to `LineChartNumericKeys<TData>` (excluding coordinate `x` and rejecting non-numeric fields such as strings, booleans, or typos). Arbitrary keys require explicit series; omitted or empty series retain each chart's legacy `y`, `y2`, and `y3` series fallback. Note that TypeScript type inference does not automatically enumerate object keys at runtime.
@@ -1527,7 +1506,7 @@ export function StandaloneViteApp() {
1527
1506
 
1528
1507
  return (
1529
1508
  <ThemeProvider>
1530
- <div className="min-h-screen bg-basalt-background text-basalt-foreground p-6 space-y-6">
1509
+ <div className="min-h-screen bg-basalt-background text-basalt-foreground p-basalt-space-lg space-y-basalt-space-lg">
1531
1510
  <PageHeader
1532
1511
  title="Standalone Workspace"
1533
1512
  description="Rendered with @nocoo/basalt/styles/standalone without Tailwind preflight"
@@ -1538,7 +1517,7 @@ export function StandaloneViteApp() {
1538
1517
  <span className="font-semibold text-basalt-foreground">Project Search</span>
1539
1518
  </LayerCard.Header>
1540
1519
  <LayerCard.Body>
1541
- <div className="flex gap-3 max-w-md">
1520
+ <div className="flex gap-basalt-space-lg max-w-md">
1542
1521
  <Input
1543
1522
  value={query}
1544
1523
  onChange={(e) => setQuery(e.target.value)}
@@ -1615,7 +1594,7 @@ export default function NextClientDashboardPage() {
1615
1594
 
1616
1595
  return (
1617
1596
  <ThemeProvider>
1618
- <div className="p-6 space-y-4">
1597
+ <div className="p-basalt-space-lg space-y-basalt-space-lg">
1619
1598
  <div className="flex justify-between items-center">
1620
1599
  <h2 className="text-xl font-bold text-basalt-foreground">Next.js Client Dashboard</h2>
1621
1600
  <ThemeToggle aria-label="Toggle theme mode" />
@@ -1628,7 +1607,7 @@ export default function NextClientDashboardPage() {
1628
1607
  onChange={(e) => setMetricName(e.target.value)}
1629
1608
  aria-label="Metric Name"
1630
1609
  />
1631
- <div className="mt-4">
1610
+ <div className="mt-basalt-space-lg">
1632
1611
  <Button variant="outline" onClick={() => setMetricName("Default Metric")}>
1633
1612
  Reset Metric
1634
1613
  </Button>
@@ -1688,12 +1667,12 @@ export function RoutedNavigationSection() {
1688
1667
  return (
1689
1668
  <MemoryRouter initialEntries={["/dashboard"]}>
1690
1669
  <LinkProvider render={ReactRouterLinkAdapter}>
1691
- <nav className="flex gap-4 p-4 border-b border-basalt-border">
1670
+ <nav className="flex gap-basalt-space-lg p-basalt-space-lg border-b border-basalt-border">
1692
1671
  <NavigationItem href="/dashboard">Dashboard</NavigationItem>
1693
1672
  <NavigationItem href="/settings">Settings</NavigationItem>
1694
1673
  <NavigationItem href="https://docs.hexly.ai">External Docs</NavigationItem>
1695
1674
  </nav>
1696
- <div className="p-4">
1675
+ <div className="p-basalt-space-lg">
1697
1676
  <Routes>
1698
1677
  <Route path="/dashboard" element={<DashboardPage />} />
1699
1678
  <Route path="/settings" element={<SettingsPage />} />
@@ -1749,7 +1728,7 @@ export function UserPreferencesForm() {
1749
1728
  };
1750
1729
 
1751
1730
  return (
1752
- <form onSubmit={handleSubmit} onReset={handleReset} className="space-y-4 max-w-sm">
1731
+ <form onSubmit={handleSubmit} onReset={handleReset} className="space-y-basalt-space-lg max-w-sm">
1753
1732
  <Field label="Username" hint="Public profile handle">
1754
1733
  <Input name="username" defaultValue="johndoe" required />
1755
1734
  </Field>
@@ -1764,7 +1743,7 @@ export function UserPreferencesForm() {
1764
1743
  </div>
1765
1744
  )}
1766
1745
 
1767
- <div className="flex gap-2">
1746
+ <div className="flex gap-basalt-space-lg">
1768
1747
  <Button type="reset" variant="secondary">
1769
1748
  Reset
1770
1749
  </Button>
@@ -1776,3 +1755,111 @@ export function UserPreferencesForm() {
1776
1755
  );
1777
1756
  }
1778
1757
  ```
1758
+
1759
+ ### Confirmation focus after responsive replacement
1760
+
1761
+ `ConfirmDialog.onCloseAutoFocus` receives the same cancelable event as `AlertDialogContent.onCloseAutoFocus`. It runs after the close focus trap releases. Call `event.preventDefault()` and focus the currently mounted action when a responsive layout replaced the original opener. Without prevention, ConfirmDialog restores the connected explicit trigger or captured opener as usual. Do not race that lifecycle with a close-state effect or timeout.
1762
+
1763
+ ```tsx excerpt:confirmation-focus-replacement
1764
+ <ConfirmDialog
1765
+ open={confirmOpen}
1766
+ onOpenChange={setConfirmOpen}
1767
+ title="Remove item?"
1768
+ description="This action cannot be undone."
1769
+ onConfirm={removeItem}
1770
+ onCloseAutoFocus={(event) => {
1771
+ event.preventDefault();
1772
+ currentActionRef.current?.focus();
1773
+ }}
1774
+ />
1775
+ ```
1776
+
1777
+ ## Agent feedback
1778
+
1779
+ `Thinking`, `ApprovalCard`, and `ToolChips` are granular component imports under `@nocoo/basalt/components/`. They use Basalt tokens, existing disclosures and controls, and reduced-motion-safe entrance/expand animations. The package's `models/agent-feedback.ts` defines data semantics; private `viewmodels/` own UI state, timers, answer validation and submission locking. Views import the ViewModels; ViewModels never import Views or DOM modules. Private model/ViewModel paths are not public package exports.
1780
+
1781
+ - `Thinking` takes stable step IDs, caller-owned statuses (`pending`, `running`, `complete`, `error`), labels and optional plain-text details. `variant` is `steps`, `reasoning`, `search` or `coding`. Disclosure can be controlled or use `defaultOpen`. It never fabricates progress or exposes hidden model reasoning; display only trace data that the application intentionally supplies.
1782
+ - `ApprovalCard` takes questions with stable question/option IDs, `single`/`multiple` choice, optional custom text and `required` (true by default). Single choice may move forward after 240ms; the last question always requires explicit Submit. Optional questions may be skipped. `onSubmit` receives a sanitized answer snapshot and may return a promise; pending submission locks controls, failure keeps answers for retry, and no work is executed by the library. Remount with a new React key when replacing an approval request. Backend authorization and validation remain mandatory.
1783
+ - `ToolChips` takes caller-owned tool steps and optional file diffs. It summarizes completion, expands individual plain-text outputs, and opens file previews in accessible popovers. It does not execute commands or parse arbitrary HTML.
1784
+ - Catalog examples simulate progress in application ViewModels. Their copyable snippets include those demo ViewModels, rather than importing private catalog paths.
1785
+
1786
+ ### Dense controls and moving list highlights
1787
+
1788
+ Button (including icon buttons), Input, SelectTrigger, InputGroup and their composing controls default to 34px high. Explicit `sm`/`lg` sizes remain available; InputArea remains multiline. Compact application headers retain their 44px minimum touch targets. ApprovalCard uses 16px horizontal / 12px vertical panel padding and compact option spacing; ToolChips uses 14px row text and 16px glyphs with 34px reference rows.
1789
+
1790
+ Select, DropdownMenu, ContextMenuPanel, MenuBarContent, Combobox/Autocomplete, MultiSelect, ApprovalCard and ToolChips use one list-local hover layer. Its private DOM adapter batches reads in requestAnimationFrame, measures only when row/focus/selection/size changes, and updates CSS variables without a React state render. Position animates with transform, not top/left layout; reduced motion removes transitions. Disabled/hidden rows are excluded, keyboard focus takes priority, and observers/listeners are released with the mounted list. CommandPalette's list shrinks with its viewport so all rows remain reachable even inside short frames.
1791
+
1792
+ Loader uses eight perimeter cells with an empty center, moving clockwise continuously. Label, shimmer and delayed elapsed-time controls are unchanged.
1793
+
1794
+ ## Dimension tokens
1795
+
1796
+ Basalt geometry no longer depends on a host's Tailwind `--spacing`. Both CSS entrypoints expose the same `--basalt-space-*` and `--basalt-size-*` variables. Override semantic roles on a shared ancestor; portaled overlays inherit from their portal ancestor, so application-wide settings belong on `:root`. Theme switches do not reset dimensions.
1797
+
1798
+ | Role | Token | Default |
1799
+ | --- | --- | --- |
1800
+ | Small / default / large control | `--basalt-size-control-sm`, `--basalt-size-control`, `--basalt-size-control-lg` | 28 / 34 / 40px |
1801
+ | Compact / standard / display icon | `--basalt-size-icon-sm`, `--basalt-size-icon`, `--basalt-size-icon-lg` | 12 / 14 / 16px |
1802
+ | Nested action | `--basalt-size-control-inset` | 28px |
1803
+ | Minimum touch target | `--basalt-size-touch` | 44px |
1804
+ | Menu option | `--basalt-size-menu-row`, `--basalt-space-menu-inset`, `--basalt-space-menu-x`, `--basalt-space-menu-y` | 34px / 8px / 8px / 6px |
1805
+ | Card padding | `--basalt-space-card-sm`, `--basalt-space-card`, `--basalt-space-card-lg`, `--basalt-space-card-xl` | 12 / 16 / 24 / 32px; default 16px |
1806
+ | Layout spacing | `--basalt-space-layout-sm`, `--basalt-space-layout`, `--basalt-space-layout-lg`, `--basalt-space-layout-xl` | 12 / 16 / 24 / 32px; grids 16px, page sections 24px |
1807
+ | Field label gap | `--basalt-space-field-gap` | 8px |
1808
+ | Textarea minimum height | `--basalt-size-textarea-sm`, `--basalt-size-textarea`, `--basalt-size-textarea-lg` | 64 / 80 / 96px |
1809
+ | Table row / horizontal / vertical inset | `--basalt-size-table-row`, `--basalt-space-table-x`, `--basalt-space-table-y` | 36 / 8 / 6px |
1810
+
1811
+ Defaults use rem and unitless line-height, preserving browser font-size preferences. [DESIGN.md](DESIGN.md) defines compact 2/4/6/8px control spacing, separate 12/16/24/32px card and layout spacing, and natural 22/34/46px inline/action/banner categories. The size tokens describe reference geometry, not fixed text-control heights. Use the semantic spacing role for the boundary being laid out; numbered geometry tokens are not padding alternatives. LayerCard accepts `padding="sm|md|lg|xl"` for unstructured content; Grid accepts `gap="sm|md|lg|xl"`. Both default to `md`; 32px `xl` is opt-in. ContentIsland owns a 16px mobile, 24px tablet, and 32px horizontal / 24px vertical desktop inset. Examples must use component size props rather than fixed-height overrides. Textareas, avatars and panels retain content-specific geometry.
1812
+
1813
+ ### Compact record tables
1814
+
1815
+ DataTable uses 13px single-line content, 36px minimum rows, tokenized horizontal/vertical insets and a native horizontally scrollable region. A column's `width` is also its minimum width; the browser must not squeeze names/dates into accidental multi-line rows. Use `column.wrap: true` for actual prose fields, or an explicitly sized inner `truncate` element with a title for long record names and URLs. `maxHeight` bounds the existing scroll region, with sticky opaque headers that stay inside it. Sorting uses a fixed 12px icon slot and retains `aria-sort`; row selection uses the shared Checkbox rather than an unstyled native checkbox. Rich `headerContent` composes icons and labels without changing the plain accessible sort name. Empty/loading/error content remains readable and wraps independently of data-cell defaults.
1816
+
1817
+ ### Recommendations
1818
+
1819
+ `RecommendationCard` (`components/recommendation-card`) accepts stable-ID options with a label, plain-text description, confidence (`high`, `review`, `none`) and optional action label/disabled state. Alternatives expand with the existing disclosure and shared hover layer; choosing one changes the recommendation, never executes it. `onAccept` runs only after the explicit action. Promise rejection preserves the selection and shows retry feedback; pending acceptance locks controls and successful acceptance cannot be repeated for the current option. Remount with a new key for a new recommendation request. The application must perform authorization and persist any real action. Private model/ViewModel modules own confidence metadata and state, without View or DOM imports.
1820
+
1821
+ ### Retrieved context
1822
+
1823
+ `ContextCards` (`components/context-cards`) displays caller-owned chunks with stable IDs, title, text, character count and source metadata. The optional total count cannot be smaller than the displayed count; omitted character counts are computed from Unicode code points. Sources without a destination remain non-interactive labels. Only clean root-relative or HTTP(S) URLs without credentials become external links; other schemes and ambiguous URLs are shown as labels, never executed. Loading, empty and retryable error states are explicit. Entrance staggering is CSS-only, capped at 240ms and disabled for reduced motion; no timer delays access to the source links. Source retrieval and authorization belong to the app.
1824
+
1825
+ ### Proposed tabular changes
1826
+
1827
+ `DiffTable` (`components/diff-table`) renders caller-owned text/number rows marked `add`, `remove` or `unchanged`. Changed rows start included and can be toggled by clicking the row or its keyboard-operable Checkbox. Add/remove labels and glyphs carry the meaning independently of tint. `onApply` receives a detached snapshot of only selected changed rows; no data is mutated internally. Pending actions lock selection and duplicate submission; rejection preserves selection with retry feedback. Remount with a new key for a new proposal. Real writes, access checks and validation stay in the application.
1828
+
1829
+ ### Record numbering and identity
1830
+
1831
+ Set `DataTable.rowNumbers` to show an additional `#` column. Numbers follow current sorted/filtered display order and continue from the resolved page offset, including server pagination. They never replace stable row IDs or selection keys. `AvatarInitials` on `components/avatar` reuses the circular Avatar surface with deterministic semantic colors and the first two word initials; `colorKey` keeps color stable across renames and `initials` supports application abbreviations. `size="sm"` is 24px for compact records. Keep the full name as its accessible label.
1832
+
1833
+ ## AI conversation primitives
1834
+
1835
+ `ChatComposer` now uses the his.fm native `field-sizing: content` pattern: one line initially, a maximum of five line-heights plus token padding, then internal vertical scrolling. No per-keystroke JS height measurement is used. Enter sends, Shift+Enter inserts a newline, and IME composition (including keyCode 229) never submits. Controlled `value`/`onValueChange` keep thread-specific drafts application-owned; async `onSend` errors preserve the draft. Attachment metadata and file selection callbacks never upload files themselves. `PromptBar` composes the same composer with application-owned model, reasoning and web-search controls. Stop remains an explicit callback; no microphone permission is requested.
1836
+
1837
+ `ChatMessage` provides user/assistant layout, trace/result slots, copy, feedback, edit and regenerate actions. `ChatMarkdown` uses the maintained `marked` lexer but renders React nodes, never raw HTML. It supports prose, lists, code, GFM tables and validated links. HTML is plain text; Markdown images become links rather than making unprompted remote requests. Business conversations, persistence, generation and approvals stay outside these views. The `marked` dependency is imported only by granular chat-content entrypoints, not the root barrel. Entity references are decoded with `entities` before link validation; code and raw HTML remain literal. `ChatMarkdown.streaming` reveals newly appended prose words with a short CSS blur/opacity animation. Existing words keep stable keys, no per-word timers are allocated, and reduced motion removes the effect. Copy confirmations belong to the exact copied content, so updated messages and code do not retain a stale success state.
1838
+
1839
+ ### Conversation workspace example
1840
+
1841
+ The catalog `/chat` route composes these public controls in a session-only workspace. `src/models/chat.ts` owns the pure conversation reducer and deterministic response fixtures; `useChatViewModel` owns the cancellable simulation clock. Views own only DOM scrolling, focus and overlay presentation. Threads keep independent drafts, models, reasoning/search settings and attachment names. Switching threads stops the current reply. Edit/resend and regeneration replace the downstream transcript rather than duplicating user messages. Approval pauses the clock until an explicit decision; decline produces a read-only explanation. Message history, feedback and draft data disappear on reload.
1842
+
1843
+ All model names, progress summaries, searches and tool outputs on this route are local examples, not real AI or hidden chain-of-thought. File selection validates a maximum of three image/text/PDF files of 10 MB each and retains metadata only: it never reads or uploads file contents. Diff acceptance and recommendation actions only prepare a follow-up draft. The library does not supply authentication, conversation persistence, model transport, voice recording or server authorization. Connect those through application ViewModels and validate every real tool action on the server.
1844
+
1845
+ ### Composite alignment
1846
+
1847
+ Use one padding owner per boundary: `--basalt-space-panel-x/y` (16/12px) for composite card/header/body/footer sections, `--basalt-space-row-x/y/gap` (8/6/8px) for selectable rows, and `--basalt-space-content-gap` (8px) between related controls inside a composite. Page section gaps instead use `--basalt-space-layout-lg` (24px). `--basalt-space-row-content` derives the detail inset from row padding + a 16px icon slot + the row gap. Selectable action rows use the 22px `--basalt-line-row`, producing 34px single-line rows and growing by full lines when content wraps. Status icons occupy a first-line-height slot, not an arbitrary top margin. Body prose and code keep the 20px body line-height. Ordinary controls use 34px; inline message actions use the 34px `--basalt-size-action` target and 14px glyphs. Align visible glyphs with prose without removing their pointer target.
1848
+
1849
+ Thinking and ToolChips share row padding; disclosures rotate only their own chevron, never content icons. Approval, recommendation, retrieved context and diff sections share panel insets rather than adding another padding layer. ChatComposer owns its field inset, not an outer painted/padded card; the application owns placement in the page. ChatMessage `sources` renders a compact disclosure beside actions and a flat source list, with the same validated URL policy as ContextCards. Use ContextCards only for actual retrieved text chunks, not as a citation footer. No additional remote images are loaded.
1850
+
1851
+ The standalone build generates every Tailwind utility once in canonical order. Do not append handwritten copies of `.p-0`, `.bg-transparent` or other native utilities after generated axis/state rules: a later shorthand can silently erase semantic padding. Browser acceptance measures actual computed insets in both CSS entrypoints, under foreign host spacing and semantic overrides.
1852
+
1853
+ Public Radio, Checkbox, Switch and SegmentControl group roots establish the scoped `.basalt-ui` reset. Fieldsets and legends have no browser-default padding/margin inside that boundary; standalone and Tailwind consumers must have identical group insets. Host form groups outside Basalt remain untouched.
1854
+
1855
+ Native form legends do not participate in a fieldset's flex gap. Radio, Checkbox and Switch legends therefore own `margin-bottom: var(--basalt-space-field-gap)` (8px) independently of the group's row layout. Do not replace the semantic legend or add wrapper divs to simulate this spacing. Override the existing field-gap token when a different group density is required.
1856
+
1857
+ ### Code panels
1858
+
1859
+ `CodeBlock` and `CodeHighlighted` share one compact code panel: optional `title`, a decorative `icon` (default file-code glyph, `null` to hide), `copyable` (true by default) and `lineNumbers` (false by default). Plain CodeBlock takes string children; CodeHighlighted takes `code` and reuses the lightweight built-in tokenizer. The root now forwards HTMLDivElement attributes and `className`; its inner named, keyboard-focusable pre owns scrolling. Do not pass rendered React children or assume the root is a pre. Use `copyable={false}` without a title for headerless output. Existing `max-h-*` classes constrain the panel while the source region scrolls; the header remains visible.
1860
+
1861
+ For edge-attached content, use `attached` rather than overriding border, radius or padding classes. It removes the outer frame except the top separator and keeps code-owned insets. Do not wrap attached code in LayerCard.Body. The standard full-width disclosure is `Collapsible asChild` around a LayerCard, with `LayerCard.Header asChild` around CollapsibleTrigger and `CollapsibleContent unstyled` around a Body or self-inset code/table. Header owns the full hit target and aligned 16/12px insets; Collapsible owns only interaction and motion. See [container boundary ownership](DESIGN.md#container-boundary-ownership), also used by the catalog's navigation and example panels.
1862
+
1863
+ Code panels contain horizontal overscroll only. Vertical wheel/touch scrolling chains to the page: natural-height examples do not trap it, and explicitly height-constrained panels hand it off at their top/bottom edge. No JavaScript wheel interception or axis conversion is used. Horizontal gestures and focused Left/Right arrow keys still scroll long source lines without moving the header.
1864
+
1865
+ Panel insets use the shared 16/12px roles. Code content uses the shared small font token (.75rem, 12px reference) on a 20px line box; titles remain 13px and the copy target is 34px. Inline Code and Chat Markdown code spans use the same small font tier. Source lines retain empty and trailing lines, with horizontal scrolling rather than breaking tokens. The hidden, non-selectable line-number gutter uses CSS-generated text, so numbers never enter clipboard output or assistive reading. Copy uses the exact original source (including CRLF), prevents concurrent writes, reports permission/API failure and permits retry; feedback belongs to that source revision. No source text is interpreted as HTML or executed. Private model/ViewModel modules own tokenization and copy state; browser clipboard access remains in the View. Chat Markdown fences compose this same panel rather than maintaining a second toolbar implementation. This is not a language parser or a text-diff viewer.