Initial commit
This commit is contained in:
490
skills/skill/SKILL.md
Normal file
490
skills/skill/SKILL.md
Normal file
@@ -0,0 +1,490 @@
|
||||
---
|
||||
name: styling-with-tailwind
|
||||
description: Creates UIs using Tailwind CSS utility classes and shadcn/ui patterns. Covers CSS variables with OKLCH colors, component variants with CVA, responsive design, dark mode, and Tailwind v4 features. Use when building interfaces with Tailwind, styling shadcn/ui components, implementing themes, or working with utility-first CSS.
|
||||
---
|
||||
|
||||
# Styling with Tailwind CSS
|
||||
|
||||
Build accessible UIs using Tailwind utility classes and shadcn/ui component patterns.
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### CSS Variables for Theming
|
||||
|
||||
shadcn/ui uses semantic CSS variables mapped to Tailwind utilities:
|
||||
|
||||
```css
|
||||
/* globals.css - Light mode */
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--border: oklch(0.922 0 0);
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
}
|
||||
|
||||
/* Tailwind v4: Map variables */
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-primary: var(--primary);
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in components:**
|
||||
```tsx
|
||||
// Background colors omit the "-background" suffix
|
||||
<div className="bg-primary text-primary-foreground">
|
||||
<div className="bg-muted text-muted-foreground">
|
||||
<div className="bg-destructive text-destructive-foreground">
|
||||
```
|
||||
|
||||
### Component Variants with CVA
|
||||
|
||||
Use `class-variance-authority` for component variants:
|
||||
|
||||
```tsx
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 px-3 text-xs",
|
||||
lg: "h-10 px-8",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Usage
|
||||
<Button variant="outline" size="sm">Click me</Button>
|
||||
```
|
||||
|
||||
### Responsive Design
|
||||
|
||||
Mobile-first breakpoints:
|
||||
|
||||
```tsx
|
||||
// Stack on mobile, grid on tablet+
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
|
||||
// Hide on mobile
|
||||
<div className="hidden md:block">
|
||||
|
||||
// Different layouts per breakpoint
|
||||
<div className="flex flex-col md:flex-row lg:gap-8">
|
||||
<aside className="w-full md:w-64">
|
||||
<main className="flex-1">
|
||||
</div>
|
||||
|
||||
// Responsive text sizes
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl">
|
||||
```
|
||||
|
||||
### Dark Mode
|
||||
|
||||
```tsx
|
||||
// Use dark: prefix for dark mode styles
|
||||
<div className="bg-white dark:bg-black text-black dark:text-white">
|
||||
|
||||
// Theme toggle component
|
||||
"use client"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
|
||||
<Sun className="rotate-0 scale-100 dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute rotate-90 scale-0 dark:rotate-0 dark:scale-100" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Common Component Patterns
|
||||
|
||||
### Card
|
||||
|
||||
```tsx
|
||||
<div className="rounded-xl border bg-card text-card-foreground shadow">
|
||||
<div className="flex flex-col space-y-1.5 p-6">
|
||||
<h3 className="font-semibold leading-none tracking-tight">Title</h3>
|
||||
<p className="text-sm text-muted-foreground">Description</p>
|
||||
</div>
|
||||
<div className="p-6 pt-0">Content</div>
|
||||
<div className="flex items-center p-6 pt-0">Footer</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Form Field
|
||||
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">Helper text</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Badge
|
||||
|
||||
```tsx
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground shadow",
|
||||
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
||||
destructive: "border-transparent bg-destructive text-destructive-foreground",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Alert
|
||||
|
||||
```tsx
|
||||
<div className="relative w-full rounded-lg border px-4 py-3 text-sm [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11">
|
||||
<AlertCircle className="size-4" />
|
||||
<div className="font-medium">Alert Title</div>
|
||||
<div className="text-sm text-muted-foreground">Description</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Loading Skeleton
|
||||
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-[250px] animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-[200px] animate-pulse rounded bg-muted" />
|
||||
</div>
|
||||
```
|
||||
|
||||
## Layout Patterns
|
||||
|
||||
### Centered Layout
|
||||
|
||||
```tsx
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="w-full max-w-md space-y-8 p-8">
|
||||
{/* Content */}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Sidebar Layout
|
||||
|
||||
```tsx
|
||||
<div className="flex h-screen">
|
||||
<aside className="w-64 border-r bg-muted/40">Sidebar</aside>
|
||||
<main className="flex-1 overflow-auto">Content</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Dashboard Grid
|
||||
|
||||
```tsx
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card className="col-span-2">Wide card</Card>
|
||||
<Card>Regular</Card>
|
||||
<Card>Regular</Card>
|
||||
<Card className="col-span-4">Full width</Card>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Container with Max Width
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4 md:px-6 lg:px-8">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{/* Centered content */}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Accessibility Patterns
|
||||
|
||||
### Focus Visible
|
||||
|
||||
```tsx
|
||||
<button className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
```
|
||||
|
||||
### Screen Reader Only
|
||||
|
||||
```tsx
|
||||
<span className="sr-only">Close dialog</span>
|
||||
```
|
||||
|
||||
### Disabled States
|
||||
|
||||
```tsx
|
||||
<button className="disabled:cursor-not-allowed disabled:opacity-50" disabled>
|
||||
```
|
||||
|
||||
### ARIA-friendly Alert
|
||||
|
||||
```tsx
|
||||
<div role="alert" className="rounded-lg border p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="size-5 text-destructive" />
|
||||
<div className="flex-1 space-y-1">
|
||||
<h5 className="font-medium">Error</h5>
|
||||
<p className="text-sm text-muted-foreground">Message</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Tailwind v4 Features
|
||||
|
||||
### Size Utility
|
||||
|
||||
```tsx
|
||||
// New syntax (replaces w-* h-*)
|
||||
<div className="size-4">
|
||||
<div className="size-8">
|
||||
<div className="size-full">
|
||||
```
|
||||
|
||||
### @theme Directive
|
||||
|
||||
```css
|
||||
/* Tailwind v4 syntax */
|
||||
@theme {
|
||||
--color-primary: oklch(0.205 0 0);
|
||||
--font-sans: "Inter", system-ui;
|
||||
}
|
||||
|
||||
/* With CSS variables */
|
||||
@theme inline {
|
||||
--color-primary: var(--primary);
|
||||
}
|
||||
```
|
||||
|
||||
### Animation
|
||||
|
||||
```css
|
||||
/* globals.css */
|
||||
@import "tw-animate-css";
|
||||
```
|
||||
|
||||
```tsx
|
||||
<div className="animate-fade-in">
|
||||
<div className="animate-slide-in-from-top">
|
||||
<div className="animate-spin">
|
||||
```
|
||||
|
||||
### Tailwind v4.1 Features (April 2025)
|
||||
|
||||
**Text Shadow:**
|
||||
```tsx
|
||||
// Subtle text shadows for depth
|
||||
<h1 className="text-shadow-sm text-4xl font-bold">
|
||||
<h2 className="text-shadow-md text-2xl">
|
||||
<div className="text-shadow-lg text-xl">
|
||||
|
||||
// Custom text shadows
|
||||
<div className="text-shadow-[0_2px_4px_rgb(0_0_0_/_0.1)]">
|
||||
```
|
||||
|
||||
**Mask Utilities:**
|
||||
```tsx
|
||||
// Gradient masks for fade effects
|
||||
<div className="mask-linear-to-b from-black to-transparent">
|
||||
Fades to transparent at bottom
|
||||
</div>
|
||||
|
||||
// Image masks
|
||||
<div className="mask-[url('/mask.svg')]">
|
||||
Masked content
|
||||
</div>
|
||||
|
||||
// Common patterns
|
||||
<div className="mask-radial-gradient">Spotlight effect</div>
|
||||
```
|
||||
|
||||
**Colored Drop Shadow:**
|
||||
```tsx
|
||||
// Brand-colored shadows
|
||||
<div className="drop-shadow-[0_4px_12px_oklch(0.488_0.243_264.376)]">
|
||||
|
||||
// Use with semantic colors
|
||||
<Button className="drop-shadow-lg drop-shadow-primary/50">
|
||||
Glowing button
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Overflow Wrap:**
|
||||
```tsx
|
||||
// Break long words
|
||||
<p className="overflow-wrap-anywhere">
|
||||
verylongwordthatneedstowrap
|
||||
</p>
|
||||
|
||||
<p className="overflow-wrap-break-word">
|
||||
URLs and long strings
|
||||
</p>
|
||||
```
|
||||
|
||||
## OKLCH Colors
|
||||
|
||||
Use OKLCH for better color perception:
|
||||
|
||||
```css
|
||||
/* Format: oklch(lightness chroma hue) */
|
||||
--primary: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
|
||||
/* Benefits: perceptually uniform, consistent lightness across hues */
|
||||
```
|
||||
|
||||
## Base Color Palettes
|
||||
|
||||
shadcn/ui provides multiple base colors:
|
||||
|
||||
```css
|
||||
/* Neutral (default) - pure grayscale */
|
||||
--primary: oklch(0.205 0 0);
|
||||
|
||||
/* Zinc - cooler, blue-gray */
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
|
||||
/* Slate - balanced blue-gray */
|
||||
--primary: oklch(0.208 0.042 265.755);
|
||||
|
||||
/* Stone - warmer, brown-gray */
|
||||
--primary: oklch(0.216 0.006 56.043);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Prefer Semantic Colors
|
||||
|
||||
```tsx
|
||||
// Good - uses theme
|
||||
<div className="bg-background text-foreground">
|
||||
|
||||
// Avoid - hardcoded
|
||||
<div className="bg-white text-black dark:bg-zinc-950">
|
||||
```
|
||||
|
||||
### Group Related Utilities
|
||||
|
||||
```tsx
|
||||
<div className="
|
||||
flex items-center justify-between
|
||||
rounded-lg border
|
||||
bg-card text-card-foreground
|
||||
p-4 shadow-sm
|
||||
hover:bg-accent
|
||||
">
|
||||
```
|
||||
|
||||
### Avoid Arbitrary Values
|
||||
|
||||
```tsx
|
||||
// Prefer design tokens
|
||||
<div className="p-4 text-sm">
|
||||
|
||||
// Avoid when unnecessary
|
||||
<div className="p-[17px] text-[13px]">
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Initialize shadcn/ui
|
||||
pnpm dlx shadcn@latest init
|
||||
|
||||
# Add components
|
||||
pnpm dlx shadcn@latest add button card form
|
||||
|
||||
# Add all components
|
||||
pnpm dlx shadcn@latest add --all
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Colors not updating:**
|
||||
1. Check CSS variable in globals.css
|
||||
2. Verify @theme inline includes variable
|
||||
3. Clear build cache
|
||||
|
||||
**Dark mode not working:**
|
||||
1. Verify ThemeProvider wraps app
|
||||
2. Check suppressHydrationWarning on html tag
|
||||
3. Ensure dark: variants defined
|
||||
|
||||
**Tailwind v4 migration:**
|
||||
1. Run `@tailwindcss/upgrade@next` codemod
|
||||
2. Update CSS variables with hsl() wrappers
|
||||
3. Change @theme to @theme inline
|
||||
4. Install tw-animate-css
|
||||
|
||||
## Component Patterns
|
||||
|
||||
For detailed component patterns see [components.md](components.md):
|
||||
- **Composition**: asChild pattern for wrapping elements
|
||||
- **Typography**: Heading scales, prose styles, inline code
|
||||
- **Forms**: React Hook Form with Zod validation
|
||||
- **Icons**: Lucide icons integration and sizing
|
||||
- **Inputs**: OTP, file, grouped inputs
|
||||
- **Dialogs**: Modal patterns and composition
|
||||
- **Data Tables**: TanStack table integration
|
||||
- **Toasts**: Sonner notifications
|
||||
- **CLI**: Complete command reference
|
||||
|
||||
## Resources
|
||||
|
||||
See [theming.md](theming.md) for complete color system reference and examples.
|
||||
|
||||
## Summary
|
||||
|
||||
Key concepts:
|
||||
- Use semantic CSS variables for theming
|
||||
- Apply CVA for component variants
|
||||
- Follow mobile-first responsive patterns
|
||||
- Implement dark mode with next-themes
|
||||
- Use OKLCH for modern color handling
|
||||
- Prefer Tailwind v4 features (size-*, @theme)
|
||||
- Always ensure accessibility with focus-visible, sr-only
|
||||
|
||||
This skill focuses on shadcn/ui patterns with Tailwind CSS. For component-specific examples, refer to the official shadcn/ui documentation.
|
||||
830
skills/skill/references/components.md
Normal file
830
skills/skill/references/components.md
Normal file
@@ -0,0 +1,830 @@
|
||||
# Component Patterns Reference
|
||||
|
||||
## Composition with asChild
|
||||
|
||||
Use `asChild` to compose components without wrapper divs:
|
||||
|
||||
```tsx
|
||||
// Button as a Link (Next.js)
|
||||
import Link from "next/link"
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/login">Login</Link>
|
||||
</Button>
|
||||
|
||||
// Renders: <a href="/login" class="...button classes">Login</a>
|
||||
// No wrapper div!
|
||||
|
||||
// Button as a custom component
|
||||
<Button asChild variant="outline">
|
||||
<a href="https://example.com" target="_blank">
|
||||
External Link
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
// Dialog trigger with custom element
|
||||
<DialogTrigger asChild>
|
||||
<div className="cursor-pointer">
|
||||
Custom trigger element
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Wrapping navigation links
|
||||
- Custom interactive elements
|
||||
- Avoiding nested buttons
|
||||
- Semantic HTML (button → link when navigating)
|
||||
|
||||
## Typography Patterns
|
||||
|
||||
shadcn/ui typography scales using Tailwind utilities:
|
||||
|
||||
```tsx
|
||||
// Headings with responsive sizing
|
||||
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||
Taxing Laughter: The Joke Tax Chronicles
|
||||
</h1>
|
||||
|
||||
<h2 className="scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0">
|
||||
The People of the Kingdom
|
||||
</h2>
|
||||
|
||||
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight">
|
||||
The Joke Tax
|
||||
</h3>
|
||||
|
||||
<h4 className="scroll-m-20 text-xl font-semibold tracking-tight">
|
||||
People stopped telling jokes
|
||||
</h4>
|
||||
|
||||
// Paragraph
|
||||
<p className="leading-7 [&:not(:first-child)]:mt-6">
|
||||
The king, seeing how much happier his subjects were, realized the error of his ways.
|
||||
</p>
|
||||
|
||||
// Blockquote
|
||||
<blockquote className="mt-6 border-l-2 pl-6 italic">
|
||||
"After all," he said, "everyone enjoys a good joke."
|
||||
</blockquote>
|
||||
|
||||
// Inline code
|
||||
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
|
||||
@radix-ui/react-alert-dialog
|
||||
</code>
|
||||
|
||||
// Lead text (larger paragraph)
|
||||
<p className="text-xl text-muted-foreground">
|
||||
A modal dialog that interrupts the user with important content.
|
||||
</p>
|
||||
|
||||
// Small text
|
||||
<small className="text-sm font-medium leading-none">Email address</small>
|
||||
|
||||
// Muted text
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enter your email address.
|
||||
</p>
|
||||
|
||||
// List
|
||||
<ul className="my-6 ml-6 list-disc [&>li]:mt-2">
|
||||
<li>1st level of puns: 5 gold coins</li>
|
||||
<li>2nd level of jokes: 10 gold coins</li>
|
||||
<li>3rd level of one-liners: 20 gold coins</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
## Icons with Lucide
|
||||
|
||||
```tsx
|
||||
import { ChevronRight, Check, X, AlertCircle, Loader2 } from "lucide-react"
|
||||
|
||||
// Icon sizing with components
|
||||
<Button>
|
||||
<ChevronRight className="size-4" />
|
||||
Next
|
||||
</Button>
|
||||
|
||||
// Icons automatically adjust to button size
|
||||
<Button size="sm">
|
||||
<Check className="size-4" />
|
||||
Small Button
|
||||
</Button>
|
||||
|
||||
<Button size="lg">
|
||||
<Check className="size-4" />
|
||||
Large Button
|
||||
</Button>
|
||||
|
||||
// Icon-only button
|
||||
<Button size="icon" variant="outline">
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
|
||||
// Loading state
|
||||
<Button disabled>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
|
||||
// Icon with semantic colors
|
||||
<AlertCircle className="size-4 text-destructive" />
|
||||
<Check className="size-4 text-green-500" />
|
||||
|
||||
// In alerts
|
||||
<Alert>
|
||||
<AlertCircle className="size-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your session has expired.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
```
|
||||
|
||||
**Icon sizing reference:**
|
||||
- `size-3` - Extra small (12px)
|
||||
- `size-4` - Small/default (16px)
|
||||
- `size-5` - Medium (20px)
|
||||
- `size-6` - Large (24px)
|
||||
|
||||
## Form with React Hook Form
|
||||
|
||||
Complete form example with validation:
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { toast } from "sonner"
|
||||
|
||||
// Define schema
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(2, {
|
||||
message: "Username must be at least 2 characters.",
|
||||
}),
|
||||
email: z.string().email({
|
||||
message: "Please enter a valid email address.",
|
||||
}),
|
||||
bio: z.string().max(160).min(4),
|
||||
})
|
||||
|
||||
export function ProfileForm() {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
email: "",
|
||||
bio: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
toast.success("Profile updated successfully")
|
||||
console.log(values)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="shadcn" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bio"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bio</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Tell us about yourself"
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
You can write up to 160 characters.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit">Update profile</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Input Variants
|
||||
|
||||
### Input OTP
|
||||
|
||||
```tsx
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp"
|
||||
|
||||
<InputOTP maxLength={6}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
```
|
||||
|
||||
### Input with Icon
|
||||
|
||||
```tsx
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 size-4 text-muted-foreground" />
|
||||
<Input placeholder="Search" className="pl-8" />
|
||||
</div>
|
||||
```
|
||||
|
||||
### File Input
|
||||
|
||||
```tsx
|
||||
<Input
|
||||
type="file"
|
||||
className="cursor-pointer file:mr-4 file:rounded-md file:border-0 file:bg-primary file:px-4 file:py-2 file:text-sm file:font-semibold file:text-primary-foreground hover:file:bg-primary/90"
|
||||
/>
|
||||
```
|
||||
|
||||
### Input Group
|
||||
|
||||
```tsx
|
||||
import { InputGroup, InputGroupText } from "@/components/ui/input-group"
|
||||
|
||||
<InputGroup>
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
<Input placeholder="example.com" />
|
||||
</InputGroup>
|
||||
|
||||
<InputGroup>
|
||||
<Input placeholder="Amount" />
|
||||
<InputGroupText>USD</InputGroupText>
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
## Data-Slot Composition
|
||||
|
||||
Components use `data-slot` attributes for styling child elements:
|
||||
|
||||
```tsx
|
||||
// Button automatically styles icons with data-slot
|
||||
<Button>
|
||||
<CheckIcon data-slot="icon" />
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
// Custom component using data-slot pattern
|
||||
function CustomCard({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-lg border p-4 [&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:text-muted-foreground">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Usage
|
||||
<CustomCard>
|
||||
<AlertCircle data-slot="icon" />
|
||||
<p>This icon is automatically styled</p>
|
||||
</CustomCard>
|
||||
```
|
||||
|
||||
**Common data-slot values:**
|
||||
- `icon` - Icons within components
|
||||
- `title` - Heading elements
|
||||
- `description` - Descriptive text
|
||||
- `action` - Action buttons or triggers
|
||||
|
||||
## Select Component
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
|
||||
<Select>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="blueberry">Blueberry</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
// With form
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fruit"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Fruit</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Checkbox and Radio Groups
|
||||
|
||||
```tsx
|
||||
// Checkbox
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Accept terms and conditions
|
||||
</label>
|
||||
</div>
|
||||
|
||||
// Radio Group
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
|
||||
<RadioGroup defaultValue="comfortable">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="default" id="r1" />
|
||||
<Label htmlFor="r1">Default</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="comfortable" id="r2" />
|
||||
<Label htmlFor="r2">Comfortable</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="compact" id="r3" />
|
||||
<Label htmlFor="r3">Compact</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
```
|
||||
|
||||
## Dialog Pattern
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Edit Profile</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit profile</DialogTitle>
|
||||
<DialogDescription>
|
||||
Make changes to your profile here. Click save when you're done.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name
|
||||
</Label>
|
||||
<Input id="name" value="Pedro Duarte" className="col-span-3" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit">Save changes</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Dropdown Menu
|
||||
|
||||
```tsx
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Open</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Billing</DropdownMenuItem>
|
||||
<DropdownMenuItem>Team</DropdownMenuItem>
|
||||
<DropdownMenuItem>Subscription</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
```
|
||||
|
||||
## Toast Notifications
|
||||
|
||||
```tsx
|
||||
import { toast } from "sonner"
|
||||
|
||||
// Success
|
||||
toast.success("Event created successfully")
|
||||
|
||||
// Error
|
||||
toast.error("Something went wrong")
|
||||
|
||||
// Info
|
||||
toast.info("Be aware that...")
|
||||
|
||||
// Warning
|
||||
toast.warning("Proceed with caution")
|
||||
|
||||
// Loading
|
||||
toast.loading("Uploading...")
|
||||
|
||||
// Custom
|
||||
toast("Event created", {
|
||||
description: "Monday, January 3rd at 6:00pm",
|
||||
action: {
|
||||
label: "Undo",
|
||||
onClick: () => console.log("Undo"),
|
||||
},
|
||||
})
|
||||
|
||||
// Promise
|
||||
toast.promise(promise, {
|
||||
loading: "Loading...",
|
||||
success: (data) => `${data.name} created`,
|
||||
error: "Error creating event",
|
||||
})
|
||||
```
|
||||
|
||||
## Data Table Pattern
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## CLI Commands Reference
|
||||
|
||||
```bash
|
||||
# Initialize project
|
||||
pnpm dlx shadcn@latest init
|
||||
|
||||
# Add specific components
|
||||
pnpm dlx shadcn@latest add button
|
||||
pnpm dlx shadcn@latest add card form input
|
||||
|
||||
# Add all components
|
||||
pnpm dlx shadcn@latest add --all
|
||||
|
||||
# Update/overwrite existing components
|
||||
pnpm dlx shadcn@latest add button --overwrite
|
||||
pnpm dlx shadcn@latest add --all --overwrite
|
||||
|
||||
# Show component diff (see what changed)
|
||||
pnpm dlx shadcn@latest diff button
|
||||
|
||||
# List available components
|
||||
pnpm dlx shadcn@latest add
|
||||
|
||||
# Use canary release (for Tailwind v4 + React 19)
|
||||
pnpm dlx shadcn@canary init
|
||||
pnpm dlx shadcn@canary add button
|
||||
```
|
||||
|
||||
## Field Component (October 2025)
|
||||
|
||||
Simplified form field wrapper without React Hook Form:
|
||||
|
||||
```tsx
|
||||
import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field"
|
||||
|
||||
<Field>
|
||||
<FieldLabel>Email address</FieldLabel>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
aria-describedby="email-description email-error"
|
||||
/>
|
||||
<FieldDescription id="email-description">
|
||||
We'll never share your email.
|
||||
</FieldDescription>
|
||||
<FieldError id="email-error">
|
||||
{errors.email?.message}
|
||||
</FieldError>
|
||||
</Field>
|
||||
|
||||
// With validation state
|
||||
<Field invalid={!!errors.password}>
|
||||
<FieldLabel required>Password</FieldLabel>
|
||||
<Input type="password" />
|
||||
<FieldError>{errors.password?.message}</FieldError>
|
||||
</Field>
|
||||
|
||||
// Inline field
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel>Subscribe</FieldLabel>
|
||||
<Checkbox />
|
||||
<FieldDescription>Get updates via email</FieldDescription>
|
||||
</Field>
|
||||
```
|
||||
|
||||
## Item Component (October 2025)
|
||||
|
||||
Flex container for list items with consistent spacing:
|
||||
|
||||
```tsx
|
||||
import { Item, ItemIcon, ItemLabel, ItemDescription } from "@/components/ui/item"
|
||||
|
||||
// List item with icon
|
||||
<Item>
|
||||
<ItemIcon>
|
||||
<FileIcon className="size-4" />
|
||||
</ItemIcon>
|
||||
<div>
|
||||
<ItemLabel>document.pdf</ItemLabel>
|
||||
<ItemDescription>2.4 MB</ItemDescription>
|
||||
</div>
|
||||
</Item>
|
||||
|
||||
// Card-style items
|
||||
<Item asChild>
|
||||
<a href="/dashboard" className="rounded-lg border p-4 hover:bg-accent">
|
||||
<ItemIcon>
|
||||
<LayoutDashboard className="size-5" />
|
||||
</ItemIcon>
|
||||
<div>
|
||||
<ItemLabel>Dashboard</ItemLabel>
|
||||
<ItemDescription>View your analytics</ItemDescription>
|
||||
</div>
|
||||
</a>
|
||||
</Item>
|
||||
|
||||
// Navigation items
|
||||
<nav className="space-y-1">
|
||||
<Item asChild>
|
||||
<a href="/home" className="px-3 py-2 rounded-md hover:bg-accent">
|
||||
<ItemIcon><Home className="size-4" /></ItemIcon>
|
||||
<ItemLabel>Home</ItemLabel>
|
||||
</a>
|
||||
</Item>
|
||||
</nav>
|
||||
```
|
||||
|
||||
## Spinner Component (October 2025)
|
||||
|
||||
Dedicated loading spinner component:
|
||||
|
||||
```tsx
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
|
||||
// Default spinner
|
||||
<Spinner />
|
||||
|
||||
// With size variants
|
||||
<Spinner size="sm" />
|
||||
<Spinner size="md" />
|
||||
<Spinner size="lg" />
|
||||
|
||||
// In buttons
|
||||
<Button disabled>
|
||||
<Spinner size="sm" />
|
||||
Loading...
|
||||
</Button>
|
||||
|
||||
// Full page loading
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
|
||||
// With custom colors
|
||||
<Spinner className="text-primary" />
|
||||
```
|
||||
|
||||
## Button Group (October 2025)
|
||||
|
||||
Grouped buttons with consistent styling:
|
||||
|
||||
```tsx
|
||||
import { ButtonGroup, ButtonGroupButton } from "@/components/ui/button-group"
|
||||
|
||||
// Basic button group
|
||||
<ButtonGroup>
|
||||
<ButtonGroupButton>Left</ButtonGroupButton>
|
||||
<ButtonGroupButton>Center</ButtonGroupButton>
|
||||
<ButtonGroupButton>Right</ButtonGroupButton>
|
||||
</ButtonGroup>
|
||||
|
||||
// With active state
|
||||
<ButtonGroup>
|
||||
<ButtonGroupButton active>Day</ButtonGroupButton>
|
||||
<ButtonGroupButton>Week</ButtonGroupButton>
|
||||
<ButtonGroupButton>Month</ButtonGroupButton>
|
||||
</ButtonGroup>
|
||||
|
||||
// With icons
|
||||
<ButtonGroup>
|
||||
<ButtonGroupButton>
|
||||
<Bold className="size-4" />
|
||||
</ButtonGroupButton>
|
||||
<ButtonGroupButton>
|
||||
<Italic className="size-4" />
|
||||
</ButtonGroupButton>
|
||||
<ButtonGroupButton>
|
||||
<Underline className="size-4" />
|
||||
</ButtonGroupButton>
|
||||
</ButtonGroup>
|
||||
|
||||
// Vertical orientation
|
||||
<ButtonGroup orientation="vertical">
|
||||
<ButtonGroupButton>Top</ButtonGroupButton>
|
||||
<ButtonGroupButton>Middle</ButtonGroupButton>
|
||||
<ButtonGroupButton>Bottom</ButtonGroupButton>
|
||||
</ButtonGroup>
|
||||
```
|
||||
|
||||
## Keyboard Shortcuts Component
|
||||
|
||||
```tsx
|
||||
import { Kbd } from "@/components/ui/kbd"
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Kbd>⌘</Kbd>
|
||||
<Kbd>K</Kbd>
|
||||
</div>
|
||||
|
||||
// Search shortcut display
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Press <Kbd>⌘</Kbd> + <Kbd>K</Kbd> to search
|
||||
</div>
|
||||
```
|
||||
|
||||
## Empty State
|
||||
|
||||
```tsx
|
||||
import { Empty } from "@/components/ui/empty"
|
||||
import { FileIcon } from "lucide-react"
|
||||
|
||||
<Empty>
|
||||
<FileIcon className="size-10 text-muted-foreground" />
|
||||
<h3 className="mt-4 text-lg font-semibold">No files uploaded</h3>
|
||||
<p className="mb-4 mt-2 text-sm text-muted-foreground">
|
||||
Upload your first file to get started
|
||||
</p>
|
||||
<Button>Upload File</Button>
|
||||
</Empty>
|
||||
```
|
||||
342
skills/skill/references/theming.md
Normal file
342
skills/skill/references/theming.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# Complete Theming Reference
|
||||
|
||||
## CSS Variables Structure
|
||||
|
||||
### Required Variables
|
||||
|
||||
```css
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.269 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.371 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Base Color Palettes
|
||||
|
||||
### Neutral (Default)
|
||||
|
||||
```css
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
}
|
||||
```
|
||||
|
||||
### Zinc (Blue-Gray)
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
}
|
||||
```
|
||||
|
||||
### Slate (Balanced Blue-Gray)
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.129 0.042 264.695);
|
||||
--primary: oklch(0.208 0.042 265.755);
|
||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--secondary: oklch(0.968 0.007 247.896);
|
||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||
--muted: oklch(0.968 0.007 247.896);
|
||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
||||
--border: oklch(0.929 0.013 255.508);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.129 0.042 264.695);
|
||||
--foreground: oklch(0.984 0.003 247.858);
|
||||
--primary: oklch(0.929 0.013 255.508);
|
||||
--primary-foreground: oklch(0.208 0.042 265.755);
|
||||
--secondary: oklch(0.279 0.041 260.031);
|
||||
--secondary-foreground: oklch(0.984 0.003 247.858);
|
||||
--muted: oklch(0.279 0.041 260.031);
|
||||
--muted-foreground: oklch(0.704 0.04 256.788);
|
||||
}
|
||||
```
|
||||
|
||||
### Stone (Warm Brown-Gray)
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.147 0.004 49.25);
|
||||
--primary: oklch(0.216 0.006 56.043);
|
||||
--primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--secondary: oklch(0.97 0.001 106.424);
|
||||
--secondary-foreground: oklch(0.216 0.006 56.043);
|
||||
--muted: oklch(0.97 0.001 106.424);
|
||||
--muted-foreground: oklch(0.553 0.013 58.071);
|
||||
--border: oklch(0.923 0.003 48.717);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.147 0.004 49.25);
|
||||
--foreground: oklch(0.985 0.001 106.423);
|
||||
--primary: oklch(0.923 0.003 48.717);
|
||||
--primary-foreground: oklch(0.216 0.006 56.043);
|
||||
--secondary: oklch(0.268 0.007 34.298);
|
||||
--secondary-foreground: oklch(0.985 0.001 106.423);
|
||||
--muted: oklch(0.268 0.007 34.298);
|
||||
--muted-foreground: oklch(0.709 0.01 56.259);
|
||||
}
|
||||
```
|
||||
|
||||
### Gray (Purple-Gray)
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.13 0.028 261.692);
|
||||
--primary: oklch(0.21 0.034 264.665);
|
||||
--primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--secondary: oklch(0.967 0.003 264.542);
|
||||
--secondary-foreground: oklch(0.21 0.034 264.665);
|
||||
--muted: oklch(0.967 0.003 264.542);
|
||||
--muted-foreground: oklch(0.551 0.027 264.364);
|
||||
--border: oklch(0.928 0.006 264.531);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.13 0.028 261.692);
|
||||
--foreground: oklch(0.985 0.002 247.839);
|
||||
--primary: oklch(0.928 0.006 264.531);
|
||||
--primary-foreground: oklch(0.21 0.034 264.665);
|
||||
--secondary: oklch(0.278 0.033 256.848);
|
||||
--secondary-foreground: oklch(0.985 0.002 247.839);
|
||||
--muted: oklch(0.278 0.033 256.848);
|
||||
--muted-foreground: oklch(0.707 0.022 261.325);
|
||||
}
|
||||
```
|
||||
|
||||
## Adding Custom Colors
|
||||
|
||||
```css
|
||||
/* Add new color to root */
|
||||
:root {
|
||||
--warning: oklch(0.84 0.16 84);
|
||||
--warning-foreground: oklch(0.28 0.07 46);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--warning: oklch(0.41 0.11 46);
|
||||
--warning-foreground: oklch(0.99 0.02 95);
|
||||
}
|
||||
|
||||
/* Map to Tailwind */
|
||||
@theme inline {
|
||||
--color-warning: var(--warning);
|
||||
--color-warning-foreground: var(--warning-foreground);
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
```tsx
|
||||
<div className="bg-warning text-warning-foreground">
|
||||
Warning message
|
||||
</div>
|
||||
```
|
||||
|
||||
## Chart Colors
|
||||
|
||||
```css
|
||||
:root {
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
}
|
||||
```
|
||||
|
||||
## Sidebar Colors (Optional)
|
||||
|
||||
```css
|
||||
:root {
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
```
|
||||
|
||||
## Understanding OKLCH
|
||||
|
||||
**Format:** `oklch(lightness chroma hue [/ alpha])`
|
||||
|
||||
- **Lightness** (0-1): Brightness, 0 = black, 1 = white
|
||||
- **Chroma** (0-0.4): Color intensity, 0 = gray
|
||||
- **Hue** (0-360): Color angle, e.g., 0/360 = red, 120 = green, 240 = blue
|
||||
- **Alpha** (optional): Transparency, 0 = transparent, 1 = opaque
|
||||
|
||||
**Benefits:**
|
||||
- Perceptually uniform (equal steps look equal)
|
||||
- Consistent lightness across all hues
|
||||
- Better for programmatic color manipulation
|
||||
- Future-proof for modern browsers
|
||||
|
||||
**Examples:**
|
||||
```css
|
||||
/* Pure grayscale (chroma = 0) */
|
||||
--background: oklch(1 0 0); /* White */
|
||||
--foreground: oklch(0.145 0 0); /* Dark gray */
|
||||
|
||||
/* Colored (chroma > 0) */
|
||||
--primary: oklch(0.577 0.245 27.325); /* Red-orange */
|
||||
--accent: oklch(0.646 0.222 41.116); /* Yellow-orange */
|
||||
|
||||
/* With opacity */
|
||||
--border: oklch(1 0 0 / 10%); /* 10% opacity white */
|
||||
```
|
||||
|
||||
## Color Naming Convention
|
||||
|
||||
- **base**: Background color (no suffix)
|
||||
- **base-foreground**: Text color on base background
|
||||
- Always pair background/foreground for accessible contrast
|
||||
|
||||
Examples:
|
||||
- `bg-primary` + `text-primary-foreground`
|
||||
- `bg-muted` + `text-muted-foreground`
|
||||
- `bg-destructive` + `text-destructive-foreground`
|
||||
Reference in New Issue
Block a user