import { Checkbox } from '@/components/checkbox';
import { Combobox } from '@/components/combobox';
import { Switch } from '@/components/switch';
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from '@/components/field';
import type { FieldProps } from '@/components/field';
import { Input } from '@/components/input';
import type { InputProps } from '@/components/input';
import { InputNumber } from '@/components/input-number';
import { Label } from '@/components/label';
import { SearchIcon } from '@/icons';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/select';
import type { SelectTriggerProps } from '@/components/select';
import { Textarea } from '@/components/textarea';
import type { TextareaProps } from '@/components/textarea';
import { FIELD_SIZES } from './axis-values';
import type { Entry } from './types';
export const input: Entry = {
slug: 'input',
name: 'Input',
group: 'Forms',
description:
'A single-line text field. Form fields run on their own height ladder — 32 / 36 / 44 / 56px — because CBAR draws them taller than buttons from `md` up. That mismatch is deliberate.',
imports: { subpath: 'input', names: ['Input'] },
/* Focus, filled and invalid are variants on the canvas and CSS here, so the
resting cell is the one to compare against. */
figma: { set: 'Input', pin: { state: 'default', '.isInvalid?': 'False' } },
axes: {
variant: ['outline', 'flushed'],
size: FIELD_SIZES,
disabled: [false, true],
},
defaults: { variant: 'outline', size: 'md', placeholder: 'you@example.com' },
render: (props) => ,
matrices: [
{ title: 'Variant × size', rows: 'variant', cols: 'size' },
{ title: 'Disabled', rows: 'variant', cols: 'size', pin: { disabled: true } },
],
compositions: [
{
name: 'Invalid',
description: 'aria-invalid drives the styling — no separate `error` prop to keep in sync.',
/* Every bare Input here carries an `aria-label`. They are not decoration:
the `?a11y=on` panel reported all of them as axe `label` failures, and a
documentation page showing an unlabelled field teaches the wrong thing
however small the example is. Where a composition has a real Label or a
FieldLabel it does the naming instead. */
render: () => (
),
},
{
name: 'Types',
render: () => (
),
},
{
name: 'Figma slots',
description:
"CBAR's Input set carries `.label?`, `showPlaceholder?`, `showErrorMessage?` and `.isRequired?`. All four land here, but as composition rather than props on Input: Field supplies the label, the required marker and the error, and the placeholder is a native attribute.",
render: () => (
),
},
{
name: 'Adornments',
description:
"Figma's `.leftElement?` and `.rightElement?`, both on by default. `startElement` is inert — a click passes through to the field, which is what an icon or a currency symbol wants. `endElement` stays interactive, so a clear button or a reveal toggle works there. With either set, the box becomes a wrapper and `className` sizes it; use `classNames.input` to reach the field.",
render: () => (
),
},
],
spec: {
caption: "CBAR's field ladder, which is not the Button ladder.",
head: ['Size', 'Height', 'Radius'],
rows: [
['xs', '32px', '4px'],
['sm', '36px', '4px'],
['md', '44px', '4px'],
['lg', '56px', '4px'],
],
},
};
export const textarea: Entry = {
slug: 'textarea',
name: 'Textarea',
group: 'Forms',
description: 'Multi-line free text. Same variants as Input; height comes from `rows`.',
imports: { subpath: 'textarea', names: ['Textarea'] },
figma: {
set: 'Textarea',
pin: { state: 'default', '.isInvalid?': 'False', '.isFilled?': 'False' },
},
axes: {
variant: ['outline', 'flushed'],
size: FIELD_SIZES,
},
defaults: { variant: 'outline', size: 'md', placeholder: 'Tell us more…' },
render: (props) => ,
matrices: [{ title: 'Variant × size', rows: 'variant', cols: 'size' }],
compositions: [
{
name: 'Figma slots',
description:
"CBAR's Textarea set has `.label?` (on), `.helperText?` (off) and `.isRequired?` (off), plus `errorText`. All of them are Field's job here — Textarea itself is the control and nothing else.",
render: () => (
),
},
{
name: 'Grows with content',
description:
'CSS `field-sizing: content` does the resizing, so there is no measuring effect and no hidden mirror element — type a line and the box follows.',
render: () => (
),
},
],
};
export const label: Entry = {
slug: 'label',
name: 'Label',
group: 'Forms',
description:
'A caption bound to a control. Pair `htmlFor` with the control’s `id` so the whole label becomes a hit target.',
imports: { subpath: 'label', names: ['Label'] },
compositions: [
{
name: 'Bound to an input',
render: () => (
),
},
{
name: 'Dims with a disabled control',
description:
"Tailwind's `peer` on the control lets the label read its sibling's disabled state in CSS — no prop to pass and nothing to keep in sync.",
render: () => (
),
},
],
};
export const field: Entry = {
slug: 'field',
name: 'Field',
group: 'Forms',
description:
'The wrapper that lays a control out with its label, description and error — without pulling in a form library. Use `Form` instead when you are already on react-hook-form.',
imports: { subpath: 'field', names: ['Field', 'FieldLabel', 'FieldDescription', 'FieldError'] },
axes: {
orientation: ['vertical', 'horizontal'],
invalid: [false, true],
},
defaults: { orientation: 'vertical', invalid: false },
render: (props) => (
Display name
),
matrices: [{ title: 'Orientation × invalid', rows: 'orientation', cols: 'invalid' }],
compositions: [
{
name: 'Full anatomy',
render: () => (
),
},
{
name: 'Horizontal',
description:
'The orientation the matrix above cannot show the point of: horizontal is for a control that reads as one line with its label — a switch or a checkbox — rather than for a text field, which still wants its label above it.',
render: () => (
Join the beta programmeWeekly digest
),
},
{
name: 'Required',
description:
"CBAR's `.isRequired?` slot. The asterisk is `aria-hidden` — it draws the requirement, it does not announce it, so set `required` on the control as well and assistive technology hears it from the place it already looks.",
render: () => (
Email
We only use it to send the receipt.
),
},
],
};
export const select: Entry = {
slug: 'select',
name: 'Select',
group: 'Forms',
description:
'A closed list of options, built on Radix Select so keyboard and screen-reader behaviour is not re-implemented. `variant` and `size` live on SelectTrigger, and they are Input\'s two axes — the trigger renders through `inputVariants`, so it shares the 32 / 36 / 44 / 56px field ladder rather than the button one.',
imports: {
subpath: 'select',
names: ['Select', 'SelectTrigger', 'SelectValue', 'SelectContent', 'SelectItem'],
},
figma: { set: 'Select', pin: { state: 'default' } },
axes: {
variant: ['outline', 'flushed'],
size: FIELD_SIZES,
},
defaults: { variant: 'outline', size: 'md' },
render: (props) => (
),
matrices: [{ title: 'Variant × size', rows: 'variant', cols: 'size' }],
compositions: [
{
name: 'Figma slots',
description:
"CBAR's Select set has `.label?`, `showErrorMessage?` and `errorText` — Field's job here — plus `.rightElement?`, which SelectTrigger draws as a fixed chevron, `.isRequired?` on FieldLabel and `.isClearable?` as `clearable` on the trigger. Clearing sets the value to `''`, which is how Radix returns to the placeholder; that is also why no item may use `''` as its own value.",
render: () => (
),
},
{
name: 'Grouped options',
render: () => (
),
},
],
};
export const combobox: Entry = {
slug: 'combobox',
name: 'Combobox',
group: 'Forms',
description:
"A searchable select. Rebuilt on the kit's own Command + Popover + Button rather than ported from shadcn, whose version needs @base-ui/react and InputGroup.",
imports: { subpath: 'combobox', names: ['Combobox'] },
compositions: [
{
name: 'Filterable list',
render: () => (
),
},
{
name: 'Custom copy',
description:
'Four strings, all of them yours: the trigger placeholder, the search placeholder, the empty message, and a disabled option that stays visible so the list does not appear to lose entries.',
render: () => (
),
},
],
};
export const inputNumber: Entry = {
slug: 'input-number',
name: 'InputNumber',
group: 'Forms',
description:
'A numeric field with steppers, clamping and keyboard arrows. Written locally rather than pulled from rc-input-number.',
imports: { subpath: 'input-number', names: ['InputNumber'] },
compositions: [
{
name: 'Bounded',
description: 'min / max / step, with the arrows and the wheel honouring all three.',
render: () => (
),
},
{
name: 'Formatted',
description:
'`formatter` and `parser` are a pair: the first decides what is shown, the second turns that back into a number. The committed value stays numeric throughout, which is why `role="spinbutton"` can still announce it with its bounds.',
render: () => (
),
},
{
name: 'Decimals',
description:
'`precision` fixes the number of decimals on commit, so stepping by 0.1 cannot drift into a float artefact.',
render: () => (
),
},
{
name: 'Without the steppers',
description:
'`controls={false}` drops the buttons and keeps everything else — the arrow keys, the wheel and the clamping still work.',
render: () => (
),
},
],
};