Floating pills
In reviewLive · web
Session chips bar + scroll-to-bottom pill, floating over a scrolling feed
Pulled the SessionChipsBar concept: a flat LazyRow of keycaps.
Make the active one lift higher, no glow.
Done. A soft cyan selection wash + a higher shadow tier.
And the context line under each chip?
Cyan → amber → red by usage. No number, just the line.
Scroll up and the jump-to-bottom pill appears.
…and hides once you reach the bottom.
Each pill is a real, keyboard-reachable button.
Switched session 0 times · active gateway. The island is live.
// Web: SHIPPED in @dizyx/nockerl-react (promoted from this page's canon, task 2654).// NockerlSessionChipsBar floats a scroll-fading cluster of keycaps OVER content,// distinct from the inline NockerlChip. Positioning is HOST-owned (place it// absolutely over the feed, top or bottom, exactly like native).import { NockerlContextChip, NockerlSessionChip, NockerlSessionChipsBar } from '@dizyx/nockerl-react';
<NockerlSessionChipsBar ariaLabel="Sessions" onAdd={createSession}> {sessions.map((s) => ( <NockerlSessionChip key={s.slug} name={s.name} engineGlyph={s.engine === 'nockerl' ? chipGlyph : cloudGlyph} // engine identity, shape-coded dot={s.dot} // status-dot priority: attention > streaming > unread > active > idle ratio={s.ratio} // 0..1 fills the inner context line; null = a faint empty track active={s.slug === active} mark={checkGlyph} // the cyan selection check, shown only while active onSelect={() => selectSession(s.slug)} /> ))}</NockerlSessionChipsBar>
// The same pill, read-only, for an inline strip (the Context gauge right rail):<NockerlContextChip engineGlyph={cloudGlyph} name="docs site" ratio={0.42} />// Android: Jetpack Compose (canonical). com.nockerl.app.chat.ui.SessionChipsBar// The shipped floating-pills surface: a flat, horizontally-scrollable LazyRow of filled// session keycaps + a trailing cyan "+" CTA, floating over the chat feed (NO surrounding// pill, NO border). Each keycap carries a status dot, engine glyph, name, and an under-cap// context line; the active keycap takes a brighter chromeActive fill at a HIGHER elevation.SessionChipsBar( sessions = subSessions, activeSessionSlug = activeSlug, onSessionSelect = viewModel::selectSession, onCreateSession = viewModel::createSession, onRenameSession = viewModel::rename, onEditSession = viewModel::editProvider, onClearContextSession = viewModel::clearContext, onDeleteSession = viewModel::delete, onForkSession = viewModel::fork, streamStates = streamStates, // per-slug StreamState → the status dots contextRatios = contextRatios, // per-slug 0..1 → the under-cap context line engineAccent = engineAccent, // harness accent for the LOW (healthy) band)
// The floating scroll-to-bottom FAB lives in ChatScreen.kt: a circular Surface// (CircleShape) on chromeSurface with an engine-accent border, lifted at// NockerlElevation.Level3, shown only while the feed is scrolled up.// macOS: SwiftUI (canonical). NockerlVoice/UI/RecordingHUD.swift// Voice ships a SINGLE floating status pill (not a tappable row): a Capsule on// chromeSurface with an accent stroke + tinted drop shadow, floating centered in an// NSPanel over whatever has focus. Its width springs as the phase content cross-fades.private var pill: some View { HStack(spacing: 11) { NockerlLogo().frame(width: 18, height: 16) // the fixed left anchor Rectangle().fill(NockerlTheme.divider).frame(width: 1, height: 22) content // recording / transcribing / result / error .transition(.opacity) .id(state.phase.transitionID) } .padding(.horizontal, 14) .padding(.vertical, 9) .background(NockerlTheme.chromeSurface, in: Capsule()) .overlay(Capsule().strokeBorder(borderColor, lineWidth: 1.5)) .shadow(color: .black.opacity(0.4), radius: 14, y: 5) .animation(.spring(response: 0.42, dampingFraction: 0.82), value: state.phase)}Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
children * | ReactNode | The keycaps: NockerlSessionChip children, rendered inside the fading scroller. | |
onAdd | (() => void) | undefined | Renders the trailing cyan "+" CTA (the real NockerlIconButton filled-circle) and wires it. | |
addLabel | string | 'New session' | Accessible label for the "+" CTA. |
ariaLabel | string | 'Sessions' | Accessible name for the bar (role="group"). |
className | string | ||
style | CSSProperties |
| Prop | Type | Default | Description |
|---|---|---|---|
active * | boolean | Selected keycap: the sanctioned cyan selection tint + higher lift + aria-pressed. | |
dot * | NockerlSessionChipDot | Leading status dot from the priority ladder. attention/streaming pulse. | |
engineGlyph * | ReactNode | The engine identity glyph (an <NockerlIcon>, currentColor). | |
name * | string | The session name (ellipsized). Carries the accessible name. | |
mark | ReactNode | Optional selection mark (an accent check) shown only while active. | |
ratio * | number | null | Context-usage ratio 0..1 → the inner line (cyan → amber → red); null = a faint empty track. | |
onSelect * | () => void | ||
className | string | ||
style | CSSProperties | ||
ariaLabel | string |
| Prop | Type | Default | Description |
|---|---|---|---|
engineGlyph * | ReactNode | The engine identity glyph (an <NockerlIcon>, currentColor). | |
name * | string | The session name (ellipsized). | |
ratio * | number | null | Context-usage ratio 0..1 → the inner line; null = a faint empty track. | |
tooltip | string | Native hover tooltip. Named tooltip because title means heading-text everywhere else in the family. | |
ariaLabel | string |
The scroll-to-bottom key on this page is the site-level _ScrollToBottom composition
(a lifted circle + the same floating border + an unread-count badge), not yet a
package export.
| Parameter | Type | Default | Description |
|---|---|---|---|
sessions * | List<SubSession> | The sub-sessions rendered as keycaps. Blank / duplicate slugs are filtered; an empty list hides the bar. | |
activeSessionSlug * | String | The selected sub-session slug → the brighter chromeActive keycap at the higher lift. | |
onSessionSelect * | (String) -> Unit | Invoked with a slug when a keycap is tapped. | |
onCreateSession * | () -> Unit | Invoked when the trailing cyan “+” keycap is tapped. | |
onRenameSession / onEditSession / onClearContextSession / onDeleteSession / onForkSession * | (SubSession) -> Unit | Long-press context-menu actions (rename / delete hidden on the primary; fork gated to the Cloud Agent harness). | |
modifier | Modifier | Modifier | Outer modifier; the row fills max width. |
streamStates | Map<String, StreamState> | emptyMap() | Per-slug stream state driving each keycap’s status dot. |
contextRatios | Map<String, Float> | emptyMap() | Per-slug context-usage ratio (0..1) → the under-cap context line. A slug ABSENT from the map renders an empty track. |
engineAccent | Color? | null | Accent for the LOW (healthy) context band, the active harness accent. Falls back to accentPrimary. |
SwiftUI ships a single floating pill (the recording HUD status pill), not a tappable
cluster. It’s a Capsule on chromeSurface with an accent stroke, lifted by a tinted drop
shadow and centered in a floating NSPanel. There is no scrolling row, session keycap, or
suggestion-pill type on this platform yet (see drift note).
| Style | Type | Default | Description |
|---|---|---|---|
HUDState.phase | HUDPhase | Drives the pill content + border color: recording / transcribing / result(pasted:) / error(message:). The pill width springs as the content cross-fades. | |
.background(_:in: Capsule()) | ShapeStyle | The pill fill: NockerlTheme.chromeSurface (the shared floating-chrome material). | |
.overlay(Capsule().strokeBorder(…)) | Color | Accent stroke at 1.5pt: accent normally, warning on an error phase. | |
.shadow(color:radius:y:) | Shadow | The tinted drop shadow that lifts the pill off the focused app behind it. |