Bottom sheet
Detent · grip · content: set them, then open the sheet
Sheet opened 1 time. The drag handle, a scrim tap, or Esc dismisses it (no X by default; flip Close on to add the opt-in X). The island is live.
// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform;// this is the canonical API the published @dizyx/nockerl-react package exposes.)//// No `showClose` → NO X (Material canon): the drag handle + a scrim tap + Esc dismiss it.// Actions compose the real NockerlListItem primitive (canonical rows); a form CTA composes NockerlButton// (light-300 / UPPERCASE / -0.03em ramp), never a hand-rolled <button>.// NockerlBottomSheet composes the shipped NockerlOverlay primitive (scrim + focus-trap + Esc); it// supplies the pull-up panel + its slide. NockerlListItem ships today.import { NockerlBottomSheet, NockerlListItem } from '@dizyx/nockerl-react';
export function SessionOptions({ open, stage, onDismiss }: SheetProps) { return ( <NockerlBottomSheet open={open} onDismiss={onDismiss} stage={stage} title="nockerl-design · docs" detent="half"> <NockerlListItem primary="Share session" leadingIcon={<ShareIcon />} onSelect={share} /> <NockerlListItem primary="Rename" leadingIcon={<RenameIcon />} onSelect={rename} /> <NockerlListItem primary="Delete session" leadingIcon={<TrashIcon />} danger onSelect={remove} /> </NockerlBottomSheet> );}// (com.dizyx.nockerl.design on GitHub Packages Maven) wraps Material3 ModalBottomSheet:// a cardAlt → canvasAlt gradient ground, a palette scrim, and NockerlSheetGrip (the drag// handle hosted inside the body, dragHandle = null).//// Material canon: NO close (X) button. The DRAG HANDLE + scrim tap + back/Esc ARE the// dismissal. Do NOT add an X in the header by default; expose one only opt-in (a rare// persistent-affordance case), mirroring the web `showClose` prop.import androidx.compose.material3.rememberModalBottomSheetStateimport com.dizyx.nockerl.design.components.NockerlBottomSheet
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
NockerlBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState,) { // content composes in the sheet's ColumnScope (grip + gradient are provided; no X) Column(Modifier.fillMaxWidth().padding(horizontal = 20.dp).padding(bottom = 24.dp)) { Text("nockerl-design · docs", style = MaterialTheme.typography.titleLarge) NockerlButton("Share session", onClick = ::share, modifier = Modifier.fillMaxWidth()) NockerlButton("Delete session", onClick = ::delete, variant = NockerlButtonVariant.DESTRUCTIVE) }}macOS/Swift has no bottom-sheet idiom (use a popover / sheet there), so this component targets web + Android only.
Session-management views (configs, not components)
Section titled “Session-management views (configs, not components)”Per the component-vs-view rule on the agent-console epic (task 2654), the session
sheets are pure configurations of this sheet + shipped parts; none of them is a
new component. Native cites them as NockerlBottomSheet call-sites already
(AvatarSettingsSheet, SamplingAdvancedSettings); the settings form in the demo
above is exactly this shape:
| View | Sheet body composes | Notes |
|---|---|---|
| Session create / edit | TextField (name) + Select (harness/model) + Switch rows + SegmentedControl |
the demo’s settings form, with a full-width confirm |
| Session switcher / picker | ListItem rows (selection = the ratified wash + check, never a rail) |
rename / delete / fork confirms hand off to Dialog |
| Override controls | Form layout sections over the same field primitives | per-session provider overrides |
| Streaming target | RadioGroup or ListItem selection |
one target, wash + check |
| Sampling advanced | Slider + RangeSlider + Stepper + Switch |
the native SamplingAdvancedSettings call-site |
Approval moments inside a sheet use the Approval content
anatomy. Its CTA row pins in this sheet’s footer slot.
Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
open * | boolean | Whether the sheet is presented. | |
onDismiss * | () => void | Dismiss handler: scrim tap, Esc, or (when opted-in) the close button. | |
title * | string | Accessible title shown in the header (carries the dialog name). | |
subtitle | string | Optional supporting line under the title. | |
leadingIcon | React.ReactNode | Optional LEADING ICON shown top-left beside the title (e.g. a settings cog next to "Settings"). A plain functional glyph on the on-plane ink, NOT a status disc, NOT cyan. It aligns to the title line; omit for the default text-only header. The header divider is unchanged either way (explicitly neutral, never cyan). | |
detent | SheetDetent | 'half' | Detent: a half-height peek or a near-full panel. |
grip | boolean | true | Show the drag grip at the top. |
showClose | boolean | false | OPT-IN close (X) button. Material canon: a bottom sheet has NO X by default. The drag handle + scrim-tap + Esc ARE the dismissal. Set true only when a persistent, always-visible dismiss affordance is genuinely warranted. Default false. |
stage * | HTMLElement | null | The contained stage element that gates rendering so the sheet never escapes it. | |
footer | React.ReactNode | PINNED FOOTER-ACTION BAR (task 2653): an approve/deny row that NEVER scrolls away, rendered below the scrolling body on the sheet's own plane, with a hairline top edge and an upward scroll-under cue when more body remains beneath it. Compose the ratified CTA grammar in here (outline confirm · ghost cancel · destructive outline-red, the Dialog r5 canon). Empty = no footer, byte-identical sheet. | |
onBack | (() => void) | undefined | NESTED-SHEET BACK NAVIGATION (task 2653): when set, the header leads with a back chevron (a real NockerlIconButton) BEFORE the title. View swapping stays with the host (swap title/children on your own state); this is the affordance + a11y. | |
backLabel | string | 'Back' | Accessible name for the back affordance. Default "Back". |
children * | React.ReactNode | Sheet body. |
| Parameter | Type | Default | Description |
|---|---|---|---|
onDismissRequest * | () -> Unit | Invoked when the sheet is dismissed (scrim tap or swipe down). | |
modifier | Modifier | Modifier | Outer modifier forwarded to the underlying ModalBottomSheet. |
sheetState | SheetState | rememberModalBottomSheetState(skipPartiallyExpanded = true) | The sheet state. Pass skipPartiallyExpanded = false for a half / full partial detent. |
contentWindowInsets | @Composable -> WindowInsets | BottomSheetDefaults.windowInsets | Insets applied to the content. Pass WindowInsets(0) for full-bleed. |
content * | @Composable ColumnScope.() -> Unit | Body laid out in the sheet's ColumnScope; the grip + gradient ground are provided by the wrapper. |