···11+---
22+name: shadcn
33+description: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset".
44+user-invocable: false
55+allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
66+---
77+88+# shadcn/ui
99+1010+A framework for building ui, components and design systems. Components are added as source code to the user's project via the CLI.
1111+1212+> **IMPORTANT:** Run all CLI commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest` — based on the project's `packageManager`. Examples below use `npx shadcn@latest` but substitute the correct runner for the project.
1313+1414+## Current Project Context
1515+1616+```json
1717+!`npx shadcn@latest info --json`
1818+```
1919+2020+The JSON above contains the project config and installed components. Use `npx shadcn@latest docs <component>` to get documentation and example URLs for any component.
2121+2222+## Principles
2323+2424+1. **Use existing components first.** Use `npx shadcn@latest search` to check registries before writing custom UI. Check community registries too.
2525+2. **Compose, don't reinvent.** Settings page = Tabs + Card + form controls. Dashboard = Sidebar + Card + Chart + Table.
2626+3. **Use built-in variants before custom styles.** `variant="outline"`, `size="sm"`, etc.
2727+4. **Use semantic colors.** `bg-primary`, `text-muted-foreground` — never raw values like `bg-blue-500`.
2828+2929+## Critical Rules
3030+3131+These rules are **always enforced**. Each links to a file with Incorrect/Correct code pairs.
3232+3333+### Styling & Tailwind → [styling.md](./rules/styling.md)
3434+3535+- **`className` for layout, not styling.** Never override component colors or typography.
3636+- **No `space-x-*` or `space-y-*`.** Use `flex` with `gap-*`. For vertical stacks, `flex flex-col gap-*`.
3737+- **Use `size-*` when width and height are equal.** `size-10` not `w-10 h-10`.
3838+- **Use `truncate` shorthand.** Not `overflow-hidden text-ellipsis whitespace-nowrap`.
3939+- **No manual `dark:` color overrides.** Use semantic tokens (`bg-background`, `text-muted-foreground`).
4040+- **Use `cn()` for conditional classes.** Don't write manual template literal ternaries.
4141+- **No manual `z-index` on overlay components.** Dialog, Sheet, Popover, etc. handle their own stacking.
4242+4343+### Forms & Inputs → [forms.md](./rules/forms.md)
4444+4545+- **Forms use `FieldGroup` + `Field`.** Never use raw `div` with `space-y-*` or `grid gap-*` for form layout.
4646+- **`InputGroup` uses `InputGroupInput`/`InputGroupTextarea`.** Never raw `Input`/`Textarea` inside `InputGroup`.
4747+- **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.**
4848+- **Option sets (2–7 choices) use `ToggleGroup`.** Don't loop `Button` with manual active state.
4949+- **`FieldSet` + `FieldLegend` for grouping related checkboxes/radios.** Don't use a `div` with a heading.
5050+- **Field validation uses `data-invalid` + `aria-invalid`.** `data-invalid` on `Field`, `aria-invalid` on the control. For disabled: `data-disabled` on `Field`, `disabled` on the control.
5151+5252+### Component Structure → [composition.md](./rules/composition.md)
5353+5454+- **Items always inside their Group.** `SelectItem` → `SelectGroup`. `DropdownMenuItem` → `DropdownMenuGroup`. `CommandItem` → `CommandGroup`.
5555+- **Use `asChild` (radix) or `render` (base) for custom triggers.** Check `base` field from `npx shadcn@latest info`. → [base-vs-radix.md](./rules/base-vs-radix.md)
5656+- **Dialog, Sheet, and Drawer always need a Title.** `DialogTitle`, `SheetTitle`, `DrawerTitle` required for accessibility. Use `className="sr-only"` if visually hidden.
5757+- **Use full Card composition.** `CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`. Don't dump everything in `CardContent`.
5858+- **Button has no `isPending`/`isLoading`.** Compose with `Spinner` + `data-icon` + `disabled`.
5959+- **`TabsTrigger` must be inside `TabsList`.** Never render triggers directly in `Tabs`.
6060+- **`Avatar` always needs `AvatarFallback`.** For when the image fails to load.
6161+6262+### Use Components, Not Custom Markup → [composition.md](./rules/composition.md)
6363+6464+- **Use existing components before custom markup.** Check if a component exists before writing a styled `div`.
6565+- **Callouts use `Alert`.** Don't build custom styled divs.
6666+- **Empty states use `Empty`.** Don't build custom empty state markup.
6767+- **Toast via `sonner`.** Use `toast()` from `sonner`.
6868+- **Use `Separator`** instead of `<hr>` or `<div className="border-t">`.
6969+- **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs.
7070+- **Use `Badge`** instead of custom styled spans.
7171+7272+### Icons → [icons.md](./rules/icons.md)
7373+7474+- **Icons in `Button` use `data-icon`.** `data-icon="inline-start"` or `data-icon="inline-end"` on the icon.
7575+- **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`.
7676+- **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup.
7777+7878+### CLI
7979+8080+- **Never decode preset codes or build preset URLs manually.** Use `npx shadcn@latest preset decode <code>`, `preset url <code>`, or `preset open <code>`. For project-aware preset detection, use `npx shadcn@latest preset resolve`.
8181+- **Apply preset codes directly with the CLI.** Use `npx shadcn@latest apply <code>` for existing projects, or `npx shadcn@latest init --preset <code>` when initializing.
8282+8383+## Key Patterns
8484+8585+These are the most common patterns that differentiate correct shadcn/ui code. For edge cases, see the linked rule files above.
8686+8787+```tsx
8888+// Form layout: FieldGroup + Field, not div + Label.
8989+<FieldGroup>
9090+ <Field>
9191+ <FieldLabel htmlFor="email">Email</FieldLabel>
9292+ <Input id="email" />
9393+ </Field>
9494+</FieldGroup>
9595+9696+// Validation: data-invalid on Field, aria-invalid on the control.
9797+<Field data-invalid>
9898+ <FieldLabel>Email</FieldLabel>
9999+ <Input aria-invalid />
100100+ <FieldDescription>Invalid email.</FieldDescription>
101101+</Field>
102102+103103+// Icons in buttons: data-icon, no sizing classes.
104104+<Button>
105105+ <SearchIcon data-icon="inline-start" />
106106+ Search
107107+</Button>
108108+109109+// Spacing: gap-*, not space-y-*.
110110+<div className="flex flex-col gap-4"> // correct
111111+<div className="space-y-4"> // wrong
112112+113113+// Equal dimensions: size-*, not w-* h-*.
114114+<Avatar className="size-10"> // correct
115115+<Avatar className="w-10 h-10"> // wrong
116116+117117+// Status colors: Badge variants or semantic tokens, not raw colors.
118118+<Badge variant="secondary">+20.1%</Badge> // correct
119119+<span className="text-emerald-600">+20.1%</span> // wrong
120120+```
121121+122122+## Component Selection
123123+124124+| Need | Use |
125125+| -------------------------- | --------------------------------------------------------------------------------------------------- |
126126+| Button/action | `Button` with appropriate variant |
127127+| Form inputs | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` |
128128+| Toggle between 2–5 options | `ToggleGroup` + `ToggleGroupItem` |
129129+| Data display | `Table`, `Card`, `Badge`, `Avatar` |
130130+| Navigation | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination` |
131131+| Overlays | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation) |
132132+| Feedback | `sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner` |
133133+| Command palette | `Command` inside `Dialog` |
134134+| Charts | `Chart` (wraps Recharts) |
135135+| Layout | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible` |
136136+| Empty states | `Empty` |
137137+| Menus | `DropdownMenu`, `ContextMenu`, `Menubar` |
138138+| Tooltips/info | `Tooltip`, `HoverCard`, `Popover` |
139139+140140+## Key Fields
141141+142142+The injected project context contains these key fields:
143143+144144+- **`aliases`** → use the actual alias prefix for imports (e.g. `@/`, `~/`), never hardcode.
145145+- **`isRSC`** → when `true`, components using `useState`, `useEffect`, event handlers, or browser APIs need `"use client"` at the top of the file. Always reference this field when advising on the directive.
146146+- **`tailwindVersion`** → `"v4"` uses `@theme inline` blocks; `"v3"` uses `tailwind.config.js`.
147147+- **`tailwindCssFile`** → the global CSS file where custom CSS variables are defined. Always edit this file, never create a new one.
148148+- **`style`** → component visual treatment (e.g. `nova`, `vega`).
149149+- **`base`** → primitive library (`radix` or `base`). Affects component APIs and available props.
150150+- **`iconLibrary`** → determines icon imports. Use `lucide-react` for `lucide`, `@tabler/icons-react` for `tabler`, etc. Never assume `lucide-react`.
151151+- **`resolvedPaths`** → exact file-system destinations for components, utils, hooks, etc.
152152+- **`framework`** → routing and file conventions (e.g. Next.js App Router vs Vite SPA).
153153+- **`packageManager`** → use this for any non-shadcn dependency installs (e.g. `pnpm add date-fns` vs `npm install date-fns`).
154154+- **`preset`** → resolved preset code and values for the current project. Use `npx shadcn@latest preset resolve --json` when you only need preset information.
155155+156156+See [cli.md — `info` command](./cli.md) for the full field reference.
157157+158158+## Component Docs, Examples, and Usage
159159+160160+Run `npx shadcn@latest docs <component>` to get the URLs for a component's documentation, examples, and API reference. Fetch these URLs to get the actual content.
161161+162162+```bash
163163+npx shadcn@latest docs button dialog select
164164+```
165165+166166+**When creating, fixing, debugging, or using a component, always run `npx shadcn@latest docs` and fetch the URLs first.** This ensures you're working with the correct API and usage patterns rather than guessing.
167167+168168+## Workflow
169169+170170+1. **Get project context** — already injected above. Run `npx shadcn@latest info` again if you need to refresh.
171171+2. **Check installed components first** — before running `add`, always check the `components` list from project context or list the `resolvedPaths.ui` directory. Don't import components that haven't been added, and don't re-add ones already installed.
172172+3. **Find components** — `npx shadcn@latest search`.
173173+4. **Get docs and examples** — run `npx shadcn@latest docs <component>` to get URLs, then fetch them. Use `npx shadcn@latest view` to browse registry items you haven't installed. To preview changes to installed components, use `npx shadcn@latest add --diff`.
174174+5. **Install or update** — `npx shadcn@latest add`. When updating existing components, use `--dry-run` and `--diff` to preview changes first (see [Updating Components](#updating-components) below).
175175+6. **Fix imports in third-party components** — After adding components from community registries (e.g. `@bundui`, `@magicui`), check the added non-UI files for hardcoded import paths like `@/components/ui/...`. These won't match the project's actual aliases. Use `npx shadcn@latest info` to get the correct `ui` alias (e.g. `@workspace/ui/components`) and rewrite the imports accordingly. The CLI rewrites imports for its own UI files, but third-party registry components may use default paths that don't match the project.
176176+7. **Review added components** — After adding a component or block from any registry, **always read the added files and verify they are correct**. Check for missing sub-components (e.g. `SelectItem` without `SelectGroup`), missing imports, incorrect composition, or violations of the [Critical Rules](#critical-rules). Also replace any icon imports with the project's `iconLibrary` from the project context (e.g. if the registry item uses `lucide-react` but the project uses `hugeicons`, swap the imports and icon names accordingly). Fix all issues before moving on.
177177+8. **Registry must be explicit** — When the user asks to add a block or component, **do not guess the registry**. If no registry is specified (e.g. user says "add a login block" without specifying `@shadcn`, `@tailark`, etc.), ask which registry to use. Never default to a registry on behalf of the user.
178178+9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**?
179179+ - **Inspect current preset**: `npx shadcn@latest preset resolve`. Use `--json` when you need structured values.
180180+ - **Inspect incoming preset**: `npx shadcn@latest preset decode <code>`. Use `preset url <code>` or `preset open <code>` to share or open the preset builder.
181181+ - **Overwrite**: `npx shadcn@latest apply <code>`. Overwrites detected components, fonts, and CSS variables.
182182+ - **Partial**: `npx shadcn@latest apply <code> --only theme,font`. Updates only the selected preset parts without reinstalling UI components. Supported values are `theme` and `font`; comma-separated combinations are allowed. `icon` is intentionally not supported, because icon changes may require full component reinstall and transforms.
183183+ - **Merge**: `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to list installed components, then for each installed component use `--dry-run` and `--diff` to [smart merge](#updating-components) it individually.
184184+ - **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.
185185+ - **Important**: Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
186186+187187+## Updating Components
188188+189189+When the user asks to update a component from upstream while keeping their local changes, use `--dry-run` and `--diff` to intelligently merge. **NEVER fetch raw files from GitHub manually — always use the CLI.**
190190+191191+1. Run `npx shadcn@latest add <component> --dry-run` to see all files that would be affected.
192192+2. For each file, run `npx shadcn@latest add <component> --diff <file>` to see what changed upstream vs local.
193193+3. Decide per file based on the diff:
194194+ - No local changes → safe to overwrite.
195195+ - Has local changes → read the local file, analyze the diff, and apply upstream updates while preserving local modifications.
196196+ - User says "just update everything" → use `--overwrite`, but confirm first.
197197+4. **Never use `--overwrite` without the user's explicit approval.**
198198+199199+## Quick Reference
200200+201201+```bash
202202+# Create a new project.
203203+npx shadcn@latest init --name my-app --preset base-nova
204204+npx shadcn@latest init --name my-app --preset a2r6bw --template vite
205205+206206+# Create a monorepo project.
207207+npx shadcn@latest init --name my-app --preset base-nova --monorepo
208208+npx shadcn@latest init --name my-app --preset base-nova --template next --monorepo
209209+210210+# Initialize existing project.
211211+npx shadcn@latest init --preset base-nova
212212+npx shadcn@latest init --defaults # shortcut: --template=next --preset=nova (base style implied)
213213+214214+# Apply a preset to an existing project.
215215+npx shadcn@latest apply a2r6bw
216216+npx shadcn@latest apply a2r6bw --only theme
217217+npx shadcn@latest apply a2r6bw --only font
218218+npx shadcn@latest apply a2r6bw --only theme,font
219219+220220+# Inspect preset codes and project preset state.
221221+npx shadcn@latest preset decode a2r6bw
222222+npx shadcn@latest preset url a2r6bw
223223+npx shadcn@latest preset open a2r6bw
224224+npx shadcn@latest preset resolve
225225+npx shadcn@latest preset resolve --json
226226+227227+# Add components.
228228+npx shadcn@latest add button card dialog
229229+npx shadcn@latest add @magicui/shimmer-button
230230+npx shadcn@latest add --all
231231+232232+# Preview changes before adding/updating.
233233+npx shadcn@latest add button --dry-run
234234+npx shadcn@latest add button --diff button.tsx
235235+npx shadcn@latest add @acme/form --view button.tsx
236236+237237+# Search registries.
238238+npx shadcn@latest search @shadcn -q "sidebar"
239239+npx shadcn@latest search @tailark -q "stats"
240240+241241+# Get component docs and example URLs.
242242+npx shadcn@latest docs button dialog select
243243+244244+# View registry item details (for items not yet installed).
245245+npx shadcn@latest view @shadcn/button
246246+```
247247+248248+**Named presets:** `nova`, `vega`, `maia`, `lyra`, `mira`, `luma`
249249+**Templates:** `next`, `vite`, `start`, `react-router`, `astro` (all support `--monorepo`) and `laravel` (not supported for monorepo)
250250+**Preset codes:** Version-prefixed base62 strings (e.g. `a2r6bw` or `b0`), from [ui.shadcn.com](https://ui.shadcn.com).
251251+252252+## Detailed References
253253+254254+- [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states
255255+- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading
256256+- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects
257257+- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index
258258+- [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion
259259+- [cli.md](./cli.md) — Commands, flags, presets, templates
260260+- [customization.md](./customization.md) — Theming, CSS variables, extending components
···11+# shadcn CLI Reference
22+33+Configuration is read from `components.json`.
44+55+> **IMPORTANT:** Always run commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest`. Check `packageManager` from project context to choose the right one. Examples below use `npx shadcn@latest` but substitute the correct runner for the project.
66+77+> **IMPORTANT:** Only use the flags documented below. Do not invent or guess flags — if a flag isn't listed here, it doesn't exist. The CLI auto-detects the package manager from the project's lockfile; there is no `--package-manager` flag.
88+99+## Contents
1010+1111+- Commands: init, apply, add (dry-run, smart merge), search, view, docs, info, build
1212+- Templates: next, vite, start, react-router, astro
1313+- Presets: named, code, URL formats and fields
1414+- Switching presets
1515+1616+---
1717+1818+## Commands
1919+2020+### `init` — Initialize or create a project
2121+2222+```bash
2323+npx shadcn@latest init [components...] [options]
2424+```
2525+2626+Initializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step.
2727+2828+| Flag | Short | Description | Default |
2929+| ----------------------- | ----- | --------------------------------------------------------- | ------- |
3030+| `--template <template>` | `-t` | Template (next, start, vite, next-monorepo, react-router) | — |
3131+| `--preset [name]` | `-p` | Preset configuration (named, code, or URL) | — |
3232+| `--yes` | `-y` | Skip confirmation prompt | `true` |
3333+| `--defaults` | `-d` | Use defaults (`--template=next --preset=base-nova`) | `false` |
3434+| `--force` | `-f` | Force overwrite existing configuration | `false` |
3535+| `--cwd <cwd>` | `-c` | Working directory | current |
3636+| `--name <name>` | `-n` | Name for new project | — |
3737+| `--silent` | `-s` | Mute output | `false` |
3838+| `--rtl` | | Enable RTL support | — |
3939+| `--reinstall` | | Re-install existing UI components | `false` |
4040+| `--monorepo` | | Scaffold a monorepo project | — |
4141+| `--no-monorepo` | | Skip the monorepo prompt | — |
4242+4343+`npx shadcn@latest create` is an alias for `npx shadcn@latest init`.
4444+4545+### `apply` — Apply a preset to an existing project
4646+4747+```bash
4848+npx shadcn@latest apply [preset] [options]
4949+```
5050+5151+Applies a preset to an existing project, overwriting preset-driven config, fonts, CSS variables, and detected UI components.
5252+5353+| Flag | Short | Description | Default |
5454+| ------------------- | ----- | ------------------------------------------ | ------- |
5555+| `--preset <preset>` | — | Preset configuration (named, code, or URL) | — |
5656+| `--yes` | `-y` | Skip confirmation prompt | `false` |
5757+| `--cwd <cwd>` | `-c` | Working directory | current |
5858+| `--silent` | `-s` | Mute output | `false` |
5959+6060+`[preset]` is a shorthand for `--preset <preset>`. If both are provided, they must match.
6161+If no preset is provided, the CLI offers to open the custom preset builder on `ui.shadcn.com/create`.
6262+6363+### `add` — Add components
6464+6565+> **IMPORTANT:** To compare local components against upstream or to preview changes, ALWAYS use `npx shadcn@latest add <component> --dry-run`, `--diff`, or `--view`. NEVER fetch raw files from GitHub or other sources manually. The CLI handles registry resolution, file paths, and CSS diffing automatically.
6666+6767+```bash
6868+npx shadcn@latest add [components...] [options]
6969+```
7070+7171+Accepts component names, registry-prefixed names (`@magicui/shimmer-button`), URLs, or local paths.
7272+7373+| Flag | Short | Description | Default |
7474+| --------------- | ----- | -------------------------------------------------------------------------------------------------------------------- | ------- |
7575+| `--yes` | `-y` | Skip confirmation prompt | `false` |
7676+| `--overwrite` | `-o` | Overwrite existing files | `false` |
7777+| `--cwd <cwd>` | `-c` | Working directory | current |
7878+| `--all` | `-a` | Add all available components | `false` |
7979+| `--path <path>` | `-p` | Target path for the component | — |
8080+| `--silent` | `-s` | Mute output | `false` |
8181+| `--dry-run` | | Preview all changes without writing files | `false` |
8282+| `--diff [path]` | | Show diffs. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | — |
8383+| `--view [path]` | | Show file contents. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | — |
8484+8585+#### Dry-Run Mode
8686+8787+Use `--dry-run` to preview what `add` would do without writing any files. `--diff` and `--view` both imply `--dry-run`.
8888+8989+```bash
9090+# Preview all changes.
9191+npx shadcn@latest add button --dry-run
9292+9393+# Show diffs for all files (top 5).
9494+npx shadcn@latest add button --diff
9595+9696+# Show the diff for a specific file.
9797+npx shadcn@latest add button --diff button.tsx
9898+9999+# Show contents for all files (top 5).
100100+npx shadcn@latest add button --view
101101+102102+# Show the full content of a specific file.
103103+npx shadcn@latest add button --view button.tsx
104104+105105+# Works with URLs too.
106106+npx shadcn@latest add https://api.npoint.io/abc123 --dry-run
107107+108108+# CSS diffs.
109109+npx shadcn@latest add button --diff globals.css
110110+```
111111+112112+**When to use dry-run:**
113113+114114+- When the user asks "what files will this add?" or "what will this change?" — use `--dry-run`.
115115+- Before overwriting existing components — use `--diff` to preview the changes first.
116116+- When the user wants to inspect component source code without installing — use `--view`.
117117+- When checking what CSS changes would be made to `globals.css` — use `--diff globals.css`.
118118+- When the user asks to review or audit third-party registry code before installing — use `--view` to inspect the source.
119119+120120+> **`npx shadcn@latest add --dry-run` vs `npx shadcn@latest view`:** Prefer `npx shadcn@latest add --dry-run/--diff/--view` over `npx shadcn@latest view` when the user wants to preview changes to their project. `npx shadcn@latest view` only shows raw registry metadata. `npx shadcn@latest add --dry-run` shows exactly what would happen in the user's project: resolved file paths, diffs against existing files, and CSS updates. Use `npx shadcn@latest view` only when the user wants to browse registry info without a project context.
121121+122122+#### Smart Merge from Upstream
123123+124124+See [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full workflow.
125125+126126+### `search` — Search registries
127127+128128+```bash
129129+npx shadcn@latest search <registries...> [options]
130130+```
131131+132132+Fuzzy search across registries. Also aliased as `npx shadcn@latest list`. Without `-q`, lists all items.
133133+134134+| Flag | Short | Description | Default |
135135+| ------------------- | ----- | ---------------------- | ------- |
136136+| `--query <query>` | `-q` | Search query | — |
137137+| `--limit <number>` | `-l` | Max items per registry | `100` |
138138+| `--offset <number>` | `-o` | Items to skip | `0` |
139139+| `--cwd <cwd>` | `-c` | Working directory | current |
140140+141141+### `view` — View item details
142142+143143+```bash
144144+npx shadcn@latest view <items...> [options]
145145+```
146146+147147+Displays item info including file contents. Example: `npx shadcn@latest view @shadcn/button`.
148148+149149+### `docs` — Get component documentation URLs
150150+151151+```bash
152152+npx shadcn@latest docs <components...> [options]
153153+```
154154+155155+Outputs resolved URLs for component documentation, examples, and API references. Accepts one or more component names. Fetch the URLs to get the actual content.
156156+157157+Example output for `npx shadcn@latest docs input button`:
158158+159159+```
160160+base radix
161161+162162+input
163163+ docs https://ui.shadcn.com/docs/components/radix/input
164164+ examples https://raw.githubusercontent.com/.../examples/input-example.tsx
165165+166166+button
167167+ docs https://ui.shadcn.com/docs/components/radix/button
168168+ examples https://raw.githubusercontent.com/.../examples/button-example.tsx
169169+```
170170+171171+Some components include an `api` link to the underlying library (e.g. `cmdk` for the command component).
172172+173173+### `diff` — Check for updates
174174+175175+Do not use this command. Use `npx shadcn@latest add --diff` instead.
176176+177177+### `info` — Project information
178178+179179+```bash
180180+npx shadcn@latest info [options]
181181+```
182182+183183+Displays project info and `components.json` configuration. Run this first to discover the project's framework, aliases, Tailwind version, and resolved paths.
184184+185185+| Flag | Short | Description | Default |
186186+| ------------- | ----- | ----------------- | ------- |
187187+| `--cwd <cwd>` | `-c` | Working directory | current |
188188+189189+**Project Info fields:**
190190+191191+| Field | Type | Meaning |
192192+| -------------------- | --------- | ------------------------------------------------------------------ |
193193+| `framework` | `string` | Detected framework (`next`, `vite`, `react-router`, `start`, etc.) |
194194+| `frameworkVersion` | `string` | Framework version (e.g. `15.2.4`) |
195195+| `isSrcDir` | `boolean` | Whether the project uses a `src/` directory |
196196+| `isRSC` | `boolean` | Whether React Server Components are enabled |
197197+| `isTsx` | `boolean` | Whether the project uses TypeScript |
198198+| `tailwindVersion` | `string` | `"v3"` or `"v4"` |
199199+| `tailwindConfigFile` | `string` | Path to the Tailwind config file |
200200+| `tailwindCssFile` | `string` | Path to the global CSS file |
201201+| `aliasPrefix` | `string` | Import alias prefix (e.g. `@`, `~`, `@/`) |
202202+| `packageManager` | `string` | Detected package manager (`npm`, `pnpm`, `yarn`, `bun`) |
203203+204204+**Components.json fields:**
205205+206206+| Field | Type | Meaning |
207207+| -------------------- | --------- | ------------------------------------------------------------------------------------------ |
208208+| `base` | `string` | Primitive library (`radix` or `base`) — determines component APIs and available props |
209209+| `style` | `string` | Visual style (e.g. `nova`, `vega`) |
210210+| `rsc` | `boolean` | RSC flag from config |
211211+| `tsx` | `boolean` | TypeScript flag |
212212+| `tailwind.config` | `string` | Tailwind config path |
213213+| `tailwind.css` | `string` | Global CSS path — this is where custom CSS variables go |
214214+| `iconLibrary` | `string` | Icon library — determines icon import package (e.g. `lucide-react`, `@tabler/icons-react`) |
215215+| `aliases.components` | `string` | Component import alias (e.g. `@/components`) |
216216+| `aliases.utils` | `string` | Utils import alias (e.g. `@/lib/utils`) |
217217+| `aliases.ui` | `string` | UI component alias (e.g. `@/components/ui`) |
218218+| `aliases.lib` | `string` | Lib alias (e.g. `@/lib`) |
219219+| `aliases.hooks` | `string` | Hooks alias (e.g. `@/hooks`) |
220220+| `resolvedPaths` | `object` | Absolute file-system paths for each alias |
221221+| `registries` | `object` | Configured custom registries |
222222+223223+**Links fields:**
224224+225225+The `info` output includes a **Links** section with templated URLs for component docs, source, and examples. For resolved URLs, use `npx shadcn@latest docs <component>` instead.
226226+227227+### `build` — Build a custom registry
228228+229229+```bash
230230+npx shadcn@latest build [registry] [options]
231231+```
232232+233233+Builds `registry.json` into individual JSON files for distribution. Default input: `./registry.json`, default output: `./public/r`.
234234+235235+| Flag | Short | Description | Default |
236236+| ----------------- | ----- | ----------------- | ------------ |
237237+| `--output <path>` | `-o` | Output directory | `./public/r` |
238238+| `--cwd <cwd>` | `-c` | Working directory | current |
239239+240240+---
241241+242242+## Templates
243243+244244+| Value | Framework | Monorepo support |
245245+| -------------- | -------------- | ---------------- |
246246+| `next` | Next.js | Yes |
247247+| `vite` | Vite | Yes |
248248+| `start` | TanStack Start | Yes |
249249+| `react-router` | React Router | Yes |
250250+| `astro` | Astro | Yes |
251251+| `laravel` | Laravel | No |
252252+253253+All templates support monorepo scaffolding via the `--monorepo` flag. When passed, the CLI uses a monorepo-specific template directory (e.g. `next-monorepo`, `vite-monorepo`). When neither `--monorepo` nor `--no-monorepo` is passed, the CLI prompts interactively. Laravel does not support monorepo scaffolding.
254254+255255+---
256256+257257+## Presets
258258+259259+Three ways to specify a preset via `--preset`:
260260+261261+1. **Named:** `--preset nova` or `--preset lyra`
262262+2. **Code:** `--preset a2r6bw` (version-prefixed base62 string, e.g. `a2r6bw` or `b0`)
263263+3. **URL:** `--preset "https://ui.shadcn.com/init?base=radix&style=nova&..."`
264264+265265+> **IMPORTANT:** Never try to decode, fetch, or resolve preset codes manually. Preset codes are opaque — pass them directly to `npx shadcn@latest init --preset <code>` and let the CLI handle resolution.
266266+> Use `npx shadcn@latest apply --preset <code>` when overwriting an existing project's preset.
267267+268268+## Switching Presets
269269+270270+Ask the user first: **overwrite**, **merge**, or **skip** existing components?
271271+272272+- **Overwrite / Re-install** → `npx shadcn@latest apply --preset <code>`. Overwrites all detected component files with the new preset styles. Use when the user hasn't customized components.
273273+- **Merge** → `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to get the list of installed components and use the [smart merge workflow](./SKILL.md#updating-components) to update them one by one, preserving local changes. Use when the user has customized components.
274274+- **Skip** → `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS variables, leaves existing components as-is.
275275+276276+Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
+209
.agents/skills/shadcn/customization.md
···11+# Customization & Theming
22+33+Components reference semantic CSS variable tokens. Change the variables to change every component.
44+55+## Contents
66+77+- How it works (CSS variables → Tailwind utilities → components)
88+- Color variables and OKLCH format
99+- Dark mode setup
1010+- Changing the theme (presets, CSS variables)
1111+- Adding custom colors (Tailwind v3 and v4)
1212+- Border radius
1313+- Customizing components (variants, className, wrappers)
1414+- Checking for updates
1515+1616+---
1717+1818+## How It Works
1919+2020+1. CSS variables defined in `:root` (light) and `.dark` (dark mode).
2121+2. Tailwind maps them to utilities: `bg-primary`, `text-muted-foreground`, etc.
2222+3. Components use these utilities — changing a variable changes all components that reference it.
2323+2424+---
2525+2626+## Color Variables
2727+2828+Every color follows the `name` / `name-foreground` convention. The base variable is for backgrounds, `-foreground` is for text/icons on that background.
2929+3030+| Variable | Purpose |
3131+| -------------------------------------------- | -------------------------------- |
3232+| `--background` / `--foreground` | Page background and default text |
3333+| `--card` / `--card-foreground` | Card surfaces |
3434+| `--primary` / `--primary-foreground` | Primary buttons and actions |
3535+| `--secondary` / `--secondary-foreground` | Secondary actions |
3636+| `--muted` / `--muted-foreground` | Muted/disabled states |
3737+| `--accent` / `--accent-foreground` | Hover and accent states |
3838+| `--destructive` / `--destructive-foreground` | Error and destructive actions |
3939+| `--border` | Default border color |
4040+| `--input` | Form input borders |
4141+| `--ring` | Focus ring color |
4242+| `--chart-1` through `--chart-5` | Chart/data visualization |
4343+| `--sidebar-*` | Sidebar-specific colors |
4444+| `--surface` / `--surface-foreground` | Secondary surface |
4545+4646+Colors use OKLCH: `--primary: oklch(0.205 0 0)` where values are lightness (0–1), chroma (0 = gray), and hue (0–360).
4747+4848+---
4949+5050+## Dark Mode
5151+5252+Class-based toggle via `.dark` on the root element. In Next.js, use `next-themes`:
5353+5454+```tsx
5555+import { ThemeProvider } from "next-themes"
5656+5757+<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
5858+ {children}
5959+</ThemeProvider>
6060+```
6161+6262+---
6363+6464+## Changing the Theme
6565+6666+```bash
6767+# Apply a preset code from ui.shadcn.com.
6868+npx shadcn@latest apply --preset a2r6bw
6969+7070+# Positional shorthand also works.
7171+npx shadcn@latest apply a2r6bw
7272+7373+# Switch to a named preset and overwrite existing components.
7474+npx shadcn@latest apply --preset nova
7575+7676+# Preserve existing components instead.
7777+npx shadcn@latest init --preset nova --force --no-reinstall
7878+7979+# Use a custom theme URL.
8080+npx shadcn@latest apply --preset "https://ui.shadcn.com/init?base=radix&style=nova&theme=blue&..."
8181+```
8282+8383+Or edit CSS variables directly in `globals.css`.
8484+8585+---
8686+8787+## Adding Custom Colors
8888+8989+Add variables to the file at `tailwindCssFile` from `npx shadcn@latest info` (typically `globals.css`). Never create a new CSS file for this.
9090+9191+```css
9292+/* 1. Define in the global CSS file. */
9393+:root {
9494+ --warning: oklch(0.84 0.16 84);
9595+ --warning-foreground: oklch(0.28 0.07 46);
9696+}
9797+.dark {
9898+ --warning: oklch(0.41 0.11 46);
9999+ --warning-foreground: oklch(0.99 0.02 95);
100100+}
101101+```
102102+103103+```css
104104+/* 2a. Register with Tailwind v4 (@theme inline). */
105105+@theme inline {
106106+ --color-warning: var(--warning);
107107+ --color-warning-foreground: var(--warning-foreground);
108108+}
109109+```
110110+111111+When `tailwindVersion` is `"v3"` (check via `npx shadcn@latest info`), register in `tailwind.config.js` instead:
112112+113113+```js
114114+// 2b. Register with Tailwind v3 (tailwind.config.js).
115115+module.exports = {
116116+ theme: {
117117+ extend: {
118118+ colors: {
119119+ warning: "oklch(var(--warning) / <alpha-value>)",
120120+ "warning-foreground":
121121+ "oklch(var(--warning-foreground) / <alpha-value>)",
122122+ },
123123+ },
124124+ },
125125+}
126126+```
127127+128128+```tsx
129129+// 3. Use in components.
130130+<div className="bg-warning text-warning-foreground">Warning</div>
131131+```
132132+133133+---
134134+135135+## Border Radius
136136+137137+`--radius` controls border radius globally. Components derive values from it (`rounded-lg` = `var(--radius)`, `rounded-md` = `calc(var(--radius) - 2px)`).
138138+139139+---
140140+141141+## Customizing Components
142142+143143+See also: [rules/styling.md](./rules/styling.md) for Incorrect/Correct examples.
144144+145145+Prefer these approaches in order:
146146+147147+### 1. Built-in variants
148148+149149+```tsx
150150+<Button variant="outline" size="sm">
151151+ Click
152152+</Button>
153153+```
154154+155155+### 2. Tailwind classes via `className`
156156+157157+```tsx
158158+<Card className="mx-auto max-w-md">...</Card>
159159+```
160160+161161+### 3. Add a new variant
162162+163163+Edit the component source to add a variant via `cva`:
164164+165165+```tsx
166166+// components/ui/button.tsx
167167+warning: "bg-warning text-warning-foreground hover:bg-warning/90",
168168+```
169169+170170+### 4. Wrapper components
171171+172172+Compose shadcn/ui primitives into higher-level components:
173173+174174+```tsx
175175+export function ConfirmDialog({ title, description, onConfirm, children }) {
176176+ return (
177177+ <AlertDialog>
178178+ <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
179179+ <AlertDialogContent>
180180+ <AlertDialogHeader>
181181+ <AlertDialogTitle>{title}</AlertDialogTitle>
182182+ <AlertDialogDescription>{description}</AlertDialogDescription>
183183+ </AlertDialogHeader>
184184+ <AlertDialogFooter>
185185+ <AlertDialogCancel>Cancel</AlertDialogCancel>
186186+ <AlertDialogAction onClick={onConfirm}>Confirm</AlertDialogAction>
187187+ </AlertDialogFooter>
188188+ </AlertDialogContent>
189189+ </AlertDialog>
190190+ )
191191+}
192192+```
193193+194194+---
195195+196196+## Checking for Updates
197197+198198+```bash
199199+npx shadcn@latest add button --diff
200200+```
201201+202202+To preview exactly what would change before updating, use `--dry-run` and `--diff`:
203203+204204+```bash
205205+npx shadcn@latest add button --dry-run # see all affected files
206206+npx shadcn@latest add button --diff button.tsx # see the diff for a specific file
207207+```
208208+209209+See [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full smart merge workflow.
+47
.agents/skills/shadcn/evals/evals.json
···11+{
22+ "skill_name": "shadcn",
33+ "evals": [
44+ {
55+ "id": 1,
66+ "prompt": "I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Create a settings form component with fields for: full name, email address, and notification preferences (email, SMS, push notifications as toggle options). Add validation states for required fields.",
77+ "expected_output": "A React component using FieldGroup, Field, ToggleGroup, data-invalid/aria-invalid validation, gap-* spacing, and semantic colors.",
88+ "files": [],
99+ "expectations": [
1010+ "Uses FieldGroup and Field components for form layout instead of raw div with space-y",
1111+ "Uses Switch for independent on/off notification toggles (not looping Button with manual active state)",
1212+ "Uses data-invalid on Field and aria-invalid on the input control for validation states",
1313+ "Uses gap-* (e.g. gap-4, gap-6) instead of space-y-* or space-x-* for spacing",
1414+ "Uses semantic color tokens (e.g. bg-background, text-muted-foreground, text-destructive) instead of raw colors like bg-red-500",
1515+ "No manual dark: color overrides"
1616+ ]
1717+ },
1818+ {
1919+ "id": 2,
2020+ "prompt": "Create a dialog component for editing a user profile. It should have the user's avatar at the top, input fields for name and bio, and Save/Cancel buttons with appropriate icons. Using shadcn/ui with radix-nova preset and tabler icons.",
2121+ "expected_output": "A React component with DialogTitle, Avatar+AvatarFallback, data-icon on icon buttons, no icon sizing classes, tabler icon imports.",
2222+ "files": [],
2323+ "expectations": [
2424+ "Includes DialogTitle for accessibility (visible or with sr-only class)",
2525+ "Avatar component includes AvatarFallback",
2626+ "Icons on buttons use the data-icon attribute (data-icon=\"inline-start\" or data-icon=\"inline-end\")",
2727+ "No sizing classes on icons inside components (no size-4, w-4, h-4, etc.)",
2828+ "Uses tabler icons (@tabler/icons-react) instead of lucide-react",
2929+ "Uses asChild for custom triggers (radix preset)"
3030+ ]
3131+ },
3232+ {
3333+ "id": 3,
3434+ "prompt": "Create a dashboard component that shows 4 stat cards in a grid. Each card has a title, large number, percentage change badge, and a loading skeleton state. Using shadcn/ui with base-nova preset and lucide icons.",
3535+ "expected_output": "A React component with full Card composition, Skeleton for loading, Badge for changes, semantic colors, gap-* spacing.",
3636+ "files": [],
3737+ "expectations": [
3838+ "Uses full Card composition with CardHeader, CardTitle, CardContent (not dumping everything into CardContent)",
3939+ "Uses Skeleton component for loading placeholders instead of custom animate-pulse divs",
4040+ "Uses Badge component for percentage change instead of custom styled spans",
4141+ "Uses semantic color tokens instead of raw color values like bg-green-500 or text-red-600",
4242+ "Uses gap-* instead of space-y-* or space-x-* for spacing",
4343+ "Uses size-* when width and height are equal instead of separate w-* h-*"
4444+ ]
4545+ }
4646+ ]
4747+}
+94
.agents/skills/shadcn/mcp.md
···11+# shadcn MCP Server
22+33+The CLI includes an MCP server that lets AI assistants search, browse, view, and install components from registries.
44+55+---
66+77+## Setup
88+99+```bash
1010+shadcn mcp # start the MCP server (stdio)
1111+shadcn mcp init # write config for your editor
1212+```
1313+1414+Editor config files:
1515+1616+| Editor | Config file |
1717+|--------|------------|
1818+| Claude Code | `.mcp.json` |
1919+| Cursor | `.cursor/mcp.json` |
2020+| VS Code | `.vscode/mcp.json` |
2121+| OpenCode | `opencode.json` |
2222+| Codex | `~/.codex/config.toml` (manual) |
2323+2424+---
2525+2626+## Tools
2727+2828+> **Tip:** MCP tools handle registry operations (search, view, install). For project configuration (aliases, framework, Tailwind version), use `npx shadcn@latest info` — there is no MCP equivalent.
2929+3030+### `shadcn:get_project_registries`
3131+3232+Returns registry names from `components.json`. Errors if no `components.json` exists.
3333+3434+**Input:** none
3535+3636+### `shadcn:list_items_in_registries`
3737+3838+Lists all items from one or more registries.
3939+4040+**Input:** `registries` (string[]), `limit` (number, optional), `offset` (number, optional)
4141+4242+### `shadcn:search_items_in_registries`
4343+4444+Fuzzy search across registries.
4545+4646+**Input:** `registries` (string[]), `query` (string), `limit` (number, optional), `offset` (number, optional)
4747+4848+### `shadcn:view_items_in_registries`
4949+5050+View item details including full file contents.
5151+5252+**Input:** `items` (string[]) — e.g. `["@shadcn/button", "@shadcn/card"]`
5353+5454+### `shadcn:get_item_examples_from_registries`
5555+5656+Find usage examples and demos with source code.
5757+5858+**Input:** `registries` (string[]), `query` (string) — e.g. `"accordion-demo"`, `"button example"`
5959+6060+### `shadcn:get_add_command_for_items`
6161+6262+Returns the CLI install command.
6363+6464+**Input:** `items` (string[]) — e.g. `["@shadcn/button"]`
6565+6666+### `shadcn:get_audit_checklist`
6767+6868+Returns a checklist for verifying components (imports, deps, lint, TypeScript).
6969+7070+**Input:** none
7171+7272+---
7373+7474+## Configuring Registries
7575+7676+Registries are set in `components.json`. The `@shadcn` registry is always built-in.
7777+7878+```json
7979+{
8080+ "registries": {
8181+ "@acme": "https://acme.com/r/{name}.json",
8282+ "@private": {
8383+ "url": "https://private.com/r/{name}.json",
8484+ "headers": { "Authorization": "Bearer ${MY_TOKEN}" }
8585+ }
8686+ }
8787+}
8888+```
8989+9090+- Names must start with `@`.
9191+- URLs must contain `{name}`.
9292+- `${VAR}` references are resolved from environment variables.
9393+9494+Community registry index: `https://ui.shadcn.com/r/registries.json`
+306
.agents/skills/shadcn/rules/base-vs-radix.md
···11+# Base vs Radix
22+33+API differences between `base` and `radix`. Check the `base` field from `npx shadcn@latest info`.
44+55+## Contents
66+77+- Composition: asChild vs render
88+- Button / trigger as non-button element
99+- Select (items prop, placeholder, positioning, multiple, object values)
1010+- ToggleGroup (type vs multiple)
1111+- Slider (scalar vs array)
1212+- Accordion (type and defaultValue)
1313+1414+---
1515+1616+## Composition: asChild (radix) vs render (base)
1717+1818+Radix uses `asChild` to replace the default element. Base uses `render`. Don't wrap triggers in extra elements.
1919+2020+**Incorrect:**
2121+2222+```tsx
2323+<DialogTrigger>
2424+ <div>
2525+ <Button>Open</Button>
2626+ </div>
2727+</DialogTrigger>
2828+```
2929+3030+**Correct (radix):**
3131+3232+```tsx
3333+<DialogTrigger asChild>
3434+ <Button>Open</Button>
3535+</DialogTrigger>
3636+```
3737+3838+**Correct (base):**
3939+4040+```tsx
4141+<DialogTrigger render={<Button />}>Open</DialogTrigger>
4242+```
4343+4444+This applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`.
4545+4646+---
4747+4848+## Button / trigger as non-button element (base only)
4949+5050+When `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`.
5151+5252+**Incorrect (base):** missing `nativeButton={false}`.
5353+5454+```tsx
5555+<Button render={<a href="/docs" />}>Read the docs</Button>
5656+```
5757+5858+**Correct (base):**
5959+6060+```tsx
6161+<Button render={<a href="/docs" />} nativeButton={false}>
6262+ Read the docs
6363+</Button>
6464+```
6565+6666+**Correct (radix):**
6767+6868+```tsx
6969+<Button asChild>
7070+ <a href="/docs">Read the docs</a>
7171+</Button>
7272+```
7373+7474+Same for triggers whose `render` is not a `Button`:
7575+7676+```tsx
7777+// base.
7878+<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>
7979+ Pick date
8080+</PopoverTrigger>
8181+```
8282+8383+---
8484+8585+## Select
8686+8787+**items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only.
8888+8989+**Incorrect (base):**
9090+9191+```tsx
9292+<Select>
9393+ <SelectTrigger><SelectValue placeholder="Select a fruit" /></SelectTrigger>
9494+</Select>
9595+```
9696+9797+**Correct (base):**
9898+9999+```tsx
100100+const items = [
101101+ { label: "Select a fruit", value: null },
102102+ { label: "Apple", value: "apple" },
103103+ { label: "Banana", value: "banana" },
104104+]
105105+106106+<Select items={items}>
107107+ <SelectTrigger>
108108+ <SelectValue />
109109+ </SelectTrigger>
110110+ <SelectContent>
111111+ <SelectGroup>
112112+ {items.map((item) => (
113113+ <SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>
114114+ ))}
115115+ </SelectGroup>
116116+ </SelectContent>
117117+</Select>
118118+```
119119+120120+**Correct (radix):**
121121+122122+```tsx
123123+<Select>
124124+ <SelectTrigger>
125125+ <SelectValue placeholder="Select a fruit" />
126126+ </SelectTrigger>
127127+ <SelectContent>
128128+ <SelectGroup>
129129+ <SelectItem value="apple">Apple</SelectItem>
130130+ <SelectItem value="banana">Banana</SelectItem>
131131+ </SelectGroup>
132132+ </SelectContent>
133133+</Select>
134134+```
135135+136136+**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder="...">`.
137137+138138+**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.
139139+140140+```tsx
141141+// base.
142142+<SelectContent alignItemWithTrigger={false} side="bottom">
143143+144144+// radix.
145145+<SelectContent position="popper">
146146+```
147147+148148+---
149149+150150+## Select — multiple selection and object values (base only)
151151+152152+Base supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only.
153153+154154+**Correct (base — multiple selection):**
155155+156156+```tsx
157157+<Select items={items} multiple defaultValue={[]}>
158158+ <SelectTrigger>
159159+ <SelectValue>
160160+ {(value: string[]) => value.length === 0 ? "Select fruits" : `${value.length} selected`}
161161+ </SelectValue>
162162+ </SelectTrigger>
163163+ ...
164164+</Select>
165165+```
166166+167167+**Correct (base — object values):**
168168+169169+```tsx
170170+<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>
171171+ <SelectTrigger>
172172+ <SelectValue>{(value) => value.name}</SelectValue>
173173+ </SelectTrigger>
174174+ ...
175175+</Select>
176176+```
177177+178178+---
179179+180180+## ToggleGroup
181181+182182+Base uses a `multiple` boolean prop. Radix uses `type="single"` or `type="multiple"`.
183183+184184+**Incorrect (base):**
185185+186186+```tsx
187187+<ToggleGroup type="single" defaultValue="daily">
188188+ <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
189189+</ToggleGroup>
190190+```
191191+192192+**Correct (base):**
193193+194194+```tsx
195195+// Single (no prop needed), defaultValue is always an array.
196196+<ToggleGroup defaultValue={["daily"]} spacing={2}>
197197+ <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
198198+ <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
199199+</ToggleGroup>
200200+201201+// Multi-selection.
202202+<ToggleGroup multiple>
203203+ <ToggleGroupItem value="bold">Bold</ToggleGroupItem>
204204+ <ToggleGroupItem value="italic">Italic</ToggleGroupItem>
205205+</ToggleGroup>
206206+```
207207+208208+**Correct (radix):**
209209+210210+```tsx
211211+// Single, defaultValue is a string.
212212+<ToggleGroup type="single" defaultValue="daily" spacing={2}>
213213+ <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
214214+ <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
215215+</ToggleGroup>
216216+217217+// Multi-selection.
218218+<ToggleGroup type="multiple">
219219+ <ToggleGroupItem value="bold">Bold</ToggleGroupItem>
220220+ <ToggleGroupItem value="italic">Italic</ToggleGroupItem>
221221+</ToggleGroup>
222222+```
223223+224224+**Controlled single value:**
225225+226226+```tsx
227227+// base — wrap/unwrap arrays.
228228+const [value, setValue] = React.useState("normal")
229229+<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>
230230+231231+// radix — plain string.
232232+const [value, setValue] = React.useState("normal")
233233+<ToggleGroup type="single" value={value} onValueChange={setValue}>
234234+```
235235+236236+---
237237+238238+## Slider
239239+240240+Base accepts a plain number for a single thumb. Radix always requires an array.
241241+242242+**Incorrect (base):**
243243+244244+```tsx
245245+<Slider defaultValue={[50]} max={100} step={1} />
246246+```
247247+248248+**Correct (base):**
249249+250250+```tsx
251251+<Slider defaultValue={50} max={100} step={1} />
252252+```
253253+254254+**Correct (radix):**
255255+256256+```tsx
257257+<Slider defaultValue={[50]} max={100} step={1} />
258258+```
259259+260260+Both use arrays for range sliders. Controlled `onValueChange` in base may need a cast:
261261+262262+```tsx
263263+// base.
264264+const [value, setValue] = React.useState([0.3, 0.7])
265265+<Slider value={value} onValueChange={(v) => setValue(v as number[])} />
266266+267267+// radix.
268268+const [value, setValue] = React.useState([0.3, 0.7])
269269+<Slider value={value} onValueChange={setValue} />
270270+```
271271+272272+---
273273+274274+## Accordion
275275+276276+Radix requires `type="single"` or `type="multiple"` and supports `collapsible`. `defaultValue` is a string. Base uses no `type` prop, uses `multiple` boolean, and `defaultValue` is always an array.
277277+278278+**Incorrect (base):**
279279+280280+```tsx
281281+<Accordion type="single" collapsible defaultValue="item-1">
282282+ <AccordionItem value="item-1">...</AccordionItem>
283283+</Accordion>
284284+```
285285+286286+**Correct (base):**
287287+288288+```tsx
289289+<Accordion defaultValue={["item-1"]}>
290290+ <AccordionItem value="item-1">...</AccordionItem>
291291+</Accordion>
292292+293293+// Multi-select.
294294+<Accordion multiple defaultValue={["item-1", "item-2"]}>
295295+ <AccordionItem value="item-1">...</AccordionItem>
296296+ <AccordionItem value="item-2">...</AccordionItem>
297297+</Accordion>
298298+```
299299+300300+**Correct (radix):**
301301+302302+```tsx
303303+<Accordion type="single" collapsible defaultValue="item-1">
304304+ <AccordionItem value="item-1">...</AccordionItem>
305305+</Accordion>
306306+```
+195
.agents/skills/shadcn/rules/composition.md
···11+# Component Composition
22+33+## Contents
44+55+- Items always inside their Group component
66+- Callouts use Alert
77+- Empty states use Empty component
88+- Toast notifications use sonner
99+- Choosing between overlay components
1010+- Dialog, Sheet, and Drawer always need a Title
1111+- Card structure
1212+- Button has no isPending or isLoading prop
1313+- TabsTrigger must be inside TabsList
1414+- Avatar always needs AvatarFallback
1515+- Use Separator instead of raw hr or border divs
1616+- Use Skeleton for loading placeholders
1717+- Use Badge instead of custom styled spans
1818+1919+---
2020+2121+## Items always inside their Group component
2222+2323+Never render items directly inside the content container.
2424+2525+**Incorrect:**
2626+2727+```tsx
2828+<SelectContent>
2929+ <SelectItem value="apple">Apple</SelectItem>
3030+ <SelectItem value="banana">Banana</SelectItem>
3131+</SelectContent>
3232+```
3333+3434+**Correct:**
3535+3636+```tsx
3737+<SelectContent>
3838+ <SelectGroup>
3939+ <SelectItem value="apple">Apple</SelectItem>
4040+ <SelectItem value="banana">Banana</SelectItem>
4141+ </SelectGroup>
4242+</SelectContent>
4343+```
4444+4545+This applies to all group-based components:
4646+4747+| Item | Group |
4848+|------|-------|
4949+| `SelectItem`, `SelectLabel` | `SelectGroup` |
5050+| `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` |
5151+| `MenubarItem` | `MenubarGroup` |
5252+| `ContextMenuItem` | `ContextMenuGroup` |
5353+| `CommandItem` | `CommandGroup` |
5454+5555+---
5656+5757+## Callouts use Alert
5858+5959+```tsx
6060+<Alert>
6161+ <AlertTitle>Warning</AlertTitle>
6262+ <AlertDescription>Something needs attention.</AlertDescription>
6363+</Alert>
6464+```
6565+6666+---
6767+6868+## Empty states use Empty component
6969+7070+```tsx
7171+<Empty>
7272+ <EmptyHeader>
7373+ <EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
7474+ <EmptyTitle>No projects yet</EmptyTitle>
7575+ <EmptyDescription>Get started by creating a new project.</EmptyDescription>
7676+ </EmptyHeader>
7777+ <EmptyContent>
7878+ <Button>Create Project</Button>
7979+ </EmptyContent>
8080+</Empty>
8181+```
8282+8383+---
8484+8585+## Toast notifications use sonner
8686+8787+```tsx
8888+import { toast } from "sonner"
8989+9090+toast.success("Changes saved.")
9191+toast.error("Something went wrong.")
9292+toast("File deleted.", {
9393+ action: { label: "Undo", onClick: () => undoDelete() },
9494+})
9595+```
9696+9797+---
9898+9999+## Choosing between overlay components
100100+101101+| Use case | Component |
102102+|----------|-----------|
103103+| Focused task that requires input | `Dialog` |
104104+| Destructive action confirmation | `AlertDialog` |
105105+| Side panel with details or filters | `Sheet` |
106106+| Mobile-first bottom panel | `Drawer` |
107107+| Quick info on hover | `HoverCard` |
108108+| Small contextual content on click | `Popover` |
109109+110110+---
111111+112112+## Dialog, Sheet, and Drawer always need a Title
113113+114114+`DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className="sr-only"` if visually hidden.
115115+116116+```tsx
117117+<DialogContent>
118118+ <DialogHeader>
119119+ <DialogTitle>Edit Profile</DialogTitle>
120120+ <DialogDescription>Update your profile.</DialogDescription>
121121+ </DialogHeader>
122122+ ...
123123+</DialogContent>
124124+```
125125+126126+---
127127+128128+## Card structure
129129+130130+Use full composition — don't dump everything into `CardContent`:
131131+132132+```tsx
133133+<Card>
134134+ <CardHeader>
135135+ <CardTitle>Team Members</CardTitle>
136136+ <CardDescription>Manage your team.</CardDescription>
137137+ </CardHeader>
138138+ <CardContent>...</CardContent>
139139+ <CardFooter>
140140+ <Button>Invite</Button>
141141+ </CardFooter>
142142+</Card>
143143+```
144144+145145+---
146146+147147+## Button has no isPending or isLoading prop
148148+149149+Compose with `Spinner` + `data-icon` + `disabled`:
150150+151151+```tsx
152152+<Button disabled>
153153+ <Spinner data-icon="inline-start" />
154154+ Saving...
155155+</Button>
156156+```
157157+158158+---
159159+160160+## TabsTrigger must be inside TabsList
161161+162162+Never render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`:
163163+164164+```tsx
165165+<Tabs defaultValue="account">
166166+ <TabsList>
167167+ <TabsTrigger value="account">Account</TabsTrigger>
168168+ <TabsTrigger value="password">Password</TabsTrigger>
169169+ </TabsList>
170170+ <TabsContent value="account">...</TabsContent>
171171+</Tabs>
172172+```
173173+174174+---
175175+176176+## Avatar always needs AvatarFallback
177177+178178+Always include `AvatarFallback` for when the image fails to load:
179179+180180+```tsx
181181+<Avatar>
182182+ <AvatarImage src="/avatar.png" alt="User" />
183183+ <AvatarFallback>JD</AvatarFallback>
184184+</Avatar>
185185+```
186186+187187+---
188188+189189+## Use existing components instead of custom markup
190190+191191+| Instead of | Use |
192192+|---|---|
193193+| `<hr>` or `<div className="border-t">` | `<Separator />` |
194194+| `<div className="animate-pulse">` with styled divs | `<Skeleton className="h-4 w-3/4" />` |
195195+| `<span className="rounded-full bg-green-100 ...">` | `<Badge variant="secondary">` |
+192
.agents/skills/shadcn/rules/forms.md
···11+# Forms & Inputs
22+33+## Contents
44+55+- Forms use FieldGroup + Field
66+- InputGroup requires InputGroupInput/InputGroupTextarea
77+- Buttons inside inputs use InputGroup + InputGroupAddon
88+- Option sets (2–7 choices) use ToggleGroup
99+- FieldSet + FieldLegend for grouping related fields
1010+- Field validation and disabled states
1111+1212+---
1313+1414+## Forms use FieldGroup + Field
1515+1616+Always use `FieldGroup` + `Field` — never raw `div` with `space-y-*`:
1717+1818+```tsx
1919+<FieldGroup>
2020+ <Field>
2121+ <FieldLabel htmlFor="email">Email</FieldLabel>
2222+ <Input id="email" type="email" />
2323+ </Field>
2424+ <Field>
2525+ <FieldLabel htmlFor="password">Password</FieldLabel>
2626+ <Input id="password" type="password" />
2727+ </Field>
2828+</FieldGroup>
2929+```
3030+3131+Use `Field orientation="horizontal"` for settings pages. Use `FieldLabel className="sr-only"` for visually hidden labels.
3232+3333+**Choosing form controls:**
3434+3535+- Simple text input → `Input`
3636+- Dropdown with predefined options → `Select`
3737+- Searchable dropdown → `Combobox`
3838+- Native HTML select (no JS) → `native-select`
3939+- Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms)
4040+- Single choice from few options → `RadioGroup`
4141+- Toggle between 2–5 options → `ToggleGroup` + `ToggleGroupItem`
4242+- OTP/verification code → `InputOTP`
4343+- Multi-line text → `Textarea`
4444+4545+---
4646+4747+## InputGroup requires InputGroupInput/InputGroupTextarea
4848+4949+Never use raw `Input` or `Textarea` inside an `InputGroup`.
5050+5151+**Incorrect:**
5252+5353+```tsx
5454+<InputGroup>
5555+ <Input placeholder="Search..." />
5656+</InputGroup>
5757+```
5858+5959+**Correct:**
6060+6161+```tsx
6262+import { InputGroup, InputGroupInput } from "@/components/ui/input-group"
6363+6464+<InputGroup>
6565+ <InputGroupInput placeholder="Search..." />
6666+</InputGroup>
6767+```
6868+6969+---
7070+7171+## Buttons inside inputs use InputGroup + InputGroupAddon
7272+7373+Never place a `Button` directly inside or adjacent to an `Input` with custom positioning.
7474+7575+**Incorrect:**
7676+7777+```tsx
7878+<div className="relative">
7979+ <Input placeholder="Search..." className="pr-10" />
8080+ <Button className="absolute right-0 top-0" size="icon">
8181+ <SearchIcon />
8282+ </Button>
8383+</div>
8484+```
8585+8686+**Correct:**
8787+8888+```tsx
8989+import { InputGroup, InputGroupInput, InputGroupAddon } from "@/components/ui/input-group"
9090+9191+<InputGroup>
9292+ <InputGroupInput placeholder="Search..." />
9393+ <InputGroupAddon>
9494+ <Button size="icon">
9595+ <SearchIcon data-icon="inline-start" />
9696+ </Button>
9797+ </InputGroupAddon>
9898+</InputGroup>
9999+```
100100+101101+---
102102+103103+## Option sets (2–7 choices) use ToggleGroup
104104+105105+Don't manually loop `Button` components with active state.
106106+107107+**Incorrect:**
108108+109109+```tsx
110110+const [selected, setSelected] = useState("daily")
111111+112112+<div className="flex gap-2">
113113+ {["daily", "weekly", "monthly"].map((option) => (
114114+ <Button
115115+ key={option}
116116+ variant={selected === option ? "default" : "outline"}
117117+ onClick={() => setSelected(option)}
118118+ >
119119+ {option}
120120+ </Button>
121121+ ))}
122122+</div>
123123+```
124124+125125+**Correct:**
126126+127127+```tsx
128128+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
129129+130130+<ToggleGroup spacing={2}>
131131+ <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
132132+ <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
133133+ <ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
134134+</ToggleGroup>
135135+```
136136+137137+Combine with `Field` for labelled toggle groups:
138138+139139+```tsx
140140+<Field orientation="horizontal">
141141+ <FieldTitle id="theme-label">Theme</FieldTitle>
142142+ <ToggleGroup aria-labelledby="theme-label" spacing={2}>
143143+ <ToggleGroupItem value="light">Light</ToggleGroupItem>
144144+ <ToggleGroupItem value="dark">Dark</ToggleGroupItem>
145145+ <ToggleGroupItem value="system">System</ToggleGroupItem>
146146+ </ToggleGroup>
147147+</Field>
148148+```
149149+150150+> **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-vs-radix.md](./base-vs-radix.md#togglegroup).
151151+152152+---
153153+154154+## FieldSet + FieldLegend for grouping related fields
155155+156156+Use `FieldSet` + `FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading:
157157+158158+```tsx
159159+<FieldSet>
160160+ <FieldLegend variant="label">Preferences</FieldLegend>
161161+ <FieldDescription>Select all that apply.</FieldDescription>
162162+ <FieldGroup className="gap-3">
163163+ <Field orientation="horizontal">
164164+ <Checkbox id="dark" />
165165+ <FieldLabel htmlFor="dark" className="font-normal">Dark mode</FieldLabel>
166166+ </Field>
167167+ </FieldGroup>
168168+</FieldSet>
169169+```
170170+171171+---
172172+173173+## Field validation and disabled states
174174+175175+Both attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control.
176176+177177+```tsx
178178+// Invalid.
179179+<Field data-invalid>
180180+ <FieldLabel htmlFor="email">Email</FieldLabel>
181181+ <Input id="email" aria-invalid />
182182+ <FieldDescription>Invalid email address.</FieldDescription>
183183+</Field>
184184+185185+// Disabled.
186186+<Field data-disabled>
187187+ <FieldLabel htmlFor="email">Email</FieldLabel>
188188+ <Input id="email" disabled />
189189+</Field>
190190+```
191191+192192+Works for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.
+101
.agents/skills/shadcn/rules/icons.md
···11+# Icons
22+33+**Always use the project's configured `iconLibrary` for imports.** Check the `iconLibrary` field from project context: `lucide` → `lucide-react`, `tabler` → `@tabler/icons-react`, etc. Never assume `lucide-react`.
44+55+---
66+77+## Icons in Button use data-icon attribute
88+99+Add `data-icon="inline-start"` (prefix) or `data-icon="inline-end"` (suffix) to the icon. No sizing classes on the icon.
1010+1111+**Incorrect:**
1212+1313+```tsx
1414+<Button>
1515+ <SearchIcon className="mr-2 size-4" />
1616+ Search
1717+</Button>
1818+```
1919+2020+**Correct:**
2121+2222+```tsx
2323+<Button>
2424+ <SearchIcon data-icon="inline-start"/>
2525+ Search
2626+</Button>
2727+2828+<Button>
2929+ Next
3030+ <ArrowRightIcon data-icon="inline-end"/>
3131+</Button>
3232+```
3333+3434+---
3535+3636+## No sizing classes on icons inside components
3737+3838+Components handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons inside `Button`, `DropdownMenuItem`, `Alert`, `Sidebar*`, or other shadcn components. Unless the user explicitly asks for custom icon sizes.
3939+4040+**Incorrect:**
4141+4242+```tsx
4343+<Button>
4444+ <SearchIcon className="size-4" data-icon="inline-start" />
4545+ Search
4646+</Button>
4747+4848+<DropdownMenuItem>
4949+ <SettingsIcon className="mr-2 size-4" />
5050+ Settings
5151+</DropdownMenuItem>
5252+```
5353+5454+**Correct:**
5555+5656+```tsx
5757+<Button>
5858+ <SearchIcon data-icon="inline-start" />
5959+ Search
6060+</Button>
6161+6262+<DropdownMenuItem>
6363+ <SettingsIcon />
6464+ Settings
6565+</DropdownMenuItem>
6666+```
6767+6868+---
6969+7070+## Pass icons as component objects, not string keys
7171+7272+Use `icon={CheckIcon}`, not a string key to a lookup map.
7373+7474+**Incorrect:**
7575+7676+```tsx
7777+const iconMap = {
7878+ check: CheckIcon,
7979+ alert: AlertIcon,
8080+}
8181+8282+function StatusBadge({ icon }: { icon: string }) {
8383+ const Icon = iconMap[icon]
8484+ return <Icon />
8585+}
8686+8787+<StatusBadge icon="check" />
8888+```
8989+9090+**Correct:**
9191+9292+```tsx
9393+// Import from the project's configured iconLibrary (e.g. lucide-react, @tabler/icons-react).
9494+import { CheckIcon } from "lucide-react"
9595+9696+function StatusBadge({ icon: Icon }: { icon: React.ComponentType }) {
9797+ return <Icon />
9898+}
9999+100100+<StatusBadge icon={CheckIcon} />
101101+```
+162
.agents/skills/shadcn/rules/styling.md
···11+# Styling & Customization
22+33+See [customization.md](../customization.md) for theming, CSS variables, and adding custom colors.
44+55+## Contents
66+77+- Semantic colors
88+- Built-in variants first
99+- className for layout only
1010+- No space-x-* / space-y-*
1111+- Prefer size-* over w-* h-* when equal
1212+- Prefer truncate shorthand
1313+- No manual dark: color overrides
1414+- Use cn() for conditional classes
1515+- No manual z-index on overlay components
1616+1717+---
1818+1919+## Semantic colors
2020+2121+**Incorrect:**
2222+2323+```tsx
2424+<div className="bg-blue-500 text-white">
2525+ <p className="text-gray-600">Secondary text</p>
2626+</div>
2727+```
2828+2929+**Correct:**
3030+3131+```tsx
3232+<div className="bg-primary text-primary-foreground">
3333+ <p className="text-muted-foreground">Secondary text</p>
3434+</div>
3535+```
3636+3737+---
3838+3939+## No raw color values for status/state indicators
4040+4141+For positive, negative, or status indicators, use Badge variants, semantic tokens like `text-destructive`, or define custom CSS variables — don't reach for raw Tailwind colors.
4242+4343+**Incorrect:**
4444+4545+```tsx
4646+<span className="text-emerald-600">+20.1%</span>
4747+<span className="text-green-500">Active</span>
4848+<span className="text-red-600">-3.2%</span>
4949+```
5050+5151+**Correct:**
5252+5353+```tsx
5454+<Badge variant="secondary">+20.1%</Badge>
5555+<Badge>Active</Badge>
5656+<span className="text-destructive">-3.2%</span>
5757+```
5858+5959+If you need a success/positive color that doesn't exist as a semantic token, use a Badge variant or ask the user about adding a custom CSS variable to the theme (see [customization.md](../customization.md)).
6060+6161+---
6262+6363+## Built-in variants first
6464+6565+**Incorrect:**
6666+6767+```tsx
6868+<Button className="border border-input bg-transparent hover:bg-accent">
6969+ Click me
7070+</Button>
7171+```
7272+7373+**Correct:**
7474+7575+```tsx
7676+<Button variant="outline">Click me</Button>
7777+```
7878+7979+---
8080+8181+## className for layout only
8282+8383+Use `className` for layout (e.g. `max-w-md`, `mx-auto`, `mt-4`), **not** for overriding component colors or typography. To change colors, use semantic tokens, built-in variants, or CSS variables.
8484+8585+**Incorrect:**
8686+8787+```tsx
8888+<Card className="bg-blue-100 text-blue-900 font-bold">
8989+ <CardContent>Dashboard</CardContent>
9090+</Card>
9191+```
9292+9393+**Correct:**
9494+9595+```tsx
9696+<Card className="max-w-md mx-auto">
9797+ <CardContent>Dashboard</CardContent>
9898+</Card>
9999+```
100100+101101+To customize a component's appearance, prefer these approaches in order:
102102+1. **Built-in variants** — `variant="outline"`, `variant="destructive"`, etc.
103103+2. **Semantic color tokens** — `bg-primary`, `text-muted-foreground`.
104104+3. **CSS variables** — define custom colors in the global CSS file (see [customization.md](../customization.md)).
105105+106106+---
107107+108108+## No space-x-* / space-y-*
109109+110110+Use `gap-*` instead. `space-y-4` → `flex flex-col gap-4`. `space-x-2` → `flex gap-2`.
111111+112112+```tsx
113113+<div className="flex flex-col gap-4">
114114+ <Input />
115115+ <Input />
116116+ <Button>Submit</Button>
117117+</div>
118118+```
119119+120120+---
121121+122122+## Prefer size-* over w-* h-* when equal
123123+124124+`size-10` not `w-10 h-10`. Applies to icons, avatars, skeletons, etc.
125125+126126+---
127127+128128+## Prefer truncate shorthand
129129+130130+`truncate` not `overflow-hidden text-ellipsis whitespace-nowrap`.
131131+132132+---
133133+134134+## No manual dark: color overrides
135135+136136+Use semantic tokens — they handle light/dark via CSS variables. `bg-background text-foreground` not `bg-white dark:bg-gray-950`.
137137+138138+---
139139+140140+## Use cn() for conditional classes
141141+142142+Use the `cn()` utility from the project for conditional or merged class names. Don't write manual ternaries in className strings.
143143+144144+**Incorrect:**
145145+146146+```tsx
147147+<div className={`flex items-center ${isActive ? "bg-primary text-primary-foreground" : "bg-muted"}`}>
148148+```
149149+150150+**Correct:**
151151+152152+```tsx
153153+import { cn } from "@/lib/utils"
154154+155155+<div className={cn("flex items-center", isActive ? "bg-primary text-primary-foreground" : "bg-muted")}>
156156+```
157157+158158+---
159159+160160+## No manual z-index on overlay components
161161+162162+`Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`.
···2222}
23232424/**
2525+ * Format a date-time string into a human-readable date and time.
2626+ * Respects user timezone and 12/24-hour format preferences.
2727+ * Falls back to the raw string if parsing fails.
2828+ */
2929+export function formatDateTime(
3030+ dateString: string,
3131+ timezone?: string,
3232+ timeFormat?: "12h" | "24h",
3333+): string {
3434+ if (!dateString) return "Unknown";
3535+ try {
3636+ return new Date(dateString).toLocaleString(
3737+ "en-US",
3838+ withUserLocale(
3939+ {
4040+ month: "short",
4141+ day: "numeric",
4242+ year: "numeric",
4343+ hour: "numeric",
4444+ minute: "2-digit",
4545+ },
4646+ timezone,
4747+ timeFormat,
4848+ ),
4949+ );
5050+ } catch {
5151+ return dateString;
5252+ }
5353+}
5454+5555+/**
2556 * Format a date string into a human-readable date.
2657 * Falls back to the raw string if parsing fails.
2758 */
+47
apps/web/src/lib/hooks/usePublicProfile.ts
···11+import type { PaginatedSocialUsersDto } from "@opnshelf/api";
22+import { client } from "@opnshelf/api";
33+import { useQuery } from "@tanstack/react-query";
44+55+// Custom API functions for public profile endpoints not yet in the generated SDK
66+77+export async function getPublicFollowers(
88+ handle: string,
99+ page = 1,
1010+ pageSize = 20,
1111+): Promise<PaginatedSocialUsersDto> {
1212+ const { data } = await client.get({
1313+ url: `/users/${handle}/followers`,
1414+ query: { page, pageSize },
1515+ });
1616+ return data as PaginatedSocialUsersDto;
1717+}
1818+1919+export async function getPublicFollowing(
2020+ handle: string,
2121+ page = 1,
2222+ pageSize = 20,
2323+): Promise<PaginatedSocialUsersDto> {
2424+ const { data } = await client.get({
2525+ url: `/users/${handle}/following`,
2626+ query: { page, pageSize },
2727+ });
2828+ return data as PaginatedSocialUsersDto;
2929+}
3030+3131+// TanStack Query hooks
3232+3333+export function usePublicFollowers(handle: string, page = 1, pageSize = 20) {
3434+ return useQuery({
3535+ queryKey: ["public-profile", "followers", handle, page, pageSize],
3636+ queryFn: () => getPublicFollowers(handle, page, pageSize),
3737+ enabled: !!handle,
3838+ });
3939+}
4040+4141+export function usePublicFollowing(handle: string, page = 1, pageSize = 20) {
4242+ return useQuery({
4343+ queryKey: ["public-profile", "following", handle, page, pageSize],
4444+ queryFn: () => getPublicFollowing(handle, page, pageSize),
4545+ enabled: !!handle,
4646+ });
4747+}