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.

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.

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.

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: string
onPress: (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 | null
onChange: (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?: number
disabled?: 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?: boolean
onOpenChange?: (open: boolean) => void
}

Exports

ChipSelecttype ChipActiontype ChipPlacementtype ChipSelectProps