agents-templated 2.2.6 → 2.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -21
- package/bin/cli.js +10 -7
- package/lib/instructions.js +6 -6
- package/lib/layout.js +2 -2
- package/package.json +1 -1
- package/templates/.claude/rules/lessons-learned.md +44 -0
- package/templates/CLAUDE.md +16 -8
- package/templates/README.md +18 -14
- package/templates/agent-docs/ARCHITECTURE.md +2 -2
- package/templates/agent-docs/README.md +2 -2
- package/templates/agents/rules/guardrails.md +1 -1
- package/templates/agents/rules/lessons-learned.md +44 -0
- package/templates/agents/skills/README.md +4 -0
- package/templates/agents/skills/error-patterns/SKILL.md +70 -0
- package/templates/agents/skills/shadcn-ui/SKILL.md +1932 -0
- package/templates/agents/skills/shadcn-ui/references/chart.md +306 -0
- package/templates/agents/skills/shadcn-ui/references/learn.md +145 -0
- package/templates/agents/skills/shadcn-ui/references/official-ui-reference.md +1729 -0
- package/templates/agents/skills/shadcn-ui/references/reference.md +586 -0
- package/templates/agents/skills/shadcn-ui/references/ui-reference.md +1578 -0
- package/templates/.github/copilot-instructions.md +0 -9
|
@@ -0,0 +1,1932 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shadcn-ui
|
|
3
|
+
description: Provides complete shadcn/ui component library patterns including installation, configuration, and implementation of accessible React components. Use when setting up shadcn/ui, installing components, building forms with React Hook Form and Zod, customizing themes with Tailwind CSS, or implementing UI patterns like buttons, dialogs, dropdowns, tables, and complex form layouts.
|
|
4
|
+
allowed-tools: Read, Write, Bash, Edit, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# shadcn/ui Component Patterns
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Expert guide for building accessible, customizable UI components with shadcn/ui, Radix UI, and Tailwind CSS. This skill provides comprehensive patterns for implementing production-ready components with full accessibility support.
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [When to Use](#when-to-use)
|
|
16
|
+
- [Quick Start](#quick-start)
|
|
17
|
+
- [Installation & Setup](#installation--setup)
|
|
18
|
+
- [Project Configuration](#project-configuration)
|
|
19
|
+
- [Core Components](#core-components)
|
|
20
|
+
- [Button](#button-component)
|
|
21
|
+
- [Input & Form Fields](#input--form-fields)
|
|
22
|
+
- [Forms with Validation](#forms-with-validation)
|
|
23
|
+
- [Card](#card-component)
|
|
24
|
+
- [Dialog (Modal)](#dialog-modal-component)
|
|
25
|
+
- [Select (Dropdown)](#select-dropdown-component)
|
|
26
|
+
- [Sheet (Slide-over)](#sheet-slide-over-component)
|
|
27
|
+
- [Menubar & Navigation](#menubar--navigation)
|
|
28
|
+
- [Table](#table-component)
|
|
29
|
+
- [Toast Notifications](#toast-notifications)
|
|
30
|
+
- [Charts](#charts-component)
|
|
31
|
+
- [Advanced Patterns](#advanced-patterns)
|
|
32
|
+
- [Customization](#customization)
|
|
33
|
+
- [Next.js Integration](#nextjs-integration)
|
|
34
|
+
- [Best Practices](#best-practices)
|
|
35
|
+
- [Common Component Combinations](#common-component-combinations)
|
|
36
|
+
|
|
37
|
+
## When to Use
|
|
38
|
+
|
|
39
|
+
- Setting up a new project with shadcn/ui
|
|
40
|
+
- Installing or configuring individual components
|
|
41
|
+
- Building forms with React Hook Form and Zod validation
|
|
42
|
+
- Creating accessible UI components (buttons, dialogs, dropdowns, sheets)
|
|
43
|
+
- Customizing component styling with Tailwind CSS
|
|
44
|
+
- Implementing design systems with shadcn/ui
|
|
45
|
+
- Building Next.js applications with TypeScript
|
|
46
|
+
- Creating complex layouts and data displays
|
|
47
|
+
|
|
48
|
+
## Instructions
|
|
49
|
+
|
|
50
|
+
1. **Initialize Project**: Run `npx shadcn@latest init` to configure shadcn/ui
|
|
51
|
+
2. **Install Components**: Add components with `npx shadcn@latest add <component>`
|
|
52
|
+
3. **Configure Theme**: Customize CSS variables in globals.css for theming
|
|
53
|
+
4. **Import Components**: Use components from `@/components/ui/` directory
|
|
54
|
+
5. **Customize as Needed**: Modify component code directly in your project
|
|
55
|
+
6. **Add Form Validation**: Integrate React Hook Form with Zod schemas
|
|
56
|
+
7. **Test Accessibility**: Verify ARIA attributes and keyboard navigation
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
### Complete Form with Validation
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
"use client"
|
|
64
|
+
|
|
65
|
+
import { zodResolver } from "@hookform/resolvers/zod"
|
|
66
|
+
import { useForm } from "react-hook-form"
|
|
67
|
+
import { z } from "zod"
|
|
68
|
+
import { Button } from "@/components/ui/button"
|
|
69
|
+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
|
70
|
+
import { Input } from "@/components/ui/input"
|
|
71
|
+
|
|
72
|
+
const formSchema = z.object({
|
|
73
|
+
email: z.string().email("Invalid email"),
|
|
74
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
export function LoginForm() {
|
|
78
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
79
|
+
resolver: zodResolver(formSchema),
|
|
80
|
+
defaultValues: { email: "", password: "" },
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Form {...form}>
|
|
85
|
+
<form onSubmit={form.handleSubmit(console.log)} className="space-y-4">
|
|
86
|
+
<FormField name="email" render={({ field }) => (
|
|
87
|
+
<FormItem>
|
|
88
|
+
<FormLabel>Email</FormLabel>
|
|
89
|
+
<FormControl><Input type="email" {...field} /></FormControl>
|
|
90
|
+
<FormMessage />
|
|
91
|
+
</FormItem>
|
|
92
|
+
)} />
|
|
93
|
+
<Button type="submit">Login</Button>
|
|
94
|
+
</form>
|
|
95
|
+
</Form>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Constraints and Warnings
|
|
101
|
+
|
|
102
|
+
- **Not an NPM Package**: Components are copied to your project; you own the code
|
|
103
|
+
- **Registry Security**: Components installed via `npx shadcn@latest add` are fetched from remote registries (e.g., `ui.shadcn.com`); always verify the registry source is trusted before installation, and review generated component code before use in production
|
|
104
|
+
- **Custom Registry Validation**: When configuring custom registries in `components.json`, only use trusted private registry URLs; never point to untrusted third-party registry endpoints as they could inject malicious code
|
|
105
|
+
- **Client Components**: Most components require "use client" directive
|
|
106
|
+
- **Radix Dependencies**: Ensure all `@`radix-ui packages are installed
|
|
107
|
+
- **Tailwind Required**: Components rely on Tailwind CSS utilities
|
|
108
|
+
- **TypeScript**: Designed for TypeScript projects; type definitions included
|
|
109
|
+
- **Path Aliases**: Configure @ alias in tsconfig.json for imports
|
|
110
|
+
- **Dark Mode**: Set up dark mode with CSS variables or class strategy
|
|
111
|
+
|
|
112
|
+
## Quick Start
|
|
113
|
+
|
|
114
|
+
For new projects, use the automated setup:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Create Next.js project with shadcn/ui
|
|
118
|
+
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
|
|
119
|
+
cd my-app
|
|
120
|
+
npx shadcn@latest init
|
|
121
|
+
|
|
122
|
+
# Install essential components
|
|
123
|
+
npx shadcn@latest add button input form card dialog select
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
For existing projects:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Install dependencies
|
|
130
|
+
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
|
|
131
|
+
|
|
132
|
+
# Initialize shadcn/ui
|
|
133
|
+
npx shadcn@latest init
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## What is shadcn/ui?
|
|
137
|
+
|
|
138
|
+
shadcn/ui is **not** a traditional component library or npm package. Instead:
|
|
139
|
+
|
|
140
|
+
- It's a **collection of reusable components** that you can copy into your project
|
|
141
|
+
- Components are **yours to customize** - you own the code
|
|
142
|
+
- Built with **Radix UI** primitives for accessibility
|
|
143
|
+
- Styled with **Tailwind CSS** utilities
|
|
144
|
+
- Includes CLI tool for easy component installation
|
|
145
|
+
|
|
146
|
+
## Installation & Setup
|
|
147
|
+
|
|
148
|
+
### Initial Setup
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Initialize shadcn/ui in your project
|
|
152
|
+
npx shadcn@latest init
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
During setup, you'll configure:
|
|
156
|
+
- TypeScript or JavaScript
|
|
157
|
+
- Style (Default, New York, etc.)
|
|
158
|
+
- Base color theme
|
|
159
|
+
- CSS variables or Tailwind CSS classes
|
|
160
|
+
- Component installation path
|
|
161
|
+
|
|
162
|
+
### Installing Individual Components
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Install a single component
|
|
166
|
+
npx shadcn@latest add button
|
|
167
|
+
|
|
168
|
+
# Install multiple components
|
|
169
|
+
npx shadcn@latest add button input form
|
|
170
|
+
|
|
171
|
+
# Install all components
|
|
172
|
+
npx shadcn@latest add --all
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Manual Installation
|
|
176
|
+
|
|
177
|
+
If you prefer manual setup:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Install dependencies for a specific component
|
|
181
|
+
npm install @radix-ui/react-slot
|
|
182
|
+
|
|
183
|
+
# Copy component code from ui.shadcn.com
|
|
184
|
+
# Place in src/components/ui/
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Project Configuration
|
|
188
|
+
|
|
189
|
+
### Required Dependencies
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"dependencies": {
|
|
194
|
+
"@radix-ui/react-accordion": "^1.1.2",
|
|
195
|
+
"@radix-ui/react-alert-dialog": "^1.0.5",
|
|
196
|
+
"@radix-ui/react-dialog": "^1.0.5",
|
|
197
|
+
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
|
198
|
+
"@radix-ui/react-label": "^2.0.2",
|
|
199
|
+
"@radix-ui/react-select": "^2.0.0",
|
|
200
|
+
"@radix-ui/react-separator": "^1.0.3",
|
|
201
|
+
"@radix-ui/react-slot": "^1.0.2",
|
|
202
|
+
"@radix-ui/react-toast": "^1.1.5",
|
|
203
|
+
"class-variance-authority": "^0.7.0",
|
|
204
|
+
"clsx": "^2.0.0",
|
|
205
|
+
"lucide-react": "^0.294.0",
|
|
206
|
+
"tailwind-merge": "^2.0.0",
|
|
207
|
+
"tailwindcss-animate": "^1.0.7"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### TSConfig Configuration
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"compilerOptions": {
|
|
217
|
+
"target": "es5",
|
|
218
|
+
"lib": ["dom", "dom.iterable", "es6"],
|
|
219
|
+
"allowJs": true,
|
|
220
|
+
"skipLibCheck": true,
|
|
221
|
+
"strict": true,
|
|
222
|
+
"forceConsistentCasingInFileNames": true,
|
|
223
|
+
"noEmit": true,
|
|
224
|
+
"esModuleInterop": true,
|
|
225
|
+
"module": "esnext",
|
|
226
|
+
"moduleResolution": "node",
|
|
227
|
+
"resolveJsonModule": true,
|
|
228
|
+
"isolatedModules": true,
|
|
229
|
+
"jsx": "preserve",
|
|
230
|
+
"incremental": true,
|
|
231
|
+
"plugins": [
|
|
232
|
+
{
|
|
233
|
+
"name": "next"
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
"baseUrl": ".",
|
|
237
|
+
"paths": {
|
|
238
|
+
"@/components/*": ["./src/components/*"],
|
|
239
|
+
"@/lib/*": ["./src/lib/*"]
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
243
|
+
"exclude": ["node_modules"]
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Tailwind Configuration
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// tailwind.config.js
|
|
251
|
+
/** @type {import('tailwindcss').Config} */
|
|
252
|
+
module.exports = {
|
|
253
|
+
darkMode: ["class"],
|
|
254
|
+
content: [
|
|
255
|
+
'./pages/**/*.{ts,tsx}',
|
|
256
|
+
'./components/**/*.{ts,tsx}',
|
|
257
|
+
'./app/**/*.{ts,tsx}',
|
|
258
|
+
'./src/**/*.{ts,tsx}',
|
|
259
|
+
],
|
|
260
|
+
prefix: "",
|
|
261
|
+
theme: {
|
|
262
|
+
container: {
|
|
263
|
+
center: true,
|
|
264
|
+
padding: "2rem",
|
|
265
|
+
screens: {
|
|
266
|
+
"2xl": "1400px",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
extend: {
|
|
270
|
+
colors: {
|
|
271
|
+
border: "hsl(var(--border))",
|
|
272
|
+
input: "hsl(var(--input))",
|
|
273
|
+
ring: "hsl(var(--ring))",
|
|
274
|
+
background: "hsl(var(--background))",
|
|
275
|
+
foreground: "hsl(var(--foreground))",
|
|
276
|
+
primary: {
|
|
277
|
+
DEFAULT: "hsl(var(--primary))",
|
|
278
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
279
|
+
},
|
|
280
|
+
secondary: {
|
|
281
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
282
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
283
|
+
},
|
|
284
|
+
destructive: {
|
|
285
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
286
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
287
|
+
},
|
|
288
|
+
muted: {
|
|
289
|
+
DEFAULT: "hsl(var(--muted))",
|
|
290
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
291
|
+
},
|
|
292
|
+
accent: {
|
|
293
|
+
DEFAULT: "hsl(var(--accent))",
|
|
294
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
295
|
+
},
|
|
296
|
+
popover: {
|
|
297
|
+
DEFAULT: "hsl(var(--popover))",
|
|
298
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
299
|
+
},
|
|
300
|
+
card: {
|
|
301
|
+
DEFAULT: "hsl(var(--card))",
|
|
302
|
+
foreground: "hsl(var(--card-foreground))",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
borderRadius: {
|
|
306
|
+
lg: "var(--radius)",
|
|
307
|
+
md: "calc(var(--radius) - 2px)",
|
|
308
|
+
sm: "calc(var(--radius) - 4px)",
|
|
309
|
+
},
|
|
310
|
+
keyframes: {
|
|
311
|
+
"accordion-down": {
|
|
312
|
+
from: { height: "0" },
|
|
313
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
314
|
+
},
|
|
315
|
+
"accordion-up": {
|
|
316
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
317
|
+
to: { height: "0" },
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
animation: {
|
|
321
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
322
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
plugins: [require("tailwindcss-animate")],
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### CSS Variables (globals.css)
|
|
331
|
+
|
|
332
|
+
```css
|
|
333
|
+
@tailwind base;
|
|
334
|
+
@tailwind components;
|
|
335
|
+
@tailwind utilities;
|
|
336
|
+
|
|
337
|
+
@layer base {
|
|
338
|
+
:root {
|
|
339
|
+
--background: 0 0% 100%;
|
|
340
|
+
--foreground: 222.2 84% 4.9%;
|
|
341
|
+
--card: 0 0% 100%;
|
|
342
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
343
|
+
--popover: 0 0% 100%;
|
|
344
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
345
|
+
--primary: 222.2 47.4% 11.2%;
|
|
346
|
+
--primary-foreground: 210 40% 98%;
|
|
347
|
+
--secondary: 210 40% 96.1%;
|
|
348
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
349
|
+
--muted: 210 40% 96.1%;
|
|
350
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
351
|
+
--accent: 210 40% 96.1%;
|
|
352
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
353
|
+
--destructive: 0 84.2% 60.2%;
|
|
354
|
+
--destructive-foreground: 210 40% 98%;
|
|
355
|
+
--border: 214.3 31.8% 91.4%;
|
|
356
|
+
--input: 214.3 31.8% 91.4%;
|
|
357
|
+
--ring: 222.2 84% 4.9%;
|
|
358
|
+
--radius: 0.5rem;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.dark {
|
|
362
|
+
--background: 222.2 84% 4.9%;
|
|
363
|
+
--foreground: 210 40% 98%;
|
|
364
|
+
--card: 222.2 84% 4.9%;
|
|
365
|
+
--card-foreground: 210 40% 98%;
|
|
366
|
+
--popover: 222.2 84% 4.9%;
|
|
367
|
+
--popover-foreground: 210 40% 98%;
|
|
368
|
+
--primary: 210 40% 98%;
|
|
369
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
370
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
371
|
+
--secondary-foreground: 210 40% 98%;
|
|
372
|
+
--muted: 217.2 32.6% 17.5%;
|
|
373
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
374
|
+
--accent: 217.2 32.6% 17.5%;
|
|
375
|
+
--accent-foreground: 210 40% 98%;
|
|
376
|
+
--destructive: 0 62.8% 30.6%;
|
|
377
|
+
--destructive-foreground: 210 40% 98%;
|
|
378
|
+
--border: 217.2 32.6% 17.5%;
|
|
379
|
+
--input: 217.2 32.6% 17.5%;
|
|
380
|
+
--ring: 212.7 26.8% 83.9%;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@layer base {
|
|
385
|
+
* {
|
|
386
|
+
@apply border-border;
|
|
387
|
+
}
|
|
388
|
+
body {
|
|
389
|
+
@apply bg-background text-foreground;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Core Components
|
|
395
|
+
|
|
396
|
+
### Button Component
|
|
397
|
+
|
|
398
|
+
Installation:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
npx shadcn@latest add button
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Basic usage:
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
import { Button } from "@/components/ui/button";
|
|
408
|
+
|
|
409
|
+
export function ButtonDemo() {
|
|
410
|
+
return <Button>Click me</Button>;
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Button variants:
|
|
415
|
+
|
|
416
|
+
```tsx
|
|
417
|
+
import { Button } from "@/components/ui/button";
|
|
418
|
+
|
|
419
|
+
export function ButtonVariants() {
|
|
420
|
+
return (
|
|
421
|
+
<div className="flex gap-4">
|
|
422
|
+
<Button variant="default">Default</Button>
|
|
423
|
+
<Button variant="destructive">Destructive</Button>
|
|
424
|
+
<Button variant="outline">Outline</Button>
|
|
425
|
+
<Button variant="secondary">Secondary</Button>
|
|
426
|
+
<Button variant="ghost">Ghost</Button>
|
|
427
|
+
<Button variant="link">Link</Button>
|
|
428
|
+
</div>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Button sizes:
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
<div className="flex gap-4 items-center">
|
|
437
|
+
<Button size="default">Default</Button>
|
|
438
|
+
<Button size="sm">Small</Button>
|
|
439
|
+
<Button size="lg">Large</Button>
|
|
440
|
+
<Button size="icon">
|
|
441
|
+
<Icon className="h-4 w-4" />
|
|
442
|
+
</Button>
|
|
443
|
+
</div>
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
With loading state:
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
import { Button } from "@/components/ui/button";
|
|
450
|
+
import { Loader2 } from "lucide-react";
|
|
451
|
+
|
|
452
|
+
export function ButtonLoading() {
|
|
453
|
+
return (
|
|
454
|
+
<Button disabled>
|
|
455
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
456
|
+
Please wait
|
|
457
|
+
</Button>
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Input & Form Fields
|
|
463
|
+
|
|
464
|
+
#### Input Component
|
|
465
|
+
|
|
466
|
+
Installation:
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
npx shadcn@latest add input
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Basic input:
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
import { Input } from "@/components/ui/input";
|
|
476
|
+
|
|
477
|
+
export function InputDemo() {
|
|
478
|
+
return <Input type="email" placeholder="Email" />;
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Input with label:
|
|
483
|
+
|
|
484
|
+
```tsx
|
|
485
|
+
import { Input } from "@/components/ui/input";
|
|
486
|
+
import { Label } from "@/components/ui/label";
|
|
487
|
+
|
|
488
|
+
export function InputWithLabel() {
|
|
489
|
+
return (
|
|
490
|
+
<div className="grid w-full max-w-sm items-center gap-1.5">
|
|
491
|
+
<Label htmlFor="email">Email</Label>
|
|
492
|
+
<Input type="email" id="email" placeholder="Email" />
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Input with button:
|
|
499
|
+
|
|
500
|
+
```tsx
|
|
501
|
+
import { Button } from "@/components/ui/button";
|
|
502
|
+
import { Input } from "@/components/ui/input";
|
|
503
|
+
|
|
504
|
+
export function InputWithButton() {
|
|
505
|
+
return (
|
|
506
|
+
<div className="flex w-full max-w-sm items-center gap-2">
|
|
507
|
+
<Input type="email" placeholder="Email" />
|
|
508
|
+
<Button type="submit" variant="outline">Subscribe</Button>
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Forms with Validation
|
|
515
|
+
|
|
516
|
+
Installation:
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
npx shadcn@latest add form
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
This installs React Hook Form, Zod, and form components.
|
|
523
|
+
|
|
524
|
+
Complete form example:
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
"use client"
|
|
528
|
+
|
|
529
|
+
import { zodResolver } from "@hookform/resolvers/zod"
|
|
530
|
+
import { useForm } from "react-hook-form"
|
|
531
|
+
import * as z from "zod"
|
|
532
|
+
|
|
533
|
+
import { Button } from "@/components/ui/button"
|
|
534
|
+
import {
|
|
535
|
+
Form,
|
|
536
|
+
FormControl,
|
|
537
|
+
FormDescription,
|
|
538
|
+
FormField,
|
|
539
|
+
FormItem,
|
|
540
|
+
FormLabel,
|
|
541
|
+
FormMessage,
|
|
542
|
+
} from "@/components/ui/form"
|
|
543
|
+
import { Input } from "@/components/ui/input"
|
|
544
|
+
import { toast } from "@/components/ui/use-toast"
|
|
545
|
+
|
|
546
|
+
const formSchema = z.object({
|
|
547
|
+
username: z.string().min(2, {
|
|
548
|
+
message: "Username must be at least 2 characters.",
|
|
549
|
+
}),
|
|
550
|
+
email: z.string().email({
|
|
551
|
+
message: "Please enter a valid email address.",
|
|
552
|
+
}),
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
export function ProfileForm() {
|
|
556
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
557
|
+
resolver: zodResolver(formSchema),
|
|
558
|
+
defaultValues: {
|
|
559
|
+
username: "",
|
|
560
|
+
email: "",
|
|
561
|
+
},
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
function onSubmit(values: z.infer<typeof formSchema>) {
|
|
565
|
+
toast({
|
|
566
|
+
title: "You submitted the following values:",
|
|
567
|
+
description: (
|
|
568
|
+
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
|
569
|
+
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
|
|
570
|
+
</pre>
|
|
571
|
+
),
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<Form {...form}>
|
|
577
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
|
578
|
+
<FormField
|
|
579
|
+
control={form.control}
|
|
580
|
+
name="username"
|
|
581
|
+
render={({ field }) => (
|
|
582
|
+
<FormItem>
|
|
583
|
+
<FormLabel>Username</FormLabel>
|
|
584
|
+
<FormControl>
|
|
585
|
+
<Input placeholder="shadcn" {...field} />
|
|
586
|
+
</FormControl>
|
|
587
|
+
<FormDescription>
|
|
588
|
+
This is your public display name.
|
|
589
|
+
</FormDescription>
|
|
590
|
+
<FormMessage />
|
|
591
|
+
</FormItem>
|
|
592
|
+
)}
|
|
593
|
+
/>
|
|
594
|
+
|
|
595
|
+
<FormField
|
|
596
|
+
control={form.control}
|
|
597
|
+
name="email"
|
|
598
|
+
render={({ field }) => (
|
|
599
|
+
<FormItem>
|
|
600
|
+
<FormLabel>Email</FormLabel>
|
|
601
|
+
<FormControl>
|
|
602
|
+
<Input type="email" placeholder="you@example.com" {...field} />
|
|
603
|
+
</FormControl>
|
|
604
|
+
<FormMessage />
|
|
605
|
+
</FormItem>
|
|
606
|
+
)}
|
|
607
|
+
/>
|
|
608
|
+
|
|
609
|
+
<Button type="submit">Submit</Button>
|
|
610
|
+
</form>
|
|
611
|
+
</Form>
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Card Component
|
|
617
|
+
|
|
618
|
+
Installation:
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
npx shadcn@latest add card
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
Basic card:
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
import {
|
|
628
|
+
Card,
|
|
629
|
+
CardContent,
|
|
630
|
+
CardDescription,
|
|
631
|
+
CardFooter,
|
|
632
|
+
CardHeader,
|
|
633
|
+
CardTitle,
|
|
634
|
+
} from "@/components/ui/card"
|
|
635
|
+
|
|
636
|
+
export function CardDemo() {
|
|
637
|
+
return (
|
|
638
|
+
<Card>
|
|
639
|
+
<CardHeader>
|
|
640
|
+
<CardTitle>Card Title</CardTitle>
|
|
641
|
+
<CardDescription>Card Description</CardDescription>
|
|
642
|
+
</CardHeader>
|
|
643
|
+
<CardContent>
|
|
644
|
+
<p>Card Content</p>
|
|
645
|
+
</CardContent>
|
|
646
|
+
<CardFooter>
|
|
647
|
+
<p>Card Footer</p>
|
|
648
|
+
</CardFooter>
|
|
649
|
+
</Card>
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Card with form:
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
import { Button } from "@/components/ui/button"
|
|
658
|
+
import {
|
|
659
|
+
Card,
|
|
660
|
+
CardContent,
|
|
661
|
+
CardDescription,
|
|
662
|
+
CardFooter,
|
|
663
|
+
CardHeader,
|
|
664
|
+
CardTitle,
|
|
665
|
+
} from "@/components/ui/card"
|
|
666
|
+
import { Input } from "@/components/ui/input"
|
|
667
|
+
import { Label } from "@/components/ui/label"
|
|
668
|
+
|
|
669
|
+
export function CardWithForm() {
|
|
670
|
+
return (
|
|
671
|
+
<Card className="w-[350px]">
|
|
672
|
+
<CardHeader>
|
|
673
|
+
<CardTitle>Create project</CardTitle>
|
|
674
|
+
<CardDescription>Deploy your new project in one-click.</CardDescription>
|
|
675
|
+
</CardHeader>
|
|
676
|
+
<CardContent>
|
|
677
|
+
<form>
|
|
678
|
+
<div className="grid w-full items-center gap-4">
|
|
679
|
+
<div className="flex flex-col space-y-1.5">
|
|
680
|
+
<Label htmlFor="name">Name</Label>
|
|
681
|
+
<Input id="name" placeholder="Name of your project" />
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
</form>
|
|
685
|
+
</CardContent>
|
|
686
|
+
<CardFooter className="flex justify-between">
|
|
687
|
+
<Button variant="outline">Cancel</Button>
|
|
688
|
+
<Button>Deploy</Button>
|
|
689
|
+
</CardFooter>
|
|
690
|
+
</Card>
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Dialog (Modal) Component
|
|
696
|
+
|
|
697
|
+
Installation:
|
|
698
|
+
|
|
699
|
+
```bash
|
|
700
|
+
npx shadcn@latest add dialog
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Basic dialog:
|
|
704
|
+
|
|
705
|
+
```tsx
|
|
706
|
+
import { Button } from "@/components/ui/button"
|
|
707
|
+
import {
|
|
708
|
+
Dialog,
|
|
709
|
+
DialogContent,
|
|
710
|
+
DialogDescription,
|
|
711
|
+
DialogFooter,
|
|
712
|
+
DialogHeader,
|
|
713
|
+
DialogTitle,
|
|
714
|
+
DialogTrigger,
|
|
715
|
+
} from "@/components/ui/dialog"
|
|
716
|
+
|
|
717
|
+
export function DialogDemo() {
|
|
718
|
+
return (
|
|
719
|
+
<Dialog>
|
|
720
|
+
<DialogTrigger asChild>
|
|
721
|
+
<Button variant="outline">Open Dialog</Button>
|
|
722
|
+
</DialogTrigger>
|
|
723
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
724
|
+
<DialogHeader>
|
|
725
|
+
<DialogTitle>Edit profile</DialogTitle>
|
|
726
|
+
<DialogDescription>
|
|
727
|
+
Make changes to your profile here. Click save when you're done.
|
|
728
|
+
</DialogDescription>
|
|
729
|
+
</DialogHeader>
|
|
730
|
+
<div className="grid gap-4 py-4">
|
|
731
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
732
|
+
<Label htmlFor="name" className="text-right">
|
|
733
|
+
Name
|
|
734
|
+
</Label>
|
|
735
|
+
<Input id="name" value="Pedro Duarte" className="col-span-3" />
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
<DialogFooter>
|
|
739
|
+
<Button type="submit">Save changes</Button>
|
|
740
|
+
</DialogFooter>
|
|
741
|
+
</DialogContent>
|
|
742
|
+
</Dialog>
|
|
743
|
+
)
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Sheet (Slide-over) Component
|
|
748
|
+
|
|
749
|
+
Installation:
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
npx shadcn@latest add sheet
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Basic sheet:
|
|
756
|
+
|
|
757
|
+
```tsx
|
|
758
|
+
import { Button } from "@/components/ui/button"
|
|
759
|
+
import {
|
|
760
|
+
Sheet,
|
|
761
|
+
SheetContent,
|
|
762
|
+
SheetDescription,
|
|
763
|
+
SheetHeader,
|
|
764
|
+
SheetTitle,
|
|
765
|
+
SheetTrigger,
|
|
766
|
+
} from "@/components/ui/sheet"
|
|
767
|
+
|
|
768
|
+
export function SheetDemo() {
|
|
769
|
+
return (
|
|
770
|
+
<Sheet>
|
|
771
|
+
<SheetTrigger asChild>
|
|
772
|
+
<Button variant="outline">Open Sheet</Button>
|
|
773
|
+
</SheetTrigger>
|
|
774
|
+
<SheetContent>
|
|
775
|
+
<SheetHeader>
|
|
776
|
+
<SheetTitle>Edit profile</SheetTitle>
|
|
777
|
+
<SheetDescription>
|
|
778
|
+
Make changes to your profile here. Click save when you're done.
|
|
779
|
+
</SheetDescription>
|
|
780
|
+
</SheetHeader>
|
|
781
|
+
<div className="grid gap-4 py-4">
|
|
782
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
783
|
+
<Label htmlFor="name" className="text-right">
|
|
784
|
+
Name
|
|
785
|
+
</Label>
|
|
786
|
+
<Input id="name" value="Pedro Duarte" className="col-span-3" />
|
|
787
|
+
</div>
|
|
788
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
789
|
+
<Label htmlFor="username" className="text-right">
|
|
790
|
+
Username
|
|
791
|
+
</Label>
|
|
792
|
+
<Input id="username" value="@peduarte" className="col-span-3" />
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
</SheetContent>
|
|
796
|
+
</Sheet>
|
|
797
|
+
)
|
|
798
|
+
}
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
Sheet with side placement:
|
|
802
|
+
|
|
803
|
+
```tsx
|
|
804
|
+
<Sheet>
|
|
805
|
+
<SheetTrigger asChild>
|
|
806
|
+
<Button variant="outline">Open Right Sheet</Button>
|
|
807
|
+
</SheetTrigger>
|
|
808
|
+
<SheetContent side="right">
|
|
809
|
+
<SheetHeader>
|
|
810
|
+
<SheetTitle>Settings</SheetTitle>
|
|
811
|
+
<SheetDescription>
|
|
812
|
+
Configure your application settings here.
|
|
813
|
+
</SheetDescription>
|
|
814
|
+
</SheetHeader>
|
|
815
|
+
{/* Settings content */}
|
|
816
|
+
</SheetContent>
|
|
817
|
+
</Sheet>
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### Menubar & Navigation
|
|
821
|
+
|
|
822
|
+
#### Menubar Component
|
|
823
|
+
|
|
824
|
+
Installation:
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
npx shadcn@latest add menubar
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
Basic menubar:
|
|
831
|
+
|
|
832
|
+
```tsx
|
|
833
|
+
import {
|
|
834
|
+
Menubar,
|
|
835
|
+
MenubarContent,
|
|
836
|
+
MenubarItem,
|
|
837
|
+
MenubarMenu,
|
|
838
|
+
MenubarSeparator,
|
|
839
|
+
MenubarShortcut,
|
|
840
|
+
MenubarSub,
|
|
841
|
+
MenubarSubContent,
|
|
842
|
+
MenubarSubTrigger,
|
|
843
|
+
MenubarTrigger,
|
|
844
|
+
} from "@/components/ui/menubar"
|
|
845
|
+
|
|
846
|
+
export function MenubarDemo() {
|
|
847
|
+
return (
|
|
848
|
+
<Menubar>
|
|
849
|
+
<MenubarMenu>
|
|
850
|
+
<MenubarTrigger>File</MenubarTrigger>
|
|
851
|
+
<MenubarContent>
|
|
852
|
+
<MenubarItem>
|
|
853
|
+
New Tab <MenubarShortcut>⌘T</MenubarShortcut>
|
|
854
|
+
</MenubarItem>
|
|
855
|
+
<MenubarItem>
|
|
856
|
+
New Window <MenubarShortcut>⌘N</MenubarShortcut>
|
|
857
|
+
</MenubarItem>
|
|
858
|
+
<MenubarSeparator />
|
|
859
|
+
<MenubarItem>Share</MenubarItem>
|
|
860
|
+
<MenubarSeparator />
|
|
861
|
+
<MenubarItem>Print</MenubarItem>
|
|
862
|
+
</MenubarContent>
|
|
863
|
+
</MenubarMenu>
|
|
864
|
+
<MenubarMenu>
|
|
865
|
+
<MenubarTrigger>Edit</MenubarTrigger>
|
|
866
|
+
<MenubarContent>
|
|
867
|
+
<MenubarItem>
|
|
868
|
+
Undo <MenubarShortcut>⌘Z</MenubarShortcut>
|
|
869
|
+
</MenubarItem>
|
|
870
|
+
<MenubarItem>
|
|
871
|
+
Redo <MenubarShortcut>⌘Y</MenubarShortcut>
|
|
872
|
+
</MenubarItem>
|
|
873
|
+
<MenubarSeparator />
|
|
874
|
+
<MenubarSub>
|
|
875
|
+
<MenubarSubTrigger>Find</MenubarSubTrigger>
|
|
876
|
+
<MenubarSubContent>
|
|
877
|
+
<MenubarItem>Search the web</MenubarItem>
|
|
878
|
+
<MenubarItem>Find...</MenubarItem>
|
|
879
|
+
<MenubarItem>Find Next</MenubarItem>
|
|
880
|
+
<MenubarItem>Find Previous</MenubarItem>
|
|
881
|
+
</MenubarSubContent>
|
|
882
|
+
</MenubarSub>
|
|
883
|
+
</MenubarContent>
|
|
884
|
+
</MenubarMenu>
|
|
885
|
+
</Menubar>
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
### Select (Dropdown) Component
|
|
891
|
+
|
|
892
|
+
Installation:
|
|
893
|
+
|
|
894
|
+
```bash
|
|
895
|
+
npx shadcn@latest add select
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
Basic select:
|
|
899
|
+
|
|
900
|
+
```tsx
|
|
901
|
+
import {
|
|
902
|
+
Select,
|
|
903
|
+
SelectContent,
|
|
904
|
+
SelectItem,
|
|
905
|
+
SelectTrigger,
|
|
906
|
+
SelectValue,
|
|
907
|
+
} from "@/components/ui/select"
|
|
908
|
+
|
|
909
|
+
export function SelectDemo() {
|
|
910
|
+
return (
|
|
911
|
+
<Select>
|
|
912
|
+
<SelectTrigger className="w-[180px]">
|
|
913
|
+
<SelectValue placeholder="Select a fruit" />
|
|
914
|
+
</SelectTrigger>
|
|
915
|
+
<SelectContent>
|
|
916
|
+
<SelectItem value="apple">Apple</SelectItem>
|
|
917
|
+
<SelectItem value="banana">Banana</SelectItem>
|
|
918
|
+
<SelectItem value="orange">Orange</SelectItem>
|
|
919
|
+
</SelectContent>
|
|
920
|
+
</Select>
|
|
921
|
+
)
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
Select in form:
|
|
926
|
+
|
|
927
|
+
```tsx
|
|
928
|
+
<FormField
|
|
929
|
+
control={form.control}
|
|
930
|
+
name="role"
|
|
931
|
+
render={({ field }) => (
|
|
932
|
+
<FormItem>
|
|
933
|
+
<FormLabel>Role</FormLabel>
|
|
934
|
+
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
935
|
+
<FormControl>
|
|
936
|
+
<SelectTrigger>
|
|
937
|
+
<SelectValue placeholder="Select a role" />
|
|
938
|
+
</SelectTrigger>
|
|
939
|
+
</FormControl>
|
|
940
|
+
<SelectContent>
|
|
941
|
+
<SelectItem value="admin">Admin</SelectItem>
|
|
942
|
+
<SelectItem value="user">User</SelectItem>
|
|
943
|
+
<SelectItem value="guest">Guest</SelectItem>
|
|
944
|
+
</SelectContent>
|
|
945
|
+
</Select>
|
|
946
|
+
<FormMessage />
|
|
947
|
+
</FormItem>
|
|
948
|
+
)}
|
|
949
|
+
/>
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
### Toast Notifications
|
|
953
|
+
|
|
954
|
+
Installation:
|
|
955
|
+
|
|
956
|
+
```bash
|
|
957
|
+
npx shadcn@latest add toast
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
Setup toast provider in root layout:
|
|
961
|
+
|
|
962
|
+
```tsx
|
|
963
|
+
import { Toaster } from "@/components/ui/toaster"
|
|
964
|
+
|
|
965
|
+
export default function RootLayout({ children }) {
|
|
966
|
+
return (
|
|
967
|
+
<html lang="en">
|
|
968
|
+
<body>
|
|
969
|
+
{children}
|
|
970
|
+
<Toaster />
|
|
971
|
+
</body>
|
|
972
|
+
</html>
|
|
973
|
+
)
|
|
974
|
+
}
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
Using toast:
|
|
978
|
+
|
|
979
|
+
```tsx
|
|
980
|
+
import { useToast } from "@/components/ui/use-toast"
|
|
981
|
+
import { Button } from "@/components/ui/button"
|
|
982
|
+
|
|
983
|
+
export function ToastDemo() {
|
|
984
|
+
const { toast } = useToast()
|
|
985
|
+
|
|
986
|
+
return (
|
|
987
|
+
<Button
|
|
988
|
+
onClick={() => {
|
|
989
|
+
toast({
|
|
990
|
+
title: "Scheduled: Catch up",
|
|
991
|
+
description: "Friday, February 10, 2023 at 5:57 PM",
|
|
992
|
+
})
|
|
993
|
+
}}
|
|
994
|
+
>
|
|
995
|
+
Show Toast
|
|
996
|
+
</Button>
|
|
997
|
+
)
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
Toast variants:
|
|
1002
|
+
|
|
1003
|
+
```tsx
|
|
1004
|
+
// Success
|
|
1005
|
+
toast({
|
|
1006
|
+
title: "Success",
|
|
1007
|
+
description: "Your changes have been saved.",
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
// Error
|
|
1011
|
+
toast({
|
|
1012
|
+
variant: "destructive",
|
|
1013
|
+
title: "Error",
|
|
1014
|
+
description: "Something went wrong.",
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
// With action
|
|
1018
|
+
toast({
|
|
1019
|
+
title: "Uh oh! Something went wrong.",
|
|
1020
|
+
description: "There was a problem with your request.",
|
|
1021
|
+
action: <ToastAction altText="Try again">Try again</ToastAction>,
|
|
1022
|
+
})
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
### Table Component
|
|
1026
|
+
|
|
1027
|
+
Installation:
|
|
1028
|
+
|
|
1029
|
+
```bash
|
|
1030
|
+
npx shadcn@latest add table
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
Basic table:
|
|
1034
|
+
|
|
1035
|
+
```tsx
|
|
1036
|
+
import {
|
|
1037
|
+
Table,
|
|
1038
|
+
TableBody,
|
|
1039
|
+
TableCaption,
|
|
1040
|
+
TableCell,
|
|
1041
|
+
TableHead,
|
|
1042
|
+
TableHeader,
|
|
1043
|
+
TableRow,
|
|
1044
|
+
} from "@/components/ui/table"
|
|
1045
|
+
|
|
1046
|
+
const invoices = [
|
|
1047
|
+
{ invoice: "INV001", status: "Paid", method: "Credit Card", amount: "$250.00" },
|
|
1048
|
+
{ invoice: "INV002", status: "Pending", method: "PayPal", amount: "$150.00" },
|
|
1049
|
+
]
|
|
1050
|
+
|
|
1051
|
+
export function TableDemo() {
|
|
1052
|
+
return (
|
|
1053
|
+
<Table>
|
|
1054
|
+
<TableCaption>A list of your recent invoices.</TableCaption>
|
|
1055
|
+
<TableHeader>
|
|
1056
|
+
<TableRow>
|
|
1057
|
+
<TableHead>Invoice</TableHead>
|
|
1058
|
+
<TableHead>Status</TableHead>
|
|
1059
|
+
<TableHead>Method</TableHead>
|
|
1060
|
+
<TableHead className="text-right">Amount</TableHead>
|
|
1061
|
+
</TableRow>
|
|
1062
|
+
</TableHeader>
|
|
1063
|
+
<TableBody>
|
|
1064
|
+
{invoices.map((invoice) => (
|
|
1065
|
+
<TableRow key={invoice.invoice}>
|
|
1066
|
+
<TableCell className="font-medium">{invoice.invoice}</TableCell>
|
|
1067
|
+
<TableCell>{invoice.status}</TableCell>
|
|
1068
|
+
<TableCell>{invoice.method}</TableCell>
|
|
1069
|
+
<TableCell className="text-right">{invoice.amount}</TableCell>
|
|
1070
|
+
</TableRow>
|
|
1071
|
+
))}
|
|
1072
|
+
</TableBody>
|
|
1073
|
+
</Table>
|
|
1074
|
+
)
|
|
1075
|
+
}
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
### Charts Component
|
|
1079
|
+
|
|
1080
|
+
Installation:
|
|
1081
|
+
|
|
1082
|
+
```bash
|
|
1083
|
+
npx shadcn@latest add chart
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
The charts component in shadcn/ui is built on **Recharts** - providing direct access to all Recharts capabilities with consistent theming and styling.
|
|
1087
|
+
|
|
1088
|
+
#### ChartContainer and ChartConfig
|
|
1089
|
+
|
|
1090
|
+
The `ChartContainer` wraps your Recharts component and accepts a `config` prop for theming:
|
|
1091
|
+
|
|
1092
|
+
```tsx
|
|
1093
|
+
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
|
1094
|
+
import { ChartContainer, ChartTooltipContent } from "@/components/ui/chart"
|
|
1095
|
+
|
|
1096
|
+
const chartConfig = {
|
|
1097
|
+
desktop: {
|
|
1098
|
+
label: "Desktop",
|
|
1099
|
+
color: "var(--chart-1)",
|
|
1100
|
+
},
|
|
1101
|
+
mobile: {
|
|
1102
|
+
label: "Mobile",
|
|
1103
|
+
color: "var(--chart-2)",
|
|
1104
|
+
},
|
|
1105
|
+
} satisfies import("@/components/ui/chart").ChartConfig
|
|
1106
|
+
|
|
1107
|
+
const chartData = [
|
|
1108
|
+
{ month: "January", desktop: 186, mobile: 80 },
|
|
1109
|
+
{ month: "February", desktop: 305, mobile: 200 },
|
|
1110
|
+
{ month: "March", desktop: 237, mobile: 120 },
|
|
1111
|
+
]
|
|
1112
|
+
|
|
1113
|
+
export function BarChartDemo() {
|
|
1114
|
+
return (
|
|
1115
|
+
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
|
|
1116
|
+
<BarChart data={chartData}>
|
|
1117
|
+
<CartesianGrid vertical={false} />
|
|
1118
|
+
<XAxis
|
|
1119
|
+
dataKey="month"
|
|
1120
|
+
tickLine={false}
|
|
1121
|
+
axisLine={false}
|
|
1122
|
+
tickFormatter={(value) => value.slice(0, 3)}
|
|
1123
|
+
/>
|
|
1124
|
+
<Bar
|
|
1125
|
+
dataKey="desktop"
|
|
1126
|
+
fill="var(--color-desktop)"
|
|
1127
|
+
radius={4}
|
|
1128
|
+
/>
|
|
1129
|
+
<Bar
|
|
1130
|
+
dataKey="mobile"
|
|
1131
|
+
fill="var(--color-mobile)"
|
|
1132
|
+
radius={4}
|
|
1133
|
+
/>
|
|
1134
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
1135
|
+
</BarChart>
|
|
1136
|
+
</ChartContainer>
|
|
1137
|
+
)
|
|
1138
|
+
}
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
#### ChartConfig with Custom Colors
|
|
1142
|
+
|
|
1143
|
+
You can define custom colors directly in the configuration:
|
|
1144
|
+
|
|
1145
|
+
```tsx
|
|
1146
|
+
const chartConfig = {
|
|
1147
|
+
visitors: {
|
|
1148
|
+
label: "Visitors",
|
|
1149
|
+
color: "#2563eb", // Custom hex color
|
|
1150
|
+
theme: {
|
|
1151
|
+
light: "#2563eb",
|
|
1152
|
+
dark: "#60a5fa",
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
sales: {
|
|
1156
|
+
label: "Sales",
|
|
1157
|
+
color: "var(--chart-1)", // CSS variable
|
|
1158
|
+
theme: {
|
|
1159
|
+
light: "oklch(0.646 0.222 41.116)",
|
|
1160
|
+
dark: "oklch(0.696 0.182 281.41)",
|
|
1161
|
+
},
|
|
1162
|
+
},
|
|
1163
|
+
} satisfies import("@/components/ui/chart").ChartConfig
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
#### CSS Variables for Charts
|
|
1167
|
+
|
|
1168
|
+
Add chart color variables to your `globals.css`:
|
|
1169
|
+
|
|
1170
|
+
```css
|
|
1171
|
+
@layer base {
|
|
1172
|
+
:root {
|
|
1173
|
+
/* Chart colors */
|
|
1174
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
1175
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
1176
|
+
--chart-3: oklch(0.546 0.198 38.228);
|
|
1177
|
+
--chart-4: oklch(0.596 0.151 343.253);
|
|
1178
|
+
--chart-5: oklch(0.546 0.158 49.157);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
.dark {
|
|
1182
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
1183
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
1184
|
+
--chart-3: oklch(0.698 0.141 24.311);
|
|
1185
|
+
--chart-4: oklch(0.676 0.172 171.196);
|
|
1186
|
+
--chart-5: oklch(0.578 0.192 302.85);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
#### Line Chart Example
|
|
1192
|
+
|
|
1193
|
+
```tsx
|
|
1194
|
+
import { Line, LineChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
|
1195
|
+
import { ChartContainer, ChartTooltipContent } from "@/components/ui/chart"
|
|
1196
|
+
|
|
1197
|
+
const chartConfig = {
|
|
1198
|
+
price: {
|
|
1199
|
+
label: "Price",
|
|
1200
|
+
color: "var(--chart-1)",
|
|
1201
|
+
},
|
|
1202
|
+
} satisfies import("@/components/ui/chart").ChartConfig
|
|
1203
|
+
|
|
1204
|
+
const chartData = [
|
|
1205
|
+
{ month: "January", price: 186 },
|
|
1206
|
+
{ month: "February", price: 305 },
|
|
1207
|
+
{ month: "March", price: 237 },
|
|
1208
|
+
{ month: "April", price: 203 },
|
|
1209
|
+
{ month: "May", price: 276 },
|
|
1210
|
+
]
|
|
1211
|
+
|
|
1212
|
+
export function LineChartDemo() {
|
|
1213
|
+
return (
|
|
1214
|
+
<ChartContainer config={chartConfig} className="min-h-[200px]">
|
|
1215
|
+
<LineChart data={chartData}>
|
|
1216
|
+
<CartesianGrid vertical={false} />
|
|
1217
|
+
<XAxis dataKey="month" tickLine={false} axisLine={false} />
|
|
1218
|
+
<YAxis tickLine={false} axisLine={false} tickFormatter={(value) => `$${value}`} />
|
|
1219
|
+
<Line
|
|
1220
|
+
dataKey="price"
|
|
1221
|
+
stroke="var(--color-price)"
|
|
1222
|
+
strokeWidth={2}
|
|
1223
|
+
dot={false}
|
|
1224
|
+
/>
|
|
1225
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
1226
|
+
</LineChart>
|
|
1227
|
+
</ChartContainer>
|
|
1228
|
+
)
|
|
1229
|
+
}
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
#### Area Chart Example
|
|
1233
|
+
|
|
1234
|
+
```tsx
|
|
1235
|
+
import { Area, AreaChart, XAxis, YAxis } from "recharts"
|
|
1236
|
+
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltipContent } from "@/components/ui/chart"
|
|
1237
|
+
|
|
1238
|
+
const chartConfig = {
|
|
1239
|
+
desktop: { label: "Desktop", color: "var(--chart-1)" },
|
|
1240
|
+
mobile: { label: "Mobile", color: "var(--chart-2)" },
|
|
1241
|
+
} satisfies import("@/components/ui/chart").ChartConfig
|
|
1242
|
+
|
|
1243
|
+
export function AreaChartDemo() {
|
|
1244
|
+
return (
|
|
1245
|
+
<ChartContainer config={chartConfig} className="min-h-[200px]">
|
|
1246
|
+
<AreaChart data={chartData}>
|
|
1247
|
+
<XAxis dataKey="month" tickLine={false} axisLine={false} />
|
|
1248
|
+
<YAxis tickLine={false} axisLine={false} />
|
|
1249
|
+
<Area
|
|
1250
|
+
dataKey="desktop"
|
|
1251
|
+
fill="var(--color-desktop)"
|
|
1252
|
+
stroke="var(--color-desktop)"
|
|
1253
|
+
fillOpacity={0.3}
|
|
1254
|
+
/>
|
|
1255
|
+
<Area
|
|
1256
|
+
dataKey="mobile"
|
|
1257
|
+
fill="var(--color-mobile)"
|
|
1258
|
+
stroke="var(--color-mobile)"
|
|
1259
|
+
fillOpacity={0.3}
|
|
1260
|
+
/>
|
|
1261
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
1262
|
+
<ChartLegend content={<ChartLegendContent />} />
|
|
1263
|
+
</AreaChart>
|
|
1264
|
+
</ChartContainer>
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
#### Pie Chart Example
|
|
1270
|
+
|
|
1271
|
+
```tsx
|
|
1272
|
+
import { Pie, PieChart } from "recharts"
|
|
1273
|
+
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltipContent } from "@/components/ui/chart"
|
|
1274
|
+
|
|
1275
|
+
const chartConfig = {
|
|
1276
|
+
chrome: { label: "Chrome", color: "var(--chart-1)" },
|
|
1277
|
+
safari: { label: "Safari", color: "var(--chart-2)" },
|
|
1278
|
+
firefox: { label: "Firefox", color: "var(--chart-3)" },
|
|
1279
|
+
} satisfies import("@/components/ui/chart").ChartConfig
|
|
1280
|
+
|
|
1281
|
+
const pieData = [
|
|
1282
|
+
{ browser: "Chrome", visitors: 275, fill: "var(--color-chrome)" },
|
|
1283
|
+
{ browser: "Safari", visitors: 200, fill: "var(--color-safari)" },
|
|
1284
|
+
{ browser: "Firefox", visitors: 187, fill: "var(--color-firefox)" },
|
|
1285
|
+
]
|
|
1286
|
+
|
|
1287
|
+
export function PieChartDemo() {
|
|
1288
|
+
return (
|
|
1289
|
+
<ChartContainer config={chartConfig} className="min-h-[200px]">
|
|
1290
|
+
<PieChart>
|
|
1291
|
+
<Pie
|
|
1292
|
+
data={pieData}
|
|
1293
|
+
dataKey="visitors"
|
|
1294
|
+
nameKey="browser"
|
|
1295
|
+
cx="50%"
|
|
1296
|
+
cy="50%"
|
|
1297
|
+
outerRadius={80}
|
|
1298
|
+
/>
|
|
1299
|
+
<ChartTooltip content={<ChartTooltipContent />} />
|
|
1300
|
+
<ChartLegend content={<ChartLegendContent />} />
|
|
1301
|
+
</PieChart>
|
|
1302
|
+
</ChartContainer>
|
|
1303
|
+
)
|
|
1304
|
+
}
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
#### ChartTooltipContent Props
|
|
1308
|
+
|
|
1309
|
+
| Prop | Type | Default | Description |
|
|
1310
|
+
|------|------|---------|-------------|
|
|
1311
|
+
| `labelKey` | string | "label" | Key for tooltip label |
|
|
1312
|
+
| `nameKey` | string | "name" | Key for tooltip name |
|
|
1313
|
+
| `indicator` | "dot" \| "line" \| "dashed" | "dot" | Indicator style |
|
|
1314
|
+
| `hideLabel` | boolean | false | Hide label |
|
|
1315
|
+
| `hideIndicator` | boolean | false | Hide indicator |
|
|
1316
|
+
|
|
1317
|
+
#### Accessibility
|
|
1318
|
+
|
|
1319
|
+
Enable keyboard navigation and screen reader support:
|
|
1320
|
+
|
|
1321
|
+
```tsx
|
|
1322
|
+
<BarChart accessibilityLayer data={chartData}>...</BarChart>
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
This adds:
|
|
1326
|
+
- Keyboard arrow key navigation
|
|
1327
|
+
- ARIA labels for chart elements
|
|
1328
|
+
- Screen reader announcements for data values
|
|
1329
|
+
|
|
1330
|
+
## Customization
|
|
1331
|
+
|
|
1332
|
+
### Theming with CSS Variables
|
|
1333
|
+
|
|
1334
|
+
shadcn/ui uses CSS variables for theming. Configure in `globals.css`:
|
|
1335
|
+
|
|
1336
|
+
```css
|
|
1337
|
+
@layer base {
|
|
1338
|
+
:root {
|
|
1339
|
+
--background: 0 0% 100%;
|
|
1340
|
+
--foreground: 222.2 84% 4.9%;
|
|
1341
|
+
--primary: 222.2 47.4% 11.2%;
|
|
1342
|
+
--primary-foreground: 210 40% 98%;
|
|
1343
|
+
--secondary: 210 40% 96.1%;
|
|
1344
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
1345
|
+
--muted: 210 40% 96.1%;
|
|
1346
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
1347
|
+
--accent: 210 40% 96.1%;
|
|
1348
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
1349
|
+
--destructive: 0 84.2% 60.2%;
|
|
1350
|
+
--destructive-foreground: 210 40% 98%;
|
|
1351
|
+
--border: 214.3 31.8% 91.4%;
|
|
1352
|
+
--input: 214.3 31.8% 91.4%;
|
|
1353
|
+
--ring: 222.2 84% 4.9%;
|
|
1354
|
+
--radius: 0.5rem;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
.dark {
|
|
1358
|
+
--background: 222.2 84% 4.9%;
|
|
1359
|
+
--foreground: 210 40% 98%;
|
|
1360
|
+
--primary: 210 40% 98%;
|
|
1361
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
1362
|
+
/* ... other dark mode variables */
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
### Customizing Components
|
|
1368
|
+
|
|
1369
|
+
Since you own the code, customize directly:
|
|
1370
|
+
|
|
1371
|
+
```tsx
|
|
1372
|
+
// components/ui/button.tsx
|
|
1373
|
+
import * as React from "react"
|
|
1374
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
1375
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
1376
|
+
import { cn } from "@/lib/utils"
|
|
1377
|
+
|
|
1378
|
+
const buttonVariants = cva(
|
|
1379
|
+
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
|
|
1380
|
+
{
|
|
1381
|
+
variants: {
|
|
1382
|
+
variant: {
|
|
1383
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
1384
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
1385
|
+
outline: "border border-input bg-background hover:bg-accent",
|
|
1386
|
+
// Add custom variant
|
|
1387
|
+
custom: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
|
|
1388
|
+
},
|
|
1389
|
+
size: {
|
|
1390
|
+
default: "h-10 px-4 py-2",
|
|
1391
|
+
sm: "h-9 rounded-md px-3",
|
|
1392
|
+
lg: "h-11 rounded-md px-8",
|
|
1393
|
+
// Add custom size
|
|
1394
|
+
xl: "h-14 rounded-md px-10 text-lg",
|
|
1395
|
+
},
|
|
1396
|
+
},
|
|
1397
|
+
defaultVariants: {
|
|
1398
|
+
variant: "default",
|
|
1399
|
+
size: "default",
|
|
1400
|
+
},
|
|
1401
|
+
}
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
export interface ButtonProps
|
|
1405
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
1406
|
+
VariantProps<typeof buttonVariants> {
|
|
1407
|
+
asChild?: boolean
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
1411
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
1412
|
+
const Comp = asChild ? Slot : "button"
|
|
1413
|
+
return (
|
|
1414
|
+
<Comp
|
|
1415
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
1416
|
+
ref={ref}
|
|
1417
|
+
{...props}
|
|
1418
|
+
/>
|
|
1419
|
+
)
|
|
1420
|
+
}
|
|
1421
|
+
)
|
|
1422
|
+
Button.displayName = "Button"
|
|
1423
|
+
|
|
1424
|
+
export { Button, buttonVariants }
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
## Next.js Integration
|
|
1428
|
+
|
|
1429
|
+
### App Router Setup
|
|
1430
|
+
|
|
1431
|
+
For Next.js 13+ with App Router, ensure components use `"use client"` directive:
|
|
1432
|
+
|
|
1433
|
+
```tsx
|
|
1434
|
+
// src/components/ui/button.tsx
|
|
1435
|
+
"use client"
|
|
1436
|
+
|
|
1437
|
+
import * as React from "react"
|
|
1438
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
1439
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
1440
|
+
import { cn } from "@/lib/utils"
|
|
1441
|
+
|
|
1442
|
+
// ... rest of component
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
### Layout Integration
|
|
1446
|
+
|
|
1447
|
+
Add the Toaster to your root layout:
|
|
1448
|
+
|
|
1449
|
+
```tsx
|
|
1450
|
+
// app/layout.tsx
|
|
1451
|
+
import { Toaster } from "@/components/ui/toaster"
|
|
1452
|
+
import "./globals.css"
|
|
1453
|
+
|
|
1454
|
+
export default function RootLayout({
|
|
1455
|
+
children,
|
|
1456
|
+
}: {
|
|
1457
|
+
children: React.ReactNode
|
|
1458
|
+
}) {
|
|
1459
|
+
return (
|
|
1460
|
+
<html lang="en" suppressHydrationWarning>
|
|
1461
|
+
<body className="min-h-screen bg-background font-sans antialiased">
|
|
1462
|
+
{children}
|
|
1463
|
+
<Toaster />
|
|
1464
|
+
</body>
|
|
1465
|
+
</html>
|
|
1466
|
+
)
|
|
1467
|
+
}
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
### Server Components
|
|
1471
|
+
|
|
1472
|
+
When using shadcn/ui components in Server Components, wrap them in a Client Component:
|
|
1473
|
+
|
|
1474
|
+
```tsx
|
|
1475
|
+
// app/dashboard/page.tsx
|
|
1476
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
1477
|
+
import { ButtonClient } from "@/components/ui/button-client"
|
|
1478
|
+
|
|
1479
|
+
export default function DashboardPage() {
|
|
1480
|
+
return (
|
|
1481
|
+
<div className="container mx-auto p-6">
|
|
1482
|
+
<Card>
|
|
1483
|
+
<CardHeader>
|
|
1484
|
+
<CardTitle>Dashboard</CardTitle>
|
|
1485
|
+
</CardHeader>
|
|
1486
|
+
<CardContent>
|
|
1487
|
+
<ButtonClient>Interactive Button</ButtonClient>
|
|
1488
|
+
</CardContent>
|
|
1489
|
+
</Card>
|
|
1490
|
+
</div>
|
|
1491
|
+
)
|
|
1492
|
+
}
|
|
1493
|
+
```
|
|
1494
|
+
|
|
1495
|
+
```tsx
|
|
1496
|
+
// src/components/ui/button-client.tsx
|
|
1497
|
+
"use client"
|
|
1498
|
+
|
|
1499
|
+
import { Button } from "./button"
|
|
1500
|
+
|
|
1501
|
+
export function ButtonClient(props: React.ComponentProps<typeof Button>) {
|
|
1502
|
+
return <Button {...props} />
|
|
1503
|
+
}
|
|
1504
|
+
```
|
|
1505
|
+
|
|
1506
|
+
### Route Handlers with Forms
|
|
1507
|
+
|
|
1508
|
+
Create API routes for form submissions:
|
|
1509
|
+
|
|
1510
|
+
```tsx
|
|
1511
|
+
// app/api/contact/route.ts
|
|
1512
|
+
import { NextRequest, NextResponse } from "next/server"
|
|
1513
|
+
import { z } from "zod"
|
|
1514
|
+
|
|
1515
|
+
const contactSchema = z.object({
|
|
1516
|
+
name: z.string().min(2),
|
|
1517
|
+
email: z.string().email(),
|
|
1518
|
+
message: z.string().min(10),
|
|
1519
|
+
})
|
|
1520
|
+
|
|
1521
|
+
export async function POST(request: NextRequest) {
|
|
1522
|
+
try {
|
|
1523
|
+
const body = await request.json()
|
|
1524
|
+
const validated = contactSchema.parse(body)
|
|
1525
|
+
|
|
1526
|
+
// Process form data
|
|
1527
|
+
console.log("Form submission:", validated)
|
|
1528
|
+
|
|
1529
|
+
return NextResponse.json({ success: true })
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
if (error instanceof z.ZodError) {
|
|
1532
|
+
return NextResponse.json(
|
|
1533
|
+
{ errors: error.errors },
|
|
1534
|
+
{ status: 400 }
|
|
1535
|
+
)
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
return NextResponse.json(
|
|
1539
|
+
{ error: "Internal server error" },
|
|
1540
|
+
{ status: 500 }
|
|
1541
|
+
)
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
```
|
|
1545
|
+
|
|
1546
|
+
### Form with Server Action
|
|
1547
|
+
|
|
1548
|
+
Using Next.js 14+ Server Actions:
|
|
1549
|
+
|
|
1550
|
+
```tsx
|
|
1551
|
+
// app/contact/page.tsx
|
|
1552
|
+
"use client"
|
|
1553
|
+
|
|
1554
|
+
import { zodResolver } from "@hookform/resolvers/zod"
|
|
1555
|
+
import { useForm } from "react-hook-form"
|
|
1556
|
+
import * as z from "zod"
|
|
1557
|
+
import { Button } from "@/components/ui/button"
|
|
1558
|
+
import {
|
|
1559
|
+
Form,
|
|
1560
|
+
FormControl,
|
|
1561
|
+
FormField,
|
|
1562
|
+
FormItem,
|
|
1563
|
+
FormLabel,
|
|
1564
|
+
FormMessage,
|
|
1565
|
+
} from "@/components/ui/form"
|
|
1566
|
+
import { Input } from "@/components/ui/input"
|
|
1567
|
+
import { Textarea } from "@/components/ui/textarea"
|
|
1568
|
+
import { toast } from "@/components/ui/use-toast"
|
|
1569
|
+
|
|
1570
|
+
const formSchema = z.object({
|
|
1571
|
+
name: z.string().min(2),
|
|
1572
|
+
email: z.string().email(),
|
|
1573
|
+
message: z.string().min(10),
|
|
1574
|
+
})
|
|
1575
|
+
|
|
1576
|
+
async function onSubmit(values: z.infer<typeof formSchema>) {
|
|
1577
|
+
try {
|
|
1578
|
+
const response = await fetch("/api/contact", {
|
|
1579
|
+
method: "POST",
|
|
1580
|
+
headers: { "Content-Type": "application/json" },
|
|
1581
|
+
body: JSON.stringify(values),
|
|
1582
|
+
})
|
|
1583
|
+
|
|
1584
|
+
if (!response.ok) throw new Error("Failed to submit")
|
|
1585
|
+
|
|
1586
|
+
toast({
|
|
1587
|
+
title: "Success!",
|
|
1588
|
+
description: "Your message has been sent.",
|
|
1589
|
+
})
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
toast({
|
|
1592
|
+
variant: "destructive",
|
|
1593
|
+
title: "Error",
|
|
1594
|
+
description: "Failed to send message. Please try again.",
|
|
1595
|
+
})
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
export default function ContactPage() {
|
|
1600
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
1601
|
+
resolver: zodResolver(formSchema),
|
|
1602
|
+
})
|
|
1603
|
+
|
|
1604
|
+
return (
|
|
1605
|
+
<div className="container mx-auto max-w-2xl py-8">
|
|
1606
|
+
<Form {...form}>
|
|
1607
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
1608
|
+
<FormField
|
|
1609
|
+
control={form.control}
|
|
1610
|
+
name="name"
|
|
1611
|
+
render={({ field }) => (
|
|
1612
|
+
<FormItem>
|
|
1613
|
+
<FormLabel>Name</FormLabel>
|
|
1614
|
+
<FormControl>
|
|
1615
|
+
<Input placeholder="Your name" {...field} />
|
|
1616
|
+
</FormControl>
|
|
1617
|
+
<FormMessage />
|
|
1618
|
+
</FormItem>
|
|
1619
|
+
)}
|
|
1620
|
+
/>
|
|
1621
|
+
|
|
1622
|
+
<FormField
|
|
1623
|
+
control={form.control}
|
|
1624
|
+
name="email"
|
|
1625
|
+
render={({ field }) => (
|
|
1626
|
+
<FormItem>
|
|
1627
|
+
<FormLabel>Email</FormLabel>
|
|
1628
|
+
<FormControl>
|
|
1629
|
+
<Input type="email" placeholder="your@email.com" {...field} />
|
|
1630
|
+
</FormControl>
|
|
1631
|
+
<FormMessage />
|
|
1632
|
+
</FormItem>
|
|
1633
|
+
)}
|
|
1634
|
+
/>
|
|
1635
|
+
|
|
1636
|
+
<FormField
|
|
1637
|
+
control={form.control}
|
|
1638
|
+
name="message"
|
|
1639
|
+
render={({ field }) => (
|
|
1640
|
+
<FormItem>
|
|
1641
|
+
<FormLabel>Message</FormLabel>
|
|
1642
|
+
<FormControl>
|
|
1643
|
+
<Textarea
|
|
1644
|
+
placeholder="Your message..."
|
|
1645
|
+
className="resize-none"
|
|
1646
|
+
{...field}
|
|
1647
|
+
/>
|
|
1648
|
+
</FormControl>
|
|
1649
|
+
<FormMessage />
|
|
1650
|
+
</FormItem>
|
|
1651
|
+
)}
|
|
1652
|
+
/>
|
|
1653
|
+
|
|
1654
|
+
<Button type="submit" className="w-full">
|
|
1655
|
+
Send Message
|
|
1656
|
+
</Button>
|
|
1657
|
+
</form>
|
|
1658
|
+
</Form>
|
|
1659
|
+
</div>
|
|
1660
|
+
)
|
|
1661
|
+
}
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
### Metadata with shadcn/ui
|
|
1665
|
+
|
|
1666
|
+
Using shadcn/ui components in metadata:
|
|
1667
|
+
|
|
1668
|
+
```tsx
|
|
1669
|
+
// app/layout.tsx
|
|
1670
|
+
import { Metadata } from "next"
|
|
1671
|
+
|
|
1672
|
+
export const metadata: Metadata = {
|
|
1673
|
+
title: {
|
|
1674
|
+
default: "My App",
|
|
1675
|
+
template: "%s | My App",
|
|
1676
|
+
},
|
|
1677
|
+
description: "Built with shadcn/ui and Next.js",
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// app/about/page.tsx
|
|
1681
|
+
import { Metadata } from "next"
|
|
1682
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
1683
|
+
|
|
1684
|
+
export const metadata: Metadata = {
|
|
1685
|
+
title: "About Us",
|
|
1686
|
+
description: "Learn more about our company",
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
export default function AboutPage() {
|
|
1690
|
+
return (
|
|
1691
|
+
<div className="container mx-auto py-8">
|
|
1692
|
+
<Card>
|
|
1693
|
+
<CardHeader>
|
|
1694
|
+
<CardTitle>About Our Company</CardTitle>
|
|
1695
|
+
</CardHeader>
|
|
1696
|
+
<CardContent>
|
|
1697
|
+
<p>We build amazing products with modern web technologies.</p>
|
|
1698
|
+
</CardContent>
|
|
1699
|
+
</Card>
|
|
1700
|
+
</div>
|
|
1701
|
+
)
|
|
1702
|
+
}
|
|
1703
|
+
```
|
|
1704
|
+
|
|
1705
|
+
### Font Optimization
|
|
1706
|
+
|
|
1707
|
+
Optimize fonts with next/font:
|
|
1708
|
+
|
|
1709
|
+
```tsx
|
|
1710
|
+
// app/layout.tsx
|
|
1711
|
+
import { Inter } from "next/font/google"
|
|
1712
|
+
import { Toaster } from "@/components/ui/toaster"
|
|
1713
|
+
import { cn } from "@/lib/utils"
|
|
1714
|
+
import "./globals.css"
|
|
1715
|
+
|
|
1716
|
+
const inter = Inter({ subsets: ["latin"] })
|
|
1717
|
+
|
|
1718
|
+
export default function RootLayout({
|
|
1719
|
+
children,
|
|
1720
|
+
}: {
|
|
1721
|
+
children: React.ReactNode
|
|
1722
|
+
}) {
|
|
1723
|
+
return (
|
|
1724
|
+
<html lang="en" suppressHydrationWarning>
|
|
1725
|
+
<body className={cn("min-h-screen bg-background font-sans antialiased", inter.className)}>
|
|
1726
|
+
{children}
|
|
1727
|
+
<Toaster />
|
|
1728
|
+
</body>
|
|
1729
|
+
</html>
|
|
1730
|
+
)
|
|
1731
|
+
}
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
## Advanced Patterns
|
|
1735
|
+
|
|
1736
|
+
### Form with Multiple Fields
|
|
1737
|
+
|
|
1738
|
+
```tsx
|
|
1739
|
+
const formSchema = z.object({
|
|
1740
|
+
username: z.string().min(2).max(50),
|
|
1741
|
+
email: z.string().email(),
|
|
1742
|
+
bio: z.string().max(160).min(4),
|
|
1743
|
+
role: z.enum(["admin", "user", "guest"]),
|
|
1744
|
+
notifications: z.boolean().default(false),
|
|
1745
|
+
})
|
|
1746
|
+
|
|
1747
|
+
export function AdvancedForm() {
|
|
1748
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
1749
|
+
resolver: zodResolver(formSchema),
|
|
1750
|
+
defaultValues: {
|
|
1751
|
+
username: "",
|
|
1752
|
+
email: "",
|
|
1753
|
+
bio: "",
|
|
1754
|
+
role: "user",
|
|
1755
|
+
notifications: false,
|
|
1756
|
+
},
|
|
1757
|
+
})
|
|
1758
|
+
|
|
1759
|
+
function onSubmit(values: z.infer<typeof formSchema>) {
|
|
1760
|
+
console.log(values)
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
return (
|
|
1764
|
+
<Form {...form}>
|
|
1765
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
|
1766
|
+
{/* Username field */}
|
|
1767
|
+
<FormField
|
|
1768
|
+
control={form.control}
|
|
1769
|
+
name="username"
|
|
1770
|
+
render={({ field }) => (
|
|
1771
|
+
<FormItem>
|
|
1772
|
+
<FormLabel>Username</FormLabel>
|
|
1773
|
+
<FormControl>
|
|
1774
|
+
<Input placeholder="johndoe" {...field} />
|
|
1775
|
+
</FormControl>
|
|
1776
|
+
<FormMessage />
|
|
1777
|
+
</FormItem>
|
|
1778
|
+
)}
|
|
1779
|
+
/>
|
|
1780
|
+
|
|
1781
|
+
{/* Email field */}
|
|
1782
|
+
<FormField
|
|
1783
|
+
control={form.control}
|
|
1784
|
+
name="email"
|
|
1785
|
+
render={({ field }) => (
|
|
1786
|
+
<FormItem>
|
|
1787
|
+
<FormLabel>Email</FormLabel>
|
|
1788
|
+
<FormControl>
|
|
1789
|
+
<Input type="email" placeholder="john@example.com" {...field} />
|
|
1790
|
+
</FormControl>
|
|
1791
|
+
<FormMessage />
|
|
1792
|
+
</FormItem>
|
|
1793
|
+
)}
|
|
1794
|
+
/>
|
|
1795
|
+
|
|
1796
|
+
{/* Textarea field */}
|
|
1797
|
+
<FormField
|
|
1798
|
+
control={form.control}
|
|
1799
|
+
name="bio"
|
|
1800
|
+
render={({ field }) => (
|
|
1801
|
+
<FormItem>
|
|
1802
|
+
<FormLabel>Bio</FormLabel>
|
|
1803
|
+
<FormControl>
|
|
1804
|
+
<Textarea
|
|
1805
|
+
placeholder="Tell us about yourself"
|
|
1806
|
+
className="resize-none"
|
|
1807
|
+
{...field}
|
|
1808
|
+
/>
|
|
1809
|
+
</FormControl>
|
|
1810
|
+
<FormMessage />
|
|
1811
|
+
</FormItem>
|
|
1812
|
+
)}
|
|
1813
|
+
/>
|
|
1814
|
+
|
|
1815
|
+
{/* Select field */}
|
|
1816
|
+
<FormField
|
|
1817
|
+
control={form.control}
|
|
1818
|
+
name="role"
|
|
1819
|
+
render={({ field }) => (
|
|
1820
|
+
<FormItem>
|
|
1821
|
+
<FormLabel>Role</FormLabel>
|
|
1822
|
+
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
1823
|
+
<FormControl>
|
|
1824
|
+
<SelectTrigger>
|
|
1825
|
+
<SelectValue placeholder="Select a role" />
|
|
1826
|
+
</SelectTrigger>
|
|
1827
|
+
</FormControl>
|
|
1828
|
+
<SelectContent>
|
|
1829
|
+
<SelectItem value="admin">Admin</SelectItem>
|
|
1830
|
+
<SelectItem value="user">User</SelectItem>
|
|
1831
|
+
<SelectItem value="guest">Guest</SelectItem>
|
|
1832
|
+
</SelectContent>
|
|
1833
|
+
</Select>
|
|
1834
|
+
<FormMessage />
|
|
1835
|
+
</FormItem>
|
|
1836
|
+
)}
|
|
1837
|
+
/>
|
|
1838
|
+
|
|
1839
|
+
{/* Checkbox field */}
|
|
1840
|
+
<FormField
|
|
1841
|
+
control={form.control}
|
|
1842
|
+
name="notifications"
|
|
1843
|
+
render={({ field }) => (
|
|
1844
|
+
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
|
1845
|
+
<FormControl>
|
|
1846
|
+
<Checkbox
|
|
1847
|
+
checked={field.value}
|
|
1848
|
+
onCheckedChange={field.onChange}
|
|
1849
|
+
/>
|
|
1850
|
+
</FormControl>
|
|
1851
|
+
<div className="space-y-1 leading-none">
|
|
1852
|
+
<FormLabel>Email notifications</FormLabel>
|
|
1853
|
+
<FormDescription>
|
|
1854
|
+
Receive emails about your account activity.
|
|
1855
|
+
</FormDescription>
|
|
1856
|
+
</div>
|
|
1857
|
+
</FormItem>
|
|
1858
|
+
)}
|
|
1859
|
+
/>
|
|
1860
|
+
|
|
1861
|
+
<Button type="submit">Submit</Button>
|
|
1862
|
+
</form>
|
|
1863
|
+
</Form>
|
|
1864
|
+
)
|
|
1865
|
+
}
|
|
1866
|
+
```
|
|
1867
|
+
|
|
1868
|
+
## Best Practices
|
|
1869
|
+
|
|
1870
|
+
1. **Accessibility**: Components use Radix UI primitives for ARIA compliance
|
|
1871
|
+
2. **Customization**: Modify components directly in your codebase
|
|
1872
|
+
3. **Type Safety**: Use TypeScript for type-safe props and state
|
|
1873
|
+
4. **Validation**: Use Zod schemas for form validation
|
|
1874
|
+
5. **Styling**: Leverage Tailwind utilities and CSS variables
|
|
1875
|
+
6. **Consistency**: Use the same component patterns across your app
|
|
1876
|
+
7. **Testing**: Components are testable with React Testing Library
|
|
1877
|
+
8. **Performance**: Components are optimized and tree-shakeable
|
|
1878
|
+
|
|
1879
|
+
## Common Component Combinations
|
|
1880
|
+
|
|
1881
|
+
### Login Form
|
|
1882
|
+
|
|
1883
|
+
```tsx
|
|
1884
|
+
<Card className="w-[350px]">
|
|
1885
|
+
<CardHeader>
|
|
1886
|
+
<CardTitle>Login</CardTitle>
|
|
1887
|
+
<CardDescription>Enter your credentials to continue</CardDescription>
|
|
1888
|
+
</CardHeader>
|
|
1889
|
+
<CardContent>
|
|
1890
|
+
<Form {...form}>
|
|
1891
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
1892
|
+
<FormField
|
|
1893
|
+
control={form.control}
|
|
1894
|
+
name="email"
|
|
1895
|
+
render={({ field }) => (
|
|
1896
|
+
<FormItem>
|
|
1897
|
+
<FormLabel>Email</FormLabel>
|
|
1898
|
+
<FormControl>
|
|
1899
|
+
<Input type="email" placeholder="you@example.com" {...field} />
|
|
1900
|
+
</FormControl>
|
|
1901
|
+
<FormMessage />
|
|
1902
|
+
</FormItem>
|
|
1903
|
+
)}
|
|
1904
|
+
/>
|
|
1905
|
+
<FormField
|
|
1906
|
+
control={form.control}
|
|
1907
|
+
name="password"
|
|
1908
|
+
render={({ field }) => (
|
|
1909
|
+
<FormItem>
|
|
1910
|
+
<FormLabel>Password</FormLabel>
|
|
1911
|
+
<FormControl>
|
|
1912
|
+
<Input type="password" {...field} />
|
|
1913
|
+
</FormControl>
|
|
1914
|
+
<FormMessage />
|
|
1915
|
+
</FormItem>
|
|
1916
|
+
)}
|
|
1917
|
+
/>
|
|
1918
|
+
<Button type="submit" className="w-full">Login</Button>
|
|
1919
|
+
</form>
|
|
1920
|
+
</Form>
|
|
1921
|
+
</CardContent>
|
|
1922
|
+
</Card>
|
|
1923
|
+
```
|
|
1924
|
+
|
|
1925
|
+
## References
|
|
1926
|
+
|
|
1927
|
+
- Official Docs: https://ui.shadcn.com
|
|
1928
|
+
- Radix UI: https://www.radix-ui.com
|
|
1929
|
+
- React Hook Form: https://react-hook-form.com
|
|
1930
|
+
- Zod: https://zod.dev
|
|
1931
|
+
- Tailwind CSS: https://tailwindcss.com
|
|
1932
|
+
- Examples: https://ui.shadcn.com/examples
|