# CADS component catalog (v0.1.0)

Package: `@codeai/cads-react`. Figma file: `DGekOeToRVifvFAhfqpeC1`.

Only use these components and props. Do not invent APIs.

## Button

Triggers an action with a single tap or click. Contained, outlined, and text styles across primary, secondary, tertiary, orange, and error colors.

Props:

- `variant`: `"contained" | "outlined" | "text"` — default `"contained"`
- `color`: `"primary" | "secondary" | "tertiary" | "orange" | "error"` — default `"primary"` — Color role. Tertiary is only for text + icon-only; orange is only for contained (run button). Other combos fall back (tertiary→secondary, orange→primary).
- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"` — Control heights: 48 / 40 / 32 / 24
- `iconOnly`: `boolean` — Square icon-only geometry; inferred when no children
- `startIconName`: `FaIconName`
- `endIconName`: `FaIconName`
- `loading`: `boolean` — Replaces visible content with a centered FA spinner; preserves button width (no layout shift). Does not use disabled styling.
- `disabled`: `boolean`
- `fullWidth`: `boolean`
- `children`: `ReactNode`

Rules:

- Use variant=contained + color=primary for primary CTAs (brand fill).
- Contained secondary uses --background-neutral-primary-inverse; outlined primary uses --border-neutral-solid.
- Contained disabled text uses --text-disabled-neutral-inverse (solid fill).
- color=tertiary is Figma-valid only for text + iconOnly; other combos warn and fall back to secondary.
- color=orange is only for the run button (contained, labeled or icon-only); other variants warn and fall back to primary.
- loading replaces all visible content with a centered spinner and keeps the prior width — do not swap startIcon for a spinner.
- Use semantic color CSS vars only — never hard-coded hex. No --ds- prefix.

Example:

```tsx
<Button variant="contained" color="primary" size="medium">Continue</Button>
```

## CloseIconButton

Icon-only close action for dismissible components such as alerts, popovers, dialogs, drawers, and tabs.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"large"` — Figma close-control geometry: 24 / 18 / 18 / 13px.
- `color`: `"primary" | "secondary" | "brand" | "pink" | "orange" | "success" | "error" | "warning" | "info"` — default `"primary"`
- `disabled`: `boolean`
- `onClick`: `(event) => void`
- `aria-label`: `string` — default `"Close"` — Accessible name for the icon-only action.

Rules:

- Use only for close or dismiss actions.
- Choose the color that matches the host component's sentiment; use secondary on neutral surfaces.
- Interaction states are CSS recipes — do not expose state as a React prop.
- Override aria-label when context such as Dismiss tab is clearer than Close.

Example:

```tsx
<CloseIconButton size="medium" color="secondary" onClick={onClose} />
```

## SegmentedButton

A group of connected buttons for choosing one option from a small mutually exclusive set. Selected segment uses selected tokens.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `value`: `string`
- `defaultValue`: `string`
- `onChange`: `(value: string) => void`
- `options`: `SegmentedButtonOption[]` **(required)** — Maps Block label / leftIcon / rightIcon / show*Icon / isActive via value. Optional per-option `tooltip` (code-only) wraps that segment in Tooltip — useful for iconOnly.
- `iconOnly`: `boolean` — Square icon-only segments
- `disabled`: `boolean`
- `aria-label`: `string`

Rules:

- Use selected tokens (fill + border + text) for the active segment — never brand fills for selected chrome.
- Segments are connected with -1px overlap; first/last get --shape-sm on outer corners only.
- Labels are always Body Semi Bold — do not invent labelStyle / thick|thin.
- Figma Group exposes color=Primary only; unselected border is always --border-neutral-secondary (not a consumer prop).
- Unselected press softens label/icon to --text-neutral-tertiary (hover keeps --text-neutral-primary); selected press keeps --text-selected-primary.
- Focus uses outline 2px with -2px offset (brand-light / focused-inverse) — same flush recipe as Dropdown menu items, not outer FOCUS_RING.
- Keyboard: radiogroup manual activation — one Tab stop; ←/→/↑/↓ + Home/End move focus only; Space/Enter commits selection (click still selects immediately).
- Figma Block-only: position (first|middle|last), state, isActive — derived in code from options index / value / CSS / disabled.
- Prefer the group API; Segmented Button Block (8000:4554) is an internal building block.
- Optional option.tooltip is code-only (not in Figma) — mirrors MUI's Tooltip-around-ToggleButton composition without fighting cloneElement.

Example:

```tsx
<SegmentedButton aria-label="View" options={[{value:"list",label:"List"},{value:"grid",label:"Grid"}]} defaultValue="list" />
```

## IconToggle

Icon-only binary on/off control. Optional label + secondToggle covers Figma Icon Toggle + Label (up to 2 toggles).

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `color`: `"primary" | "secondary" | "brand" | "success" | "error"` — default `"brand"`
- `iconName`: `FaIconName` **(required)**
- `pressed`: `boolean` — Controlled on/off (Figma isOn)
- `defaultPressed`: `boolean`
- `onPressedChange`: `(pressed: boolean) => void`
- `label`: `ReactNode` — When set, renders Figma Icon Toggle + Label layout
- `secondToggle`: `{ iconName; color?; pressed?; defaultPressed?; onPressedChange?; aria-label; disabled? }` — Figma hasTwoToggles — second toggle with its own icon/color/aria-label (e.g. thumbs-down + error)
- `exclusive`: `boolean` — default `false` — With secondToggle: mutual exclusion (thumbs up/down). Figma does not specify exclusive — default is independent.
- `disabled`: `boolean`
- `aria-label`: `string`

Rules:

- Off = FA Regular + quaternary text; on = FA Solid + color on-token.
- Hover/press surfaces are color-specific (brand-light, error-light, etc.) — never a generic grey for all colors.
- Standalone and labeled Figma sets map to one React component (secondToggle for dual).
- In dual mode, set color per toggle: parent color = first; secondToggle.color = second (e.g. success + error).
- Dual toggles are independent unless exclusive; Figma has no exclusive prop.
- Always provide aria-label when there is no visible label.

Example:

```tsx
<IconToggle label="Was this helpful?" iconName="thumbs-up" color="success" exclusive aria-label="Thumbs up" secondToggle={{ iconName: "thumbs-down", color: "error", "aria-label": "Thumbs down" }} />
```

## FieldWrapper

Generic container that pairs any input with a label and helper text, including validation messaging for errors and warnings.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `sentiment`: `"default" | "success" | "warning" | "error"` — default `"default"`
- `label`: `ReactNode`
- `helperText`: `ReactNode`
- `helperIconName`: `FaIconName`
- `showHelper`: `boolean` — default `true`
- `children`: `ReactNode` **(required)**
- `required`: `boolean` — default `false` — Appends * after the label (same type style, no gap).
- `disabled`: `boolean`
- `htmlFor`: `string`

Rules:

- Nest any CADS control in children (TextInput, Dropdown, Checkbox, etc.).
- Warning helper text stays tertiary; only the icon uses warning color.
- Non-default sentiments always show helper when helperText is set.
- disabled applies --text-disabled-neutral to label, helper text, and helper icon (Figma isDisabled).
- required appends * after the label; pair with native required on the nested control when applicable.

Example:

```tsx
<FieldWrapper label="Email" helperText="We never share this" sentiment="default"><TextInput placeholder="you@example.com" /></FieldWrapper>
```

## TextInput

Single-line field or multiline area with Field Wrapper label/helper. Figma name: Text Input.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `color`: `"primary" | "secondary"` — default `"primary"` — primary = solid border; secondary = soft border
- `multiline`: `boolean` — default `false` — Figma type=area when true
- `startIconName`: `FaIconName` — Leading FA icon inside the field. Field-only; ignored when multiline. Omit for no icon (Figma boolean startIcon collapsed into presence of this prop).
- `label`: `string`
- `helperText`: `ReactNode`
- `helperIconName`: `FaIconName`
- `showHelper`: `boolean` — default `true`
- `sentiment`: `"default" | "success" | "warning" | "error"` — default `"default"`
- `error`: `boolean`
- `readOnly`: `boolean`
- `disabled`: `boolean`
- `required`: `boolean` — default `false` — Native required on the control + * after the Field Wrapper label.
- `placeholder`: `string`
- `value`: `string`
- `defaultValue`: `string`
- `rows`: `number`

Rules:

- Prefer TextInput over the deprecated TextField alias.
- Figma type=field|area maps to multiline={false|true}.
- startIconName is field-only; ignored when multiline (type=area). Omit for no icon.
- extraSmall field height uses the shared 24px control scale (Figma building block is 22px).
- Do not invent floating labels — label is always above via Field Wrapper.
- required sets the native HTML attribute and appends * after the label.

Example:

```tsx
<TextInput label="Email" size="medium" startIconName="envelope" placeholder="you@example.com" helperText="Required" />
```

## Dropdown

Reveals options in a collapsible panel. Works as a form select (role=input) or as an action menu (role=action).

Props:

- `role`: `"input" | "action"` **(required)** — input = form select; action = button menu
- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `menuType`: `"default" | "checklist"` — default `"default"` — default = single-select list (item iconName optional); checklist = multi-select (input-only)
- `menuPlacement`: `"bottomLeft" | "bottomRight" | "topLeft" | "topRight"` — default `"bottomLeft"`
- `width`: `string` — default `"hug"` — Input-role only. "hug" (default) = static width from longest option/placeholder; "full" = 100%; or any CSS length (12rem, 240px, 50%). Numbers are px.
- `menuWidth`: `"hug" | "trigger" | number | `${number}%`` — default `"hug"` — Menu panel sizing. "hug" fits content (never narrower than the trigger); "trigger" locks to the trigger; a number sets a px minimum; a percentage is an exact trigger-relative width.
- `options`: `DropdownOption[]` **(required)** — Items, separators ({type:"separator"}), and group headers ({type:"group",label}). Items support iconName (text-only when omitted).
- `label`: `ReactNode`
- `helperText`: `ReactNode`
- `placeholder`: `string`
- `value`: `string | string[]`
- `defaultValue`: `string | string[]`
- `onChange`: `(value: string | string[]) => void`
- `onAction`: `(value: string) => void`
- `open`: `boolean`
- `defaultOpen`: `boolean`
- `disabled`: `boolean`
- `readOnly`: `boolean`
- `error`: `boolean`
- `required`: `boolean` — default `false` — Input-role only. Appends * after the Field Wrapper label.
- `color`: `"primary" | "secondary"`
- `labelStyle`: `"thick" | "thin"`
- `startIconName`: `FaIconName`
- `buttonVariant`: `"contained" | "outlined" | "text"`
- `buttonColor`: `"primary" | "secondary" | "tertiary" | "orange" | "error"`
- `aria-label`: `string`

Rules:

- role=input composes Field Wrapper + Dropdown Button; role=action reuses Button.
- Triggers skip Press scale when experimentalMotion is on — open motion belongs to the menu Surface.
- menuType=checklist is input-only; menuType=default is single-select — item icons are per-option (iconName), not a list-level mode.
- options may include {type:"separator"} and {type:"group",label} (non-selectable; skipped in keyboard nav). Destructive is action-only.
- Input-role width defaults to hug (static width from the longest option/placeholder — selection does not resize the field). Use "full" or a CSS length otherwise.
- menuWidth defaults to hug (content + trigger floor). Use "trigger" to match the field, a number for a px minimum, or a percentage for a narrower/wider trigger-relative panel.
- Selected menu items use selected tokens — never brand fills.
- Trigger focus uses outer FOCUS_RING; menu-item keyboard focus uses outline 2px with -2px offset (brand-light / selected / error) — not the same recipe as hover.
- Menu item press: neutral softens to text-neutral-tertiary; destructive keeps error-light + text-error-secondary; selected press returns to selected-primary (hover uses selected-strong).
- Dropdown Button / Menu List / Menu Item / menuSeparator / menuOptGroup are internal — do not import standalone.

Example:

```tsx
<Dropdown role="input" label="Sort" width="hug" options={[{type:"group",label:"Recent"},{value:"a",label:"Option A"},{type:"separator"},{value:"b",label:"Option B"}]} defaultValue="a" />
```

## Checkbox

Binary or indeterminate checkbox. Public API maps Figma Checkbox + Label; box chrome from the Checkbox building block.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `labelStyle`: `"thin" | "thick"` — default `"thin"` — Figma labelStyle — thin=regular, thick=semibold
- `label`: `ReactNode`
- `checked`: `boolean`
- `defaultChecked`: `boolean`
- `indeterminate`: `boolean` — Maps Figma block status=indeterminate
- `disabled`: `boolean`
- `onChange`: `(event, checked) => void`

Rules:

- Checked / indeterminate fills use selected tokens — never brand fills.
- Prefer labeled usage; standalone box only when + Label does not suffice.
- Interaction states (hover/focus/press) are CSS recipes — not React props.

Example:

```tsx
<Checkbox label="Remember me" size="medium" labelStyle="thin" />
```

## Radio

Single-select radio. Public API maps Figma Radio Button + Label; circle chrome from Radio Buttons Block.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `labelStyle`: `"thin" | "thick"` — default `"thin"` — Figma labelStyle — thin=regular, thick=semibold
- `label`: `ReactNode`
- `checked`: `boolean`
- `value`: `string`
- `disabled`: `boolean`
- `onChange`: `(event) => void`

Rules:

- Selected state is ring + inner dot on neutral fill — not a filled selected square.
- Group with MUI RadioGroup when needed.
- Interaction states are CSS recipes — not React props.

Example:

```tsx
<Radio label="Option A" value="a" size="medium" />
```

## Toggle

On/off switch with optional track icons (defaults check/xmark). Heights match Checkbox/Radio (22/20/18/16). Public API maps Figma Toggle + Label; track chrome from Toggle building block. Distinct from IconToggle.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `labelPlacement`: `"left" | "right"` — default `"left"` — Label on the left (switch right) or label on the right (switch left). Corrects inverted Figma axis naming.
- `label`: `ReactNode`
- `checked`: `boolean`
- `defaultChecked`: `boolean`
- `disabled`: `boolean`
- `onChange`: `(event, checked) => void`
- `hasIcons`: `boolean` — default `true` — When false, hide track icons entirely (Figma hasIcons).
- `onIcon`: `FaIconName` — default `"check"` — Track icon when on (Figma onIcon). Ignored when hasIcons is false.
- `offIcon`: `FaIconName` — default `"xmark"` — Track icon when off (Figma offIcon). Ignored when hasIcons is false.
- `aria-label`: `string` — Required when unlabeled

Rules:

- On track uses selected tokens; off track uses neutral septenary/octonary.
- Track heights match Checkbox/Radio at each size (22/20/18/16).
- Defaults to check (on) / xmark (off); override with onIcon / offIcon, or set hasIcons={false} for a plain switch.
- Not the same as IconToggle (icon-only binary button).
- Interaction states are CSS recipes — not React props.

Example:

```tsx
<Toggle label="Notifications" labelPlacement="left" defaultChecked onIcon="check" offIcon="xmark" />
```

## Slider

Continuous or stepped value control with optional ± buttons, display value, and stepper ticks. Track fill uses selected tokens.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"` — Maps Figma labelSize for label/helper type.
- `sentiment`: `"default" | "error"` — default `"default"` — Maps field-level Figma state=error.
- `label`: `ReactNode`
- `displayValue`: `ReactNode`
- `showDisplayValue`: `boolean` — default `true`
- `showLabelRow`: `boolean` — default `true`
- `helperText`: `ReactNode`
- `helperIconName`: `FaIconName`
- `showHelper`: `boolean` — default `true`
- `showControls`: `boolean` — default `false`
- `showTicks`: `boolean` — default `false` — Show value labels under the track. Discrete step grids label every step; continuous (step=null) labels only min and max.
- `startsFrom`: `"side" | "center"` — default `"side"` — Track fill origin (Figma Slider Bar). "side" fills from min→value (0…100); "center" is bipolar around 0 (−100…100).
- `min`: `number` — default `0` — Range minimum. Defaults to 0 when startsFrom is side, or -100 when center.
- `max`: `number` — default `100` — Range maximum. Default 100 for both side and center.
- `width`: `number | string` — default `300` — Control width. Numbers are px; strings are CSS lengths (e.g. "16rem"). Ignored when fullWidth is true. Default matches Figma (300px).
- `fullWidth`: `boolean` — default `false` — Stretch to 100% of the parent. Takes precedence over width.
- `value`: `number | number[]`
- `defaultValue`: `number | number[]` — default `50` — Uncontrolled initial value. Defaults to 50 when startsFrom is side, or 0 when center.
- `step`: `number | null` — default `1` — Value increment for drag, ± controls, and tick labels when showTicks is on. Use null for continuous (ticks show only min and max).
- `disabled`: `boolean`
- `onChange`: `(event, value, activeThumb) => void`
- `aria-label`: `string`

Rules:

- Filled track uses selected tokens — never brand fills for the active range.
- Knob hover/focus/press are CSS recipes from Slider Knob — not React props.
- Set min/max for the value domain — side defaults 0…100; center defaults -100…100 with 0 at the origin.
- startsFrom="center" is bipolar: negative values fill left of 0, positive fill right.
- showTicks labels follow the step grid (not Figma’s 1…N demo text). Continuous step={null} labels only min and max.
- Default width is 300px (Figma symbol). Use fullWidth in fluid layouts, or width for a fixed size.

Example:

```tsx
<Slider label="Volume" defaultValue={50} showControls showHelper helperText="Helper text" />
```

## Chip

Selectable pill for options or quick actions. Prefer ChipGroup for labeled multi-select sets. Distinct from Tag (status/category label).

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `color`: `"primary" | "secondary"` — default `"primary"` — Unselected border treatment.
- `labelStyle`: `"thick" | "thin"` — default `"thick"`
- `selected`: `boolean` — default `false`
- `label`: `ReactNode`
- `startIconName`: `FaIconName` — Leading FA icon. Omit for no start icon (Figma boolean startIcon collapsed into presence of this prop).
- `endIconName`: `FaIconName` — Trailing FA icon. Omit for no end icon (Figma boolean endIcon collapsed into presence of this prop).
- `disabled`: `boolean`
- `onClick`: `(event) => void`
- `aria-label`: `string`

Rules:

- Selected chrome uses selected tokens — never brand fills.
- Not the same as Tag (status/category badge).
- Icons render only when startIconName / endIconName are set — there is no separate boolean gate.
- Interaction states are CSS recipes — not React props.

Example:

```tsx
<Chip label="Option" selected color="primary" size="medium" />
```

## ChipGroup

Labeled multi-select group of Chips with Field Wrapper chrome. Preferred over composing Chip alone.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `color`: `"primary" | "secondary"` — default `"primary"`
- `labelStyle`: `"thick" | "thin"` — default `"thick"`
- `label`: `ReactNode`
- `helperText`: `ReactNode`
- `helperIconName`: `FaIconName`
- `showHelper`: `boolean` — default `true`
- `options`: `ChipGroupOption[]` **(required)**
- `value`: `string[]`
- `defaultValue`: `string[]`
- `onChange`: `(value: string[]) => void`
- `disabled`: `boolean`
- `aria-label`: `string`

Rules:

- Multi-select: value is string[]. Selected chips use selected tokens.
- Prefer ChipGroup over ad-hoc Chip rows when a field label is needed.

Example:

```tsx
<ChipGroup label="Interests" options={[{value:"a",label:"Art"},{value:"b",label:"Music"}]} defaultValue={["a"]} />
```

## Link

Navigates to another page or resource. Primary (brand) and secondary (neutral) types with optional external icon.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall" | "extraExtraSmall"` — default `"medium"` — extraExtraSmall is Link-only (body/xxs).
- `type`: `"primary" | "secondary"` — default `"primary"` — Figma type axis — primary uses brand text; secondary uses neutral.
- `isExternal`: `boolean` — default `true` — Shows up-right-from-square end icon.
- `href`: `string`
- `disabled`: `boolean`
- `children`: `ReactNode` **(required)**

Rules:

- Use type=primary for brand CTAs/links; secondary for neutral inline navigation.
- Visited/hover/press are CSS recipes — not React props.
- Brand tokens for primary links — never selected fills.

Example:

```tsx
<Link href="/docs" type="primary" size="medium" isExternal>Link</Link>
```

## Breadcrumbs

Trail of breadcrumb links with chevron separators. When maxItems is exceeded, middle crumbs collapse into Breadcrumb Overflow (ellipsis → dropdown of truncated pages). Composes Figma Links / Separators / Overflow.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `items`: `BreadcrumbItem[]` **(required)** — Trail items. Optional per-item iconName (leading icon on any crumb), iconOnly, current (Figma isCurrent), href/onClick.
- `maxItems`: `number` — default `8` — MUI-compatible. Collapse middle into Overflow ellipsis when exceeded.
- `itemsBeforeCollapse`: `number` — default `1` — MUI-compatible. Crumbs kept before the overflow ellipsis.
- `itemsAfterCollapse`: `number` — default `1` — MUI-compatible. Crumbs kept after the overflow ellipsis (Figma default composition uses 2).
- `expandText`: `string` — default `"Show path"` — Accessible name for the overflow trigger.
- `aria-label`: `string` — default `"Breadcrumb"`

Rules:

- Mark the current page with items[].current — rendered as text, not a link.
- Optional leading icon via items[].iconName on any crumb (not first-only); iconOnly hides the label.
- Prefer items[] composition; do not use Breadcrumb Links standalone.
- Overflow ellipsis opens a dropdown of truncated pages (Figma Breadcrumb Overflow + Dropdown).
- Interaction states are CSS recipes — not React props. No underline on hover.

Example:

```tsx
<Breadcrumbs size="medium" maxItems={4} itemsBeforeCollapse={1} itemsAfterCollapse={2} items={[{label:"Home",href:"/"},{label:"Library",href:"#",iconName:"box-archive"},{label:"A",href:"#"},{label:"B",href:"#"},{label:"Page",current:true}]} />
```

## Tabs

Tab Group — primary (underline) or secondary (contained) tablist. Tab Item is an internal building block.

Props:

- `type`: `"primary" | "secondary"` — default `"primary"` — primary = underline; secondary = contained.
- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `items`: `TabsItem[]` **(required)**
- `value`: `string`
- `defaultValue`: `string`
- `onChange`: `(value: string) => void`
- `onItemDismiss`: `(value: string) => void`
- `aria-label`: `string`

Rules:

- Selection is owned by the group via value/defaultValue — do not expose isCurrent.
- Selected chrome uses selected tokens; never brand fills for the active tab.
- Keyboard: tablist manual activation — one Tab stop; ←/→ + Home/End move focus only; Space/Enter commits selection (click still selects immediately).
- Tabs may not be used as standalone Tab Items — always via Tabs.

Example:

```tsx
<Tabs type="primary" size="medium" defaultValue="a" items={[{value:"a",label:"Tab Label"},{value:"b",label:"Tab Label"}]} />
```

## Pagination

Page pagination — segmented page numbers with optional first/last. Figma Pagination type=page. Table pagination is TablePagination (same docs page).

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `count`: `number` **(required)** — Total number of pages.
- `page`: `number` — 1-based current page (controlled).
- `defaultPage`: `number` — Initial page when uncontrolled.
- `onChange`: `(event, page: number) => void` — Called when the page changes.
- `layout`: `"auto" | "segmented" | "compact"` — default `"auto"` — Narrow-container behavior. auto swaps to compact (prev + Page X of Y + next) when the segmented trail would overflow.
- `labelCompactPages`: `(args: { page: number; count: number }) => ReactNode` — Formats the compact-mode label. Default "Page X of Y".
- `showFirstButton`: `boolean` — default `true` — Show jump-to-first (Figma hasFirstLast).
- `showLastButton`: `boolean` — default `true` — Show jump-to-last (Figma hasFirstLast).
- `siblingCount`: `number` — default `1` — Pages shown on each side of the current page.
- `boundaryCount`: `number` — default `1` — Pages always shown at the start and end.
- `disabled`: `boolean`
- `aria-label`: `string`

Rules:

- Page type uses Segmented Button Block geometry with nav chrome tweaks (secondary fill + quaternary icons on first/prev/next/last).
- Selected page uses selected tokens — never brand fills.
- Disabled (group or boundary item): transparent fill + disabled border/text; selected-while-disabled uses disabled-neutral fill + inverse text.
- layout defaults to auto: when the segmented trail would overflow, swap to compact prev + Page X of Y + next (no wrap). Use layout=segmented to force the trail (scrolls if needed) or layout=compact always.
- For table footers use TablePagination (same Figma set, type=table).
- Additional MUI Pagination props are available; see MUI docs from the props table.

Example:

```tsx
<Pagination count={10} page={4} showFirstButton showLastButton aria-label="Pagination" />
```

## TablePagination

Table footer pagination — rows-per-page Dropdown, range label, and prev/next. Figma Pagination type=table. Docs live on the Pagination page.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"`
- `count`: `number` **(required)** — Total number of rows.
- `page`: `number` **(required)** — 0-based page index (MUI TablePagination).
- `rowsPerPage`: `number` **(required)**
- `onPageChange`: `(event, newPage: number) => void` **(required)**
- `onRowsPerPageChange`: `(event) => void`
- `rowsPerPageOptions`: `Array<number | { value: number; label: string }>` — default `[10, 25, 50, 100]`
- `labelRowsPerPage`: `ReactNode` — default `"Rows per page"`
- `labelDisplayedRows`: `(args) => ReactNode` — Formats the range text (from-to of count).
- `disabled`: `boolean`
- `aria-label`: `string`

Rules:

- Composes Dropdown (input, secondary, hug field + menuWidth=trigger) and Button (outlined secondary iconOnly).
- Rows-per-page uses role=input so the menu highlights the current value; menu matches the hugged trigger width.
- Desktop follows Figma: one horizontal row with a vertical divider.
- At 760px and below, stacks rows-per-page above range/nav and hides the divider. Mobile stack gap L16 / M12 / S8 / XS6.
- page is 0-based (MUI TablePagination convention); Pagination page is 1-based.
- Documented on the Pagination component page with a dedicated props table.

Example:

```tsx
<TablePagination count={100} page={1} rowsPerPage={10} onPageChange={() => {}} onRowsPerPageChange={() => {}} />
```

## Alert

Inline banner for contextual status within a page. Supports sentiment variants, icons, optional action and dismiss.

Props:

- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"large"`
- `sentiment`: `"brand" | "pink" | "success" | "error" | "warning" | "info" | "neutral"` — default `"brand"`
- `children`: `ReactNode`
- `iconName`: `FaIconName | false` — Leading icon. Omit for sentiment default; false hides the icon (MUI convention); string overrides.
- `hasAction`: `boolean` — default `false`
- `actionLabel`: `ReactNode` — default `"Button"`
- `actionStartIconName`: `FaIconName`
- `actionEndIconName`: `FaIconName`
- `onAction`: `() => void`
- `isDismissible`: `boolean` — default `false`
- `onClose`: `() => void`
- `fullWidth`: `boolean` — default `true`

Rules:

- Use for in-page contextual messaging — not temporary overlays (Toast) or page-level banners (NotificationBanner).
- Action is locked to outlined secondary; size follows Alert size. Always pass actionLabel (empty falls back to Button).
- Status sentiments supply default icons when iconName is omitted; set iconName={false} to hide (same as MUI Alert icon={false}).

Example:

```tsx
<Alert sentiment="success" size="medium" isDismissible actionLabel="Undo" hasAction>Saved successfully.</Alert>
```

## Toast

Brief elevated notification for lightweight feedback. Dismisses automatically in product UIs; chrome matches Figma Toast.

Props:

- `sentiment`: `"primary" | "pink" | "success" | "error" | "warning" | "info" | "neutral"` — default `"primary"`
- `children`: `ReactNode`
- `iconName`: `FaIconName | false` — Leading icon. Omit for sentiment default; false hides the icon (MUI convention); string overrides.
- `hasAction`: `boolean` — default `false`
- `actionLabel`: `ReactNode` — default `"Button"`
- `actionStartIconName`: `FaIconName`
- `actionEndIconName`: `FaIconName`
- `onAction`: `() => void`
- `isDismissible`: `boolean` — default `true`
- `onClose`: `() => void`
- `open`: `boolean` — Controlled visibility for the snackbar host. Omit for inline/fixture surface-only render.
- `placement`: `"topLeft" | "topCenter" | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight"` — default `"bottomCenter"` — Viewport corner/edge when hosted (MUI Snackbar anchorOrigin).
- `offset`: `number` — default `64` — Distance in px from the pinned viewport edge(s).
- `surfaceOnly`: `boolean` — Render elevated chrome without the snackbar portal. Default when open is omitted.

Rules:

- Figma uses sentiment=primary for brand chrome (not brand).
- Action is locked to outlined secondary at small size. Always pass actionLabel.
- Pass open + placement to host via MUI Snackbar; omit open (or surfaceOnly) for inline/fixture chrome. Host apps still own queueing / auto-dismiss timing.
- Status sentiments supply default icons when iconName is omitted; set iconName={false} to hide.

Example:

```tsx
<Toast open placement="bottomCenter" sentiment="success" hasAction actionLabel="View" onClose={hide}>This is a toast.</Toast>
```

## NotificationBanner

Persistent page-level banner with title, description, icon, and optional actions. Expanded cousin of Alert.

Props:

- `sentiment`: `"brand" | "pink" | "success" | "error" | "warning" | "info" | "neutral"` — default `"brand"`
- `fillStyle`: `"none" | "color"` — default `"none"`
- `title`: `ReactNode` **(required)**
- `description`: `ReactNode` **(required)**
- `iconName`: `FaIconName`
- `hasPrimaryAction`: `boolean` — default `true`
- `hasSecondaryAction`: `boolean` — default `true`
- `primaryActionLabel`: `ReactNode` — default `"Button"`
- `secondaryActionLabel`: `ReactNode` — default `"Button"`
- `onPrimaryAction`: `() => void`
- `onSecondaryAction`: `() => void`
- `isDismissible`: `boolean` — default `false`
- `onClose`: `() => void`
- `fullWidth`: `boolean` — default `true`

Rules:

- title and description are required.
- fillStyle=none is the white surface; fillStyle=color tints the banner and switches primary action to inverse/secondary.
- Prefer NotificationBanner for persistent page messaging; Alert for compact inline status.

Example:

```tsx
<NotificationBanner sentiment="error" fillStyle="color" title="Action required" description="Review the settings before continuing." />
```

## Tag

Compact badge / status label (distinct from selectable Chip). Optionally dismissible for removable filters.

Props:

- `color`: `"neutral" | "brand" | "pink" | "orange" | "success" | "error" | "warning" | "info"` — default `"neutral"`
- `size`: `"large" | "medium" | "small"` — default `"large"`
- `label`: `ReactNode`
- `startIconName`: `FaIconName` — Leading FA icon. Omit for no start icon (Figma boolean startIcon collapsed into presence of this prop).
- `endIconName`: `FaIconName` — Trailing FA icon. Omit for no end icon (Figma boolean endIcon collapsed into presence of this prop).
- `isDismissible`: `boolean` — default `false`
- `onClose`: `() => void`

Rules:

- Not the same as Chip (selectable). Use Tag for status/category labels.
- Use semantic colors — do not invent custom hues.
- Icons render only when startIconName / endIconName are set — there is no separate boolean gate.

Example:

```tsx
<Tag color="success" size="medium" label="Passed" />
```

## IconTooltip

Info-style icon that is purely a Tooltip affordance — no button chrome (fill/border/press scale), just a focusable glyph. Messaging trigger for a specific help use case.

Props:

- `title`: `ReactNode` **(required)**
- `iconName`: `FaIconName | string` — default `"circle-info"` — FA icon rendered as the trigger affordance.
- `color`: `"primary" | "secondary" | "tertiary"` — default `"tertiary"` — primary → brand; secondary → neutral-primary; tertiary → neutral-quaternary (muted).
- `size`: `"large" | "medium" | "small" | "extraSmall"` — default `"medium"` — Glyph size on the shared control size scale.
- `placement`: `"bottom-start" | "bottom" | "bottom-end" | "top-start" | "top" | "top-end" | "left-start" | "left" | "left-end" | "right-start" | "right" | "right-end"` — default `"top"` — MUI placement, passed through to Tooltip.
- `hasCaret`: `boolean` — default `true`
- `aria-label`: `string` — Accessible trigger name. Falls back to title when title is a plain string; required when title is rich content.

Rules:

- No hover/press/border chrome on the icon itself — only a required focus-visible ring.
- Composes Tooltip for positioning/caret; does not reimplement tooltip surface.
- Figma Info Tooltip (17051:27346) models size × color × state; hover/focus variants pin a Tooltip instance above the icon. placement/hasCaret stay code-only — Figma covers the default top placement.
- Also accepts other Tooltip/MUI Tooltip props (enterDelay, slotProps, …) except iconName (reserved for the trigger glyph) and surfaceOnly, plus a triggerProps escape hatch for the underlying icon trigger (ButtonBase props).

Example:

```tsx
<IconTooltip title="Help text" color="tertiary" placement="top" />
```

## Tooltip

Small hover/focus overlay with brief contextual text on an inverse surface.

Props:

- `title`: `ReactNode` **(required)**
- `children`: `ReactElement` — Trigger element. Required for anchored mode; omit with surfaceOnly.
- `placement`: `"bottom-start" | "bottom" | "bottom-end" | "top-start" | "top" | "top-end" | "left-start" | "left" | "left-end" | "right-start" | "right" | "right-end"` — default `"bottom"` — MUI placement — where the tooltip sits relative to the trigger. *-start / *-end pin the caret near that edge.
- `hasCaret`: `boolean` — default `true`
- `iconName`: `FaIconName | string` — Leading FA icon. Omit for no icon (Figma boolean startIcon collapsed into presence of this prop).
- `surfaceOnly`: `boolean` — Render bubble (+ caret) inline without Popper / trigger. Used by docs Inspect and static previews.

Rules:

- Child must be able to hold a ref (forwardRef element).
- Prefer supplementary hints — avoid sole source of critical info.
- Use MUI placement for position (bottom, top-start, etc.). Figma’s caretPlacement maps inverted (Figma top → placement bottom).
- Icons render only when iconName is set — there is no separate boolean gate.
- Pass a trigger child for anchored mode, or use surfaceOnly for static previews.
- Also accepts other MUI Tooltip props (slotProps, open, followCursor, …) except arrow (use hasCaret).

Example:

```tsx
<Tooltip title="Save" placement="bottom"><Button>Save</Button></Tooltip>
```

## Popover

Dismissible anchored card with title, body, optional image/custom content, stepper, and actions.

Props:

- `content`: `"textOnly" | "textImage" | "custom"` — default `"textOnly"`
- `caretPlacement`: `"bottomLeft" | "bottomCenter" | "bottomRight" | "topLeft" | "topCenter" | "topRight" | "leftTop" | "leftCenter" | "leftBottom" | "rightTop" | "rightCenter" | "rightBottom"` — default `"bottomLeft"`
- `hasCaret`: `boolean` — default `true`
- `title`: `ReactNode`
- `body`: `ReactNode`
- `image`: `ReactNode`
- `customContent`: `ReactNode`
- `hasActionRow`: `boolean` — default `true`
- `hasStepper`: `boolean` — default `true`
- `stepperText`: `ReactNode` — default `"1/3"`
- `hasPrimaryAction`: `boolean` — default `true`
- `hasSecondaryAction`: `boolean` — default `true`
- `primaryActionLabel`: `ReactNode` — default `"Next"`
- `secondaryActionLabel`: `ReactNode` — default `"Back"`
- `isDismissible`: `boolean` — default `true`
- `onClose`: `() => void`
- `children`: `ReactElement | ReactNode`
- `surfaceOnly`: `boolean`

Rules:

- Well suited to guided walkthroughs (stepper + Back/Next).
- Pass a trigger child for anchored mode, or use surfaceOnly for static previews.
- caretPlacement sets both caret chrome and Popper anchor (e.g. bottomLeft → above trigger, start-aligned).
- content="custom" is an unpadded blank canvas — own padding and density in customContent.

Example:

```tsx
<Popover content="textOnly" title="Tour step" body="Learn about this control." stepperText="1/3"><Button>Open</Button></Popover>
```

## Drawer

Bottom sheet that slides over page content without dimming or blocking it.

Props:

- `type`: `"textOnly" | "customContent"` — default `"textOnly"`
- `title`: `ReactNode`
- `description`: `ReactNode`
- `hasDescription`: `boolean` — default `true`
- `hasActionRow`: `boolean` — default `true`
- `primaryActionLabel`: `ReactNode` — default `"Button"`
- `secondaryActionLabel`: `ReactNode` — default `"Button"`
- `isDismissible`: `boolean` — default `true`
- `onClose`: `() => void`
- `open`: `boolean` — default `false`
- `children`: `ReactNode`
- `surfaceOnly`: `boolean`

Rules:

- More prominent than inline content, less disruptive than a modal — use sparingly.
- Does not dim the page (no backdrop).

Example:

```tsx
<>
  <Button onClick={() => setOpen(true)}>Open drawer</Button>
  <Drawer open={open} onClose={() => setOpen(false)} type="textOnly" title="This is a heading" description="This is descriptive text." />
</>
```

## Dialog

Blocking confirmation overlay with short title, message, and usually two actions.

Props:

- `type`: `"default" | "iconTop" | "customContent"` — default `"default"`
- `title`: `ReactNode`
- `description`: `ReactNode`
- `hasImage`: `boolean` — default `false`
- `image`: `ReactNode`
- `topIconName`: `FaIconName | string` — default `"smile"` — Icon for the floating badge when type is iconTop (Figma shortcode; smile → face-smile).
- `hasSecondaryAction`: `boolean` — default `true`
- `primaryActionLabel`: `ReactNode` — default `"Button"`
- `secondaryActionLabel`: `ReactNode` — default `"Button"`
- `isDismissable`: `boolean` — default `true`
- `onClose`: `() => void`
- `open`: `boolean` — default `false`
- `children`: `ReactNode`
- `surfaceOnly`: `boolean`

Rules:

- Use for short decisions — prefer Modal for rich interactive content.
- Spelling isDismissable matches Figma.

Example:

```tsx
<>
  <Button onClick={() => setOpen(true)}>Open dialog</Button>
  <Dialog open={open} onClose={() => setOpen(false)} type="default" title="Dialog Title" description="Confirm this action." />
</>
```

## Modal

Blocking overlay for rich interactive content (forms, media, multi-step flows).

Props:

- `type`: `"default" | "verticalImage" | "horizontalImage"` — default `"default"`
- `title`: `ReactNode`
- `body`: `ReactNode` — Default body copy (pre-seed). Used when children is omitted — full body on default, text slot on image layouts.
- `image`: `ReactNode`
- `children`: `ReactNode` — Custom content for the body text slot (Figma customContent). Replaces body for every type; image layouts keep the fixed image slot.
- `hasSecondaryAction`: `boolean` — default `true`
- `primaryActionLabel`: `ReactNode` — default `"Button"`
- `secondaryActionLabel`: `ReactNode` — default `"Button"`
- `isDismissable`: `boolean` — default `true`
- `onClose`: `() => void`
- `open`: `boolean` — default `false`
- `surfaceOnly`: `boolean`

Rules:

- Unlike Dialog, Modal holds richer layouts with a header bar and footer actions.
- Long content scrolls inside the body; header and footer stay fixed.
- Pass children for custom body content (forms, media controls); body is the text pre-seed when children is omitted.
- Spelling isDismissable matches Figma.

Example:

```tsx
<>
  <Button onClick={() => setOpen(true)}>Open modal</Button>
  <Modal open={open} onClose={() => setOpen(false)} type="default" title="Title">
    <form>{/* custom content */}</form>
  </Modal>
</>
```

## ChatFileRemoveButton

Compact circular remove control for AI chat file chips in the composer.

Props:

- `disabled`: `boolean`
- `onClick`: `(event) => void`
- `aria-label`: `string` — default `"Remove"` — Accessible name for the icon-only action.

Rules:

- Prefer composing via AiChatFileChip useCase=inputField rather than placing alone.
- Hover/press use error fills to signal destructive remove.

Example:

```tsx
<ChatFileRemoveButton onClick={onRemove} aria-label="Remove attachment" />
```

## AiChatFileChip

Attachment chip for AI chat streams and the composer. File, image, and code-snippet types; inputField use case composes ChatFileRemoveButton.

Props:

- `type`: `"file" | "image" | "codeSnippet"` — default `"file"`
- `useCase`: `"chatStream" | "inputField"` — default `"chatStream"` — inputField shows the remove control.
- `fileName`: `ReactNode` — default `"filename.ext"`
- `metadata`: `ReactNode` — default `"12:56PM"` — Shown for type=codeSnippet.
- `imageSrc`: `string`
- `imageAlt`: `string` — default `"Attachment"`
- `iconName`: `FaIconName` — default `"file-code"` — Brand tile icon for file/codeSnippet.
- `onRemove`: `() => void`

Rules:

- Use chatStream in the message transcript; inputField in AiChatInput attachments.
- Pass imageSrc for type=image thumbnails.

Example:

```tsx
<AiChatFileChip type="file" useCase="inputField" fileName="lesson.pdf" onRemove={onRemove} />
```

## AiChatMessage

Chat bubble for human and AI turns. TA vs Tutor contexts restyle the human bubble; AI messages can include file uploads and a feedback action row.

Props:

- `context`: `"TA" | "Tutor"` — default `"TA"` — Teacher Assistant vs student Tutor product context.
- `author`: `"Human" | "AI"` — default `"Human"`
- `children`: `ReactNode` — Message body.
- `fileUploads`: `ReactNode` — Optional AiChatFileChip row above an AI bubble.
- `hasActionRow`: `boolean` — default `true`
- `hasLeftActions`: `boolean` — default `true`
- `hasDownload`: `boolean` — default `true` — Download control in the left action group.
- `hasRightActions`: `boolean` — default `true`
- `hasFlagging`: `boolean` — default `true`
- `feedbackLabel`: `ReactNode` — default `"Was this helpful?"`
- `onCopy`: `() => void`
- `onDownload`: `() => void`
- `helpfulValue`: `"up" | "down" | null` — Controlled thumbs feedback.
- `defaultHelpfulValue`: `"up" | "down" | null`
- `onHelpfulChange`: `(value: "up" | "down" | null) => void`
- `flagged`: `boolean`
- `defaultFlagged`: `boolean`
- `onFlagChange`: `(flagged: boolean) => void`

Rules:

- Human TA bubbles use brand-strong; Tutor human bubbles use info-mid.
- Action row (copy/download/feedback/flag) only renders for author=AI.
- Compose file chips via fileUploads rather than inventing a second message type.

Example:

```tsx
<AiChatMessage context="TA" author="AI">Sure — try a 10× name loop warm-up.</AiChatMessage>
```

## AiChatInput

AI chat composer combining a text area, optional attachments, Add file action, and send. Send enables when the field has content.

Props:

- `value`: `string`
- `defaultValue`: `string`
- `onChange`: `(event: ChangeEvent<HTMLTextAreaElement>) => void`
- `placeholder`: `string` — default `"Type something"`
- `leftActions`: `ReactNode` — Overrides the default Add file Button.
- `addFileLabel`: `ReactNode` — default `"Add file"`
- `onAddFile`: `() => void`
- `onSubmit`: `(event: FormEvent) => void`
- `disabled`: `boolean`
- `attachments`: `ReactNode` — AiChatFileChip row above the textarea.

Rules:

- Filled chrome (enabled send) is derived from non-empty value — do not pass a separate isFilled prop.
- Enter submits; Shift+Enter inserts a newline.
- Pass attachments as AiChatFileChip with useCase=inputField.

Example:

```tsx
<AiChatInput placeholder="Ask anything" onSubmit={onSend} onAddFile={onAttach} />
```

## FaIcon

Font Awesome 7 Pro (solid/regular) / Brands webfont glyph.

Props:

- `name`: `FaIconName | FaBrandIconName` **(required)**
- `family`: `"solid" | "regular" | "brands"` — default `"solid"`
- `size`: `"inherit" | "extraSmall" | "small" | "medium" | "large"` — default `"medium"`
- `title`: `string`

Rules:

- Import @codeai/cads-react/icons/fonts.css once at app root.
- Internal FA Pro license — do not publish fonts publicly.

Example:

```tsx
<FaIcon name="arrow-right" size="medium" />
```
