Multi-select
Token display: chips + "+N more" overflow, optional inline filter
Open · Space toggles a row · Backspace removes the last · Enter confirms.
Count summary: Select all / Clear all in the header
The header states the running total and offers bulk actions.
Max-selection limit: unchecked rows lock at the cap
Select all fills up to the cap; the rest disable until you remove one.
Disabled: inert, still legible
Inert, but the chosen values stay readable.
Edited the selection 0 times. 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.)
const LABELS = [ { value: 'bug', label: 'bug', secondary: 'Defect to fix', status: 'error' }, { value: 'feature', label: 'feature', secondary: 'New capability', status: 'info' }, { value: 'design', label: 'design', secondary: 'Design framework', status: 'success' }, { value: 'wontfix', label: 'wontfix', secondary: 'Locked', status: 'idle', disabled: true },];
export function LabelPicker() { const [labels, setLabels] = useState<string[]>(['bug', 'feature']); return ( <MultiSelect label="Labels" options={LABELS} value={labels} onChange={setLabels} display="tokens" // removable chips in the field (or "count" → "N selected") maxTokens={3} // collapse the rest to "+N more" max={5} // cap: unchecked rows lock once reached filterable // optional inline filter (secondary, not the headline) placeholder="Add labels…" /> );}// Android: Jetpack Compose (canonical). No reusable NockerlMultiSelect ships yet;// the multi-select pattern is checkbox rows bound to a MutableSet<String>, exactly// as in chat/ui/AskUserQuestionSheet.kt. The brand checkbox + chip vocabulary:import androidx.compose.material3.Checkboximport androidx.compose.material3.CheckboxDefaultsimport com.nockerl.app.core.ui.NockerlChipimport com.nockerl.app.core.theme.LocalNockerlColors
val selected = remember { mutableStateMapOf<Int, MutableSet<String>>() }val chosen = selected.getOrPut(index) { mutableSetOf() }
// Each option is a Row that owns the click; the Checkbox is decorative// (onCheckedChange = null) and tinted to the brand accent.Row( modifier = Modifier.fillMaxWidth().clickable { if (chosen.contains(option.label)) chosen.remove(option.label) else chosen.add(option.label) selected[index] = chosen // trigger recomposition }, verticalAlignment = Alignment.CenterVertically,) { Checkbox( checked = chosen.contains(option.label), onCheckedChange = null, colors = CheckboxDefaults.colors(checkedColor = LocalNockerlColors.current.accentPrimary), ) Text(option.label)}
// Chosen values surface as removable chips (the token row):NockerlChip(text = "bug", onClick = { chosen.remove("bug") }, selected = true)// macOS: SwiftUI. No formal multi-select ships; it composes the shipped Chip +// FlowLayout token field with a checkmark option list (NockerlVoice/UI/// SettingsComponents.swift + the checkmark idiom). This is the intended assembly.@State private var selected: Set<String> = ["bug", "feature"]
// The trigger field: chosen values as removable token chips, wrapped by FlowLayout.FlowLayout(spacing: 6) { ForEach(Array(selected), id: \.self) { value in Chip(text: value) { selected.remove(value) } // cyan capsule + ✕ }}
// The dropdown: checkable option rows (selection = a cyan checkmark, never a glow).ForEach(options, id: \.value) { option in Button { if selected.contains(option.value) { selected.remove(option.value) } else { selected.insert(option.value) } } label: { HStack { Text(option.label) Spacer() if selected.contains(option.value) { Image(systemName: "checkmark").foregroundStyle(NockerlTheme.accent) } } } .buttonStyle(.plain)}Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
label * | string | Persistent label above the trigger. Never a placeholder. | |
options * | MultiSelectOption[] | Full set. Each: value, label, optional secondary / status / disabled. | |
value * | string[] | The selected set (controlled). | |
onChange * | (next: string[]) => void | Emits the next selected set on toggle, select-all, clear-all, or token removal. | |
display | 'tokens' | 'count' | 'tokens' | tokens = removable chips in the field; count = a "N selected" summary pill. |
maxTokens | number | 3 | Tokens shown before collapsing the rest to +N more (tokens mode). |
max | number | Selection cap. Unchecked rows disable once reached; select-all fills up to it. | |
filterable | boolean | false | Show the optional inline filter input. Secondary: search is the Combobox’s headline, not this control’s. |
placeholder | string | Shown in the field when nothing is selected. | |
helperText | string | Supporting line under the trigger. Replaced by a warm limit message at the cap. | |
disabled | boolean | false | Inert and clearly seen (never faded to invisible); chosen values stay readable. |
No reusable NockerlMultiSelect composable ships yet. The pattern below is the real
multi-select in chat/ui/AskUserQuestionSheet.kt; the parameters are the primitives
it composes (Material3 Checkbox + NockerlChip).
| Parameter | Type | Default | Description |
|---|---|---|---|
selections * | MutableMap<Int, MutableSet<String>> | The selected set per question. Toggling a row adds/removes the option from its MutableSet. | |
checked * | Boolean | Checkbox arg: whether this option is in the set. The parent Row owns the click. | |
onCheckedChange | ((Boolean) -> Unit)? | null | Passed null so the box is decorative; the surrounding Row.clickable mutates the set. |
colors | CheckboxColors | checkedColor = accentPrimary | Brand override via CheckboxDefaults.colors. The checked fill is the cyan accent. |
text * | String | NockerlChip arg: a chosen value rendered as a removable token. | |
selected | Boolean | false | NockerlChip arg: a chosen token renders selected (solid cyan). |
onClick * | () -> Unit | NockerlChip arg: remove the value from the set. |
No formal multi-select ships. It composes the shipped Chip + FlowLayout (the token
field) with a checkmark option list. State is a Set<String> toggled per row.
| Style | Type | Default | Description |
|---|---|---|---|
Set<String> | @State | The selection model. Insert / remove on each row tap; the source of truth for chips + checkmarks. | |
Chip(text:onRemove:) | View | A chosen value as a removable token: cyan capsule (logoMid → accentDark gradient) + an xmark button. | |
FlowLayout(spacing:) | Layout | Wraps the chosen-value chips across lines inside the trigger field. | |
Image(systemName: "checkmark") | View | The selected mark on an option row, tinted NockerlTheme.accent (#0CC0DF). Selection is never a glow. | |
SegmentedSelector | View | Ships for single selection; stands in where the choice is mutually exclusive, not a set. |