@nocoo/basalt 2.2.0 → 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.
- package/README.md +2 -0
- package/ai/COMPATIBILITY.md +11 -1
- package/ai/DESIGN.md +342 -0
- package/ai/INTEGRATION.md +179 -128
- package/ai/RECIPES.md +51 -26
- package/ai/USAGE.md +4 -4
- package/ai/registry.json +2836 -270
- package/dist/charts/config.d.ts +11 -11
- package/dist/charts/config.js +4 -4
- package/dist/charts/config.js.map +1 -1
- package/dist/charts/date-navigation.js +10 -10
- package/dist/charts/date-navigation.js.map +1 -1
- package/dist/charts/donut.js +1 -1
- package/dist/charts/donut.js.map +1 -1
- package/dist/charts/frame.d.ts +1 -1
- package/dist/charts/frame.js +4 -4
- package/dist/charts/frame.js.map +1 -1
- package/dist/charts/gauge.js +5 -5
- package/dist/charts/gauge.js.map +1 -1
- package/dist/charts/heatmap-calendar.js +11 -11
- package/dist/charts/heatmap-calendar.js.map +1 -1
- package/dist/charts/heatmap-matrix.js +8 -8
- package/dist/charts/heatmap-matrix.js.map +1 -1
- package/dist/charts/item-list.js +1 -1
- package/dist/charts/item-list.js.map +1 -1
- package/dist/charts/legend.js +19 -19
- package/dist/charts/legend.js.map +1 -1
- package/dist/charts/palette.js +2 -2
- package/dist/charts/palette.js.map +1 -1
- package/dist/charts/slot-bar.js +2 -2
- package/dist/charts/slot-bar.js.map +1 -1
- package/dist/charts/sparkline.js +38 -23
- package/dist/charts/sparkline.js.map +1 -1
- package/dist/charts/stat-card.d.ts +2 -2
- package/dist/charts/stat-card.js +61 -58
- package/dist/charts/stat-card.js.map +1 -1
- package/dist/charts/timeline.js +18 -9
- package/dist/charts/timeline.js.map +1 -1
- package/dist/charts/tooltip.js +16 -16
- package/dist/charts/tooltip.js.map +1 -1
- package/dist/components/accordion.js +1 -1
- package/dist/components/accordion.js.map +1 -1
- package/dist/components/alert-dialog.js +4 -4
- package/dist/components/alert-dialog.js.map +1 -1
- package/dist/components/app-header.js +8 -8
- package/dist/components/app-header.js.map +1 -1
- package/dist/components/app-shell.js +1 -1
- package/dist/components/app-shell.js.map +1 -1
- package/dist/components/approval-card.d.ts +13 -0
- package/dist/components/approval-card.js +161 -0
- package/dist/components/approval-card.js.map +1 -0
- package/dist/components/avatar.d.ts +12 -0
- package/dist/components/avatar.js +39 -18
- package/dist/components/avatar.js.map +1 -1
- package/dist/components/badge.js +8 -8
- package/dist/components/badge.js.map +1 -1
- package/dist/components/banner.d.ts +2 -2
- package/dist/components/banner.js +9 -8
- package/dist/components/banner.js.map +1 -1
- package/dist/components/basalt-mark.js +1 -1
- package/dist/components/basalt-mark.js.map +1 -1
- package/dist/components/battery-meter.js +5 -5
- package/dist/components/battery-meter.js.map +1 -1
- package/dist/components/breadcrumbs.js +6 -6
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button.js +10 -10
- package/dist/components/button.js.map +1 -1
- package/dist/components/chat-bubble.js +4 -3
- package/dist/components/chat-bubble.js.map +1 -1
- package/dist/components/chat-composer.d.ts +21 -3
- package/dist/components/chat-composer.js +126 -60
- package/dist/components/chat-composer.js.map +1 -1
- package/dist/components/chat-header.js +6 -6
- package/dist/components/chat-header.js.map +1 -1
- package/dist/components/chat-inbox.js +34 -30
- package/dist/components/chat-inbox.js.map +1 -1
- package/dist/components/chat-markdown.d.ts +8 -0
- package/dist/components/chat-markdown.js +122 -0
- package/dist/components/chat-markdown.js.map +1 -0
- package/dist/components/chat-message.d.ts +26 -0
- package/dist/components/chat-message.js +145 -0
- package/dist/components/chat-message.js.map +1 -0
- package/dist/components/checkbox.js +11 -11
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/clipboard-text.js +3 -3
- package/dist/components/clipboard-text.js.map +1 -1
- package/dist/components/code.d.ts +22 -20
- package/dist/components/code.js +94 -32
- package/dist/components/code.js.map +1 -1
- package/dist/components/collapsible.js +8 -4
- package/dist/components/collapsible.js.map +1 -1
- package/dist/components/command-palette.js +73 -47
- package/dist/components/command-palette.js.map +1 -1
- package/dist/components/context-cards.d.ts +15 -0
- package/dist/components/context-cards.js +148 -0
- package/dist/components/context-cards.js.map +1 -0
- package/dist/components/context-menu.d.ts +1 -1
- package/dist/components/context-menu.js +18 -14
- package/dist/components/context-menu.js.map +1 -1
- package/dist/components/data-table.d.ts +8 -2
- package/dist/components/data-table.js +161 -106
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +13 -13
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/delete-resource.js +1 -1
- package/dist/components/delete-resource.js.map +1 -1
- package/dist/components/description-list.js +3 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog.d.ts +2 -2
- package/dist/components/dialog.js +8 -8
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/diff-table.d.ts +15 -0
- package/dist/components/diff-table.js +146 -0
- package/dist/components/diff-table.js.map +1 -0
- package/dist/components/dock.js +2 -2
- package/dist/components/dock.js.map +1 -1
- package/dist/components/dropdown-menu.js +23 -18
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/editable-nav-item.js +63 -58
- package/dist/components/editable-nav-item.js.map +1 -1
- package/dist/components/empty.js +6 -6
- package/dist/components/empty.js.map +1 -1
- package/dist/components/fab.js +1 -1
- package/dist/components/fab.js.map +1 -1
- package/dist/components/field.js +3 -3
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-dropzone.js +7 -7
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/filter-bar.js +6 -6
- package/dist/components/filter-bar.js.map +1 -1
- package/dist/components/flow.js +4 -3
- package/dist/components/flow.js.map +1 -1
- package/dist/components/grid.d.ts +3 -1
- package/dist/components/grid.js +15 -8
- package/dist/components/grid.js.map +1 -1
- package/dist/components/hover-card.js +1 -1
- package/dist/components/hover-card.js.map +1 -1
- package/dist/components/icon-picker.d.ts +8 -2
- package/dist/components/icon-picker.js +75 -47
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-editable.js +6 -6
- package/dist/components/inline-editable.js.map +1 -1
- package/dist/components/input-area.js +3 -3
- package/dist/components/input-area.js.map +1 -1
- package/dist/components/input-group.js +5 -5
- package/dist/components/input-group.js.map +1 -1
- package/dist/components/input.js +4 -4
- package/dist/components/input.js.map +1 -1
- package/dist/components/label.js +3 -3
- package/dist/components/label.js.map +1 -1
- package/dist/components/layer-card.d.ts +9 -4
- package/dist/components/layer-card.js +72 -67
- package/dist/components/layer-card.js.map +1 -1
- package/dist/components/loader.d.ts +16 -10
- package/dist/components/loader.js +46 -53
- package/dist/components/loader.js.map +1 -1
- package/dist/components/loading-screen.js +3 -3
- package/dist/components/loading-screen.js.map +1 -1
- package/dist/components/menu-bar.js +32 -27
- package/dist/components/menu-bar.js.map +1 -1
- package/dist/components/meter.d.ts +5 -5
- package/dist/components/meter.js +25 -12
- package/dist/components/meter.js.map +1 -1
- package/dist/components/multi-select.js +66 -62
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/navigation-menu.d.ts +2 -2
- package/dist/components/navigation-menu.js +22 -8
- package/dist/components/navigation-menu.js.map +1 -1
- package/dist/components/overlay.js +2 -2
- package/dist/components/overlay.js.map +1 -1
- package/dist/components/page-header.d.ts +3 -1
- package/dist/components/page-header.js +12 -12
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/pagination.js +3 -3
- package/dist/components/pagination.js.map +1 -1
- package/dist/components/popover.js +18 -23
- package/dist/components/popover.js.map +1 -1
- package/dist/components/prompt-bar.d.ts +16 -0
- package/dist/components/prompt-bar.js +49 -0
- package/dist/components/prompt-bar.js.map +1 -0
- package/dist/components/radio.js +9 -9
- package/dist/components/radio.js.map +1 -1
- package/dist/components/recommendation-card.d.ts +13 -0
- package/dist/components/recommendation-card.js +123 -0
- package/dist/components/recommendation-card.js.map +1 -0
- package/dist/components/resource-list.js +1 -1
- package/dist/components/resource-list.js.map +1 -1
- package/dist/components/responsive-master-detail.js +4 -4
- package/dist/components/responsive-master-detail.js.map +1 -1
- package/dist/components/scroll-area.js +3 -3
- package/dist/components/scroll-area.js.map +1 -1
- package/dist/components/section-rule.d.ts +5 -3
- package/dist/components/section-rule.js +12 -12
- package/dist/components/section-rule.js.map +1 -1
- package/dist/components/segment-control.js +3 -3
- package/dist/components/segment-control.js.map +1 -1
- package/dist/components/select.js +47 -32
- package/dist/components/select.js.map +1 -1
- package/dist/components/sensitive-input.js +17 -19
- package/dist/components/sensitive-input.js.map +1 -1
- package/dist/components/sheet.d.ts +4 -4
- package/dist/components/sheet.js +11 -10
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/sidebar.js +146 -137
- package/dist/components/sidebar.js.map +1 -1
- package/dist/components/skeleton-line.d.ts +1 -1
- package/dist/components/skeleton-line.js +2 -2
- package/dist/components/skeleton-line.js.map +1 -1
- package/dist/components/slider.js +2 -2
- package/dist/components/slider.js.map +1 -1
- package/dist/components/stat-strip.js +5 -4
- package/dist/components/stat-strip.js.map +1 -1
- package/dist/components/switch.js +6 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table-of-contents.js +5 -5
- package/dist/components/table-of-contents.js.map +1 -1
- package/dist/components/table-pager.js +2 -2
- package/dist/components/table-pager.js.map +1 -1
- package/dist/components/table.js +4 -4
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.js +7 -6
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/tag-badge.d.ts +10 -10
- package/dist/components/tag-badge.js +11 -11
- package/dist/components/tag-badge.js.map +1 -1
- package/dist/components/tag-color-picker.d.ts +8 -1
- package/dist/components/tag-color-picker.js +33 -25
- package/dist/components/tag-color-picker.js.map +1 -1
- package/dist/components/text.js +5 -5
- package/dist/components/text.js.map +1 -1
- package/dist/components/theme-toggle.js +1 -1
- package/dist/components/theme-toggle.js.map +1 -1
- package/dist/components/thinking.d.ts +16 -0
- package/dist/components/thinking.js +85 -0
- package/dist/components/thinking.js.map +1 -0
- package/dist/components/toast.js +25 -20
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +6 -5
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle.js +6 -6
- package/dist/components/toggle.js.map +1 -1
- package/dist/components/tool-chips.d.ts +12 -0
- package/dist/components/tool-chips.js +138 -0
- package/dist/components/tool-chips.js.map +1 -0
- package/dist/components/toolbar.js +3 -3
- package/dist/components/toolbar.js.map +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/typeahead-field.js +59 -56
- package/dist/components/typeahead-field.js.map +1 -1
- package/dist/components/upload-queue.js +15 -14
- package/dist/components/upload-queue.js.map +1 -1
- package/dist/models/agent-feedback.d.ts +44 -0
- package/dist/models/agent-feedback.js +28 -0
- package/dist/models/agent-feedback.js.map +1 -0
- package/dist/models/avatar.d.ts +2 -0
- package/dist/models/avatar.js +15 -0
- package/dist/models/avatar.js.map +1 -0
- package/dist/models/code.d.ts +6 -0
- package/dist/models/code.js +24 -0
- package/dist/models/code.js.map +1 -0
- package/dist/models/context-cards.d.ts +13 -0
- package/dist/models/context-cards.js +17 -0
- package/dist/models/context-cards.js.map +1 -0
- package/dist/models/diff-table.d.ts +12 -0
- package/dist/models/diff-table.js +9 -0
- package/dist/models/diff-table.js.map +1 -0
- package/dist/models/recommendation.d.ts +26 -0
- package/dist/models/recommendation.js +23 -0
- package/dist/models/recommendation.js.map +1 -0
- package/dist/styles/base.css +14 -2
- package/dist/styles/standalone.css +1865 -929
- package/dist/styles/tailwind.css +179 -13
- package/dist/styles/tokens.css +679 -20
- package/dist/utils/cn.js +25 -4
- package/dist/utils/cn.js.map +1 -1
- package/dist/utils/control-surface.d.ts +1 -1
- package/dist/utils/control-surface.js +1 -1
- package/dist/utils/control-surface.js.map +1 -1
- package/dist/utils/navigation.d.ts +3 -0
- package/dist/utils/navigation.js +8 -0
- package/dist/utils/navigation.js.map +1 -0
- package/dist/utils/selection-indicator.d.ts +1 -1
- package/dist/utils/selection-indicator.js +49 -72
- package/dist/utils/selection-indicator.js.map +1 -1
- package/dist/utils/use-hover-highlight.d.ts +4 -0
- package/dist/utils/use-hover-highlight.js +80 -0
- package/dist/utils/use-hover-highlight.js.map +1 -0
- package/dist/viewmodels/use-approval-card.d.ts +20 -0
- package/dist/viewmodels/use-approval-card.js +83 -0
- package/dist/viewmodels/use-approval-card.js.map +1 -0
- package/dist/viewmodels/use-chat-composer.d.ts +16 -0
- package/dist/viewmodels/use-chat-composer.js +34 -0
- package/dist/viewmodels/use-chat-composer.js.map +1 -0
- package/dist/viewmodels/use-chat-message.d.ts +16 -0
- package/dist/viewmodels/use-chat-message.js +26 -0
- package/dist/viewmodels/use-chat-message.js.map +1 -0
- package/dist/viewmodels/use-code-block.d.ts +8 -0
- package/dist/viewmodels/use-code-block.js +38 -0
- package/dist/viewmodels/use-code-block.js.map +1 -0
- package/dist/viewmodels/use-context-cards.d.ts +17 -0
- package/dist/viewmodels/use-context-cards.js +18 -0
- package/dist/viewmodels/use-context-cards.js.map +1 -0
- package/dist/viewmodels/use-diff-table.d.ts +16 -0
- package/dist/viewmodels/use-diff-table.js +42 -0
- package/dist/viewmodels/use-diff-table.js.map +1 -0
- package/dist/viewmodels/use-loader.d.ts +4 -0
- package/dist/viewmodels/use-loader.js +22 -0
- package/dist/viewmodels/use-loader.js.map +1 -0
- package/dist/viewmodels/use-recommendation-card.d.ts +17 -0
- package/dist/viewmodels/use-recommendation-card.js +41 -0
- package/dist/viewmodels/use-recommendation-card.js.map +1 -0
- package/dist/viewmodels/use-thinking.d.ts +13 -0
- package/dist/viewmodels/use-thinking.js +18 -0
- package/dist/viewmodels/use-thinking.js.map +1 -0
- package/dist/viewmodels/use-tool-chips.d.ts +10 -0
- package/dist/viewmodels/use-tool-chips.js +20 -0
- package/dist/viewmodels/use-tool-chips.js.map +1 -0
- 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-
|
|
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
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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>
|
|
@@ -388,16 +390,16 @@ Bounded shells use border-box `height/max-height: 100dvh`. Document shells use `
|
|
|
388
390
|
<AppSkipLink>Skip to content</AppSkipLink>
|
|
389
391
|
<AppMain>
|
|
390
392
|
<AppHeader title="Field notes" actions={actions} />
|
|
391
|
-
<div className="flex min-h-0 min-w-0 flex-1 flex-col md:px-
|
|
393
|
+
<div className="flex min-h-0 min-w-0 flex-1 flex-col md:px-basalt-space-lg md:pb-basalt-space-lg">
|
|
392
394
|
<ContentIsland mobileSurface="edge-to-edge">
|
|
393
|
-
<div className="mx-auto max-w-prose px-
|
|
395
|
+
<div className="mx-auto max-w-prose px-basalt-space-lg py-basalt-space-lg md:p-0">{content}</div>
|
|
394
396
|
</ContentIsland>
|
|
395
397
|
</div>
|
|
396
398
|
</AppMain>
|
|
397
399
|
</AppShell>
|
|
398
400
|
```
|
|
399
401
|
|
|
400
|
-
The wrapper intentionally has **no mobile `px-
|
|
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.
|
|
401
403
|
|
|
402
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.
|
|
403
405
|
|
|
@@ -418,45 +420,30 @@ Copyable complete recipes: [mobile layouts](packages/basalt/ai/RECIPES.md#mobile
|
|
|
418
420
|
- shell-derived bounded height (standalone `100dvh`), `flex-col`, `shrink-0`, sticky positioning at the shell-owned safe top
|
|
419
421
|
- expanded width **260px** (inline `width`)
|
|
420
422
|
- collapsed width **68px**
|
|
421
|
-
- collapse animation `transition-
|
|
423
|
+
- collapse animation `transition-[width] duration-basalt-normal ease-basalt` (disabled for reduced motion)
|
|
422
424
|
|
|
423
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.
|
|
424
426
|
|
|
425
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`.
|
|
426
428
|
|
|
427
|
-
###
|
|
429
|
+
### Shared navigation geometry
|
|
428
430
|
|
|
429
|
-
|
|
430
|
-
Sidebar ← collapsed={false}; owns 260px and bounded height
|
|
431
|
-
├── SidebarHeader ← h-14 px-3 already
|
|
432
|
-
│ └── brand row ← flex, items-center, justify-between, w-full
|
|
433
|
-
│ (no extra horizontal padding)
|
|
434
|
-
├── search wrap ← px-3 pb-1 only
|
|
435
|
-
│ └── SidebarSearch
|
|
436
|
-
├── SidebarNav ← pt-1; flex-1
|
|
437
|
-
│ ├── SidebarPartition ← label; already px-6
|
|
438
|
-
│ └── item stack ← flex flex-col gap-0.5 px-3
|
|
439
|
-
│ └── SidebarItem ← already px-3 py-2.5
|
|
440
|
-
└── SidebarFooter ← px-4 py-3 already
|
|
441
|
-
└── SidebarUser
|
|
442
|
-
```
|
|
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.
|
|
443
432
|
|
|
444
|
-
`
|
|
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.
|
|
445
434
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
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.
|
|
449
436
|
|
|
450
437
|
```tsx excerpt:sidebar-expanded-structure
|
|
451
438
|
<Sidebar collapsed={collapsed}>
|
|
452
439
|
<SidebarHeader>
|
|
453
440
|
<div className="flex w-full items-center justify-between">
|
|
454
|
-
<div className="flex min-w-0 items-center gap-
|
|
441
|
+
<div className="flex min-w-0 items-center gap-basalt-space-lg">
|
|
455
442
|
<BasaltMark className="h-5 w-5 shrink-0" />
|
|
456
443
|
<span className="truncate text-lg font-semibold text-basalt-foreground md:text-xl">
|
|
457
444
|
Acme
|
|
458
445
|
</span>
|
|
459
|
-
<span className="shrink-0 rounded-md bg-basalt-secondary px-
|
|
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">
|
|
460
447
|
v{version}
|
|
461
448
|
</span>
|
|
462
449
|
</div>
|
|
@@ -471,14 +458,14 @@ Collapsible sections use `SidebarGroup` instead of Partition + stack. `SidebarGr
|
|
|
471
458
|
</Button>
|
|
472
459
|
</div>
|
|
473
460
|
</SidebarHeader>
|
|
474
|
-
<div className="px-
|
|
461
|
+
<div className="px-basalt-nav-inset">
|
|
475
462
|
<SidebarSearch onClick={() => setSearchOpen(true)}>Search</SidebarSearch>
|
|
476
463
|
</div>
|
|
477
|
-
<SidebarNav
|
|
464
|
+
<SidebarNav>
|
|
478
465
|
{groups.map((group) => (
|
|
479
466
|
<div key={group.label}>
|
|
480
467
|
<SidebarPartition>{group.label}</SidebarPartition>
|
|
481
|
-
<div className="flex flex-col gap-
|
|
468
|
+
<div className="flex flex-col gap-basalt-nav-gap">
|
|
482
469
|
{group.items.map((item) => (
|
|
483
470
|
<SidebarItem
|
|
484
471
|
key={item.href}
|
|
@@ -499,56 +486,48 @@ Collapsible sections use `SidebarGroup` instead of Partition + stack. `SidebarGr
|
|
|
499
486
|
</Sidebar>
|
|
500
487
|
```
|
|
501
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
|
+
|
|
502
491
|
### Collapsed tree
|
|
503
492
|
|
|
504
493
|
Pass `collapsed` on `Sidebar`. The element becomes 68px. Children are icon-sized and centered. Still no inner width/height wrapper.
|
|
505
494
|
|
|
506
|
-
|
|
507
|
-
Sidebar ← collapsed; owns 68px
|
|
508
|
-
├── SidebarHeader ← justify-center px-0
|
|
509
|
-
├── collapse Button ← ghost icon, mb-1
|
|
510
|
-
├── search SidebarIconItem ← mb-2; tooltip
|
|
511
|
-
├── SidebarNav ← w-full items-center gap-1 pt-1
|
|
512
|
-
│ └── SidebarIconItem ← h-10 w-10; tooltip
|
|
513
|
-
└── SidebarFooter ← flex w-full justify-center px-0
|
|
514
|
-
```
|
|
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.
|
|
515
496
|
|
|
516
497
|
```tsx excerpt:sidebar-collapsed-structure
|
|
517
498
|
<Sidebar collapsed={collapsed}>
|
|
518
|
-
<SidebarHeader
|
|
499
|
+
<SidebarHeader>
|
|
519
500
|
<BasaltMark className="h-5 w-5" />
|
|
520
501
|
</SidebarHeader>
|
|
521
502
|
<Button
|
|
522
503
|
variant="ghost"
|
|
523
504
|
size="icon"
|
|
524
|
-
className="mb-
|
|
505
|
+
className="mb-basalt-space-sm self-center"
|
|
525
506
|
onClick={onToggle}
|
|
526
507
|
aria-label="Expand sidebar"
|
|
527
508
|
>
|
|
528
509
|
<PanelLeft aria-hidden="true" />
|
|
529
510
|
</Button>
|
|
530
|
-
<
|
|
531
|
-
<
|
|
532
|
-
<
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
<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>
|
|
545
525
|
{items.map((item) => (
|
|
546
526
|
<Tooltip key={item.href} delayDuration={0}>
|
|
547
527
|
<TooltipTrigger asChild>
|
|
548
528
|
<SidebarIconItem
|
|
549
529
|
active={active(item.href)}
|
|
550
530
|
aria-label={item.label}
|
|
551
|
-
className="self-center"
|
|
552
531
|
onClick={() => navigate(item.href)}
|
|
553
532
|
>
|
|
554
533
|
<item.icon className="h-4 w-4" strokeWidth={1.5} />
|
|
@@ -560,7 +539,7 @@ Sidebar ← collapsed; owns 68px
|
|
|
560
539
|
</Tooltip>
|
|
561
540
|
))}
|
|
562
541
|
</SidebarNav>
|
|
563
|
-
<SidebarFooter
|
|
542
|
+
<SidebarFooter>
|
|
564
543
|
<Tooltip delayDuration={0}>
|
|
565
544
|
<TooltipTrigger asChild>
|
|
566
545
|
<span className="inline-flex">{avatar}</span>
|
|
@@ -605,6 +584,8 @@ Breakpoint: `768px`. Below that, the in-flow rail is omitted. The same sidebar c
|
|
|
605
584
|
|
|
606
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.
|
|
607
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.
|
|
588
|
+
|
|
608
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.
|
|
609
590
|
|
|
610
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.
|
|
@@ -681,7 +662,7 @@ export function AppFrame() {
|
|
|
681
662
|
title={title}
|
|
682
663
|
actions={<ThemeToggle aria-label={`Toggle theme (now ${theme})`} />}
|
|
683
664
|
/>
|
|
684
|
-
<div className="flex min-h-0 flex-1 flex-col px-
|
|
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">
|
|
685
666
|
<ContentIsland>
|
|
686
667
|
<Outlet />
|
|
687
668
|
</ContentIsland>
|
|
@@ -701,7 +682,7 @@ export function AppFrame() {
|
|
|
701
682
|
`AppHeader` inherits root `header` attributes (`HTMLAttributes<HTMLElement>`).
|
|
702
683
|
|
|
703
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`.
|
|
704
|
-
- **`breadcrumbs?: { href?: string; label: ReactNode }[]`**:
|
|
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.
|
|
705
686
|
- **`leading?: ReactNode`**: Optional slot before breadcrumbs, typically the mobile hamburger drawer trigger.
|
|
706
687
|
- **`actions?: ReactNode`**: Top-right framework controls (`ThemeToggle`, account dropdown, etc.). This slot is reserved for framework controls, **not** the page-level create button.
|
|
707
688
|
- **Native & Ref Boundary**: Inherits native `HTMLAttributes<HTMLElement>`, but does not forward ref.
|
|
@@ -714,56 +695,29 @@ Product title and version live in `SidebarHeader`, not in `AppHeader`. Read the
|
|
|
714
695
|
|
|
715
696
|
Login is **not** inside `AppShell`. It is a centered badge on the full viewport.
|
|
716
697
|
|
|
717
|
-
Shape:
|
|
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.
|
|
718
699
|
|
|
719
700
|
```tsx excerpt:login-badge-page
|
|
720
701
|
export function LoginPage() {
|
|
721
702
|
return (
|
|
722
|
-
<div className="relative flex min-h-
|
|
723
|
-
<div
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
>
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
<div className="h-4 w-8 rounded-full bg-basalt-background/80" />
|
|
741
|
-
<div className="flex items-center gap-2">
|
|
742
|
-
<BasaltMark className="h-4 w-4 text-basalt-primary-foreground" />
|
|
743
|
-
<span className="text-sm font-semibold text-basalt-primary-foreground">
|
|
744
|
-
Acme
|
|
745
|
-
</span>
|
|
746
|
-
</div>
|
|
747
|
-
<span className="text-[10px] font-medium tracking-widest text-basalt-primary-foreground/60 uppercase">
|
|
748
|
-
Visitor
|
|
749
|
-
</span>
|
|
750
|
-
</div>
|
|
751
|
-
</div>
|
|
752
|
-
<div className="flex flex-1 flex-col items-center px-6 pt-6 pb-14">
|
|
753
|
-
<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">
|
|
754
|
-
<BasaltMark className="h-10 w-10 text-basalt-muted-foreground" />
|
|
755
|
-
</div>
|
|
756
|
-
<p className="mt-5 text-lg font-semibold text-basalt-foreground">Welcome</p>
|
|
757
|
-
<p className="mt-1 text-xs text-basalt-muted-foreground">Sign in to continue</p>
|
|
758
|
-
<div className="mt-5 h-px w-full bg-basalt-border" />
|
|
759
|
-
<div className="flex-1" />
|
|
760
|
-
<Button variant="secondary" className="w-full rounded-xl py-3">
|
|
761
|
-
Continue
|
|
762
|
-
</Button>
|
|
763
|
-
</div>
|
|
764
|
-
<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">
|
|
765
|
-
<span className="text-[10px] text-basalt-muted-foreground">Secure sign-in</span>
|
|
766
|
-
</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>
|
|
767
721
|
</div>
|
|
768
722
|
</div>
|
|
769
723
|
</div>
|
|
@@ -797,13 +751,15 @@ Boot and route gates use `LoadingScreen` — a centered mark and a 6rem shimmer
|
|
|
797
751
|
|
|
798
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.
|
|
799
753
|
|
|
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
|
|
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.
|
|
801
755
|
|
|
802
|
-
The showcase
|
|
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.
|
|
803
759
|
|
|
804
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.
|
|
805
761
|
|
|
806
|
-
`PageHeader` is flush on `ContentIsland`. Do not wrap
|
|
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.
|
|
807
763
|
|
|
808
764
|
### Create and filters
|
|
809
765
|
|
|
@@ -824,7 +780,7 @@ import { PageHeader } from "@nocoo/basalt/components/page-header";
|
|
|
824
780
|
|
|
825
781
|
export default function ProjectsPage() {
|
|
826
782
|
return (
|
|
827
|
-
<div className="space-y-
|
|
783
|
+
<div className="space-y-basalt-layout-lg">
|
|
828
784
|
<PageHeader
|
|
829
785
|
title="Projects"
|
|
830
786
|
description="Active work in this workspace."
|
|
@@ -854,7 +810,7 @@ import { SectionRule } from "@nocoo/basalt/components/section-rule";
|
|
|
854
810
|
|
|
855
811
|
export default function ProjectsPage() {
|
|
856
812
|
return (
|
|
857
|
-
<div className="space-y-
|
|
813
|
+
<div className="space-y-basalt-layout-lg">
|
|
858
814
|
<PageHeader
|
|
859
815
|
title="Projects"
|
|
860
816
|
description="Active work in this workspace."
|
|
@@ -873,7 +829,7 @@ export default function ProjectsPage() {
|
|
|
873
829
|
}
|
|
874
830
|
/>
|
|
875
831
|
<SectionRule title="Overview" hint="Live totals for the current workspace.">
|
|
876
|
-
<div className="grid grid-cols-2 gap-
|
|
832
|
+
<div className="grid grid-cols-2 gap-basalt-layout">
|
|
877
833
|
<LayerCard>
|
|
878
834
|
<p className="text-xs text-basalt-muted-foreground">Projects</p>
|
|
879
835
|
<p className="text-2xl font-semibold">24</p>
|
|
@@ -972,7 +928,9 @@ Place cards with Grid or flex. Those classes do not paint. A grid of `LayerCard`
|
|
|
972
928
|
|
|
973
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`.
|
|
974
930
|
|
|
975
|
-
`Table` sets `data-basalt-table`. Zebra, hover, and selected paint on `td`, from `--basalt-zebra-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).
|
|
976
934
|
|
|
977
935
|
`DescriptionList` is layout and type only. It is not a surface.
|
|
978
936
|
|
|
@@ -1090,7 +1048,7 @@ export function ProfileForm() {
|
|
|
1090
1048
|
<Switch name="notifications" defaultChecked />
|
|
1091
1049
|
</Field>
|
|
1092
1050
|
|
|
1093
|
-
<div className="flex gap-
|
|
1051
|
+
<div className="flex gap-basalt-space-lg mt-basalt-space-lg">
|
|
1094
1052
|
<Button type="reset" variant="secondary">Reset</Button>
|
|
1095
1053
|
<Button type="submit" variant="default">Save Changes</Button>
|
|
1096
1054
|
</div>
|
|
@@ -1179,7 +1137,7 @@ export function EmptyActionExample() {
|
|
|
1179
1137
|
const [created, setCreated] = useState(false);
|
|
1180
1138
|
|
|
1181
1139
|
return (
|
|
1182
|
-
<LayerCard className="max-w-md
|
|
1140
|
+
<LayerCard className="max-w-md">
|
|
1183
1141
|
{created ? (
|
|
1184
1142
|
<p className="text-sm text-basalt-foreground">Project created successfully.</p>
|
|
1185
1143
|
) : (
|
|
@@ -1245,7 +1203,7 @@ export function ProjectSettingsForm() {
|
|
|
1245
1203
|
};
|
|
1246
1204
|
|
|
1247
1205
|
return (
|
|
1248
|
-
<form onSubmit={handleSubmit(onSubmit)} className="space-y-
|
|
1206
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-basalt-space-lg">
|
|
1249
1207
|
<Field
|
|
1250
1208
|
label="Project Name"
|
|
1251
1209
|
hint="Unique name for your project workspace"
|
|
@@ -1283,12 +1241,12 @@ export function ProjectSettingsForm() {
|
|
|
1283
1241
|
/>
|
|
1284
1242
|
|
|
1285
1243
|
{submittedData && (
|
|
1286
|
-
<div data-testid="submission-output" className="text-sm font-mono p-
|
|
1244
|
+
<div data-testid="submission-output" className="text-sm font-mono p-basalt-space-lg border rounded">
|
|
1287
1245
|
Submitted: {JSON.stringify(submittedData)}
|
|
1288
1246
|
</div>
|
|
1289
1247
|
)}
|
|
1290
1248
|
|
|
1291
|
-
<div className="flex gap-
|
|
1249
|
+
<div className="flex gap-basalt-space-lg">
|
|
1292
1250
|
<Button
|
|
1293
1251
|
type="button"
|
|
1294
1252
|
variant="secondary"
|
|
@@ -1487,6 +1445,7 @@ Basalt charts are composed of responsive frame, legend, and tooltip subsystem pr
|
|
|
1487
1445
|
### StatCard & StatGrid (`@nocoo/basalt/charts/stat-card`)
|
|
1488
1446
|
|
|
1489
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.
|
|
1490
1449
|
- **Props (`StatCardProps`)**:
|
|
1491
1450
|
- `value: string | number` (required): Core metric value. Formatted via `toLocaleString()` if numeric. Overridden when `status` is provided.
|
|
1492
1451
|
- `title?: string` / `label?: string` (optional): Heading text (`title` takes precedence over `label`).
|
|
@@ -1513,6 +1472,8 @@ Basalt charts are composed of responsive frame, legend, and tooltip subsystem pr
|
|
|
1513
1472
|
|
|
1514
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:
|
|
1515
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
|
+
|
|
1516
1477
|
- **Generic Data Binding**:
|
|
1517
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`).
|
|
1518
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.
|
|
@@ -1545,7 +1506,7 @@ export function StandaloneViteApp() {
|
|
|
1545
1506
|
|
|
1546
1507
|
return (
|
|
1547
1508
|
<ThemeProvider>
|
|
1548
|
-
<div className="min-h-screen bg-basalt-background text-basalt-foreground p-
|
|
1509
|
+
<div className="min-h-screen bg-basalt-background text-basalt-foreground p-basalt-space-lg space-y-basalt-space-lg">
|
|
1549
1510
|
<PageHeader
|
|
1550
1511
|
title="Standalone Workspace"
|
|
1551
1512
|
description="Rendered with @nocoo/basalt/styles/standalone without Tailwind preflight"
|
|
@@ -1556,7 +1517,7 @@ export function StandaloneViteApp() {
|
|
|
1556
1517
|
<span className="font-semibold text-basalt-foreground">Project Search</span>
|
|
1557
1518
|
</LayerCard.Header>
|
|
1558
1519
|
<LayerCard.Body>
|
|
1559
|
-
<div className="flex gap-
|
|
1520
|
+
<div className="flex gap-basalt-space-lg max-w-md">
|
|
1560
1521
|
<Input
|
|
1561
1522
|
value={query}
|
|
1562
1523
|
onChange={(e) => setQuery(e.target.value)}
|
|
@@ -1633,7 +1594,7 @@ export default function NextClientDashboardPage() {
|
|
|
1633
1594
|
|
|
1634
1595
|
return (
|
|
1635
1596
|
<ThemeProvider>
|
|
1636
|
-
<div className="p-
|
|
1597
|
+
<div className="p-basalt-space-lg space-y-basalt-space-lg">
|
|
1637
1598
|
<div className="flex justify-between items-center">
|
|
1638
1599
|
<h2 className="text-xl font-bold text-basalt-foreground">Next.js Client Dashboard</h2>
|
|
1639
1600
|
<ThemeToggle aria-label="Toggle theme mode" />
|
|
@@ -1646,7 +1607,7 @@ export default function NextClientDashboardPage() {
|
|
|
1646
1607
|
onChange={(e) => setMetricName(e.target.value)}
|
|
1647
1608
|
aria-label="Metric Name"
|
|
1648
1609
|
/>
|
|
1649
|
-
<div className="mt-
|
|
1610
|
+
<div className="mt-basalt-space-lg">
|
|
1650
1611
|
<Button variant="outline" onClick={() => setMetricName("Default Metric")}>
|
|
1651
1612
|
Reset Metric
|
|
1652
1613
|
</Button>
|
|
@@ -1706,12 +1667,12 @@ export function RoutedNavigationSection() {
|
|
|
1706
1667
|
return (
|
|
1707
1668
|
<MemoryRouter initialEntries={["/dashboard"]}>
|
|
1708
1669
|
<LinkProvider render={ReactRouterLinkAdapter}>
|
|
1709
|
-
<nav className="flex gap-
|
|
1670
|
+
<nav className="flex gap-basalt-space-lg p-basalt-space-lg border-b border-basalt-border">
|
|
1710
1671
|
<NavigationItem href="/dashboard">Dashboard</NavigationItem>
|
|
1711
1672
|
<NavigationItem href="/settings">Settings</NavigationItem>
|
|
1712
1673
|
<NavigationItem href="https://docs.hexly.ai">External Docs</NavigationItem>
|
|
1713
1674
|
</nav>
|
|
1714
|
-
<div className="p-
|
|
1675
|
+
<div className="p-basalt-space-lg">
|
|
1715
1676
|
<Routes>
|
|
1716
1677
|
<Route path="/dashboard" element={<DashboardPage />} />
|
|
1717
1678
|
<Route path="/settings" element={<SettingsPage />} />
|
|
@@ -1767,7 +1728,7 @@ export function UserPreferencesForm() {
|
|
|
1767
1728
|
};
|
|
1768
1729
|
|
|
1769
1730
|
return (
|
|
1770
|
-
<form onSubmit={handleSubmit} onReset={handleReset} className="space-y-
|
|
1731
|
+
<form onSubmit={handleSubmit} onReset={handleReset} className="space-y-basalt-space-lg max-w-sm">
|
|
1771
1732
|
<Field label="Username" hint="Public profile handle">
|
|
1772
1733
|
<Input name="username" defaultValue="johndoe" required />
|
|
1773
1734
|
</Field>
|
|
@@ -1782,7 +1743,7 @@ export function UserPreferencesForm() {
|
|
|
1782
1743
|
</div>
|
|
1783
1744
|
)}
|
|
1784
1745
|
|
|
1785
|
-
<div className="flex gap-
|
|
1746
|
+
<div className="flex gap-basalt-space-lg">
|
|
1786
1747
|
<Button type="reset" variant="secondary">
|
|
1787
1748
|
Reset
|
|
1788
1749
|
</Button>
|
|
@@ -1812,3 +1773,93 @@ export function UserPreferencesForm() {
|
|
|
1812
1773
|
}}
|
|
1813
1774
|
/>
|
|
1814
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.
|