UNPKG

alouette

Version:

A modern, customizable design system built on top of NativeWind v5 with configurable defaults

199 lines (163 loc) 9.15 kB
# Choice inputs — select, autocomplete and single-select groups ## Select One value picked from a list, without typing. Options are data here (`options`, an array of `{ label, value, disabled? }`) rather than composed children — that is what separates it from the radio families below, whose children are JSX. ```tsx import { Select } from "alouette"; <Select aria-label="Fruit" placeholder="Pick a fruit" options={[ { label: "Apple", value: "apple" }, { label: "Durian (sold out)", value: "durian", disabled: true }, ]} defaultValue="apple" onValueChange={setFruit} />; ``` The value is controllable (`value` / `defaultValue` / `onValueChange`), and it also takes `accent`, `disabled` and `placeholder` (shown until something is selected). It renders no label element, so `aria-label` or `aria-labelledby` is required — that is the accessible name of the `combobox` it exposes on both platforms. The trigger is the `InputText` outlined material at the 44px touch height and needs no styling. Its presentation is per platform and needs no handling either: web renders a real `<select>` (the OS picker and its keyboard handling), native opens a `Popover` listbox over an `InteractiveBox` trigger. Inside a `FormField`, bind it as `value={field.value}` `onValueChange={field.onChange}` `aria-labelledby={labelId}`. It takes no `ref` and no `onBlur`, so those two `field` bindings have nowhere to go — pressing the label does not focus it. ## InputTextAutocomplete A text field backed by a filtered listbox — use it when the user types to narrow a known list. `Select` above stays the right choice when the value is picked from the list without typing. ```tsx import { InputTextAutocomplete } from "alouette"; <InputTextAutocomplete aria-label="Fruit" placeholder="Search a fruit..." options={[ { label: "Apple", value: "apple" }, { label: "Banana", value: "banana" }, { label: "Durian (sold out)", value: "durian", disabled: true }, ]} onValueChange={setFruit} />; ``` Two values, each independently controllable: the **selection** (`value` / `defaultValue` / `onValueChange`, called with `""` when cleared) and the **text** (`inputValue` / `defaultInputValue` / `onInputValueChange`). `defaultInputValue` falls back to the label of the initially selected option, so a caller that only controls the selection still starts on the right text. Options are filtered by a case-insensitive substring match on `label`; pass `filterOption(option, inputValue)` for anything else. `emptyLabel` (default `"No result"`) is announced in place of the list when nothing matches. It also takes `accent`, `disabled`, `mode` (the `InputText` modes) and `aria-label` / `aria-labelledby` — it renders no label element of its own, so one of the two is what names both the field and the listbox. The presentation is per platform and needs no handling: web anchors the menu under the field with full keyboard navigation, while native opens a sheet holding the editable input (a `Modal` resigns the keyboard behind it, so the field left in the layout is a read-only trigger). ## Single-select groups Three families share one API: the group owns the value (`value`, `defaultValue`, `onValueChange`, plus `accent` and `disabled`), children are composed rather than passed as an options array, and a child's own `disabled` affects only that option. Label the group via `aria-labelledby`. - `RadioGroup` + `Radio` — circle-dot list, for longer or self-evident options. - `RadioButtonGroup` + `RadioButton` — segmented pill bar: a lowered 44px track, each pressable filling the tap target around a shorter visible chip. It is a form input; for the same material used to move between destinations or switch views use `NavBar` / `Tabs` (alouette-navigation/SKILL.md). `compact` tightens each chip's horizontal padding so more options fit on one row — the 44px tap target is unchanged. `variant="icon"` makes it a pill of square icon-only chips: the option renders its `icon` alone and its `label` stays the accessible name. A `RadioButton` also takes `activeIcon` (the duotone twin, swapped in while hovered/focused/pressed and for as long as it is checked) with `activeAccent` to tint it, an `indicator` badge over the chip's top-right (icon groups only, and it adds to `icon` rather than replacing it — a badge has no accessible name, so spell the state out in `label`), and its own `onPress`, which replaces the group's `onValueChange` and therefore requires a controlled group. - `RadioCardGroup` + `RadioCard` — cards with `icon`, `label`, `description` and a radio indicator, for options that need explaining. Group `variant` is the card material, `PressableBox`'s `"contained"` (default) or `"outlined"`: every card shares it, the selected card takes the accent and the others `accent="neutral"`. Group `layout` is `"list"` (default, one per row) or `"stack"` (cards wrap and share a row from a 240px basis); both the group and each card take a `className` for layout (widths, wrapping), not for restyling the card material. ```tsx <RadioGroup defaultValue="week" onValueChange={setRange} aria-labelledby={labelId}> <Radio value="day" label="Day" /> <Radio value="month" label="Month" disabled /> </RadioGroup> <RadioButtonGroup compact defaultValue="week" accent="brand"> <RadioButton value="day" label="Day" /> <RadioButton value="week" label="Week" /> </RadioButtonGroup> <RadioCardGroup layout="stack" defaultValue="public" onValueChange={setVisibility}> <RadioCard value="public" icon={<GlobeRegularIcon />} label="Public" description="Anyone with the link" /> <RadioCard value="private" icon={<LockRegularIcon />} label="Private" /> </RadioCardGroup> <RadioButtonGroup variant="icon" aria-label="View" defaultValue="list"> <RadioButton value="list" label="List" icon={<ListRegularIcon />} activeIcon={<ListDuotoneIcon />} /> <RadioButton value="grid" label="Grid" icon={<SquaresFourRegularIcon />} /> </RadioButtonGroup> ``` ## Multi-select groups The same three families with checkboxes: the group owns `values` (`string[]`), `defaultValues` and `onValuesChange(values)`, and each child toggles its `value` in or out. Roles are `group` + `checkbox`/`aria-checked`; label the group with `aria-label` or `aria-labelledby`. - `CheckboxGroup` + `Checkbox` — square-check list. - `CheckboxButtonGroup` + `CheckboxButton` — the segmented bar, several chips raised at once. Same `compact`, `variant="icon"`, `activeIcon`, `activeAccent` and `indicator` as `RadioButton`; no per-item `onPress`. - `CheckboxCardGroup` + `CheckboxCard` — the cards, same `layout` and `variant` as `RadioCardGroup`: every checked card takes the accent. A `Checkbox` rendered outside a `CheckboxGroup` is a standalone boolean: `checked`, `defaultChecked`, `onValueChange(checked)` and its own `accent`. Inside a group it requires `value` and ignores those. `CheckboxButton` and `CheckboxCard` throw outside their groups. ```tsx <CheckboxGroup defaultValues={["email"]} onValuesChange={setChannels} aria-labelledby={labelId}> <Checkbox value="email" label="Email" /> <Checkbox value="sms" label="SMS" /> </CheckboxGroup> <Checkbox label="I accept the terms" checked={accepted} onValueChange={setAccepted} /> <CheckboxButtonGroup compact defaultValues={["mon", "fri"]}> <CheckboxButton value="mon" label="Mon" /> <CheckboxButton value="fri" label="Fri" /> </CheckboxButtonGroup> ``` ## ColorModePicker The ready-made light/dark control built on that icon pill, over a stored `ColorModePreference` (`"light" | "dark" | "system"`). It reports the choice only — the app applies it, passing `useResolvedColorMode(preference)` to a `ScopedTheme` (alouette-theming/SKILL.md). ```tsx import { ColorModePicker, ScopedTheme, useResolvedColorMode } from "alouette"; const [preference, setPreference] = useState<ColorModePreference>("system"); <ScopedTheme theme={useResolvedColorMode(preference)}> <ColorModePicker value={preference} onValueChange={setPreference} /> </ScopedTheme>; ``` `variant="system-lock"` (the default) is two chips: `system` folds into the chip it resolves to, which keeps its sun or moon and adds the system badge, and pressing that chip toggles the lock — while coming back to it from the other chip follows the system rather than locking the same mode by hand. Because a badge has no accessible name, the state is announced through the name instead (`"Light (system)"`, from the overridable `followingSystemLabel`). `variant="with-system"` is three chips, `system` getting its own. Every label is overridable (`lightLabel`, `darkLabel`, `systemLabel`, `aria-label`) for i18n. `label` is each option's accessible name — required even when a description carries the detail. Inside a form, render any of the three from a `FormField` `render` prop: `value={field.value}` + `onValueChange={field.onChange}` + `aria-labelledby={labelId}` (no `ref`, they are not focusable text). Since `field.value` is typed from `name`, a `"day" | "week"` union field arrives as that union, not as a widened `string`.