Sessions: tab / click a row to select (one cyan indicator)
Settings: value rows, a disabled row, and a trailing control (toggle = its own target)
Push notificationsAlerts when a session needs you
Messages: tap a row to expand (chevron rotates down, then up); tap again to collapse
Accordion variant shipped. The trailing chevron now rotates down-to-up and the body reveals via an interpolated grid-row track, not a display swap. Selection moved to the leading slot.
A push to main is queued behind your sign-off. Warm tones read as status only; the leading mark stays a status color and never borrows the brand cyan.
The Docker build failed at the typecheck step. Expand to read the tail of the log; collapse to keep the list dense. Reduced-motion drops the height transition.
Selected session: s2 · push notifications on · open message: m1. The island is live.
Collapsible / accordion variant
Pass expandable (with expanded + onToggle + a details body) to turn a row into a
collapsible accordion, the pattern the native Android messages list uses: tap a row to
reveal its message body, tap again to collapse. Key behaviours:
Affordance: the trailing chevron points down at rest and rotates up when
expanded (the canonical tree disclosure, made interpolatable), not the right-pointing
chevron, which stays reserved for genuinely navigational rows.
Reveal: the body is a sibling role="region" below the row, revealed via an
interpolated grid-template-rows: 0fr → 1fr + opacity, never a display or fill swap;
prefers-reduced-motion drops the transition.
A11y: the row stays one <button> carrying aria-expanded + aria-controls; the
region is aria-labelledby the primary text and hidden while collapsed.
Selection vs expansion: selection now renders its cyan check in the leading slot
(a cyan edge bounding the run, cyan ink, and a leading mark), so selection and the trailing
nav/expand chevron no longer compete for the one trailing slot.
Primary text. Carries the row's accessible name (label.large role).
secondary
string
n/a
Optional supporting line under the primary text (body.small role).
status
NockerlListItemStatus
n/a
Optional leading status mark: renders the canonical status ICON (shape + color dual-coding, never a bare dot) in the status color. Status colors only, never cyan.
leadingIcon
React.ReactNode
n/a
Optional leading ICON (any node) for action rows (share / rename / delete). Shown in the leading slot when the row is neither selected nor carrying a status mark.
danger
boolean
false
Destructive treatment. The primary text + leading icon go status-error (a delete row).
value
string
n/a
Optional trailing value text (e.g. a count, a timestamp).
trailing
React.ReactNode
n/a
Optional trailing NODE (a NockerlBadge / NockerlChip / status pill) shown before the chevron. For rich rows: an accordion header's tone pill, a tool-card's state chip. Compose a real component (NockerlBadge / NockerlStatusDisc / NockerlChip), never a hand-rolled facsimile.
chevron
boolean
false
Show a trailing NAV chevron (right-pointing, for genuinely navigational rows).
expandable
boolean
false
Make the row an accordion: trailing chevron points DOWN at rest, rotates UP when expanded; the row gets aria-expanded + aria-controls and reveals a body region below it. Mutually exclusive with the nav chevron.
expanded
boolean
false
Expanded state for an expandable row; drives the chevron rotation + body reveal.
onToggle
() => void
n/a
Toggle handler for an expandable row (whole-row tap). Ignored while disabled.
details
React.ReactNode
n/a
The body of an expandable row, revealed when the row expands.
selected
boolean
false
Selected state: a cyan edge bounding the run, cyan ink, and a LEADING check. NOT a glow, not a left-rail.
disabled
boolean
false
Inert + clearly-seen (never invisible) state.
onSelect
() => void
n/a
Activate handler (whole-row tap). Ignored while disabled.
Also accepts the native <button> attributes (e.g. onClick, disabled, id, aria-*, data-*), forwarded straight through, plus a forwarded ref.
NockerlListItemRow is the row scaffold; it applies no height, padding, background, or
click of its own. Callers supply those via modifier, so each list keeps its own anatomy.
The leading mark is the canonical NockerlLeadingStatusMark.
Parameter
Type
Default
Description
leadingMark *
@Composable -> Unit
n/a
The leading status mark slot, typically a NockerlLeadingStatusMark.
modifier
Modifier
Modifier
Outer modifier: the caller supplies height, padding, background, and click here (the row adds none).
trailing
(@Composable -> Unit)?
null
Optional trailing content pinned after the body (e.g. a chevron).
content *
RowScope.() -> Unit
n/a
The row body, laid out in a RowScope between the leading mark and any trailing content.
icon *
ImageVector
n/a
NockerlLeadingStatusMark: the status icon (from a semantic statusIcon mapping).
tint *
Color
n/a
NockerlLeadingStatusMark: the status color (a semantic statusColor token). Never a raw Color(0x…) or the brand cyan.
contentDescription *
String?
n/a
NockerlLeadingStatusMark: accessibility label for the status; null only when an adjacent label already names it.
size
Dp
LeadingMarkSize (18.dp)
NockerlLeadingStatusMark: mark size; defaults to the canonical shared 18dp.
SwiftUI (Voice) has no extracted list-item component. Rows are composed inline from
an HStack whose label Button (.buttonStyle(.plain)) is the whole-row tap target, plus
trailing NockerlIconButtons. There is no parameterized API to table; these are the inline
building blocks the shipped HistoryView row uses. Tracked as a conformance item below.
Style
Type
Default
Description
Button(.plain)
ButtonStyle
n/a
Wraps the row content (leading badge + text) so the whole row is one tap target with one accessible name; .contentShape(Rectangle()) makes the full width tappable.
ProviderBadge
View
n/a
The leading mark, a small capsule tag tinted by NockerlTheme.accent / a status color (the inline stand-in for NockerlLeadingStatusMark).
NockerlIconButton
View
n/a
A trailing CONTROL (copy / delete) that is a separate focusable target from the row, with its own help / accessibility label.
Rectangle (hairline)
View
n/a
A 1pt NockerlTheme.hairline bottom overlay separates rows; depth is the wrapping card, not a per-row shadow.
Cross-platform drift (tracked for reconciliation)
The grammar is shared; these are the real divergences between the shipped platforms, tracked
in the Review queue:
No Swift component: Voice builds rows inline in each view (HistoryView.row(_:));
there is no NockerlListItemRow equivalent. Status, spacing, and the leading mark are
re-implemented per row, exactly the drift the Compose grammar exists to prevent. Extracting
a shared Swift row is the open conformance item.
Leading mark shape(resolved): the leading mark is an
icon (shape + color dual-coding), never a bare colored dot. Compose ships the icon at
18dp; web now renders it inside the NockerlListItem primitive (pass status, get the
icon by default; no wrapper needed). Voice (a colored dot / capsule badge today) is the
remaining conformance item.
Selection model: the one selection expression is a cyan edge bounding the run, cyan
ink and a check, never a left stripe and never a wash. An earlier draft used a soft cyan
wash; design law 6 (state by outline, not fill) superseded that, and the web row now
carries the outline. The web row exposes an explicit selected; the Compose scaffold (a caller
modifier background) still fills, so Android is the platform that lags here, and Voice has no
persistent selection at all (it toggles expanded).
Trailing control: web makes the trailing toggle a separate role="switch"; Voice uses
NockerlIconButtons; Compose leaves it to the row’s trailing slot.