@nocoo/basalt 2.2.0 → 3.1.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 +398 -0
- package/ai/INTEGRATION.md +255 -129
- package/ai/RECIPES.md +51 -26
- package/ai/USAGE.md +4 -4
- package/ai/registry.json +3018 -271
- 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 +14 -2
- package/dist/components/data-table.js +185 -135
- 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 +151 -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 +149 -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.d.ts +39 -2
- package/dist/components/table.js +48 -17
- 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 +1901 -943
- package/dist/styles/tailwind.css +180 -13
- package/dist/styles/tokens.css +681 -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 +82 -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 +28 -26
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. Sidebar destinations and search use 8px horizontal/vertical padding, an 8px icon gap and a 22px line box for a 38px reference row. This is local to Sidebar: editable/folder rows, horizontal navigation links and menus retain 6px vertical padding and 34px rows. 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. Sidebar hover appears at the actual pointer row and disappears there on leave or over a gap/disclosure label; it never launches from or returns to the active route or stale pointer focus. Only continuous row-to-row hover animates. Keyboard focus remains independent. NavigationMenuList retains its focus/selection-following behavior. 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 a 22px line box, 8px insets and 12px group separation. 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 keeps the 34px control width and derives its 38px minimum height from the sidebar line box and padding. 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.
|
|
755
|
+
|
|
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).
|
|
801
757
|
|
|
802
|
-
The
|
|
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
|
|
|
@@ -1063,6 +1021,12 @@ Stack one `SectionRule` per region. Live recipe: `/layout`. Catalog: `/ui/page-h
|
|
|
1063
1021
|
|
|
1064
1022
|
Basalt form controls participate in standard forms and controlled state pipelines.
|
|
1065
1023
|
|
|
1024
|
+
Keep the Field's label/control/hint gap at 8px. Compose related fields with
|
|
1025
|
+
`space-y-basalt-layout` or `gap-basalt-layout` (16px); separate sections with the
|
|
1026
|
+
24px `layout-lg` role. The form owns the group gap, not each Field's margin.
|
|
1027
|
+
Action buttons keep their 8px internal gap. DialogFooter owns 16px separation
|
|
1028
|
+
from preceding content, so callers do not need a repeated margin override.
|
|
1029
|
+
|
|
1066
1030
|
### Working Native HTML Form & FormData Pattern
|
|
1067
1031
|
|
|
1068
1032
|
Standard inputs (`Input`, `InputArea`, `Checkbox`, `Radio`, `Switch`) reliably supply their values via standard `FormData(event.currentTarget)`:
|
|
@@ -1081,7 +1045,7 @@ export function ProfileForm() {
|
|
|
1081
1045
|
};
|
|
1082
1046
|
|
|
1083
1047
|
return (
|
|
1084
|
-
<form onSubmit={handleSubmit}>
|
|
1048
|
+
<form onSubmit={handleSubmit} className="space-y-basalt-layout">
|
|
1085
1049
|
<Field label="Email" hint="Your primary email address">
|
|
1086
1050
|
<Input name="email" type="email" required />
|
|
1087
1051
|
</Field>
|
|
@@ -1090,7 +1054,7 @@ export function ProfileForm() {
|
|
|
1090
1054
|
<Switch name="notifications" defaultChecked />
|
|
1091
1055
|
</Field>
|
|
1092
1056
|
|
|
1093
|
-
<div className="flex gap-
|
|
1057
|
+
<div className="flex gap-basalt-space-lg">
|
|
1094
1058
|
<Button type="reset" variant="secondary">Reset</Button>
|
|
1095
1059
|
<Button type="submit" variant="default">Save Changes</Button>
|
|
1096
1060
|
</div>
|
|
@@ -1179,7 +1143,7 @@ export function EmptyActionExample() {
|
|
|
1179
1143
|
const [created, setCreated] = useState(false);
|
|
1180
1144
|
|
|
1181
1145
|
return (
|
|
1182
|
-
<LayerCard className="max-w-md
|
|
1146
|
+
<LayerCard className="max-w-md">
|
|
1183
1147
|
{created ? (
|
|
1184
1148
|
<p className="text-sm text-basalt-foreground">Project created successfully.</p>
|
|
1185
1149
|
) : (
|
|
@@ -1245,7 +1209,7 @@ export function ProjectSettingsForm() {
|
|
|
1245
1209
|
};
|
|
1246
1210
|
|
|
1247
1211
|
return (
|
|
1248
|
-
<form onSubmit={handleSubmit(onSubmit)} className="space-y-
|
|
1212
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-basalt-layout">
|
|
1249
1213
|
<Field
|
|
1250
1214
|
label="Project Name"
|
|
1251
1215
|
hint="Unique name for your project workspace"
|
|
@@ -1283,12 +1247,12 @@ export function ProjectSettingsForm() {
|
|
|
1283
1247
|
/>
|
|
1284
1248
|
|
|
1285
1249
|
{submittedData && (
|
|
1286
|
-
<div data-testid="submission-output" className="text-sm font-mono p-
|
|
1250
|
+
<div data-testid="submission-output" className="text-sm font-mono p-basalt-space-lg border rounded">
|
|
1287
1251
|
Submitted: {JSON.stringify(submittedData)}
|
|
1288
1252
|
</div>
|
|
1289
1253
|
)}
|
|
1290
1254
|
|
|
1291
|
-
<div className="flex gap-
|
|
1255
|
+
<div className="flex gap-basalt-space-lg">
|
|
1292
1256
|
<Button
|
|
1293
1257
|
type="button"
|
|
1294
1258
|
variant="secondary"
|
|
@@ -1487,6 +1451,7 @@ Basalt charts are composed of responsive frame, legend, and tooltip subsystem pr
|
|
|
1487
1451
|
### StatCard & StatGrid (`@nocoo/basalt/charts/stat-card`)
|
|
1488
1452
|
|
|
1489
1453
|
- **`StatCard`**: High-level KPI and telemetry presentation card supporting incremental interactive slots and status state transitions.
|
|
1454
|
+
- **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
1455
|
- **Props (`StatCardProps`)**:
|
|
1491
1456
|
- `value: string | number` (required): Core metric value. Formatted via `toLocaleString()` if numeric. Overridden when `status` is provided.
|
|
1492
1457
|
- `title?: string` / `label?: string` (optional): Heading text (`title` takes precedence over `label`).
|
|
@@ -1513,6 +1478,8 @@ Basalt charts are composed of responsive frame, legend, and tooltip subsystem pr
|
|
|
1513
1478
|
|
|
1514
1479
|
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
1480
|
|
|
1481
|
+
`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.
|
|
1482
|
+
|
|
1516
1483
|
- **Generic Data Binding**:
|
|
1517
1484
|
- `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
1485
|
- `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 +1512,7 @@ export function StandaloneViteApp() {
|
|
|
1545
1512
|
|
|
1546
1513
|
return (
|
|
1547
1514
|
<ThemeProvider>
|
|
1548
|
-
<div className="min-h-screen bg-basalt-background text-basalt-foreground p-
|
|
1515
|
+
<div className="min-h-screen bg-basalt-background text-basalt-foreground p-basalt-space-lg space-y-basalt-space-lg">
|
|
1549
1516
|
<PageHeader
|
|
1550
1517
|
title="Standalone Workspace"
|
|
1551
1518
|
description="Rendered with @nocoo/basalt/styles/standalone without Tailwind preflight"
|
|
@@ -1556,7 +1523,7 @@ export function StandaloneViteApp() {
|
|
|
1556
1523
|
<span className="font-semibold text-basalt-foreground">Project Search</span>
|
|
1557
1524
|
</LayerCard.Header>
|
|
1558
1525
|
<LayerCard.Body>
|
|
1559
|
-
<div className="flex gap-
|
|
1526
|
+
<div className="flex gap-basalt-space-lg max-w-md">
|
|
1560
1527
|
<Input
|
|
1561
1528
|
value={query}
|
|
1562
1529
|
onChange={(e) => setQuery(e.target.value)}
|
|
@@ -1633,7 +1600,7 @@ export default function NextClientDashboardPage() {
|
|
|
1633
1600
|
|
|
1634
1601
|
return (
|
|
1635
1602
|
<ThemeProvider>
|
|
1636
|
-
<div className="p-
|
|
1603
|
+
<div className="p-basalt-space-lg space-y-basalt-space-lg">
|
|
1637
1604
|
<div className="flex justify-between items-center">
|
|
1638
1605
|
<h2 className="text-xl font-bold text-basalt-foreground">Next.js Client Dashboard</h2>
|
|
1639
1606
|
<ThemeToggle aria-label="Toggle theme mode" />
|
|
@@ -1646,7 +1613,7 @@ export default function NextClientDashboardPage() {
|
|
|
1646
1613
|
onChange={(e) => setMetricName(e.target.value)}
|
|
1647
1614
|
aria-label="Metric Name"
|
|
1648
1615
|
/>
|
|
1649
|
-
<div className="mt-
|
|
1616
|
+
<div className="mt-basalt-space-lg">
|
|
1650
1617
|
<Button variant="outline" onClick={() => setMetricName("Default Metric")}>
|
|
1651
1618
|
Reset Metric
|
|
1652
1619
|
</Button>
|
|
@@ -1706,12 +1673,12 @@ export function RoutedNavigationSection() {
|
|
|
1706
1673
|
return (
|
|
1707
1674
|
<MemoryRouter initialEntries={["/dashboard"]}>
|
|
1708
1675
|
<LinkProvider render={ReactRouterLinkAdapter}>
|
|
1709
|
-
<nav className="flex gap-
|
|
1676
|
+
<nav className="flex gap-basalt-space-lg p-basalt-space-lg border-b border-basalt-border">
|
|
1710
1677
|
<NavigationItem href="/dashboard">Dashboard</NavigationItem>
|
|
1711
1678
|
<NavigationItem href="/settings">Settings</NavigationItem>
|
|
1712
1679
|
<NavigationItem href="https://docs.hexly.ai">External Docs</NavigationItem>
|
|
1713
1680
|
</nav>
|
|
1714
|
-
<div className="p-
|
|
1681
|
+
<div className="p-basalt-space-lg">
|
|
1715
1682
|
<Routes>
|
|
1716
1683
|
<Route path="/dashboard" element={<DashboardPage />} />
|
|
1717
1684
|
<Route path="/settings" element={<SettingsPage />} />
|
|
@@ -1767,7 +1734,7 @@ export function UserPreferencesForm() {
|
|
|
1767
1734
|
};
|
|
1768
1735
|
|
|
1769
1736
|
return (
|
|
1770
|
-
<form onSubmit={handleSubmit} onReset={handleReset} className="space-y-
|
|
1737
|
+
<form onSubmit={handleSubmit} onReset={handleReset} className="space-y-basalt-layout max-w-sm">
|
|
1771
1738
|
<Field label="Username" hint="Public profile handle">
|
|
1772
1739
|
<Input name="username" defaultValue="johndoe" required />
|
|
1773
1740
|
</Field>
|
|
@@ -1782,7 +1749,7 @@ export function UserPreferencesForm() {
|
|
|
1782
1749
|
</div>
|
|
1783
1750
|
)}
|
|
1784
1751
|
|
|
1785
|
-
<div className="flex gap-
|
|
1752
|
+
<div className="flex gap-basalt-space-lg">
|
|
1786
1753
|
<Button type="reset" variant="secondary">
|
|
1787
1754
|
Reset
|
|
1788
1755
|
</Button>
|
|
@@ -1812,3 +1779,162 @@ export function UserPreferencesForm() {
|
|
|
1812
1779
|
}}
|
|
1813
1780
|
/>
|
|
1814
1781
|
```
|
|
1782
|
+
|
|
1783
|
+
## Agent feedback
|
|
1784
|
+
|
|
1785
|
+
`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.
|
|
1786
|
+
|
|
1787
|
+
- `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.
|
|
1788
|
+
- `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.
|
|
1789
|
+
- `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.
|
|
1790
|
+
- Catalog examples simulate progress in application ViewModels. Their copyable snippets include those demo ViewModels, rather than importing private catalog paths.
|
|
1791
|
+
|
|
1792
|
+
### Dense controls and moving list highlights
|
|
1793
|
+
|
|
1794
|
+
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.
|
|
1795
|
+
|
|
1796
|
+
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.
|
|
1797
|
+
|
|
1798
|
+
Loader uses eight perimeter cells with an empty center, moving clockwise continuously. Label, shimmer and delayed elapsed-time controls are unchanged.
|
|
1799
|
+
|
|
1800
|
+
## Dimension tokens
|
|
1801
|
+
|
|
1802
|
+
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.
|
|
1803
|
+
|
|
1804
|
+
| Role | Token | Default |
|
|
1805
|
+
| --- | --- | --- |
|
|
1806
|
+
| Small / default / large control | `--basalt-size-control-sm`, `--basalt-size-control`, `--basalt-size-control-lg` | 28 / 34 / 40px |
|
|
1807
|
+
| Compact / standard / display icon | `--basalt-size-icon-sm`, `--basalt-size-icon`, `--basalt-size-icon-lg` | 12 / 14 / 16px |
|
|
1808
|
+
| Nested action | `--basalt-size-control-inset` | 28px |
|
|
1809
|
+
| Minimum touch target | `--basalt-size-touch` | 44px |
|
|
1810
|
+
| Menu option | `--basalt-size-menu-row`, `--basalt-space-menu-inset`, `--basalt-space-menu-x`, `--basalt-space-menu-y` | 34px / 8px / 8px / 6px |
|
|
1811
|
+
| Card padding | `--basalt-space-card-sm`, `--basalt-space-card`, `--basalt-space-card-lg`, `--basalt-space-card-xl` | 12 / 16 / 24 / 32px; default 16px |
|
|
1812
|
+
| 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 |
|
|
1813
|
+
| Field label gap | `--basalt-space-field-gap` | 8px |
|
|
1814
|
+
| Textarea minimum height | `--basalt-size-textarea-sm`, `--basalt-size-textarea`, `--basalt-size-textarea-lg` | 64 / 80 / 96px |
|
|
1815
|
+
| Table row / horizontal / vertical inset | `--basalt-size-table-row`, `--basalt-space-table-x`, `--basalt-space-table-y` | 36 / 8 / 6px |
|
|
1816
|
+
| Attached table outer edge | `--basalt-space-table-edge` | Panel horizontal inset, 16px |
|
|
1817
|
+
|
|
1818
|
+
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.
|
|
1819
|
+
|
|
1820
|
+
### Compact record tables
|
|
1821
|
+
|
|
1822
|
+
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.
|
|
1823
|
+
|
|
1824
|
+
Use `column.textAlign: "end"` for comparable numbers: the header and values share
|
|
1825
|
+
a logical end rail, with the sort glyph inward of the header label. `"start"` is
|
|
1826
|
+
the other explicit direction; omitted body-cell alignment remains inherited.
|
|
1827
|
+
The same `textAlign` prop is available on TableHead, TableCell and
|
|
1828
|
+
TableSortableHeader from `@nocoo/basalt/components/table`. It does not replace
|
|
1829
|
+
native HTML `align` attributes. Logical directions follow LTR/RTL consistently.
|
|
1830
|
+
|
|
1831
|
+
TableSortableHeader is a flush, ref-forwarding button, not a generic Button with
|
|
1832
|
+
extra padding. Pass its `sorted` direction (`"asc"` / `"desc"`, or omitted) and
|
|
1833
|
+
sort callback; the enclosing TableHead owns `aria-sort`. Unsortable headers remain
|
|
1834
|
+
plain text, not disabled sort controls. DataTable handles those details for its
|
|
1835
|
+
own columns.
|
|
1836
|
+
|
|
1837
|
+
For a DataTable directly attached to a LayerCard, set `edge="attached"`. It uses
|
|
1838
|
+
the 16px panel rail only at the outermost cells, including optional number/select
|
|
1839
|
+
columns and full-width state rows. Intercolumn spacing stays 8px per cell side.
|
|
1840
|
+
Built-in pagination shares that panel rail and its 12px bottom inset.
|
|
1841
|
+
The default `edge="inset"` keeps ordinary table spacing inside an already padded
|
|
1842
|
+
region; do not add a zero-padding Body around an attached table.
|
|
1843
|
+
|
|
1844
|
+
```tsx compile:integration-attached-table
|
|
1845
|
+
import { DataTable, type DataTableColumn } from "@nocoo/basalt/components/data-table";
|
|
1846
|
+
import { LayerCard } from "@nocoo/basalt/components/layer-card";
|
|
1847
|
+
|
|
1848
|
+
type Invoice = { id: string; amount: number };
|
|
1849
|
+
const columns: DataTableColumn<Invoice>[] = [
|
|
1850
|
+
{ id: "id", header: "Invoice", accessor: (row) => row.id },
|
|
1851
|
+
{ id: "amount", header: "Amount", accessor: (row) => row.amount, textAlign: "end" },
|
|
1852
|
+
];
|
|
1853
|
+
|
|
1854
|
+
export function InvoicePanel() {
|
|
1855
|
+
return (
|
|
1856
|
+
<LayerCard>
|
|
1857
|
+
<LayerCard.Header>Invoices</LayerCard.Header>
|
|
1858
|
+
<DataTable
|
|
1859
|
+
aria-label="Invoices"
|
|
1860
|
+
edge="attached"
|
|
1861
|
+
columns={columns}
|
|
1862
|
+
data={[{ id: "INV-01", amount: 1240 }, { id: "INV-02", amount: 80 }]}
|
|
1863
|
+
getRowId={(row) => row.id}
|
|
1864
|
+
/>
|
|
1865
|
+
</LayerCard>
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1870
|
+
Native Table does not infer a grid from its children. For an attached native
|
|
1871
|
+
composition, mark the true inline-start cells with `ps-basalt-table-edge`, the
|
|
1872
|
+
inline-end cells with `pe-basalt-table-edge`, and a full-width cell with both.
|
|
1873
|
+
Rows continuing a preceding `rowSpan` do not gain a new edge at their first DOM
|
|
1874
|
+
child. These token utilities keep cell backgrounds, sticky headers and separators
|
|
1875
|
+
full width without affecting nested tables. Use ordinary Table cells when no
|
|
1876
|
+
panel edge is being attached.
|
|
1877
|
+
|
|
1878
|
+
TableCaption owns balanced `--basalt-space-panel-x` / `--basalt-space-panel-y`
|
|
1879
|
+
insets (16px horizontal / 12px vertical), not an outside top margin. Keep it
|
|
1880
|
+
inside the native table; do not pad the entire scroll region or compensate with
|
|
1881
|
+
an extra footer wrapper. This preserves full-width row paint and readable space
|
|
1882
|
+
above and below the description inside an enclosing frame.
|
|
1883
|
+
|
|
1884
|
+
### Recommendations
|
|
1885
|
+
|
|
1886
|
+
`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.
|
|
1887
|
+
|
|
1888
|
+
### Retrieved context
|
|
1889
|
+
|
|
1890
|
+
`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.
|
|
1891
|
+
|
|
1892
|
+
### Proposed tabular changes
|
|
1893
|
+
|
|
1894
|
+
`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.
|
|
1895
|
+
|
|
1896
|
+
### Record numbering and identity
|
|
1897
|
+
|
|
1898
|
+
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.
|
|
1899
|
+
|
|
1900
|
+
## AI conversation primitives
|
|
1901
|
+
|
|
1902
|
+
`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.
|
|
1903
|
+
|
|
1904
|
+
`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.
|
|
1905
|
+
|
|
1906
|
+
### Conversation workspace example
|
|
1907
|
+
|
|
1908
|
+
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.
|
|
1909
|
+
|
|
1910
|
+
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.
|
|
1911
|
+
|
|
1912
|
+
### Composite alignment
|
|
1913
|
+
|
|
1914
|
+
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.
|
|
1915
|
+
|
|
1916
|
+
These are local alignment rails, not one universal left edge. Page headings align
|
|
1917
|
+
to the island; card slots align to their panel; columns align their own labels and
|
|
1918
|
+
values. A checkbox, row number or optional status glyph has a consistent semantic
|
|
1919
|
+
slot. Distinct boundaries may add spacing, but the same boundary has one owner.
|
|
1920
|
+
See [alignment hierarchy](DESIGN.md#alignment-hierarchy). Keep nested preview focus
|
|
1921
|
+
clearance and give wide table examples enough columns rather than shrinking all
|
|
1922
|
+
their padding to fit a thumbnail.
|
|
1923
|
+
|
|
1924
|
+
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.
|
|
1925
|
+
|
|
1926
|
+
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.
|
|
1927
|
+
|
|
1928
|
+
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.
|
|
1929
|
+
|
|
1930
|
+
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.
|
|
1931
|
+
|
|
1932
|
+
### Code panels
|
|
1933
|
+
|
|
1934
|
+
`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.
|
|
1935
|
+
|
|
1936
|
+
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.
|
|
1937
|
+
|
|
1938
|
+
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.
|
|
1939
|
+
|
|
1940
|
+
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.
|