Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:58 +08:00
commit 925b2de0f6
21 changed files with 3934 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "tailwind-shadcn-ui-setup",
"description": "This skill should be used when setting up, configuring, or initializing Tailwind CSS (v3 or v4) and shadcn/ui for Next.js 16 App Router projects. Configure dark mode, design tokens, base layout with header/sidebar, accessibility defaults, and generate example components. Includes comprehensive setup automation, theme customization, and production-ready patterns. Use when the user requests \"setup Tailwind\", \"configure shadcn/ui\", \"add dark mode\", \"initialize design system\", or \"setup UI framework",
"version": "1.0.0",
"author": {
"name": "Hope Overture",
"email": "support@worldbuilding-app-skills.dev"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# tailwind-shadcn-ui-setup
This skill should be used when setting up, configuring, or initializing Tailwind CSS (v3 or v4) and shadcn/ui for Next.js 16 App Router projects. Configure dark mode, design tokens, base layout with header/sidebar, accessibility defaults, and generate example components. Includes comprehensive setup automation, theme customization, and production-ready patterns. Use when the user requests "setup Tailwind", "configure shadcn/ui", "add dark mode", "initialize design system", or "setup UI framework

113
plugin.lock.json Normal file
View File

@@ -0,0 +1,113 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:hopeoverture/worldbuilding-app-skills:plugins/tailwind-shadcn-ui-setup",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "2a453c582511149b870f486b5ce27ae5a13a6e79",
"treeHash": "076df72864866cc70e4f826cb101696866bc6e89804fe471d9f08f622f9826e2",
"generatedAt": "2025-11-28T10:17:30.212016Z",
"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": "tailwind-shadcn-ui-setup",
"description": "This skill should be used when setting up, configuring, or initializing Tailwind CSS (v3 or v4) and shadcn/ui for Next.js 16 App Router projects. Configure dark mode, design tokens, base layout with header/sidebar, accessibility defaults, and generate example components. Includes comprehensive setup automation, theme customization, and production-ready patterns. Use when the user requests \"setup Tailwind\", \"configure shadcn/ui\", \"add dark mode\", \"initialize design system\", or \"setup UI framework",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "fba890e745615aa495f5d54ab4c5d1eb085900ded4074c062da3423418a7bfa4"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "0755cac23890c7efaad50afadff34524cc71bdb882294fe0739e7a2019142a9f"
},
{
"path": "skills/tailwind-shadcn-ui-setup/SKILL.md",
"sha256": "fb6bf12616c516607ece08f5844e0c7d2d645f51a1549222fd6a318c1b5a6d41"
},
{
"path": "skills/tailwind-shadcn-ui-setup/references/tailwind-v4-migration.md",
"sha256": "b32a1f32f1c51d9ab2c5ee726ff3dda295a5d0c32fde1d8264231a02386f4bf0"
},
{
"path": "skills/tailwind-shadcn-ui-setup/references/theme-tokens.md",
"sha256": "160b0a1232386e57ddfb96870610bcba937478f60aafa84a91c1c684fea655cf"
},
{
"path": "skills/tailwind-shadcn-ui-setup/references/accessibility-checklist.md",
"sha256": "1c0b1d4d39775a19db69a6ecd44a2de5897aa6084113692a215cd47dfc0b102a"
},
{
"path": "skills/tailwind-shadcn-ui-setup/references/shadcn-component-list.md",
"sha256": "33b1400d39c832b2ddab7c80b38a441501c1c292cc0dec5d49809407012365ff"
},
{
"path": "skills/tailwind-shadcn-ui-setup/scripts/setup_tailwind_shadcn.py",
"sha256": "6712fc39a1bc5222dfe6c2af2b06b23e442b5e705f0589da9707a4c4c51b2d4d"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/postcss.config.js.template",
"sha256": "251ecddd4672c9cf467547e3dc535de00ad2129df26e7e7ae728fa4e5ac45fc5"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/tailwind.config.ts.template",
"sha256": "00e4e54e5edb802f3e9b6e6522a31558977be08750429d8aaffc11bc6e226adc"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/globals.css.template",
"sha256": "5a1cba6b1bdf564e91d110f44240eb8f77cf8eff5ac2653aef6f4536190ff7e4"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/app/page.tsx.template",
"sha256": "97113ed69a36b408b01aaf3eed4b6cca68682cd1d3787ac8e99cea06caefec51"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/app/layout.tsx.template",
"sha256": "b90c038a0299b4f191515269f96e9a8db694189d42ee6a10032a7e2912be56c4"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/app/examples/layout.tsx",
"sha256": "12b9c084c56857ab4fa688c5229875cfcde36877f911f9246b7e4fcef4210778"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/app/examples/forms/page.tsx",
"sha256": "50cd40620bedcbeffad94cbb1d4234a4b82e37d06d590d013c8f23c8ba5cfc8b"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/app/examples/theme/page.tsx",
"sha256": "56199431367bab563056200774d29d503afc1afcbda516a5adacc400997c7fca"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/app/examples/dialogs/page.tsx",
"sha256": "fad97b8a80358cb48062a6345ac414847676fa7373873557504eb3179bb1e20b"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/components/theme-provider.tsx",
"sha256": "7d81e692e911cfafa5bf7c210334a0e655a87d8851cc5be4ce1799e4d14cdd97"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/components/mode-toggle.tsx",
"sha256": "77a94853d8a6da873f5de0f90a44c9800db2af276ad33469a974a3863bd1bdd3"
},
{
"path": "skills/tailwind-shadcn-ui-setup/assets/components/app-shell.tsx",
"sha256": "1b815c60babff5c58b2c0fe53d33a9f8a07f3968a992702ff2ff5e55dc08b51a"
}
],
"dirSha256": "076df72864866cc70e4f826cb101696866bc6e89804fe471d9f08f622f9826e2"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,493 @@
---
name: tailwind-shadcn-ui-setup
description: This skill should be used when setting up, configuring, or initializing Tailwind CSS (v3 or v4) and shadcn/ui for Next.js 16 App Router projects. Configure dark mode, design tokens, base layout with header/sidebar, accessibility defaults, and generate example components. Includes comprehensive setup automation, theme customization, and production-ready patterns. Use when the user requests "setup Tailwind", "configure shadcn/ui", "add dark mode", "initialize design system", or "setup UI framework" for Next.js projects.
---
# Tailwind + shadcn/ui Setup for Next.js 16
## Overview
Configure a production-ready Tailwind CSS (v3/v4) + shadcn/ui setup for Next.js 16 App Router projects. This skill automates dependency installation, configuration, component generation, and provides:
- **Tailwind CSS v4-ready configuration** (v3 with forward-compatible patterns)
- **shadcn/ui components** (Radix UI-based, fully accessible)
- **Dark mode** with `next-themes` (class strategy)
- **Base application layout** (header, optional sidebar, responsive)
- **Design token system** (CSS variables for easy theming)
- **Accessibility defaults** (WCAG 2.1 AA compliant)
- **Example pages** (forms, dialogs, theme showcase)
## Prerequisites
Before running this skill, verify:
1. **Next.js 16 project exists** with App Router (`app/` directory)
2. **Package manager**: npm (script uses `npm install`)
3. **Project structure**: Valid `package.json` at project root
4. **TypeScript**: Recommended (all templates use `.tsx`/`.ts`)
To verify:
```bash
# Check Next.js version
cat package.json | grep "\"next\":"
# Confirm app/ directory exists
ls -la app/
```
## Setup Workflow
### Step 1: Gather User Requirements
Use `AskUserQuestion` tool to collect configuration preferences:
1. **Enable dark mode?** (default: yes)
- Installs `next-themes`, adds `ThemeProvider`, creates mode toggle
2. **Theme preset** (zinc | slate | neutral, default: zinc)
- Determines base color palette in CSS variables
3. **Include sidebar layout?** (default: yes)
- Adds responsive sidebar navigation using `Sheet` component
4. **Include example pages?** (default: yes)
- Generates example pages for forms, dialogs, theme showcase
### Step 2: Run Automation Script
Execute the Python setup script to install dependencies and initialize shadcn/ui:
```bash
cd /path/to/nextjs-project
python /path/to/skill/scripts/setup_tailwind_shadcn.py
```
The script will:
- Detect existing Tailwind/shadcn installations
- Install required npm packages (non-interactive)
- Create `components.json` for shadcn/ui
- Add baseline shadcn components (button, card, input, label, dialog, separator)
- Create `lib/utils.ts` with `cn()` helper
### Step 3: Copy Configuration Files
Copy and process template files from `assets/` directory:
1. **Tailwind Configuration**
```bash
# Copy and create
cp assets/tailwind.config.ts.template project/tailwind.config.ts
cp assets/postcss.config.js.template project/postcss.config.js
```
2. **Global Styles**
```bash
# Copy and replace {{THEME_PRESET}} with user's choice
cp assets/globals.css.template project/app/globals.css
# Replace: {{THEME_PRESET}} → zinc | slate | neutral
```
3. **Utility Functions**
```bash
cp assets/lib/utils.ts project/lib/utils.ts
```
### Step 4: Add Core Components
Copy theme and layout components from `assets/components/`:
1. **Theme Provider** (if dark mode enabled)
```bash
cp assets/components/theme-provider.tsx project/components/theme-provider.tsx
cp assets/components/mode-toggle.tsx project/components/mode-toggle.tsx
```
2. **App Shell** (if sidebar layout enabled)
```bash
cp assets/components/app-shell.tsx project/components/app-shell.tsx
```
### Step 5: Update App Layout
Update or create `app/layout.tsx`:
```bash
# If layout.tsx exists, carefully merge changes
# If not, copy template
cp assets/app/layout.tsx.template project/app/layout.tsx
```
**Key additions:**
- Import `globals.css`
- Wrap with `ThemeProvider` (if dark mode enabled)
- Add skip link for accessibility
- Include `<Toaster />` from Sonner for notifications
**Merge strategy if layout exists:**
- Add missing imports at top
- Wrap existing content with `ThemeProvider`
- Add skip link before main content
- Add `Toaster` before closing `body` tag
- Ensure `suppressHydrationWarning` on `<html>` tag
### Step 6: Generate Example Pages (Optional)
If user requested examples, copy example pages:
```bash
# Copy home page
cp assets/app/page.tsx.template project/app/page.tsx
# Copy examples directory structure
cp -r assets/app/examples/ project/app/examples/
```
**Example pages include:**
- **Homepage** (`app/page.tsx`): Hero, features grid, CTA
- **Forms** (`app/examples/forms/page.tsx`): Contact form, validation patterns
- **Dialogs** (`app/examples/dialogs/page.tsx`): Modal examples, A11y notes
- **Theme** (`app/examples/theme/page.tsx`): Color tokens, customization guide
### Step 7: Add Additional shadcn Components
Install additional components as needed:
```bash
# Common components for examples
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add sheet
npx shadcn-ui@latest add separator
# Optional: Form components
npx shadcn-ui@latest add form
npx shadcn-ui@latest add checkbox
npx shadcn-ui@latest add select
```
Consult `references/shadcn-component-list.md` for full component catalog.
### Step 8: Verify Installation
Run checks to ensure setup is complete:
```bash
# Check for TypeScript errors
npx tsc --noEmit
# Start dev server
npm run dev
# Open browser to http://localhost:3000
```
**Visual verification:**
- Page loads without errors
- Dark mode toggle works (if enabled)
- Colors match theme preset
- Example pages render correctly (if included)
- No accessibility warnings in console
### Step 9: Document Changes
Add a "Design System & UI" section to project `README.md`:
````markdown
## Design System & UI
This project uses Tailwind CSS and shadcn/ui for styling and components.
### Customizing Colors
Edit CSS variables in `app/globals.css`:
```css
:root {
--primary: 270 80% 45%; /* Your brand color (HSL) */
--radius: 0.75rem; /* Border radius */
}
```
Test contrast ratios: https://webaim.org/resources/contrastchecker/
### Adding Components
```bash
# Add any shadcn/ui component
npx shadcn-ui@latest add [component-name]
# Example: Add a combobox
npx shadcn-ui@latest add combobox
```
Available components: https://ui.shadcn.com/docs/components
### Dark Mode
Toggle theme programmatically:
```tsx
import { useTheme } from 'next-themes'
export function Example() {
const { theme, setTheme } = useTheme()
// theme: 'light' | 'dark' | 'system'
// setTheme('dark')
}
```
### Accessibility
- All components meet WCAG 2.1 Level AA
- Focus indicators on all interactive elements
- Keyboard navigation supported
- Screen reader compatible
See `references/accessibility-checklist.md` for full guidelines.
````
## Configuration Options
### Theme Presets
**Zinc (default)** - Cool, neutral gray tones:
```css
--primary: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
```
**Slate** - Slightly cooler, tech-focused:
```css
--primary: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
```
**Neutral** - True neutral grays:
```css
--primary: 0 0% 9%;
--muted: 0 0% 96.1%;
```
Customize further by editing `app/globals.css` `:root` and `.dark` blocks.
### Tailwind v4 Considerations
Check Tailwind version before setup:
```bash
npm view tailwindcss version
```
**If v4.0.0+ is available:**
- Use v4 configuration format (`@theme` directive)
- Consult `references/tailwind-v4-migration.md`
**If v4 not available (current default):**
- Use v3 with forward-compatible CSS variables
- Add comments in generated files: `// TODO: Upgrade to Tailwind v4 when stable`
### Sidebar Layout Options
If `sidebarLayout = true`:
- Uses `AppShell` component with responsive sidebar
- Mobile: Hamburger menu → `Sheet` (slide-over)
- Desktop: Fixed sidebar with navigation items
If `sidebarLayout = false`:
- Simple header + content layout
- Header contains site title + actions (mode toggle)
Users can customize navigation in layout files by passing `navigation` prop:
```tsx
<AppShell
navigation={[
{ title: 'Home', href: '/', icon: Home },
{ title: 'About', href: '/about', icon: Info },
]}
siteTitle="My App"
>
{children}
</AppShell>
```
## Troubleshooting
### Issue: npm install fails
**Cause**: Network issues, registry timeout, or package conflicts
**Solution**:
```bash
# Clear npm cache
npm cache clean --force
# Retry with verbose logging
npm install --verbose
# Or use specific registry
npm install --registry https://registry.npmjs.org/
```
### Issue: TypeScript errors in components
**Cause**: Missing type definitions or tsconfig issues
**Solution**:
```bash
# Install missing types
npm install --save-dev @types/react @types/react-dom @types/node
# Check tsconfig.json paths
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}
```
### Issue: Dark mode not working
**Cause**: ThemeProvider not wrapping content, or `suppressHydrationWarning` missing
**Solution**:
1. Verify `<html suppressHydrationWarning>` in layout
2. Ensure `ThemeProvider` wraps `{children}`
3. Check `tailwind.config.ts` has `darkMode: 'class'`
### Issue: shadcn components not found
**Cause**: `components.json` misconfigured or components not installed
**Solution**:
```bash
# Reinitialize shadcn/ui
npx shadcn-ui@latest init
# Re-add components
npx shadcn-ui@latest add button card input
```
### Issue: Focus styles not visible
**Cause**: Global CSS focus styles not applied
**Solution**:
- Verify `app/globals.css` includes focus styles layer
- Check `*:focus-visible` rule is present
- Ensure `--ring` CSS variable is defined
## Resources
This skill bundles comprehensive resources for reference:
### References (Loaded as Needed)
- **`tailwind-v4-migration.md`**: Tailwind v3 vs v4 differences, migration guide
- **`shadcn-component-list.md`**: Complete shadcn/ui component catalog with usage examples
- **accessibility-checklist.md**: WCAG 2.1 AA compliance checklist, best practices
- **`theme-tokens.md`**: Design token system, color customization guide
To read a reference:
```bash
# From skill directory
cat references/theme-tokens.md
```
### Scripts (Executable)
- **`setup_tailwind_shadcn.py`**: Main automation script for dependency installation and configuration
Execute directly:
```bash
python scripts/setup_tailwind_shadcn.py
```
### Assets (Templates for Output)
- **Config templates**: `tailwind.config.ts.template`, `postcss.config.js.template`, `globals.css.template`
- **Component templates**: `theme-provider.tsx`, `mode-toggle.tsx`, `app-shell.tsx`
- **Utility templates**: `lib/utils.ts`
- **Page templates**: `app/layout.tsx.template`, `app/page.tsx.template`
- **Example pages**: `app/examples/forms/`, `app/examples/dialogs/`, `app/examples/theme/`
Copy and customize as needed for the target project.
## Best Practices
### 1. Always Test Both Themes
After setup, manually toggle dark mode and verify:
- Color contrast ratios meet WCAG standards
- All text remains readable
- Focus indicators are visible
- Components render correctly
### 2. Use Semantic Tokens
```tsx
[OK] Good (semantic)
<div className="bg-primary text-primary-foreground">
[ERROR] Bad (hard-coded)
<div className="bg-blue-600 text-white">
```
### 3. Maintain Type Safety
Use TypeScript for all components:
```tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline'
}
```
### 4. Keep Accessibility in Mind
- Always pair labels with inputs
- Provide `aria-label` for icon-only buttons
- Test keyboard navigation
- Use semantic HTML
### 5. Document Customizations
When customizing tokens or components, document changes in project README or inline comments.
## Post-Setup Tasks
After running this skill, recommend users:
1. **Customize brand colors**
- Edit `--primary` in `app/globals.css`
- Test contrast ratios
2. **Add more components**
- Run `npx shadcn-ui add [component]` as needed
- See `references/shadcn-component-list.md` for options
3. **Configure responsive breakpoints**
- Adjust Tailwind `screens` in `tailwind.config.ts` if needed
4. **Set up linting**
- Install `eslint-plugin-jsx-a11y` for accessibility linting
- Add Prettier + Tailwind plugin for formatting
5. **Test production build**
```bash
npm run build
npm start
```
## Summary
This skill provides a complete, production-ready Tailwind CSS + shadcn/ui setup for Next.js 16 App Router projects. It handles:
[OK] Dependency installation (Tailwind, shadcn/ui, next-themes)
[OK] Configuration (Tailwind config, PostCSS, global CSS)
[OK] Dark mode setup (ThemeProvider, toggle component)
[OK] Base layout (responsive header, optional sidebar)
[OK] Design tokens (semantic CSS variables)
[OK] Accessibility (WCAG 2.1 AA, keyboard nav, screen readers)
[OK] Example pages (forms, dialogs, theme showcase)
[OK] Documentation (README updates, customization guides)
The setup is forward-compatible with Tailwind v4 and follows official Anthropic skill best practices.

View File

@@ -0,0 +1,266 @@
'use client'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { toast } from 'sonner'
export default function DialogsPage() {
const [isOpen, setIsOpen] = React.useState(false)
const handleSave = () => {
toast.success('Changes saved successfully!')
setIsOpen(false)
}
return (
<div className="container max-w-4xl py-8">
<div className="mb-8">
<h1 className="text-4xl font-bold">Dialog Examples</h1>
<p className="mt-2 text-lg text-muted-foreground">
Modal dialogs with focus trapping and keyboard navigation
</p>
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* Basic Dialog */}
<Card>
<CardHeader>
<CardTitle>Basic Dialog</CardTitle>
<CardDescription>Simple dialog with title and content</CardDescription>
</CardHeader>
<CardContent>
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Welcome!</DialogTitle>
<DialogDescription>
This is a basic dialog example. Press ESC or click outside to
close.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p className="text-sm text-muted-foreground">
Dialogs automatically handle:
</p>
<ul className="ml-6 mt-2 list-disc text-sm text-muted-foreground">
<li>Focus trapping</li>
<li>Keyboard navigation (ESC to close)</li>
<li>Backdrop click to dismiss</li>
<li>Return focus on close</li>
</ul>
</div>
<DialogFooter>
<Button onClick={() => toast.success('Action completed!')}>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
{/* Dialog with Form */}
<Card>
<CardHeader>
<CardTitle>Dialog with Form</CardTitle>
<CardDescription>Dialog containing a form with inputs</CardDescription>
</CardHeader>
<CardContent>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>Edit Profile</Button>
</DialogTrigger>
<DialogContent>
<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 gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="John Doe" />
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
defaultValue="john@example.com"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button onClick={handleSave}>Save Changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
{/* Confirmation Dialog */}
<Card>
<CardHeader>
<CardTitle>Confirmation Dialog</CardTitle>
<CardDescription>Destructive action confirmation</CardDescription>
</CardHeader>
<CardContent>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete Item</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete
your item and remove your data from our servers.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button
variant="destructive"
onClick={() => {
toast.error('Item deleted')
}}
>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
{/* Info Dialog */}
<Card>
<CardHeader>
<CardTitle>Information Dialog</CardTitle>
<CardDescription>Display information without actions</CardDescription>
</CardHeader>
<CardContent>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Show Info</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>About This App</DialogTitle>
<DialogDescription>
Built with modern web technologies
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div>
<h4 className="mb-2 text-sm font-medium">Stack</h4>
<ul className="space-y-1 text-sm text-muted-foreground">
<li>• Next.js 16 with App Router</li>
<li>• React 19</li>
<li>• Tailwind CSS v3/v4</li>
<li>• shadcn/ui components</li>
<li>• TypeScript</li>
</ul>
</div>
<div>
<h4 className="mb-2 text-sm font-medium">Features</h4>
<ul className="space-y-1 text-sm text-muted-foreground">
<li>• Dark mode support</li>
<li>• Fully accessible (WCAG 2.1 AA)</li>
<li>• Type-safe</li>
<li>• Production-ready</li>
</ul>
</div>
</div>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</div>
{/* Accessibility Info */}
<Card className="mt-6">
<CardHeader>
<CardTitle>Dialog Accessibility</CardTitle>
<CardDescription>
shadcn/ui dialogs are built on Radix UI with full accessibility
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4 text-sm">
<div>
<h4 className="mb-2 font-medium">Keyboard Navigation</h4>
<ul className="ml-6 list-disc space-y-1 text-muted-foreground">
<li>
<kbd className="rounded bg-muted px-1.5 py-0.5">ESC</kbd> -
Close dialog
</li>
<li>
<kbd className="rounded bg-muted px-1.5 py-0.5">Tab</kbd> -
Move focus forward
</li>
<li>
<kbd className="rounded bg-muted px-1.5 py-0.5">
Shift + Tab
</kbd>{' '}
- Move focus backward
</li>
</ul>
</div>
<div>
<h4 className="mb-2 font-medium">Screen Reader Support</h4>
<ul className="ml-6 list-disc space-y-1 text-muted-foreground">
<li>
Dialog title is announced via{' '}
<code className="rounded bg-muted px-1">aria-labelledby</code>
</li>
<li>
Description is read via{' '}
<code className="rounded bg-muted px-1">aria-describedby</code>
</li>
<li>Focus is trapped within dialog when open</li>
<li>Focus returns to trigger element on close</li>
</ul>
</div>
<div>
<h4 className="mb-2 font-medium">Mouse/Touch Support</h4>
<ul className="ml-6 list-disc space-y-1 text-muted-foreground">
<li>Click backdrop to dismiss (optional)</li>
<li>Close button in top-right corner</li>
<li>Scroll content if overflowing</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,195 @@
'use client'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { toast } from 'sonner'
export default function FormsPage() {
const [isSubmitting, setIsSubmitting] = React.useState(false)
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setIsSubmitting(true)
const formData = new FormData(event.currentTarget)
const data = {
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
}
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000))
console.log('Form submitted:', data)
toast.success('Form submitted successfully!')
// Reset form
event.currentTarget.reset()
setIsSubmitting(false)
}
return (
<div className="container max-w-4xl py-8">
<div className="mb-8">
<h1 className="text-4xl font-bold">Form Examples</h1>
<p className="mt-2 text-lg text-muted-foreground">
Accessible form components with HTML5 validation
</p>
</div>
<div className="space-y-8">
{/* Basic Form */}
<Card>
<CardHeader>
<CardTitle>Contact Form</CardTitle>
<CardDescription>
Basic form with HTML5 validation and toast notifications
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">
Name <span className="text-destructive">*</span>
</Label>
<Input
id="name"
name="name"
placeholder="John Doe"
required
aria-required="true"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">
Email <span className="text-destructive">*</span>
</Label>
<Input
id="email"
name="email"
type="email"
placeholder="john@example.com"
required
aria-required="true"
/>
</div>
<div className="space-y-2">
<Label htmlFor="message">
Message <span className="text-destructive">*</span>
</Label>
<textarea
id="message"
name="message"
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Your message here..."
required
aria-required="true"
/>
</div>
</CardContent>
<CardFooter className="flex gap-2">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
<Button type="reset" variant="outline" disabled={isSubmitting}>
Reset
</Button>
</CardFooter>
</form>
</Card>
{/* Form Validation Example */}
<Card>
<CardHeader>
<CardTitle>Advanced Validation</CardTitle>
<CardDescription>
For production forms, consider using React Hook Form + Zod
</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-lg bg-muted p-4">
<code className="text-sm">
<pre className="overflow-x-auto">
{`import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import * as z from 'zod'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
export function LoginForm() {
const form = useForm({
resolver: zodResolver(schema)
})
// ... form implementation
}`}
</pre>
</code>
</div>
<div className="mt-4">
<p className="text-sm text-muted-foreground">
Install dependencies:{' '}
<code className="rounded bg-muted px-1 py-0.5">
npm install react-hook-form @hookform/resolvers zod
</code>
</p>
</div>
</CardContent>
</Card>
{/* Accessibility Notes */}
<Card>
<CardHeader>
<CardTitle>Accessibility Guidelines</CardTitle>
<CardDescription>Best practices for accessible forms</CardDescription>
</CardHeader>
<CardContent>
<ul className="ml-6 list-disc space-y-2 text-sm">
<li>
<strong>Always pair labels with inputs</strong> using{' '}
<code className="rounded bg-muted px-1">htmlFor</code> and{' '}
<code className="rounded bg-muted px-1">id</code>
</li>
<li>
<strong>Mark required fields</strong> visually and with{' '}
<code className="rounded bg-muted px-1">aria-required</code>
</li>
<li>
<strong>Provide clear error messages</strong> linked with{' '}
<code className="rounded bg-muted px-1">aria-describedby</code>
</li>
<li>
<strong>Use appropriate input types</strong> (email, tel, url,
etc.)
</li>
<li>
<strong>Disable submit during processing</strong> with{' '}
<code className="rounded bg-muted px-1">aria-busy</code>
</li>
<li>
<strong>Don't use placeholder as label</strong> - placeholders
disappear on focus
</li>
</ul>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,32 @@
import { AppShell } from '@/components/app-shell'
import { FileText, Palette, SquareStack } from 'lucide-react'
const navigation = [
{
title: 'Forms',
href: '/examples/forms',
icon: FileText,
},
{
title: 'Dialogs',
href: '/examples/dialogs',
icon: SquareStack,
},
{
title: 'Theme',
href: '/examples/theme',
icon: Palette,
},
]
export default function ExamplesLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<AppShell navigation={navigation} siteTitle="Examples">
{children}
</AppShell>
)
}

View File

@@ -0,0 +1,303 @@
'use client'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { useTheme } from 'next-themes'
export default function ThemePage() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = React.useState(false)
// Prevent hydration mismatch
React.useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<div className="container max-w-6xl py-8">
<div className="mb-8">
<h1 className="text-4xl font-bold">Theme & Design Tokens</h1>
<p className="mt-2 text-lg text-muted-foreground">
Explore the color system and design tokens
</p>
</div>
{/* Theme Switcher */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Theme Switcher</CardTitle>
<CardDescription>
Current theme: <strong className="capitalize">{theme}</strong>
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Button
variant={theme === 'light' ? 'default' : 'outline'}
onClick={() => setTheme('light')}
>
Light
</Button>
<Button
variant={theme === 'dark' ? 'default' : 'outline'}
onClick={() => setTheme('dark')}
>
Dark
</Button>
<Button
variant={theme === 'system' ? 'default' : 'outline'}
onClick={() => setTheme('system')}
>
System
</Button>
</div>
</CardContent>
</Card>
{/* Color Tokens */}
<div className="space-y-8">
<Card>
<CardHeader>
<CardTitle>Semantic Colors</CardTitle>
<CardDescription>
Colors that describe purpose, not appearance
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2">
<ColorSwatch
label="Background"
description="Main background color"
className="bg-background text-foreground"
/>
<ColorSwatch
label="Foreground"
description="Main text color"
className="bg-foreground text-background"
/>
<ColorSwatch
label="Primary"
description="Brand color for main actions"
className="bg-primary text-primary-foreground"
/>
<ColorSwatch
label="Secondary"
description="Less prominent actions"
className="bg-secondary text-secondary-foreground"
/>
<ColorSwatch
label="Muted"
description="Disabled states, subtle backgrounds"
className="bg-muted text-muted-foreground"
/>
<ColorSwatch
label="Accent"
description="Highlights, hover states"
className="bg-accent text-accent-foreground"
/>
<ColorSwatch
label="Destructive"
description="Errors, delete actions"
className="bg-destructive text-destructive-foreground"
/>
<ColorSwatch
label="Card"
description="Elevated surfaces"
className="border bg-card text-card-foreground"
/>
</div>
</CardContent>
</Card>
{/* Component Variants */}
<Card>
<CardHeader>
<CardTitle>Button Variants</CardTitle>
<CardDescription>
Different button styles using semantic colors
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
<Separator className="my-4" />
<div className="flex flex-wrap gap-2">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</Button>
</div>
</CardContent>
</Card>
{/* Typography */}
<Card>
<CardHeader>
<CardTitle>Typography Scale</CardTitle>
<CardDescription>Text styles with proper hierarchy</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h1 className="text-4xl font-bold">Heading 1 (4xl)</h1>
<p className="text-sm text-muted-foreground">
text-4xl font-bold
</p>
</div>
<div>
<h2 className="text-3xl font-bold">Heading 2 (3xl)</h2>
<p className="text-sm text-muted-foreground">
text-3xl font-bold
</p>
</div>
<div>
<h3 className="text-2xl font-semibold">Heading 3 (2xl)</h3>
<p className="text-sm text-muted-foreground">
text-2xl font-semibold
</p>
</div>
<div>
<h4 className="text-xl font-semibold">Heading 4 (xl)</h4>
<p className="text-sm text-muted-foreground">
text-xl font-semibold
</p>
</div>
<div>
<p className="text-base">Body text (base)</p>
<p className="text-sm text-muted-foreground">text-base</p>
</div>
<div>
<p className="text-sm">Small text (sm)</p>
<p className="text-sm text-muted-foreground">text-sm</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
Extra small (xs)
</p>
<p className="text-sm text-muted-foreground">text-xs</p>
</div>
</div>
</CardContent>
</Card>
{/* Customization Guide */}
<Card>
<CardHeader>
<CardTitle>Customization</CardTitle>
<CardDescription>How to customize your theme</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h4 className="mb-2 font-medium">1. Update CSS Variables</h4>
<p className="mb-2 text-sm text-muted-foreground">
Edit <code className="rounded bg-muted px-1">app/globals.css</code>:
</p>
<pre className="overflow-x-auto rounded-lg bg-muted p-4 text-sm">
<code>{`:root {
--primary: 270 80% 45%; /* Change to your brand color */
--radius: 0.75rem; /* Adjust border radius */
}`}</code>
</pre>
</div>
<div>
<h4 className="mb-2 font-medium">2. Use HSL Color Picker</h4>
<p className="text-sm text-muted-foreground">
Find HSL values using:{' '}
<a
href="https://hslpicker.com"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
HSL Color Picker
</a>
</p>
</div>
<div>
<h4 className="mb-2 font-medium">3. Test Contrast</h4>
<p className="text-sm text-muted-foreground">
Ensure WCAG compliance using:{' '}
<a
href="https://webaim.org/resources/contrastchecker/"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
WebAIM Contrast Checker
</a>
</p>
</div>
<div>
<h4 className="mb-2 font-medium">4. Test Both Themes</h4>
<p className="text-sm text-muted-foreground">
Always verify colors look good in both light and dark mode
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
function ColorSwatch({
label,
description,
className,
}: {
label: string
description: string
className: string
}) {
return (
<div className="space-y-2">
<div
className={`flex h-20 items-center justify-center rounded-lg border ${className}`}
>
<span className="font-medium">{label}</span>
</div>
<div>
<p className="text-sm font-medium">{label}</p>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,40 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { ThemeProvider } from '@/components/theme-provider'
import { Toaster } from 'sonner'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{/* Skip link for accessibility */}
<a href="#main-content" className="skip-link">
Skip to main content
</a>
{children}
<Toaster richColors closeButton position="top-right" />
</ThemeProvider>
</body>
</html>
)
}

View File

@@ -0,0 +1,141 @@
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
export default function Home() {
return (
<main id="main-content" className="container py-8">
{/* Hero section */}
<section className="flex flex-col items-center gap-4 py-12 text-center">
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl lg:text-7xl">
Welcome to Your App
</h1>
<p className="max-w-[700px] text-lg text-muted-foreground sm:text-xl">
Built with Next.js 16, Tailwind CSS, and shadcn/ui. Beautiful,
accessible, and fully customizable.
</p>
<div className="flex gap-4">
<Button size="lg" asChild>
<Link href="/examples">View Examples</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link href="https://ui.shadcn.com" target="_blank" rel="noopener noreferrer">
Documentation
</Link>
</Button>
</div>
</section>
{/* Features grid */}
<section className="py-12">
<h2 className="mb-8 text-center text-3xl font-bold">Features</h2>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>Tailwind CSS</CardTitle>
<CardDescription>
Utility-first CSS framework with custom design tokens
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Fully configured with CSS variables for easy theming and
customization.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>shadcn/ui</CardTitle>
<CardDescription>
Beautiful, accessible component library
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Built on Radix UI primitives with full TypeScript support.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Dark Mode</CardTitle>
<CardDescription>
Automatic dark mode with system preference
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Seamless theme switching with high contrast in both modes.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Accessibility</CardTitle>
<CardDescription>
WCAG 2.1 Level AA compliant
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Built with semantic HTML, ARIA attributes, and keyboard
navigation.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Type Safe</CardTitle>
<CardDescription>
Full TypeScript support
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Catch errors at compile time with comprehensive type definitions.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Production Ready</CardTitle>
<CardDescription>
Optimized for performance
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Fast builds, small bundles, and excellent Lighthouse scores.
</p>
</CardContent>
</Card>
</div>
</section>
{/* CTA section */}
<section className="flex flex-col items-center gap-4 py-12 text-center">
<h2 className="text-3xl font-bold">Ready to build?</h2>
<p className="max-w-[600px] text-muted-foreground">
Explore the example pages to see components in action, or start
building your own features.
</p>
<Button asChild>
<Link href="/examples">Explore Examples</Link>
</Button>
</section>
</main>
)
}

View File

@@ -0,0 +1,162 @@
'use client'
import * as React from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Menu } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import { Separator } from '@/components/ui/separator'
import { ModeToggle } from '@/components/mode-toggle'
import { cn } from '@/lib/utils'
interface NavItem {
title: string
href: string
icon?: React.ComponentType<{ className?: string }>
}
interface AppShellProps {
children: React.ReactNode
navigation?: NavItem[]
siteTitle?: string
}
export function AppShell({
children,
navigation = [],
siteTitle = 'App',
}: AppShellProps) {
const pathname = usePathname()
const [open, setOpen] = React.useState(false)
return (
<div className="flex min-h-screen flex-col">
{/* Header */}
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center justify-between">
<div className="flex items-center gap-4">
{/* Mobile menu trigger */}
{navigation.length > 0 && (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
size="icon"
className="md:hidden"
aria-label="Open menu"
>
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-64 pr-0">
<MobileNav
items={navigation}
pathname={pathname}
onItemClick={() => setOpen(false)}
/>
</SheetContent>
</Sheet>
)}
{/* Site title */}
<Link href="/" className="flex items-center space-x-2">
<span className="text-xl font-bold">{siteTitle}</span>
</Link>
</div>
{/* Right side actions */}
<div className="flex items-center gap-2">
<ModeToggle />
</div>
</div>
</header>
<div className="flex flex-1">
{/* Desktop sidebar */}
{navigation.length > 0 && (
<aside className="hidden w-64 border-r bg-background md:block">
<nav className="sticky top-16 flex h-[calc(100vh-4rem)] flex-col gap-2 p-4">
<DesktopNav items={navigation} pathname={pathname} />
</nav>
</aside>
)}
{/* Main content */}
<main id="main-content" className="flex-1">
{children}
</main>
</div>
</div>
)
}
function MobileNav({
items,
pathname,
onItemClick,
}: {
items: NavItem[]
pathname: string
onItemClick: () => void
}) {
return (
<nav className="flex flex-col gap-2 py-4">
{items.map((item) => {
const Icon = item.icon
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
onClick={onItemClick}
className={cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
)}
>
{Icon && <Icon className="h-4 w-4" />}
{item.title}
</Link>
)
})}
</nav>
)
}
function DesktopNav({
items,
pathname,
}: {
items: NavItem[]
pathname: string
}) {
return (
<>
{items.map((item) => {
const Icon = item.icon
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
)}
>
{Icon && <Icon className="h-4 w-4" />}
{item.title}
</Link>
)
})}
</>
)
}

View File

@@ -0,0 +1,40 @@
'use client'
import * as React from 'react'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Toggle theme">
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -0,0 +1,9 @@
'use client'
import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { type ThemeProviderProps } from 'next-themes/dist/types'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@@ -0,0 +1,97 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply border-border;
}
html {
@apply scroll-smooth;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
/* Base theme: {{THEME_PRESET}} */
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
/* Focus styles */
*:focus-visible {
@apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
}
@layer components {
/* Skip link for accessibility */
.skip-link {
@apply sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50;
@apply px-4 py-2 bg-background border-2 border-ring rounded-md;
@apply text-foreground font-medium;
}
}
@layer utilities {
/* Text balance for headings */
.text-balance {
text-wrap: balance;
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,75 @@
import type { Config } from 'tailwindcss'
// TODO: When Tailwind v4 is stable, consider migrating to @theme directive
// See references/tailwind-v4-migration.md for details
export default {
darkMode: 'class',
content: [
'./app/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./lib/**/*.{ts,tsx}',
],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
} satisfies Config

View File

@@ -0,0 +1,484 @@
# Accessibility Checklist for Tailwind + shadcn/ui Setup
## Overview
This checklist ensures the Tailwind + shadcn/ui setup meets WCAG 2.1 Level AA standards and provides an inclusive user experience.
## Color & Contrast
### Requirements
- [OK] Normal text (< 18pt): Contrast ratio ≥ 4.5:1
- [OK] Large text (≥ 18pt or bold 14pt): Contrast ratio ≥ 3:1
- [OK] UI components: Contrast ratio ≥ 3:1
- [OK] Focus indicators: Contrast ratio ≥ 3:1
### Implementation
```css
/* Light mode - High contrast */
:root {
--background: 0 0% 100%; /* White */
--foreground: 222.2 84% 4.9%; /* Near black - 16.7:1 ratio */
--muted: 210 40% 96.1%; /* Light gray background */
--muted-foreground: 215.4 16.3% 46.9%; /* Medium gray text - 4.6:1 ratio */
--border: 214.3 31.8% 91.4%; /* Light border */
}
/* Dark mode - High contrast */
.dark {
--background: 222.2 84% 4.9%; /* Near black */
--foreground: 210 40% 98%; /* Near white - 16.5:1 ratio */
--muted: 217.2 32.6% 17.5%; /* Dark gray background */
--muted-foreground: 215 20.2% 65.1%; /* Light gray text - 6.8:1 ratio */
--border: 217.2 32.6% 17.5%; /* Dark border */
}
```
### Testing Contrast
```bash
# Use online tools:
# - https://webaim.org/resources/contrastchecker/
# - https://contrast-ratio.com/
# - Chrome DevTools (Lighthouse audit)
# Or programmatically:
npm install --save-dev axe-core @axe-core/playwright
```
## Focus Management
### Visible Focus Indicators
```css
/* Global focus styles */
@layer base {
*:focus-visible {
@apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background;
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
*:focus-visible {
@apply transition-none;
}
}
}
```
### Focus Order
- [OK] Logical tab order (follows visual order)
- [OK] No keyboard traps
- [OK] Skip links for navigation
- [OK] Focus moves appropriately in modals/dialogs
### Implementation
```tsx
// Skip link (in layout)
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50"
>
Skip to main content
</a>
{/* Rest of layout */}
<main id="main-content">
{children}
</main>
```
## Keyboard Navigation
### Requirements
- [OK] All interactive elements keyboard accessible
- [OK] Logical tab order
- [OK] Keyboard shortcuts don't conflict
- [OK] Escape closes modals/dropdowns
- [OK] Arrow keys for menus/lists
- [OK] Enter/Space activates buttons
### shadcn/ui Components
All shadcn/ui components support keyboard navigation out of the box:
```tsx
// Dialog - auto-handles:
// - ESC to close
// - Focus trap
// - Return focus on close
<Dialog>
<DialogContent>
<DialogTitle>Title</DialogTitle>
{/* Content */}
</DialogContent>
</Dialog>
// Dropdown - auto-handles:
// - Arrow keys for navigation
// - Enter to select
// - ESC to close
<DropdownMenu>
<DropdownMenuTrigger>Menu</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item 1</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
```
## Semantic HTML
### Use Proper Elements
```tsx
[OK] Good - Semantic
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
<main>
<article>
<h1>Article Title</h1>
<p>Content...</p>
</article>
</main>
<footer>
<p>&copy; 2025 Company</p>
</footer>
[ERROR] Bad - Non-semantic
<div className="nav">
<div className="link" onClick={goHome}>Home</div>
</div>
<div className="main">
<div className="article">
<div className="title">Article Title</div>
<div>Content...</div>
</div>
</div>
```
### Heading Hierarchy
```tsx
[OK] Good - Logical hierarchy
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
<h2>Another Section</h2>
[ERROR] Bad - Skips levels
<h1>Page Title</h1>
<h3>Section</h3> {/* Skipped h2 */}
<h2>Another Section</h2> {/* Out of order */}
```
## Form Accessibility
### Always Pair Labels with Inputs
```tsx
[OK] Good
<div className="space-y-2">
<Label htmlFor="email">Email address</Label>
<Input id="email" type="email" required aria-required="true" />
</div>
[ERROR] Bad
<Input placeholder="Email address" /> {/* Placeholder is not a label */}
```
### Error Messages
```tsx
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
aria-invalid={!!errors.password}
aria-describedby={errors.password ? "password-error" : undefined}
/>
{errors.password && (
<p id="password-error" className="text-sm text-destructive" role="alert">
{errors.password.message}
</p>
)}
</div>
```
### Required Fields
```tsx
<Label htmlFor="username">
Username
<span className="text-destructive" aria-label="required">*</span>
</Label>
<Input id="username" required aria-required="true" />
```
## ARIA Attributes
### When to Use ARIA
**First Rule**: Don't use ARIA unless necessary. Use semantic HTML first.
```tsx
[OK] Good - Semantic HTML (no ARIA needed)
<button>Click me</button>
[ERROR] Unnecessary ARIA
<div role="button" tabIndex={0} onClick={...}>Click me</div>
```
### Common ARIA Patterns
```tsx
// Live regions for dynamic content
<div role="status" aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// Loading state
<Button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? "Loading..." : "Submit"}
</Button>
// Icon buttons need labels
<Button size="icon" aria-label="Close menu">
<X className="h-4 w-4" />
</Button>
// Expanded/collapsed state
<Button
aria-expanded={isOpen}
aria-controls="content-id"
onClick={() => setIsOpen(!isOpen)}
>
Toggle
</Button>
<div id="content-id" hidden={!isOpen}>
Content
</div>
```
## Screen Reader Support
### Visually Hidden Content
Use `.sr-only` for screen-reader-only text:
```tsx
<Button>
<span className="sr-only">Delete item</span>
<Trash className="h-4 w-4" aria-hidden="true" />
</Button>
```
### Skip Links
```tsx
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-background focus:ring-2"
>
Skip to main content
</a>
```
### Icon-Only Buttons
```tsx
// Always provide text alternative
<Button variant="ghost" size="icon" aria-label="Search">
<Search className="h-4 w-4" />
</Button>
// Or use tooltip with title
<Button variant="ghost" size="icon" title="Search">
<Search className="h-4 w-4" />
<span className="sr-only">Search</span>
</Button>
```
## Motion & Animation
### Respect User Preferences
```css
/* Disable animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
```
### Implementation in Components
```tsx
// Add to globals.css
@layer base {
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
}
```
## Dark Mode Accessibility
### Proper Contrast in Both Modes
```css
/* Test both themes */
:root {
/* Light mode */
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* Ensure ≥ 4.5:1 ratio */
}
.dark {
/* Dark mode */
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
/* Ensure ≥ 4.5:1 ratio */
}
```
### Flash Prevention
```tsx
// In app/layout.tsx
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange {/* Prevent flash */}
>
{children}
</ThemeProvider>
</body>
</html>
```
## Testing Checklist
### Manual Testing
- [ ] Navigate entire app with keyboard only (no mouse)
- [ ] Test with screen reader (NVDA, JAWS, VoiceOver)
- [ ] Zoom to 200% and ensure layout doesn't break
- [ ] Test in high contrast mode
- [ ] Verify dark mode contrast
- [ ] Check focus indicators on all interactive elements
- [ ] Test form validation with screen reader
### Automated Testing
```bash
# Install axe-core
npm install --save-dev @axe-core/playwright
# Use in tests
import { test, expect } from '@playwright/test'
import { injectAxe, checkA11y } from 'axe-playwright'
test('homepage is accessible', async ({ page }) => {
await page.goto('http://localhost:3000')
await injectAxe(page)
await checkA11y(page)
})
```
### Browser DevTools
- Chrome Lighthouse (Accessibility audit)
- Firefox Accessibility Inspector
- Edge Accessibility Insights
- axe DevTools Extension
## Common Issues & Fixes
### Issue: Missing Form Labels
```tsx
[ERROR] Problem
<Input placeholder="Email" />
[OK] Fix
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" placeholder="you@example.com" />
</div>
```
### Issue: Non-Accessible Custom Components
```tsx
[ERROR] Problem
<div onClick={handleClick} className="cursor-pointer">
Click me
</div>
[OK] Fix
<button onClick={handleClick} type="button">
Click me
</button>
```
### Issue: Low Color Contrast
```tsx
[ERROR] Problem
<p className="text-gray-400">Important text</p> {/* 2.8:1 ratio */}
[OK] Fix
<p className="text-gray-700 dark:text-gray-300">Important text</p> {/* 5.2:1 ratio */}
```
### Issue: Missing Focus Indicators
```tsx
[ERROR] Problem
<Button className="focus:outline-none">Click</Button>
[OK] Fix
<Button className="focus-visible:ring-2 focus-visible:ring-ring">Click</Button>
```
## Resources
- **WCAG Guidelines**: https://www.w3.org/WAI/WCAG21/quickref/
- **WebAIM**: https://webaim.org/
- **a11y Project**: https://www.a11yproject.com/
- **Radix UI (shadcn base)**: https://radix-ui.com (includes A11y)
- **MDN Accessibility**: https://developer.mozilla.org/en-US/docs/Web/Accessibility
## Skill Implementation
This skill ensures:
- [OK] High contrast color tokens (≥ 4.5:1 for text)
- [OK] Visible focus styles with ring utilities
- [OK] Skip link in base layout
- [OK] Semantic HTML landmarks (header, nav, main, footer)
- [OK] Proper label/input associations
- [OK] Dark mode with accessible contrast
- [OK] Reduced motion support
- [OK] All shadcn components use Radix (accessible primitives)

View File

@@ -0,0 +1,370 @@
# shadcn/ui Component Reference
## Overview
shadcn/ui provides a collection of re-usable components built with Radix UI and Tailwind CSS. Components are NOT installed as dependencies but copied into your project, giving you full control.
## Installation Command
```bash
npx shadcn-ui@latest add [component-name]
```
## Baseline Components for This Skill
The following components are installed by default:
### Core UI Elements
- **button**: Primary interactive element with variants
- **card**: Container for grouped content
- **input**: Text input field
- **label**: Form label with accessibility support
- **dialog**: Modal dialog/overlay
- **separator**: Visual or semantic separator
### Layout & Navigation
- **sheet**: Slide-over panel (mobile sidebar)
- **dropdown-menu**: Contextual menu with submenus
- **navigation-menu**: Main navigation component
### Feedback & Notifications
- **toast**: Temporary notifications (via Sonner)
- **alert**: Static notification messages
- **badge**: Status or category indicator
## Available Components by Category
### Form Components
- `checkbox` - Checkbox input with label
- `input` - Text input field
- `label` - Form label
- `textarea` - Multi-line text input
- `select` - Dropdown select
- `radio-group` - Radio button group
- `switch` - Toggle switch
- `slider` - Range slider
- `form` - Form wrapper with React Hook Form integration
- `combobox` - Searchable select (autocomplete)
- `date-picker` - Date selection with calendar
- `calendar` - Calendar component
### Layout Components
- `card` - Content card with header/footer
- `sheet` - Slide-over panel
- `dialog` - Modal dialog
- `popover` - Floating popover
- `drawer` - Bottom drawer (mobile)
- `separator` - Divider line
- `scroll-area` - Custom scrollable area
- `aspect-ratio` - Maintain aspect ratio
- `resizable` - Resizable panels
### Navigation Components
- `navigation-menu` - Main navigation
- `menubar` - Desktop menu bar
- `dropdown-menu` - Context/dropdown menu
- `context-menu` - Right-click context menu
- `tabs` - Tab navigation
- `breadcrumb` - Breadcrumb navigation
- `pagination` - Page navigation
- `command` - Command palette (⌘K)
### Feedback Components
- `toast` - Toast notifications
- `alert` - Alert messages
- `alert-dialog` - Confirmation dialog
- `badge` - Status badge
- `progress` - Progress indicator
- `skeleton` - Loading skeleton
- `spinner` - Loading spinner
### Data Display
- `table` - Data table
- `avatar` - User avatar
- `tooltip` - Hover tooltip
- `accordion` - Collapsible sections
- `collapsible` - Simple collapsible
- `hover-card` - Rich hover card
- `data-table` - Advanced data table with sorting/filtering
### Utility Components
- `toggle` - Toggle button
- `toggle-group` - Toggle button group
- `sonner` - Toast notification library integration
- `carousel` - Image/content carousel
## Usage Patterns
### Adding a Single Component
```bash
npx shadcn-ui add button
```
This creates:
- `components/ui/button.tsx`
- Updates `components.json` if needed
### Adding Multiple Components
```bash
npx shadcn-ui add button card input label
```
### Using a Component
```tsx
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
export default function Example() {
return (
<Card>
<CardHeader>
<CardTitle>Example Card</CardTitle>
</CardHeader>
<CardContent>
<Button>Click me</Button>
</CardContent>
</Card>
)
}
```
## Component Composition Patterns
### Form with Validation
```tsx
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { toast } from "sonner"
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
export function LoginForm() {
const form = useForm({
resolver: zodResolver(schema)
})
const onSubmit = (data) => {
toast.success("Login successful!")
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" {...form.register("email")} />
</div>
<Button type="submit">Login</Button>
</form>
)
}
```
### Dialog with Form
```tsx
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function CreateDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Create New</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Item</DialogTitle>
</DialogHeader>
<form>
<Input placeholder="Item name" />
<Button type="submit">Create</Button>
</form>
</DialogContent>
</Dialog>
)
}
```
### Navigation with Dropdown
```tsx
import { NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuLink } from "@/components/ui/navigation-menu"
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"
import { Button } from "@/components/ui/button"
export function AppNav() {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink href="/">Home</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">Products</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Product A</DropdownMenuItem>
<DropdownMenuItem>Product B</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
)
}
```
## Customization
### Modifying Component Styles
Components use Tailwind classes and can be customized:
```tsx
// Customize colors via CSS variables in globals.css
:root {
--primary: 200 100% 50%; /* Change primary color */
}
// Or override with props
<Button className="bg-purple-600 hover:bg-purple-700">
Custom Color
</Button>
```
### Creating Variants
Use `class-variance-authority` (CVA) for variants:
```tsx
import { cva } from "class-variance-authority"
const alertVariants = cva(
"rounded-lg border p-4",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive: "bg-destructive text-destructive-foreground",
success: "bg-green-50 text-green-900"
}
},
defaultVariants: {
variant: "default"
}
}
)
```
## Accessibility Features
All shadcn/ui components include:
- [OK] Proper ARIA attributes
- [OK] Keyboard navigation
- [OK] Focus management
- [OK] Screen reader support
- [OK] Reduced motion support
### Example: Dialog Accessibility
```tsx
<Dialog>
{/* Auto-handled: */}
{/* - Focus trap */}
{/* - ESC to close */}
{/* - aria-labelledby */}
{/* - Backdrop click to close */}
<DialogContent>
<DialogHeader>
<DialogTitle>Title (used for aria-labelledby)</DialogTitle>
</DialogHeader>
</DialogContent>
</Dialog>
```
## Best Practices
### 1. Always Use Labels with Inputs
```tsx
[OK] Good
<div>
<Label htmlFor="username">Username</Label>
<Input id="username" />
</div>
[ERROR] Bad
<Input placeholder="Username" /> {/* Placeholder is not a label */}
```
### 2. Provide Accessible Names
```tsx
[OK] Good
<Button aria-label="Close menu">
<X className="h-4 w-4" />
</Button>
[ERROR] Bad
<Button>
<X className="h-4 w-4" /> {/* No text or aria-label */}
</Button>
```
### 3. Handle Loading States
```tsx
<Button disabled={isLoading}>
{isLoading ? "Loading..." : "Submit"}
</Button>
```
### 4. Use Semantic HTML
```tsx
[OK] Good
<form onSubmit={handleSubmit}>
<Button type="submit">Submit</Button>
</form>
[ERROR] Bad
<div onClick={handleSubmit}>
<Button type="button">Submit</Button>
</div>
```
## Resources
- **Official Docs**: https://ui.shadcn.com
- **Component Examples**: https://ui.shadcn.com/examples
- **Radix UI Docs**: https://radix-ui.com
- **GitHub**: https://github.com/shadcn-ui/ui
## Integration with This Skill
The skill installs these baseline components automatically:
1. `button` - Primary actions
2. `card` - Content containers
3. `input` - Form inputs
4. `label` - Form labels
5. `dialog` - Modals
6. `separator` - Visual dividers
Additional components can be added as needed:
```bash
npx shadcn-ui add [component-name]
```

View File

@@ -0,0 +1,206 @@
# Tailwind CSS v4 Migration Guide
## Overview
Tailwind CSS v4 introduces significant changes to configuration and usage patterns. This document outlines the differences and provides migration guidance.
## Current Status (as of January 2025)
- **Tailwind CSS v3.x**: Stable, widely adopted
- **Tailwind CSS v4**: In development, alpha/beta releases available
- **Recommendation**: Use v3 with forward-compatible patterns for production projects
## Key Differences: v3 vs v4
### Configuration Format
**v3 (Current)**
```ts
// tailwind.config.ts
import type { Config } from 'tailwindcss'
export default {
darkMode: 'class',
content: ['./app/**/*.{ts,tsx}'],
theme: {
extend: {
colors: { /* ... */ }
}
},
plugins: []
} satisfies Config
```
**v4 (Future)**
```css
/* @config directive in CSS */
@config "./tailwind.config.js";
@theme {
--color-brand: #3b82f6;
--font-sans: system-ui, sans-serif;
}
```
### CSS Variables & Tokens
**v3 Pattern (Forward-Compatible)**
```css
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
}
```
```ts
// tailwind.config.ts
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))'
}
```
**v4 Expected Pattern**
```css
@theme {
--color-background: oklch(100% 0 0);
--color-foreground: oklch(20% 0.02 270);
}
```
### Plugin System
**v3**: JavaScript-based plugins in config file
**v4**: CSS-based plugins with `@plugin` directive
## Migration Strategy
### Phase 1: v3 with Forward-Compatible Patterns (Current)
Use CSS variables extensively:
- Define semantic tokens (--primary, --background, etc.)
- Use HSL/OKLCH color space for easier manipulation
- Structure CSS variables to match expected v4 patterns
### Phase 2: v4 Alpha/Beta Testing (When Available)
Test v4 alphas in non-production projects:
```bash
npm install tailwindcss@next
```
Update configuration:
- Move color tokens to `@theme` directive
- Convert plugins to CSS-based format
- Test build performance and output
### Phase 3: v4 Stable Migration (Future)
When v4 is stable:
1. Update package: `npm install tailwindcss@latest`
2. Migrate config format (may have migration tool)
3. Test thoroughly across all components
4. Update documentation
## Recommended Patterns for This Skill
### Use CSS Custom Properties
[OK] **Good (Forward-Compatible)**
```css
:root {
--radius: 0.5rem;
--primary: 222.2 47.4% 11.2%;
}
.card {
border-radius: var(--radius);
background: hsl(var(--primary));
}
```
[ERROR] **Avoid (Hard-Coded)**
```css
.card {
border-radius: 0.5rem;
background: #1e293b;
}
```
### Semantic Color Naming
Use semantic names that describe purpose, not appearance:
- [OK] `--primary`, `--destructive`, `--muted`
- [ERROR] `--blue-500`, `--red-600`, `--gray-200`
### HSL Color Space
Use HSL (or OKLCH when v4 is available) for better color manipulation:
```css
/* HSL: Hue Saturation Lightness */
--primary: 222.2 47.4% 11.2%;
/* Usage in CSS */
background: hsl(var(--primary));
background: hsl(var(--primary) / 0.8); /* With alpha */
```
## Checking for v4 Availability
```bash
# Check installed version
npm list tailwindcss
# Check latest available version
npm view tailwindcss versions --json
# Install specific version
npm install tailwindcss@4.0.0 # When available
```
## V4-Specific Features to Watch For
### CSS-First Configuration
- `@theme` directive for token definition
- `@plugin` for extending functionality
- Native CSS nesting support
### Improved Color System
- OKLCH color space support
- Better color mixing
- Improved dark mode handling
### Performance Improvements
- Faster build times
- Smaller CSS output
- Better JIT performance
### New Utilities
- Container queries (better support)
- Cascade layers
- View transitions
## Resources
- **Tailwind CSS Docs**: https://tailwindcss.com/docs
- **V4 Alpha Docs**: https://tailwindcss.com/docs/v4-alpha
- **GitHub Discussions**: https://github.com/tailwindlabs/tailwindcss/discussions
- **Upgrade Guide**: https://tailwindcss.com/docs/upgrade-guide (when v4 is stable)
## Notes for Skill Usage
When this skill runs:
1. Check Tailwind version: `npm view tailwindcss version`
2. If v4 stable is available: Use v4 configuration patterns
3. If v4 is not available: Use v3 with forward-compatible CSS variables
4. Add comments in generated files indicating v4 migration points:
```ts
// TODO: When upgrading to Tailwind v4, move these tokens to @theme directive
```
This approach ensures the skill produces production-ready code today while being ready for v4 when it arrives.

View File

@@ -0,0 +1,548 @@
# Theme Token System
## Overview
This document describes the CSS custom property-based design token system used for Tailwind + shadcn/ui theming.
## Token Philosophy
### Semantic Naming
Tokens describe **purpose**, not appearance:
- [OK] `--primary` (describes role)
- [ERROR] `--blue-600` (describes appearance)
### Benefits
- Easy theme switching
- Consistent design system
- Automatic dark mode support
- Forward-compatible with Tailwind v4
## Color Token Structure
### HSL Format
All colors use HSL (Hue, Saturation, Lightness) format without the `hsl()` wrapper:
```css
/* Define as space-separated values */
--primary: 222.2 47.4% 11.2%;
/* Use with hsl() wrapper */
background-color: hsl(var(--primary));
/* With alpha transparency */
background-color: hsl(var(--primary) / 0.5);
```
### Why HSL?
- **Intuitive**: Easier to adjust than RGB
- **Lightness control**: Simple to create shades
- **Alpha transparency**: Works with modern CSS syntax
- **Tooling**: Better design tool support
## Core Color Tokens
### Base Colors
```css
:root {
/* Background & Foreground */
--background: 0 0% 100%; /* Pure white */
--foreground: 222.2 84% 4.9%; /* Near black text */
/* Card (elevated surfaces) */
--card: 0 0% 100%; /* White card background */
--card-foreground: 222.2 84% 4.9%; /* Card text */
/* Popover (floating UI) */
--popover: 0 0% 100%; /* White popover */
--popover-foreground: 222.2 84% 4.9%; /* Popover text */
}
```
### Semantic Colors
```css
:root {
/* Primary (brand color, main actions) */
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* Secondary (less prominent actions) */
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
/* Muted (disabled states, subtle backgrounds) */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Accent (highlights, hover states) */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Destructive (errors, delete actions) */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
}
```
### UI Element Colors
```css
:root {
/* Borders */
--border: 214.3 31.8% 91.4%; /* Subtle border */
--input: 214.3 31.8% 91.4%; /* Input border */
/* Focus & Selection */
--ring: 222.2 84% 4.9%; /* Focus ring */
/* Status Colors (optional) */
--success: 142 76% 36%;
--success-foreground: 0 0% 100%;
--warning: 38 92% 50%;
--warning-foreground: 0 0% 100%;
--info: 199 89% 48%;
--info-foreground: 0 0% 100%;
}
```
## Dark Mode Tokens
### Dark Mode Strategy
Use `.dark` class to override tokens:
```css
.dark {
/* Base */
--background: 222.2 84% 4.9%; /* Near black */
--foreground: 210 40% 98%; /* Near white */
/* Card */
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
/* Primary */
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* Secondary */
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
/* Muted */
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
/* Accent */
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
/* Destructive */
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
/* Borders */
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
/* Focus */
--ring: 212.7 26.8% 83.9%;
}
```
## Theme Presets
### Zinc (Default)
Cool, neutral gray tones. Professional and versatile.
```css
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--border: 240 5.9% 90%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--border: 240 3.7% 15.9%;
}
```
### Slate
Slightly cooler than Zinc. Tech-focused feel.
```css
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--border: 214.3 31.8% 91.4%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--border: 217.2 32.6% 17.5%;
}
```
### Neutral
True neutral grays. Clean and minimal.
```css
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--border: 0 0% 89.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--border: 0 0% 14.9%;
}
```
## Spacing & Sizing Tokens
### Border Radius
```css
:root {
--radius: 0.5rem; /* Base radius: 8px */
}
/* Usage in Tailwind config */
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
}
```
### Font Family (Optional)
```css
:root {
--font-sans: ui-sans-serif, system-ui, sans-serif;
--font-mono: ui-monospace, 'Cascadia Code', monospace;
}
```
## Usage in Tailwind Config
### Extending Theme
```ts
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
}
}
```
## Using Tokens in Components
### In CSS
```css
.custom-component {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
}
/* With alpha */
.overlay {
background-color: hsl(var(--background) / 0.8);
}
```
### With Tailwind Utilities
```tsx
<div className="bg-background text-foreground border-border">
<h1 className="text-primary">Heading</h1>
<p className="text-muted-foreground">Subtitle</p>
<Button variant="destructive">Delete</Button>
</div>
```
## Customization Guide
### Changing Primary Color
1. **Find your HSL values**: Use a color picker that shows HSL
2. **Update light mode**:
```css
:root {
--primary: 270 80% 45%; /* Purple example */
--primary-foreground: 0 0% 100%; /* White text */
}
```
3. **Update dark mode**:
```css
.dark {
--primary: 270 80% 65%; /* Lighter purple */
--primary-foreground: 240 10% 10%; /* Dark text */
}
```
4. **Test contrast**: Use WebAIM contrast checker
### Creating Custom Tokens
```css
/* Add to globals.css */
:root {
/* Custom tokens */
--sidebar-width: 16rem;
--header-height: 4rem;
--brand-gradient: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)));
}
/* Use in components */
.sidebar {
width: var(--sidebar-width);
}
```
### Adding More Semantic Colors
```css
:root {
--success: 142 76% 36%;
--success-foreground: 0 0% 100%;
--warning: 38 92% 50%;
--warning-foreground: 0 0% 100%;
}
.dark {
--success: 142 71% 45%;
--warning: 38 92% 60%;
}
```
```ts
// Add to tailwind.config.ts
colors: {
success: {
DEFAULT: 'hsl(var(--success))',
foreground: 'hsl(var(--success-foreground))'
},
warning: {
DEFAULT: 'hsl(var(--warning))',
foreground: 'hsl(var(--warning-foreground))'
}
}
```
## Best Practices
### 1. Always Pair Background/Foreground
```tsx
[OK] Good
<div className="bg-primary text-primary-foreground">
Readable text
</div>
[ERROR] Bad
<div className="bg-primary text-foreground">
Low contrast
</div>
```
### 2. Test Both Themes
Always verify colors in both light and dark mode:
```bash
# Use browser DevTools to toggle:
document.documentElement.classList.toggle('dark')
```
### 3. Use Semantic Tokens
```tsx
[OK] Good (semantic)
<p className="text-muted-foreground">Helper text</p>
<Button variant="destructive">Delete</Button>
[ERROR] Bad (hard-coded)
<p className="text-gray-500 dark:text-gray-400">Helper text</p>
<Button className="bg-red-600">Delete</Button>
```
### 4. Maintain Contrast Ratios
- Normal text: ≥ 4.5:1
- Large text: ≥ 3:1
- UI components: ≥ 3:1
## Tools for Theme Development
### Color Pickers with HSL
- [HSL Color Picker](https://hslpicker.com/)
- [Coolors](https://coolors.co/)
- [Palettte App](https://palettte.app/)
### Contrast Checkers
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [Contrast Ratio](https://contrast-ratio.com/)
- Chrome DevTools (Lighthouse)
### Theme Generators
- [Realtime Colors](https://realtimecolors.com/)
- [shadcn/ui Themes](https://ui.shadcn.com/themes)
- [Tailwind Color Generator](https://uicolors.app/create)
## Example: Complete Theme
```css
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Base */
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
/* UI Elements */
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
/* Primary Brand */
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
/* Secondary Actions */
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
/* Muted Elements */
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
/* Accents */
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
/* Destructive */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
/* Borders & Inputs */
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
/* Radius */
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
```
## Resources
- **HSL Color Space**: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl
- **shadcn/ui Theming**: https://ui.shadcn.com/docs/theming
- **Tailwind CSS Customization**: https://tailwindcss.com/docs/customizing-colors
- **Design Tokens**: https://www.w3.org/community/design-tokens/

View File

@@ -0,0 +1,339 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Tailwind CSS v4 + shadcn/ui Setup Automation Script
Configures Tailwind CSS and shadcn/ui for Next.js 16 App Router projects.
Handles detection of existing setup, merging configurations, and installing dependencies.
"""
import io
import json
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional
# Configure stdout for UTF-8 encoding (prevents Windows encoding errors)
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
class SetupManager:
"""Manages the Tailwind + shadcn/ui setup process."""
def __init__(self, project_root: Path):
self.project_root = project_root
self.package_json_path = project_root / "package.json"
self.existing_tailwind = False
self.existing_shadcn = False
def detect_existing_setup(self) -> Dict[str, bool]:
"""Detect existing Tailwind and shadcn/ui installations."""
print("[INFO] Detecting existing setup...")
if not self.package_json_path.exists():
print("[ERROR] Error: package.json not found. Is this a Next.js project?")
sys.exit(1)
with open(self.package_json_path) as f:
package_data = json.load(f)
dependencies = {
**package_data.get("dependencies", {}),
**package_data.get("devDependencies", {})
}
self.existing_tailwind = "tailwindcss" in dependencies
self.existing_shadcn = "class-variance-authority" in dependencies
return {
"tailwind": self.existing_tailwind,
"shadcn": self.existing_shadcn,
"nextjs": "next" in dependencies
}
def install_dependencies(self, use_dark_mode: bool = True):
"""Install required npm packages."""
print("\n[INSTALL] Installing dependencies...")
# Core dependencies
core_deps = [
"tailwindcss",
"postcss",
"autoprefixer",
"class-variance-authority",
"clsx",
"tailwind-merge",
"lucide-react",
"@tailwindcss/forms",
"@tailwindcss/typography"
]
if use_dark_mode:
core_deps.append("next-themes")
# shadcn/ui peer dependencies
shadcn_deps = [
"@radix-ui/react-slot",
"@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu",
"@radix-ui/react-separator",
"sonner"
]
all_deps = core_deps + shadcn_deps
print(f"Installing: {', '.join(all_deps)}")
try:
subprocess.run(
["npm", "install", "--save"] + all_deps,
cwd=self.project_root,
check=True,
capture_output=True,
text=True
)
print("[OK] Dependencies installed successfully")
except subprocess.CalledProcessError as e:
print(f"[ERROR] Error installing dependencies: {e.stderr}")
sys.exit(1)
def check_shadcn_init(self) -> bool:
"""Check if shadcn/ui has been initialized."""
components_json = self.project_root / "components.json"
return components_json.exists()
def init_shadcn(self):
"""Initialize shadcn/ui configuration."""
if self.check_shadcn_init():
print("[OK] shadcn/ui already initialized (components.json found)")
return
print("\n[SETUP] Initializing shadcn/ui...")
print("Note: This will create components.json with default settings")
# Create a default components.json
components_config = {
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": True,
"tsx": True,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": True
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
components_json_path = self.project_root / "components.json"
with open(components_json_path, 'w') as f:
json.dump(components_config, f, indent=2)
print("[OK] Created components.json")
def add_shadcn_components(self, components: List[str]):
"""Add shadcn/ui components."""
print(f"\n[SETUP] Adding shadcn/ui components: {', '.join(components)}")
for component in components:
try:
print(f" Adding {component}...")
subprocess.run(
["npx", "shadcn-ui@latest", "add", component, "--yes"],
cwd=self.project_root,
check=True,
capture_output=True,
text=True
)
except subprocess.CalledProcessError as e:
print(f" [WARN] Warning: Could not add {component}: {e.stderr}")
print(f" You can add it manually later with: npx shadcn-ui add {component}")
print("[OK] shadcn/ui components added")
def copy_template_files(self, skill_dir: Path, theme_preset: str, use_dark_mode: bool, sidebar_layout: bool):
"""Copy template files from assets directory."""
print("\n[SETUP] Copying template files...")
assets_dir = skill_dir / "assets"
# Files to copy with their destinations
template_mappings = {
"tailwind.config.ts.template": self.project_root / "tailwind.config.ts",
"postcss.config.js.template": self.project_root / "postcss.config.js",
"globals.css.template": self.project_root / "app" / "globals.css",
}
for template_name, dest_path in template_mappings.items():
template_path = assets_dir / template_name
if template_path.exists():
dest_path.parent.mkdir(parents=True, exist_ok=True)
# Read template and replace placeholders
content = template_path.read_text()
content = content.replace("{{THEME_PRESET}}", theme_preset)
dest_path.write_text(content)
print(f" [OK] Created {dest_path.relative_to(self.project_root)}")
def create_utils_file(self):
"""Create lib/utils.ts with cn helper."""
utils_dir = self.project_root / "lib"
utils_dir.mkdir(exist_ok=True)
utils_content = '''import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
'''
utils_path = utils_dir / "utils.ts"
if not utils_path.exists():
utils_path.write_text(utils_content)
print("[OK] Created lib/utils.ts")
else:
print("[INFO] lib/utils.ts already exists, skipping")
def print_summary(self, config: Dict):
"""Print setup summary."""
print("\n" + "="*60)
print("[OK] Tailwind CSS + shadcn/ui Setup Complete!")
print("="*60)
print(f"\n[CONFIG] Configuration:")
print(f" • Theme Preset: {config['theme_preset']}")
print(f" • Dark Mode: {'Enabled' if config['use_dark_mode'] else 'Disabled'}")
print(f" • Sidebar Layout: {'Yes' if config['sidebar_layout'] else 'No'}")
print(f" • Examples: {'Included' if config['include_examples'] else 'Not included'}")
print(f"\n[FILES] Files Created/Updated:")
print(f" • tailwind.config.ts")
print(f" • postcss.config.js")
print(f" • app/globals.css")
print(f" • components.json")
print(f" • lib/utils.ts")
print(f"\n[NEXT] Next Steps:")
print(f" 1. Review tailwind.config.ts and customize tokens")
print(f" 2. Check app/globals.css for CSS variables")
print(f" 3. Start your dev server: npm run dev")
print(f" 4. Add more shadcn components: npx shadcn-ui add [component]")
print(f"\n[TIP] Tip: Run the skill again to add example pages and components")
print("="*60 + "\n")
def get_user_input(prompt: str, default: str, options: Optional[List[str]] = None) -> str:
"""Get user input with validation."""
while True:
if options:
prompt_with_options = f"{prompt} ({'/'.join(options)}) [{default}]: "
else:
prompt_with_options = f"{prompt} [{default}]: "
response = input(prompt_with_options).strip() or default
if options and response not in options:
print(f"Invalid option. Please choose from: {', '.join(options)}")
continue
return response
def main():
"""Main setup function."""
print("==> Tailwind CSS v4 + shadcn/ui Setup")
print("="*60)
# Detect project root
project_root = Path.cwd()
# Initialize setup manager
manager = SetupManager(project_root)
# Detect existing setup
existing = manager.detect_existing_setup()
if not existing["nextjs"]:
print("[ERROR] Error: This doesn't appear to be a Next.js project")
print(" Make sure you're in the project root with package.json")
sys.exit(1)
if existing["tailwind"]:
print("[OK] Existing Tailwind CSS installation detected")
if existing["shadcn"]:
print("[OK] Existing shadcn/ui installation detected")
print("\n[CONFIG] Configuration Options\n")
# Get user preferences
use_dark_mode = get_user_input(
"Enable dark mode?",
"yes",
["yes", "no"]
) == "yes"
theme_preset = get_user_input(
"Theme preset",
"zinc",
["zinc", "slate", "neutral"]
)
sidebar_layout = get_user_input(
"Include sidebar layout?",
"yes",
["yes", "no"]
) == "yes"
include_examples = get_user_input(
"Include example pages?",
"yes",
["yes", "no"]
) == "yes"
config = {
"use_dark_mode": use_dark_mode,
"theme_preset": theme_preset,
"sidebar_layout": sidebar_layout,
"include_examples": include_examples
}
print("\n" + "="*60)
print("Starting setup with your configuration...")
print("="*60 + "\n")
# Install dependencies
manager.install_dependencies(use_dark_mode)
# Initialize shadcn/ui
manager.init_shadcn()
# Add base shadcn components
base_components = ["button", "card", "input", "label", "dialog", "separator"]
manager.add_shadcn_components(base_components)
# Create utils file
manager.create_utils_file()
# Note: Template file copying requires the skill's asset directory
# This would be handled by the SKILL.md instructions to copy files
print("\n[INFO] Note: Template files should be copied by the skill instructions")
print(" This script handles dependency installation and shadcn/ui setup")
# Print summary
manager.print_summary(config)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\n[WARN] Setup cancelled by user")
sys.exit(1)
except Exception as e:
print(f"\n[ERROR] Error: {e}")
sys.exit(1)