feat: update skill

This commit is contained in:
shadcn
2026-02-27 21:53:16 +04:00
parent 7d28dfdb15
commit 9954e2b014
4 changed files with 250 additions and 263 deletions

View File

@@ -42,6 +42,7 @@ These rules are **always enforced**. See [patterns.md](./patterns.md) for full e
11. **Toast via `sonner`.** Use `toast()` from `sonner`. Don't build custom toast/notification markup.
12. **Pass icons as objects, not string keys.** Use `icon={CheckIcon}`, not a string key to a lookup map.
13. **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.** Never place a `Button` directly inside or adjacent to an `Input` with custom positioning. Wrap in `InputGroup` and use `InputGroupAddon` for the button.
14. **Never decode or fetch preset codes manually.** Preset codes are opaque. Pass them directly to `shadcn init --preset <code>` and let the CLI handle resolution.
## Key Fields
@@ -79,7 +80,10 @@ shadcn docs button dialog select
5. **Install or update** — MCP `shadcn:get_add_command_for_items` or `shadcn add`. When updating existing components, use `--dry-run` and `--diff` to preview changes first (see [Updating Components](#updating-components) below).
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 `shadcn 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.
7. **Verify** — MCP `shadcn:get_audit_checklist`.
8. **Switching presets**Before running `shadcn init --preset <code>` in an existing project, always ask the user if they'd like to reinstall existing UI components. If yes, use the `--reinstall` flag to overwrite them with the new preset styles.
8. **Switching presets**Ask the user first: **reinstall**, **merge**, or **skip**?
- **Reinstall**: `shadcn init --preset <code> --force --reinstall`. Overwrites all components.
- **Merge**: `shadcn init --preset <code> --force --no-reinstall`, then `shadcn info` to get installed components, then [smart merge](#updating-components) each one.
- **Skip**: `shadcn init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.
If MCP is unavailable, use CLI: `shadcn search`, `shadcn view`, `shadcn add`.
@@ -140,5 +144,6 @@ shadcn view @shadcn/button
- [cli.md](./cli.md) — Commands, flags, presets, templates
- [mcp.md](./mcp.md) — MCP server setup, tools, registry configuration
- [patterns.md](./patterns.md) — UI patterns and component composition rules
- [base-patterns.md](./base-patterns.md) — API differences between base and radix (asChild vs render, Select items, ToggleGroup type, Slider values, Accordion)
- [customization.md](./customization.md) — Theming, CSS variables, extending components
- [registry-authoring.md](./registry-authoring.md) — Building and publishing custom registries

View File

@@ -0,0 +1,230 @@
# Base-specific patterns
API differences between `base` and `radix`. Check the `base` field from `shadcn info` to determine which patterns to use.
## Contents
- Composition: asChild vs render
- Button / trigger as non-button element
- Select
- ToggleGroup
- Slider
- Accordion
---
## Composition: asChild (radix) vs render (base)
Radix uses `asChild` to replace the default element. Base uses `render`.
```tsx
// radix.
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
// base.
<DialogTrigger render={<Button />}>Open</DialogTrigger>
```
This applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`.
---
## Button / trigger as non-button element (base only)
When `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`.
```tsx
// base — button as link.
<Button render={<a href="/docs" />} nativeButton={false}>
Read the docs
</Button>
// radix equivalent.
<Button asChild>
<a href="/docs">Read the docs</a>
</Button>
```
Same for triggers whose `render` is not a `Button`:
```tsx
// base.
<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>
Pick date
</PopoverTrigger>
```
---
## Select
**items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only.
```tsx
// base.
const items = [
{ label: "Select a fruit", value: null },
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
]
<Select items={items}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
// radix.
<Select>
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
```
**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder="...">`.
**Multiple selection and object values (base only).** Base supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only.
```tsx
// base — multiple selection.
<Select items={items} multiple defaultValue={[]}>
<SelectTrigger>
<SelectValue>
{(value: string[]) => value.length === 0 ? "Select fruits" : `${value.length} selected`}
</SelectValue>
</SelectTrigger>
...
</Select>
// base — object values.
<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>
<SelectTrigger>
<SelectValue>{(value) => value.name}</SelectValue>
</SelectTrigger>
...
</Select>
```
**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.
```tsx
// base.
<SelectContent alignItemWithTrigger={false} side="bottom">
// radix.
<SelectContent position="popper">
```
---
## ToggleGroup
Base uses a `multiple` boolean prop. Radix uses `type="single"` or `type="multiple"`.
```tsx
// base — single (no prop needed), defaultValue is always an array.
<ToggleGroup defaultValue={["daily"]} spacing={2}>
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
</ToggleGroup>
// base — multi-selection.
<ToggleGroup multiple>
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
</ToggleGroup>
// radix — single, defaultValue is a string.
<ToggleGroup type="single" defaultValue="daily" spacing={2}>
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
</ToggleGroup>
// radix — multi-selection.
<ToggleGroup type="multiple">
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
</ToggleGroup>
```
**Controlled single value.** Base wraps/unwraps arrays. Radix uses a plain string.
```tsx
// base.
const [value, setValue] = React.useState("normal")
<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>
// radix.
const [value, setValue] = React.useState("normal")
<ToggleGroup type="single" value={value} onValueChange={setValue}>
```
---
## Slider
Base accepts a plain number for a single thumb. Radix always requires an array.
```tsx
// base.
<Slider defaultValue={50} max={100} step={1} />
// radix.
<Slider defaultValue={[50]} max={100} step={1} />
```
Both use arrays for range sliders. Controlled `onValueChange` in base may need a cast.
```tsx
// base.
const [value, setValue] = React.useState([0.3, 0.7])
<Slider value={value} onValueChange={(v) => setValue(v as number[])} />
// radix.
const [value, setValue] = React.useState([0.3, 0.7])
<Slider value={value} onValueChange={setValue} />
```
---
## Accordion
Radix requires `type="single"` or `type="multiple"` and supports `collapsible`. `defaultValue` is a string.
```tsx
// radix.
<Accordion type="single" collapsible defaultValue="item-1">
<AccordionItem value="item-1">...</AccordionItem>
</Accordion>
```
Base uses no `type` prop. Use `multiple` for multi-select. `defaultValue` is an array.
```tsx
// base.
<Accordion defaultValue={["item-1"]}>
<AccordionItem value="item-1">...</AccordionItem>
</Accordion>
// base — multi-select.
<Accordion multiple defaultValue={["item-1", "item-2"]}>
<AccordionItem value="item-1">...</AccordionItem>
<AccordionItem value="item-2">...</AccordionItem>
</Accordion>
```

View File

@@ -239,6 +239,8 @@ Three ways to specify a preset via `--preset`:
2. **Code:** `--preset a2r6bw` (base62 string, starts with lowercase `a`)
3. **URL:** `--preset "https://ui.shadcn.com/init?base=radix&style=nova&..."`
> **IMPORTANT:** Never try to decode, fetch, or resolve preset codes manually. Preset codes are opaque — pass them directly to `shadcn init --preset <code>` and let the CLI handle resolution.
### Named Presets
| Name | Base | Style | Font | Icon Library |
@@ -266,11 +268,8 @@ Both use neutral base color, neutral theme, default radius, subtle menu accent,
## Switching Presets
To change an existing project's preset:
Ask the user first: **reinstall**, **merge**, or **skip** existing components?
```bash
shadcn init --preset a2r6bw --force
shadcn init --reinstall # optional: update existing components
```
Always confirm with the user before `--reinstall` — it overwrites component files.
- **Reinstall** → `shadcn init --preset <code> --force --reinstall`. Overwrites all component files with the new preset styles. Use when the user hasn't customized components.
- **Merge** → `shadcn init --preset <code> --force --no-reinstall`, then run `shadcn 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.
- **Skip** → `shadcn init --preset <code> --force --no-reinstall`. Only updates config and CSS variables, leaves existing components as-is.

View File

@@ -13,7 +13,8 @@ Rules and examples for composing shadcn/ui components.
- Overlays
- Empty states
- Toast notifications
- Base-specific patterns
**Base-conditional API differences**: See [base-patterns.md](./base-patterns.md) for `asChild` vs `render`, Select `items` prop, ToggleGroup `type` vs `multiple`, Slider value types, and Accordion differences.
---
@@ -100,25 +101,15 @@ Use `Field orientation="horizontal"` for inline label + control layouts (e.g. se
## Toggle Groups
Use `ToggleGroup` + `ToggleGroupItem` when the user picks from a small set of options (27). Don't manually loop `Button` components with active state.
Use `ToggleGroup` + `ToggleGroupItem` when the user picks from a small set of options (27). Don't manually loop `Button` components with active state. See [base-patterns.md](./base-patterns.md#togglegroup) for `type` vs `multiple` prop differences.
```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
// Single selection (e.g. schedule type).
<ToggleGroup defaultValue={["daily"]} spacing={2}>
<ToggleGroup spacing={2}>
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
<ToggleGroupItem value="interval">Interval</ToggleGroupItem>
</ToggleGroup>
// Multi-selection (e.g. weekday picker).
<ToggleGroup value={activeDays} onValueChange={setActiveDays} spacing={2}>
{["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"].map((day) => (
<ToggleGroupItem key={day} value={day} className="size-7 rounded-full text-xs">
{day}
</ToggleGroupItem>
))}
</ToggleGroup>
```
Combine with `Field` for labelled toggle groups:
@@ -126,7 +117,7 @@ Combine with `Field` for labelled toggle groups:
```tsx
<Field orientation="horizontal">
<FieldTitle id="theme-label">Theme</FieldTitle>
<ToggleGroup aria-labelledby="theme-label" defaultValue={["system"]} spacing={2}>
<ToggleGroup aria-labelledby="theme-label" spacing={2}>
<ToggleGroupItem value="light">Light</ToggleGroupItem>
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
<ToggleGroupItem value="system">System</ToggleGroupItem>
@@ -134,6 +125,8 @@ Combine with `Field` for labelled toggle groups:
</Field>
```
> **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-patterns.md](./base-patterns.md#togglegroup).
---
## Alerts
@@ -201,246 +194,6 @@ toast("File deleted.", {
})
```
---
## Base-Specific Patterns
Check the `base` field from `shadcn info` to determine which patterns to use.
### Composition: asChild (radix) vs render (base)
Radix uses `asChild` to replace the default element. Base UI uses `render` instead.
```tsx
// radix.
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
// base.
<DialogTrigger render={<Button />}>Open</DialogTrigger>
```
```tsx
// radix.
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">Menu</Button>
</DropdownMenuTrigger>
// base.
<DropdownMenuTrigger render={<Button variant="ghost" size="icon" />}>
Menu
</DropdownMenuTrigger>
```
### Button as a link (base only)
When `render` changes a `Button` to a non-button element (`<a>`, `<span>`), add `nativeButton={false}` so Base UI doesn't apply button-specific behavior.
```tsx
// base.
<Button render={<a href="/docs" />} nativeButton={false}>
Read the docs
</Button>
// radix equivalent.
<Button asChild>
<a href="/docs">Read the docs</a>
</Button>
```
### Non-button trigger elements (base only)
When a trigger's `render` is not a `Button` (e.g. `InputGroupAddon`), add `nativeButton={false}`.
```tsx
// base.
<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>
Pick date
</PopoverTrigger>
```
### Select (base vs radix)
**items prop (base only).** Base `Select` requires an `items` prop on the root. Radix uses JSX only — no `items` prop.
```tsx
// base.
const items = [
{ label: "Select a fruit", value: null },
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
]
<Select items={items}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
// radix.
<Select>
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
```
**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder="...">`.
**Multiple selection (base only).** Base supports `multiple` and render-function children on `SelectValue`. Radix has no multi-select.
```tsx
// base.
<Select items={items} multiple defaultValue={[]}>
<SelectTrigger>
<SelectValue>
{(value: string[]) => {
if (value.length === 0) return "Select fruits"
return `${value.length} selected`
}}
</SelectValue>
</SelectTrigger>
<SelectContent>...</SelectContent>
</Select>
```
**Object values (base only).** Base supports object values with `itemToStringValue` and a render function on `SelectValue`. Radix uses string values only.
```tsx
// base.
<Select
defaultValue={plans[0]}
itemToStringValue={(plan) => plan.name}
>
<SelectTrigger>
<SelectValue>{(value) => value.name}</SelectValue>
</SelectTrigger>
...
</Select>
// radix — string values, controlled state.
const [plan, setPlan] = React.useState<string>("starter")
<Select value={plan} onValueChange={setPlan}>...</Select>
```
**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.
```tsx
// base.
<SelectContent alignItemWithTrigger={false} side="bottom">
// radix.
<SelectContent position="popper">
```
### ToggleGroup: multiple (base) vs type (radix)
Base uses a `multiple` boolean prop. Radix uses `type="single"` or `type="multiple"`.
```tsx
// base — single selection (no prop needed).
<ToggleGroup defaultValue={["daily"]} spacing={2}>
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
</ToggleGroup>
// base — multi-selection.
<ToggleGroup multiple>
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
</ToggleGroup>
// radix — single selection.
<ToggleGroup type="single" defaultValue="daily" spacing={2}>
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
</ToggleGroup>
// radix — multi-selection.
<ToggleGroup type="multiple">
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
</ToggleGroup>
```
**defaultValue / value type.** Base always uses arrays. Radix uses string for single, array for multiple.
```tsx
// base — controlled single.
const [value, setValue] = React.useState("normal")
<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>
// radix — controlled single.
const [value, setValue] = React.useState("normal")
<ToggleGroup type="single" value={value} onValueChange={setValue}>
```
### Slider: defaultValue (base vs radix)
Base accepts a plain number for a single thumb. Radix always requires an array.
```tsx
// base.
<Slider defaultValue={50} max={100} step={1} />
// radix.
<Slider defaultValue={[50]} max={100} step={1} />
```
Both use arrays for range sliders. Controlled `onValueChange` in base may need a cast.
```tsx
// base.
const [value, setValue] = React.useState([0.3, 0.7])
<Slider value={value} onValueChange={(v) => setValue(v as number[])} />
// radix.
const [value, setValue] = React.useState([0.3, 0.7])
<Slider value={value} onValueChange={setValue} />
```
### Accordion: type prop (radix only)
Radix requires `type="single"` or `type="multiple"` and supports `collapsible`. The `defaultValue` is a string.
```tsx
// radix.
<Accordion type="single" collapsible defaultValue="item-1">
<AccordionItem value="item-1">...</AccordionItem>
</Accordion>
```
Base uses no `type` prop. Use the `multiple` prop for multi-select. The `defaultValue` is an array.
```tsx
// base.
<Accordion defaultValue={["item-1"]}>
<AccordionItem value="item-1">...</AccordionItem>
</Accordion>
// base multi-select.
<Accordion multiple defaultValue={["item-1", "item-2"]}>
<AccordionItem value="item-1">...</AccordionItem>
<AccordionItem value="item-2">...</AccordionItem>
</Accordion>
```
---
For composition rules (asChild/render, className usage, Groups, data-icon, InputGroup, etc.), see the **Critical Rules** section in [SKILL.md](./SKILL.md#critical-rules).
For className usage, Groups, data-icon, InputGroup, and other composition rules, see the **Critical Rules** section in [SKILL.md](./SKILL.md#critical-rules). For asChild/render and other base-conditional APIs, see [base-patterns.md](./base-patterns.md).