Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "styling-with-tailwind",
|
||||||
|
"description": "Skill: Build beautiful UIs with Tailwind CSS and shadcn/ui - modern utility-first styling, component patterns, and Tailwind v4.1 features",
|
||||||
|
"version": "0.0.0-2025.11.28",
|
||||||
|
"author": {
|
||||||
|
"name": "Misha Kolesnik",
|
||||||
|
"email": "misha@kolesnik.io"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills/skill"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# styling-with-tailwind
|
||||||
|
|
||||||
|
Skill: Build beautiful UIs with Tailwind CSS and shadcn/ui - modern utility-first styling, component patterns, and Tailwind v4.1 features
|
||||||
52
plugin.lock.json
Normal file
52
plugin.lock.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:tenequm/claude-plugins:styling-with-tailwind",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "437c88f75d31b608740c5301d1d22e1ab46ecfef",
|
||||||
|
"treeHash": "67fae3511f3c99a6e7a3a601c981c3849ebc66663028634f497245569c8ce1c5",
|
||||||
|
"generatedAt": "2025-11-28T10:28:38.495771Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "styling-with-tailwind",
|
||||||
|
"description": "Skill: Build beautiful UIs with Tailwind CSS and shadcn/ui - modern utility-first styling, component patterns, and Tailwind v4.1 features"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "d887c0c4c600fa0a052a91981a766e83077403324aa57e012a84165a5c2e727d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "19b9bee645a9b4dbd195f88d89f081547a22825033975bc09f418684525c4c13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/SKILL.md",
|
||||||
|
"sha256": "b0a2d7183f0486103ce0071dedc1ece396b07591a5edf18ceed5e4461c375128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/components.md",
|
||||||
|
"sha256": "c42d361b5d7cf0d66c67be567e1003565d291c9daa876e813014a1dc53ae0a2d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/theming.md",
|
||||||
|
"sha256": "82d1d5f0b04c0ed992489e0ef4eb0f413a154c8cc61e897ce274a4d3d7d10c31"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "67fae3511f3c99a6e7a3a601c981c3849ebc66663028634f497245569c8ce1c5"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
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