ChipSelect
import { ChipSelect } from '@hanzo/ui/product'
A chip over a short list
The whole list is given, and the search is applied in the chip.
Default
export function Environment() {const [place, setPlace] = useState<ChipItem>(PLACES[0])return (<XStack pt={180}><ChipSelect name="Environment" icon={<Cloud size={12} />} label={place.label} chosen={place} onChange={setPlace} items={PLACES} placeholder="Search environments…" /></XStack>)}
Paged from a loader
Each page answers a cursor for the next, and reaching the end asks for it.
Pick one
export function Paged() {const all = Array.from({ length: 60 }, (_, i) => ({ id: `item-${i}`, label: `item ${i}` }))const [chosen, setChosen] = useState<ChipItem | null>(null)return (<XStack pt={320}><ChipSelect name="Item" label={chosen?.label ?? 'Pick one'} chosen={chosen} onChange={setChosen} load={async (q, after) => { const hit = all.filter((i) => i.label.includes(q)) const start = after ? Number(after) : 0 const next = start + 20 < hit.length ? String(start + 20) : null return { items: hit.slice(start, start + 20), next } }} footer="Twenty at a time. Type to search." /></XStack>)}
The quiet look
For a choice in a row of words, like a composer's model.
Zen 5
export function Quiet() {const models: ChipItem[] = [{ id: 'zen-5', label: 'Zen 5' },{ id: 'zen-5-mini', label: 'Zen 5 Mini' },]const [model, setModel] = useState(models[0])return (<XStack pt={120}><ChipSelect name="Model" label={model.label} chosen={model} onChange={setModel} items={models} quiet /></XStack>)}
Types
export type ChipPlacement = 'top-start' | 'top' | 'top-end' | 'bottom-start' | 'bottom' | 'bottom-end'
export interface ChipAction<T extends ChipItem> {label: stringonPress: (item: T) => void/** Rows the action applies to. Absent, every row. */when?: (item: T) => boolean}
export interface ChipSelectProps<T extends ChipItem> {/** What the chip says — the current choice, short. */label: string/** Drawn before the label. */icon?: ReactNode/** What the chip chooses ("Repository"): its accessible name is `<name>: <label>`. */name: string/** The chosen item, pinned first with a check. */chosen?: T | nullonChange: (item: T) => void/** Pages of rows. Takes precedence over `items`. */load?: ChipLoad<T>/** The whole list, searched here. */items?: readonly T[]/** With `items`: there are more rows than these. */more?: boolean/** With `items`: the reader reached the end — fetch and grow `items`. */onMore?: () => void/** With `items`: a fetch is in flight. */loading?: boolean/** With `items`: the last fetch failed, in a sentence. */error?: string | null/** Search debounce, ms. */delay?: number/** The search field's placeholder. */placeholder?: string/** No rows match. */empty?: ReactNode/** Under the list, above the search — what is missing and why. */footer?: ReactNode/** Above the list — a call to action that replaces a list that cannot load. */cta?: ReactNode/*** A second thing a row can do. Drawn on the row under the cursor or the* pointer; the pointer presses it, and the keyboard runs it with Ctrl/⌘+Enter* on the row under the cursor. It is a declared action rather than a slot of* arbitrary content on purpose: a listbox option may not CONTAIN a control* (axe `nested-interactive`), so the row keeps its option semantics and the* action rides on it.*/action?: ChipAction<T>placement?: ChipPlacement/** Panel width, px. */width?: number/** The list's ceiling, px; past it the list scrolls. */height?: numberdisabled?: boolean/*** The plain look: no fill and no hairline, the label in the muted ink — for a* choice that sits in a row of words (a composer's model and effort) rather* than in a row of chips. Same panel, same keyboard.*/quiet?: boolean/** Open state, when the host owns it. */open?: booleanonOpenChange?: (open: boolean) => void}
Exports
ChipSelecttype ChipActiontype ChipPlacementtype ChipSelectProps