Accordion / expander
Single-open: opening one closes the rest
Per-session overrides. Leave untouched to inherit the registry defaults for this model + provider.
Inaccessible.
Multi-open: each section toggles independently
✓ 142 modules · 2.4s
Lazily fetched on first expand, collapsed by default so a long transcript never floods the card.
Read 4 files · searched 2 sources · drafted summary.
Pushing to main needs your sign-off. Warm tones (here, REVIEW) are status only, never a decorative accent.
Header accessory + uncontrolled: always-visible delete, each self-manages
Open me and delete me independently. The trash button never toggles the row, and no host holds my open state (defaultExpanded seeds it, I keep it).
Open me and delete me independently. The trash button never toggles the row, and no host holds my open state (defaultExpanded seeds it, I keep it).
Zebra (task 2763, experimental): opt-in alternating row tone; default stays plain
Alternating rows carry a whisper of neutral wash that is theme-following, opt-in, and beneath the hairlines.
This row is the toned one (even).
Plain again (odd).
Toned (even).
Single-open: sampling · multi-open: 1 section expanded. The island is live.
// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform. Spec + live demo,// not yet exported from @dizyx/nockerl-react; promotion is tracked.)import { useState } from 'react';
export function SessionAdvanced() { // Single-open: opening one closes the rest (Voice `expandedID` semantics). const [open, setOpen] = useState<string | null>('sampling'); return ( <Accordion mode="single" value={open} onValueChange={setOpen}> <AccordionItem id="sampling" title="Sampling" hint="Temperature, top-p, penalties" icon={<TuneIcon />} pill={{ text: 'MODIFIED', tone: 'accent' }} > <SamplingSliders /> </AccordionItem> <AccordionItem id="workspace" title="Workspace" hint="Locked by your administrator" disabled> <WorkspaceFields /> </AccordionItem> </Accordion> );}// Android (Jetpack Compose, canonical). The packaged NockerlAccordion: trailing// rotating chevron, `mode` single/multiple, per-item headerAccessory + controlled// expansion. headerAccessory is declared BEFORE content so the trailing// lambda still binds to the body.val expanded = remember { mutableStateOf(setOf("drafts")) } // controlled
NockerlAccordion( items = listOf( NockerlAccordionItem( id = "drafts", title = "Drafts", headerAccessory = { // always-visible, never toggles Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { NockerlBadge(text = "3", tone = NockerlBadgeTone.ACCENT) NockerlIconButton(icon = Icons.Filled.Delete, contentDescription = "Delete drafts", onClick = ::deleteDrafts) } }, ) { Text("Three unsaved drafts.") }, ), mode = NockerlAccordionMode.SINGLE, expanded = expanded.value, // host owns the open-id set… onExpandedChange = { expanded.value = it }, // …drive it to auto-open a new item modifier = Modifier.fillMaxWidth(),)// macOS / iOS: SwiftUI, from the NockerlDesign package (the parity twin;// Voice's hand-rolled VocabRow retires on adoption). Trailing rotating chevron,// `mode` single/multiple, a per-item headerAccessory (always-visible, never toggles)// + controlled expansion via a Set binding the host owns.import NockerlDesign
@State private var openIDs: Set<String> = ["drafts"] // controlled; drive to auto-open
NockerlAccordion( items: [ NockerlAccordionItem( id: "drafts", title: "Drafts", headerAccessory: { // left of the chevron; independent taps NockerlIconButton(systemName: "trash", label: "Delete drafts", tint: .destructive) { deleteDrafts() } } ) { Text("Three unsaved drafts.").nockerlType(.bodySmall) }, ], mode: .single, expanded: $openIDs // add a term → openIDs.insert(id) opens it)Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
mode | 'single' | 'multiple' | 'single' | Disclosure policy. single closes siblings on open (Voice); multiple lets sections stack open (Android). |
value | string | string[] | null | Controlled open id(s). A string | null in single mode, a string[] in multiple mode. | |
onValueChange | (value) => void | Fired on toggle with the next open id(s). Pair with value for a controlled accordion. | |
AccordionItem.id * | string | Stable identity used by value to track which item is open. | |
AccordionItem.title * | string | Header label: the disclosure's accessible name (label.large role). | |
AccordionItem.hint | string | Optional supporting line under the title (body.small role). | |
AccordionItem.icon | ReactNode | Optional leading glyph rendered in a fixed slot, so titles align with or without it. | |
AccordionItem.pill | { text: string; tone: 'accent' | 'success' | 'warning' } | Optional trailing header pill. accent for 'modified', warm tones for status only. | |
AccordionItem.headerAccessory | ReactNode | Always-visible header controls pinned to the trailing edge, left of the chevron. Rendered outside the toggle button so a delete IconButton / count Badge fires independently and never toggles the row. Reserves trailing space via --nk-acc-accessory-reserve (default 64px). Matches the headerAccessory seat from FormSection / TodoWidget. | |
AccordionItem.expanded | boolean | Controlled open state. When set, the item is controlled. Pair with onExpandedChange. | |
AccordionItem.onExpandedChange | (next: boolean) => void | Fired with the requested next open state on toggle (controlled). | |
AccordionItem.defaultExpanded | boolean | false | Initial open state for uncontrolled usage: the item self-manages when neither expanded nor onExpandedChange is given. |
AccordionItem.disabled | boolean | false | Inert and clearly seen (never faded to invisible); header is not focusable. |
Compose ships the disclosure as a composed pattern (a clickable header Row + an
AnimatedVisibility body), not a single NockerlAccordion component. These are the
real parameters of that pattern.
| Parameter | Type | Default | Description |
|---|---|---|---|
expanded * | Boolean | Hoisted open state (var expanded by remember { mutableStateOf(false) }). Stack rows for a multi-open group. | |
onToggle * | () -> Unit | Header clickable handler that flips expanded. | |
visible * | Boolean | AnimatedVisibility(visible = expanded) gates the body. | |
enter | EnterTransition | expandVertically() + fadeIn() | Reveal animation. The shipped combo grows height while fading in. |
exit | ExitTransition | shrinkVertically() + fadeOut() | Collapse animation, the inverse of enter. |
imageVector | ImageVector | KeyboardArrowDown / KeyboardArrowUp | Trailing chevron; swapped (not rotated) on toggle, right-aligned via Modifier.weight(1f) on the title. |
contentDescription * | String | Chevron a11y label: "Expand …" / "Collapse …". |
SwiftUI ships the disclosure as a header Button toggling a single @State expandedID
with a withAnimation spring, the body gated by if expandedID == id
(SettingsView.swift · VocabRow, HistoryView.swift). There is no NockerlAccordion
style yet.
| Style | Type | Default | Description |
|---|---|---|---|
expandedID | @State UUID? | The single open id (nil = all collapsed). Single-open by construction: assigning a new id collapses the previous. | |
isExpanded | Bool | Per-row expandedID == id; selects the chevron glyph and gates the body. | |
onToggle | () -> Void | Header button action; runs the toggle inside withAnimation. | |
withAnimation | .spring(response: 0.3, dampingFraction: 0.8) | Springy, choreographed layout change as the body inserts / removes. | |
Image(systemName:) | "chevron.right" / "chevron.down" | Trailing chevron with a glyph swap on toggle (right → down), leading the row in the shipped layout. |