mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-07 14:08:47 +00:00
Compare commits
45 Commits
dependabot
...
shadcn@4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d718a8045f | ||
|
|
2c4678c8c8 | ||
|
|
2466a300f4 | ||
|
|
66fcf1e853 | ||
|
|
5ebd54198d | ||
|
|
3a2d812510 | ||
|
|
7811557088 | ||
|
|
575f1602a1 | ||
|
|
ae70ecc2f3 | ||
|
|
42284f4e64 | ||
|
|
abc65a4871 | ||
|
|
7d5af61468 | ||
|
|
2badcdc31f | ||
|
|
64b8263450 | ||
|
|
13b4593f37 | ||
|
|
7dc65da6b2 | ||
|
|
98e56b773c | ||
|
|
7ff9778ff0 | ||
|
|
4af7bbf4ba | ||
|
|
f00a94d9e5 | ||
|
|
187ae44fa7 | ||
|
|
034178bf7d | ||
|
|
4064c78bc7 | ||
|
|
943b023b7c | ||
|
|
e3d654fd26 | ||
|
|
71d0470be1 | ||
|
|
53bbdc738f | ||
|
|
97707ec08e | ||
|
|
b9ce2f10c3 | ||
|
|
7cb3b13a33 | ||
|
|
e3d2b14911 | ||
|
|
58c9dc2a7e | ||
|
|
3bdf60340d | ||
|
|
c1e29824cd | ||
|
|
62f6df75f2 | ||
|
|
62bae86e86 | ||
|
|
aa69fbf85a | ||
|
|
8d41295f2c | ||
|
|
2b053d916d | ||
|
|
0d1309f322 | ||
|
|
c26250dcfe | ||
|
|
07c5c36be8 | ||
|
|
3c59a0cd95 | ||
|
|
16b7bea50d | ||
|
|
ccc4caad9c |
@@ -63,11 +63,7 @@ export default function IndexPage() {
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageNav className="hidden md:flex">
|
||||
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper flex-1 section-soft pb-6">
|
||||
<div className="container-wrapper flex-1 pb-6">
|
||||
<div className="container overflow-hidden">
|
||||
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
|
||||
<Image
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
@@ -22,7 +23,6 @@ import { FontPicker } from "@/app/(app)/create/components/font-picker"
|
||||
import { IconLibraryPicker } from "@/app/(app)/create/components/icon-library-picker"
|
||||
import { MainMenu } from "@/app/(app)/create/components/main-menu"
|
||||
import { MenuColorPicker } from "@/app/(app)/create/components/menu-picker"
|
||||
import { ProjectForm } from "@/app/(app)/create/components/project-form"
|
||||
import { RadiusPicker } from "@/app/(app)/create/components/radius-picker"
|
||||
import { RandomButton } from "@/app/(app)/create/components/random-button"
|
||||
import { ResetDialog } from "@/app/(app)/create/components/reset-button"
|
||||
@@ -32,6 +32,13 @@ import { V0Button } from "@/app/(app)/create/components/v0-button"
|
||||
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/create/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
|
||||
// Only visible when user clicks "Create Project".
|
||||
const ProjectForm = dynamic(() =>
|
||||
import("@/app/(app)/create/components/project-form").then(
|
||||
(m) => m.ProjectForm
|
||||
)
|
||||
)
|
||||
|
||||
export function Customizer({
|
||||
itemsByBase,
|
||||
}: {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { Suspense } from "react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
|
||||
type HistoryContextValue = {
|
||||
canGoBack: boolean
|
||||
@@ -12,12 +13,28 @@ type HistoryContextValue = {
|
||||
|
||||
const HistoryContext = React.createContext<HistoryContextValue | null>(null)
|
||||
|
||||
export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
// Reads useSearchParams() in its own Suspense boundary so the
|
||||
// provider never blanks out children while search params resolve.
|
||||
function PresetSync({
|
||||
onPresetChange,
|
||||
}: {
|
||||
onPresetChange: (preset: string) => void
|
||||
}) {
|
||||
const searchParams = useSearchParams()
|
||||
const preset = searchParams.get("preset") ?? ""
|
||||
|
||||
React.useEffect(() => {
|
||||
onPresetChange(preset)
|
||||
}, [preset, onPresetChange])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
|
||||
const [preset, setPreset] = React.useState("")
|
||||
|
||||
const entriesRef = React.useRef<string[]>([preset])
|
||||
const indexRef = React.useRef(0)
|
||||
const maxIndexRef = React.useRef(0)
|
||||
@@ -26,6 +43,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
const [index, setIndex] = React.useState(0)
|
||||
const [maxIndex, setMaxIndex] = React.useState(0)
|
||||
|
||||
const onPresetChange = React.useCallback((nextPreset: string) => {
|
||||
setPreset(nextPreset)
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isNavigatingRef.current) {
|
||||
isNavigatingRef.current = false
|
||||
@@ -67,9 +88,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
} else {
|
||||
params.delete("preset")
|
||||
}
|
||||
const pathname = window.location.pathname
|
||||
const query = params.toString()
|
||||
router.replace(query ? `${pathname}?${query}` : pathname)
|
||||
}, [pathname, router])
|
||||
}, [router])
|
||||
|
||||
const goForward = React.useCallback(() => {
|
||||
if (indexRef.current >= maxIndexRef.current) {
|
||||
@@ -88,9 +110,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
} else {
|
||||
params.delete("preset")
|
||||
}
|
||||
const pathname = window.location.pathname
|
||||
const query = params.toString()
|
||||
router.replace(query ? `${pathname}?${query}` : pathname)
|
||||
}, [pathname, router])
|
||||
}, [router])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
@@ -133,7 +156,14 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
[canGoBack, canGoForward, goBack, goForward]
|
||||
)
|
||||
|
||||
return <HistoryContext value={value}>{children}</HistoryContext>
|
||||
return (
|
||||
<HistoryContext value={value}>
|
||||
<Suspense>
|
||||
<PresetSync onPresetChange={onPresetChange} />
|
||||
</Suspense>
|
||||
{children}
|
||||
</HistoryContext>
|
||||
)
|
||||
}
|
||||
|
||||
export function useHistory() {
|
||||
|
||||
@@ -24,10 +24,15 @@ const LocksContext = React.createContext<LocksContextValue | null>(null)
|
||||
|
||||
export function LocksProvider({ children }: { children: React.ReactNode }) {
|
||||
const [locks, setLocks] = React.useState<Set<LockableParam>>(new Set())
|
||||
const locksRef = React.useRef(locks)
|
||||
React.useEffect(() => {
|
||||
locksRef.current = locks
|
||||
}, [locks])
|
||||
|
||||
// Stable callback — reads from ref so it doesn't change on every lock toggle.
|
||||
const isLocked = React.useCallback(
|
||||
(param: LockableParam) => locks.has(param),
|
||||
[locks]
|
||||
(param: LockableParam) => locksRef.current.has(param),
|
||||
[]
|
||||
)
|
||||
|
||||
const toggleLock = React.useCallback((param: LockableParam) => {
|
||||
|
||||
@@ -41,7 +41,7 @@ import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||
import { resolvePresetOverrides } from "@/app/(app)/create/lib/preset-query"
|
||||
|
||||
const designSystemSearchParams = {
|
||||
preset: parseAsString.withDefault("b2D0wqNxT"),
|
||||
preset: parseAsString.withDefault("b0"),
|
||||
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
|
||||
DEFAULT_CONFIG.base
|
||||
),
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { Suspense } from "react"
|
||||
import { type Metadata } from "next"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
|
||||
import { Customizer } from "@/app/(app)/create/components/customizer"
|
||||
import { PresetHandler } from "@/app/(app)/create/components/preset-handler"
|
||||
import { Preview } from "@/app/(app)/create/components/preview"
|
||||
import { WelcomeDialog } from "@/app/(app)/create/components/welcome-dialog"
|
||||
import { getAllItems } from "@/app/(app)/create/lib/api"
|
||||
|
||||
// Only shown on first visit (checks localStorage).
|
||||
const WelcomeDialog = dynamic(() =>
|
||||
import("@/app/(app)/create/components/welcome-dialog").then(
|
||||
(m) => m.WelcomeDialog
|
||||
)
|
||||
)
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "New Project",
|
||||
description:
|
||||
@@ -37,9 +46,7 @@ export const metadata: Metadata = {
|
||||
},
|
||||
}
|
||||
|
||||
export default async function CreatePage() {
|
||||
const itemsByBase = await getAllItems()
|
||||
|
||||
export default function CreatePage() {
|
||||
return (
|
||||
<div className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden section-soft [--customizer-width:--spacing(48)] [--gap:--spacing(4)] md:[--gap:--spacing(6)] 2xl:[--customizer-width:--spacing(56)]">
|
||||
<div
|
||||
@@ -47,10 +54,21 @@ export default async function CreatePage() {
|
||||
className="flex min-h-0 flex-1 flex-col gap-(--gap) p-(--gap) pt-[calc(var(--gap)*0.25)] md:flex-row-reverse"
|
||||
>
|
||||
<Preview />
|
||||
<Customizer itemsByBase={itemsByBase} />
|
||||
<Suspense
|
||||
fallback={
|
||||
<Skeleton className="isolate min-h-[151px] w-full self-start rounded-2xl md:h-full md:max-h-full md:min-h-0 md:w-(--customizer-width)" />
|
||||
}
|
||||
>
|
||||
<CustomizerLoader />
|
||||
</Suspense>
|
||||
</div>
|
||||
<PresetHandler />
|
||||
<WelcomeDialog />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function CustomizerLoader() {
|
||||
const itemsByBase = await getAllItems()
|
||||
return <Customizer itemsByBase={itemsByBase} />
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { Button } from "@/styles/radix-nova/ui/button"
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
|
||||
const NUMBER_OF_LATEST_PAGES = 2
|
||||
|
||||
export function generateMetadata() {
|
||||
return {
|
||||
title: "Changelog",
|
||||
@@ -34,8 +36,8 @@ export function generateMetadata() {
|
||||
|
||||
export default function ChangelogPage() {
|
||||
const pages = getChangelogPages()
|
||||
const latestPages = pages.slice(0, 5)
|
||||
const olderPages = pages.slice(5)
|
||||
const latestPages = pages.slice(0, NUMBER_OF_LATEST_PAGES)
|
||||
const olderPages = pages.slice(NUMBER_OF_LATEST_PAGES)
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -44,7 +46,7 @@ export default function ChangelogPage() {
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
<div className="mx-auto flex w-full max-w-[40rem] min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
|
||||
<div className="mx-auto flex w-full max-w-160 min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="scroll-m-24 text-4xl font-semibold tracking-tight sm:text-3xl">
|
||||
|
||||
@@ -337,6 +337,44 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/*
|
||||
* ```text composition trees use box-drawing characters; per-line padding makes
|
||||
* vertical connectors look broken. rehype-pretty-code sets `data-language` on
|
||||
* `pre`/`code` (not `language-*` classes). It also sets `code { display: grid }`,
|
||||
* which can add visible row separation — reset to a normal pre stack for text.
|
||||
*/
|
||||
[data-rehype-pretty-code-figure] pre[data-language="text"] code,
|
||||
[data-rehype-pretty-code-figure] pre[data-language="plaintext"] code,
|
||||
[data-slot="docs"] pre[data-language="text"] code,
|
||||
[data-slot="docs"] pre[data-language="plaintext"] code {
|
||||
display: block !important;
|
||||
white-space: pre;
|
||||
line-height: 0.95;
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
Monaco,
|
||||
Consolas,
|
||||
"Liberation Mono",
|
||||
"Courier New",
|
||||
monospace;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] pre[data-language="text"] [data-line],
|
||||
[data-rehype-pretty-code-figure] pre[data-language="plaintext"] [data-line],
|
||||
[data-rehype-pretty-code-figure] code[data-language="text"] [data-line],
|
||||
[data-rehype-pretty-code-figure] code[data-language="plaintext"] [data-line],
|
||||
[data-slot="docs"] pre[data-language="text"] [data-line],
|
||||
[data-slot="docs"] pre[data-language="plaintext"] [data-line] {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
min-height: unset;
|
||||
line-height: 0.95;
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-line] span {
|
||||
color: var(--shiki-light);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
export function Announcement() {
|
||||
return (
|
||||
<Badge asChild variant="secondary" className="bg-muted">
|
||||
<Link href="/create">
|
||||
<Link href="/docs/changelog">
|
||||
Introducing Luma <ArrowRightIcon />
|
||||
</Link>
|
||||
</Badge>
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Use the shadcn CLI to add components to your project.
|
||||
|
||||
## init
|
||||
|
||||
Use the `init` command to initialize configuration and dependencies for a new project.
|
||||
Use the `init` command to initialize configuration and dependencies for an existing project, or create a new project with `--name`.
|
||||
|
||||
The `init` command installs dependencies, adds the `cn` util and configures CSS variables for the project.
|
||||
|
||||
@@ -21,26 +21,26 @@ Usage: shadcn init [options] [components...]
|
||||
initialize your project and install dependencies
|
||||
|
||||
Arguments:
|
||||
components name, url or local path to component
|
||||
components names, url or local path to component
|
||||
|
||||
Options:
|
||||
-t, --template <template> the template to use. (next, vite, start, react-router, laravel, astro)
|
||||
-b, --base <base> the component library to use. (radix, base)
|
||||
-p, --preset [name] use a preset configuration. (name, URL, or preset code)
|
||||
-n, --name <name> the name for the new project.
|
||||
-d, --defaults use default configuration. (default: false)
|
||||
-p, --preset [name] use a preset configuration
|
||||
-y, --yes skip confirmation prompt. (default: true)
|
||||
-d, --defaults use default configuration: --template=next --preset=nova (default: false)
|
||||
-f, --force force overwrite of existing configuration. (default: false)
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||
-n, --name <name> the name for the new project.
|
||||
-s, --silent mute output. (default: false)
|
||||
--monorepo scaffold a monorepo project.
|
||||
--no-monorepo skip the monorepo prompt.
|
||||
--reinstall re-install existing UI components.
|
||||
--no-reinstall do not re-install existing UI components.
|
||||
--rtl enable RTL support.
|
||||
--no-rtl disable RTL support.
|
||||
--css-variables use css variables for theming. (default: true)
|
||||
--no-css-variables do not use css variables for theming.
|
||||
--monorepo scaffold a monorepo project.
|
||||
--no-monorepo skip the monorepo prompt.
|
||||
--rtl enable RTL support.
|
||||
--no-rtl disable RTL support.
|
||||
--reinstall re-install existing UI components.
|
||||
--no-reinstall do not re-install existing UI components.
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
@@ -85,6 +85,34 @@ Options:
|
||||
|
||||
---
|
||||
|
||||
## apply
|
||||
|
||||
Use the `apply` command to apply a preset to an existing project.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest apply --preset a2r6bw
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
```bash
|
||||
Usage: shadcn apply [options] [preset]
|
||||
|
||||
apply a preset to an existing project
|
||||
|
||||
Arguments:
|
||||
preset the preset to apply
|
||||
|
||||
Options:
|
||||
--preset <preset> preset configuration to apply
|
||||
-y, --yes skip confirmation prompt. (default: false)
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||
-s, --silent mute output. (default: false)
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## view
|
||||
|
||||
Use the `view` command to view items from the registry before installing them.
|
||||
|
||||
37
apps/v4/content/docs/changelog/2026-03-luma.mdx
Normal file
37
apps/v4/content/docs/changelog/2026-03-luma.mdx
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: March 2026 - Introducing Luma
|
||||
description: Rounded geometry. Soft elevation. Breathable layouts. Inspired by macOS Tahoe, minus the glass.
|
||||
date: 2026-03-31
|
||||
---
|
||||
|
||||
Introducing Luma, a new shadcn/ui style. Rounded geometry. Soft elevation. Breathable layouts. Inspired by macOS Tahoe, minus the glass.
|
||||
|
||||
<a href="/create">
|
||||
<Image
|
||||
src="/images/luma-light.png"
|
||||
width="2160"
|
||||
height="1832"
|
||||
alt="Luma style preview"
|
||||
className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/images/luma-dark.png"
|
||||
width="2160"
|
||||
height="1832"
|
||||
alt="Luma style preview"
|
||||
className="mt-6 hidden w-full overflow-hidden rounded-lg border shadow-sm dark:block"
|
||||
/>
|
||||
<span className="sr-only">Try Luma in shadcn/create</span>
|
||||
</a>
|
||||
|
||||
Luma is a new foundation for your next app. It gives components softer surfaces, more open spacing, and a calmer visual rhythm while keeping the same shadcn/ui workflow.
|
||||
|
||||
Like the other new styles, Luma goes beyond theming. It changes the geometry, spacing, and feel of the components so your app starts from a different visual baseline.
|
||||
|
||||
Available now in [shadcn/create](/create) for both Radix and Base UI.
|
||||
|
||||
<Button asChild size="sm">
|
||||
<Link href="/create?preset=b2D0wqNxT" className="mt-6 no-underline!">
|
||||
Try Luma
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: April 2026 - Component Composition
|
||||
description: Composition sections across component pages—structured trees that help you and your agents build correct UI.
|
||||
date: 2026-04-06
|
||||
---
|
||||
|
||||
We've added **Composition** sections across the component docs so you can see the correct structure at a glance: what wraps what, which subcomponents belong together, and how to avoid invalid nesting.
|
||||
|
||||
```text
|
||||
Card
|
||||
├── CardHeader
|
||||
│ ├── CardTitle
|
||||
│ ├── CardDescription
|
||||
│ └── CardAction
|
||||
├── CardContent
|
||||
└── CardFooter
|
||||
```
|
||||
|
||||
## Why we added this
|
||||
|
||||
We've found that **LLMs and coding agents compose elements more reliably** when they can see the full structure: fewer missing wrappers, fewer wrong hierarchies, better matches to the examples.
|
||||
|
||||
### Bring docs into your agent
|
||||
|
||||
You or your LLM can pull the same component documentation, including composition, usage, and examples, into context from the CLI:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest docs card
|
||||
```
|
||||
|
||||
If you're using the [shadcn/skills](/docs/skills), this is done automatically for you.
|
||||
@@ -80,6 +80,20 @@ import {
|
||||
</Accordion>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Accordion`:
|
||||
|
||||
```text
|
||||
Accordion
|
||||
├── AccordionItem
|
||||
│ ├── AccordionTrigger
|
||||
│ └── AccordionContent
|
||||
└── AccordionItem
|
||||
├── AccordionTrigger
|
||||
└── AccordionContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -94,6 +94,23 @@ import {
|
||||
</AlertDialog>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `AlertDialog`:
|
||||
|
||||
```text
|
||||
AlertDialog
|
||||
├── AlertDialogTrigger
|
||||
└── AlertDialogContent
|
||||
├── AlertDialogHeader
|
||||
│ ├── AlertDialogMedia
|
||||
│ ├── AlertDialogTitle
|
||||
│ └── AlertDialogDescription
|
||||
└── AlertDialogFooter
|
||||
├── AlertDialogCancel
|
||||
└── AlertDialogAction
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -67,6 +67,18 @@ import {
|
||||
</Alert>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Alert`:
|
||||
|
||||
```text
|
||||
Alert
|
||||
├── Icon
|
||||
├── AlertTitle
|
||||
├── AlertDescription
|
||||
└── AlertAction
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -69,6 +69,32 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
</Avatar>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Avatar`:
|
||||
|
||||
```text
|
||||
Avatar
|
||||
├── AvatarImage
|
||||
├── AvatarFallback
|
||||
└── AvatarBadge
|
||||
```
|
||||
|
||||
Use the following composition to build an `AvatarGroup`:
|
||||
|
||||
```text
|
||||
AvatarGroup
|
||||
├── Avatar
|
||||
│ ├── AvatarImage
|
||||
│ ├── AvatarFallback
|
||||
│ └── AvatarBadge
|
||||
├── Avatar
|
||||
│ ├── AvatarImage
|
||||
│ ├── AvatarFallback
|
||||
│ └── AvatarBadge
|
||||
└── AvatarGroupCount
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -80,6 +80,23 @@ import {
|
||||
</Breadcrumb>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Breadcrumb`:
|
||||
|
||||
```text
|
||||
Breadcrumb
|
||||
└── BreadcrumbList
|
||||
├── BreadcrumbItem
|
||||
│ └── BreadcrumbLink
|
||||
├── BreadcrumbSeparator
|
||||
├── BreadcrumbItem
|
||||
│ └── BreadcrumbLink
|
||||
├── BreadcrumbSeparator
|
||||
└── BreadcrumbItem
|
||||
└── BreadcrumbPage
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -66,6 +66,17 @@ import {
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ButtonGroup`:
|
||||
|
||||
```text
|
||||
ButtonGroup
|
||||
├── Button or Input
|
||||
├── ButtonGroupSeparator
|
||||
└── ButtonGroupText
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- The `ButtonGroup` component has the `role` attribute set to `group`.
|
||||
|
||||
@@ -77,6 +77,20 @@ import {
|
||||
</Card>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Card`:
|
||||
|
||||
```text
|
||||
Card
|
||||
├── CardHeader
|
||||
│ ├── CardTitle
|
||||
│ ├── CardDescription
|
||||
│ └── CardAction
|
||||
├── CardContent
|
||||
└── CardFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Size
|
||||
|
||||
@@ -85,6 +85,19 @@ import {
|
||||
</Carousel>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Carousel`:
|
||||
|
||||
```text
|
||||
Carousel
|
||||
├── CarouselContent
|
||||
│ ├── CarouselItem
|
||||
│ └── CarouselItem
|
||||
├── CarouselPrevious
|
||||
└── CarouselNext
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Sizes
|
||||
|
||||
@@ -73,6 +73,16 @@ import {
|
||||
</Collapsible>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Collapsible`:
|
||||
|
||||
```text
|
||||
Collapsible
|
||||
├── CollapsibleTrigger
|
||||
└── CollapsibleContent
|
||||
```
|
||||
|
||||
## Controlled State
|
||||
|
||||
Use the `open` and `onOpenChange` props to control the state.
|
||||
|
||||
@@ -90,6 +90,62 @@ export function ExampleCombobox() {
|
||||
}
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
### Simple
|
||||
|
||||
A single-line input and a flat list (see [Basic](#basic)).
|
||||
|
||||
```text
|
||||
Combobox
|
||||
├── ComboboxInput
|
||||
└── ComboboxContent
|
||||
├── ComboboxEmpty
|
||||
└── ComboboxList
|
||||
├── ComboboxItem
|
||||
└── ComboboxItem
|
||||
```
|
||||
|
||||
### With chips
|
||||
|
||||
Multi-select with `multiple`, chips, and a chips input (see [Multiple](#multiple)).
|
||||
|
||||
```text
|
||||
Combobox
|
||||
├── ComboboxChips
|
||||
│ ├── ComboboxValue
|
||||
│ │ └── ComboboxChip
|
||||
│ └── ComboboxChipsInput
|
||||
└── ComboboxContent
|
||||
├── ComboboxEmpty
|
||||
└── ComboboxList
|
||||
├── ComboboxItem
|
||||
└── ComboboxItem
|
||||
```
|
||||
|
||||
### With groups and collection
|
||||
|
||||
Nested items per group using `ComboboxCollection` inside each `ComboboxGroup`, with a separator between groups (see [Groups](#groups)).
|
||||
|
||||
```text
|
||||
Combobox
|
||||
├── ComboboxInput
|
||||
└── ComboboxContent
|
||||
├── ComboboxEmpty
|
||||
└── ComboboxList
|
||||
├── ComboboxGroup
|
||||
│ ├── ComboboxLabel
|
||||
│ └── ComboboxCollection
|
||||
│ ├── ComboboxItem
|
||||
│ └── ComboboxItem
|
||||
├── ComboboxSeparator
|
||||
└── ComboboxGroup
|
||||
├── ComboboxLabel
|
||||
└── ComboboxCollection
|
||||
├── ComboboxItem
|
||||
└── ComboboxItem
|
||||
```
|
||||
|
||||
## Custom Items
|
||||
|
||||
Use `itemToStringValue` when your items are objects.
|
||||
|
||||
@@ -96,6 +96,24 @@ import {
|
||||
</Command>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Command`:
|
||||
|
||||
```text
|
||||
Command
|
||||
├── CommandInput
|
||||
└── CommandList
|
||||
├── CommandEmpty
|
||||
├── CommandGroup
|
||||
│ ├── CommandItem
|
||||
│ └── CommandItem
|
||||
├── CommandSeparator
|
||||
└── CommandGroup
|
||||
├── CommandItem
|
||||
└── CommandItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -79,6 +79,37 @@ import {
|
||||
</ContextMenu>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ContextMenu`:
|
||||
|
||||
```text
|
||||
ContextMenu
|
||||
├── ContextMenuTrigger
|
||||
└── ContextMenuContent
|
||||
├── ContextMenuGroup
|
||||
│ ├── ContextMenuLabel
|
||||
│ ├── ContextMenuItem
|
||||
│ └── ContextMenuItem
|
||||
├── ContextMenuSeparator
|
||||
├── ContextMenuGroup
|
||||
│ ├── ContextMenuLabel
|
||||
│ ├── ContextMenuCheckboxItem
|
||||
│ └── ContextMenuCheckboxItem
|
||||
├── ContextMenuSeparator
|
||||
├── ContextMenuGroup
|
||||
│ ├── ContextMenuLabel
|
||||
│ └── ContextMenuRadioGroup
|
||||
│ ├── ContextMenuRadioItem
|
||||
│ └── ContextMenuRadioItem
|
||||
└── ContextMenuSub
|
||||
├── ContextMenuSubTrigger
|
||||
└── ContextMenuSubContent
|
||||
└── ContextMenuGroup
|
||||
├── ContextMenuItem
|
||||
└── ContextMenuItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -58,6 +58,17 @@ export function DatePickerDemo() {
|
||||
|
||||
See the [React DayPicker](https://react-day-picker.js.org) documentation for more information.
|
||||
|
||||
## Composition
|
||||
|
||||
A date picker is built from `Popover` and `Calendar` (there is no `DatePicker` root component):
|
||||
|
||||
```text
|
||||
Popover
|
||||
├── PopoverTrigger
|
||||
└── PopoverContent
|
||||
└── Calendar
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -85,6 +85,20 @@ import {
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Dialog`:
|
||||
|
||||
```text
|
||||
Dialog
|
||||
├── DialogTrigger
|
||||
└── DialogContent
|
||||
├── DialogHeader
|
||||
│ ├── DialogTitle
|
||||
│ └── DialogDescription
|
||||
└── DialogFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom Close Button
|
||||
|
||||
@@ -88,6 +88,20 @@ import {
|
||||
</Drawer>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Drawer`:
|
||||
|
||||
```text
|
||||
Drawer
|
||||
├── DrawerTrigger
|
||||
└── DrawerContent
|
||||
├── DrawerHeader
|
||||
│ ├── DrawerTitle
|
||||
│ └── DrawerDescription
|
||||
└── DrawerFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Scrollable Content
|
||||
|
||||
@@ -92,6 +92,38 @@ import {
|
||||
</DropdownMenu>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `DropdownMenu`:
|
||||
|
||||
```text
|
||||
DropdownMenu
|
||||
├── DropdownMenuTrigger
|
||||
└── DropdownMenuContent
|
||||
├── DropdownMenuGroup
|
||||
│ ├── DropdownMenuLabel
|
||||
│ ├── DropdownMenuItem
|
||||
│ └── DropdownMenuItem
|
||||
├── DropdownMenuSeparator
|
||||
├── DropdownMenuGroup
|
||||
│ ├── DropdownMenuLabel
|
||||
│ ├── DropdownMenuCheckboxItem
|
||||
│ └── DropdownMenuCheckboxItem
|
||||
├── DropdownMenuSeparator
|
||||
├── DropdownMenuGroup
|
||||
│ ├── DropdownMenuLabel
|
||||
│ └── DropdownMenuRadioGroup
|
||||
│ ├── DropdownMenuRadioItem
|
||||
│ └── DropdownMenuRadioItem
|
||||
└── DropdownMenuSub
|
||||
├── DropdownMenuSubTrigger
|
||||
└── DropdownMenuSubContent
|
||||
└── DropdownMenuGroup
|
||||
├── DropdownMenuLabel
|
||||
├── DropdownMenuItem
|
||||
└── DropdownMenuItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -75,6 +75,19 @@ import {
|
||||
</Empty>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Empty` state:
|
||||
|
||||
```text
|
||||
Empty
|
||||
├── EmptyHeader
|
||||
│ ├── EmptyMedia
|
||||
│ ├── EmptyTitle
|
||||
│ └── EmptyDescription
|
||||
└── EmptyContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Outline
|
||||
|
||||
@@ -87,6 +87,56 @@ import {
|
||||
</FieldSet>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
### Field
|
||||
|
||||
A single control with label, helper text, and validation.
|
||||
|
||||
```text
|
||||
Field
|
||||
├── FieldLabel
|
||||
├── Input / Textarea / Switch / Select
|
||||
├── FieldDescription
|
||||
└── FieldError
|
||||
```
|
||||
|
||||
### FieldGroup
|
||||
|
||||
Related fields in one group. Use `FieldSeparator` between sections when needed.
|
||||
|
||||
```text
|
||||
FieldGroup
|
||||
├── Field
|
||||
│ ├── FieldLabel
|
||||
│ ├── Input / Textarea / Switch / Select
|
||||
│ ├── FieldDescription
|
||||
│ └── FieldError
|
||||
├── FieldSeparator
|
||||
└── Field
|
||||
├── FieldLabel
|
||||
└── Input / Textarea / Switch / Select
|
||||
```
|
||||
|
||||
### FieldSet
|
||||
|
||||
Semantic grouping with a legend and description, usually containing a `FieldGroup`.
|
||||
|
||||
```text
|
||||
FieldSet
|
||||
├── FieldLegend
|
||||
├── FieldDescription
|
||||
└── FieldGroup
|
||||
├── Field
|
||||
│ ├── FieldLabel
|
||||
│ ├── Input / Textarea / Switch / Select
|
||||
│ ├── FieldDescription
|
||||
│ └── FieldError
|
||||
└── Field
|
||||
├── FieldLabel
|
||||
└── Input / Textarea / Switch / Select
|
||||
```
|
||||
|
||||
## Anatomy
|
||||
|
||||
The `Field` family is designed for composing accessible forms. A typical field is structured as follows:
|
||||
|
||||
@@ -75,6 +75,16 @@ import {
|
||||
</HoverCard>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `HoverCard`:
|
||||
|
||||
```text
|
||||
HoverCard
|
||||
├── HoverCardTrigger
|
||||
└── HoverCardContent
|
||||
```
|
||||
|
||||
## Trigger Delays
|
||||
|
||||
Use `delay` and `closeDelay` on the trigger to control when the card opens and
|
||||
|
||||
@@ -71,6 +71,18 @@ import {
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `InputGroup`:
|
||||
|
||||
```text
|
||||
InputGroup
|
||||
├── InputGroupInput or InputGroupTextarea
|
||||
├── InputGroupAddon
|
||||
├── InputGroupButton
|
||||
└── InputGroupText
|
||||
```
|
||||
|
||||
## Align
|
||||
|
||||
Use the `align` prop on `InputGroupAddon` to position the addon relative to the input.
|
||||
|
||||
@@ -82,6 +82,27 @@ import {
|
||||
</InputOTP>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `InputOTP`:
|
||||
|
||||
```text
|
||||
InputOTP
|
||||
├── InputOTPGroup
|
||||
│ ├── InputOTPSlot
|
||||
│ ├── InputOTPSlot
|
||||
│ └── InputOTPSlot
|
||||
├── InputOTPSeparator
|
||||
├── InputOTPGroup
|
||||
│ ├── InputOTPSlot
|
||||
│ ├── InputOTPSlot
|
||||
│ └── InputOTPSlot
|
||||
├── InputOTPSeparator
|
||||
└── InputOTPGroup
|
||||
├── InputOTPSlot
|
||||
└── InputOTPSlot
|
||||
```
|
||||
|
||||
## Pattern
|
||||
|
||||
Use the `pattern` prop to define a custom pattern for the OTP input.
|
||||
|
||||
@@ -73,6 +73,22 @@ import {
|
||||
</Item>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Item`:
|
||||
|
||||
```text
|
||||
ItemGroup
|
||||
└── Item
|
||||
├── ItemHeader
|
||||
├── ItemMedia
|
||||
├── ItemContent
|
||||
│ ├── ItemTitle
|
||||
│ └── ItemDescription
|
||||
├── ItemActions
|
||||
└── ItemFooter
|
||||
```
|
||||
|
||||
## Item vs Field
|
||||
|
||||
Use `Field` if you need to display a form input such as a checkbox, input, radio, or select.
|
||||
|
||||
@@ -53,6 +53,17 @@ import { Kbd } from "@/components/ui/kbd"
|
||||
<Kbd>Ctrl</Kbd>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Kbd` and `KbdGroup`:
|
||||
|
||||
```text
|
||||
Kbd
|
||||
KbdGroup
|
||||
├── Kbd
|
||||
└── Kbd
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Group
|
||||
|
||||
@@ -88,6 +88,46 @@ import {
|
||||
</Menubar>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Menubar`:
|
||||
|
||||
```text
|
||||
Menubar
|
||||
├── MenubarMenu
|
||||
│ ├── MenubarTrigger
|
||||
│ └── MenubarContent
|
||||
│ ├── MenubarGroup
|
||||
│ │ ├── MenubarLabel
|
||||
│ │ ├── MenubarItem
|
||||
│ │ └── MenubarItem
|
||||
│ ├── MenubarSeparator
|
||||
│ ├── MenubarGroup
|
||||
│ │ ├── MenubarLabel
|
||||
│ │ ├── MenubarCheckboxItem
|
||||
│ │ └── MenubarCheckboxItem
|
||||
│ ├── MenubarSeparator
|
||||
│ ├── MenubarGroup
|
||||
│ │ ├── MenubarLabel
|
||||
│ │ └── MenubarRadioGroup
|
||||
│ │ ├── MenubarRadioItem
|
||||
│ │ └── MenubarRadioItem
|
||||
│ └── MenubarSub
|
||||
│ ├── MenubarSubTrigger
|
||||
│ └── MenubarSubContent
|
||||
│ └── MenubarGroup
|
||||
│ ├── MenubarLabel
|
||||
│ ├── MenubarItem
|
||||
│ └── MenubarItem
|
||||
└── MenubarMenu
|
||||
├── MenubarTrigger
|
||||
└── MenubarContent
|
||||
└── MenubarGroup
|
||||
├── MenubarLabel
|
||||
├── MenubarItem
|
||||
└── MenubarItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Checkbox
|
||||
|
||||
@@ -70,6 +70,34 @@ import {
|
||||
</NativeSelect>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
### Simple
|
||||
|
||||
Options placed directly under `NativeSelect` (no `NativeSelectOptGroup`).
|
||||
|
||||
```text
|
||||
NativeSelect
|
||||
├── NativeSelectOption
|
||||
├── NativeSelectOption
|
||||
├── NativeSelectOption
|
||||
└── NativeSelectOption
|
||||
```
|
||||
|
||||
### With groups
|
||||
|
||||
Use `NativeSelectOptGroup` to organize options into categories.
|
||||
|
||||
```text
|
||||
NativeSelect
|
||||
├── NativeSelectOptGroup
|
||||
│ ├── NativeSelectOption
|
||||
│ └── NativeSelectOption
|
||||
└── NativeSelectOptGroup
|
||||
├── NativeSelectOption
|
||||
└── NativeSelectOption
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Groups
|
||||
|
||||
@@ -82,6 +82,23 @@ import {
|
||||
</NavigationMenu>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `NavigationMenu`:
|
||||
|
||||
```text
|
||||
NavigationMenu
|
||||
├── NavigationMenuList
|
||||
│ ├── NavigationMenuItem
|
||||
│ │ ├── NavigationMenuTrigger
|
||||
│ │ └── NavigationMenuContent
|
||||
│ │ ├── NavigationMenuLink
|
||||
│ │ └── NavigationMenuLink
|
||||
│ └── NavigationMenuItem
|
||||
│ └── NavigationMenuLink
|
||||
└── NavigationMenuIndicator
|
||||
```
|
||||
|
||||
## Link Component
|
||||
|
||||
Use the `render` prop to compose a custom link component such as Next.js `Link`.
|
||||
|
||||
@@ -84,6 +84,23 @@ import {
|
||||
</Pagination>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Pagination`:
|
||||
|
||||
```text
|
||||
Pagination
|
||||
└── PaginationContent
|
||||
├── PaginationItem
|
||||
│ └── PaginationPrevious
|
||||
├── PaginationItem
|
||||
│ └── PaginationLink
|
||||
├── PaginationItem
|
||||
│ └── PaginationEllipsis
|
||||
└── PaginationItem
|
||||
└── PaginationNext
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple
|
||||
|
||||
@@ -79,6 +79,16 @@ import {
|
||||
</Popover>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Popover`:
|
||||
|
||||
```text
|
||||
Popover
|
||||
├── PopoverTrigger
|
||||
└── PopoverContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -62,6 +62,33 @@ import { Progress } from "@/components/ui/progress"
|
||||
<Progress value={33} />
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
### With label and value
|
||||
|
||||
Use `ProgressLabel` and `ProgressValue` to add a label and value display.
|
||||
|
||||
```tsx showLineNumbers
|
||||
import {
|
||||
Progress,
|
||||
ProgressLabel,
|
||||
ProgressValue,
|
||||
} from "@/components/ui/progress"
|
||||
|
||||
;<Progress value={56} className="w-full max-w-sm">
|
||||
<ProgressLabel>Upload progress</ProgressLabel>
|
||||
<ProgressValue />
|
||||
</Progress>
|
||||
```
|
||||
|
||||
```text
|
||||
Progress
|
||||
├── ProgressLabel
|
||||
├── ProgressValue
|
||||
└── ProgressTrack
|
||||
└── ProgressIndicator
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Label
|
||||
|
||||
@@ -72,6 +72,16 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
</RadioGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `RadioGroup`:
|
||||
|
||||
```text
|
||||
RadioGroup
|
||||
├── RadioGroupItem
|
||||
└── RadioGroupItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Description
|
||||
|
||||
@@ -78,6 +78,17 @@ import {
|
||||
</ResizablePanelGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ResizablePanelGroup`:
|
||||
|
||||
```text
|
||||
ResizablePanelGroup
|
||||
├── ResizablePanel
|
||||
├── ResizableHandle
|
||||
└── ResizablePanel
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Vertical
|
||||
|
||||
@@ -68,6 +68,15 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
|
||||
</ScrollArea>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ScrollArea`:
|
||||
|
||||
```text
|
||||
ScrollArea
|
||||
└── ScrollBar
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Horizontal
|
||||
|
||||
@@ -89,6 +89,26 @@ const items = [
|
||||
</Select>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Select`:
|
||||
|
||||
```text
|
||||
Select
|
||||
├── SelectTrigger
|
||||
│ └── SelectValue
|
||||
└── SelectContent
|
||||
├── SelectGroup
|
||||
│ ├── SelectLabel
|
||||
│ ├── SelectItem
|
||||
│ └── SelectItem
|
||||
├── SelectSeparator
|
||||
└── SelectGroup
|
||||
├── SelectLabel
|
||||
├── SelectItem
|
||||
└── SelectItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Align Item With Trigger
|
||||
|
||||
@@ -79,6 +79,20 @@ import {
|
||||
</Sheet>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Sheet`:
|
||||
|
||||
```text
|
||||
Sheet
|
||||
├── SheetTrigger
|
||||
└── SheetContent
|
||||
├── SheetHeader
|
||||
│ ├── SheetTitle
|
||||
│ └── SheetDescription
|
||||
└── SheetFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Side
|
||||
|
||||
@@ -55,32 +55,6 @@ npx shadcn@latest add sidebar
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
## Structure
|
||||
|
||||
A `Sidebar` component is composed of the following parts:
|
||||
|
||||
- `SidebarProvider` - Handles collapsible state.
|
||||
- `Sidebar` - The sidebar container.
|
||||
- `SidebarHeader` and `SidebarFooter` - Sticky at the top and bottom of the sidebar.
|
||||
- `SidebarContent` - Scrollable content.
|
||||
- `SidebarGroup` - Section within the `SidebarContent`.
|
||||
- `SidebarTrigger` - Trigger for the `Sidebar`.
|
||||
|
||||
<Image
|
||||
src="/images/sidebar-structure.png"
|
||||
width="716"
|
||||
height="420"
|
||||
alt="Sidebar Structure"
|
||||
className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/images/sidebar-structure-dark.png"
|
||||
width="716"
|
||||
height="420"
|
||||
alt="Sidebar Structure"
|
||||
className="mt-6 hidden w-full overflow-hidden rounded-lg border dark:block"
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx showLineNumbers title="app/layout.tsx"
|
||||
@@ -123,6 +97,67 @@ export function AppSidebar() {
|
||||
}
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Sidebar` layout:
|
||||
|
||||
```text
|
||||
SidebarProvider
|
||||
├── Sidebar
|
||||
│ ├── SidebarHeader
|
||||
│ ├── SidebarContent
|
||||
│ │ ├── SidebarGroup
|
||||
│ │ │ ├── SidebarGroupLabel
|
||||
│ │ │ ├── SidebarGroupAction
|
||||
│ │ │ ├── SidebarGroupContent
|
||||
│ │ │ └── SidebarMenu
|
||||
│ │ │ ├── SidebarMenuItem
|
||||
│ │ │ │ ├── SidebarMenuButton
|
||||
│ │ │ │ ├── SidebarMenuAction
|
||||
│ │ │ │ └── SidebarMenuBadge
|
||||
│ │ │ └── SidebarMenuItem
|
||||
│ │ │ ├── SidebarMenuButton
|
||||
│ │ │ └── SidebarMenuSub
|
||||
│ │ │ ├── SidebarMenuSubItem
|
||||
│ │ │ └── SidebarMenuSubItem
|
||||
│ │ └── SidebarGroup
|
||||
│ │ └── SidebarMenu
|
||||
│ │ ├── SidebarMenuItem
|
||||
│ │ └── SidebarMenuItem
|
||||
│ ├── SidebarFooter
|
||||
│ └── SidebarRail
|
||||
├── SidebarInset
|
||||
└── SidebarTrigger
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
- **SidebarProvider** — Handles collapsible state and provides sidebar context to child components.
|
||||
- **Sidebar** — The main collapsible sidebar panel.
|
||||
- **SidebarHeader** — Sticky at the top; use for branding, titles, or workspace switchers.
|
||||
- **SidebarFooter** — Sticky at the bottom; use for user menus, settings, or actions.
|
||||
- **SidebarContent** — Scrollable region between the header and footer.
|
||||
- **SidebarGroup** — Groups related navigation with optional label, action, and content areas.
|
||||
- **SidebarMenu** / **SidebarMenuItem** — Menu structure for links, badges, actions, and nested submenus.
|
||||
- **SidebarRail** — Resize handle for adjusting sidebar width when applicable.
|
||||
- **SidebarInset** — Wraps main content when using the `inset` variant.
|
||||
- **SidebarTrigger** — Control that toggles the sidebar open or collapsed.
|
||||
|
||||
<Image
|
||||
src="/images/sidebar-structure.png"
|
||||
width="716"
|
||||
height="420"
|
||||
alt="Sidebar Structure"
|
||||
className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/images/sidebar-structure-dark.png"
|
||||
width="716"
|
||||
height="420"
|
||||
alt="Sidebar Structure"
|
||||
className="mt-6 hidden w-full overflow-hidden rounded-lg border dark:block"
|
||||
/>
|
||||
|
||||
## SidebarProvider
|
||||
|
||||
The `SidebarProvider` component is used to provide the sidebar context to the `Sidebar` component. You should always wrap your application in a `SidebarProvider` component.
|
||||
|
||||
@@ -83,6 +83,33 @@ import {
|
||||
</Table>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Table`:
|
||||
|
||||
```text
|
||||
Table
|
||||
├── TableCaption
|
||||
├── TableHeader
|
||||
│ └── TableRow
|
||||
│ ├── TableHead
|
||||
│ ├── TableHead
|
||||
│ ├── TableHead
|
||||
│ └── TableHead
|
||||
├── TableBody
|
||||
│ ├── TableRow
|
||||
│ │ ├── TableCell
|
||||
│ │ ├── TableCell
|
||||
│ │ ├── TableCell
|
||||
│ │ └── TableCell
|
||||
│ └── TableRow
|
||||
│ ├── TableCell
|
||||
│ ├── TableCell
|
||||
│ ├── TableCell
|
||||
│ └── TableCell
|
||||
└── TableFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Footer
|
||||
|
||||
@@ -73,6 +73,19 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Tabs`:
|
||||
|
||||
```text
|
||||
Tabs
|
||||
├── TabsList
|
||||
│ ├── TabsTrigger
|
||||
│ └── TabsTrigger
|
||||
├── TabsContent
|
||||
└── TabsContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Line
|
||||
|
||||
@@ -66,6 +66,16 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||||
</ToggleGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ToggleGroup`:
|
||||
|
||||
```text
|
||||
ToggleGroup
|
||||
├── ToggleGroupItem
|
||||
└── ToggleGroupItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Outline
|
||||
|
||||
@@ -109,6 +109,16 @@ import {
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Tooltip`:
|
||||
|
||||
```text
|
||||
Tooltip
|
||||
├── TooltipTrigger
|
||||
└── TooltipContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Side
|
||||
|
||||
@@ -80,6 +80,20 @@ import {
|
||||
</Accordion>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Accordion`:
|
||||
|
||||
```text
|
||||
Accordion
|
||||
├── AccordionItem
|
||||
│ ├── AccordionTrigger
|
||||
│ └── AccordionContent
|
||||
└── AccordionItem
|
||||
├── AccordionTrigger
|
||||
└── AccordionContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -94,6 +94,23 @@ import {
|
||||
</AlertDialog>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `AlertDialog`:
|
||||
|
||||
```text
|
||||
AlertDialog
|
||||
├── AlertDialogTrigger
|
||||
└── AlertDialogContent
|
||||
├── AlertDialogHeader
|
||||
│ ├── AlertDialogMedia
|
||||
│ ├── AlertDialogTitle
|
||||
│ └── AlertDialogDescription
|
||||
└── AlertDialogFooter
|
||||
├── AlertDialogCancel
|
||||
└── AlertDialogAction
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -71,6 +71,18 @@ import {
|
||||
</Alert>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Alert`:
|
||||
|
||||
```text
|
||||
Alert
|
||||
├── Icon
|
||||
├── AlertTitle
|
||||
├── AlertDescription
|
||||
└── AlertAction
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -65,6 +65,32 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
</Avatar>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Avatar`:
|
||||
|
||||
```text
|
||||
Avatar
|
||||
├── AvatarImage
|
||||
├── AvatarFallback
|
||||
└── AvatarBadge
|
||||
```
|
||||
|
||||
Use the following composition to build an `AvatarGroup`:
|
||||
|
||||
```text
|
||||
AvatarGroup
|
||||
├── Avatar
|
||||
│ ├── AvatarImage
|
||||
│ ├── AvatarFallback
|
||||
│ └── AvatarBadge
|
||||
├── Avatar
|
||||
│ ├── AvatarImage
|
||||
│ ├── AvatarFallback
|
||||
│ └── AvatarBadge
|
||||
└── AvatarGroupCount
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -74,6 +74,23 @@ import {
|
||||
</Breadcrumb>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Breadcrumb`:
|
||||
|
||||
```text
|
||||
Breadcrumb
|
||||
└── BreadcrumbList
|
||||
├── BreadcrumbItem
|
||||
│ └── BreadcrumbLink
|
||||
├── BreadcrumbSeparator
|
||||
├── BreadcrumbItem
|
||||
│ └── BreadcrumbLink
|
||||
├── BreadcrumbSeparator
|
||||
└── BreadcrumbItem
|
||||
└── BreadcrumbPage
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -66,6 +66,17 @@ import {
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ButtonGroup`:
|
||||
|
||||
```text
|
||||
ButtonGroup
|
||||
├── Button or Input
|
||||
├── ButtonGroupSeparator
|
||||
└── ButtonGroupText
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- The `ButtonGroup` component has the `role` attribute set to `group`.
|
||||
|
||||
@@ -77,6 +77,20 @@ import {
|
||||
</Card>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Card`:
|
||||
|
||||
```text
|
||||
Card
|
||||
├── CardHeader
|
||||
│ ├── CardTitle
|
||||
│ ├── CardDescription
|
||||
│ └── CardAction
|
||||
├── CardContent
|
||||
└── CardFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Size
|
||||
|
||||
@@ -85,6 +85,19 @@ import {
|
||||
</Carousel>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Carousel`:
|
||||
|
||||
```text
|
||||
Carousel
|
||||
├── CarouselContent
|
||||
│ ├── CarouselItem
|
||||
│ └── CarouselItem
|
||||
├── CarouselPrevious
|
||||
└── CarouselNext
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Sizes
|
||||
|
||||
@@ -77,6 +77,16 @@ import {
|
||||
</Collapsible>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Collapsible`:
|
||||
|
||||
```text
|
||||
Collapsible
|
||||
├── CollapsibleTrigger
|
||||
└── CollapsibleContent
|
||||
```
|
||||
|
||||
## Controlled State
|
||||
|
||||
Use the `open` and `onOpenChange` props to control the state.
|
||||
|
||||
@@ -90,6 +90,62 @@ export function ExampleCombobox() {
|
||||
}
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
### Simple
|
||||
|
||||
A single-line input and a flat list (see [Basic](#basic)).
|
||||
|
||||
```text
|
||||
Combobox
|
||||
├── ComboboxInput
|
||||
└── ComboboxContent
|
||||
├── ComboboxEmpty
|
||||
└── ComboboxList
|
||||
├── ComboboxItem
|
||||
└── ComboboxItem
|
||||
```
|
||||
|
||||
### With chips
|
||||
|
||||
Multi-select with `multiple`, chips, and a chips input (see [Multiple](#multiple)).
|
||||
|
||||
```text
|
||||
Combobox
|
||||
├── ComboboxChips
|
||||
│ ├── ComboboxValue
|
||||
│ │ └── ComboboxChip
|
||||
│ └── ComboboxChipsInput
|
||||
└── ComboboxContent
|
||||
├── ComboboxEmpty
|
||||
└── ComboboxList
|
||||
├── ComboboxItem
|
||||
└── ComboboxItem
|
||||
```
|
||||
|
||||
### With groups and collection
|
||||
|
||||
Nested items per group using `ComboboxCollection` inside each `ComboboxGroup`, with a separator between groups (see [Groups](#groups)).
|
||||
|
||||
```text
|
||||
Combobox
|
||||
├── ComboboxInput
|
||||
└── ComboboxContent
|
||||
├── ComboboxEmpty
|
||||
└── ComboboxList
|
||||
├── ComboboxGroup
|
||||
│ ├── ComboboxLabel
|
||||
│ └── ComboboxCollection
|
||||
│ ├── ComboboxItem
|
||||
│ └── ComboboxItem
|
||||
├── ComboboxSeparator
|
||||
└── ComboboxGroup
|
||||
├── ComboboxLabel
|
||||
└── ComboboxCollection
|
||||
├── ComboboxItem
|
||||
└── ComboboxItem
|
||||
```
|
||||
|
||||
## Custom Items
|
||||
|
||||
Use `itemToStringValue` when your items are objects.
|
||||
|
||||
@@ -96,6 +96,24 @@ import {
|
||||
</Command>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Command`:
|
||||
|
||||
```text
|
||||
Command
|
||||
├── CommandInput
|
||||
└── CommandList
|
||||
├── CommandEmpty
|
||||
├── CommandGroup
|
||||
│ ├── CommandItem
|
||||
│ └── CommandItem
|
||||
├── CommandSeparator
|
||||
└── CommandGroup
|
||||
├── CommandItem
|
||||
└── CommandItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -79,6 +79,37 @@ import {
|
||||
</ContextMenu>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ContextMenu`:
|
||||
|
||||
```text
|
||||
ContextMenu
|
||||
├── ContextMenuTrigger
|
||||
└── ContextMenuContent
|
||||
├── ContextMenuGroup
|
||||
│ ├── ContextMenuLabel
|
||||
│ ├── ContextMenuItem
|
||||
│ └── ContextMenuItem
|
||||
├── ContextMenuSeparator
|
||||
├── ContextMenuGroup
|
||||
│ ├── ContextMenuLabel
|
||||
│ ├── ContextMenuCheckboxItem
|
||||
│ └── ContextMenuCheckboxItem
|
||||
├── ContextMenuSeparator
|
||||
├── ContextMenuGroup
|
||||
│ ├── ContextMenuLabel
|
||||
│ └── ContextMenuRadioGroup
|
||||
│ ├── ContextMenuRadioItem
|
||||
│ └── ContextMenuRadioItem
|
||||
└── ContextMenuSub
|
||||
├── ContextMenuSubTrigger
|
||||
└── ContextMenuSubContent
|
||||
└── ContextMenuGroup
|
||||
├── ContextMenuItem
|
||||
└── ContextMenuItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -56,6 +56,17 @@ export function DatePickerDemo() {
|
||||
|
||||
See the [React DayPicker](https://react-day-picker.js.org) documentation for more information.
|
||||
|
||||
## Composition
|
||||
|
||||
A date picker is built from `Popover` and `Calendar` (there is no `DatePicker` root component):
|
||||
|
||||
```text
|
||||
Popover
|
||||
├── PopoverTrigger
|
||||
└── PopoverContent
|
||||
└── Calendar
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -85,6 +85,20 @@ import {
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Dialog`:
|
||||
|
||||
```text
|
||||
Dialog
|
||||
├── DialogTrigger
|
||||
└── DialogContent
|
||||
├── DialogHeader
|
||||
│ ├── DialogTitle
|
||||
│ └── DialogDescription
|
||||
└── DialogFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom Close Button
|
||||
|
||||
@@ -88,6 +88,20 @@ import {
|
||||
</Drawer>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Drawer`:
|
||||
|
||||
```text
|
||||
Drawer
|
||||
├── DrawerTrigger
|
||||
└── DrawerContent
|
||||
├── DrawerHeader
|
||||
│ ├── DrawerTitle
|
||||
│ └── DrawerDescription
|
||||
└── DrawerFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Scrollable Content
|
||||
|
||||
@@ -92,6 +92,38 @@ import {
|
||||
</DropdownMenu>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `DropdownMenu`:
|
||||
|
||||
```text
|
||||
DropdownMenu
|
||||
├── DropdownMenuTrigger
|
||||
└── DropdownMenuContent
|
||||
├── DropdownMenuGroup
|
||||
│ ├── DropdownMenuLabel
|
||||
│ ├── DropdownMenuItem
|
||||
│ └── DropdownMenuItem
|
||||
├── DropdownMenuSeparator
|
||||
├── DropdownMenuGroup
|
||||
│ ├── DropdownMenuLabel
|
||||
│ ├── DropdownMenuCheckboxItem
|
||||
│ └── DropdownMenuCheckboxItem
|
||||
├── DropdownMenuSeparator
|
||||
├── DropdownMenuGroup
|
||||
│ ├── DropdownMenuLabel
|
||||
│ └── DropdownMenuRadioGroup
|
||||
│ ├── DropdownMenuRadioItem
|
||||
│ └── DropdownMenuRadioItem
|
||||
└── DropdownMenuSub
|
||||
├── DropdownMenuSubTrigger
|
||||
└── DropdownMenuSubContent
|
||||
└── DropdownMenuGroup
|
||||
├── DropdownMenuLabel
|
||||
├── DropdownMenuItem
|
||||
└── DropdownMenuItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -75,6 +75,19 @@ import {
|
||||
</Empty>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Empty` state:
|
||||
|
||||
```text
|
||||
Empty
|
||||
├── EmptyHeader
|
||||
│ ├── EmptyMedia
|
||||
│ ├── EmptyTitle
|
||||
│ └── EmptyDescription
|
||||
└── EmptyContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Outline
|
||||
|
||||
@@ -87,6 +87,56 @@ import {
|
||||
</FieldSet>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
### Field
|
||||
|
||||
A single control with label, helper text, and validation.
|
||||
|
||||
```text
|
||||
Field
|
||||
├── FieldLabel
|
||||
├── Input / Textarea / Switch / Select
|
||||
├── FieldDescription
|
||||
└── FieldError
|
||||
```
|
||||
|
||||
### FieldGroup
|
||||
|
||||
Related fields in one group. Use `FieldSeparator` between sections when needed.
|
||||
|
||||
```text
|
||||
FieldGroup
|
||||
├── Field
|
||||
│ ├── FieldLabel
|
||||
│ ├── Input / Textarea / Switch / Select
|
||||
│ ├── FieldDescription
|
||||
│ └── FieldError
|
||||
├── FieldSeparator
|
||||
└── Field
|
||||
├── FieldLabel
|
||||
└── Input / Textarea / Switch / Select
|
||||
```
|
||||
|
||||
### FieldSet
|
||||
|
||||
Semantic grouping with a legend and description, usually containing a `FieldGroup`.
|
||||
|
||||
```text
|
||||
FieldSet
|
||||
├── FieldLegend
|
||||
├── FieldDescription
|
||||
└── FieldGroup
|
||||
├── Field
|
||||
│ ├── FieldLabel
|
||||
│ ├── Input / Textarea / Switch / Select
|
||||
│ ├── FieldDescription
|
||||
│ └── FieldError
|
||||
└── Field
|
||||
├── FieldLabel
|
||||
└── Input / Textarea / Switch / Select
|
||||
```
|
||||
|
||||
## Anatomy
|
||||
|
||||
The `Field` family is designed for composing accessible forms. A typical field is structured as follows:
|
||||
|
||||
@@ -75,6 +75,16 @@ import {
|
||||
</HoverCard>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `HoverCard`:
|
||||
|
||||
```text
|
||||
HoverCard
|
||||
├── HoverCardTrigger
|
||||
└── HoverCardContent
|
||||
```
|
||||
|
||||
## Trigger Delays
|
||||
|
||||
Use `openDelay` and `closeDelay` on the `HoverCard` to control when the card opens and
|
||||
|
||||
@@ -71,6 +71,18 @@ import {
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `InputGroup`:
|
||||
|
||||
```text
|
||||
InputGroup
|
||||
├── InputGroupInput or InputGroupTextarea
|
||||
├── InputGroupAddon
|
||||
├── InputGroupButton
|
||||
└── InputGroupText
|
||||
```
|
||||
|
||||
## Align
|
||||
|
||||
Use the `align` prop on `InputGroupAddon` to position the addon relative to the input.
|
||||
|
||||
@@ -82,6 +82,27 @@ import {
|
||||
</InputOTP>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `InputOTP`:
|
||||
|
||||
```text
|
||||
InputOTP
|
||||
├── InputOTPGroup
|
||||
│ ├── InputOTPSlot
|
||||
│ ├── InputOTPSlot
|
||||
│ └── InputOTPSlot
|
||||
├── InputOTPSeparator
|
||||
├── InputOTPGroup
|
||||
│ ├── InputOTPSlot
|
||||
│ ├── InputOTPSlot
|
||||
│ └── InputOTPSlot
|
||||
├── InputOTPSeparator
|
||||
└── InputOTPGroup
|
||||
├── InputOTPSlot
|
||||
└── InputOTPSlot
|
||||
```
|
||||
|
||||
## Pattern
|
||||
|
||||
Use the `pattern` prop to define a custom pattern for the OTP input.
|
||||
|
||||
@@ -73,6 +73,22 @@ import {
|
||||
</Item>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build an `Item`:
|
||||
|
||||
```text
|
||||
ItemGroup
|
||||
└── Item
|
||||
├── ItemHeader
|
||||
├── ItemMedia
|
||||
├── ItemContent
|
||||
│ ├── ItemTitle
|
||||
│ └── ItemDescription
|
||||
├── ItemActions
|
||||
└── ItemFooter
|
||||
```
|
||||
|
||||
## Item vs Field
|
||||
|
||||
Use `Field` if you need to display a form input such as a checkbox, input, radio, or select.
|
||||
|
||||
@@ -53,6 +53,17 @@ import { Kbd } from "@/components/ui/kbd"
|
||||
<Kbd>Ctrl</Kbd>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Kbd` and `KbdGroup`:
|
||||
|
||||
```text
|
||||
Kbd
|
||||
KbdGroup
|
||||
├── Kbd
|
||||
└── Kbd
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Group
|
||||
|
||||
@@ -88,6 +88,46 @@ import {
|
||||
</Menubar>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Menubar`:
|
||||
|
||||
```text
|
||||
Menubar
|
||||
├── MenubarMenu
|
||||
│ ├── MenubarTrigger
|
||||
│ └── MenubarContent
|
||||
│ ├── MenubarGroup
|
||||
│ │ ├── MenubarLabel
|
||||
│ │ ├── MenubarItem
|
||||
│ │ └── MenubarItem
|
||||
│ ├── MenubarSeparator
|
||||
│ ├── MenubarGroup
|
||||
│ │ ├── MenubarLabel
|
||||
│ │ ├── MenubarCheckboxItem
|
||||
│ │ └── MenubarCheckboxItem
|
||||
│ ├── MenubarSeparator
|
||||
│ ├── MenubarGroup
|
||||
│ │ ├── MenubarLabel
|
||||
│ │ └── MenubarRadioGroup
|
||||
│ │ ├── MenubarRadioItem
|
||||
│ │ └── MenubarRadioItem
|
||||
│ └── MenubarSub
|
||||
│ ├── MenubarSubTrigger
|
||||
│ └── MenubarSubContent
|
||||
│ └── MenubarGroup
|
||||
│ ├── MenubarLabel
|
||||
│ ├── MenubarItem
|
||||
│ └── MenubarItem
|
||||
└── MenubarMenu
|
||||
├── MenubarTrigger
|
||||
└── MenubarContent
|
||||
└── MenubarGroup
|
||||
├── MenubarLabel
|
||||
├── MenubarItem
|
||||
└── MenubarItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Checkbox
|
||||
|
||||
@@ -70,6 +70,34 @@ import {
|
||||
</NativeSelect>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
### Simple
|
||||
|
||||
Options placed directly under `NativeSelect` (no `NativeSelectOptGroup`).
|
||||
|
||||
```text
|
||||
NativeSelect
|
||||
├── NativeSelectOption
|
||||
├── NativeSelectOption
|
||||
├── NativeSelectOption
|
||||
└── NativeSelectOption
|
||||
```
|
||||
|
||||
### With groups
|
||||
|
||||
Use `NativeSelectOptGroup` to organize options into categories.
|
||||
|
||||
```text
|
||||
NativeSelect
|
||||
├── NativeSelectOptGroup
|
||||
│ ├── NativeSelectOption
|
||||
│ └── NativeSelectOption
|
||||
└── NativeSelectOptGroup
|
||||
├── NativeSelectOption
|
||||
└── NativeSelectOption
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Groups
|
||||
|
||||
@@ -82,6 +82,23 @@ import {
|
||||
</NavigationMenu>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `NavigationMenu`:
|
||||
|
||||
```text
|
||||
NavigationMenu
|
||||
├── NavigationMenuList
|
||||
│ ├── NavigationMenuItem
|
||||
│ │ ├── NavigationMenuTrigger
|
||||
│ │ └── NavigationMenuContent
|
||||
│ │ ├── NavigationMenuLink
|
||||
│ │ └── NavigationMenuLink
|
||||
│ └── NavigationMenuItem
|
||||
│ └── NavigationMenuLink
|
||||
└── NavigationMenuIndicator
|
||||
```
|
||||
|
||||
## Link Component
|
||||
|
||||
Use the `asChild` prop to compose a custom link component such as Next.js `Link`.
|
||||
|
||||
@@ -84,6 +84,23 @@ import {
|
||||
</Pagination>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Pagination`:
|
||||
|
||||
```text
|
||||
Pagination
|
||||
└── PaginationContent
|
||||
├── PaginationItem
|
||||
│ └── PaginationPrevious
|
||||
├── PaginationItem
|
||||
│ └── PaginationLink
|
||||
├── PaginationItem
|
||||
│ └── PaginationEllipsis
|
||||
└── PaginationItem
|
||||
└── PaginationNext
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple
|
||||
|
||||
@@ -79,6 +79,16 @@ import {
|
||||
</Popover>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Popover`:
|
||||
|
||||
```text
|
||||
Popover
|
||||
├── PopoverTrigger
|
||||
└── PopoverContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic
|
||||
|
||||
@@ -72,6 +72,16 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
</RadioGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `RadioGroup`:
|
||||
|
||||
```text
|
||||
RadioGroup
|
||||
├── RadioGroupItem
|
||||
└── RadioGroupItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Description
|
||||
|
||||
@@ -78,6 +78,17 @@ import {
|
||||
</ResizablePanelGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ResizablePanelGroup`:
|
||||
|
||||
```text
|
||||
ResizablePanelGroup
|
||||
├── ResizablePanel
|
||||
├── ResizableHandle
|
||||
└── ResizablePanel
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Vertical
|
||||
|
||||
@@ -68,6 +68,15 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
|
||||
</ScrollArea>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ScrollArea`:
|
||||
|
||||
```text
|
||||
ScrollArea
|
||||
└── ScrollBar
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Horizontal
|
||||
|
||||
@@ -81,6 +81,26 @@ import {
|
||||
</Select>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Select`:
|
||||
|
||||
```text
|
||||
Select
|
||||
├── SelectTrigger
|
||||
│ └── SelectValue
|
||||
└── SelectContent
|
||||
├── SelectGroup
|
||||
│ ├── SelectLabel
|
||||
│ ├── SelectItem
|
||||
│ └── SelectItem
|
||||
├── SelectSeparator
|
||||
└── SelectGroup
|
||||
├── SelectLabel
|
||||
├── SelectItem
|
||||
└── SelectItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Align Item With Trigger
|
||||
|
||||
@@ -79,6 +79,20 @@ import {
|
||||
</Sheet>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Sheet`:
|
||||
|
||||
```text
|
||||
Sheet
|
||||
├── SheetTrigger
|
||||
└── SheetContent
|
||||
├── SheetHeader
|
||||
│ ├── SheetTitle
|
||||
│ └── SheetDescription
|
||||
└── SheetFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Side
|
||||
|
||||
@@ -55,32 +55,6 @@ npx shadcn@latest add sidebar
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
## Structure
|
||||
|
||||
A `Sidebar` component is composed of the following parts:
|
||||
|
||||
- `SidebarProvider` - Handles collapsible state.
|
||||
- `Sidebar` - The sidebar container.
|
||||
- `SidebarHeader` and `SidebarFooter` - Sticky at the top and bottom of the sidebar.
|
||||
- `SidebarContent` - Scrollable content.
|
||||
- `SidebarGroup` - Section within the `SidebarContent`.
|
||||
- `SidebarTrigger` - Trigger for the `Sidebar`.
|
||||
|
||||
<Image
|
||||
src="/images/sidebar-structure.png"
|
||||
width="716"
|
||||
height="420"
|
||||
alt="Sidebar Structure"
|
||||
className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/images/sidebar-structure-dark.png"
|
||||
width="716"
|
||||
height="420"
|
||||
alt="Sidebar Structure"
|
||||
className="mt-6 hidden w-full overflow-hidden rounded-lg border dark:block"
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx showLineNumbers title="app/layout.tsx"
|
||||
@@ -123,6 +97,67 @@ export function AppSidebar() {
|
||||
}
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Sidebar` layout:
|
||||
|
||||
```text
|
||||
SidebarProvider
|
||||
├── Sidebar
|
||||
│ ├── SidebarHeader
|
||||
│ ├── SidebarContent
|
||||
│ │ ├── SidebarGroup
|
||||
│ │ │ ├── SidebarGroupLabel
|
||||
│ │ │ ├── SidebarGroupAction
|
||||
│ │ │ ├── SidebarGroupContent
|
||||
│ │ │ └── SidebarMenu
|
||||
│ │ │ ├── SidebarMenuItem
|
||||
│ │ │ │ ├── SidebarMenuButton
|
||||
│ │ │ │ ├── SidebarMenuAction
|
||||
│ │ │ │ └── SidebarMenuBadge
|
||||
│ │ │ └── SidebarMenuItem
|
||||
│ │ │ ├── SidebarMenuButton
|
||||
│ │ │ └── SidebarMenuSub
|
||||
│ │ │ ├── SidebarMenuSubItem
|
||||
│ │ │ └── SidebarMenuSubItem
|
||||
│ │ └── SidebarGroup
|
||||
│ │ └── SidebarMenu
|
||||
│ │ ├── SidebarMenuItem
|
||||
│ │ └── SidebarMenuItem
|
||||
│ ├── SidebarFooter
|
||||
│ └── SidebarRail
|
||||
├── SidebarInset
|
||||
└── SidebarTrigger
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
- **SidebarProvider** — Handles collapsible state and provides sidebar context to child components.
|
||||
- **Sidebar** — The main collapsible sidebar panel.
|
||||
- **SidebarHeader** — Sticky at the top; use for branding, titles, or workspace switchers.
|
||||
- **SidebarFooter** — Sticky at the bottom; use for user menus, settings, or actions.
|
||||
- **SidebarContent** — Scrollable region between the header and footer.
|
||||
- **SidebarGroup** — Groups related navigation with optional label, action, and content areas.
|
||||
- **SidebarMenu** / **SidebarMenuItem** — Menu structure for links, badges, actions, and nested submenus.
|
||||
- **SidebarRail** — Resize handle for adjusting sidebar width when applicable.
|
||||
- **SidebarInset** — Wraps main content when using the `inset` variant.
|
||||
- **SidebarTrigger** — Control that toggles the sidebar open or collapsed.
|
||||
|
||||
<Image
|
||||
src="/images/sidebar-structure.png"
|
||||
width="716"
|
||||
height="420"
|
||||
alt="Sidebar Structure"
|
||||
className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/images/sidebar-structure-dark.png"
|
||||
width="716"
|
||||
height="420"
|
||||
alt="Sidebar Structure"
|
||||
className="mt-6 hidden w-full overflow-hidden rounded-lg border dark:block"
|
||||
/>
|
||||
|
||||
## SidebarProvider
|
||||
|
||||
The `SidebarProvider` component is used to provide the sidebar context to the `Sidebar` component. You should always wrap your application in a `SidebarProvider` component.
|
||||
|
||||
@@ -83,6 +83,33 @@ import {
|
||||
</Table>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Table`:
|
||||
|
||||
```text
|
||||
Table
|
||||
├── TableCaption
|
||||
├── TableHeader
|
||||
│ └── TableRow
|
||||
│ ├── TableHead
|
||||
│ ├── TableHead
|
||||
│ ├── TableHead
|
||||
│ └── TableHead
|
||||
├── TableBody
|
||||
│ ├── TableRow
|
||||
│ │ ├── TableCell
|
||||
│ │ ├── TableCell
|
||||
│ │ ├── TableCell
|
||||
│ │ └── TableCell
|
||||
│ └── TableRow
|
||||
│ ├── TableCell
|
||||
│ ├── TableCell
|
||||
│ ├── TableCell
|
||||
│ └── TableCell
|
||||
└── TableFooter
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Footer
|
||||
|
||||
@@ -73,6 +73,19 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Tabs`:
|
||||
|
||||
```text
|
||||
Tabs
|
||||
├── TabsList
|
||||
│ ├── TabsTrigger
|
||||
│ └── TabsTrigger
|
||||
├── TabsContent
|
||||
└── TabsContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Line
|
||||
|
||||
@@ -66,6 +66,16 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||||
</ToggleGroup>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ToggleGroup`:
|
||||
|
||||
```text
|
||||
ToggleGroup
|
||||
├── ToggleGroupItem
|
||||
└── ToggleGroupItem
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Outline
|
||||
|
||||
@@ -109,6 +109,16 @@ import {
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Tooltip`:
|
||||
|
||||
```text
|
||||
Tooltip
|
||||
├── TooltipTrigger
|
||||
└── TooltipContent
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Side
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"input-otp": "^1.4.2",
|
||||
"jotai": "^2.15.0",
|
||||
"little-date": "^1.0.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash": "^4.17.23",
|
||||
"lru-cache": "^11.2.4",
|
||||
"lucide-react": "0.474.0",
|
||||
"motion": "^12.12.1",
|
||||
@@ -76,7 +76,7 @@
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shadcn": "4.1.2",
|
||||
"shadcn": "4.2.0",
|
||||
"shiki": "^1.10.1",
|
||||
"sonner": "^2.0.0",
|
||||
"swr": "^2.3.6",
|
||||
|
||||
BIN
apps/v4/public/images/luma-dark.png
Normal file
BIN
apps/v4/public/images/luma-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
BIN
apps/v4/public/images/luma-light.png
Normal file
BIN
apps/v4/public/images/luma-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -1018,5 +1018,11 @@
|
||||
"homepage": "https://mksingh.dev/docs",
|
||||
"url": "https://mksingh.dev/r/{name}.json",
|
||||
"description": "A personal registry of production-ready ShadCN components and utilities. Everything is built to drop into your existing ShadCN project with no extra setup."
|
||||
},
|
||||
{
|
||||
"name": "@flowkit-ui",
|
||||
"homepage": "https://flowkit-ui.vzkiss.com",
|
||||
"url": "https://flowkit-ui.vzkiss.com/r/{name}.json",
|
||||
"description": "Opinionated, accessible components on Base UI and shadcn-style primitives — starting with a Creatable Combobox."
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -69,7 +69,7 @@
|
||||
"tailwindcss": "^3.4.18",
|
||||
"tsx": "^4.1.4",
|
||||
"turbo": "^1.9.9",
|
||||
"vite": "^7.1.12",
|
||||
"vite": "^7.3.2",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^2.1.9"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @shadcn/ui
|
||||
|
||||
## 4.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#10313](https://github.com/shadcn-ui/ui/pull/10313) [`c1e29824cd7a6809448e45b6b7fe8f7be71ecb0f`](https://github.com/shadcn-ui/ui/commit/c1e29824cd7a6809448e45b6b7fe8f7be71ecb0f) Thanks [@shadcn](https://github.com/shadcn)! - add shadcn apply command
|
||||
|
||||
## 4.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -20,6 +20,16 @@ The `init` command installs dependencies, adds the `cn` util, configures Tailwin
|
||||
npx shadcn init
|
||||
```
|
||||
|
||||
## apply
|
||||
|
||||
Use the `apply` command to apply a preset to an existing project.
|
||||
|
||||
The `apply` command overwrites the current preset configuration, reinstalls detected UI components, and updates fonts and CSS variables to match the new preset.
|
||||
|
||||
```bash
|
||||
npx shadcn apply --preset a2r6bw
|
||||
```
|
||||
|
||||
## add
|
||||
|
||||
Use the `add` command to add components to your project.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"version": "4.1.2",
|
||||
"version": "4.2.0",
|
||||
"description": "Add components to your apps.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
49
packages/shadcn/src/commands/apply.test.ts
Normal file
49
packages/shadcn/src/commands/apply.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { REGISTRY_URL } from "@/src/registry/constants"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { resolveApplyInitUrl } from "./apply"
|
||||
|
||||
const SHADCN_URL = REGISTRY_URL.replace(/\/r\/?$/, "")
|
||||
|
||||
describe("resolveApplyInitUrl", () => {
|
||||
it("should include the inferred template for preset codes", () => {
|
||||
const initUrl = resolveApplyInitUrl("a0", "base", {
|
||||
template: "next",
|
||||
rtl: true,
|
||||
})
|
||||
const parsed = new URL(initUrl)
|
||||
|
||||
expect(parsed.origin + parsed.pathname).toBe(`${SHADCN_URL}/init`)
|
||||
expect(parsed.searchParams.get("template")).toBe("next")
|
||||
expect(parsed.searchParams.get("preset")).toBe("a0")
|
||||
expect(parsed.searchParams.get("base")).toBe("base")
|
||||
expect(parsed.searchParams.get("rtl")).toBe("true")
|
||||
})
|
||||
|
||||
it("should include the inferred template for named presets", () => {
|
||||
const initUrl = resolveApplyInitUrl("lyra", "base", {
|
||||
template: "next",
|
||||
rtl: true,
|
||||
})
|
||||
const parsed = new URL(initUrl)
|
||||
|
||||
expect(parsed.origin + parsed.pathname).toBe(`${SHADCN_URL}/init`)
|
||||
expect(parsed.searchParams.get("template")).toBe("next")
|
||||
expect(parsed.searchParams.get("base")).toBe("base")
|
||||
expect(parsed.searchParams.get("rtl")).toBe("true")
|
||||
})
|
||||
|
||||
it("should keep the current base for raw preset URLs without injecting a template", () => {
|
||||
const presetUrl = `${SHADCN_URL}/init?base=radix&style=nova&baseColor=neutral&theme=neutral&iconLibrary=lucide&font=inter&rtl=false&menuAccent=subtle&menuColor=default&radius=default`
|
||||
const initUrl = resolveApplyInitUrl(presetUrl, "base", {
|
||||
template: "next",
|
||||
rtl: true,
|
||||
})
|
||||
const parsed = new URL(initUrl)
|
||||
|
||||
expect(parsed.searchParams.get("template")).toBeNull()
|
||||
expect(parsed.searchParams.get("track")).toBe("1")
|
||||
expect(parsed.searchParams.get("base")).toBe("base")
|
||||
expect(parsed.searchParams.get("rtl")).toBe("true")
|
||||
})
|
||||
})
|
||||
309
packages/shadcn/src/commands/apply.ts
Normal file
309
packages/shadcn/src/commands/apply.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import path from "path"
|
||||
import { runInit } from "@/src/commands/init"
|
||||
import { preFlightApply } from "@/src/preflights/preflight-apply"
|
||||
import { decodePreset, isPresetCode } from "@/src/preset/preset"
|
||||
import {
|
||||
DEFAULT_PRESETS,
|
||||
promptToOpenPresetBuilder,
|
||||
resolveCreateUrl,
|
||||
resolveInitUrl,
|
||||
resolveRegistryBaseConfig,
|
||||
} from "@/src/preset/presets"
|
||||
import { SHADCN_URL } from "@/src/registry/constants"
|
||||
import { clearRegistryContext } from "@/src/registry/context"
|
||||
import { registryConfigSchema } from "@/src/registry/schema"
|
||||
import { isUrl } from "@/src/registry/utils"
|
||||
import { getTemplateForFramework } from "@/src/templates/index"
|
||||
import { loadEnvFiles } from "@/src/utils/env-loader"
|
||||
import * as ERRORS from "@/src/utils/errors"
|
||||
import { withFileBackup } from "@/src/utils/file-helper"
|
||||
import { getBase } from "@/src/utils/get-config"
|
||||
import {
|
||||
getProjectComponents,
|
||||
getProjectInfo,
|
||||
} from "@/src/utils/get-project-info"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
import { highlighter } from "@/src/utils/highlighter"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
import { Command } from "commander"
|
||||
import prompts from "prompts"
|
||||
import { z } from "zod"
|
||||
|
||||
export const applyOptionsSchema = z.object({
|
||||
cwd: z.string(),
|
||||
positionalPreset: z.string().optional(),
|
||||
preset: z.string().optional(),
|
||||
yes: z.boolean(),
|
||||
silent: z.boolean(),
|
||||
})
|
||||
|
||||
export const apply = new Command()
|
||||
.name("apply")
|
||||
.description("apply a preset to an existing project")
|
||||
.argument("[preset]", "the preset to apply")
|
||||
.option("--preset <preset>", "preset configuration to apply")
|
||||
.option("-y, --yes", "skip confirmation prompt.", false)
|
||||
.option(
|
||||
"-c, --cwd <cwd>",
|
||||
"the working directory. defaults to the current directory.",
|
||||
process.cwd()
|
||||
)
|
||||
.option("-s, --silent", "mute output.", false)
|
||||
.action(async (positionalPreset, opts) => {
|
||||
try {
|
||||
const options = applyOptionsSchema.parse({
|
||||
...opts,
|
||||
cwd: path.resolve(opts.cwd),
|
||||
positionalPreset,
|
||||
})
|
||||
|
||||
const preset = resolveApplyPreset(options)
|
||||
|
||||
const preflight = await preFlightApply(options)
|
||||
|
||||
if (preflight.errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT]) {
|
||||
logger.break()
|
||||
logger.error(
|
||||
`The ${highlighter.info(
|
||||
"apply"
|
||||
)} command only works in an existing project.`
|
||||
)
|
||||
logger.error(`Run ${highlighter.info(getInitCommand(preset))} first.`)
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (preflight.errors[ERRORS.MISSING_CONFIG]) {
|
||||
logger.break()
|
||||
logger.error(
|
||||
`No ${highlighter.info("components.json")} found at ${highlighter.info(
|
||||
options.cwd
|
||||
)}.`
|
||||
)
|
||||
logger.error(`Run ${highlighter.info(getInitCommand(preset))} first.`)
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const existingConfig = preflight.config
|
||||
if (!existingConfig) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const rtl = existingConfig.rtl ?? false
|
||||
const template = await resolveApplyTemplate(options.cwd)
|
||||
|
||||
if (!preset) {
|
||||
const createUrl = resolveCreateUrl({
|
||||
command: "init",
|
||||
template,
|
||||
base: getBase(existingConfig.style),
|
||||
rtl,
|
||||
})
|
||||
|
||||
await promptToOpenPresetBuilder({
|
||||
createUrl,
|
||||
followUp: `Then run ${highlighter.info(
|
||||
"shadcn apply --preset <preset>"
|
||||
)} with the preset code or preset URL from ui.shadcn.com.`,
|
||||
prompt: !options.yes,
|
||||
})
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
validatePreset(preset)
|
||||
|
||||
const reinstallComponents = await getProjectComponents(options.cwd)
|
||||
|
||||
if (!options.yes) {
|
||||
logger.break()
|
||||
logger.warn(
|
||||
highlighter.warn(
|
||||
`Applying a new preset will overwrite existing UI components, fonts, and CSS variables.`
|
||||
)
|
||||
)
|
||||
logger.warn(
|
||||
`Commit or stash your changes before continuing so you can easily go back.`
|
||||
)
|
||||
logger.break()
|
||||
logger.log(" The following components will be re-installed:")
|
||||
if (reinstallComponents.length) {
|
||||
for (let i = 0; i < reinstallComponents.length; i += 8) {
|
||||
logger.log(` - ${reinstallComponents.slice(i, i + 8).join(", ")}`)
|
||||
}
|
||||
} else {
|
||||
logger.log(" - No installed UI components were detected.")
|
||||
}
|
||||
logger.break()
|
||||
|
||||
const { proceed } = await prompts({
|
||||
type: "confirm",
|
||||
name: "proceed",
|
||||
message: "Would you like to continue?",
|
||||
initial: false,
|
||||
})
|
||||
|
||||
if (!proceed) {
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
await loadEnvFiles(options.cwd)
|
||||
|
||||
const currentBase = getBase(existingConfig.style)
|
||||
const initUrl = resolveApplyInitUrl(preset, currentBase, {
|
||||
template,
|
||||
rtl,
|
||||
})
|
||||
|
||||
await withFileBackup(
|
||||
path.resolve(options.cwd, "components.json"),
|
||||
async () => {
|
||||
const {
|
||||
registryBaseConfig,
|
||||
installStyleIndex,
|
||||
url: cleanUrl,
|
||||
} = await resolveRegistryBaseConfig(initUrl, options.cwd, {
|
||||
registries: existingConfig.registries as
|
||||
| z.infer<typeof registryConfigSchema>
|
||||
| undefined,
|
||||
})
|
||||
|
||||
await runInit({
|
||||
cwd: options.cwd,
|
||||
yes: true,
|
||||
force: false,
|
||||
reinstall: true,
|
||||
defaults: false,
|
||||
silent: options.silent,
|
||||
isNewProject: false,
|
||||
cssVariables: true,
|
||||
installStyleIndex,
|
||||
registryBaseConfig,
|
||||
existingConfig,
|
||||
components: [cleanUrl, ...reinstallComponents],
|
||||
})
|
||||
},
|
||||
{
|
||||
onBackupFailure: () => {
|
||||
logger.error(
|
||||
`Could not back up ${highlighter.info(
|
||||
"components.json"
|
||||
)}. Aborting.`
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
logger.break()
|
||||
logger.log("Preset applied successfully.")
|
||||
logger.break()
|
||||
} catch (error) {
|
||||
logger.break()
|
||||
handleError(error)
|
||||
} finally {
|
||||
clearRegistryContext()
|
||||
}
|
||||
})
|
||||
|
||||
function resolveApplyPreset(options: z.infer<typeof applyOptionsSchema>) {
|
||||
const positionalPreset = options.positionalPreset?.trim()
|
||||
const flagPreset = options.preset?.trim()
|
||||
|
||||
if (positionalPreset && flagPreset && positionalPreset !== flagPreset) {
|
||||
logger.error(
|
||||
`Received two different preset values. Use either the positional preset or ${highlighter.info(
|
||||
"--preset"
|
||||
)}, or pass the same value to both.`
|
||||
)
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
return flagPreset ?? positionalPreset
|
||||
}
|
||||
|
||||
function validatePreset(preset: string) {
|
||||
if (isUrl(preset) || isPresetCode(preset)) {
|
||||
return
|
||||
}
|
||||
|
||||
const knownPresetNames = Object.keys(DEFAULT_PRESETS)
|
||||
|
||||
if (!knownPresetNames.includes(preset)) {
|
||||
logger.error(
|
||||
`Invalid preset: ${highlighter.info(
|
||||
preset
|
||||
)}.\nUse one of the available presets: ${knownPresetNames.join(", ")} \nor build your own at ${highlighter.info(`${SHADCN_URL}/create`)}`
|
||||
)
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveApplyTemplate(cwd: string) {
|
||||
const projectInfo = await getProjectInfo(cwd)
|
||||
return getTemplateForFramework(projectInfo?.framework.name)
|
||||
}
|
||||
|
||||
export function resolveApplyInitUrl(
|
||||
preset: string,
|
||||
currentBase: "radix" | "base",
|
||||
options: { template?: string; rtl?: boolean } = {}
|
||||
) {
|
||||
if (isUrl(preset)) {
|
||||
const url = new URL(preset)
|
||||
|
||||
if (url.pathname === "/init" && preset.startsWith(SHADCN_URL)) {
|
||||
url.searchParams.set("track", "1")
|
||||
}
|
||||
|
||||
url.searchParams.set("base", currentBase)
|
||||
url.searchParams.set("rtl", String(options.rtl ?? false))
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
if (isPresetCode(preset)) {
|
||||
const decoded = decodePreset(preset)
|
||||
if (!decoded) {
|
||||
logger.error(`Invalid preset code: ${highlighter.info(preset)}`)
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
return resolveInitUrl(
|
||||
{
|
||||
...decoded,
|
||||
base: currentBase,
|
||||
rtl: options.rtl ?? false,
|
||||
},
|
||||
{ preset, template: options.template }
|
||||
)
|
||||
}
|
||||
|
||||
const resolvedPreset = DEFAULT_PRESETS[preset as keyof typeof DEFAULT_PRESETS]
|
||||
|
||||
return resolveInitUrl(
|
||||
{
|
||||
...resolvedPreset,
|
||||
base: currentBase,
|
||||
rtl: options.rtl ?? resolvedPreset.rtl,
|
||||
},
|
||||
{ template: options.template }
|
||||
)
|
||||
}
|
||||
|
||||
function quoteShellArg(value: string) {
|
||||
return /[^A-Za-z0-9_./:-]/.test(value) ? JSON.stringify(value) : value
|
||||
}
|
||||
|
||||
function getInitCommand(preset?: string) {
|
||||
if (!preset) {
|
||||
return "shadcn init"
|
||||
}
|
||||
|
||||
return `shadcn init --preset ${quoteShellArg(preset)}`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user