@nocoo/basalt 2.1.7 → 2.2.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 (153) hide show
  1. package/ai/INTEGRATION.md +66 -28
  2. package/ai/RECIPES.md +46 -1
  3. package/ai/USAGE.md +2 -2
  4. package/ai/registry.json +65 -23
  5. package/dist/assets/brand-mark.js.map +1 -1
  6. package/dist/charts/area.js.map +1 -1
  7. package/dist/charts/bar.js.map +1 -1
  8. package/dist/charts/bullet.js.map +1 -1
  9. package/dist/charts/charts.js.map +1 -1
  10. package/dist/charts/config.js.map +1 -1
  11. package/dist/charts/custom-chart.js.map +1 -1
  12. package/dist/charts/date-navigation.js.map +1 -1
  13. package/dist/charts/donut.js.map +1 -1
  14. package/dist/charts/frame.js.map +1 -1
  15. package/dist/charts/funnel.js.map +1 -1
  16. package/dist/charts/gauge.js.map +1 -1
  17. package/dist/charts/grouped-bar.js.map +1 -1
  18. package/dist/charts/heatmap-calendar.js.map +1 -1
  19. package/dist/charts/heatmap-matrix.js.map +1 -1
  20. package/dist/charts/item-list.js.map +1 -1
  21. package/dist/charts/legend.js.map +1 -1
  22. package/dist/charts/line.js.map +1 -1
  23. package/dist/charts/palette.js.map +1 -1
  24. package/dist/charts/radar.js.map +1 -1
  25. package/dist/charts/sample.js.map +1 -1
  26. package/dist/charts/sankey.js.map +1 -1
  27. package/dist/charts/series.js.map +1 -1
  28. package/dist/charts/slot-bar.js.map +1 -1
  29. package/dist/charts/sparkline.js.map +1 -1
  30. package/dist/charts/stacked-bar.js.map +1 -1
  31. package/dist/charts/stat-card.js.map +1 -1
  32. package/dist/charts/timeline.js.map +1 -1
  33. package/dist/charts/timeseries.js.map +1 -1
  34. package/dist/charts/tooltip.js.map +1 -1
  35. package/dist/components/accordion.js.map +1 -1
  36. package/dist/components/alert-dialog.js.map +1 -1
  37. package/dist/components/app-header.d.ts +5 -1
  38. package/dist/components/app-header.js +12 -4
  39. package/dist/components/app-header.js.map +1 -1
  40. package/dist/components/app-shell.d.ts +4 -1
  41. package/dist/components/app-shell.js +7 -4
  42. package/dist/components/app-shell.js.map +1 -1
  43. package/dist/components/autocomplete.js.map +1 -1
  44. package/dist/components/avatar.js.map +1 -1
  45. package/dist/components/badge.js.map +1 -1
  46. package/dist/components/banner.js.map +1 -1
  47. package/dist/components/basalt-mark.js.map +1 -1
  48. package/dist/components/battery-meter.js.map +1 -1
  49. package/dist/components/breadcrumbs.js.map +1 -1
  50. package/dist/components/button.js +2 -2
  51. package/dist/components/button.js.map +1 -1
  52. package/dist/components/chat-bubble.js.map +1 -1
  53. package/dist/components/chat-composer.js.map +1 -1
  54. package/dist/components/chat-header.js.map +1 -1
  55. package/dist/components/chat-inbox.js.map +1 -1
  56. package/dist/components/checkbox.js.map +1 -1
  57. package/dist/components/clipboard-text.js.map +1 -1
  58. package/dist/components/code.js.map +1 -1
  59. package/dist/components/collapsible.js.map +1 -1
  60. package/dist/components/combobox.js.map +1 -1
  61. package/dist/components/command-palette.js.map +1 -1
  62. package/dist/components/confirm-dialog.d.ts +4 -1
  63. package/dist/components/confirm-dialog.js +20 -18
  64. package/dist/components/confirm-dialog.js.map +1 -1
  65. package/dist/components/context-menu.js.map +1 -1
  66. package/dist/components/data-table.js +1 -1
  67. package/dist/components/data-table.js.map +1 -1
  68. package/dist/components/date-picker.js.map +1 -1
  69. package/dist/components/delete-resource.js +1 -1
  70. package/dist/components/delete-resource.js.map +1 -1
  71. package/dist/components/description-list.js.map +1 -1
  72. package/dist/components/dialog.js.map +1 -1
  73. package/dist/components/dock.js.map +1 -1
  74. package/dist/components/dropdown-menu.js.map +1 -1
  75. package/dist/components/editable-nav-item.js.map +1 -1
  76. package/dist/components/empty.js.map +1 -1
  77. package/dist/components/fab.js.map +1 -1
  78. package/dist/components/field.js.map +1 -1
  79. package/dist/components/file-dropzone.js.map +1 -1
  80. package/dist/components/filter-bar.js.map +1 -1
  81. package/dist/components/flow.js.map +1 -1
  82. package/dist/components/grid.js.map +1 -1
  83. package/dist/components/hover-card.js.map +1 -1
  84. package/dist/components/icon-picker.js.map +1 -1
  85. package/dist/components/inline-editable.js.map +1 -1
  86. package/dist/components/input-area.js.map +1 -1
  87. package/dist/components/input-group.js.map +1 -1
  88. package/dist/components/input.js.map +1 -1
  89. package/dist/components/label.js.map +1 -1
  90. package/dist/components/layer-card.js.map +1 -1
  91. package/dist/components/link.js.map +1 -1
  92. package/dist/components/loader.js.map +1 -1
  93. package/dist/components/loading-screen.js.map +1 -1
  94. package/dist/components/menu-bar.js.map +1 -1
  95. package/dist/components/meter.js.map +1 -1
  96. package/dist/components/multi-select.js.map +1 -1
  97. package/dist/components/navigation-menu.js.map +1 -1
  98. package/dist/components/overlay.js.map +1 -1
  99. package/dist/components/page-header.js.map +1 -1
  100. package/dist/components/pagination.js.map +1 -1
  101. package/dist/components/popover.js.map +1 -1
  102. package/dist/components/radio.js.map +1 -1
  103. package/dist/components/resource-list.js.map +1 -1
  104. package/dist/components/responsive-master-detail.js +5 -5
  105. package/dist/components/responsive-master-detail.js.map +1 -1
  106. package/dist/components/scroll-area.js.map +1 -1
  107. package/dist/components/section-rule.js.map +1 -1
  108. package/dist/components/segment-control.js.map +1 -1
  109. package/dist/components/select.js.map +1 -1
  110. package/dist/components/sensitive-input.js.map +1 -1
  111. package/dist/components/separator.js.map +1 -1
  112. package/dist/components/sheet.js +1 -0
  113. package/dist/components/sheet.js.map +1 -1
  114. package/dist/components/sidebar.d.ts +3 -1
  115. package/dist/components/sidebar.js +8 -5
  116. package/dist/components/sidebar.js.map +1 -1
  117. package/dist/components/skeleton-line.js.map +1 -1
  118. package/dist/components/slider.js.map +1 -1
  119. package/dist/components/stat-strip.js.map +1 -1
  120. package/dist/components/switch.js.map +1 -1
  121. package/dist/components/table-of-contents.js.map +1 -1
  122. package/dist/components/table-pager.js.map +1 -1
  123. package/dist/components/table.js.map +1 -1
  124. package/dist/components/tabs.js.map +1 -1
  125. package/dist/components/tag-badge.js.map +1 -1
  126. package/dist/components/tag-color-picker.js.map +1 -1
  127. package/dist/components/text.js.map +1 -1
  128. package/dist/components/theme-toggle.js.map +1 -1
  129. package/dist/components/toast.js.map +1 -1
  130. package/dist/components/toggle-group.js +2 -2
  131. package/dist/components/toggle-group.js.map +1 -1
  132. package/dist/components/toggle.js +1 -1
  133. package/dist/components/toggle.js.map +1 -1
  134. package/dist/components/toolbar.js.map +1 -1
  135. package/dist/components/tooltip.js +14 -13
  136. package/dist/components/tooltip.js.map +1 -1
  137. package/dist/components/typeahead-field.js.map +1 -1
  138. package/dist/components/upload-queue.js.map +1 -1
  139. package/dist/providers/accent.js +1 -1
  140. package/dist/providers/accent.js.map +1 -1
  141. package/dist/providers/link.js.map +1 -1
  142. package/dist/providers/theme.js +1 -1
  143. package/dist/providers/theme.js.map +1 -1
  144. package/dist/styles/base.css +116 -0
  145. package/dist/styles/standalone.css +161 -16
  146. package/dist/utils/cn.js.map +1 -1
  147. package/dist/utils/control-surface.js.map +1 -1
  148. package/dist/utils/preference-store.js +1 -1
  149. package/dist/utils/preference-store.js.map +1 -1
  150. package/dist/utils/selection-indicator.js +1 -1
  151. package/dist/utils/selection-indicator.js.map +1 -1
  152. package/dist/utils/use-checkable-group.js.map +1 -1
  153. package/package.json +2 -2
package/ai/INTEGRATION.md CHANGED
@@ -51,7 +51,7 @@ AppShell ← dashboard and catalog routes
51
51
  └── LayerCard… ← all later product UI
52
52
  ```
53
53
 
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.
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 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
55
 
56
56
  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
57
 
@@ -365,25 +365,49 @@ Icons: `lucide-react`, `strokeWidth={1.5}`, nav size `h-4 w-4 shrink-0`.
365
365
  <a id="root-geometry"></a>
366
366
  ## 6. Root geometry
367
367
 
368
- `AppShell` is a full-viewport flex row: `h-screen w-full overflow-hidden bg-basalt-background`.
368
+ `AppShell` owns the viewport and safe area; `AppMain` and `ContentIsland` derive their scroll behavior from it. Choose deliberately:
369
369
 
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
- ```
370
+ | `layout` | Below 768px | 768px and above |
371
+ | --- | --- | --- |
372
+ | `workspace` (default) | Bounded panes | Bounded panes |
373
+ | `responsive` | Natural document scrolling | Bounded panes |
374
+ | `document` | Natural document scrolling | Natural document scrolling |
375
+
376
+ 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
377
 
380
378
  ### AppShell, AppMain, AppSkipLink API & Contracts
381
379
 
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.
380
+ - **`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.
381
+ - **`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.
382
+ - **`AppSkipLink`** points to `#main-content` unless `href` is supplied. Provide visible link text; keep it first in the shell.
383
+ - **`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.
384
+ - 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.
385
+
386
+ ```tsx excerpt:responsive-shell
387
+ <AppShell layout="responsive">
388
+ <AppSkipLink>Skip to content</AppSkipLink>
389
+ <AppMain>
390
+ <AppHeader title="Field notes" actions={actions} />
391
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col md:px-3 md:pb-3">
392
+ <ContentIsland mobileSurface="edge-to-edge">
393
+ <div className="mx-auto max-w-prose px-4 py-3 md:p-0">{content}</div>
394
+ </ContentIsland>
395
+ </div>
396
+ </AppMain>
397
+ </AppShell>
398
+ ```
399
+
400
+ The wrapper intentionally has **no mobile `px-2` / `pb-2`**. 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.
401
+
402
+ 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.
403
+
404
+ 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.
405
+
406
+ `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`.
385
407
 
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.
408
+ **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.
409
+
410
+ 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
411
 
388
412
  ---
389
413
 
@@ -391,7 +415,7 @@ The island wrap is the only extra layout div in the main column. Pages render **
391
415
 
392
416
  `Sidebar` **is** the column. It already has:
393
417
 
394
- - `h-screen`, `flex-col`, `shrink-0`, `sticky top-0`
418
+ - shell-derived bounded height (standalone `100dvh`), `flex-col`, `shrink-0`, sticky positioning at the shell-owned safe top
395
419
  - expanded width **260px** (inline `width`)
396
420
  - collapsed width **68px**
397
421
  - collapse animation `transition-all duration-300 ease-in-out`
@@ -403,7 +427,7 @@ Do not wrap those regions in another full-viewport column. Do not set `h-screen`
403
427
  ### Expanded tree
404
428
 
405
429
  ```
406
- Sidebar ← collapsed={false}; owns 260px and h-screen
430
+ Sidebar ← collapsed={false}; owns 260px and bounded height
407
431
  ├── SidebarHeader ← h-14 px-3 already
408
432
  │ └── brand row ← flex, items-center, justify-between, w-full
409
433
  │ (no extra horizontal padding)
@@ -579,9 +603,11 @@ Breakpoint: `768px`. Below that, the in-flow rail is omitted. The same sidebar c
579
603
  </Sheet>
580
604
  ```
581
605
 
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.
606
+ `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.
607
+
608
+ `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.
583
609
 
584
- `AppHeader` `leading` is the menu button on mobile only (`Button variant="ghost" size="icon" className="h-8 w-8"`).
610
+ 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.
585
611
 
586
612
  Local hook (not in the package):
587
613
 
@@ -619,12 +645,6 @@ export function AppFrame() {
619
645
  setMobileOpen(false);
620
646
  }, [location.pathname]);
621
647
 
622
- useEffect(() => {
623
- document.body.style.overflow = mobileOpen ? "hidden" : "";
624
- return () => {
625
- document.body.style.overflow = "";
626
- };
627
- }, [mobileOpen]);
628
648
 
629
649
  return (
630
650
  <AppShell>
@@ -674,7 +694,7 @@ export function AppFrame() {
674
694
 
675
695
  <a id="appheader-and-breadcrumbs"></a>
676
696
 
677
- `AppHeader` is `h-14`, matching `SidebarHeader`.
697
+ `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.
678
698
 
679
699
  ### AppHeader API & Contracts
680
700
 
@@ -775,7 +795,7 @@ Boot and route gates use `LoadingScreen` — a centered mark and a 6rem shimmer
775
795
 
776
796
  ## 13. First page
777
797
 
778
- 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.
798
+ 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.
779
799
 
780
800
  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.
781
801
 
@@ -982,7 +1002,7 @@ import { LayerCard } from "@nocoo/basalt/components/layer-card";
982
1002
 
983
1003
  ## 15. PageHeader and SectionRule
984
1004
 
985
- `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.
1005
+ `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.
986
1006
 
987
1007
  ### PageHeader
988
1008
 
@@ -1774,3 +1794,21 @@ export function UserPreferencesForm() {
1774
1794
  );
1775
1795
  }
1776
1796
  ```
1797
+
1798
+ ### Confirmation focus after responsive replacement
1799
+
1800
+ `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.
1801
+
1802
+ ```tsx excerpt:confirmation-focus-replacement
1803
+ <ConfirmDialog
1804
+ open={confirmOpen}
1805
+ onOpenChange={setConfirmOpen}
1806
+ title="Remove item?"
1807
+ description="This action cannot be undone."
1808
+ onConfirm={removeItem}
1809
+ onCloseAutoFocus={(event) => {
1810
+ event.preventDefault();
1811
+ currentActionRef.current?.focus();
1812
+ }}
1813
+ />
1814
+ ```
package/ai/RECIPES.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Application recipes
2
2
 
3
- These complete modules, included in v2.1.0, use only public Basalt imports and React.
3
+ These complete modules use only public Basalt imports and React. AppFrame, Login and Resources are available since v2.1.0; MobileLayout requires v2.2.0.
4
4
 
5
5
  Load exactly one CSS mode at the application entry: import `@nocoo/basalt/styles/standalone` for a build without Tailwind, or configure the Tailwind source scan and import `@nocoo/basalt/styles/tailwind` as described in INTEGRATION.md. In Next, import CSS in the root layout and import these modules through a `use client` boundary. Mount ThemeProvider once per application; the standalone recipes include their own root provider. Root pages should set body margin to zero.
6
6
 
@@ -159,3 +159,48 @@ export default function ResourcesRecipe() {
159
159
  </main></ThemeProvider>;
160
160
  }
161
161
  ```
162
+
163
+ ## Mobile layouts
164
+
165
+ Use `responsive` for mobile root scrolling and desktop island scrolling; `document` keeps root scrolling at every width, while `workspace` remains bounded everywhere. Select this per layout, not by user-agent. Place the compact sticky header inside the edge-to-edge island for a single immersive row. Keep a normal PageHeader in a desktop-only wrapper when it adds context. The outer wrapper must not retain mobile `px-2 pb-2`; the island cannot remove its parent's spacing.
166
+
167
+ This complete recipe compiles from the installed package. Supply content and actions from the application; there are no routing, article, account or network assumptions. The consumer owns a zero-margin body and viewport metadata (`width=device-width, initial-scale=1, viewport-fit=cover`). Do not lock body scrolling or add height/overflow constraints to intermediate wrappers. The shell handles safe areas; the sticky header paints the notch gap without charging the initial top inset twice. Bottom safe area follows the last content, not a fixed footer strip.
168
+
169
+ ```tsx compile:recipe-mobile-layout
170
+ import { AppHeader } from "@nocoo/basalt/components/app-header";
171
+ import { AppMain, AppShell, AppSkipLink } from "@nocoo/basalt/components/app-shell";
172
+ import { Button } from "@nocoo/basalt/components/button";
173
+ import { Popover, PopoverContent, PopoverTrigger } from "@nocoo/basalt/components/popover";
174
+ import { ContentIsland } from "@nocoo/basalt/components/sidebar";
175
+ import { ThemeProvider } from "@nocoo/basalt/providers/theme";
176
+
177
+ export default function MobileLayoutRecipe() {
178
+ return <ThemeProvider><AppShell layout="responsive">
179
+ <AppSkipLink>Skip to content</AppSkipLink>
180
+ <AppMain>
181
+ <ContentIsland mobileSurface="edge-to-edge">
182
+ <AppHeader sticky density="compact" title="Field notes" actions={
183
+ <Popover><PopoverTrigger asChild><Button variant="ghost">Options</Button></PopoverTrigger>
184
+ <PopoverContent aria-label="Options"><p>Application-owned reading preferences.</p></PopoverContent>
185
+ </Popover>
186
+ } />
187
+ <article style={{ maxWidth: "65ch", marginInline: "auto", padding: "1rem", lineHeight: 1.7 }}>
188
+ {Array.from({ length: 24 }, (_, index) => <section key={index}>
189
+ <h2>Observation {index + 1}</h2>
190
+ <p>The shell leaves the document in charge on a narrow screen. Content grows naturally, the header stays reachable, and the final paragraph remains above the trailing safe area.</p>
191
+ </section>)}
192
+ <p>End of notes.</p>
193
+ </article>
194
+ </ContentIsland>
195
+ </AppMain>
196
+ </AppShell></ThemeProvider>;
197
+ }
198
+ ```
199
+
200
+ ### List/detail and form boundaries
201
+
202
+ - Full-page examples: [reader](https://basaltui.com/examples/reader), [list/detail](https://basaltui.com/examples/list-detail), [workspace/form](https://basaltui.com/examples/workspace). Open them outside another AppShell; do not nest viewport shells.
203
+ - Use `ResponsiveMasterDetail` inside a `ContentIsland` with `className="md:overflow-hidden"`, and give the composition `className="md:h-full"`. Its desktop panes scroll independently; below 768px only the visible pane participates in document layout. The application owns selection. Built-in back navigation restores focus to the opening item, with a region fallback if it no longer exists.
204
+ - Keep `mobileSurface="inset"` for regular forms. Use native labels, appropriate input types and at least 44px controls; 16px input text avoids the common small-text zoom problem without disabling user zoom. Browser scrolling brings focused fields into view; never reposition the viewport with innerHeight.
205
+ - Modal `Sheet`/overlay `Sidebar` owns focus trapping, Escape, focus restoration and background scroll locking. Do not add a second manual body lock. An expanded Sidebar inside a Sheet stays bounded and owns the portal's safe-area padding once.
206
+ - Automated Chromium/WebKit evidence covers geometry, document/pane scroll owners, resize, focus and overlays. Real iPhone Safari toolbar collapse, notch behavior during toolbar transitions, and software keyboard reachability require a physical-device check; desktop WebKit is not that proof.
package/ai/USAGE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Basalt AI & Machine Usage Guide
2
2
 
3
- This document describes how AI code assistants, automated agents, and developers can consume `@nocoo/basalt` (v2.1.7).
3
+ This document describes how AI code assistants, automated agents, and developers can consume `@nocoo/basalt` (v2.2.0).
4
4
 
5
5
  ---
6
6
 
@@ -94,6 +94,6 @@ export function App() {
94
94
 
95
95
  ## 6. Application recipes and palette preferences
96
96
 
97
- Read [RECIPES.md](RECIPES.md) for complete AppFrame, Login, and Resources modules. Each module is compiled and exercised from the installed tarball in standalone, Tailwind, and Next.js consumers. These recipes keep authentication, routing, data and network requests in the application.
97
+ Read [RECIPES.md](RECIPES.md) for complete AppFrame, Login, Resources, and MobileLayout modules. Each module is compiled and exercised from the installed tarball in standalone, Tailwind, and Next.js consumers. These recipes keep authentication, routing, data and network requests in the application.
98
98
 
99
99
  Read [INTEGRATION.md#accent-provider](INTEGRATION.md#accent-provider) for the twelve control accents, optional `paletteOverrides`, and the fixed five-color chart cycle matching the classic candy swatches with its existing gray retained. Check [COMPATIBILITY.md](COMPATIBILITY.md) before migrating old color indices or stored IDs. The catalog at https://basaltui.com/palette provides an application-owned editor with localStorage persistence.
package/ai/registry.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "packageName": "@nocoo/basalt",
3
- "packageVersion": "2.1.7",
3
+ "packageVersion": "2.2.0",
4
4
  "totalModules": 122,
5
5
  "totalSymbols": 741,
6
6
  "totalValues": 395,
@@ -7070,7 +7070,7 @@
7070
7070
  "importPath": "@nocoo/basalt/components/app-header",
7071
7071
  "subpath": "./components/app-header",
7072
7072
  "sourceFile": "packages/basalt/src/components/app-header.tsx",
7073
- "sourceHash": "80acd367d8bdc9bf",
7073
+ "sourceHash": "e03e115cf4b8f4a1",
7074
7074
  "rootAvailable": false,
7075
7075
  "maturity": "ready",
7076
7076
  "kind": "framework-chrome",
@@ -7096,7 +7096,7 @@
7096
7096
  "importPath": "@nocoo/basalt/components/app-shell",
7097
7097
  "subpath": "./components/app-shell",
7098
7098
  "sourceFile": "packages/basalt/src/components/app-shell.tsx",
7099
- "sourceHash": "f581b2af0175670b",
7099
+ "sourceHash": "360ded256b2b5947",
7100
7100
  "rootAvailable": false,
7101
7101
  "maturity": "ready",
7102
7102
  "kind": "framework-chrome",
@@ -8126,7 +8126,7 @@
8126
8126
  "importPath": "@nocoo/basalt/components/button",
8127
8127
  "subpath": "./components/button",
8128
8128
  "sourceFile": "packages/basalt/src/components/button.tsx",
8129
- "sourceHash": "1b6293380c63e14c",
8129
+ "sourceHash": "fd3e84a54d2f6f61",
8130
8130
  "rootAvailable": true,
8131
8131
  "maturity": "ready",
8132
8132
  "kind": "catalog-component",
@@ -10360,7 +10360,7 @@
10360
10360
  "importPath": "@nocoo/basalt/components/confirm-dialog",
10361
10361
  "subpath": "./components/confirm-dialog",
10362
10362
  "sourceFile": "packages/basalt/src/components/confirm-dialog.tsx",
10363
- "sourceHash": "b184b69c8d5ee916",
10363
+ "sourceHash": "cafdd27c6800d3cd",
10364
10364
  "rootAvailable": true,
10365
10365
  "maturity": "ready",
10366
10366
  "kind": "catalog-component",
@@ -10443,6 +10443,12 @@
10443
10443
  "required": true,
10444
10444
  "description": "Called when the dialog requests to open or close."
10445
10445
  },
10446
+ {
10447
+ "name": "onCloseAutoFocus",
10448
+ "type": "(event: Event) => void",
10449
+ "required": false,
10450
+ "description": "Runs after the close focus trap releases. Prevent default to focus a replacement control."
10451
+ },
10446
10452
  {
10447
10453
  "name": "onConfirm",
10448
10454
  "type": "() => void | Promise<void>",
@@ -10551,6 +10557,12 @@
10551
10557
  "required": true,
10552
10558
  "description": "Called when the dialog requests to open or close."
10553
10559
  },
10560
+ {
10561
+ "name": "onCloseAutoFocus",
10562
+ "type": "(event: Event) => void",
10563
+ "required": false,
10564
+ "description": "Runs after the close focus trap releases. Prevent default to focus a replacement control."
10565
+ },
10554
10566
  {
10555
10567
  "name": "onConfirm",
10556
10568
  "type": "() => void | Promise<void>",
@@ -20358,7 +20370,7 @@
20358
20370
  "importPath": "@nocoo/basalt/components/responsive-master-detail",
20359
20371
  "subpath": "./components/responsive-master-detail",
20360
20372
  "sourceFile": "packages/basalt/src/components/responsive-master-detail.tsx",
20361
- "sourceHash": "c7e06934d2c2e706",
20373
+ "sourceHash": "43d5629efcdfbbbf",
20362
20374
  "rootAvailable": false,
20363
20375
  "maturity": "ready",
20364
20376
  "kind": "catalog-component",
@@ -21476,7 +21488,7 @@
21476
21488
  "importPath": "@nocoo/basalt/components/sheet",
21477
21489
  "subpath": "./components/sheet",
21478
21490
  "sourceFile": "packages/basalt/src/components/sheet.tsx",
21479
- "sourceHash": "1fd48fe8aa7559fa",
21491
+ "sourceHash": "49cf6f015335f773",
21480
21492
  "rootAvailable": true,
21481
21493
  "maturity": "ready",
21482
21494
  "kind": "catalog-component",
@@ -21940,7 +21952,7 @@
21940
21952
  "importPath": "@nocoo/basalt/components/sidebar",
21941
21953
  "subpath": "./components/sidebar",
21942
21954
  "sourceFile": "packages/basalt/src/components/sidebar.tsx",
21943
- "sourceHash": "5ee61278f01f173c",
21955
+ "sourceHash": "89fb8df61f17524a",
21944
21956
  "rootAvailable": true,
21945
21957
  "maturity": "ready",
21946
21958
  "kind": "catalog-component",
@@ -22366,7 +22378,15 @@
22366
22378
  },
22367
22379
  {
22368
22380
  "name": "ContentIsland",
22369
- "props": []
22381
+ "props": [
22382
+ {
22383
+ "name": "mobileSurface",
22384
+ "type": "\"edge-to-edge\" | \"inset\"",
22385
+ "required": false,
22386
+ "default": "\"inset\"",
22387
+ "description": "Mobile surface treatment; desktop remains an inset L1 island."
22388
+ }
22389
+ ]
22370
22390
  },
22371
22391
  {
22372
22392
  "name": "useSidebar()",
@@ -22633,7 +22653,15 @@
22633
22653
  },
22634
22654
  {
22635
22655
  "name": "ContentIsland",
22636
- "props": []
22656
+ "props": [
22657
+ {
22658
+ "name": "mobileSurface",
22659
+ "type": "\"edge-to-edge\" | \"inset\"",
22660
+ "required": false,
22661
+ "default": "\"inset\"",
22662
+ "description": "Mobile surface treatment; desktop remains an inset L1 island."
22663
+ }
22664
+ ]
22637
22665
  },
22638
22666
  {
22639
22667
  "name": "useSidebar()",
@@ -25878,7 +25906,7 @@
25878
25906
  "importPath": "@nocoo/basalt/components/toggle",
25879
25907
  "subpath": "./components/toggle",
25880
25908
  "sourceFile": "packages/basalt/src/components/toggle.tsx",
25881
- "sourceHash": "632d36c606c7d7e0",
25909
+ "sourceHash": "c507d31badda1b2d",
25882
25910
  "rootAvailable": false,
25883
25911
  "maturity": "ready",
25884
25912
  "kind": "catalog-component",
@@ -26016,7 +26044,7 @@
26016
26044
  "importPath": "@nocoo/basalt/components/toggle-group",
26017
26045
  "subpath": "./components/toggle-group",
26018
26046
  "sourceFile": "packages/basalt/src/components/toggle-group.tsx",
26019
- "sourceHash": "c25d112b708484f6",
26047
+ "sourceHash": "d213fc765fff2b03",
26020
26048
  "rootAvailable": false,
26021
26049
  "maturity": "ready",
26022
26050
  "kind": "catalog-component",
@@ -26482,7 +26510,7 @@
26482
26510
  "importPath": "@nocoo/basalt/components/tooltip",
26483
26511
  "subpath": "./components/tooltip",
26484
26512
  "sourceFile": "packages/basalt/src/components/tooltip.tsx",
26485
- "sourceHash": "62accdd15ce33330",
26513
+ "sourceHash": "ad4e479b428769f8",
26486
26514
  "rootAvailable": true,
26487
26515
  "maturity": "ready",
26488
26516
  "kind": "catalog-component",
@@ -27636,7 +27664,7 @@
27636
27664
  "exportName": "Button",
27637
27665
  "importPath": "@nocoo/basalt/components/button",
27638
27666
  "sourceFile": "packages/basalt/src/components/button.tsx",
27639
- "sourceHash": "1b6293380c63e14c",
27667
+ "sourceHash": "fd3e84a54d2f6f61",
27640
27668
  "rootAvailable": true,
27641
27669
  "maturity": "ready",
27642
27670
  "packageDoc": "ai/registry.json#button",
@@ -27695,7 +27723,7 @@
27695
27723
  "exportName": "LinkButton",
27696
27724
  "importPath": "@nocoo/basalt/components/button",
27697
27725
  "sourceFile": "packages/basalt/src/components/button.tsx",
27698
- "sourceHash": "1b6293380c63e14c",
27726
+ "sourceHash": "fd3e84a54d2f6f61",
27699
27727
  "rootAvailable": false,
27700
27728
  "maturity": "ready",
27701
27729
  "packageDoc": "ai/registry.json#link-button",
@@ -27915,7 +27943,7 @@
27915
27943
  "exportName": "Tooltip",
27916
27944
  "importPath": "@nocoo/basalt/components/tooltip",
27917
27945
  "sourceFile": "packages/basalt/src/components/tooltip.tsx",
27918
- "sourceHash": "62accdd15ce33330",
27946
+ "sourceHash": "ad4e479b428769f8",
27919
27947
  "rootAvailable": true,
27920
27948
  "maturity": "ready",
27921
27949
  "packageDoc": "ai/registry.json#tooltip",
@@ -29385,7 +29413,7 @@
29385
29413
  "exportName": "Toggle",
29386
29414
  "importPath": "@nocoo/basalt/components/toggle",
29387
29415
  "sourceFile": "packages/basalt/src/components/toggle.tsx",
29388
- "sourceHash": "632d36c606c7d7e0",
29416
+ "sourceHash": "c507d31badda1b2d",
29389
29417
  "rootAvailable": false,
29390
29418
  "maturity": "ready",
29391
29419
  "packageDoc": "ai/registry.json#toggle",
@@ -29443,7 +29471,7 @@
29443
29471
  "exportName": "ToggleGroup",
29444
29472
  "importPath": "@nocoo/basalt/components/toggle-group",
29445
29473
  "sourceFile": "packages/basalt/src/components/toggle-group.tsx",
29446
- "sourceHash": "c25d112b708484f6",
29474
+ "sourceHash": "d213fc765fff2b03",
29447
29475
  "rootAvailable": false,
29448
29476
  "maturity": "ready",
29449
29477
  "packageDoc": "ai/registry.json#toggle-group",
@@ -31228,7 +31256,7 @@
31228
31256
  "exportName": "ConfirmDialog",
31229
31257
  "importPath": "@nocoo/basalt/components/confirm-dialog",
31230
31258
  "sourceFile": "packages/basalt/src/components/confirm-dialog.tsx",
31231
- "sourceHash": "b184b69c8d5ee916",
31259
+ "sourceHash": "cafdd27c6800d3cd",
31232
31260
  "rootAvailable": true,
31233
31261
  "maturity": "ready",
31234
31262
  "packageDoc": "ai/registry.json#confirm-dialog",
@@ -31248,6 +31276,12 @@
31248
31276
  "required": true,
31249
31277
  "description": "Called when the dialog requests to open or close."
31250
31278
  },
31279
+ {
31280
+ "name": "onCloseAutoFocus",
31281
+ "type": "(event: Event) => void",
31282
+ "required": false,
31283
+ "description": "Runs after the close focus trap releases. Prevent default to focus a replacement control."
31284
+ },
31251
31285
  {
31252
31286
  "name": "onConfirm",
31253
31287
  "type": "() => void | Promise<void>",
@@ -32374,7 +32408,7 @@
32374
32408
  "exportName": "Sheet",
32375
32409
  "importPath": "@nocoo/basalt/components/sheet",
32376
32410
  "sourceFile": "packages/basalt/src/components/sheet.tsx",
32377
- "sourceHash": "1fd48fe8aa7559fa",
32411
+ "sourceHash": "49cf6f015335f773",
32378
32412
  "rootAvailable": true,
32379
32413
  "maturity": "ready",
32380
32414
  "packageDoc": "ai/registry.json#sheet",
@@ -33904,7 +33938,7 @@
33904
33938
  "exportName": "Sidebar",
33905
33939
  "importPath": "@nocoo/basalt/components/sidebar",
33906
33940
  "sourceFile": "packages/basalt/src/components/sidebar.tsx",
33907
- "sourceHash": "5ee61278f01f173c",
33941
+ "sourceHash": "89fb8df61f17524a",
33908
33942
  "rootAvailable": true,
33909
33943
  "maturity": "ready",
33910
33944
  "packageDoc": "ai/registry.json#sidebar",
@@ -34099,7 +34133,15 @@
34099
34133
  },
34100
34134
  {
34101
34135
  "name": "ContentIsland",
34102
- "props": []
34136
+ "props": [
34137
+ {
34138
+ "name": "mobileSurface",
34139
+ "type": "\"edge-to-edge\" | \"inset\"",
34140
+ "required": false,
34141
+ "default": "\"inset\"",
34142
+ "description": "Mobile surface treatment; desktop remains an inset L1 island."
34143
+ }
34144
+ ]
34103
34145
  },
34104
34146
  {
34105
34147
  "name": "useSidebar()",
@@ -35821,7 +35863,7 @@
35821
35863
  "exportName": "ResponsiveMasterDetail",
35822
35864
  "importPath": "@nocoo/basalt/components/responsive-master-detail",
35823
35865
  "sourceFile": "packages/basalt/src/components/responsive-master-detail.tsx",
35824
- "sourceHash": "c7e06934d2c2e706",
35866
+ "sourceHash": "43d5629efcdfbbbf",
35825
35867
  "rootAvailable": false,
35826
35868
  "maturity": "ready",
35827
35869
  "packageDoc": "ai/registry.json#responsive-master-detail",