Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "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
3
README.md
Normal 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
113
plugin.lock.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
493
skills/tailwind-shadcn-ui-setup/SKILL.md
Normal file
493
skills/tailwind-shadcn-ui-setup/SKILL.md
Normal 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.
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
141
skills/tailwind-shadcn-ui-setup/assets/app/page.tsx.template
Normal file
141
skills/tailwind-shadcn-ui-setup/assets/app/page.tsx.template
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
162
skills/tailwind-shadcn-ui-setup/assets/components/app-shell.tsx
Normal file
162
skills/tailwind-shadcn-ui-setup/assets/components/app-shell.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
97
skills/tailwind-shadcn-ui-setup/assets/globals.css.template
Normal file
97
skills/tailwind-shadcn-ui-setup/assets/globals.css.template
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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>© 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)
|
||||||
@@ -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]
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
548
skills/tailwind-shadcn-ui-setup/references/theme-tokens.md
Normal file
548
skills/tailwind-shadcn-ui-setup/references/theme-tokens.md
Normal 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/
|
||||||
339
skills/tailwind-shadcn-ui-setup/scripts/setup_tailwind_shadcn.py
Normal file
339
skills/tailwind-shadcn-ui-setup/scripts/setup_tailwind_shadcn.py
Normal 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)
|
||||||
Reference in New Issue
Block a user