shadimage
quill
fileagent-rulessource: ok · refreshed

Quill agent rules

Claude Code project rules so AI agents build on-brand with Quill — the theming contract, foundations, principles, icon names and the block index, generated from the design system.

npx shadcn add "https://www.quilldesignsystem.com/r/agent-rules.json"
free

Where it goes

logo-cloud

Visual style

terminal-mono

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Files132 · source cached 10/8/2026, 4:44:15 AM

registry/agent-rules/quill.md
# Quill design system — rules for this app

<!-- Generated by quill-ds (scripts/build-agent-rules.mjs). Do not edit by hand: update with `npx shadcn@latest add @quill/agent-rules --overwrite`. -->

This app is built on Quill (https://www.quilldesignsystem.com): a token layer with five themes and four accents, 18 shipped components and 55 blocks, installed through the shadcn CLI. Read this before touching UI. Full reference: https://www.quilldesignsystem.com/llms.txt. Per-component rules: `https://www.quilldesignsystem.com/usage/<name>.md`.

## Theming contract

- Themes — set data-theme on <html>: unset (or data-theme="light") → Dawn (default) · data-theme="dark" → Dusk · data-theme="classic-light" → Classic Light · data-theme="classic-dark" → Classic Dark · data-theme="intelligent" → Intelligent. 5 themes in total.
- Accents — set data-accent on <html>, independently of the theme: terracotta · moss (default) · indigo · gold. The accent drives links, eyebrows, focus rings and accent italics. The two axes are independent.
- Runtime contract — the theme-selector block owns both attributes: after hydration it reads localStorage ("quill-theme", "quill-accent") and stamps <html>. Prerendered markup is always Dawn + moss, so first paint matches the server.
- Charts — use the chart tokens (--chart-series-1..5, the sequential and diverging ramps), never raw pigments. Assign series colours in that fixed order and never reorder or cycle survivors when a filter drops one: the order keeps the palette distinguishable under all three dichromacies.
- Fonts — Raleway (body) and Fraunces (display) load through the theme file's @import; no next/font. Never load them again under another name: the theme matches literal names.
- Wiring — the CLI install merges the theme into your Tailwind stylesheet. A theme FILE must be imported from inside that stylesheet — `@import "./quill-theme.css"` after `@import "tailwindcss"` and below any `@custom-variant dark` line shadcn wrote (the last wins) — never from layout.tsx, where every Quill utility is dead.
- dark: utilities — stock primitives' dark: tweaks follow Quill's dark themes (and the .dark class) once the theme is wired; nothing to add by hand.
- Self-check — after changing UI, run `node scripts/quill-check.mjs` (install once: `npx shadcn@latest add @quill/check`) and clear every finding: stock colours, raw colours, bracket values where a token exists, retired names, dead classes. An exception: `// quill-check: allow <rule> — <why>` on the line above.
- Updating — re-run with --overwrite, not --yes: on a file you have changed, --yes prompts in a terminal and silently skips when non-interactive.
- Full machine-readable reference for agents: https://www.quilldesignsystem.com/llms.txt. Per-component usage guides at /usage/<name>.md.

## Colour roles

Components speak 31 colour roles as classes. Several share a colour (`card` = `popover` = `sidebar`; `secondary` = `muted` = `accent`), so pick by the job, never by the look.
- **Surfaces** — `background` page · `card` raised, in the page · `popover` anything floating · `sidebar` the nav rail · `muted` quiet wells · `secondary` only through `variant="secondary"`.
- **Text** — `foreground` strong · `muted-foreground` supporting · a `-foreground` role is text ON that surface. Never `text-muted`: it is a paper tone and vanishes.
- **Actions** — `primary` is INK: the main action and on / done states, never a brand colour. `destructive` is errors, as a 10% tint with destructive text, never a solid fill.
- **`accent` is the pale highlight, not the brand accent.** The brand accent is the accent pigment; it arrives through `ring`, links and the one italic accent word.
- **Hover** — list and menu items highlight with `accent`; buttons, toggles and table rows wash with `muted`. Same colour, two jobs.
- **Lines** — `border` hairlines and dividers · `input` the edge of anything typed into or toggled (it holds 3:1; `border` does not) · `ring` keyboard focus, and the one selected item in a set.
- **Charts** — `--chart-1`…`--chart-5` in fixed order, never as UI colour.
- **Status, as text only** — `text-link` · `text-success` · `text-warning` · `text-info` · `text-working` · `text-queued`. Never a fill behind text. `text-link` is a prose link; a link-styled Button is ink.
- **Sanctioned tints** — `bg-destructive/10` · `ring-1 ring-foreground/10` · `bg-input/30` · `bg-muted/50` · `bg-primary/10` · `text-sidebar-foreground/70` · `ring-3 ring-ring/50` · `ring-3 ring-destructive/20` · `bg-input/50`. Reach for one before inventing another opacity.
- Every role with its use-for / never-for line: the Colour roles table in llms.txt.

## Foundations

### Type
Two voices. **Fraunces** (variable display serif) is the brand voice; **Raleway** does the work.

#### Families
- `--font-display`: `"Fraunces", Georgia, serif` — headings, wordmark, captions. `--font-heading` is the same face, behind the `font-heading` utility.
- `--font-sans`: `"Raleway", -apple-system, BlinkMacSystemFont, sans-serif` — body, UI, labels.
- `--font-mono`: `ui-monospace, "SF Mono", Menlo, monospace` — code and token specimens.
- `--font-ui` / `--font-data` — the Intelligent theme's instrument faces (`"Inter"`, `"JetBrains Mono"`); leave them to that theme.

Loaded from Google Fonts by the `@import` at the top of the shipped theme file. Never load the families again under another name: the theme matches them by their literal names. Weights used: **400 / 500 / 600**.

#### Fraunces variable axes
- `opsz` 9–144 matches optical size to render size · `SOFT` 0–100 rounds terminals as size grows · `WONK` 0 | 1 is the off-kilter glyph set, **accents only**.
- Presets: `--fraunces-display` (`"opsz" 144, "SOFT" 50, "WONK" 0`) · `--fraunces-accent` (`"opsz" 144, "SOFT" 100, "WONK" 1`, the italic emphasis) · `--fraunces-text` (`"SOFT" 50, "WONK" 0`) · `--fraunces-caption` (`"opsz" 14, "SOFT" 100, "WONK" 0`).
- Every Fraunces text is soft: `h2`–`h6` and `font-heading` text take `--fraunces-text` (optical size follows the render size), `h1` takes `--fraunces-display`, `.fraunces-accent` / `.fraunces-caption` are the exceptions. The base layer applies all of it.

#### Type scale
| Token | Size | Line | Use |
|---|---|---|---|
| `--text-2xs` | 0.7rem (11.2px) | inherits | micro labels, tag pills |
| `--text-xs` | 0.75rem (12px) | 133.3% (16px) | meta, eyebrows |
| `--text-sm` | 0.85rem (13.6px) | 142.9% (19.4px) | captions, fine print |
| `--text-base` | 0.95rem (15.2px) | 150% (22.8px) | UI text, buttons |
| `--text-lg` | 1.15rem (18.4px) | 155.6% (28.6px) | lead paragraphs |
| `--text-xl` | 1.5rem (24px) | 140% (33.6px) | card titles |
| `--text-2xl` | 2rem (32px) | 133.3% (42.7px) | sub-headings |
| `--text-3xl` | 3rem (48px) | 120% (57.6px) | section titles |
| `--text-4xl` | 4rem (64px) | 111.1% (71.1px) | page heads |
| `--text-5xl` | 5.5rem (88px) | 100% (88px) | hero display |

#### Leading, tracking, rules
- A `text-*` class sets its line height with its size (the Line column, shipped as `--text-sm--line-height` and so on), so bare UI text needs no `leading-*`.
- **Leading roles** — `leading-display` 1.05 (hero and h1 lines) · `leading-heading` 1.2 (h2–h6) · `leading-ui` 1.5 (controls and dense UI text) · `leading-reading` 1.7 (body and long-form copy).
- **Tracking roles** — `tracking-display` −0.03em (Fraunces headings) · `tracking-label` 0.1em (small uppercase labels and badges) · `tracking-eyebrow` 0.15em (eyebrows above a headline).
- Each role is a class and a variable (`leading-reading`, `--leading-reading`). They are named for the text they set: `leading-snug`, `tracking-tight` and `tracking-wide` are Tailwind's own and keep Tailwind's values, so never redefine them. `body` and `h1`–`h6` already carry their roles from the base layer.
- **Headings** — Fraunces at weight 400 (never heavy or bold), tight tracking; `h1` in `--fraunces-display`, every other heading and `font-heading` text in `--fraunces-text`.
- **The one accent word** — italicize exactly one word per headline in the accent (`--accent-pigment-text`, moss by default) with `--fraunces-accent`. Never two.
- **Body / UI** — Raleway 400/500/600; relaxed leading for reading copy.
- **Eyebrows / labels** — Raleway, uppercase, `--text-xs`, `tracking-eyebrow`, `--ink-muted`; the accent variant carries a short leading dash.
- **Captions** — small Fraunces italic in `--ink-muted` with `--fraunces-caption`.
- Sentence case in prose and headings; uppercase only for eyebrows.

### Spacing & layout
A **4px base step** (`--space-1` = 0.25rem). Editorial rhythm — sections breathe.

`--space-1` 4px · `--space-2` 8px · `--space-3` 12px · `--space-4` 16px · `--space-5` 20px · `--space-6` 24px · `--space-7` 28px · `--space-8` 32px · `--space-9` 36px · `--space-10` 40px · `--space-12` 48px · `--space-16` 64px · `--space-20` 80px · `--space-24` 96px

Components sit on `--space-4` / `--space-6`; sections breathe with `--space-24`. In a component write the class (`p-4`, `gap-6`): it is the same step, and no shipped component reads the variable. The variables are for hand-written CSS.

#### Layout
No layout tokens ship; use the values the site uses. Marketing max-width **1400px** (`max-w-[1400px]`), reading column **800px**, side padding **48px** on desktop (`px-12`) and **24px** on mobile (`px-6`), vertical section rhythm **96px** (`py-24`; `py-14` on mobile).

#### Composition
- Recurring section header: eyebrow → headline (one accent word) → right-aligned italic caption.
- One idea per section: a heading plus one composition (a grid of cards, a table, a form), never a stack of unrelated widgets.
- No hero-metric row above an identical card grid — that is the generic dashboard Quill exists to avoid. Lead with the thing the page is about; let numbers sit inside it.

### Effects
#### Corner radii
`--radius-xs` 2px · `--radius-sm` 4px · `--radius-md` 6px · `--radius-lg` 8px · `--radius-xl` 16px · `--radius-2xl` 24px · `--radius-3xl` 32px · `--radius-4xl` 40px; `--radius` (0.5rem) is the shadcn base.
Cards use `rounded-xl`, buttons and inputs `rounded-lg`, pills `rounded-full`.

#### Elevation
Shadows are ink-tinted, layered and re-cut per theme — never a hard black drop. `--shadow-xs` hairline lift · `--shadow-sm` low surfaces · `--shadow` raised popovers and menus · `--shadow-lg` dialogs · `--shadow-pop` the strongest lifted state. Cards sit flat on the paper with a hairline ring, no shadow.

#### Motion
House easing `--ease-out` (`cubic-bezier(0.4, 0, 0.2, 1)`); soft `--ease-soft` (`cubic-bezier(0.22, 1, 0.36, 1)`). Durations `--dur-fast` 0.2s · `--dur` 0.3s · `--dur-slow` 0.5s. Lifts `--lift` (translateY(-4px)) and `--lift-sm` (translateY(-2px)) for hover on interactive surfaces; presses settle 1px down. Nothing bounces, nothing loops; respect `prefers-reduced-motion`.

## Principles

Three principles name the point of view; the rules underneath are how they show up in code.

- **Paper first.** Every surface, a texture you can almost feel, with a typeset that has an unhurried editorial rhythm. Everything sits on digital paper (`--paper`, `--paper-warm`, `--paper-deep`); warmth comes from the material, not decoration.
- **One italic word.** Emphasis is earned. A single accented italic per headline — never two, never shouted.
- **A gentle settle.** Hovers lift, presses set down. Nothing bounces, nothing loops, nothing hurries you along.

### Rules
- **Author against semantic tokens** — `--paper`, `--ink`, `--card`, `--primary`, `--ring` and the rest; never the per-theme `dk-*` / `cl-*` / `cd-*` / `int-*` sets and never a raw hex. That is what makes every theme free.
- **Ink for actions, accent for meaning** — primary actions are ink (`--primary`). The accent pigment (moss by default; `--accent-pigment-text` for text, `--link`, `--ring`) is reserved for the one accent word, eyebrows, links and the focus ring. Terracotta is the danger pigment: never a hover colour, and a focus ring only when it is the chosen accent.
- **Reach for a block before building one** — the registry ships 55 composable blocks (activity feed, empty state, page header, theme selector, data table…). A hand-built copy drifts from the AA-checked tokens the moment it lands. When no block or primitive fits, compose from primitives and semantic tokens within these rules and name the gap, rather than inventing a new pattern.
- **Made for people** — WCAG 2.1 AA is a feature, not a checkbox: text cuts clear 4.5:1 on every theme ground, interactive borders clear 3:1, charts use the CVD-safe chart tokens in fixed order, motion has a reduced-motion path.
- **Content** — sentence case everywhere (uppercase only for eyebrows); state the decision, then the reason; no hype punctuation, no emoji.

### Do / Don't
**Do** — sit everything on digital paper; reserve the accent for the one accent word, eyebrows, links and the focus ring; use ink for primary actions; warm layered shadows; Fraunces light and tight for headings.

**Don't** — pure white or pure black in Dawn and Dusk (the Classic themes use them by design); a hand-set focus colour (`--ring` belongs to the accent axis); terracotta on hover; blue-purple gradients; any gradient except the AI gradient in its seven placements; glassmorphism or purple-glow dark mode; emoji; heavy or bold Fraunces; tight body leading; bouncy or looping motion.

**The AI gradient** is the only gradient Quill ships and means one thing: AI. Ember (gold → terracotta → indigo), fixed across accents, via the `ai-*` utilities only. Seven placements: the AI mark; the composer's edge; a top-to-bottom wash on an AI panel or popover; motion while AI works; one soft glow on an AI home page; gradient text on an AI feature's heading; the fill of an AI usage meter. Never on answer text; Accept, Replace or Send; charts; status colours; anything not AI. One of `ai-edge` / `ai-edge-working` per element (same background); the edge fills with `--background`, so use it on page-background surfaces.

### Anti-references
- The generic SaaS/shadcn default look: white cards, blue accents, hero-metric rows, identical card grids.
- The cold gray enterprise dashboard — a dashboard where a notebook belongs.
- Loud startup maximalism: neon gradients, glassmorphism, purple-glow dark mode.

## Icons

`<Icon name="…">` from `components/ui/icon.tsx` renders Material Symbols Outlined at weight 400. Only the core set ships to this app (98 names); any other name renders an empty, size-reserved box and a console warning in development. Decorative by default (`aria-hidden`); pass `aria-label` only when the icon stands alone. Core names:

account_balance, add, add_circle, archive, arrow_back, arrow_downward, arrow_forward, arrow_upward, attach_file, attach_money, calendar_today, cancel, chat_bubble, check, check_box, check_box_outline_blank, check_circle, chevron_left, chevron_right, close, cloud_upload, content_copy, credit_card, dangerous, dark_mode, dashboard, delete, description, dock_to_left, download, draft, edit, error, favorite, filter_list, folder_open, format_align_center, format_align_left, format_align_right, format_bold, format_italic, format_underlined, forward, group, help, history, home, info, ink_pen, keep, keyboard_arrow_down, keyboard_arrow_left, keyboard_arrow_right, keyboard_arrow_up, keyboard_command_key, language, light_mode, link, lock, login, logout, mail, menu, menu_book, mic, more_horiz, more_vert, notifications, open_in_new, palette, person, person_add, photo_library, progress_activity, radio_button_checked, radio_button_unchecked, refresh, remove, reply, save, schedule, search, send, settings, share, sort, space_dashboard, star, star_shine, stop, thumb_down, thumb_up, undo, upload, visibility, visibility_off, wallet, warning

## Blocks — reach for one before building

**Before choosing a block or a primitive, load the `quill-components` skill** (`.claude/skills/quill-components/`): when to use each one, what to reach for instead, and what to do when nothing fits. Install a block with `npx shadcn@latest add @quill/<name>`; update with `--overwrite`. The same guides are online at `https://www.quilldesignsystem.com/usage/<name>.md`. Blocks by primary intent:

- **auth** (Authentication and account access — sign-in, sign-up, recovery, verification): `forgot-password`, `login`, `login-minimal`, `login-oauth`, `login-split-panel`, `otp-verification`, `signup`, `signup-social`
- **form** (Structured data entry and validation): `contact-form`, `file-upload`, `wizard`
- **data-display** (Presenting records, lists, and metrics for reading): `activity-feed`, `badge-on-card`, `data-table`, `profile-card`, `stat-cards`
- **data-viz** (Charts and quantitative visuals): `analytics-charts`
- **navigation** (Moving around an app — bars, breadcrumbs, tabs, command menus): `command-palette`, `navbar`, `page-header`, `search-results`, `tabs-page`
- **app-shell** (Full-page layout scaffolding for application screens): `dashboard`, `sidebar-nav`
- **feedback** (System status — alerts, notifications, empty and error states): `alerts`, `announcement-banner`, `cookie-consent`, `empty-state`, `error-404`, `notifications`
- **marketing** (Landing-page and promotional sections): `feature-section`, `footer`, `hero`, `newsletter`, `pricing`, `stats-band`, `team-section`, `testimonial`
- **commerce** (Purchase, checkout, and billing): `checkout`, `invoice`, `order-summary`
- **messaging** (Conversational and inbox interfaces): `chat`, `list-detail`, `mail-shell`
- **scheduling** (Dates, calendars, and bookings): `calendar-page`, `calendar-range`
- **productivity** (Task and workflow management): `kanban`, `onboarding`
- **content** (Editorial and informational blocks): `faq`
- **settings** (Configuration and preferences): `theme-selector`, `settings`
- **ai** (AI features — composers, assistant pages, AI replies and agent controls): `ai-home`, `ai-chat`, `ai-side-panel`, `conversation-history`

## Examples — start from a page

Whole pages composed from the blocks above. Install one (`npx shadcn@latest add @quill/<name>`) and replace the sample content instead of composing from scratch; keep the order and the spacing.
- `example-app-page` — sidebar-nav → page-header → stat-cards → data-table
- `example-marketing-page` — navbar → hero → feature-section → pricing → testimonial → footer
- `example-auth-page` — login-split-panel

## Primitives

Stock shadcn components restyled by the token layer — install from shadcn (`npx shadcn@latest add button`), never hand-roll; `agent-steps`, `ai-badge`, `ai-button`, `ai-feedback`, `ai-mark`, `ai-message`, `ai-notice`, `ai-popover`, `ai-thinking`, `approval-card`, `citations`, `icon`, `model-picker`, `prompt-composer`, `question-card`, `suggested-prompts`, `tone-badge` and `usage-meter` come from `@quill/`. Which one to pick is in the `quill-components` skill, for: accordion, agent-steps, ai-badge, ai-button, ai-feedback, ai-mark, ai-message, ai-notice, ai-popover, ai-thinking, suggested-prompts, model-picker, alert, alert-dialog, approval-card, aspect-ratio, avatar, badge, breadcrumb, button, button-group, calendar, card, carousel, chart, checkbox, citations, collapsible, combobox, command, context-menu, dialog, drawer, dropdown-menu, empty, field, hover-card, icon, input, input-group, input-otp, item, kbd, label, menubar, native-select, navigation-menu, pagination, popover, progress, prompt-composer, question-card, radio-group, resizable, scroll-area, select, separator, sheet, sidebar, skeleton, slider, sonner, spinner, switch, table, tabs, textarea, toggle, toggle-group, tone-badge, tooltip, usage-meter.

## Updating and verifying

- Theme: `npx shadcn@latest add @quill/quill --overwrite`. Blocks: `npx shadcn@latest add @quill/<name> --overwrite`. This file: `npx shadcn@latest add @quill/agent-rules --overwrite`. `--yes` does not overwrite a changed file — it prompts in a terminal and silently skips when non-interactive.
- After UI changes: `npx tsc --noEmit`, `npm run lint`, then switch `data-theme` and `data-accent` on `<html>` and confirm both take effect.
registry/agent-rules/skills/quill-components/SKILL.md
---
name: quill-components
description: Choose the right Quill block or primitive for a UI request — when to use each, what to reach for instead, and what to do when nothing fits. Use before building or changing any page, screen, section, form, dialog, menu, table, list, chart or navigation in this app, and before hand-building anything a component might already do.
---

# Choosing Quill components

<!-- Generated by quill-ds (scripts/build-agent-rules.mjs). Do not edit by hand: update with `npx shadcn@latest add @quill/agent-rules --overwrite`. -->

## How to choose

1. Name the job the UI does (show records, collect input, confirm an action, move between pages), not the widget you picture.
2. Look for a block first. A block is a whole section already built on Quill tokens: install it with `npx shadcn@latest add @quill/<name>` and replace its sample content.
3. If no block fits, use primitives: stock shadcn restyled by Quill, installed with `npx shadcn@latest add <name>` (`agent-steps`, `ai-badge`, `ai-button`, `ai-feedback`, `ai-mark`, `ai-message`, `ai-notice`, `ai-popover`, `ai-thinking`, `approval-card`, `citations`, `icon`, `model-picker`, `prompt-composer`, `question-card`, `suggested-prompts`, `tone-badge` and `usage-meter` with `@quill/<name>`). Never hand-roll one.
4. Before you commit to a pick, read `reference/<name>.md` in this skill for it and for each candidate after "instead". Each guide says when to use it, when to reach for something else and why, its rules, and its accessibility notes. Pick the one whose "When to use" matches the job.
5. If nothing fits, compose from primitives and semantic tokens within `.claude/rules/quill.md`, and tell the person which part Quill does not cover. Do not invent a new visual pattern.

Each line below is: name — when to use it · instead: the components it is most often confused with.

## Blocks

### auth — Authentication and account access — sign-in, sign-up, recovery, verification.
- `forgot-password` — You need a password-reset request card that sends a recovery link. · instead: `otp-verification`, `login`
- `login` — You need the standard email-and-password sign-in with remember-me and reset links. · instead: `login-split-panel`, `login-minimal`, `login-oauth`, `forgot-password`
- `login-minimal` — You need the leanest possible sign-in — one email field that sends a one-time code, no password. · instead: `login`, `login-oauth`, `otp-verification`
- `login-oauth` — Identity comes from a provider (Google, GitHub, Apple) rather than a password you store. · instead: `signup-social`, `login`, `login-minimal`
- `login-split-panel` — You need a branded full-page sign-in with a testimonial side panel and magic-link option. · instead: `login`, `login-minimal`
- `otp-verification` — You need to verify an email with a six-digit one-time passcode and resend. · instead: `login-minimal`, `forgot-password`
- `signup` — You need standard account creation — name, email, password, and a sign-in link. · instead: `signup-social`, `login`
- `signup-social` — You need signup that leads with OAuth providers and falls back to email/password. · instead: `signup`, `login-oauth`

### form — Structured data entry and validation.
- `contact-form` — You need a get-in-touch form with name, email, topic, and message fields. · instead: `newsletter`, `settings`
- `file-upload` — You need drag-and-drop asset upload with a progress-tracked queue. · instead: `contact-form`
- `wizard` — You need a multi-step setup flow with a progress stepper and back/continue navigation. · instead: `onboarding`, `settings`

### data-display — Presenting records, lists, and metrics for reading.
- `activity-feed` — You need to show a chronological history of what happened — recent events with who, what, and when. · instead: `notifications`, `data-table`
- `badge-on-card` — You need to show status or category badges attached to a content card. · instead: `tone-badge`, `data-table`
- `data-table` — You need a filterable, actionable table of records with tone-mapped status pills and row actions. · instead: `activity-feed`, `kanban`
- `profile-card` — You need a compact user profile with avatar, role, stats, and follow / message actions. · instead: `activity-feed`, `data-table`
- `stat-cards` — You need a row of KPI cards showing values with period-over-period deltas. · instead: `analytics-charts`, `data-table`

### data-viz — Charts and quantitative visuals.
- `analytics-charts` — You need a dashboard view of trends over time — an area chart plus a bar chart on card surfaces. · instead: `stat-cards`, `data-table`

### navigation — Moving around an app — bars, breadcrumbs, tabs, command menus.
- `command-palette` — You need a searchable ⌘K launcher for actions and quick navigation. · instead: `command`, `search-results`, `dropdown-menu`
- `navbar` — You need a top navigation bar with wordmark, links, and sign-in / get-started actions. · instead: `sidebar-nav`, `command-palette`
- `page-header` — You need a screen header with breadcrumbs, title, description, and primary actions. · instead: `sidebar-nav`, `hero`
- `search-results` — You need a search panel with a query input, result count, and a linked result list. · instead: `command-palette`, `data-table`
- `tabs-page` — You need a settings screen split into account, notifications, and security tabs. · instead: `settings`, `wizard`

### app-shell — Full-page layout scaffolding for application screens.
- `dashboard` — You need a full application home — sidebar, search header, and KPI cards. · instead: `sidebar-nav`, `stat-cards`
- `sidebar-nav` — You need a collapsible icon-sidebar app shell with grouped menus and a content area. · instead: `dashboard`, `mail-shell`

### feedback — System status — alerts, notifications, empty and error states.
- `alerts` — You need to surface inline status messages of varying severity (info, success, destructive) within a page. · instead: `sonner`, `announcement-banner`, `empty-state`
- `announcement-banner` — You need to announce something site-wide with a dismissible banner, either inline or full-bleed. · instead: `alerts`, `cookie-consent`
- `cookie-consent` — You need a compliance banner offering accept, reject, and preferences choices. · instead: `announcement-banner`, `alerts`
- `empty-state` — You need to explain why a view is empty and offer the primary action to fill it. · instead: `empty`, `error-404`, `onboarding`
- `error-404` — You need a full-page not-found state with recovery actions. · instead: `empty-state`, `alerts`
- `notifications` — You need a notifications center with unread indicators and a mark-all-read action. · instead: `activity-feed`, `sonner`

### marketing — Landing-page and promotional sections.
- `feature-section` — You need to present product features in an editorial multi-column grid. · instead: `stats-band`, `pricing`
- `footer` — You need a site-wide footer with link columns, a brand blurb, and legal links. · instead: `sidebar-nav`, `navbar`
- `hero` — You need the top-of-page marketing statement — headline, supporting copy, and calls to action. · instead: `feature-section`, `announcement-banner`
- `newsletter` — You need an email-capture section with social proof to grow a list. · instead: `contact-form`, `footer`
- `pricing` — You need a tiered pricing grid with a highlighted plan and feature checklists. · instead: `feature-section`, `order-summary`
- `stats-band` — You need a marketing band of headline statistics separated by dividers. · instead: `stat-cards`, `feature-section`
- `team-section` — You need a marketing team grid with member cards, roles, and contact actions. · instead: `profile-card`, `testimonial`
- `testimonial` — You need a customer-quote card with a serif pull-quote and attribution. · instead: `team-section`, `stats-band`

### commerce — Purchase, checkout, and billing.
- `checkout` — You need to collect payment and card details alongside an order summary. · instead: `order-summary`, `wizard`
- `invoice` — You need a printable invoice with parties, line-item totals, and a download action. · instead: `order-summary`, `data-table`
- `order-summary` — You need a cart-review card with line items, a promo field, a total, and a pay button. · instead: `invoice`, `checkout`

### messaging — Conversational and inbox interfaces.
- `chat` — You need a one-on-one messaging panel with a contact header, bubbles, and a composer. · instead: `list-detail`, `notifications`
- `list-detail` — You need an inbox-style split view pairing a selectable list with a detail pane. · instead: `mail-shell`, `chat`
- `mail-shell` — You need a two-pane mail client — searchable list plus a reading pane with toolbar and reply. · instead: `list-detail`, `chat`

### scheduling — Dates, calendars, and bookings.
- `calendar-page` — You need to pick a single date and show what's scheduled that day. · instead: `calendar-range`, `activity-feed`
- `calendar-range` — You need to book a span of days with a two-month range picker and confirm/clear actions. · instead: `calendar-page`, `checkout`

### productivity — Task and workflow management.
- `kanban` — You need to manage work across status columns with draggable, tagged task cards. · instead: `data-table`, `onboarding`
- `onboarding` — You need a setup checklist that tracks progress and links to remaining steps. · instead: `wizard`, `empty-state`

### content — Editorial and informational blocks.
- `faq` — You need to answer common questions in an accordion with a contact fallback. · instead: `accordion`, `contact-form`

### settings — Configuration and preferences.
- `theme-selector` — You need to let users switch among Quill's five themes and four accents, persisted to localStorage. · instead: `settings`
- `settings` — You need a profile settings form with name, bio, and notification preferences. · instead: `contact-form`, `wizard`

### ai — AI features — composers, assistant pages, AI replies and agent controls.
- `ai-home` — An app has a page whose main job is talking to its AI assistant, and people land on it with nothing typed yet. · instead: `prompt-composer`, `chat`
- `ai-chat` — An app has a page for an ongoing conversation with its AI assistant. · instead: `ai-home`, `chat`
- `ai-side-panel` — People are working on a page (a report, a record, a document) and should be able to ask the AI about it without leaving. · instead: `ai-chat`, `sheet`, `ai-popover`
- `conversation-history` — People come back to earlier conversations with an AI and need to find one, reopen it, and tidy the list. · instead: `ai-chat`, `sidebar-nav`, `command-palette`

## Primitives

- `accordion` — You have several related content sections (FAQ answers, grouped settings) that don't all need to stay visible at once, and users scan headings before choosing one to open. · instead: `collapsible`, `tabs`
- `agent-steps` (`@quill/`) — An AI agent does a task in several steps and people should see where it is, what is finished and what went wrong. · instead: `ai-thinking`, `progress`, `wizard`
- `ai-badge` (`@quill/`) — A card, field, list row or message holds content the AI wrote or proposed, and people should know before they rely on it. · instead: `tone-badge`, `ai-mark`
- `ai-button` (`@quill/`) — A button starts something an AI does: Summarize, Rewrite, Ask AI, Draft a reply. · instead: `button`, `prompt-composer`
- `ai-feedback` (`@quill/`) — Someone has marked an AI answer as bad and you want to learn why, without taking them away from the answer. · instead: `ai-message`, `dialog`, `contact-form`
- `ai-mark` (`@quill/`) — You need to show that something is AI: an AI button, an AI badge, the assistant avatar, an AI menu item, or the greeting on an AI page. · instead: `ai-badge`, `icon`
- `ai-message` (`@quill/`) — A conversation shows answers from an AI, with or without the reasoning and sources behind them. · instead: `chat`, `ai-popover`
- `ai-notice` (`@quill/`) — A composer sends to an AI and people should know its answers need checking. · instead: `alert`, `ai-badge`
- `ai-popover` (`@quill/`) — Someone selects text (or presses an AI action on a field) and the AI offers a rewrite they can take or leave without leaving the page. · instead: `popover`, `ai-message`
- `ai-thinking` (`@quill/`) — An AI takes a noticeable moment before answering and people should see it is working, and later how it got there. · instead: `spinner`, `skeleton`
- `suggested-prompts` (`@quill/`) — An AI page is empty and people need a first thing to ask. · instead: `command`, `button`
- `model-picker` (`@quill/`) — An app offers more than one AI model or speed and people may want to switch per message. · instead: `select`, `radio-group`
- `alert` — You need one persistent, inline status message (informational or error) attached to a specific section of a page — not a whole stack of them, and not transient feedback about an action just taken. · instead: `alerts`, `sonner`
- `alert-dialog` — You need the user to explicitly confirm or cancel a destructive, irreversible action — deleting a record, discarding unsaved work, removing access. · instead: `dialog`
- `approval-card` (`@quill/`) — An AI agent is about to do something on a person's behalf (send, post, pay, delete) and needs a yes first. · instead: `alert-dialog`, `agent-steps`
- `aspect-ratio` — You're placing an image, video, or embed that must keep a consistent shape (16:9, 1:1, 4:3) regardless of its container's width, and want to reserve that space before the media loads. · instead: `resizable`
- `avatar` — You need to represent a person or entity visually — a profile photo, initials, or a generic icon — in a list row, header, or comment. · instead: `item`
- `badge` — You need a compact visual label for a status, category, or count next to other content — a course card, a table cell, a nav item. · instead: `tone-badge`, `button`
- `breadcrumb` — You need to show where the current page sits inside a multi-level hierarchy (Home > Courses > Watercolor Basics) so users can jump back up a level. · instead: `page-header`
- `button` — You need to trigger an action — submit, confirm, start a flow — rather than navigate to a page. · instead: `button-group`, `toggle`
- `button-group` — You have several related Buttons that should read as one compound control — sequential actions like prev/next, or a segmented set of mutually exclusive views. · instead: `button`, `toggle-group`
- `calendar` — You're composing your own date-picking UI directly — a form field, a filter, a custom layout — rather than reaching for a pre-built scheduling pattern. · instead: `calendar-page`, `calendar-range`, `popover`
- `card` — You need to group related content (a course, a stat, a settings section) in a bounded surface with consistent padding and optional header/footer regions. · instead: `item`
- `carousel` — You need to page through a set of same-shaped items (cards, images) one view at a time within a constrained width. · instead: `scroll-area`, `tabs`
- `chart` — You need a bar, line, area, or other Recharts chart that automatically re-colors across every Quill theme and stays colorblind-safe. · instead: `data-table`, `stat-cards`
- `checkbox` — You need one or more independent boolean choices — accept terms, opt into a list, select several rows in a table. · instead: `radio-group`, `switch`
- `citations` (`@quill/`) — An AI answer draws on files, pages or records people may want to check. · instead: `hover-card`, `badge`
- `collapsible` — You need one standalone show/hide toggle — a "view more" panel, an expandable filter section — without Accordion's multi-item structure. · instead: `accordion`
- `combobox` — You need to filter a long option list by typing, or let users select multiple values as removable chips. · instead: `select`, `native-select`
- `command` — You need a searchable, keyboard-navigable list of actions or destinations — settings search, a ⌘K command palette. · instead: `command-palette`, `combobox`, `dropdown-menu`
- `context-menu` — You need a secondary set of actions on an item that most users will discover by right-clicking (desktop) or long-pressing (touch), not a primary action path. · instead: `dropdown-menu`
- `dialog` — You need the user to complete a focused task without leaving the page — edit a record, fill a short form, confirm a reversible action. · instead: `alert-dialog`, `sheet`, `drawer`, `popover`
- `drawer` — You need a mobile-first interaction — filters, quick actions, secondary navigation — that slides in from an edge and can be dragged closed. · instead: `sheet`, `dialog`
- `dropdown-menu` — You need a set of actions or choices behind one visible trigger button — a row's "more actions" menu, a settings menu. · instead: `context-menu`, `menubar`
- `empty` — A list, table, or search result surface has no data to show yet and you need to explain why, plus give the user a way to fix it (create, upload, search again). · instead: `empty-state`, `skeleton`
- `field` — You have a form control that needs a label, optional helper text, and/or a validation error message wired together. · instead: `label`
- `hover-card` — You need a richer preview on hover — a link destination, an author bio, expanded metadata — without navigating away. · instead: `tooltip`, `popover`
- `icon` (`@quill/`) — You need a glyph inside a Quill block or beside text — a nav item, a status row, an inline affordance — and want it to match the outlined, weight-400 set every block already uses. · instead: `button`, `spinner`
- `input` — You need free-form single-line text entry — an email, a name, a search query, a URL. · instead: `textarea`, `input-otp`, `input-group`
- `input-group` — The field needs a prefix or suffix merged into the same bordered control — a search icon, a currency symbol, a unit label, a copy or clear button. · instead: `input`, `combobox`
- `input-otp` — You're collecting a fixed-length code (verification, 2FA, PIN) and want one boxed slot per character instead of a single text field. · instead: `input`
- `item` — You need a repeatable list row with an optional leading icon or avatar, a title and description, and a trailing action — a course row, a search result, a settings line. · instead: `card`, `table`
- `kbd` — You need to reference a keyboard key or shortcut inline in text, a menu item, or a tooltip (e.g. "Press ⌘K to search").
- `label` — Every form control needs a Label — even when a placeholder or surrounding text seems to explain it. · instead: `field`
- `menubar` — You need a persistent horizontal row of top-level menus — editor toolbars, desktop-app chrome — not a single button-triggered menu. · instead: `dropdown-menu`, `navigation-menu`
- `native-select` — You need a short, simple option list and want the OS-native picker (mobile scroll wheel, keyboard type-ahead, zero extra JS) instead of a custom-rendered popover. · instead: `select`, `combobox`
- `navigation-menu` — You need a horizontal row of site-level navigation, some entries opening a dropdown panel of links, others going straight to a page. · instead: `menubar`
- `pagination` — You have a long list of records split across pages and need numbered links to jump between them, not just scroll further. · instead: `scroll-area`, `button`
- `popover` — You need a compact, interactive surface anchored to a button — an inline form, a filter, a quick edit — that opens on an explicit click. · instead: `dialog`, `hover-card`, `tooltip`
- `progress` — You have a real, known completion percentage to show (upload progress, a multi-step setup) — not an open-ended wait with no known duration. · instead: `spinner`, `skeleton`
- `prompt-composer` (`@quill/`) — People type a request to an AI: an assistant page, a chat thread, a side panel, or an "ask about this" bar. · instead: `ai-home`, `chat`, `textarea`
- `question-card` (`@quill/`) — An AI agent cannot go on without a choice only the person can make, and there are a few sensible answers. · instead: `radio-group`, `approval-card`
- `radio-group` — You need the user to choose exactly one option from a short, always-visible list — not collapsed behind a dropdown trigger. · instead: `checkbox`, `select`, `toggle-group`
- `resizable` — You need a layout region whose split the user can adjust themselves by dragging — a file-tree/editor/preview layout, a resizable sidebar-and-content pane. · instead: `scroll-area`, `tabs`
- `scroll-area` — You need a scrollable region (a list, a card body) with a consistently styled scrollbar instead of each OS/browser's native chrome, inside an explicit height or width you set. · instead: `pagination`, `resizable`
- `select` — You need a dropdown of up to roughly 20 options with a consistent, custom-rendered popup (grouped labels, separators) rather than the OS-native picker. · instead: `native-select`, `combobox`
- `separator` — You need a plain visual divider between grouped content — stacked sections, or inline items in a toolbar — with no label or content of its own.
- `sheet` — You need secondary context or a task panel sliding in from an edge, with the underlying page still visible — settings, item detail, a share menu. · instead: `drawer`, `dialog`
- `sidebar` — You're assembling an app-shell nav rail yourself from SidebarGroup/SidebarMenu parts and need collapse-to-icon or off-canvas mobile behavior — reach for the `sidebar-nav` pattern directly if you just need the finished, pre-composed shell. · instead: `sidebar-nav`, `navbar`
- `skeleton` — You need a loading placeholder for content that hasn't arrived yet (a list row, an avatar, a card) and want to reserve its layout space instead of causing a jump when the real content appears. · instead: `empty`
- `slider` — You need the user to pick one number, or a two-ended range, between a min and max by dragging rather than typing. · instead: `input`, `progress`
- `sonner` — You need brief, self-dismissing feedback about an action the user just took (saved, deleted, upload failed) that shouldn't block the page or require explicit dismissal. · instead: `alert`, `alerts`
- `spinner` — You need to show that something is loading but have no measurable percentage to report. · instead: `progress`, `skeleton`
- `switch` — You need a boolean setting that applies the moment it is flipped — notifications, dark mode, a feature flag. · instead: `checkbox`, `toggle`
- `table` — You need to display structured, comparable data in rows and columns (a course catalogue, a settings list, a pricing grid) where scanning down a column matters. · instead: `item`, `data-table`
- `tabs` — You need to switch between a small number of related content panels (Overview/Curriculum/Reviews) that occupy the same space, without navigating to a new page. · instead: `accordion`, `toggle-group`
- `textarea` — You need multi-line free-form text entry where a single-line Input would truncate or wrap awkwardly. · instead: `input`, `input-group`
- `toggle` — You need a single button whose on/off (pressed) state sticks after clicking — bold, italic, a filter chip. · instead: `button`, `switch`, `toggle-group`
- `toggle-group` — You have several related two-state toggles that should behave, size, and style as one group rather than as standalone Toggles. · instead: `toggle`, `radio-group`, `button-group`
- `tone-badge` (`@quill/`) — You need Quill's uppercase tag pill for a status, tier, or label — trusted/current, developing, red flag, dormant, building — and want the tone-to-pigment mapping guaranteed correct and AA-checked. · instead: `badge`
- `tooltip` — You need a one-line supplementary hint on a control that already has its own visible label or accessible name. · instead: `popover`, `hover-card`
- `usage-meter` (`@quill/`) — People spend a limited allowance on AI (credits, messages, tokens) and should see what is left before it runs out. · instead: `progress`, `stat-cards`, `ai-notice`
registry/agent-rules/skills/quill-components/reference/accordion.md
# accordion (component)

A vertically stacked set of expand/collapse sections — one open at a time by default, with an option to allow several.

### When to use
- You have several related content sections (FAQ answers, grouped settings) that don't all need to stay visible at once, and users scan headings before choosing one to open.

### Reach for instead
- **collapsible** — when you need a single standalone toggle, not a group of items sharing expand/collapse semantics.
- **tabs** — when the sections should swap in place behind a row of triggers instead of stacking and expanding vertically.

### Rules
- **Do:** Leave `multiple` unset for content where only one answer should be open at a time (FAQs) — opening a new item closes the previous one. **Don't:** Turn on `multiple` for content that reads as a single answer set — several open items at once get hard to compare and scroll past.
- **Do:** Leave the trigger's hover state as an underline with no background fill, as the default styling does, so the hairline-bordered list stays visually light. **Don't:** Add a background fill on hover — it turns each row into a heavy button and fights the intentionally minimal, hairline-divided look.

### Accessibility
- AccordionTrigger renders a real `<button>` with `aria-expanded` and `aria-controls` pointing at its panel — Enter and Space toggle it like any native disclosure control.
- The up/down chevron icons are decorative and already `aria-hidden` by default (Icon only exposes an icon to assistive tech when you pass it an explicit `aria-label`) — the open/closed state itself is announced through `aria-expanded`, not the icon swap.
- `aria-disabled` removes an item's trigger from interaction without hiding it, so screen reader users still see the option exists, just unavailable.

### Design tokens
`--ink` · `--line-soft` · `--radius-lg` · `--ring`

registry/agent-rules/skills/quill-components/reference/activity-feed.md
# activity-feed (pattern)

A card timeline of recent events — avatar, action text, and a relative timestamp per entry.

### When to use
- You need to show a chronological history of what happened — recent events with who, what, and when.

### Reach for instead
- **notifications** — when the events need an unread/read state and a mark-all-read action, not just a read-only history.
- **data-table** — when the events are really records you need to filter, sort, or act on, not just skim chronologically.

### Rules
- **Do:** Use relative timestamps ("2 hours ago") for recent activity — they read faster than absolute dates. **Don't:** Force users to parse a full timestamp for something that happened minutes ago.

### Accessibility
- The timeline is a real `<ol>` — event order is semantic, not just visual.
- Each avatar's initials are the accessible fallback content; the icon next to the timestamp is decorative (aria-hidden).

### Design tokens
`--card` · `--border` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/agent-steps.md
# agent-steps (component)

Shows the steps an AI agent is working through — done, running, waiting or failed — with a count in the header and rows that open to show what happened.

### When to use
- An AI agent does a task in several steps and people should see where it is, what is finished and what went wrong.

### Reach for instead
- **ai-thinking** — when the AI is only thinking before one answer; there are no separate steps to follow.
- **progress** — when you know a percentage and there is nothing to read step by step.
- **wizard** — when the person, not the AI, moves through the steps.

### Rules
- **Do:** Leave the ticks and the spinner in moss; the AI mark in the header already says an agent is working. **Don't:** Paint progress with the AI gradient — the gradient marks AI, moss marks progress.
- **Do:** Let finished steps go muted so the running step stands out. **Don't:** Strike finished steps through — long labels become hard to read.
- **Do:** Put what a step found in its detail, opened by pressing the row. **Don't:** Print every step's detail in the list — the list has to stay scannable.

### Accessibility
- The card is a group named by its title; the steps are an ordered list.
- Every row starts with its state in words for screen readers ("Done:", "Running:", "Waiting:", "Failed:"), so the state never rests on the icon or its colour.
- A row with detail is a button with aria-expanded that controls the detail, with the same 3px focus ring as a Button. A row without detail is plain text, not a button.
- A polite live region (role="status") is on the page from the first paint, empty. While a step is running or has failed it says which one and the count ("Running: Drafting the summary. 2 of 5 done") and follows the list as it changes; the visible count is hidden from screen readers while the region speaks, so it is not read twice. A list that is already finished when it appears announces nothing.
- Retry is a Button named "Retry: " followed by the step.
- Under reduced motion the spinner holds still; the row still says "running".

### Design tokens
`--card` · `--border` · `--input` · `--muted` · `--muted-foreground` · `--foreground` · `--ink-soft` · `--moss-deep` · `--terracotta-deep` · `--paper` · `--destructive` · `--ai-from` · `--ai-via` · `--ai-to`

registry/agent-rules/skills/quill-components/reference/ai-badge.md
# ai-badge (component)

A small outline pill with the AI sparkle that marks content an AI made or suggested — "AI draft", "Suggested".

### When to use
- A card, field, list row or message holds content the AI wrote or proposed, and people should know before they rely on it.

### Reach for instead
- **tone-badge** — when the label is a status, tier or category rather than "made by AI".
- **ai-mark** — when there is no room for words and the mark alone is enough.

### Rules
- **Do:** Use the AI badge to say who made the content: "AI draft", "Suggested", "Generated". **Don't:** Use it as a status ("Active", "New") — status belongs to ToneBadge.

### Accessibility
- Renders through Badge as a non-interactive span; the words carry the meaning and the mark is decorative.

### Design tokens
`--ai-from` · `--ai-via` · `--ai-to` · `--ink-soft`

registry/agent-rules/skills/quill-components/reference/ai-button.md
# ai-button (component)

A stock Quill button that runs an AI action, marked with the AI sparkle; the label stays plain ink.

### When to use
- A button starts something an AI does: Summarize, Rewrite, Ask AI, Draft a reply.

### Reach for instead
- **button** — when the action does not involve AI.
- **prompt-composer** — when the person needs to type what they want rather than press one action.

### Rules
- **Do:** Let the sparkle carry the gradient and keep the label in the button's normal text colour. **Don't:** Fill the button or its label with the AI gradient — the gradient belongs on the mark, and a gradient fill competes with the page.
- **Do:** Use a plain Button for Accept, Replace and Use this, even inside an AI popover. **Don't:** Make the button that applies an AI suggestion an AiButton — acting on AI output is an ordinary decision.

### Accessibility
- Inherits Button semantics and focus ring; the mark is decorative because the label already says what happens.
- For an icon-only AI button, use Button with `size="icon"` and an `AiMark` with `label`.

### Design tokens
`--ai-from` · `--ai-via` · `--ai-to`

registry/agent-rules/skills/quill-components/reference/ai-feedback.md
# ai-feedback (component)

Asks what was wrong after a thumbs-down on an AI answer — one reason to choose from a short list, an optional note, and Send — opening under the answer so what is being judged stays in view.

### When to use
- Someone has marked an AI answer as bad and you want to learn why, without taking them away from the answer.

### Reach for instead
- **ai-message** — when the thumbs alone are all you collect; they are already on the reply.
- **dialog** — when the feedback is a longer survey that needs the person's whole attention.
- **contact-form** — when people are writing to your team, not rating one answer.

### Rules
- **Do:** Offer about six short reasons, your own or the defaults. **Don't:** List every reason you can think of — a long dropdown is as unread as a wall of chips.
- **Do:** Let Send work with a reason alone, or a note alone. **Don't:** Force people to type before they can send — most will leave instead.
- **Do:** Open the form under the answer it is about (pass it to AiMessage as feedbackForm). **Don't:** Cover the answer with a dialog — people need to see what they are judging.
- **Do:** Keep the line that says what is sent with the feedback, in your own words if the defaults are not true for your app. **Don't:** Send the conversation along without saying so.

### Accessibility
- The form is a group named by its question. The reason is one stock Select, a combobox named by that same question ("What was wrong?"), showing "Choose a reason" until one is chosen; its list opens under it and is read as a listbox of options, with the 3px focus ring of the stock field.
- The note is a text area named by its label. Send is off until a reason is chosen or something is typed; spaces do not count. When only a note is written, the reason is null.
- A polite live region (role="status") is on the page from the start, empty. After Send the form is replaced by one line there ("Thanks, that helps."), and keyboard focus moves to that line, because the Send button it was on is gone.
- Close is an icon button named "Close", drawn only when onClose is given. Inside an AiMessage, closing the form returns focus to the thumbs-down button.
- Inside an AiMessage the form follows the thumbs in reading order, so the next Tab after pressing thumbs-down lands in it. Focus is not moved into it on its own.

### Design tokens
`--card` · `--border` · `--input` · `--background` · `--foreground` · `--muted` · `--muted-foreground` · `--ink-soft` · `--ring` · `--popover` · `--popover-foreground` · `--accent`

registry/agent-rules/skills/quill-components/reference/ai-mark.md
# ai-mark (component)

Quill's AI sparkle — the one icon that means AI, drawn in the AI gradient with a one-star cut for 16px and under.

### When to use
- You need to show that something is AI: an AI button, an AI badge, the assistant avatar, an AI menu item, or the greeting on an AI page.

### Reach for instead
- **ai-badge** — when you are labelling a piece of content as AI-made; the badge carries the mark and the words.
- **icon** — when the icon means anything other than AI — every other icon comes from the Material Symbols set.

### Rules
- **Do:** Use the AI mark only where the thing it sits on calls an AI or was made by one. **Don't:** Use the sparkle as decoration, a "new" marker or a rating star — once it appears on non-AI things it stops meaning AI.
- **Do:** Let the mark draw its own gradient at every size; pass `size`, never a colour. **Don't:** Recolour the mark with a text colour or a single pigment — the gradient is what makes it read as AI.
- **Do:** Pass `label` when the mark is the only thing that says "AI" (an icon-only button or avatar). **Don't:** Leave a standalone mark unlabelled — screen readers hear nothing.

### Accessibility
- Decorative by default (`aria-hidden`), because it usually sits beside words that already say AI.
- With `label`, it renders as `role="img"` with that accessible name.
- Each instance generates its own gradient ids, so several marks on one page all render in colour.

### Design tokens
`--ai-from` · `--ai-via` · `--ai-to`

registry/agent-rules/skills/quill-components/reference/ai-message.md
# ai-message (component)

An AI's reply in a conversation — the AI mark on its avatar, the answer as plain readable text, and Copy, Try again and thumbs under it. Ships with UserMessage for the person's side.

### When to use
- A conversation shows answers from an AI, with or without the reasoning and sources behind them.

### Reach for instead
- **chat** — when two people are messaging each other; no AI is involved.
- **ai-popover** — when the AI suggests an edit to selected text rather than answering in a thread.

### Rules
- **Do:** Keep the answer in plain body text; the mark on the avatar already says it is AI. **Don't:** Colour the answer, its bubble or its headings with the AI gradient — answers have to be easy to read at length.
- **Do:** Pass streaming while the answer arrives; the actions wait until it is done. **Don't:** Show Copy and thumbs on a half-written answer — there is nothing finished to copy or judge yet.

### Accessibility
- Each reply is an article; while streaming it is marked aria-busy. To have replies announced as they arrive, render the thread inside a live region (the AI chat pattern uses role="log"). A reply still streaming in should not be re-read chunk by chunk: keep it aria-busy and, where your app streams text, announce the finished answer once instead of every update. Support varies between screen readers, so test with the ones your people use.
- The avatar is decoration, so the name is the reply's only speaker label. hideName takes the name out of view where something nearby already says who is answering (the AI side panel's header); screen readers still read it, and the answer starts level with the avatar. With hideName, leave thinking out when there is none: a component passed there that renders nothing still counts as the first row.
- Pass no children until there is text. An answer made of a custom component that renders nothing still counts as an answer, so it shows a caret and Copy.
- "You stopped this answer." is announced: it appears inside a status region that is already on the page.
- Action buttons have names ("Copy", "Try again", "Good answer", "Bad answer"); the thumbs use aria-pressed.
- Copy says "Copied" for two seconds after a successful copy, and stays "Copy" if the browser refuses.
- A feedback form passed as feedbackForm is drawn under the actions only while thumbs-down is pressed, after the thumbs in reading order; when it closes itself, focus returns to the thumbs-down button.
- The form is not drawn while the reply is streaming; if the answer is retried while it is open, what was picked or typed in it is lost.

### Design tokens
`--foreground` · `--muted-foreground` · `--border` · `--card` · `--background` · `--ai-from` · `--ai-via` · `--ai-to`

registry/agent-rules/skills/quill-components/reference/ai-notice.md
# ai-notice (component)

One quiet line under an AI composer saying the AI can be wrong, with an optional link the app supplies.

### When to use
- A composer sends to an AI and people should know its answers need checking.

### Reach for instead
- **alert** — when something has actually gone wrong and needs the person to act.
- **ai-badge** — when one piece of content was made by AI and needs labelling where it sits.

### Rules
- **Do:** Keep the notice to one small centred line under the composer, always present. **Don't:** Turn it into a banner, a callout box or a toast — it is there every time, so it stays quiet.
- **Do:** Leave it in muted ink, which passes 4.5:1 in every theme. **Don't:** Lighten it further with opacity — a disclaimer people cannot read does not inform them.

### Accessibility
- Plain paragraph text; the optional link is a normal underlined link with its own words.

### Design tokens
`--muted-foreground`

registry/agent-rules/skills/quill-components/reference/ai-popover.md
# ai-popover (component)

An AI suggestion beside the text it is about — what the AI did, the suggested text on a soft AI wash, then Discard, Try again and Replace.

### When to use
- Someone selects text (or presses an AI action on a field) and the AI offers a rewrite they can take or leave without leaving the page.

### Reach for instead
- **popover** — when the overlay holds an ordinary form or filter, not an AI suggestion.
- **ai-message** — when the AI answers in a conversation rather than suggesting an edit to text on the page.

### Rules
- **Do:** Leave Replace as the plain solid button; the wash and the mark already say this came from AI. **Don't:** Put the AI gradient on Replace or on the suggested text — accepting has to feel like any other decision.
- **Do:** Wrap the selected text as the popover's anchor so it stays highlighted underneath for comparison. In a text box or an editor, where the selection cannot be wrapped, pass it as anchor instead and leave the text as it is. **Don't:** Cover or remove the original before the person has chosen.
- **Do:** Title the popover with the action and its flavour: "Rewrite: shorter". **Don't:** Title it "AI" or "Suggestion" — people should know what was asked without remembering it.
- **Do:** Leave the suggestion tile on the soft divider line (border-border). It is read-only content, told apart from the popover by its fill. **Don't:** Outline it with the control line (border-input) — that line marks things people press or type in.

### Accessibility
- The popover is a dialog named by its title. It opens on a press (or, with anchor, when your app opens it) and focus lands on the popover itself (with a visible ring), not on a button, so a held Enter cannot press Discard. Try again keeps focus inside the popover while the suggestion is replaced, and closing it any way returns focus to its anchor, where the person was.
- After Replace, put focus in the new text yourself: the anchor the popover wrapped may be gone, so focus has nowhere to return to.
- For a selection inside a text box or an editor there is no element to wrap: pass anchor (the selection's Range, or for a textarea an object whose getBoundingClientRect returns the box you worked out), drive open yourself, and pass returnFocus (the editor). The popover sits beside the anchor and follows it as the text scrolls or the page moves. The selected text is not turned into a button, so it is not a Tab stop and its accessible name is not the passage: the editor stays the control.
- With anchor, closing with Replace, Discard, Insert below or Escape moves focus to returnFocus; left out, to the control that opened it (children), else to whatever had focus when it opened. Tab past the last button closes it and carries on down the page: focus goes to the next Tab stop after where the popover is rendered, not to returnFocus, so render it just after the editor. A press outside closes it too, and a press on another control leaves focus on that control. Tab past the end, Escape and a press outside call onOpenChange(false).
- With anchor and a control that opens it (children), the keyboard works as it does around a wrapped anchor: Shift+Tab from the popover's first stop goes back to that control with the popover still open, and Tab from the control goes in to the first stop again, whatever it is (a link in the suggestion, the scrolling text, or Discard). That control has aria-expanded, and aria-controls naming the popover while it is open. With anchor alone there is no control to step back to: Shift+Tab from the first stop puts focus on the popover itself, which stays open, and Tab goes on to the first stop.
- An anchor inside a scaled ancestor (a zoomed canvas) is followed at its size on screen. Not supported: a rotated, skewed or mirrored ancestor, where the popover sits near the anchor, not on it, and holds still; and an anchor with no size inside a scaled ancestor, which is placed approximately. If the anchor cannot say where it is (its text was redrawn and it reports an empty box, or its getBoundingClientRect throws), the popover stays where it last was; in development the first throw is reported once in the console.
- The popover is placed by an empty box kept over the anchor. That box is hidden from screen readers, is not a Tab stop and takes no presses: they reach the text underneath.
- On a short screen the popover is capped to the space left; the suggestion scrolls while the header and the buttons stay in view. While it overflows, the scrolling area is a Tab stop named "Suggested text" with the usual focus ring, so the arrow and Page keys scroll it; a suggestion that fits adds no Tab stop.
- It stays open while you press inside it; Escape, a press outside it, or a second press on its anchor closes it.
- An element the popover wraps that is not a button (highlighted text) is given the button role and a place in the Tab order.
- While the AI is writing, a polite live region (role="status") says the working label; when a suggestion follows it says "Suggestion ready". The region is in the popover from its first paint, empty, and the visible label is hidden from screen readers so it is not read twice. A popover that opens with a finished suggestion announces nothing extra.
- Under reduced motion the shimmer and the line hold still; the label stays readable.

### Design tokens
`--popover` · `--background` · `--border` · `--foreground` · `--muted-foreground` · `--primary` · `--ai-from` · `--ai-via` · `--ai-to` · `--ai-text-via`

registry/agent-rules/skills/quill-components/reference/ai-thinking.md
# ai-thinking (component)

Shows an AI working before its answer — a shimmering "Thinking" with what it is doing — then folds into one quiet "Thought for 8 s" row that opens the reasoning.

### When to use
- An AI takes a noticeable moment before answering and people should see it is working, and later how it got there.

### Reach for instead
- **spinner** — when something that is not AI is loading.
- **skeleton** — when a page or card is loading its layout, not an AI thinking.

### Rules
- **Do:** Switch to status="done" the moment the answer starts, so the shimmer and line stop. **Don't:** Leave the shimmer running beside a finished answer — motion means the AI is still working.
- **Do:** Keep the reasoning folded behind "Thought for N s" once the answer is there. **Don't:** Show the reasoning open by default after the answer lands — it pushes the answer down.

### Accessibility
- A polite live region (role="status") is on the page from the first paint in both states. It starts empty and is filled a moment after the component mounts, then follows the label and activity as they change, without moving focus. The visible label is hidden from screen readers so it is not read twice; the region holds the one copy. When the work is done it empties and announces nothing, so a finished reply restored from history stays quiet.
- The finished row is a button with aria-expanded that controls the reasoning list, with the same 3px focus ring as a Button.
- Under reduced motion the shimmer and sweeping line hold still; the label stays readable.

### Design tokens
`--ai-from` · `--ai-via` · `--ai-to` · `--ai-text-via` · `--muted-foreground` · `--border`

registry/agent-rules/skills/quill-components/reference/suggested-prompts.md
# suggested-prompts (component)

Ready-made things to ask an AI — chips for follow-ups under an answer, cards for starters on an empty page, a list in a narrow panel. Picking one fills the composer; it never sends.

### When to use
- An AI page is empty and people need a first thing to ask.
- An answer has natural next questions worth offering.

### Reach for instead
- **command** — when people pick from a long, searchable list of actions.
- **button** — when the choice runs an action straight away instead of filling the composer.

### Rules
- **Do:** Fill the composer and focus it, so people can edit before sending. **Don't:** Send the suggestion the moment it is clicked — people lose the chance to adjust it.
- **Do:** Use chips under an answer, cards on an empty AI page, and the list in a narrow panel. **Don't:** Put large starter cards under every answer — they crowd the thread.

### Accessibility
- A labelled list of buttons; each button's text is the suggestion, so it reads well out of context.

### Design tokens
`--border` · `--background` · `--card` · `--input` · `--muted` · `--ink-soft` · `--foreground` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/model-picker.md
# model-picker (component)

Lets people choose which AI model answers — a quiet name-and-chevron trigger in the composer that opens a list with Auto first, a line on what each model is good for, and a check on the current one.

### When to use
- An app offers more than one AI model or speed and people may want to switch per message.

### Reach for instead
- **select** — when the choice is an ordinary form setting, not an AI model.
- **radio-group** — when the model is chosen once on a settings page; show the same options as radios.

### Rules
- **Do:** Give every model a one-line description of what it is good for ("Fast answers, fewer credits"). **Don't:** List bare model names — people cannot choose between names they do not know.
- **Do:** Offer Auto first so people who do not care never have to choose. **Don't:** Make people pick a model before their first message.

### Accessibility
- The trigger is named with the current choice ("Model: Auto"); the menu is a radio group, so arrow keys move and the current model is announced as checked.
- With no Auto option and no models there is nothing to choose, so nothing is drawn; there is never an empty button.

### Design tokens
`--popover` · `--accent` · `--muted` · `--ink-soft` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/alert.md
# alert (component)

A single inline status box for a message tied to a specific area of the page. `alerts` is this primitive already assembled into a vertical stack of several.

### When to use
- You need one persistent, inline status message (informational or error) attached to a specific section of a page — not a whole stack of them, and not transient feedback about an action just taken.

### Reach for instead
- **alerts** — when you're showing more than one status message together — reach for the alerts pattern, which stacks them with the right spacing already handled.
- **sonner** — when the message is transient feedback about an action just taken, not a persistent note tied to a page section.

### Rules
- **Do:** Use variant="destructive" for real errors, and leave the default variant for neutral or informational messages, as the AllVariants story does. **Don't:** Style a real error with the default (neutral) variant — a blocking failure that looks like routine information gets missed.
- **Do:** Include an icon (the Icon component, as most stories here do) so severity is scannable at a glance — the layout automatically reserves a leading column for it. **Don't:** Omit the icon on a destructive or otherwise consequential alert, leaving severity to be read from color or text alone.

### Accessibility
- Alert renders `role="alert"`, a live region assistive tech announces automatically when it mounts — reserve it for messages that need attention, not decorative or already-visible content.
- Severity is conveyed by variant plus an icon, never color alone — a destructive alert's red text by itself isn't sufficient for colorblind users.
- AlertAction positions its child absolutely in the corner — pair it with a real accessible control (e.g. a Button with `aria-label`, as the WithAction story does), not an unlabeled icon.

### Design tokens
`--card` · `--ink` · `--destructive` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/alert-dialog.md
# alert-dialog (component)

A modal that forces an explicit response to a destructive or irreversible action — unlike Dialog, it cannot be dismissed by clicking outside.

### When to use
- You need the user to explicitly confirm or cancel a destructive, irreversible action — deleting a record, discarding unsaved work, removing access.

### Reach for instead
- **dialog** — when the task is not destructive — editing, viewing details, filling a form. Dialog dismisses on backdrop click or Escape; AlertDialog dismisses only on Escape or an explicit button.

### Rules
- **Do:** Reserve AlertDialog for destructive or irreversible confirmations — deleting, overwriting, removing access. **Don't:** Reach for AlertDialog on a routine informational modal just to borrow its heavier visual weight — use Dialog instead.
- **Do:** Label the action button with the consequence — "Delete course," "Remove student." **Don't:** Label the action button "Confirm" or "OK" — users can't tell what they're agreeing to without rereading the description.
- **Do:** Give the trigger a destructive-variant Button for a true delete, so the risk is visible before the dialog even opens. **Don't:** Use a neutral or primary-styled trigger for a hard delete — nothing signals danger until the dialog is already open.

### Accessibility
- Renders role="alertdialog" and traps focus inside the popup while it is open.
- Clicking the backdrop does not close it — only Escape or an explicit Cancel/Action press does, so a destructive choice can't be dismissed by accident.
- Always render AlertDialogTitle — it is the accessible name announced when the dialog opens.

### Design tokens
`--popover` · `--destructive` · `--muted` · `--radius-xl`

registry/agent-rules/skills/quill-components/reference/alerts.md
# alerts (pattern)

A vertical stack of informational, success, and destructive alerts with icons.

### When to use
- You need to surface inline status messages of varying severity (info, success, destructive) within a page.

### Reach for instead
- **sonner** — when the message is transient feedback about an action just taken — use a toast, not an inline alert.
- **announcement-banner** — when the message is site-wide and dismissible rather than local to a page section.
- **empty-state** — when the "message" is really an empty view that needs an explanation and a call to action.

### Rules
- **Do:** Match severity to consequence: destructive for blocking errors, default for neutral information. **Don't:** Use destructive styling for emphasis on non-error messages.

### Accessibility
- Severity is conveyed by the icon and the text, never by color alone.

registry/agent-rules/skills/quill-components/reference/analytics-charts.md
# analytics-charts (pattern)

A dashboard pair — an area chart of audience growth beside a bar chart of weekly sales, each on its own card.

### When to use
- You need a dashboard view of trends over time — an area chart plus a bar chart on card surfaces.

### Reach for instead
- **stat-cards** — when you need at-a-glance numbers, not a trend shape — a single KPI reads faster as a number than a chart.
- **data-table** — when the audience needs to inspect exact values per row, not the shape of change over time.

### Rules
- **Do:** Color each series from the chart token set (--chart-1…--chart-5), assigned in a fixed order. **Don't:** Hardcode a raw accent pigment (--terracotta, --moss, --indigo…) for a series — some pairs are documented as failing colorblind-distinguishability.
- **Do:** Pair a multi-series chart with a ChartLegend, as the Audience chart does — color is tied to a series name in text, never left for the reader to infer. **Don't:** Ship a multi-series chart with no legend — color becomes the only signal distinguishing readers from subscribers.

### Accessibility
- Each chart has a ChartTooltip that surfaces exact values on hover/focus — the visual shape alone is never the only way to read the data.
- A chart's legend (ChartLegend) ties color to series name — color is never the only signal distinguishing readers from subscribers.

### Design tokens
`--card` · `--chart-1` · `--chart-2` · `--chart-3` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/announcement-banner.md
# announcement-banner (pattern)

Two dismissible announcement banners — an inline bordered page banner and a full-bleed primary-color variant.

### When to use
- You need to announce something site-wide with a dismissible banner, either inline or full-bleed.

### Reach for instead
- **alerts** — when the message is local to a page section and tied to system status, not a site-wide announcement.
- **cookie-consent** — when the banner requires an explicit choice (accept/reject), not just a dismiss.

### Rules
- **Do:** Give every banner variant its own dismiss button with a specific aria-label (e.g. "Dismiss workshop banner"), not a generic one. **Don't:** Ship a banner with no way to dismiss it, or multiple banners sharing one ambiguous "Dismiss" label.

### Accessibility
- Each banner's dismiss button has a distinct, content-specific aria-label so screen-reader users can tell which banner they're closing when more than one is stacked.
- The full-bleed variant's dismiss icon overrides its hover color explicitly (text-primary-foreground) so it stays visible against the primary-color background, not just inheriting a default that might vanish.

### Design tokens
`--card` · `--border` · `--primary` · `--primary-foreground`

registry/agent-rules/skills/quill-components/reference/approval-card.md
# approval-card (component)

Asks before an AI agent acts — an inline card that names the action, shows exactly what will happen, lets people edit it in place, and has a main button that says what it does.

### When to use
- An AI agent is about to do something on a person's behalf (send, post, pay, delete) and needs a yes first.

### Reach for instead
- **alert-dialog** — when a person started a destructive action themselves and has to confirm it before anything else happens.
- **agent-steps** — when you are showing what the agent is doing, not asking permission for it.

### Rules
- **Do:** Give the main button the action's own words: "Send email", "Delete 3 drafts". **Don't:** Label it "Approve", "OK" or "Yes" — people should not have to read the title to know what they are agreeing to.
- **Do:** Show the real thing: who it goes to, the subject, the text, and one line on the consequence. **Don't:** Ask for approval of a summary ("Send the report?") while hiding what will actually be sent.
- **Do:** Keep the buttons stock: solid ink for the action, destructive for anything that deletes or cannot be undone. **Don't:** Put the AI gradient on the action button — approving has to feel like any other decision.
- **Do:** Leave the proposal tile on the soft divider line (border-border). It is read-only content, told apart from the card by its fill. **Don't:** Outline it with the control line (border-input) — that line marks things people press or type in.

### Accessibility
- The card is a group named by its title; it sits in the conversation and never takes focus on its own.
- The rows are a description list (term and value). The card ends each label with a colon ("To:"), so pass the bare word; a label that already ends in a colon or a question mark is left as it is.
- Edit is a toggle button (aria-pressed). Pressing it puts the cursor in the field, which is named by bodyLabel.
- The main button is named by the action itself. A body that was given and is now empty disables it.
- The outcome line appears inside a status region that is already on the page, so it is announced; while empty the region is a real box that takes no space. When the person presses the card's own main or decline button, the buttons go and focus moves to that line without scrolling the page, so keyboard users keep their place. An outcome that arrives on its own, with nothing in the card pressed, moves focus nowhere; nor is focus pulled back if the person has since moved to another control.

### Design tokens
`--card` · `--background` · `--border` · `--input` · `--muted` · `--muted-foreground` · `--ink-soft` · `--primary` · `--destructive` · `--ai-from` · `--ai-via` · `--ai-to`

registry/agent-rules/skills/quill-components/reference/aspect-ratio.md
# aspect-ratio (component)

Constrains any child to a fixed width-to-height ratio — a CSS-only box, most often used to reserve space for images, video embeds, and thumbnails.

### When to use
- You're placing an image, video, or embed that must keep a consistent shape (16:9, 1:1, 4:3) regardless of its container's width, and want to reserve that space before the media loads.

### Reach for instead
- **resizable** — when the box's size needs to be user-adjustable by dragging, not fixed to a mathematical ratio.

### Rules
- **Do:** Pass `ratio` as a width ÷ height fraction (e.g. `16 / 9`, `1`, `4 / 3`) so the value reads as the actual proportion. **Don't:** Hardcode the decimal by hand (`1.7777777777777777`) — the fraction form documents itself and is what every story here uses.
- **Do:** Pair an `<img>` child with `object-cover w-full h-full` (as WithImage does) so the image fills the box and crops instead of distorting or overflowing. **Don't:** Drop a raw `<img>` in with no sizing classes — the browser renders it at its own natural proportions and it spills past the ratio box.

### Accessibility
- AspectRatio itself renders a plain `<div>` with no semantics of its own — accessibility (alt text on an image, captions on a video) comes entirely from whatever child you put inside it.

registry/agent-rules/skills/quill-components/reference/avatar.md
# avatar (component)

A round image, initials, or icon representing a person or entity — reserves its shape immediately and swaps in the real photo once it loads.

### When to use
- You need to represent a person or entity visually — a profile photo, initials, or a generic icon — in a list row, header, or comment.
- You need to show a cluster of related people (assignees, collaborators) — pair Avatar with AvatarGroup.
- You need to overlay a small presence, status, or notification dot on the avatar's corner — pair Avatar with AvatarBadge.

### Reach for instead
- **item** — when the avatar is one part of a larger list row with a title and actions — compose it inside ItemMedia rather than styling Avatar standalone.

### Rules
- **Do:** Always render an AvatarFallback with 1–2 character initials as a sibling of AvatarImage. **Don't:** Render AvatarImage with no AvatarFallback — a broken, slow, or missing image leaves a blank circle with nothing to look at.
- **Do:** Use AvatarGroup to overlap a cluster of avatars and AvatarGroupCount (e.g. "+4") to cap how many render. **Don't:** Lay out many avatars edge-to-edge in a plain flex row — with no overlap or ring they read as an unrelated list, not one group.
- **Do:** Pair AvatarBadge with a visible text label elsewhere in the row (e.g. "Online" in an Item description) when the status it shows must reach screen reader users. **Don't:** Rely on AvatarBadge alone to convey presence or status — it is a decorative, unlabeled span with nothing for assistive tech to announce.

### Accessibility
- AvatarFallback renders whenever the image status is not "loaded" — idle, loading, or error — so the fallback initials are always the accessible content until a real image lands.
- AvatarImage needs a real `alt` attribute naming the person or entity; Avatar itself renders a plain `<span>` with no built-in accessible name of its own.
- AvatarBadge renders a plain, empty `<span>` by default — it carries no accessible name or role of its own, so it is purely decorative; give the same information as visible text elsewhere in the row if it needs to reach screen reader users.

### Design tokens
`--paper-deep` · `--ink-muted` · `--line-soft` · `--primary` · `--primary-foreground` · `--background`

registry/agent-rules/skills/quill-components/reference/badge.md
# badge (component)

A small, non-interactive label for status, category, or count — not a button or a link on its own.

### When to use
- You need a compact visual label for a status, category, or count next to other content — a course card, a table cell, a nav item.

### Reach for instead
- **tone-badge** — when the label is one of Quill's uppercase tag pills (status/tier/live cue) — ToneBadge wraps Badge with the pigment vocabulary baked in.
- **button** — when the element needs to trigger an action rather than just display state.

### Rules
- **Do:** Keep badge text to 1–3 words — "Beta", "New", "42 unread". **Don't:** Put a full sentence in a badge — whitespace-nowrap and overflow-hidden clip the overflow instead of wrapping it, mid-word.
- **Do:** Treat Badge as a status display by default; pass `render={<a href=... />}` only when the badge truly needs to be keyboard-navigable or open a URL. **Don't:** Attach an onClick to a bare Badge — a plain `<span>` with a click handler cannot be tabbed to or activated with Enter.

### Accessibility
- Badge renders a plain `<span>` by default — no role, no accessible name beyond its text content, and no keyboard focus.
- Passing `render={<a href=... />}` (as the AsLink story does) swaps the rendered tag via Base UI's useRender, merging Badge's classes onto the real anchor so focus, Enter, and href behavior all come from that native element.

### Design tokens
`--primary` · `--secondary` · `--destructive` · `--border` · `--ring` · `--radius-4xl`

registry/agent-rules/skills/quill-components/reference/badge-on-card.md
# badge-on-card (pattern)

Default, secondary, and destructive badges shown together on a card surface.

### When to use
- You need to show status or category badges attached to a content card.

### Reach for instead
- **tone-badge** — when you need a badge whose color is driven by a semantic tone token rather than the three fixed variants here.
- **data-table** — when the badges are actually per-row status in a list of records, not a single card's labels.

### Rules
- **Do:** Reserve the destructive variant for a status that actually needs attention (blocked, failed, overdue). **Don't:** Use destructive styling for emphasis on a badge that isn't actually a warning.

### Accessibility
- Badges are inline `<span>` elements with no independent accessible name beyond their visible text — never ship an icon-only badge without one.
- Destructive badges on a card surface must clear the same 4.5:1 text contrast as any other text (a past regression here hit 4.45:1).

### Design tokens
`--primary` · `--secondary` · `--destructive` · `--card`

registry/agent-rules/skills/quill-components/reference/breadcrumb.md
# breadcrumb (component)

A trail of links showing the current page's position in a hierarchy — the last crumb is always the current, non-link page.

### When to use
- You need to show where the current page sits inside a multi-level hierarchy (Home > Courses > Watercolor Basics) so users can jump back up a level.

### Reach for instead
- **page-header** — when you need the breadcrumb bundled with a title, description, and page actions in one header block, not just the trail by itself.

### Rules
- **Do:** Render the final crumb as BreadcrumbPage (plain, non-link, current) — every earlier crumb as BreadcrumbLink. **Don't:** Make every crumb, including the current page, a clickable BreadcrumbLink — you can't navigate to where you already are.
- **Do:** Swap the middle crumbs for BreadcrumbEllipsis once the path goes past about 4 levels deep. **Don't:** Render every level of a very deep path — the trail wraps and stops reading as a quick way back up.

### Accessibility
- Breadcrumb renders a real `<nav aria-label="breadcrumb">` wrapping an `<ol>` — the hierarchy is structural markup, not a row of plain text joined by slashes.
- BreadcrumbPage sets `aria-current="page"` and `aria-disabled="true"` on a `<span>` — it announces as the current location, not as an inert or broken link.
- BreadcrumbSeparator and BreadcrumbEllipsis are `aria-hidden` presentation-only; BreadcrumbEllipsis also carries a visually-hidden "More" label so screen readers get equivalent text, not silence.
- `BreadcrumbLink` accepts a `render` prop (Base UI's `useRender`) so it can become a router-aware Link component instead of a plain `<a>`, without losing its link semantics.

### Design tokens
`--ink-muted` · `--ink`

registry/agent-rules/skills/quill-components/reference/button.md
# button (component)

Buttons trigger actions. The default (primary) variant is the single most important action on a surface.

### When to use
- You need to trigger an action — submit, confirm, start a flow — rather than navigate to a page.
- You need an icon-only action trigger (use an icon size with an aria-label).

### Reach for instead
- **button-group** — when several related actions belong together as one segmented control.
- **toggle** — when the control switches a state on/off rather than firing an action.

### Rules
- **Do:** Use one default (primary) button per surface, for the single most important action. **Don't:** Scatter several primary buttons on one surface — they compete and none reads as primary.
- **Do:** Leave the focus ring on --ring, the accent-driven ink ring. **Don't:** Restyle the focus ring with a hardcoded pigment such as terracotta.
- **Do:** When the action goes to another page, render a real anchor (or your router's Link), styled with the button's link variant if it should look like one. **Don't:** Navigate from a button onClick — it cannot be opened in a new tab and announces as a button, not a link.
- **Do:** Give every icon-only button an explicit aria-label. **Don't:** Ship an icon-only button with no accessible name.

### Accessibility
- Native `<button>` semantics: Enter and Space activate; disabled removes it from the tab order.
- Icon-only sizes (icon, icon-xs, icon-sm, icon-lg) require an aria-label.
- The focus ring uses --ring and must stay visible on every theme ground.

### Design tokens
`--primary` · `--secondary` · `--destructive` · `--muted` · `--radius-lg` · `--ring`

registry/agent-rules/skills/quill-components/reference/button-group.md
# button-group (component)

A structural wrapper that merges adjacent Buttons into one visually connected control — shared borders, no owned selection state.

### When to use
- You have several related Buttons that should read as one compound control — sequential actions like prev/next, or a segmented set of mutually exclusive views.

### Reach for instead
- **button** — when there is only one action — a single Button does not need a ButtonGroup wrapper.
- **toggle-group** — when the buttons need real pressed/selected state built in — ButtonGroup only merges borders and tracks no selection itself (see ActiveState, which manages it by hand).

### Rules
- **Do:** Use variant="outline" on every Button inside a ButtonGroup — the group strips the shared inner border between items, which only reads as a seam when each button has a border to begin with. **Don't:** Group default (solid) Buttons — with no border to strip, they blend into one undifferentiated block instead of a visibly segmented control.
- **Do:** Treat ButtonGroup as a border-merging wrapper only — manage active or selected state yourself, or reach for ToggleGroup if that state should be built in. **Don't:** Expect ButtonGroup to track which button is active — it renders role="group" and merges borders, nothing else.

### Accessibility
- Renders a plain `<div role="group">` — a semantic hint that the buttons are related; every child Button still needs its own accessible name, such as aria-label on icon-only buttons.
- No built-in roving keyboard navigation — Tab moves through each Button individually, unlike ToggleGroup toolbar-style arrow-key behavior.
- ButtonGroupSeparator renders role="separator" but is non-focusable and sits outside the tab order — it does not interrupt tabbing through the buttons.

### Design tokens
`--border` · `--muted` · `--input` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/calendar.md
# calendar (component)

The date-picker primitive built on react-day-picker — supports single, multiple, and range selection modes. `calendar-page` and `calendar-range` are this primitive already composed into full scheduling and booking patterns.

### When to use
- You're composing your own date-picking UI directly — a form field, a filter, a custom layout — rather than reaching for a pre-built scheduling pattern.

### Reach for instead
- **calendar-page** — when you need a single-date picker paired with a synced list of what's scheduled that day, already assembled.
- **calendar-range** — when you need a two-month range picker with Clear/Reserve actions, already assembled.
- **popover** — when you need to tuck the calendar inside a compact trigger instead of showing it inline — pair Calendar with PopoverContent yourself.

### Rules
- **Do:** Let today render with its own --muted marker and let the selected day render with --primary — two visually distinct tokens, not one fill reused for both. **Don't:** Reuse one color for both today and the selected day — they read as the same state and a user can't tell which date is actually picked.
- **Do:** Pick the selection mode that matches the task — `single` for one date, `range` for a span, `multiple` for a handful of unrelated dates — and pass the matching `selected`/`onSelect` value shape for that mode. **Don't:** Force a single-date task through `mode="range"` (or vice versa) — the selected value shape (a Date vs. a DateRange vs. an array) stops matching what the rest of your form expects.
- **Do:** Pass a `disabled` matcher (e.g. `{ before: today }`) to block dates that are never valid choices, as WithDisabledDates does. **Don't:** Leave every date pickable and validate only after submit — a user can select a date that was never going to be accepted.

### Accessibility
- The day grid renders real ARIA grid semantics — `role="gridcell"` per day, `aria-selected` on the selected cell(s) — wired automatically by react-day-picker, so avoid overriding day rendering in a way that drops them.
- Keyboard navigation (arrow keys, Home/End, Enter/Space to select) comes from react-day-picker out of the box.
- A disabled day is non-interactive mostly via the native `disabled` attribute, but via `aria-disabled` instead when that day currently holds roving-tabindex focus — so keyboard users tabbing through the grid never lose their place.

### Design tokens
`--background` · `--primary` · `--primary-foreground` · `--muted` · `--radius-md` · `--ring`

registry/agent-rules/skills/quill-components/reference/calendar-page.md
# calendar-page (pattern)

A single-date calendar paired with the selected day's session list, in one studio-schedule card.

### When to use
- You need to pick a single date and show what's scheduled that day.

### Reach for instead
- **calendar-range** — when the user is booking a span of days, not viewing what's scheduled on one.
- **activity-feed** — when you want a chronological history of past events, not a schedule of upcoming sessions tied to a picked date.

### Rules
- **Do:** Keep the session list in sync with whatever date is selected in the calendar — the two panes are one control, not two independent widgets. **Don't:** Let the calendar selection and the listed sessions fall out of sync (e.g. a static list that ignores the picked date).

### Accessibility
- The selected day carries both a filled background (--primary) and inverted text color, not a border or dot alone — today's marker uses --muted, a visually distinct token, so the two states don't collide.
- Each session's time is presented as visible text ("9:00"), not implied by list order alone.

### Design tokens
`--primary` · `--primary-foreground` · `--muted` · `--border`

registry/agent-rules/skills/quill-components/reference/calendar-range.md
# calendar-range (pattern)

A stateful two-month range calendar for booking a span of days, with clear and reserve actions.

### When to use
- You need to book a span of days with a two-month range picker and confirm/clear actions.

### Reach for instead
- **calendar-page** — when the user is picking a single day to view, not a multi-day span to reserve.
- **checkout** — when the range is just one field within a larger purchase flow, not the whole card.

### Rules
- **Do:** Keep the Clear action available whenever a range is selected, separate from Reserve. **Don't:** Force a user to re-click through the whole range to correct a mis-click — that's what Clear is for.
- **Do:** Disable Reserve until both a start and end date are selected. **Don't:** Let a half-made range (just a start date) submit as if it were a valid booking.

### Accessibility
- The selected range communicates start, middle, and end through distinct fill states (solid at the ends, muted between), not shading alone.
- Calendar's day grid comes from react-day-picker, which provides keyboard navigation (arrow keys, Enter to select) out of the box — don't override day rendering in a way that breaks it.

### Design tokens
`--primary` · `--primary-foreground` · `--muted` · `--border`

registry/agent-rules/skills/quill-components/reference/card.md
# card (component)

A bounded content container with header, body, and footer slots — paper-warm surface with a soft ring instead of a box-shadow border.

### When to use
- You need to group related content (a course, a stat, a settings section) in a bounded surface with consistent padding and optional header/footer regions.

### Reach for instead
- **item** — when the content is one row in a list or feed rather than a standalone bounded surface — Item is the lighter-weight primitive for that.

### Rules
- **Do:** Put footer actions in CardFooter — it gets a top border and `bg-muted/50` for free, visually separating it from the body. **Don't:** Add a Button straight into CardContent as the last element — it sits flush with the body text with no visual break.
- **Do:** Place CardAction inside CardHeader for a top-right control (a badge, an icon button) alongside the title — it already handles the grid placement. **Don't:** Position a top-right action outside CardHeader with hand-rolled absolute positioning instead of using CardAction.

### Accessibility
- Card and its parts (CardHeader, CardContent, CardFooter, CardAction) are all plain `<div>`s with no built-in role or heading semantics.
- CardTitle is a styled `<div>`, not a real heading — add real heading markup (`<h2>`–`<h6>`) inside it when the card needs to appear in the page's heading outline.

### Design tokens
`--card` · `--card-foreground` · `--foreground` · `--muted` · `--border` · `--radius-xl`

registry/agent-rules/skills/quill-components/reference/carousel.md
# carousel (component)

A horizontally or vertically scrolling item viewport built on Embla Carousel, with Previous/Next controls and keyboard arrow-key support.

### When to use
- You need to page through a set of same-shaped items (cards, images) one view at a time within a constrained width.

### Reach for instead
- **scroll-area** — when you just need an overflow-scrolling container, not paged navigation with Previous/Next controls and snap points.
- **tabs** — when the items are distinct named views the user picks by label, not a sequential set to page through.

### Rules
- **Do:** Render CarouselPrevious and CarouselNext alongside CarouselContent, as every story here does — they auto-disable at the first/last slide via canScrollPrev/canScrollNext. **Don't:** Ship CarouselContent with no visible Previous/Next controls — touch users can swipe, but mouse and keyboard users have no way to advance.
- **Do:** Set a `basis-*` class on CarouselItem (as MultipleItems does with `basis-1/3`) to control how many items show at once. **Don't:** Leave every CarouselItem at the default `basis-full` when you actually want several items visible per view — each item fills the whole track instead.

### Accessibility
- Carousel renders `role="region"` with `aria-roledescription="carousel"` and a default `aria-label="carousel"` — pass your own `aria-label` when a page has more than one carousel, since the default isn't unique.
- CarouselItem renders `role="group"` with `aria-roledescription="slide"` on each item, so assistive tech announces position within the set the way native carousels do.
- Left/Right arrow keys scroll prev/next while focus is anywhere inside the carousel container; CarouselPrevious/CarouselNext disable via the native `disabled` attribute when there's nowhere left to scroll.

registry/agent-rules/skills/quill-components/reference/chart.md
# chart (component)

A themeable wrapper around Recharts — ChartContainer wires a config object to CSS custom properties per series, plus a matching ChartTooltip and ChartLegend.

### When to use
- You need a bar, line, area, or other Recharts chart that automatically re-colors across every Quill theme and stays colorblind-safe.

### Reach for instead
- **data-table** — when the audience needs to read or compare exact values, not the shape of change — a table beats a chart for precision.
- **stat-cards** — when you need a single at-a-glance number, not a trend shape.

### Rules
- **Do:** Color each series from the chart token set in fixed order (--chart-1 first, then --chart-2, …) via `config.color`, as chartConfig does here. **Don't:** Hardcode a raw accent pigment (e.g. `#BC6751`, `var(--terracotta)`) for a series, or reorder which chart-N a survivor gets when a filter drops one — raw pigments fail the colorblind-distinguishability checks these tokens were built to pass.
- **Do:** Always include ChartTooltip (with ChartTooltipContent), as every story here does, so hovering or focusing a data point surfaces its exact value. **Don't:** Ship a chart with no ChartTooltip — the shape communicates a trend, but exact values become invisible to anyone who can't precisely read pixel height.
- **Do:** Prefer `color: "var(--chart-N)"` in a series config entry — the tokens already re-cut correctly for every Quill theme. **Don't:** Reach for the `theme` field (`{ light, dark }`) expecting it to map Quill's themes one-to-one — it only distinguishes two CSS buckets, light and dark, and Quill has more themes than that, so hand-authored hex pairs there won't track Quill's palette the way a token reference does.

### Accessibility
- Each chart needs a ChartTooltip so exact values are available on hover and keyboard focus, not just visually inferred from the shape.
- A chart's legend (ChartLegend/ChartLegendContent) ties color to series name in text — color is never the only signal distinguishing series.

### Design tokens
`--chart-1` · `--chart-2` · `--chart-3` · `--chart-4` · `--chart-5` · `--chart-seq-1` · `--chart-seq-5` · `--chart-div-1` · `--chart-div-5`

registry/agent-rules/skills/quill-components/reference/chat.md
# chat (pattern)

A one-on-one messaging panel — contact header, message bubbles, and a composer input.

### When to use
- You need a one-on-one messaging panel with a contact header, bubbles, and a composer.

### Reach for instead
- **list-detail** — when you need a list of conversations alongside the open thread, not just the single open thread.
- **notifications** — when the messages are one-way system alerts, not a two-way conversation with someone.

### Rules
- **Do:** Align the current user's messages to one side (right) and the other party's to the other (left) — position is the primary cue for who said what. **Don't:** Rely on color alone to distinguish sender — pair it with consistent alignment.

### Accessibility
- The composer's Input has an explicit aria-label ("Message") since there's no visible `<label>` in this compact layout.
- The send button is icon-only with an aria-label ("Send") — never ship it without one.

### Design tokens
`--border` · `--card` · `--primary` · `--muted`

registry/agent-rules/skills/quill-components/reference/ai-home.md
# ai-home (pattern)

An AI assistant's start page — a soft AI glow, a greeting, the large composer with Ask/Agent modes, and starter cards.

### When to use
- An app has a page whose main job is talking to its AI assistant, and people land on it with nothing typed yet.

### Reach for instead
- **prompt-composer** — when you only need the box, inside a page that is about something else.
- **chat** — when two people are messaging; there is no AI.

### Rules
- **Do:** Keep the AI glow to this one start page. **Don't:** Add the glow behind other sections or pages — it marks the AI home, and repeated it becomes decoration.

### Accessibility
- The greeting is the page heading; the composer and starter cards follow in reading order.
- Starter cards are buttons that fill the composer; they do not send on their own.

### Design tokens
`--ai-from` · `--ai-via` · `--ai-to` · `--background` · `--card` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/ai-chat.md
# ai-chat (pattern)

A full AI conversation page — past chats on the left, the thread in a readable column with reasoning, source chips and follow-ups, and the composer with the model picker and AI notice pinned to the bottom.

### When to use
- An app has a page for an ongoing conversation with its AI assistant.

### Reach for instead
- **ai-home** — when people land with nothing typed yet and need a greeting and starters, not a thread.
- **chat** — when two people are messaging; no AI is involved.

### Rules
- **Do:** Keep the thread on the plain page; the marks and the composer edge already say AI. **Don't:** Add the AI glow behind the conversation — it belongs to the AI home alone.
- **Do:** Pass your conversation as children and drive the composer with status, onStop and onSubmit. The conversation shown without children is sample content. For a conversation with nothing in it yet pass null, false or an empty list: those count as passed, and only undefined shows the sample. Scroll to a new turn yourself: the page is what scrolls, so the block does not (the side panel, which scrolls inside itself, does). **Don't:** Ship the sample conversation, or edit the block to hard-code yours — the block is overwritten on update.
- **Do:** Pass the conversation-history block as sidebar: the built-in list of chats is a sample. Pass null for a page with no left column: anything React draws nothing for (null, true, false, an empty string) is no column, so cond && list gives none while cond is false, and only undefined shows the sample list. Pass composerTrailing for your own control in place of the sample model picker (the same values for none). **Don't:** Ship the sample list of chats or the sample model picker.

### Accessibility
- The sample sidebar is a navigation landmark named "Chats" with the open chat marked aria-current; the thread and composer sit in a region named "Conversation". A sidebar you pass names itself (the conversation-history block is a navigation landmark of its own); with sidebar null there is no left column and no navigation landmark. Under 768px wide the left column is not shown, whatever is in it.
- The thread is a log named "Messages", a polite live region, so each new turn is read out as it is added, without moving focus.
- Each reply is an article. A working reply has a status region that is empty at first, filled a moment after it mounts and then updated as the activity changes; once it is stopped, "You stopped this answer." is announced from a status region that was already there.
- The chat links, suggestion chips, source chips and the thinking toggle show the same 3px focus ring as a Button.
- With your own conversation, Stop calls onStop and moves the cursor back to the message box.

### Design tokens
`--background` · `--border` · `--muted` · `--muted-foreground` · `--ai-from` · `--ai-via` · `--ai-to`

registry/agent-rules/skills/quill-components/reference/ai-side-panel.md
# ai-side-panel (pattern)

An AI assistant docked beside the page — a header with History and Close, a removable chip saying what it is looking at, a short thread, and the small composer, on one soft top-to-bottom AI wash.

### When to use
- People are working on a page (a report, a record, a document) and should be able to ask the AI about it without leaving.

### Reach for instead
- **ai-chat** — when the conversation is the whole page, not a helper beside other work.
- **sheet** — when the side panel holds an ordinary form or details, not an AI assistant.
- **ai-popover** — when the AI suggests one edit to selected text and no conversation is needed.

### Rules
- **Do:** Keep the "Looking at" chip so people know what the assistant can read, and let them remove it. **Don't:** Send the page to the AI without saying so, or pin a scope people cannot drop.
- **Do:** Let the one wash run from the top of the panel to the bottom, behind the header, the thread and the composer. **Don't:** Give the header its own gradient band or add a second wash — a band reads as a hard edge.
- **Do:** Wire History to your own list of past chats with onHistory; the conversation-history block is that list, and passing it as body shows it in the panel itself, in place of the thread, until you stop passing it (anything React draws nothing for shows the thread: undefined, null, true, false, an empty string; so open ? list : null and open && list both work). History only calls onHistory, and with it left out the button is not drawn. **Don't:** Ship a History button that does nothing, or put the list among the messages — it is not a turn of the conversation.
- **Do:** Use the small composer and the list layout for suggestions; the panel is narrow. **Don't:** Put the large composer or starter cards in the panel — they belong to the AI home page.
- **Do:** Pass your conversation as children and drive the composer with status, onStop and onSubmit. The conversation shown without children is sample content. For a conversation with nothing in it yet pass null, false or an empty list: unlike body, anything but undefined counts as passed here, and only undefined shows the sample. **Don't:** Ship the sample conversation, or edit the block to hard-code yours — the block is overwritten on update.
- **Do:** Keep the composer's gradient edge on the washed panel here. It is on purpose, even though the general rule puts the edge on page-background surfaces. **Don't:** Take it as a reason to put the edge on other tinted surfaces.

### Accessibility
- In the Sheet the panel is a modal dialog named by its title. Escape or Close closes it and focus returns to what opened it; opening it puts the cursor in the message box. Opened by a touch on its trigger, focus goes to the panel itself instead, so the on-screen keyboard stays down until the box is pressed; opened by your app through the open prop, the same happens on a device whose main pointer is a finger (pointer: coarse).
- On a phone (under 640px wide) the Sheet covers the whole screen, so Close in the header is the way out besides Escape; from 640px up it is the stock 24rem panel.
- Placed in a layout on its own (AiPanel), the panel is a region named by its title.
- The thread is a log named "Messages", a polite live region, so each new turn is read out as it is added. The thread scrolls inside the panel; the header and the composer stay in view. Messages scrolled under the header fade out; at the top of the thread nothing fades, and a control reached by keyboard is scrolled clear of the fade.
- The sample replies do not show their name, because the header already says it; the name is still read to screen readers. Pass hideName on the replies in your own conversation for the same.
- With your own conversation the panel opens on the newest turn, and brings the newest into view when a turn is added or a reply starts being written (status "working"). Each turn has to be a direct child for an added one to be noticed. Stop moves the cursor back to the message box.
- While body is passed, the thread is hidden, the scope chip and the log with it, so nothing in it is read or reached by Tab; the header and the composer stay. What you pass names itself (the conversation-history block is a navigation landmark). When body goes away the thread is back where it was scrolled to, and if the cursor was on something in the body it lands in the message box; on a device whose main pointer is a finger (pointer: coarse) it lands on the panel itself instead, so the on-screen keyboard stays down (drawn inline, the panel is focusable only until focus leaves it again; an element of your own around it is never touched).
- The chip's remove button is named "Stop looking at" followed by the scope and shows the same 3px focus ring as a Button; removing the chip moves the cursor to the composer. A scope your app controls, with no onScopeRemove, shows no remove button.
- History and Close are icon buttons with names. History is drawn only when onHistory is given.

### Design tokens
`--popover` · `--popover-foreground` · `--background` · `--border` · `--input` · `--muted` · `--ink-soft` · `--foreground` · `--ai-from` · `--ai-via` · `--ai-to`

registry/agent-rules/skills/quill-components/reference/conversation-history.md
# conversation-history (pattern)

Past AI chats grouped by when they happened — pinned first, then today, yesterday, the last week and earlier months — with the open chat filled and a menu on each row to rename, pin or delete it.

### When to use
- People come back to earlier conversations with an AI and need to find one, reopen it, and tidy the list.

### Reach for instead
- **ai-chat** — when you want the whole chat page; it carries a short sample list of its own, and takes this block as its sidebar.
- **sidebar-nav** — when the side column moves between sections of the app, not between past chats.
- **command-palette** — when people look a chat up by typing its name instead of browsing by date.

### Rules
- **Do:** Pass your chats as conversations and wire onSelect, onRename, onPin and onDelete. The five chats shown without it are sample content. **Don't:** Ship the sample list, or edit the block to hard-code yours — the block is overwritten on update.
- **Do:** Leave both steps in: the confirmation before, and Undo in the list for a few seconds after. onDelete is called at most once for each confirmed delete: when the Undo time has passed, or sooner if another delete is confirmed, the list is removed, or the page is hidden or closed. Send your request from onDelete with keepalive (or sendBeacon) so one made as the page closes is not dropped. **Don't:** Delete on the first press, or delete on your side before onDelete is called — Undo could not bring the chat back.
- **Do:** Pass now again (for example when the window regains focus) if the list can stay open overnight; "Today" is worked out when the list is first drawn. **Don't:** Expect the groups to roll over at midnight by themselves.
- **Do:** Leave out a callback you do not support; its menu item is not drawn. **Don't:** Pass a callback that does nothing so the menu looks complete.
- **Do:** Open this list from the AI side panel's History button (onHistory). Pass it as the panel's body to show it in the panel itself, in place of the thread, and pass undefined again once a chat is chosen; or show it in your own sheet or column. **Don't:** Expect the side panel to show it by itself — History only calls onHistory, and the list appears when you pass it.
- **Do:** Pass timeZone (the person's zone) and now when the list is rendered on a server, so the server and the browser put each chat in the same group. **Don't:** Leave the zone to the server's clock — a chat from this evening reads as Yesterday on the server and Today in the browser.

### Accessibility
- The list is a navigation landmark named "Past chats". Each group is a list named by its heading. The open chat is marked aria-current and set in medium weight, so it does not rest on the fill alone.
- Each row's "more" button is named "More for" followed by the chat's title. It is hidden until the row is hovered, and fully shown on keyboard focus, on touch screens, on the open chat and while its menu is open.
- The menu is the stock DropdownMenu: arrow keys, Escape, and Delete set apart and in the destructive colour.
- Rename turns the row into a text field named "Rename" followed by the title, with the cursor in it and the text selected. Enter saves, Escape cancels, and focus returns to the row. Moving away with the pointer saves and leaves focus where it was put.
- Delete opens the stock AlertDialog first. After it is confirmed the row becomes a line saying what was deleted with an Undo button, and focus moves to Undo. The countdown waits while the pointer or keyboard focus is on that line.
- A polite live region (role="status") is on the page from the start; it says when a chat is deleted and that Undo is available, and when a chat is restored.

### Design tokens
`--background` · `--foreground` · `--muted` · `--muted-foreground` · `--border` · `--input` · `--ring` · `--destructive` · `--popover` · `--sidebar` · `--sidebar-border`

registry/agent-rules/skills/quill-components/reference/checkbox.md
# checkbox (component)

A single independent yes/no selection — check it or leave it, with no relation to any other Checkbox.

### When to use
- You need one or more independent boolean choices — accept terms, opt into a list, select several rows in a table.

### Reach for instead
- **radio-group** — when only one of several options can be true at once — Checkbox has no concept of mutual exclusion.
- **switch** — when the boolean is a setting that applies immediately, not a value collected as part of a form.

### Rules
- **Do:** Always pair Checkbox with a Label via htmlFor/id, or wrap both in one label — not just visual proximity to nearby text. **Don't:** Render a bare Checkbox next to plain text with no htmlFor/id association — screen readers announce no name.
- **Do:** Mark a failed validation with aria-invalid — the border and ring switch to --destructive automatically. **Don't:** Reach for a custom red className to show an error state — it fights the built-in invalid styling and skips the aria wiring assistive tech needs.
- **Do:** Use Checkbox for independent yes/no selections — each one can be true or false on its own. **Don't:** Group several Checkboxes to represent a single mutually-exclusive choice — that's what RadioGroup is for.

### Accessibility
- Renders role="checkbox" with aria-checked reflecting true/false (or "mixed" for an indeterminate state).
- A visually-hidden native `<input type="checkbox">` underneath handles the real keyboard behavior — Space toggles it, Enter is deliberately blocked so it cannot double as a submit trigger.
- aria-invalid drives the destructive border/ring automatically, the same token pattern as Input and Select.

### Design tokens
`--primary` · `--input` · `--ring` · `--destructive` · `--radius-sm`

registry/agent-rules/skills/quill-components/reference/checkout.md
# checkout (pattern)

A two-column checkout — payment method picker and card form beside an order summary sidebar.

### When to use
- You need to collect payment and card details alongside an order summary.

### Reach for instead
- **order-summary** — when you only need the cart-review card, not the full payment form beside it.
- **wizard** — when payment is one step in a longer multi-step flow, not a single standalone page.

### Rules
- **Do:** Use a RadioGroup for payment-method choice (Card/Bank/Wallet) — it's a single mutually exclusive choice, styled as cards. **Don't:** Use checkboxes or a dropdown for a single mutually exclusive payment method.
- **Do:** Pair the payment submit button with a lock icon, so the action itself visibly reads as a secure transaction. **Don't:** Ship the payment submit button with no lock icon — nothing on the button signals that submitting card data is secure.

### Accessibility
- The RadioGroup carries an explicit aria-label ("Payment method") since there is no visible fieldset legend for the icon-card options.
- Each payment-method option's actual radio input is sr-only, but the whole label (icon + text) is the click target and carries the accessible name.

### Design tokens
`--card` · `--input` · `--primary` · `--ring` · `--border`

registry/agent-rules/skills/quill-components/reference/citations.md
# citations (component)

Shows where an AI answer came from — a small chip after each claim, named for its source, that previews it, and an "N sources" list under the answer.

### When to use
- An AI answer draws on files, pages or records people may want to check.

### Reach for instead
- **hover-card** — when you need a preview on an ordinary link that is not an AI source.
- **badge** — when you are labelling a category, not pointing to where a claim came from.

### Rules
- **Do:** Put the chip right after the sentence it backs, named for its source. **Don't:** Collect sources only in a footer — people cannot tell which source backs which claim.
- **Do:** Leave the chips and list in neutral ink. **Don't:** Colour citations with the AI gradient or a status colour — they are references, not AI and not status.

### Accessibility
- Each chip is a link (or a button when there is no address) named "Q3 deck, source: Q3 board deck.pdf" (the visible label first, then the full title; "Source: `<title>`" when there is no label); keyboard focus opens the same preview as hover.
- A chip with no address opens its preview when pressed or tapped and keeps it open (a press never closes a preview that hover or focus just opened); pressing it again, pressing Escape, pressing outside, or moving focus away closes it. It tells assistive tech with aria-expanded. A chip with an address stays a link.
- Chips are 20px tall, larger than a superscript mark, so they are easy to hit.
- The sources pill is a disclosure button with aria-expanded controlling the list.

### Design tokens
`--muted` · `--ink-soft` · `--foreground` · `--background` · `--border` · `--muted-foreground` · `--popover`

registry/agent-rules/skills/quill-components/reference/collapsible.md
# collapsible (component)

A single open/closed disclosure toggle — the lower-level primitive behind Accordion, with no item/trigger/content grouping or built-in styling.

### When to use
- You need one standalone show/hide toggle — a "view more" panel, an expandable filter section — without Accordion's multi-item structure.

### Reach for instead
- **accordion** — when you have several related items that should share expand/collapse semantics as a group, with only one (or a chosen few) open at once.

### Rules
- **Do:** Pair CollapsibleTrigger with a chevron icon that rotates on open, as the story does, so the toggle state reads visually. **Don't:** Ship a bare text trigger with no open/closed affordance — users can't tell whether clicking will show or hide content.
- **Do:** When you swap the default `<button>` via `render` (e.g. onto a styled Button, as this story does), keep the target a real interactive element so focus and Enter/Space keep working. **Don't:** Pass `render` onto a plain `<div>` or `<span>` with no button semantics — you lose keyboard focusability and native activation.

### Accessibility
- CollapsibleTrigger renders a real `<button>` by default, with `aria-expanded` and `aria-controls` linked to the panel — the `render` prop, as used with Button in the story, swaps in a different element while keeping that same behavior and ARIA wiring.
- Collapsible ships with no built-in visual styling — the focus ring, hover state, and chevron rotation all come from what you render inside it, so build those affordances deliberately rather than assuming Accordion's look carries over.

registry/agent-rules/skills/quill-components/reference/combobox.md
# combobox (component)

A searchable dropdown — type to filter a list, or multi-select with removable chips.

### When to use
- You need to filter a long option list by typing, or let users select multiple values as removable chips.

### Reach for instead
- **select** — when the list is short (≤20) and doesn't need search — Select is less markup and less wiring.
- **native-select** — when you want the OS-native picker instead — no search, no custom popup.

### Rules
- **Do:** Pass the full item array to `<Combobox items={…}>` on the root so ComboboxCollection can filter it. **Don't:** Omit items on the root — ComboboxCollection has nothing to filter, so the popup renders empty ("No results found") no matter what the user types.
- **Do:** Wrap items in ComboboxCollection with a render-function child — it gets called once per filtered item. **Don't:** Render a static list of ComboboxItem elements outside ComboboxCollection — nothing gets filtered.
- **Do:** Pass ComboboxValue as a direct child of ComboboxTrigger, and keep the render element (e.g. Button) childless. **Don't:** Nest content inside the render prop's own JSX (e.g. `<Button>…<ComboboxValue/></Button>`) — Base UI's prop merge gives the render element's own children priority over ComboboxTrigger's, so anything else passed as ComboboxTrigger's real children, including this wrapper's own dropdown chevron icon, is silently discarded rather than shown alongside it.

### Accessibility
- The trigger/input exposes role="combobox" with aria-expanded/aria-controls wired to the popup listbox by Base UI.
- ComboboxEmpty renders "No results found" when the filtered set is empty — always provide it so the popup never goes silently blank.
- Each ComboboxChip's remove control ships with a built-in aria-label ("Remove") — no extra wiring needed for multi-select chips.

### Design tokens
`--input` · `--ring` · `--popover` · `--accent` · `--destructive` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/command.md
# command (component)

A keyboard-first search palette — embed inline for settings search, or wrap in CommandDialog for a ⌘K-style command palette.

### When to use
- You need a searchable, keyboard-navigable list of actions or destinations — settings search, a ⌘K command palette.

### Reach for instead
- **command-palette** — when you want the finished ⌘K launcher, with grouped actions, recent items and shortcuts, ready to drop in. Use Command to build your own palette or an inline search list from its parts.
- **combobox** — when you need a single searchable field bound to a form value, not a multi-group action palette.
- **dropdown-menu** — when the action set is short enough for a plain click-triggered list, with no need for search.

### Rules
- **Do:** Group related commands under a CommandGroup heading ("Files", "Settings"), separated by CommandSeparator. **Don't:** List every command flat with no CommandGroup heading — a long palette with no structure is hard to scan or search meaningfully.
- **Do:** Always include CommandEmpty ("No results found.") — cmdk shows it automatically when a search matches nothing. **Don't:** Omit CommandEmpty — an unmatched search then renders a silently blank list with no explanation.
- **Do:** Embed Command inline for a settings-search panel, or wrap it in CommandDialog for a ⌘K palette — CommandDialog is Dialog underneath, so it inherits Dialog's focus trap and Escape-to-close. **Don't:** Reimplement the ⌘K modal shell yourself instead of using CommandDialog — you'd have to re-earn Dialog's focus management by hand.

### Accessibility
- The input drives a listbox: arrow keys move the highlighted item, Enter selects it, and typed text filters the list live.
- CommandDialog defaults showCloseButton to false and gives the dialog a sr-only title/description, since the search input itself is the primary, visible entry point.
- CommandSeparator is marked aria-hidden — it is a purely visual divider, not a semantic list boundary.

### Design tokens
`--popover` · `--border` · `--radius-xl`

registry/agent-rules/skills/quill-components/reference/command-palette.md
# command-palette (pattern)

A searchable ⌘K command palette with grouped actions, recent items, and keyboard shortcuts.

### When to use
- You need a searchable ⌘K launcher for actions and quick navigation.

### Reach for instead
- **command** — when the list is embedded in the page (settings search) or needs groups and behaviour the finished launcher does not have, so you build it from Command parts. Use command-palette for the ready-made ⌘K overlay.
- **search-results** — when search results should live inline on the page, not in a global keyboard-triggered overlay.
- **dropdown-menu** — when the list of options is small and tied to a specific trigger element, not a global searchable launcher.

### Rules
- **Do:** Group items into labeled sections (Actions, Recent) so a long list stays scannable. **Don't:** Dump every possible command into one flat, unlabeled list.

### Accessibility
- CommandInput is the palette's real search field — typing filters the list live, and arrow keys move focus through CommandItem rows without a mouse.
- Keyboard shortcuts (CommandShortcut) are supplementary hints, not the only way to trigger an action — every item is also reachable by typing its label and pressing Enter.

### Design tokens
`--border` · `--card` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/contact-form.md
# contact-form (pattern)

A get-in-touch card — name, email, a topic select, and a message field.

### When to use
- You need a get-in-touch form with name, email, topic, and message fields.

### Reach for instead
- **newsletter** — when you only need a single email field to grow a list, not a full multi-field inquiry form.
- **settings** — when the fields are updating an existing profile, not sending a one-off message.

### Rules
- **Do:** State a response-time expectation in the description ("We usually reply within one working day.") so the form doesn't feel like it disappears into a void. **Don't:** Leave users with no sense of whether or when they'll hear back.

### Accessibility
- Every field, including the Select, has its own `<Label htmlFor>`, and the topic Select's trigger id matches its label's htmlFor.
- The markdown hint under Message is supplementary text, not a replacement for the field's own label.

### Design tokens
`--card` · `--input` · `--primary` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/context-menu.md
# context-menu (component)

A menu that appears at the pointer on right-click or long-press — a secondary, discoverable-only-by-trying affordance, not a replacement for a visible trigger.

### When to use
- You need a secondary set of actions on an item that most users will discover by right-clicking (desktop) or long-pressing (touch), not a primary action path.

### Reach for instead
- **dropdown-menu** — when the trigger should be a visible button rather than a right-click-only zone.

### Rules
- **Do:** Give the trigger zone a visible affordance and hint text ("Right-click here") — right-click has no visible button, so nothing else tells users the surface is interactive. **Don't:** Make the trigger zone look identical to ordinary static content — users have no way to discover that right-click does anything there.
- **Do:** Keep menus short — group related actions and separate groups with ContextMenuSeparator. Max ~8 items. **Don't:** Flatten a long, ungrouped list of actions into one ContextMenu — right-click menus are for quick, scannable choices.
- **Do:** Put a destructive item (variant="destructive") last, separated from routine actions by ContextMenuSeparator. **Don't:** Mix a destructive item in among routine actions with no separator — a misread click becomes an easy accident.

### Accessibility
- Opens on right-click (desktop) or long-press (touch); once open, arrow keys navigate items and typeahead jumps to a matching label.
- ContextMenuTrigger wraps the target element — its own accessible name/role stays whatever the wrapped element already provides.
- Escape closes the menu and returns focus to the trigger.

### Design tokens
`--popover` · `--shadow-md` · `--destructive` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/cookie-consent.md
# cookie-consent (pattern)

A cookie consent banner offering accept, reject, and preferences actions.

### When to use
- You need a compliance banner offering accept, reject, and preferences choices.

### Reach for instead
- **announcement-banner** — when the message is informational only and doesn't require an explicit accept/reject decision.
- **alerts** — when the message is a local, in-page status, not a site-wide compliance notice.

### Rules
- **Do:** "Reject non-essential" gets the same one-click ease as "Accept all" — both are top-level buttons, not one button plus a buried settings link. **Don't:** Make rejecting cookies harder to find or reach than accepting them — that's a dark pattern, not a real choice.

### Accessibility
- All three choices (Accept all, Reject non-essential, Preferences) are real, separately labeled buttons, not one deceptively generic "OK".
- The banner's icon is decorative; the actual compliance information is conveyed entirely through the paragraph text, not the icon.

### Design tokens
`--card` · `--border` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/dashboard.md
# dashboard (pattern)

An app shell with sidebar navigation, a search header, and KPI stat cards.

### When to use
- You need a full application home — sidebar, search header, and KPI cards.

### Reach for instead
- **sidebar-nav** — when you need the collapsible icon-sidebar variant with grouped menus, not this fixed-width text sidebar.
- **stat-cards** — when you only need the KPI row, not the full shell around it.

### Rules
- **Do:** Give the current nav item a distinct active state (background + text color), not just a subtle underline. **Don't:** Leave every nav item looking identical — users lose track of where they are.

### Accessibility
- Nav buttons use both background color and text color for the active state, not a single subtle cue easy to miss.
- The notifications button is icon-only with an explicit aria-label, consistent with every other icon-only trigger in the catalog.

### Design tokens
`--sidebar` · `--accent` · `--accent-foreground` · `--border`

registry/agent-rules/skills/quill-components/reference/data-table.md
# data-table (pattern)

A filterable members table with a toolbar, ToneBadge status pills, and a per-row actions menu.

### When to use
- You need a filterable, actionable table of records with tone-mapped status pills and row actions.

### Reach for instead
- **activity-feed** — when you want a read-only chronological list, not sortable/filterable records with row actions.
- **kanban** — when the records are better organized by status columns than by table rows.

### Rules
- **Do:** Give every icon-only row-action button a per-row aria-label (e.g. "Actions for Ada Lovelace"), not a generic one. **Don't:** Ship a table full of icon-only buttons that all announce as the same unlabeled button to a screen reader.

### Accessibility
- Status is conveyed by the ToneBadge's text, not its tone alone ("Active" vs "Invited" read as different words, not just moss vs gold).
- The actions column header uses a visually-hidden ("sr-only") label instead of empty text, so the column still has an accessible name.

### Design tokens
`--border` · `--muted` · `--moss` · `--moss-deep` · `--gold` · `--gold-text`

registry/agent-rules/skills/quill-components/reference/dialog.md
# dialog (component)

A modal window over the page for focused, non-destructive tasks — forms, details, reversible confirmations.

### When to use
- You need the user to complete a focused task without leaving the page — edit a record, fill a short form, confirm a reversible action.

### Reach for instead
- **alert-dialog** — when the action is destructive or irreversible (delete, overwrite) — AlertDialog forces an explicit choice.
- **sheet** — when the content is secondary context sliding in from an edge and the page should stay visible.
- **drawer** — when you need the edge-panel job on touch devices, with swipe-to-dismiss.
- **popover** — when the content is a small, light overlay anchored to its trigger, not a blocking task.

### Rules
- **Do:** Confirm destructive actions with AlertDialog. **Don't:** Use Dialog to confirm deletes or overwrites — it dismisses too easily.
- **Do:** Keep the default close button, or pass showCloseButton={false} only when a footer button offers an explicit way out. **Don't:** Remove the close button and leave no visible way to dismiss.
- **Do:** Style the Dialog trigger as outline, secondary, or default — Dialog is for non-destructive tasks. **Don't:** Style the Dialog trigger as destructive — that treatment belongs to AlertDialog's triggers, not Dialog's.

### Accessibility
- Focus moves into the dialog on open and returns to the trigger on close.
- Escape closes the dialog.
- Always render DialogTitle — it is the accessible name; pair it with DialogDescription for context.

### Design tokens
`--card` · `--shadow-lg` · `--radius-xl`

registry/agent-rules/skills/quill-components/reference/drawer.md
# drawer (component)

A bottom- or edge-anchored panel built on Vaul, with drag-to-dismiss — a mobile-first alternative to Dialog's centered modal.

### When to use
- You need a mobile-first interaction — filters, quick actions, secondary navigation — that slides in from an edge and can be dragged closed.

### Reach for instead
- **sheet** — when you don't need the drag gesture — Sheet is the same edge-panel job without Vaul's swipe-to-dismiss.
- **dialog** — when the task is a focused, centered action rather than a mobile-style edge panel.

### Rules
- **Do:** Wrap the trigger child with asChild on DrawerTrigger — Drawer is built on Vaul, which uses Radix's composition API. **Don't:** Pass a `render` prop to DrawerTrigger the way you would for Sheet or Dialog — those are Base UI components; Drawer has no render prop.
- **Do:** Leave the default direction="bottom" for mobile-style quick actions and filters — that's also the only direction that renders the drag handle. **Don't:** Set direction="left/right/top" out of habit for every Drawer — bottom is the touch-first pattern Vaul is built around.
- **Do:** Label the trigger for what will open — "Open side panel" for a direction="right" Drawer, not a generic "Open drawer." **Don't:** Reuse a generic "Open drawer" label regardless of direction — a right-side panel trigger that still says "Open drawer" reads like the default bottom sheet.

### Accessibility
- Extends the same dialog semantics as Dialog: focus moves into the panel on open and returns to the trigger on close.
- Drag-to-dismiss is a pointer/touch gesture only — Escape and DrawerClose remain the keyboard and screen-reader way to close it.
- Always render DrawerTitle — it is the accessible name; pair it with DrawerDescription for context.

### Design tokens
`--popover` · `--muted` · `--border` · `--radius-xl`

registry/agent-rules/skills/quill-components/reference/dropdown-menu.md
# dropdown-menu (component)

A click-triggered list of actions anchored to a visible button, with full keyboard navigation and typeahead.

### When to use
- You need a set of actions or choices behind one visible trigger button — a row's "more actions" menu, a settings menu.

### Reach for instead
- **context-menu** — when the action set is a secondary, right-click-only affordance rather than something driven by a visible trigger button.
- **menubar** — when you need a persistent horizontal strip of always-visible top-level menus, not a single dropdown.

### Rules
- **Do:** Give an icon-only trigger (e.g. a "more actions" icon button) an explicit aria-label, the way the more_horiz trigger uses "More actions." **Don't:** Ship an icon-only trigger with no aria-label — there is no visible text for screen readers to fall back on.
- **Do:** Group related items with DropdownMenuLabel and DropdownMenuSeparator; keep the menu to roughly 8 items. **Don't:** Run every action together in one flat, ungrouped list — long unstructured menus are hard to scan.
- **Do:** Put destructive items last, separated from routine actions with DropdownMenuSeparator. **Don't:** Mix a destructive item in with routine actions with no separator to set it apart.

### Accessibility
- Renders role="menu" with roving keyboard focus — arrow keys move between items, typeahead jumps to a matching label, Escape closes and returns focus to the trigger.
- closeOnClick defaults to true per item — set it to false on a DropdownMenuCheckboxItem or DropdownMenuItem that should stay open after selection.
- Base UI keeps the popup mounted through its closing transition; code that asserts on menu absence should wait for it to fully unmount, not just for the close click.

### Design tokens
`--popover` · `--shadow-md` · `--destructive` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/empty.md
# empty (component)

An empty-state block for zero-data surfaces — icon, title, description, and a primary action, centered in the available space.

### When to use
- A list, table, or search result surface has no data to show yet and you need to explain why, plus give the user a way to fix it (create, upload, search again).

### Reach for instead
- **empty-state** — when you want the finished panel dropped into a page as it is: a dashed-border card with a round icon well, a line of text and one primary action, already styled. Use Empty when the empty state sits inside your own list, table or card and you compose it from its parts.
- **skeleton** — when the surface is still loading real data and not yet confirmed empty — use Skeleton for the loading state, and switch to Empty only once loading finishes with zero results.

### Rules
- **Do:** Pair every Empty with a title, a short description, and at least one primary action, as both stories here do, so the user has a next step. **Don't:** Show an icon and a title alone with no description or action — the user learns there's nothing here but not what to do about it.
- **Do:** Pick an EmptyMedia icon that matches the content type — a book for courses, a folder for files. **Don't:** Reuse the same generic icon for every empty state across the app regardless of what's missing — it stops carrying any information.

### Accessibility
- Empty, EmptyHeader, EmptyContent, and EmptyMedia are all plain `<div>`s with no built-in role; EmptyTitle is a styled `<div>`, not a heading, so add real heading markup inside it when the empty state should appear in the page's heading outline.
- EmptyDescription renders as a `<div>` in the current component (its prop type is inherited from `<p>`, but the rendered tag is `<div>`) — treat it as body text and keep it as the second read after the title, before any action buttons.

### Design tokens
`--muted` · `--muted-foreground` · `--foreground` · `--border`

registry/agent-rules/skills/quill-components/reference/empty-state.md
# empty-state (pattern)

A dashed-border empty state with an icon, explanatory text, and a primary action.

### When to use
- You need to explain why a view is empty and offer the primary action to fill it.

### Reach for instead
- **empty** — when the empty state sits inside an existing surface (a list, a table, a card, a search result) and you compose it from parts: media, title, description and actions. Use empty-state for the finished stand-alone panel.
- **error-404** — when the view is empty because the resource doesn't exist or the route is wrong, not because the user hasn't created anything yet.
- **onboarding** — when there's a multi-step setup to walk through, not a single action to take right now.

### Rules
- **Do:** Pair the explanation with a real, primary action button that actually fills the view (e.g. "New project"). **Don't:** Explain that a view is empty and leave the user with no way to act on it from that screen.

### Accessibility
- The icon and dashed border are decorative framing; the heading and body text carry the actual meaning, not the visual style alone.
- The heading uses a real `<h3>`, giving the empty state its own accessible name within the surrounding page structure.

### Design tokens
`--card` · `--border` · `--muted` · `--primary`

registry/agent-rules/skills/quill-components/reference/error-404.md
# error-404 (pattern)

A full-page 404 error state with go-back and home recovery actions.

### When to use
- You need a full-page not-found state with recovery actions.

### Reach for instead
- **empty-state** — when the resource exists but is legitimately empty (no items yet), not missing or misrouted.
- **alerts** — when the error is recoverable inline on the current page, not a full-page dead end that needs its own route.

### Rules
- **Do:** Offer both a contextual escape (Go back) and an absolute one (Take me home) — never strand the user with only one recovery path. **Don't:** Leave a 404 page with no action at all, or only a single 'Home' link buried in a header the user has to hunt for.

### Accessibility
- The page has a real `<h1>` ("Page not found") — the large "404" numeral above it is decorative display text, not the page's accessible name.
- Go back and Take me home are two distinct, clearly labeled buttons, not a single ambiguous action.

### Design tokens
`--background` · `--muted-foreground` · `--primary`

registry/agent-rules/skills/quill-components/reference/example-app-page.md
# example-app-page (pattern)

A whole signed-in app page, composed: the sidebar-nav shell around a page-header, then stat-cards, then a data-table.

### When to use
- You are building a signed-in screen about one collection of records (members, projects, invoices): what the page is, a few numbers about it, and the records themselves.
- You want to start from a page whose shell, title and order are already right, and only replace the sample content.

### Reach for instead
- **example-marketing-page** — when the page is public and persuades a visitor to act: no signed-in shell, no records, read top to bottom and left through a call to action.
- **example-auth-page** — when the screen has one task, signing in, and nothing else may share the viewport: no shell, no navigation, no numbers.
- **dashboard** — when you need an application home with a search header and a fixed text sidebar rather than a page about one collection. It is its own shell, so never put it inside this page.
- **list-detail** — when people work through the records one at a time and need the selected one open beside the list, not a table they scan.
- **tabs-page** — when the page is settings split into panels (account, notifications, security), not a collection with numbers.

### Rules
- **Do:** Keep page-header first inside the shell: its breadcrumb and title say what the page is before any number appears. **Don't:** Open the page with the stat-cards row, the generic dashboard Quill avoids. Numbers sit inside a page that has already said what it is about.
- **Do:** Pass the page content to sidebar-nav as its children, so the top bar, the main area and the 24px gap between pieces come from the shell. **Don't:** Wrap the page in a second shell (dashboard, another sidebar) or add your own padding around the children; the shell already provides both.
- **Do:** Make each stat card a number about the records in the table below it, and rename the header, cards and columns together. **Don't:** Keep the sample content half-replaced: a "Project settings" title over member counts reads as two pages stitched together.

### Accessibility
- Landmarks: the shell gives a banner (the top bar with the sidebar toggle) and one main; page-header adds a navigation named "breadcrumb". The sidebar menu is not a landmark, so if your app has no other navigation landmark, wrap the menu in a nav with an aria-label.
- Heading order: page-header holds the only heading, the h1. The stat-card titles and the table are not headings; give any section you add an h2 so the outline stays h1 → h2.
- Focus order follows the composition: sidebar menu items, the sidebar toggle, the breadcrumb links, Duplicate and New, the table filter, Filter and Invite, then one labelled actions button per row. At phone width the sidebar is off-canvas, so focus starts at the toggle.
- There is no skip link. Add one as the first focusable element, pointing at the main element, so keyboard users can pass the sidebar menu without tabbing through every item.

### Design tokens
`--sidebar` · `--sidebar-foreground` · `--sidebar-border` · `--background` · `--foreground` · `--muted-foreground` · `--border` · `--card` · `--primary` · `--space-4` · `--space-6`

registry/agent-rules/skills/quill-components/reference/example-auth-page.md
# example-auth-page (pattern)

A whole sign-in page, composed: login-split-panel alone on the page, with nothing competing with the one task.

### When to use
- You need the page people land on to sign in, standing on its own: no navbar, no footer, no promo strip.
- You want to start from a page that already gives the sign-in the whole screen, and only replace the brand quote and copy.

### Reach for instead
- **example-app-page** — when the person is already signed in and the page shows their records inside the app shell.
- **example-marketing-page** — when the page sells the product to a visitor. It links to this page through Sign in rather than holding a form.
- **login** — when the sign-in is a card inside a page that already has its own shell, not a page of its own.
- **login-minimal** — when you have no brand quote worth the space and want a bare sign-in by one-time code.
- **signup** — when the person is creating an account, not signing in to one.

### Rules
- **Do:** Leave the sign-in alone on the page; the only other actions are the form's own: Forgot? and the magic link. **Don't:** Add a navbar, a footer or a promo strip around it: every other link is a way to abandon signing in.
- **Do:** Replace the brand panel's quote and wordmark with your own, and keep the form column as it is. **Don't:** Move product claims or extra fields into the form column; the panel is the only place for brand story, and it hides on phones.

### Accessibility
- Landmarks: the page has no main landmark. Wrap the form column in a main element in your app. The quote's attribution is a footer inside the blockquote, which Chrome reports as a page footer (contentinfo); make it a figcaption or a p if you keep the quote.
- Heading order: "Welcome back" is the only heading, the h1. The brand panel has no heading, which is right for decorative copy.
- Focus order: email, Forgot?, password, Sign in, then Continue with a magic link. Forgot? sits between the two fields because it shares the password label's row; if that trips people up, move it below the password field.
- There is no skip link and none is needed: the email field is the first focusable element. At phone width the brand panel is hidden, and with it the quote, so nothing a screen reader needs is lost.

### Design tokens
`--primary` · `--primary-foreground` · `--background` · `--foreground` · `--muted-foreground` · `--input` · `--ring` · `--border` · `--font-heading`

registry/agent-rules/skills/quill-components/reference/example-marketing-page.md
# example-marketing-page (pattern)

A whole public marketing page, composed: navbar, hero, feature-section, pricing, testimonial and footer, one idea per section.

### When to use
- You are building a public page that persuades a visitor, such as a product landing page or a pricing page, read top to bottom and left through a call to action.
- You want to start from a page whose section order and 96px section rhythm are already right, and only replace the sample content.

### Reach for instead
- **example-app-page** — when the visitor is signed in and works with records: a sidebar shell, a page title and a table, not sections that sell.
- **example-auth-page** — when the page only signs someone in. The navbar's Sign in leads there; do not build a sign-in form into a marketing section.
- **ai-home** — when the first screen is an AI assistant's start page, with a greeting and a composer, not a page that sells a product.
- **error-404** — when the page is a missing-page state: one message and two ways out, not a page of sections.

### Rules
- **Do:** Give every section one idea: a heading and one composition (the feature grid, the pricing grid, one quote). **Don't:** Stack unrelated widgets in one section, or add a section that repeats the hero's promise in other words.
- **Do:** Wrap a section you add the way pricing and testimonial are wrapped: py-24 (py-14 on mobile), px-12 (px-6 on mobile), inside the 1400px width. **Don't:** Give each section its own padding, or add padding around hero and feature-section, which already carry their own rhythm.
- **Do:** Keep one primary call to action per view (Get started in the navbar and hero, the featured plan in pricing) with quieter ghost or outline buttons beside it. **Don't:** Make every button primary: when everything is the main action, nothing is.

### Accessibility
- Landmarks: the navbar is a navigation, the sections sit in one main, and the footer is the contentinfo with three navigations named Product, Resources and Company. The navbar's navigation has no name; give it aria-label="Main" so it is told apart from the footer's three.
- Heading order: the hero holds the h1, feature-section an h2 with h3 per feature. Pricing and testimonial have no heading of their own; when you add one, make it an h2 so the outline stays h1 → h2 → h3.
- Focus order follows the composition: navbar links, Sign in, Get started, the hero's two actions, the three plan buttons, then the footer links and the legal links. Below the md width the navbar's link list is hidden and nothing replaces it, so add a menu button if those links matter on phones.
- There is no skip link. Add one as the first focusable element, pointing at the main element, so keyboard users can pass the navbar.

### Design tokens
`--background` · `--foreground` · `--muted-foreground` · `--border` · `--card` · `--primary` · `--ring` · `--font-heading` · `--space-24`

registry/agent-rules/skills/quill-components/reference/faq.md
# faq (pattern)

A centered FAQ section — an accordion of common questions with a contact call-to-action footer.

### When to use
- You need to answer common questions in an accordion with a contact fallback.

### Reach for instead
- **accordion** — when you need a bare accordion with no marketing framing (heading, contact footer) around it.
- **contact-form** — when the user needs to ask something not covered — this pattern only offers a CTA button, not the form itself.

### Rules
- **Do:** Open the first question by default (defaultValue=['item-0']) so the section doesn't read as empty on load. **Don't:** Start every item collapsed and force a first click just to prove the section has content.

### Accessibility
- AccordionTrigger elements are real buttons — each question is reachable and operable by keyboard, not just click.
- The contact fallback card pairs plain text ("Still curious?") with a real button, not a bare link buried in a paragraph.

### Design tokens
`--card` · `--border` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/feature-section.md
# feature-section (pattern)

A marketing section with an editorial heading and a three-column feature grid.

### When to use
- You need to present product features in an editorial multi-column grid.

### Reach for instead
- **stats-band** — when you're presenting numbers, not descriptive feature callouts with icons and body text.
- **pricing** — when the features are tied to specific paid tiers, not the product as a whole.

### Rules
- **Do:** Keep the grid to three (or a multiple of three) features so it reads evenly across the row. **Don't:** Let an uneven feature count leave a dangling, unbalanced last row on wide screens.

### Accessibility
- Each feature's icon is paired with a real heading (`<h3>`) and body text — the icon alone never carries the feature's meaning.
- Feature headings use a real heading level (`<h3>`) nested under the section's own `<h2>`, keeping the outline logical for screen-reader navigation.

### Design tokens
`--muted` · `--primary` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/field.md
# field (component)

A form field wrapper that connects a Label, FieldDescription, and FieldError to a control.

### When to use
- You have a form control that needs a label, optional helper text, and/or a validation error message wired together.

### Reach for instead
- **label** — when the control needs nothing more than an accessible name — no description, no error state to wire up.

### Rules
- **Do:** Add data-disabled to Field (alongside the control's real disabled attribute) so FieldLabel dims — e.g. `<Field data-disabled>`. **Don't:** Disable only the control and expect FieldLabel to dim on its own — Field doesn't inspect its child's disabled attribute; the dimming is opt-in via data-disabled on Field itself.
- **Do:** Wrap every form control in Field, with FieldLabel/FieldDescription/FieldError as siblings — Field's role="group" plus FieldError's role="alert" gives you the aria wiring for free. **Don't:** Hand-assemble label/input/error markup ad hoc per field — you have to re-derive the association and alert wiring Field already provides.

### Accessibility
- Field renders role="group"; FieldError renders role="alert", so a validation message appearing after submit gets announced automatically.
- FieldLabel dimming on disable is opt-in — set data-disabled on Field yourself; it isn't derived automatically from the control's disabled attribute.
- orientation="horizontal" changes layout only; it does not change any of the aria wiring.

### Design tokens
`--primary` · `--destructive` · `--muted` · `--border` · `--background`

registry/agent-rules/skills/quill-components/reference/file-upload.md
# file-upload (pattern)

An asset-upload card with a drag-and-drop dropzone and a progress-tracked upload queue.

### When to use
- You need drag-and-drop asset upload with a progress-tracked queue.

### Reach for instead
- **contact-form** — when you need a single attachment inside a broader form, not a dedicated upload queue as the whole card.

### Rules
- **Do:** Make the dropzone a real, focusable `<button>` that also opens a file picker on click/Enter — never make drag-and-drop the only way in. **Don't:** Ship a dropzone that only responds to a pointer drag gesture.
- **Do:** Give each in-progress file its own progress bar, so users can see exactly how far each individual upload has gotten. **Don't:** Show one shared progress bar for the whole queue — users can't tell which file it belongs to or how far any single file has gotten.

### Accessibility
- Each in-progress file's Progress has its own aria-label (e.g. "Uploading field-notes-issue-001.pdf"), not one shared label for the whole queue.
- Every remove button carries a per-file aria-label ("Remove field-notes-issue-001.pdf"), not a generic "Remove".

### Design tokens
`--card` · `--input` · `--muted` · `--primary`

registry/agent-rules/skills/quill-components/reference/footer.md
# footer (pattern)

A marketing footer with brand blurb, link columns, and legal links.

### When to use
- You need a site-wide footer with link columns, a brand blurb, and legal links.

### Reach for instead
- **sidebar-nav** — when the links are primary in-app navigation, not secondary site-wide links at the page's end.
- **navbar** — when the links belong at the top of the page as primary navigation, not the bottom as reference.

### Rules
- **Do:** Wrap each link column in its own `<nav aria-label>`, as this block already does, so screen-reader users can jump between them. **Don't:** Render link columns as bare, unlabeled `<div>` lists that all announce as one undifferentiated block.
- **Do:** Separate the bottom legal row (copyright, Privacy, Terms) from the link columns with a visible Separator, as this block does. **Don't:** Rely on extra margin alone with no divider — the legal row reads as just another link column, not a distinct footer end-cap.

### Accessibility
- Each link column is a real `<nav>` with an aria-label matching its visible heading (e.g. "Product", "Resources"), not a generic landmark.
- The bottom legal row (copyright, Privacy, Terms) is visually and structurally separated from the link columns by a Separator, not just extra margin.

### Design tokens
`--background` · `--muted-foreground` · `--border`

registry/agent-rules/skills/quill-components/reference/forgot-password.md
# forgot-password (pattern)

A password-reset request card — an email field, a send-link action, and a way back to sign in.

### When to use
- You need a password-reset request card that sends a recovery link.

### Reach for instead
- **otp-verification** — when recovery works by a typed one-time code rather than an emailed link.
- **login** — when the user hasn't actually forgotten anything — this card is only the detour reached from login's "Forgot your password?" link.

### Rules
- **Do:** Keep the ghost "Back to sign in" button so a user who clicked in by mistake isn't stuck. **Don't:** Ship this card as a dead end with no return path to sign-in.

### Accessibility
- The back-to-sign-in icon is decorative (aria-hidden by default); the button's accessible name comes from its visible text, not the arrow glyph.
- The email field uses a real `<Label htmlFor>`, same as every other auth card in this set.

### Design tokens
`--card` · `--input` · `--primary` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/hero.md
# hero (pattern)

A centered marketing hero — a release badge, serif headline, supporting copy, and primary/secondary calls to action.

### When to use
- You need the top-of-page marketing statement — headline, supporting copy, and calls to action.

### Reach for instead
- **feature-section** — when you're past the opening statement and need to break down specific product capabilities.
- **announcement-banner** — when the message is a small, dismissible update, not the page's primary opening statement.

### Rules
- **Do:** Keep exactly one primary (default-variant) button — the single most important action — with any secondary action as outline/ghost. **Don't:** Give two calls-to-action equal visual weight; competing primaries dilute the one action that matters most.

### Accessibility
- The headline is a real `<h1>` — a hero is typically the page's first heading and its accessible name.
- The release badge ("New — v2.0 is here") is supplementary context, not the page's only way to communicate the headline's message — it is not a substitute for the `<h1>`.

### Design tokens
`--background` · `--foreground` · `--primary` · `--secondary`

registry/agent-rules/skills/quill-components/reference/hover-card.md
# hover-card (component)

A glanceable preview that opens after a brief hover-intent delay on a link — a visual enhancement for sighted mouse and keyboard users, informational only.

### When to use
- You need a richer preview on hover — a link destination, an author bio, expanded metadata — without navigating away.

### Reach for instead
- **tooltip** — when you just need a short one-line hint on a control, not a rich preview.
- **popover** — when the interaction should be an explicit click rather than hover.

### Rules
- **Do:** Keep HoverCard content informational — preview text, an avatar, metadata a user only needs to glance at. **Don't:** Put an interactive control (a button, a form) inside HoverCard — hover is unreliable on touch devices, so the action becomes unreachable there.
- **Do:** Treat the hover card as a bonus preview — keep the same information reachable by visiting the link itself. **Don't:** Make the hover card the only place certain information lives — preview cards are a sighted mouse/keyboard enhancement only; touch and screen-reader users never see them.

### Accessibility
- Preview cards are visual-only, like Tooltip — not accessible to touch or screen-reader users. Never put unique or essential information only in the popup.
- Opens after an intent delay (600ms by default) rather than instantly, so a cursor merely passing over the trigger doesn't fire it.
- HoverCardTrigger renders as a link — give it real link text or an aria-label; the popup content is not the trigger's accessible name.

### Design tokens
`--popover` · `--shadow-md` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/icon.md
# icon (component)

The Quill icon component — Material Symbols Outlined at weight 400, shipped as a self-contained core set of 98 names covering every icon the Quill blocks use.

### When to use
- You need a glyph inside a Quill block or beside text — a nav item, a status row, an inline affordance — and want it to match the outlined, weight-400 set every block already uses.

### Reach for instead
- **button** — when the glyph is the whole control — use Button with an icon size (`icon`, `icon-sm`) and an `aria-label`; Icon on its own is never interactive.
- **spinner** — when the glyph means "loading" — Spinner carries the motion and the status semantics an icon does not.

### Rules
- **Do:** Pick a name from the core set the `icon` item ships (98 names): account_balance, add, add_circle, archive, arrow_back, arrow_downward, arrow_forward, arrow_upward, attach_file, attach_money, calendar_today, cancel, chat_bubble, check, check_box, check_box_outline_blank, check_circle, chevron_left, chevron_right, close, cloud_upload, content_copy, credit_card, dangerous, dark_mode, dashboard, delete, description, dock_to_left, download, draft, edit, error, favorite, filter_list, folder_open, format_align_center, format_align_left, format_align_right, format_bold, format_italic, format_underlined, forward, group, help, history, home, info, ink_pen, keep, keyboard_arrow_down, keyboard_arrow_left, keyboard_arrow_right, keyboard_arrow_up, keyboard_command_key, language, light_mode, link, lock, login, logout, mail, menu, menu_book, mic, more_horiz, more_vert, notifications, open_in_new, palette, person, person_add, photo_library, progress_activity, radio_button_checked, radio_button_unchecked, refresh, remove, reply, save, schedule, search, send, settings, share, sort, space_dashboard, star, star_shine, stop, thumb_down, thumb_up, undo, upload, visibility, visibility_off, wallet, warning. **Don't:** Use any other Material Symbol name in an app. The consumer `<Icon>` bundles only the core set, so an unknown name renders as an empty, size-reserved box (plus a console warning in development); the design-system site lazy-loads the whole library, so a name can look fine on quilldesignsystem.com and be blank in your app.
- **Do:** Size through the `size` prop — a pixel number, or leave the `1em` default so the glyph follows the surrounding text. **Don't:** Scale an icon with a `transform` or a width/height class — the reserved box stops matching the glyph and the layout shifts.
- **Do:** Leave an icon decorative (the default renders `aria-hidden`) when the text next to it already carries the meaning. **Don't:** Add an `aria-label` to an icon that sits beside its own label — screen readers then announce the meaning twice.

### Accessibility
- Without `aria-label` the SVG is `aria-hidden`; with one it becomes `role="img"` and is announced. Decorative next to text, labelled when the icon stands alone.
- Colour comes from `currentColor`, so an icon inherits the contrast of the text it sits in; carry status meaning in text or a ToneBadge, never in icon colour alone.

registry/agent-rules/skills/quill-components/reference/input.md
# input (component)

A styled native text input for free-form single-line entry — email, name, search, URL.

### When to use
- You need free-form single-line text entry — an email, a name, a search query, a URL.

### Reach for instead
- **textarea** — when the value is multi-line free text (a bio, a description) rather than a single line.
- **input-otp** — when you're collecting a fixed-length code (2FA, email verification) — one boxed slot per character reads better than a single field.
- **input-group** — when the field needs a prefix or suffix — an icon, a unit label, a button — merged into the same bordered control.

### Rules
- **Do:** Pair Input with a real Label (or FieldLabel) — placeholder text disappears the moment the user types. **Don't:** Use placeholder as the only label.
- **Do:** Set aria-invalid="true" on the input for validation errors — it applies the destructive border and ring automatically. **Don't:** Hand-roll red border classes for an error state instead of aria-invalid.

### Accessibility
- Renders a native `<input data-slot="input">`, so standard input semantics (type, autocomplete, native validation) apply.
- aria-invalid="true" drives both the destructive-colored ring and the invalid state assistive tech announces — set it rather than faking it with custom classes.
- disabled removes the input from the tab order (native `<input disabled>` semantics).

### Design tokens
`--input` · `--ring` · `--destructive` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/input-group.md
# input-group (component)

Merges an input (or textarea) with prefix/suffix addons — icons, buttons, plain text — inside one bordered control.

### When to use
- The field needs a prefix or suffix merged into the same bordered control — a search icon, a currency symbol, a unit label, a copy or clear button.

### Reach for instead
- **input** — when the field doesn't need any prefix/suffix affordance — a bare Input is less markup.
- **combobox** — when the "input" is really a searchable picker — Combobox already composes InputGroup internally for its trigger.

### Rules
- **Do:** Set align="inline-end" on InputGroupAddon when it trails the value (a unit label, a suffix icon). **Don't:** Leave a suffix addon at the default align="inline-start" — it renders squeezed onto the left with any prefix instead of trailing the value.
- **Do:** Use InputGroupButton for any clickable suffix (copy, clear, search) so it stays a real `<button>`. **Don't:** Fake a button with a styled clickable `<div>` inside InputGroupAddon — the addon's own click-to-focus handler only skips real `<button>` elements, so it still steals the click.

### Accessibility
- The wrapper and each InputGroupAddon carry role="group", grouping the control with its affixes for assistive tech.
- Clicking a non-interactive addon (an icon, plain text) focuses the input inside it — the click only stays put when it lands on a real `<button>`.
- An aria-invalid control inside the group (InputGroupInput or InputGroupTextarea) turns the whole group border/ring destructive — there is no separate error prop on InputGroup itself.

### Design tokens
`--input` · `--ring` · `--destructive` · `--radius-lg` · `--radius`

registry/agent-rules/skills/quill-components/reference/input-otp.md
# input-otp (component)

Digit-by-digit secure code entry — email verification, 2FA, PIN.

### When to use
- You're collecting a fixed-length code (verification, 2FA, PIN) and want one boxed slot per character instead of a single text field.

### Reach for instead
- **input** — when the value isn't a fixed-length code — free-form text belongs in a single Input.

### Rules
- **Do:** Set maxLength to match the total number of rendered InputOTPSlot elements (e.g. 6 slots → maxLength={6}). **Don't:** Let maxLength drift from the rendered slot count — fewer slots than maxLength hides digits the user already typed; more slots than maxLength leaves boxes that can never fill.
- **Do:** Set aria-invalid on InputOTPGroup to mark that group of slots as errored. **Don't:** Style an error state by hand-coloring individual slots instead of using aria-invalid.

### Accessibility
- Under the hood, one real (transparent, overlaid) `<input>` handles all keyboard input, paste, and autofill — the visible InputOTPSlot boxes are decorative, not separate form fields.
- autoComplete defaults to "one-time-code", so mobile browsers can offer SMS-code autofill with no extra wiring.
- InputOTPSeparator has role="separator" and an aria-hidden icon, so it is not announced as a character slot.

### Design tokens
`--input` · `--ring` · `--destructive` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/invoice.md
# invoice (pattern)

A printable invoice card — sender and recipient details, a ToneBadge payment status, a line-item table with totals, and a download action.

### When to use
- You need a printable invoice with parties, line-item totals, and a download action.

### Reach for instead
- **order-summary** — when you're showing a cart before payment, not a record of a completed transaction.
- **data-table** — when you need a list of many invoices to browse, not the full detail of one.

### Rules
- **Do:** Keep the total in a table footer, visually distinct from line items (its own row, right-aligned). **Don't:** Let the total blend in as just another line item — it needs to read as the sum, not another charge.

### Accessibility
- The line-item table uses real `<TableHeader>`/`<TableFooter>` semantics, so the total is programmatically distinguishable from a line item, not just visually styled differently.
- The paid/unpaid status is a ToneBadge with visible text ("Paid") on the moss tint, not a color-only indicator.

### Design tokens
`--card` · `--border` · `--muted-foreground` · `--moss` · `--moss-deep`

registry/agent-rules/skills/quill-components/reference/item.md
# item (component)

A general-purpose list row — compose ItemMedia (icon or avatar), ItemContent (title + description), and ItemActions (trailing button) into a consistent row for lists, search results, and settings.

### When to use
- You need a repeatable list row with an optional leading icon or avatar, a title and description, and a trailing action — a course row, a search result, a settings line.

### Reach for instead
- **card** — when the content is a standalone bounded surface rather than one row in a list — reach for Card instead of a single Item.
- **table** — when the content is genuinely tabular (aligned columns to scan down) rather than a list of self-contained rows.

### Rules
- **Do:** Compose Item from its slots — ItemMedia for the leading icon/avatar, ItemContent for title + description, ItemActions for the trailing control — as every story here does. **Don't:** Drop an icon, text, and a button as loose siblings inside Item with no slot wrappers — the built-in gap and alignment rules key off those slot classes and stop working.
- **Do:** Wrap a list of Items in ItemGroup — it renders `role="group"` and supplies the vertical gap between rows for every size. **Don't:** Stack bare Item elements in a plain `<div>` — you lose the group semantics and have to hand-roll the vertical gap.
- **Do:** When the whole row navigates somewhere, pass `render={<a href=... />}` to Item (as the AsLink story does) so the row is one real link. **Don't:** Nest an `<a>` inside Item's content instead of using `render` — only part of the row becomes clickable and focusable, not the whole row.
- **Do:** Add ItemSeparator between Items when the rows need a visible dividing line, not just the vertical gap ItemGroup already provides. **Don't:** Hand-roll a border or divider `<div>` between Items — ItemSeparator already renders the border-token line at the right spacing, with real separator semantics.
- **Do:** Pair ItemHeader and ItemFooter with a stacked Item (`className="flex-col items-start"`, as WithHeaderAndFooter does) to build a multi-line row — ItemHeader for the top line (title + badge), ItemFooter for the bottom line (meta text + actions). **Don't:** Drop ItemHeader or ItemFooter into the default Item layout expecting them to sit inline like ItemMedia or ItemActions — both are full-width (`basis-full`) rows meant to occupy their own line.

### Accessibility
- ItemGroup renders `role="group"`, grouping its Items for assistive tech without implying list or listbox semantics.
- Item renders a plain `<div>` by default; pass `render={<a href=... />}` (as the AsLink story does) to make the whole row a real, keyboard-focusable link via Base UI's useRender.
- ItemTitle and ItemDescription are styled `<div>`/`<p>` elements, not headings — add real heading markup inside ItemTitle if the row needs to appear in the page's heading outline.
- ItemSeparator renders `role="separator"` with `aria-orientation="horizontal"` (via the underlying Separator primitive) — a real, announced divider. ItemHeader and ItemFooter, by contrast, are plain layout `<div>`s with no semantic role of their own.

### Design tokens
`--border` · `--muted` · `--ring`

registry/agent-rules/skills/quill-components/reference/kanban.md
# kanban (pattern)

A three-column kanban board with card counts, tagged task cards, and an add-card affordance per column.

### When to use
- You need to manage work across status columns with draggable, tagged task cards.

### Reach for instead
- **data-table** — when status is one column among many sortable fields — a table reads better than columns once you need to sort or filter across many records.
- **onboarding** — when the steps are a single sequential checklist, not parallel work items across independent columns.

### Rules
- **Do:** Keep each column's card-count badge in sync with its actual card list — it's a running total, not a caption. **Don't:** Let the count badge drift out of sync when cards move between columns.

### Accessibility
- This story renders a static layout only — a real implementation needs its own keyboard-operable way to move a card between columns (e.g. a menu action), since drag-and-drop alone excludes keyboard and screen-reader users.
- Each card's assignee avatar shows initials as real text content, not just a colored circle with no accessible name.

### Design tokens
`--muted` · `--border` · `--card` · `--secondary`

registry/agent-rules/skills/quill-components/reference/kbd.md
# kbd (component)

A keyboard key badge — a single label rendered as Kbd, or a compound shortcut via KbdGroup — for hotkeys in tooltips, menus, and shortcut references.

### When to use
- You need to reference a keyboard key or shortcut inline in text, a menu item, or a tooltip (e.g. "Press ⌘K to search").

### Rules
- **Do:** Wrap a compound shortcut's individual keys in KbdGroup (e.g. ⌘ + K) so they render as one tight sequence. **Don't:** Render each key of a combo as a standalone Kbd in a normal-gap row — they read as unrelated hints instead of one shortcut.
- **Do:** Label each Kbd with the actual key glyph or short name your app uses (⌘, Esc, Tab), matching how the OS labels it. **Don't:** Spell out a full instruction inside a Kbd ("press command", "hit escape") — Kbd is sized and styled for a single short key label, not a sentence.

### Accessibility
- Kbd renders a real `<kbd>` element, the semantic tag for keyboard input, so assistive tech and browser text tooling recognize it as a key reference rather than plain text.
- Kbd is `pointer-events-none` and purely typographic — it carries no interactive semantics of its own, so it should only ever label a real control (a button, a shortcut list) rather than act as the control itself.

### Design tokens
`--muted` · `--muted-foreground` · `--radius-sm`

registry/agent-rules/skills/quill-components/reference/label.md
# label (component)

The accessible name for a form control — always associate it via htmlFor/id or co-location.

### When to use
- Every form control needs a Label — even when a placeholder or surrounding text seems to explain it.

### Reach for instead
- **field** — when you're building a full form field — Field wraps Label with FieldDescription/FieldError and the aria wiring between them.

### Rules
- **Do:** Put the disabled-able control (className="peer") before Label in DOM order when you want the label to dim on disable — reverse the visual order with flex-col-reverse if the label needs to read first. **Don't:** Place Label before the control in DOM order and expect peer-disabled to dim it — the CSS sibling selector only looks at siblings that come after the peer, so the label never dims.
- **Do:** Associate Label with its control via htmlFor/id, or wrap the control inside it. **Don't:** Render a Label with no htmlFor and no co-location — screen readers cannot connect it to any control.

### Accessibility
- A native `<label>`; htmlFor/id association (or wrapping the control) gives it the accessible-name link and native click-to-focus behavior for free.
- Dimming (peer-disabled / group-data-[disabled=true]) is presentation only — it doesn't itself announce "disabled" to assistive tech; that comes from the control's own disabled attribute.
- Renders whatever children you pass — there is no built-in required-field marker; add that content yourself if the field is required.

### Design tokens
`--ink-soft`

registry/agent-rules/skills/quill-components/reference/list-detail.md
# list-detail (pattern)

An inbox-style split view — a selectable message list beside a detail pane with archive and delete actions.

### When to use
- You need an inbox-style split view pairing a selectable list with a detail pane.

### Reach for instead
- **mail-shell** — when you need a fuller mail client — search, reply composer, forward — not just list-and-read.
- **chat** — when the conversation is a live back-and-forth thread, not discrete messages you select and read one at a time.

### Rules
- **Do:** Keep the selected thread visually marked in the list (background fill) while its detail is open, so context isn't lost. **Don't:** Let the list's selection state go stale or unmarked once a thread is open in the detail pane.

### Accessibility
- Each thread in the list is a real, focusable `<button>`, not a clickable `<div>` with no keyboard access.
- Archive and Delete are icon-only buttons with explicit aria-labels, not bare icons with no accessible name.

### Design tokens
`--border` · `--accent` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/login.md
# login (pattern)

The standard email-and-password sign-in card, with remember-me and a forgot-password escape hatch.

### When to use
- You need the standard email-and-password sign-in with remember-me and reset links.

### Reach for instead
- **login-split-panel** — when the sign-in is a full page, not an embedded card, and you want a branded testimonial panel alongside it.
- **login-minimal** — when you want the leanest possible sign-in — a single email field for a one-time code, no password at all.
- **login-oauth** — when most of your users sign in through Google, GitHub, or Apple rather than a password you store.
- **forgot-password** — when the user has already clicked "Forgot your password?" — that's a separate card, not a state of this one.

### Rules
- **Do:** Treat "Remember me" as extending session length, never as the only thing standing between a device and the account. **Don't:** Rely on the remember-me checkbox as a security boundary.
- **Do:** Style "Forgot your password?" as a link beneath "Sign in" — one unmistakable primary action per card. **Don't:** Give "Forgot your password?" the same solid button treatment as "Sign in" — now two buttons compete to be primary.

### Accessibility
- Email and password inputs are each labeled via `<Label htmlFor>`, not placeholder text alone.
- The remember-me checkbox has its own `<Label>`, so its accessible name doesn't depend on proximity.

### Design tokens
`--card` · `--input` · `--primary` · `--ring` · `--radius-xl`

registry/agent-rules/skills/quill-components/reference/login-minimal.md
# login-minimal (pattern)

The bare-minimum sign-in — no card chrome, a single email field, sign-in by one-time code.

### When to use
- You need the leanest possible sign-in — one email field that sends a one-time code, no password.

### Reach for instead
- **login** — when you're collecting a password, not sending a one-time code — this pattern has no password field at all.
- **login-oauth** — when you want a provider-first flow instead of email-only.
- **otp-verification** — when the user already submitted their email and you're now showing the six-digit code entry — that's a separate step from this one.

### Rules
- **Do:** Reserve the bare, uncontained layout for cases where the surrounding page or modal already frames it. **Don't:** Drop this pattern directly onto a busy page background with nothing to separate it visually.

### Accessibility
- The heading ("Sign in to Quill") is a real `<h1>`, giving the page or dialog an accessible name even though there's no visible header chrome.
- The brand mark ("Q") is decorative; it carries no accessible name and isn't announced.

### Design tokens
`--primary` · `--primary-foreground` · `--input` · `--foreground` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/login-oauth.md
# login-oauth (pattern)

Provider-first sign-in — Google, GitHub, and Apple buttons above an email fallback.

### When to use
- Identity comes from a provider (Google, GitHub, Apple) rather than a password you store.

### Reach for instead
- **signup-social** — when this is account creation, not sign-in — signup-social also leads with OAuth providers.
- **login** — when you store first-party passwords and don't want a provider-first order at all.
- **login-minimal** — when you don't have OAuth providers configured and just need a single email field.

### Rules
- **Do:** Treat the provider buttons as the primary path; keep the email form below the rule as a fallback, not the headline. **Don't:** Give the email field equal visual weight to the provider buttons when most users have a provider account.
- **Do:** Keep provider marks as their official inlined artwork, with GitHub and Apple taking currentColor so they invert correctly in Dusk. **Don't:** Restyle a provider mark to match your palette — that breaks the brand guidelines you agree to by using their sign-in.

### Accessibility
- Provider marks are decorative (aria-hidden); each button's accessible name comes from its visible "Continue with X" text, not the icon.
- The email fallback still uses a real `<Label htmlFor>`, same as every other password/email pattern in this set.

### Design tokens
`--primary` · `--muted-foreground` · `--border` · `--radius`

registry/agent-rules/skills/quill-components/reference/login-split-panel.md
# login-split-panel (pattern)

A full-page, branded sign-in — a testimonial panel beside an email/password form with a magic-link option.

### When to use
- You need a branded full-page sign-in with a testimonial side panel and magic-link option.

### Reach for instead
- **login** — when the sign-in is an embedded card inside an existing page shell, not its own full page.
- **login-minimal** — when you don't have a testimonial or brand story worth the real estate — go bare instead.

### Rules
- **Do:** Let the brand panel drop out on small viewports and let the form take the full width. **Don't:** Force the two-column grid at phone widths — the testimonial panel has no room to be legible.

### Accessibility
- The brand panel's blockquote is decorative marketing copy; the actual auth task lives entirely in the form column, so screen-reader users lose nothing when it collapses on mobile.
- Magic-link is offered as a real secondary action (its own button), not a hidden footnote.

### Design tokens
`--primary` · `--primary-foreground` · `--input` · `--ring` · `--border`

registry/agent-rules/skills/quill-components/reference/mail-shell.md
# mail-shell (pattern)

A two-pane mail client shell — a searchable message list and a reading pane with toolbar and inline reply.

### When to use
- You need a two-pane mail client — searchable list plus a reading pane with toolbar and reply.

### Reach for instead
- **list-detail** — when you need the simpler read-and-select pattern with no search, toolbar, or reply composer.
- **chat** — when the messaging is a live conversational thread, not discrete emails with subjects.

### Rules
- **Do:** Hide the reading pane on narrow viewports so the message list gets the full width instead of squeezing both panes. **Don't:** Force the two-pane layout at phone widths where neither pane has room to be usable.

### Accessibility
- The active message uses aria-current="true" on its list button, not just a background tint, so assistive tech knows which message is open.
- Toolbar actions (Archive, Delete, Reply, Forward) are all icon-only buttons with explicit aria-labels.

### Design tokens
`--border` · `--muted` · `--ring` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/menubar.md
# menubar (component)

A desktop application-style menu strip — a row of always-visible top-level menus, each opening its own dropdown on click.

### When to use
- You need a persistent horizontal row of top-level menus — editor toolbars, desktop-app chrome — not a single button-triggered menu.

### Reach for instead
- **dropdown-menu** — when you only need one menu button, not a horizontal strip of them.
- **navigation-menu** — when the row is public site or marketing navigation rather than desktop application commands.

### Rules
- **Do:** Reach for Menubar only for desktop application-style command strips — editor toolbars, app chrome. **Don't:** Use Menubar for public site navigation — it reads as an unfamiliar desktop-app metaphor for visitors; use NavigationMenu instead.
- **Do:** Group related commands with MenubarSeparator inside each menu, the way File separates New/Open from Save/Export. **Don't:** Run every command together with no separator — a long flat list gives users no visual grouping to scan.

### Accessibility
- The strip itself is a menu bar container; arrow keys move focus horizontally between top-level triggers, matching desktop app menu conventions.
- Each MenubarMenu reuses the DropdownMenu primitive underneath — same role="menu" popup, same roving keyboard focus, same Escape-to-close behavior.
- Escape closes the open menu and returns focus to its trigger; the horizontal strip itself keeps focus rather than losing it.

### Design tokens
`--popover` · `--shadow-md` · `--border`

registry/agent-rules/skills/quill-components/reference/native-select.md
# native-select (component)

The browser's own `<select>`, restyled to match the theme — the OS-native picker, not a custom-rendered popover.

### When to use
- You need a short, simple option list and want the OS-native picker (mobile scroll wheel, keyboard type-ahead, zero extra JS) instead of a custom-rendered popover.

### Reach for instead
- **select** — when you want the popup fully custom-rendered — consistent look across platforms, grouped labels, separators — instead of the OS-native picker.
- **combobox** — when the list is long or users need to filter by typing — native `<select>` has no search.

### Rules
- **Do:** Add a disabled first option (value="") as the placeholder prompt, e.g. `<NativeSelectOption value="" disabled>Select category…</NativeSelectOption>` — native `<select>` has no placeholder attribute. **Don't:** Default to a real option's value with no placeholder option — the first real option then looks pre-chosen even though the user never picked it.
- **Do:** Build every option list with NativeSelectOption and NativeSelectOptGroup. **Don't:** Render raw `<option>`/`<optgroup>` tags — they skip the bg-[Canvas]/text-[CanvasText] theming those subcomponents apply for readable native popovers across themes.

### Accessibility
- Renders a real native `<select>`; keyboard, screen reader, and mobile scroll-wheel behavior all come from the browser for free.
- The chevron icon is aria-hidden; the accessible name comes from a `<label htmlFor>` or aria-label on the `<select>` itself, not from the icon.
- size="sm" only changes height (h-7 vs h-8); tab order and semantics stay identical.

### Design tokens
`--input` · `--ring` · `--destructive` · `--radius-lg` · `--radius-md`

registry/agent-rules/skills/quill-components/reference/navbar.md
# navbar (pattern)

A top navigation bar with brand wordmark, a responsive link list, and sign-in / get-started actions.

### When to use
- You need a top navigation bar with wordmark, links, and sign-in / get-started actions.

### Reach for instead
- **sidebar-nav** — when primary navigation belongs in a persistent side rail for an app, not a top bar for a marketing site.
- **command-palette** — when navigation is keyboard-driven and searchable, not a fixed row of links.

### Rules
- **Do:** Keep "Get started" as the one default-variant (primary) button; "Sign in" stays ghost so it doesn't compete. **Don't:** Style both actions as equally prominent primary buttons.

### Accessibility
- The link list is hidden on narrow viewports (hidden md:flex) — an installed version needs its own mobile menu trigger, since this story doesn't render one.
- Nav links are real `<a>` elements, not buttons styled to look like links — they are navigations, and should read as such to assistive tech.

### Design tokens
`--background` · `--border` · `--muted-foreground` · `--accent`

registry/agent-rules/skills/quill-components/reference/navigation-menu.md
# navigation-menu (component)

Top-level site navigation with mega-dropdown panels — for public/marketing navigation, not desktop application chrome.

### When to use
- You need a horizontal row of site-level navigation, some entries opening a dropdown panel of links, others going straight to a page.

### Reach for instead
- **menubar** — when the row represents desktop application commands rather than public site or marketing navigation.

### Rules
- **Do:** Use NavigationMenuTrigger + NavigationMenuContent only for entries that open a dropdown panel of choices. **Don't:** Wrap a single destination link in Trigger + Content just to reuse the chevron affordance — a plain NavigationMenuLink is simpler and correct.
- **Do:** Apply navigationMenuTriggerStyle() to a plain NavigationMenuLink so it matches the visual weight of the dropdown triggers next to it. **Don't:** Leave a plain NavigationMenuLink unstyled next to NavigationMenuTrigger buttons — it reads smaller and less clickable, breaking the row's visual rhythm.

### Accessibility
- NavigationMenuTrigger renders a real button with an aria-hidden chevron that rotates when its panel opens.
- Arrow keys move focus along the top-level row; the open dropdown panel keeps its own Tab order separate from the row behind it.
- Escape closes an open dropdown panel and returns focus to its trigger.

### Design tokens
`--popover` · `--border` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/newsletter.md
# newsletter (pattern)

A centered newsletter-signup section — an email field, subscribe action, and social proof.

### When to use
- You need an email-capture section with social proof to grow a list.

### Reach for instead
- **contact-form** — when you need more than an email — name, topic, a message — this pattern is intentionally a single field.
- **footer** — when the signup is one small element within a full site footer, not its own standalone section.

### Rules
- **Do:** Say plainly what subscribers are signing up for and that they can leave any time ("No spam, no noise — unsubscribe any time."). **Don't:** Ask for an email with no context on frequency or an easy way out.

### Accessibility
- The email field's `<Label>` is visually hidden (sr-only) but still present — the placeholder text alone is never a substitute for it.
- Social proof ("Joined by 4,200 fellow practitioners") is supporting text, not the only cue that the form is trustworthy — it does not replace a clear privacy or spam promise.

### Design tokens
`--card` · `--input` · `--primary` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/notifications.md
# notifications (pattern)

A notifications card listing recent events with icons, timestamps, unread indicators, and a mark-all-read action.

### When to use
- You need a notifications center with unread indicators and a mark-all-read action.

### Reach for instead
- **activity-feed** — when the list is a read-only history with no read/unread state or mark-all action.
- **sonner** — when the message is transient feedback about something that just happened — use a toast, not a persistent notifications list.

### Rules
- **Do:** Pair the unread dot with a real accessible label (role="img" aria-label="Unread"), as this block already does. **Don't:** Ship an unread indicator that's purely decorative with no accessible signal.
- **Do:** Give each notification an icon that matches its content (a person icon for membership events, a check for successful deploys) so the list can be scanned by icon alone. **Don't:** Reuse one generic bell icon for every notification regardless of type, forcing people to read each title just to tell them apart.

### Accessibility
- The unread dot uses role="img" and aria-label="Unread" — the state is announced, not just shown as a colored dot.
- Mark all read is a real, focusable button, not a link styled to look like one.

### Design tokens
`--card` · `--muted` · `--primary` · `--border`

registry/agent-rules/skills/quill-components/reference/onboarding.md
# onboarding (pattern)

A setup checklist card with a progress bar, completed and pending tasks, and start links for remaining steps.

### When to use
- You need a setup checklist that tracks progress and links to remaining steps.

### Reach for instead
- **wizard** — when the steps must happen in a strict sequence in one sitting, not a checklist a user dips in and out of over time.
- **empty-state** — when there's a single action to take, not a multi-step checklist to track.

### Rules
- **Do:** Pair the visual progress bar with a text fraction ("2 of 4 complete") so the state isn't conveyed by bar width alone. **Don't:** Show only a progress bar with no numeric readout of how much is actually done.

### Accessibility
- Completed tasks are marked with both a filled check icon and strikethrough text — two signals, not color or icon alone.
- Each pending task's "Start" link should carry a task-specific accessible name (e.g. "Start: Invite your team") in an installed version — this story renders four identical "Start" labels, which read as indistinguishable to screen-reader users navigating by button list.

### Design tokens
`--primary` · `--primary-foreground` · `--muted` · `--input`

registry/agent-rules/skills/quill-components/reference/order-summary.md
# order-summary (pattern)

A checkout order-summary card — line items, a promo code field, a total, and a pay-now button.

### When to use
- You need a cart-review card with line items, a promo field, a total, and a pay button.

### Reach for instead
- **invoice** — when you're showing a record of a completed, already-paid transaction, not a cart pending payment.
- **checkout** — when you need the full payment form (card fields, billing address) alongside the summary, not just the review.

### Rules
- **Do:** Recompute the total the instant a promo code applies or a line item changes — never leave a stale total on screen after an update. **Don't:** Require a page refresh or separate action to see the corrected total.
- **Do:** Show an applied promo as its own line item with the discount amount, so the line items and the total add up to the same number. **Don't:** Let the total drop after a promo applies with no corresponding line item — buyers can't verify what changed.

### Accessibility
- The total is set apart with a larger font size and its own row, not just bolded inline with the line items.
- Pay now includes both an icon and the visible text "Pay now" — the icon alone would not be an adequate accessible name.

### Design tokens
`--card` · `--input` · `--primary` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/otp-verification.md
# otp-verification (pattern)

A six-digit one-time-passcode entry card for verifying an email, with expiry messaging and a resend action.

### When to use
- You need to verify an email with a six-digit one-time passcode and resend.

### Reach for instead
- **login-minimal** — when you're sending the initial one-time code, not verifying a code the user already received — that's a separate step upstream of this card.
- **forgot-password** — when recovery works by emailed link, not a typed code.

### Rules
- **Do:** State the code's expiry window in the card body ("expires in 10 minutes"), not just in the email. **Don't:** Leave users guessing why a code that worked a moment ago now fails.

### Accessibility
- The OTP field's accessible name comes from a visually-hidden `<Label>` (sr-only), since the six boxed slots have no room for visible text.
- The six slots read to assistive tech as one field via the shared label, not as six separate unlabeled boxes.

### Design tokens
`--input` · `--ring` · `--destructive` · `--foreground`

registry/agent-rules/skills/quill-components/reference/page-header.md
# page-header (pattern)

A page header with a breadcrumb trail, title and description, and duplicate/new action buttons.

### When to use
- You need a screen header with breadcrumbs, title, description, and primary actions.

### Reach for instead
- **sidebar-nav** — when you need the breadcrumb as part of a full app shell's top bar, not a standalone header block.
- **hero** — when this is a marketing page's opening statement, not a utility screen's title bar.

### Rules
- **Do:** Render the current page as BreadcrumbPage (plain text), not a link — you can't navigate to where you already are. **Don't:** Make every breadcrumb crumb, including the current page, a clickable link.

### Accessibility
- The breadcrumb trail uses real `<nav>`/`<ol>` semantics (via the Breadcrumb primitive), not a row of plain text separated by slashes.
- Duplicate and New are two distinct, separately labeled buttons, not one ambiguous combined action.

### Design tokens
`--muted-foreground` · `--primary` · `--border`

registry/agent-rules/skills/quill-components/reference/pagination.md
# pagination (component)

Page navigation for splitting a long list of records into discrete numbered pages, with Previous/Next controls and an ellipsis for skipped ranges.

### When to use
- You have a long list of records split across pages and need numbered links to jump between them, not just scroll further.

### Reach for instead
- **scroll-area** — when the list is short enough to browse in one continuously scrolling region instead of paging.
- **button** — when you only need a single "Load more" action appending more items, not numbered page links.

### Rules
- **Do:** Always render PaginationPrevious and PaginationNext, even on the first or last page — they stay visible and simply lead to the boundary. **Don't:** Omit Previous or Next entirely at the boundaries — the row loses a fixed, predictable place to click.
- **Do:** Use PaginationEllipsis to represent skipped page ranges instead of listing every page number. **Don't:** List every single page number when there are dozens of pages — the control turns into an unreadable, hard-to-scan row.

### Accessibility
- Pagination renders a real `<nav role="navigation" aria-label="pagination">` — the page list is structural navigation, not a bare row of buttons.
- PaginationLink sets `aria-current="page"` when `isActive` is true, and PaginationPrevious/PaginationNext each carry an explicit `aria-label` ("Go to previous/next page") since their visible text hides on small screens.
- PaginationEllipsis is decorative — its icon is `aria-hidden` and it carries a visually-hidden "More pages" label so screen readers get equivalent text.

registry/agent-rules/skills/quill-components/reference/popover.md
# popover (component)

A small, click-triggered overlay anchored to its trigger — for inline forms, filters, and contextual editing, not a blocking task.

### When to use
- You need a compact, interactive surface anchored to a button — an inline form, a filter, a quick edit — that opens on an explicit click.

### Reach for instead
- **dialog** — when the content is a focused, blocking task rather than a light contextual overlay anchored to a trigger.
- **hover-card** — when you want it to open on hover instead of a click, for glanceable preview content.
- **tooltip** — when you only need a one-line hint, not an interactive popup with its own content.

### Rules
- **Do:** Open Popover on click — it's built for a deliberate, explicit interaction. **Don't:** Wire Popover to open on hover to mimic HoverCard — Popover has no built-in hover trigger; use HoverCard, or Popover's own openOnHover prop if you need an accessible hover-triggered popup.
- **Do:** Give the popover a PopoverTitle so its content stays connected to what triggered it ("Rename workspace"). **Don't:** Drop straight into form fields with no PopoverTitle — the popup has no heading tying it back to the trigger that opened it.

### Accessibility
- Focus moves into the popover on open; Escape or an outside click closes it and returns focus to the trigger.
- Popover has no built-in close button — dismissal relies on Escape or outside click, so don't nest content so deep that outside click becomes hard to reach.
- PopoverContent is fixed at w-72 (288px) — keep content short; a multi-section form belongs in Dialog or Sheet instead.

### Design tokens
`--popover` · `--shadow-md` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/pricing.md
# pricing (pattern)

A three-tier pricing grid with a highlighted popular plan, feature checklists, and per-plan calls to action.

### When to use
- You need a tiered pricing grid with a highlighted plan and feature checklists.

### Reach for instead
- **feature-section** — when you're describing capabilities in general, not comparing specific paid tiers against each other.
- **order-summary** — when the user has already chosen a plan and is now reviewing a specific purchase, not comparing options.

### Rules
- **Do:** Mark exactly one plan as featured (ring + Popular badge) to guide the decision. **Don't:** Feature more than one plan, or none — either dilutes the recommendation this pattern exists to make.

### Accessibility
- The "Popular" badge is visible text, not conveyed by the ring border alone — a screen-reader user still learns which plan is recommended.
- Each plan's feature list uses a check icon plus visible text per line, not an icon-only list with no textual confirmation of what's included.

### Design tokens
`--card` · `--ring` · `--primary` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/profile-card.md
# profile-card (pattern)

A compact, centered profile card — avatar initials, role, follower stats, and follow/message actions.

### When to use
- You need a compact user profile with avatar, role, stats, and follow / message actions.

### Reach for instead
- **activity-feed** — when you want to show what someone did, not a static summary of who they are.
- **data-table** — when you're listing many people at once — a table or list reads better than a full card per person.

### Rules
- **Do:** Label every stat ("Followers", "Following") directly beneath its number — a bare number has no meaning on its own. **Don't:** Show raw numbers with no label and expect position alone to convey what they mean.

### Accessibility
- The avatar circle's initials ("AL") are real text content, not a background-image with no fallback.
- Follow and Message are two distinct, separately-labeled buttons — never collapse them into one ambiguous action.

### Design tokens
`--card` · `--muted` · `--primary` · `--secondary`

registry/agent-rules/skills/quill-components/reference/progress.md
# progress (component)

A determinate progress bar for a real, measurable completion value — pass a numeric `value` and pair it with ProgressLabel/ProgressValue for a labeled, live-updating readout.

### When to use
- You have a real, known completion percentage to show (upload progress, a multi-step setup) — not an open-ended wait with no known duration.

### Reach for instead
- **spinner** — when the wait has no known duration or percentage — indeterminate loading needs a spinner, not a fabricated progress value.
- **skeleton** — when content is still loading and you want to preview its eventual shape, not report a numeric completion percentage.

### Rules
- **Do:** Pair Progress with ProgressLabel so its id is auto-wired to the root's aria-labelledby, as every story here does. **Don't:** Render a bare Progress with only a `value` and no ProgressLabel or aria-label — the bar has no accessible name.
- **Do:** Reserve Progress for a real, known completion value. **Don't:** Animate a fake value climbing toward 100 to simulate progress — use Spinner's indeterminate state instead so users aren't misled about a real percentage existing.

### Accessibility
- Progress.Root renders `role="progressbar"` and computes `aria-valuemin`/`aria-valuemax`/`aria-valuenow` from `min`/`max`/`value` automatically — pass `value`, and don't hand-roll these attributes yourself.
- ProgressLabel's `id` auto-registers to the root's `aria-labelledby` — pass an explicit `aria-label` on Progress only when you skip ProgressLabel entirely.
- Omitting `value` (or passing `undefined`) renders indeterminate progress, with `aria-valuetext` set to "indeterminate progress" — for a genuinely indeterminate wait, prefer Spinner, which never implies a percentage exists.

### Design tokens
`--primary` · `--input`

registry/agent-rules/skills/quill-components/reference/prompt-composer.md
# prompt-composer (component)

The box people type to an AI in — grows with the text, sends on Enter, turns Send into Stop while the AI works, with slots for tools, files and a model picker.

### When to use
- People type a request to an AI: an assistant page, a chat thread, a side panel, or an "ask about this" bar.

### Reach for instead
- **ai-home** — when you need the whole AI start page around the composer, not just the box.
- **chat** — when two people are messaging each other and no AI is involved.
- **textarea** — when it is an ordinary form field whose text is saved, not sent to an AI.

### Rules
- **Do:** Let the composer draw its own AI edge: muted at rest, full on focus, sweeping while working. **Don't:** Wrap it in a thick glowing gradient ring or a second border — one thin line marks AI.
- **Do:** Keep Send and Stop solid ink in the same spot. **Don't:** Fill the Send button with the AI gradient — it competes with the edge and breaks the never-on-Send rule.
- **Do:** Use `lg` on an AI home page and `sm` in threads, side panels and inline bars. **Don't:** Put the large composer inside a narrow panel — use `sm`.
- **Do:** When you pass `value`, clear it yourself in `onSubmit` (set it to an empty string). **Don't:** Pass `value` and expect the composer to empty itself after sending — it only clears its own state when uncontrolled.
- **Do:** Show active AI tools (Analyze data, Web search) as chips with the mark, and attached files as neutral file chips. **Don't:** Put the AI mark on attached files — the files are the person's, not the AI's.
- **Do:** Mark AI commands in the / menu with the AI sparkle and leave ordinary app commands unmarked. **Don't:** Mark every command — the mark is how people tell which actions call the AI.
- **Do:** Set `status="working"` while the AI answers and pass `onStop`. **Don't:** Disable the whole composer while the AI works — people should be able to stop it.
- **Do:** Use `variant="notebook"` where the prompt is a piece of writing — a brief, a spec, a long instruction. **Don't:** Use the notebook for quick questions or a help widget — its ruled page reads as "write at length".
- **Do:** Give each mode its own icon (`icon`) — the AI mode uses `ai: true` for the mark — and use `showModes={false}` to hide the tabs where a page has only one mode. **Don't:** Remove the `modes` list just to hide the tabs; that also drops each mode's placeholder.

### Accessibility
- The text box has an accessible name (`label`, default "Message").
- Enter sends, Shift+Enter adds a line. The Enter that confirms a word in a Japanese or Chinese input method never sends or picks a command, in every browser — including Safari, which delivers that Enter just after the word pop-up closes.
- While working, the send button becomes "Stop" and keeps focus position; the edge animation stops under reduced motion.
- Errors are announced with `role="alert"` under the box.
- The `/` and `@` menu: Arrow Up and Down move through the options, Enter picks one, and Escape closes the menu without changing the text. Focus stays in the box; the active option is announced through `aria-activedescendant`.
- Mode tabs (Ask, Agent) are a `tablist` of `tab` buttons named by their label, with the selected one marked `aria-selected`; Tab reaches them and Enter or Space switches mode.
- The notebook word count is visible text, not a live region, so a screen reader is not interrupted after every word.

### Design tokens
`--ai-from` · `--ai-via` · `--ai-to` · `--background` · `--line-control` · `--line-soft` · `--input` · `--ring` · `--muted-foreground` · `--card` · `--primary` · `--primary-foreground` · `--destructive`

registry/agent-rules/skills/quill-components/reference/question-card.md
# question-card (component)

An AI agent asks the person to choose — the question, options as full rows with the recommended one first, a free-text row for anything else, and Continue that waits for an answer.

### When to use
- An AI agent cannot go on without a choice only the person can make, and there are a few sensible answers.

### Reach for instead
- **radio-group** — when an ordinary form asks the question; no AI agent is waiting on it.
- **approval-card** — when the agent already knows what it wants to do and needs a yes or a no.

### Rules
- **Do:** Give every option a title and one line on what choosing it means. **Don't:** Offer a row of bare buttons — people cannot weigh options they have to guess at.
- **Do:** Pass the agent's pick as recommended so it comes first with the moss tag. **Don't:** Tag several options as recommended, or bury the recommendation in the middle of the list.
- **Do:** Keep the free-text row, and offer Skip when the agent can decide for itself. **Don't:** Force a choice between options when none of them may fit.

### Accessibility
- The card is a group named by its title ("The agent has a question"). The options are a radio group named by the question, so the question is read once: one Tab stop, arrow keys move between options and choose.
- Each radio is named by its title (the recommended one adds "Recommended") and described by its one line.
- Choosing the free-text row shows its field, named by the question and that option ("Who should get it?: Someone else") and described by that option's line; the cursor stays on the radio so the arrow keys keep working, and Tab moves into the field. In the field, arrow keys move the caret and never change the choice; Shift+Tab goes back to the radio.
- Continue is disabled until there is an answer. Enter in the field continues.
- The outcome line appears inside a status region that is already on the page, so it is announced; while empty the region is a real box that takes no space.
- When the person presses Continue or Skip, or Enter in the field, the buttons go away and focus moves to the outcome line without scrolling the page, so they keep their place. An outcome that arrives on its own, with nothing in the card pressed, moves focus nowhere; nor is focus pulled back if the person has since moved to another control.
- Long unbroken words wrap inside the card. A chosen answer passed without onAnswerChange locks the rows rather than leaving rows that do nothing.

### Design tokens
`--card` · `--background` · `--border` · `--input` · `--foreground` · `--muted-foreground` · `--ink-soft` · `--primary` · `--moss` · `--moss-deep` · `--ai-from` · `--ai-via` · `--ai-to`

registry/agent-rules/skills/quill-components/reference/radio-group.md
# radio-group (component)

A set of mutually exclusive options rendered as visible radio buttons — pick exactly one from a short, always-visible list.

### When to use
- You need the user to choose exactly one option from a short, always-visible list — not collapsed behind a dropdown trigger.

### Reach for instead
- **checkbox** — when more than one option can be true at once — RadioGroup only ever holds a single value.
- **select** — when the option list is long enough that showing every choice on screen wastes space — collapse it behind a dropdown instead.
- **toggle-group** — when the choices read better as icon buttons (alignment, view mode) than as labeled radio dots.

### Rules
- **Do:** Always pair each RadioGroupItem with a Label via htmlFor/id. **Don't:** Render a bare RadioGroupItem next to unassociated text — screen readers announce no name for the option.
- **Do:** Use RadioGroup only when exactly one of the options can be selected at a time. **Don't:** Reuse RadioGroup to fake a multi-select list — swap to a group of Checkboxes instead.
- **Do:** Pick one form: defaultValue for uncontrolled, or value + onValueChange for controlled. **Don't:** Pass both defaultValue and value — value wins and defaultValue is silently ignored after the first render.

### Accessibility
- The group renders role="radiogroup"; each item renders role="radio" with aria-checked reflecting selection.
- Arrow keys move focus and selection together between items, matching native `<input type="radio">` grouping — Tab enters and exits the whole group as a single stop.
- aria-invalid on an item drives the destructive border/ring, the same token pattern as Checkbox.

### Design tokens
`--primary` · `--input` · `--ring` · `--destructive`

registry/agent-rules/skills/quill-components/reference/resizable.md
# resizable (component)

Drag-resizable split panels — the user controls how space is divided between two or more regions by dragging a handle between them.

### When to use
- You need a layout region whose split the user can adjust themselves by dragging — a file-tree/editor/preview layout, a resizable sidebar-and-content pane.

### Reach for instead
- **scroll-area** — when the pane just needs a fixed size with its own scrollbar, not a user-draggable boundary.
- **tabs** — when you want to switch between views one at a time instead of showing multiple panes side-by-side permanently.

### Rules
- **Do:** Give every ResizablePanel a defaultSize that adds up to 100 across the group, as each story here does (50/50, 60/40, 25/50/25). **Don't:** Leave defaultSize unset or mismatched on some panels — panels can end up unevenly or unpredictably sized on first render.
- **Do:** Pass `withHandle` to ResizableHandle when the drag affordance needs to be visually obvious — it adds a small grip indicator on the divider. **Don't:** Leave the handle as a bare 1px line in a layout where users might not realize the boundary is draggable.

### Accessibility
- ResizableHandle renders as a real focusable `role="separator"` — arrow keys resize the adjoining panels, Home/End jump to the boundary extremes, and Enter collapses/expands, all without a pointer.
- ResizablePanel wraps its children in a `tabIndex={0}` focusable div, since the library's own internal scroll container isn't otherwise keyboard-reachable — this keeps the scrollable region satisfying the scrollable-region-focusable a11y rule.

### Design tokens
`--border` · `--ring`

registry/agent-rules/skills/quill-components/reference/scroll-area.md
# scroll-area (component)

A styled custom scrollbar over a fixed-size scrollable region — replaces the browser's native scrollbar chrome without changing scroll behavior.

### When to use
- You need a scrollable region (a list, a card body) with a consistently styled scrollbar instead of each OS/browser's native chrome, inside an explicit height or width you set.

### Reach for instead
- **pagination** — when the list is long enough that paging through discrete chunks reads better than one continuously scrolling region.
- **resizable** — when the region's size itself should be user-adjustable by dragging, not fixed with an internal scrollbar.

### Rules
- **Do:** Give ScrollArea an explicit height and/or width (e.g. `h-64 w-56`), as every story here does — the region needs a fixed size to know when to show a scrollbar. **Don't:** Drop ScrollArea into a layout with no height constraint — content just grows the container instead of ever scrolling.
- **Do:** Add `<ScrollBar orientation="horizontal" />` when the content scrolls sideways — ScrollArea only renders a vertical scrollbar by default. **Don't:** Rely on the default vertical ScrollBar for a horizontally-scrolling row of content — there's no visible scrollbar or affordance for the sideways scroll.

### Accessibility
- The scrollable Viewport carries `focus-visible:ring` and is keyboard-focusable, so keyboard users can Tab to it and scroll with arrow keys even though the custom scrollbar itself isn't a native control.
- ScrollBar's Thumb is presentation only — the real, keyboard- and screen-reader-operable scrolling happens on the Viewport underneath it.

### Design tokens
`--border` · `--ring`

registry/agent-rules/skills/quill-components/reference/search-results.md
# search-results (pattern)

A docs search panel — query input, result count, and a linked result list with breadcrumbs and type badges.

### When to use
- You need a search panel with a query input, result count, and a linked result list.

### Reach for instead
- **command-palette** — when search is a global, keyboard-triggered overlay (⌘K) rather than an inline panel on the page.
- **data-table** — when results are structured records with multiple sortable columns, not a simple linked list.

### Rules
- **Do:** State the result count in visible text (2 results for “token”) so users know the query actually ran. **Don't:** Leave users guessing whether zero results means the search hasn't run yet or genuinely found nothing.

### Accessibility
- The search input has an explicit aria-label ("Search") since its visible placeholder text is not a substitute for a label.
- Each result is a single focusable `<a>` wrapping its full content (title, breadcrumb, badge) — not a div with a separate, hard-to-target link inside.

### Design tokens
`--border` · `--muted` · `--accent`

registry/agent-rules/skills/quill-components/reference/select.md
# select (component)

A custom-rendered dropdown for choosing one option from a short list — the same look on every platform, unlike the OS-native picker.

### When to use
- You need a dropdown of up to roughly 20 options with a consistent, custom-rendered popup (grouped labels, separators) rather than the OS-native picker.

### Reach for instead
- **native-select** — when you'd rather hand the picker UI to the OS (mobile scroll wheel, zero extra JS) and don't need custom-rendered items.
- **combobox** — when the list is long enough that users need to filter it by typing.

### Rules
- **Do:** Always pass a placeholder to `<SelectValue placeholder="…" />`. **Don't:** Leave SelectValue with no placeholder — an empty trigger reads as broken, not merely unselected.
- **Do:** Reach for Select when the option list is short (roughly ≤20) and doesn't need search. **Don't:** Force a long option list into Select with no search — users have to scroll through everything blind.

### Accessibility
- SelectTrigger renders role="combobox"; opening it expands a listbox — arrow keys navigate items, Enter selects, Escape closes.
- SelectValue's placeholder prop supplies the accessible "nothing selected yet" text — don't leave it empty.
- aria-invalid="true" on the trigger applies the destructive border/ring, the same token pattern as Input.

### Design tokens
`--input` · `--ring` · `--popover` · `--accent` · `--destructive` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/separator.md
# separator (component)

A thin decorative divider line between stacked or inline content — horizontal by default, vertical for toolbars and inline groups.

### When to use
- You need a plain visual divider between grouped content — stacked sections, or inline items in a toolbar — with no label or content of its own.

### Rules
- **Do:** Give a vertical Separator an explicit height (e.g. `h-4`), as every vertical story here does — it renders correctly regardless of the parent layout. **Don't:** Rely on `self-stretch` alone by dropping `orientation="vertical"` into a wrapper that isn't a flex container — with no flex ancestor to stretch against and no explicit height class, the separator collapses to zero height and disappears.
- **Do:** Use the default `orientation="horizontal"` between stacked content, and `orientation="vertical"` between inline items in a row. **Don't:** Use a horizontal Separator inside an inline row expecting it to divide items side-by-side — it renders as a full-width line, not a thin vertical rule.

### Accessibility
- Separator renders `role="separator"` via Base UI and is purely decorative — it carries no accessible name and should never be the only thing conveying a grouping; pair it with real heading or section structure for that.

### Design tokens
`--border`

registry/agent-rules/skills/quill-components/reference/settings.md
# settings (pattern)

A profile settings card — name fields, a bio textarea, and an email-notification toggle.

### When to use
- You need a profile settings form with name, bio, and notification preferences.

### Reach for instead
- **contact-form** — when you're sending a one-off message, not editing a persistent profile.
- **wizard** — when setup needs multiple sequential steps, not a single settings page a user returns to and edits freely.

### Rules
- **Do:** Keep Save changes and Cancel as two distinct, clearly differentiated actions (primary vs ghost). **Don't:** Make Cancel look as prominent as Save — the user should never mis-tap away unsaved changes as the default action.

### Accessibility
- The notification toggle's `<Label>` and the Switch share the same id/htmlFor pairing, so clicking the label text also toggles the switch.
- The switch's current state (on/off) is exposed through its own semantics, not conveyed by color alone.

### Design tokens
`--card` · `--input` · `--primary` · `--border`

registry/agent-rules/skills/quill-components/reference/sheet.md
# sheet (component)

A panel that slides in from a screen edge while the page stays visible behind it — the same Base UI dialog underneath Dialog, styled as an edge panel instead of a centered card.

### When to use
- You need secondary context or a task panel sliding in from an edge, with the underlying page still visible — settings, item detail, a share menu.

### Reach for instead
- **drawer** — when you want the same edge-panel layout with drag-to-dismiss on touch — Drawer adds Vaul's swipe gesture.
- **dialog** — when the task is a focused, centered action rather than an edge panel.

### Rules
- **Do:** Pick side by role — right for settings/detail, left for navigation, bottom for mobile actions. **Don't:** Default every Sheet to the same side regardless of content — a settings panel and a nav menu should read as spatially different jobs.
- **Do:** Always render SheetTitle — it is the sheet's accessible name, announced when it opens. **Don't:** Skip SheetTitle because the panel's context looks obvious on screen — screen reader users still need an announced name.
- **Do:** Label the trigger with the destination — "Open Settings" — since the panel's content is hidden until it opens. **Don't:** Use a generic label like "Open" — the user has no idea what's inside until they click.

### Accessibility
- Focus moves into the sheet on open and returns to the trigger on close, the same as Dialog.
- Escape closes the sheet; clicking the backdrop also closes it since Sheet is non-destructive.
- Keep the default close button, or pass showCloseButton={false} only when a footer button (usually SheetClose) offers an explicit way out.

### Design tokens
`--popover` · `--shadow-lg` · `--border`

registry/agent-rules/skills/quill-components/reference/sidebar.md
# sidebar (component)

The persistent side-rail navigation primitive — expands, collapses to icons, or slides in as an off-canvas sheet on mobile. `sidebar-nav` is this primitive already assembled into a full app shell with grouped menus and a breadcrumb header.

### When to use
- You're assembling an app-shell nav rail yourself from SidebarGroup/SidebarMenu parts and need collapse-to-icon or off-canvas mobile behavior — reach for the `sidebar-nav` pattern directly if you just need the finished, pre-composed shell.

### Reach for instead
- **sidebar-nav** — when you need the complete collapsible icon-sidebar app shell — grouped menus, a breadcrumb header, a content area — already assembled, not just the underlying primitive to compose yourself.
- **navbar** — when primary navigation belongs in a top bar for a marketing or simple site, not a persistent side rail.

### Rules
- **Do:** Always wrap Sidebar (and the page content beside it) in SidebarProvider — it holds the open/collapsed state every other Sidebar part reads from. **Don't:** Render Sidebar outside a SidebarProvider — every subcomponent calls useSidebar() internally and throws if no provider is found.
- **Do:** Use SidebarMenuBadge to show a count on a menu item, as the Community item does in these stories — it positions itself correctly whether the sidebar is expanded or collapsed to icons. **Don't:** Append a count directly into the visible label text (e.g. "Community (3)") instead of SidebarMenuBadge — it doesn't adapt to collapsed icon mode and gets cut off along with the rest of the label.

### Accessibility
- SidebarProvider adds a Cmd/Ctrl+B keyboard shortcut that calls the same toggle as SidebarTrigger, giving keyboard users a global way to collapse the rail without tabbing to the trigger button.
- SidebarTrigger is a real button with a visually-hidden "Toggle Sidebar" label (`sr-only` span) — the icon alone carries no accessible name.
- SidebarMenuButton's `tooltip` prop renders through TooltipContent and stays `hidden` unless the sidebar is actually collapsed to icons and not on mobile — it only appears once the visible text label is genuinely gone, becoming the item's accessible label in that state.
- On mobile (detected via `useIsMobile`), Sidebar swaps its entire rendering to a Sheet (an off-canvas dialog), inheriting Sheet's own focus-trap and Escape-to-close behavior instead of the desktop collapse animation.

### Design tokens
`--sidebar` · `--sidebar-foreground` · `--sidebar-accent` · `--sidebar-accent-foreground` · `--sidebar-border` · `--sidebar-ring`

registry/agent-rules/skills/quill-components/reference/sidebar-nav.md
# sidebar-nav (pattern)

A collapsible icon-sidebar app shell with grouped menus, badges, a top bar, and a main area for the page — a card grid by default, or the children you pass in.

### When to use
- You need a collapsible icon-sidebar app shell with grouped menus and a content area.

### Reach for instead
- **dashboard** — when you need a simpler, fixed-width text sidebar without the icon-collapse behavior.
- **mail-shell** — when the main content area is a two-pane message list/reading layout, not a card grid.

### Rules
- **Do:** Group sidebar items under labeled sections (e.g. "Workspace", "Studio"), as this block already does, rather than one long flat list. **Don't:** List every nav item at the same level with no grouping once the count grows past a handful.

### Accessibility
- SidebarTrigger toggles the collapsed state and lives next to a vertical Separator, giving keyboard users an explicit, focusable control instead of relying on a hover-only affordance.
- Each SidebarMenuButton carries a tooltip prop that also serves as its accessible label when the sidebar collapses to icons-only.

### Design tokens
`--sidebar` · `--sidebar-foreground` · `--sidebar-ring` · `--border`

registry/agent-rules/skills/quill-components/reference/signup.md
# signup (pattern)

Standard account-creation card — name, email, password, and a link back to sign-in.

### When to use
- You need standard account creation — name, email, password, and a sign-in link.

### Reach for instead
- **signup-social** — when you want OAuth providers to lead instead of a bare email/password form.
- **login** — when the user already has an account — this card is only for new ones.

### Rules
- **Do:** Use the description line to state what happens next (trial length, no card required) so the form isn't just fields with no context. **Don't:** Leave the card as a bare field list with no framing for why someone would fill it out.

### Accessibility
- All three fields (name, email, password) have their own `<Label htmlFor>`; the password placeholder is a hint, not a substitute for real validation messaging.
- The "Sign in" link at the bottom is a real `<a>`, not a button — it's a navigation, and should read as one to assistive tech.

### Design tokens
`--card` · `--input` · `--primary` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/signup-social.md
# signup-social (pattern)

Signup that leads with GitHub and Google provider buttons, falling back to an email/password form.

### When to use
- You need signup that leads with OAuth providers and falls back to email/password.

### Reach for instead
- **signup** — when you don't have OAuth providers configured, or want email/password as the primary path, not a fallback.
- **login-oauth** — when this is sign-in, not new-account creation — login-oauth is the matching pattern for returning users.

### Rules
- **Do:** Put the provider buttons above the divider and the email/password fields below it, matching login-oauth's provider-first order. **Don't:** Bury the provider buttons below a full email/password form — that defeats the point of offering them.

### Accessibility
- Provider marks (GitHub, Google) are decorative; button accessible names come from the visible "Continue with X" text.
- Email and password fields keep real `<Label htmlFor>` pairs even though they're the fallback path, not the headline.

### Design tokens
`--card` · `--input` · `--primary` · `--border`

registry/agent-rules/skills/quill-components/reference/skeleton.md
# skeleton (component)

An animate-pulse loading placeholder that reserves space and previews the shape of content while it loads.

### When to use
- You need a loading placeholder for content that hasn't arrived yet (a list row, an avatar, a card) and want to reserve its layout space instead of causing a jump when the real content appears.

### Reach for instead
- **empty** — when loading has finished and there is confirmed to be no data — swap to Empty once you know the result, not while still waiting.

### Rules
- **Do:** Size each Skeleton to match the real content it stands in for — a circle sized like the avatar, text lines at the real line-height and width proportions. **Don't:** Use one generic block-shaped Skeleton to cover a whole region — the layout shifts once real content (a circular avatar, several text lines) replaces it.
- **Do:** Compose several Skeletons in the same layout as the real content (avatar + two text lines, as CardSkeleton does) so the loading state previews the eventual shape. **Don't:** Center one large Skeleton in the space with no relation to the content's actual structure — it reads as a generic spinner, not a preview of what's coming.

### Accessibility
- Skeleton is purely decorative — it renders a plain `<div>` with no ARIA role, so wrap the loading region in a container with `aria-busy="true"` (or an `aria-live` region announcing "Loading…") if screen-reader users need to know content is on the way.

### Design tokens
`--muted`

registry/agent-rules/skills/quill-components/reference/slider.md
# slider (component)

A draggable control for picking a numeric value, or range, within a bounded min/max — position matters as much as the exact figure.

### When to use
- You need the user to pick one number, or a two-ended range, between a min and max by dragging rather than typing.

### Reach for instead
- **input** — when the value needs to be exact — an Input with type="number" is easier to type a precise figure into than to drag onto.
- **progress** — when you only need to display a value, not let the user change it — Progress is read-only, Slider is always interactive.

### Rules
- **Do:** Pair Slider with a visible label and current-value readout above it — the control itself renders no text. **Don't:** Drop a bare Slider on the page with nothing telling the user what they are adjusting or what it is currently set to.
- **Do:** Pass defaultValue, or value, as an array — one element for a single handle, two for a range. **Don't:** Pass a bare number instead of an array — Slider always expects array input, even for a single thumb.

### Accessibility
- Each thumb is a native `<input type="range">`, so Arrow keys, Home/End, and Page Up/Down all adjust the value for free.
- Every thumb gets a default aria-label ("Minimum"/"Maximum" for a range, "Value" for a single thumb) — override it with something specific, like "Volume," when the number means more than just a value.
- aria-valuetext and aria-orientation are set automatically so screen readers announce the current position, not just a raw number.

### Design tokens
`--primary` · `--input` · `--ring`

registry/agent-rules/skills/quill-components/reference/sonner.md
# sonner (component)

A toast-notification stack for transient feedback about an action just taken — mount one Toaster at the app root, then trigger toasts imperatively with the `toast()` function from `sonner`.

### When to use
- You need brief, self-dismissing feedback about an action the user just took (saved, deleted, upload failed) that shouldn't block the page or require explicit dismissal.

### Reach for instead
- **alert** — when the message is persistent and tied to a specific section of the page, not a transient notification that disappears on its own.
- **alerts** — when you need several persistent status messages stacked inline, not ephemeral toasts.

### Rules
- **Do:** Mount exactly one Toaster at the app root — Quill's Storybook preview does this globally, so individual stories never render their own. **Don't:** Add a second local Toaster inside a page or story — two toast regions trip axe's landmark-unique rule and can double-render the same toast.
- **Do:** Call the typed variant that matches the outcome (`toast.success`, `toast.error`, `toast.info`, `toast.warning`, `toast.loading`) — each ships its own icon, so meaning reads before the copy does. **Don't:** Call the untyped `toast()` for a message that's really a success or an error — it renders with no icon at all, so the outcome is buried in the text.
- **Do:** Keep toast copy to one short sentence — a status update, not an explanation. **Don't:** Pack a paragraph of detail into a toast — it auto-dismisses before anyone can finish reading it.

### Accessibility
- Toaster wraps its region in `aria-live="polite"` (`aria-relevant="additions text"`) automatically, so new toasts are announced without moving focus — a second Toaster on the page duplicates this live region, which is what trips the landmark-unique check.
- The live region's accessible name defaults to "Notifications" plus the reveal hotkey, overridable via the `containerAriaLabel` prop.
- Individual toasts carry no `role` of their own — the announcement comes entirely from the wrapping live region, so every toast needs real, readable text (not an icon alone) to be announced meaningfully.

### Design tokens
`--popover` · `--popover-foreground` · `--border` · `--radius`

registry/agent-rules/skills/quill-components/reference/spinner.md
# spinner (component)

An indeterminate loading indicator — a spinning icon that inherits its color from `currentColor`, for waits with no known duration or percentage.

### When to use
- You need to show that something is loading but have no measurable percentage to report.

### Reach for instead
- **progress** — when you have a real, known completion percentage to show — Progress communicates more than a spinner can.
- **skeleton** — when you want to preview the shape of content that's loading, rather than show a generic spinning indicator.

### Rules
- **Do:** Pair Spinner with a short visible status next to it (as the Default story does: "Saving changes…") whenever there's room — the spinner alone doesn't say what's loading. **Don't:** Drop a bare Spinner with no adjacent text when there's room to add one — sighted users see motion but not what it means.
- **Do:** Let Spinner inherit color from its text context (`text-primary`, `text-destructive`, or the ambient text color) via `currentColor`. **Don't:** Force Spinner into a fixed off-token color unrelated to its surrounding text — it stops reading as part of that context.

### Accessibility
- Spinner sets `role="status"` and `aria-label="Loading"` by default (from the underlying Icon component) — an ARIA live region assistive tech announces on mount. Pass your own `aria-label` (e.g. "Uploading…") for a more specific announcement.
- Because Spinner already carries an accessible name, don't wrap it in a redundant `aria-hidden` container or a second live region — that suppresses or doubles the announcement.

registry/agent-rules/skills/quill-components/reference/stat-cards.md
# stat-cards (pattern)

A responsive grid of KPI cards, each showing a value with a delta badge versus last month.

### When to use
- You need a row of KPI cards showing values with period-over-period deltas.

### Reach for instead
- **analytics-charts** — when you need to show the trend shape over time, not just the current value and one delta.
- **data-table** — when you have more metrics than fit comfortably in a card grid, or need to compare many rows at once.

### Rules
- **Do:** Match badge tone to whether the delta is actually good news for that metric (e.g. rising churn is bad, so it gets destructive tone even though the number itself looks like growth). **Don't:** Default every rising number to a positive tone — a rising "Open tickets" count is bad news, not good.

### Accessibility
- Each delta includes its sign and unit as visible text ("+12.4%", "−0.3%"), not conveyed by badge color alone.
- The comparison period ("vs last month") is stated as visible text next to every card, not just implied by page context.

### Design tokens
`--card` · `--secondary` · `--destructive` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/stats-band.md
# stats-band (pattern)

A marketing band of headline statistics separated by vertical dividers on a card background.

### When to use
- You need a marketing band of headline statistics separated by dividers.

### Reach for instead
- **stat-cards** — when each number needs its own card with a period-over-period delta — this pattern is a single flat band with no per-item chrome or comparison.
- **feature-section** — when you're describing capabilities, not reporting numbers.

### Rules
- **Do:** Color headline numbers from the accent's TEXT cut (--accent-pigment-text), so they follow the page's data-accent and still clear WCAG contrast on every ground. **Don't:** Use --accent-pigment for text. That is the decorative cut: on a Dawn card it is 2.9:1 for moss (the default accent) and 2.1:1 for gold, both under the 3:1 large-text minimum. It is also what --link and --ring already avoid. And never hardcode a fixed pigment (e.g. --terracotta) — that breaks the moment a consumer sets a different accent.
- **Do:** Pair every headline number with its own visible label directly beneath it (e.g. "Components"), as every stat in this band does. **Don't:** Ship a bare number with no label — a viewer has no way to tell what it counts.

### Accessibility
- Dividers between stats are decorative and hidden on narrow viewports where the layout stacks — the numbers and labels alone carry the meaning.
- Every number is paired with its own visible label directly beneath it ("Components", "Patterns") — a bare number is never left to speak for itself.
- Headline numbers use --accent-pigment-text, not --accent-pigment: the decorative cut fails the 3:1 large-text minimum on light grounds for the moss and gold accents.

### Design tokens
`--card` · `--accent-pigment-text` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/switch.md
# switch (component)

An on/off control for a setting that takes effect immediately — no separate Save step.

### When to use
- You need a boolean setting that applies the moment it is flipped — notifications, dark mode, a feature flag.

### Reach for instead
- **checkbox** — when the boolean is part of a form submitted later, not applied instantly.
- **toggle** — when you need a pressed/unpressed icon button (formatting, filters) rather than a labeled setting.

### Rules
- **Do:** Always pair Switch with a Label for an accessible name — the control itself renders no text. **Don't:** Ship a bare Switch with no Label — screen readers announce only "switch, off," not what it controls.
- **Do:** Use defaultChecked for uncontrolled state, or checked + onCheckedChange for controlled. **Don't:** Pass checked with no onCheckedChange — the switch stops responding to clicks because nothing ever updates the controlling value.

### Accessibility
- Renders role="switch" with aria-checked reflecting on/off.
- A visually-hidden native `<input type="checkbox">` underneath gives Space-to-toggle for free, the same native behavior as Checkbox.
- aria-invalid drives the destructive border/ring, matching the rest of the form-control set.

### Design tokens
`--primary` · `--input` · `--ring` · `--destructive`

registry/agent-rules/skills/quill-components/reference/table.md
# table (component)

A data table for rows of tabular content — headers, cells, an optional footer for totals, and a caption for the accessible description.

### When to use
- You need to display structured, comparable data in rows and columns (a course catalogue, a settings list, a pricing grid) where scanning down a column matters.

### Reach for instead
- **item** — when each row is really a card-like list entry (icon, title, description, actions) rather than aligned tabular data — Item reads better than table cells for that.
- **data-table** — when you need sorting, filtering, pagination, or row selection on top of the base table — DataTable is the composed pattern built on Table.

### Rules
- **Do:** Give every table a TableCaption describing what it holds — "Course catalogue", "Design token colour values". **Don't:** Ship a table with no caption and no other visible heading — screen-reader users land on rows of data with no context for what it means.
- **Do:** Use TableFooter for a totals or summary row — it renders with a top border and `bg-muted/50` automatically, as the "Total students" row does. **Don't:** Append a totals row as a plain TableRow inside TableBody — it reads as one more data row instead of a summary.

### Accessibility
- Table renders a real `<table>` (wrapped in a scrollable `<div>` for horizontal overflow), so native table semantics — row and column headers announced together with each cell — apply for free.
- TableCaption renders a real `<caption>` element, positioned by `caption-bottom` on the table; it is the table's accessible description, read by screen readers alongside the table.
- TableHead cells are real `<th>` elements, so screen readers announce the column name alongside each cell in that column as you navigate.

### Design tokens
`--border` · `--muted` · `--foreground` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/tabs.md
# tabs (component)

A set of triggers that swap which panel is visible in place — content panels share the same position, only one shown at a time.

### When to use
- You need to switch between a small number of related content panels (Overview/Curriculum/Reviews) that occupy the same space, without navigating to a new page.

### Reach for instead
- **accordion** — when the sections should stack and expand in place instead of swapping behind a row of triggers — useful when more than one section might stay visible at once.
- **toggle-group** — when the control only changes a display filter with no distinct panel content per option, not full content sections.

### Rules
- **Do:** Give every TabsTrigger a matching TabsContent with the exact same `value` string, as every story here does, so the active tab always has a panel to show. **Don't:** Let a TabsTrigger's `value` drift out of sync with its TabsContent's `value` (a rename in one place but not the other) — the trigger still renders as active since its own value matches, but the panel area shows nothing because no TabsContent shares that value.
- **Do:** Use the `default` (segmented/pill) variant for a self-contained content switcher, and `line` (underline) for a lighter-weight filter row. **Don't:** Mix both TabsList variants inconsistently for the same kind of switcher across a page — pick one variant per context so tabs read as one predictable pattern.

### Accessibility
- TabsTrigger renders `role="tab"` with `aria-selected` and `aria-controls` pointing at its panel — Base UI wires the full ARIA tabs pattern automatically.
- By default, arrow keys only move focus between tabs (`activateOnFocus` is `false`) — the panel doesn't change until the focused tab is activated with Enter, Space, or a click.
- `orientation="vertical"` switches the roving arrow-key navigation to Up/Down, sets `aria-orientation="vertical"` on TabsList, and moves the active-state indicator bar from the underline at the bottom to a bar on the trailing edge.

### Design tokens
`--muted` · `--foreground` · `--ring` · `--background`

registry/agent-rules/skills/quill-components/reference/tabs-page.md
# tabs-page (pattern)

A tabbed settings page with account, notifications, and security panels.

### When to use
- You need a settings screen split into account, notifications, and security tabs.

### Reach for instead
- **settings** — when everything fits on one page without needing to split into separate tabbed sections.
- **wizard** — when the sections are sequential setup steps completed once, not independent settings a user revisits and edits freely.

### Rules
- **Do:** Keep each TabsTrigger's `value` identical to its TabsContent's `value` when you rename a section (here, `account`/`notifications`), so the tab a user lands on always has a panel to show. **Don't:** Rename a TabsTrigger's `value` (e.g. Notifications → Alerts) without updating its TabsContent to match — the renamed tab still highlights as active since its own value is valid, but the panel underneath renders empty because no content shares that value anymore.

### Accessibility
- TabsTrigger/TabsList follow the standard tab-panel pattern — arrow keys move between tabs, and only the active panel's content is in the accessibility tree.
- Each tab panel is its own Card with its own heading, so switching tabs doesn't just swap unlabeled content underneath a single ambiguous heading.

### Design tokens
`--card` · `--muted` · `--border` · `--primary`

registry/agent-rules/skills/quill-components/reference/team-section.md
# team-section (pattern)

A marketing team grid with avatar cards showing each member's name, role, specialty badge, and email action.

### When to use
- You need a marketing team grid with member cards, roles, and contact actions.

### Reach for instead
- **profile-card** — when you're showing one person in detail (stats, follow/message), not a grid of many team members at a glance.
- **testimonial** — when you're quoting what someone said, not introducing who's on the team.

### Rules
- **Do:** Give every team member their own contact action, labeled with their name (e.g. "Email Ada Lovelace"). **Don't:** Ship one generic "Contact us" action for the whole grid — it defeats the point of a per-person team grid.

### Accessibility
- Each member's email button carries a per-person aria-label, not a shared generic one — confirmed directly in this block's source.
- Avatar initials are real text content (AvatarFallback), so every team member has an identifiable avatar even before any photo loads.

### Design tokens
`--card` · `--muted` · `--secondary`

registry/agent-rules/skills/quill-components/reference/testimonial.md
# testimonial (pattern)

A customer-quote card with a serif pull-quote, avatar initials, and attribution.

### When to use
- You need a customer-quote card with a serif pull-quote and attribution.

### Reach for instead
- **team-section** — when you're introducing people, not quoting what a customer said about the product.
- **stats-band** — when the proof point is a number, not a quote.

### Rules
- **Do:** Pair every quote with a real name, role, and organization — an anonymous quote reads as unverifiable. **Don't:** Ship a testimonial with no attribution, or attribution too small/muted to actually read.

### Accessibility
- The pattern uses real `<figure>`/`<blockquote>`/`<figcaption>` semantics, so the quote and its attribution stay programmatically linked, not just visually adjacent.
- The avatar's initials are real text content, matching the fallback pattern used everywhere else in the catalog (profile-card, team-section, activity-feed).

### Design tokens
`--card` · `--border` · `--muted-foreground`

registry/agent-rules/skills/quill-components/reference/textarea.md
# textarea (component)

A styled native `<textarea>` for multi-line free text — descriptions, bios, comments.

### When to use
- You need multi-line free-form text entry where a single-line Input would truncate or wrap awkwardly.

### Reach for instead
- **input** — when the value is a single line — a name, an email, a search query.
- **input-group** — when you need an addon (a label like "Note", an icon) attached to a multi-line field — InputGroupTextarea handles that composition.

### Rules
- **Do:** Always pair Textarea with a Label. **Don't:** Render a bare Textarea with no associated Label.
- **Do:** Set aria-invalid on Textarea for validation errors — same destructive ring token as Input. **Don't:** Style the error state by hand instead of using aria-invalid.

### Accessibility
- Renders a plain native `<textarea>`; no Label is provided automatically — associate one via htmlFor/id.
- field-sizing-content lets the box grow with typed content past its rows-based starting height, rather than staying fixed and scrolling internally.
- aria-invalid="true" applies the same destructive border/ring treatment as Input.

### Design tokens
`--input` · `--ring` · `--destructive` · `--radius-lg`

registry/agent-rules/skills/quill-components/reference/theme-selector.md
# theme-selector (pattern)

A dropdown picker for the five Quill themes and four pigment accents — sets data-theme/data-accent and persists both to localStorage.

### When to use
- You need to let users switch among Quill's five themes and four accents, persisted to localStorage.

### Reach for instead
- **settings** — when theme choice is one field among many in a broader preferences form, not a standalone quick-switch control.

### Rules
- **Do:** Read the persisted theme/accent from localStorage in an effect, after mount — never during render, or prerendered markup mismatches the client. **Don't:** Read localStorage synchronously during render; that guarantees a hydration mismatch since the server always renders Dawn/moss.
- **Do:** Show a color swatch next to each accent option in the menu, as this picker does, so people see the actual hue before picking it. **Don't:** List accent names as plain text with no swatch, forcing a guess at what "Terracotta" or "Indigo" actually look like.

### Accessibility
- The trigger button's aria-label states the current theme by name (e.g. "Theme: Dusk"), not just an icon with no accessible name.
- Both radio groups (Theme, Accent) use DropdownMenuRadioGroup semantics, so the current selection is announced as a checked radio item, not just a visual highlight.

### Design tokens
`--accent-pigment` · `--text-accent-color` · `--link` · `--border`

registry/agent-rules/skills/quill-components/reference/toggle.md
# toggle (component)

A two-state icon or icon-plus-text button whose pressed state persists after the click — a formatting toggle inside a toolbar.

### When to use
- You need a single button whose on/off (pressed) state sticks after clicking — bold, italic, a filter chip.

### Reach for instead
- **button** — when the click fires a one-time action instead of persisting a pressed state.
- **switch** — when the two-state control represents a labeled setting, not an icon action — Switch pairs with a text Label, Toggle usually sits icon-only in a toolbar.
- **toggle-group** — when you have several related Toggles that should share selection state or sizing — reach for the group instead of several standalone Toggles.

### Rules
- **Do:** Give every icon-only Toggle an explicit aria-label, e.g. "Bold." **Don't:** Ship an icon-only Toggle with no aria-label — there is no visible text to fall back on as the accessible name.
- **Do:** Use defaultPressed for uncontrolled state, or pressed + onPressedChange for controlled. **Don't:** Pass both defaultPressed and pressed — pressed wins and defaultPressed is silently ignored.
- **Do:** Use variant="outline" for a Toggle that stands alone outside a toolbar or ButtonGroup, so it has a visible boundary before it is ever pressed. **Don't:** Leave a standalone Toggle at the default variant — bg-transparent with no border makes it invisible until hover or press.

### Accessibility
- Renders a real `<button>` with aria-pressed reflecting on/off — unlike Checkbox and Switch, both Enter and Space activate it, since it is a native button rather than a checkbox input.
- aria-invalid drives the destructive border/ring, the same token pattern as the rest of the form-control set.
- Icon-only Toggles need an aria-label — there is no visible text to serve as the accessible name.

### Design tokens
`--muted` · `--foreground` · `--input` · `--ring` · `--destructive`

registry/agent-rules/skills/quill-components/reference/toggle-group.md
# toggle-group (component)

A row, or column, of Toggle buttons sharing selection, sizing, and spacing — single-select for exclusive choices, multi-select for independent ones.

### When to use
- You have several related two-state toggles that should behave, size, and style as one group rather than as standalone Toggles.

### Reach for instead
- **toggle** — when you only need one standalone two-state button, not a group sharing selection or sizing.
- **radio-group** — when the choices are described in words rather than icons — RadioGroup labeled dots read better for a list of text options.
- **button-group** — when the buttons fire one-off actions instead of holding a persisted pressed state.

### Rules
- **Do:** Pass multiple for independent options that can all be on at once, like bold, italic, and underline together; omit it for mutually-exclusive choices, like alignment, where picking one clears the rest. **Don't:** Omit multiple on a formatting toolbar — selecting italic silently deselects bold.
- **Do:** Use spacing={0} for a connected, segmented-button look — it works with both the default and outline variants. **Don't:** Leave the default spacing between icon-only items when you want a single fused control — the gaps break the segmented-control read.

### Accessibility
- Overrides Base UI default role="group" with role="toolbar" so aria-orientation is valid on the container — arrow keys move focus between items.
- Each ToggleGroupItem carries its own aria-pressed, exactly like a standalone Toggle.
- variant and size set on ToggleGroup cascade to every item automatically — do not set them again on individual items.

### Design tokens
`--muted` · `--foreground` · `--input` · `--ring` · `--destructive`

registry/agent-rules/skills/quill-components/reference/tone-badge.md
# tone-badge (component)

Quill's uppercase tag pill for status, tiers, and labels — wraps Badge with the pigment vocabulary (moss/gold/terracotta/indigo/neutral/muted) baked in so tone never drifts per call site.

### When to use
- You need Quill's uppercase tag pill for a status, tier, or label — trusted/current, developing, red flag, dormant, building — and want the tone-to-pigment mapping guaranteed correct and AA-checked.

### Reach for instead
- **badge** — when the label is a generic chip outside Quill's tag-pill vocabulary — use Badge directly instead of forcing it through a tone.

### Rules
- **Do:** Leave `solid` false (the tinted default) for regular status/tier labels — it is the visually quieter option and still hits AA contrast per tone. **Don't:** Set `solid` on every ToneBadge in a view — solid is reserved for the one strong cue (a "current" marker, a live state) and loses its signal once everything is solid.
- **Do:** Render every uppercase tag pill through ToneBadge, even one-off labels. **Don't:** Hand-roll a `rounded-full … uppercase` span with ad hoc tone colors — it drifts from the AA-checked tint/solid pairs and the two-size scale.
- **Do:** Use ToneBadge's own size scale — `md` (20px) or `sm` (16px, the count-pill scale). **Don't:** Shrink a pill by hand-tightening its tracking or padding instead of passing `size="sm"` — type stays `--text-2xs` at 0.1em tracking in both sizes.

### Accessibility
- ToneBadge renders through Badge, so it inherits the same non-interactive `<span>` semantics — no built-in role or keyboard focus.
- Pass `title` for a native tooltip when a tone label is abbreviated or needs a fuller description.
- Every tint/solid pairing is contrast-checked per tone in the component source — e.g. solid gold uses `--gold-text`, not `--gold-deep`, because paper text on `--gold-deep` measures 3.3:1, under AA.

### Design tokens
`--moss` · `--moss-deep` · `--gold` · `--gold-text` · `--terracotta` · `--terracotta-deep` · `--indigo` · `--indigo-deep` · `--paper-deep` · `--ink-soft` · `--ink-muted` · `--ink` · `--paper` · `--text-2xs`

registry/agent-rules/skills/quill-components/reference/tooltip.md
# tooltip (component)

A short hint that appears on hover or focus — a supplementary visual label for sighted mouse and keyboard users, not a reliable way to deliver essential information.

### When to use
- You need a one-line supplementary hint on a control that already has its own visible label or accessible name.

### Reach for instead
- **popover** — when the content is interactive or essential to understanding the control — Popover (with its openOnHover prop, if hover is still desired) reaches touch and screen-reader users; Tooltip does not.
- **hover-card** — when the hover target is a link and the content is a richer preview (avatar, bio) rather than a one-line hint.

### Rules
- **Do:** Wrap your tree in one TooltipProvider near the root, the way the Storybook decorator does for every story on this page. **Don't:** Instantiate a separate TooltipProvider around every individual Tooltip — the shared delay/closeDelay grouping only works when sibling tooltips share one provider.
- **Do:** Use Tooltip to add a hint alongside a control that already has a visible label or its own accessible name. **Don't:** Put essential meaning only in the tooltip on an unlabeled icon button — touch and screen-reader users never see it.

### Accessibility
- Tooltips are visual-only: not accessible to touch or screen-reader users. The trigger needs its own accessible name (visible text or aria-label) — the tooltip is never a substitute for one.
- If the content is important to understanding the control, don't hide it behind a tooltip — use inline text, or Popover if space is limited.
- This library's TooltipProvider defaults delay to 0ms (near-instant open) rather than Base UI's default 600ms — pass delay explicitly to add hover-intent time back.

### Design tokens
`--foreground` · `--background` · `--radius-md`

registry/agent-rules/skills/quill-components/reference/usage-meter.md
# usage-meter (component)

Shows how much AI credit is left — the number, a bar in the AI gradient, when it renews, what the rest will buy, and a breakdown — with a compact ring for a composer footer.

### When to use
- People spend a limited allowance on AI (credits, messages, tokens) and should see what is left before it runs out.

### Reach for instead
- **progress** — when the bar shows how far a task has got, not how much of an allowance is left.
- **stat-cards** — when the number is a business metric on a dashboard, not an allowance being spent.
- **ai-notice** — when you only need one line of small print under the composer, with no amount.

### Rules
- **Do:** Let a low bar or card say "Running low" in words beside the amount; the bar itself does not change colour. **Don't:** Rely on the short fill alone to say credit is low.
- **Do:** Keep the AI gradient on the fill of the bar and the ring. The gradient is laid across the whole bar and the fill reveals it, so a nearly empty bar is plain gold. Everything else on the card is plain. **Don't:** Put the gradient on the number, the card or the "Get more" button.
- **Do:** Add a plain-language line ("About 150 more long answers") so the number means something. **Don't:** Show a bare count of credits or tokens and leave people to work out what it buys.
- **Do:** Use the compact ring where space is tight: a composer footer, a sidebar. It looks the same whether or not credit is low. **Don't:** Redraw the compact meter as a filled pie wedge, or drop its text and leave the ring alone.

### Accessibility
- The bar is the stock Progress: a progressbar named by the label plus "left" ("AI credits left"), whose value text is the whole sentence ("1,500 of 5,000 credits left"), with ". Running low" added when it is low.
- On the bar and the card, low credit is said in visible words ("Running low", beside the amount), never by the fill alone; the total ("of 5,000") stays on the line.
- On the compact ring the visible line does not change when credit is low, so low is said to screen readers only: "Running low: " is read before the amount.
- The card is a group named by its label. The breakdown is a table with row headers and a caption for screen readers.
- The compact ring is decoration (aria-hidden); the text beside it carries the amount, and screen readers also hear the total, and "Running low: " first when it is low.
- The gradient itself does not animate; the fill only eases to its new width when the amount changes, as the stock Progress does.

### Design tokens
`--card` · `--border` · `--muted` · `--muted-foreground` · `--foreground` · `--destructive` · `--ai-from` · `--ai-text-from` · `--ai-via` · `--ai-to`

registry/agent-rules/skills/quill-components/reference/wizard.md
# wizard (pattern)

A multi-step setup card — a progress stepper, workspace fields, and back/continue navigation.

### When to use
- You need a multi-step setup flow with a progress stepper and back/continue navigation.

### Reach for instead
- **onboarding** — when the steps are a checklist of tasks to complete over time, not a linear form flow completed in one sitting.
- **settings** — when there's only one step and no sequence to track — this is a single settings page, not a flow.

### Rules
- **Do:** Show every step in the stepper up front (done/current/todo), so users know how much is left. **Don't:** Reveal steps one at a time with no sense of total progress.

### Accessibility
- Each step's state (done/current/todo) is conveyed by icon, position, and text color together, not a single color swatch alone.
- Back and Continue are two distinct, clearly labeled buttons — never a single ambiguous "Next" that also has to mean "Previous" depending on context.

### Design tokens
`--primary` · `--primary-foreground` · `--muted` · `--border`

More from quill

Browse all components in this library →