diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-01-16 08:30:14 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-01-16 08:30:14 +0900 |
| commit | 3fbb9a18372f2b6a675dd6c039ba52be76f3eeb4 (patch) | |
| tree | aa694a36cdd323a7853672ee7a2ba60409ac3b06 /ui/tailwindcss/.claude/commands | |
updates
Diffstat (limited to 'ui/tailwindcss/.claude/commands')
| -rw-r--r-- | ui/tailwindcss/.claude/commands/add-plugin.md | 721 | ||||
| -rw-r--r-- | ui/tailwindcss/.claude/commands/analyze-usage.md | 545 | ||||
| -rw-r--r-- | ui/tailwindcss/.claude/commands/component.md | 18 | ||||
| -rw-r--r-- | ui/tailwindcss/.claude/commands/create-component.md | 716 | ||||
| -rw-r--r-- | ui/tailwindcss/.claude/commands/init-tailwind.md | 229 | ||||
| -rw-r--r-- | ui/tailwindcss/.claude/commands/optimize-config.md | 412 | ||||
| -rw-r--r-- | ui/tailwindcss/.claude/commands/setup-dark-mode.md | 721 |
7 files changed, 3362 insertions, 0 deletions
diff --git a/ui/tailwindcss/.claude/commands/add-plugin.md b/ui/tailwindcss/.claude/commands/add-plugin.md new file mode 100644 index 0000000..36f16ed --- /dev/null +++ b/ui/tailwindcss/.claude/commands/add-plugin.md @@ -0,0 +1,721 @@ +--- +name: add-plugin +description: Add and configure TailwindCSS plugins for extended functionality, forms, typography, animations, and custom utilities +tools: Bash, Edit, Read, Write +--- + +# Add TailwindCSS Plugin + +This command helps you add, configure, and optimize TailwindCSS plugins to extend functionality and enhance your design system. + +## What This Command Does + +1. **Plugin Installation** + - Installs official and community TailwindCSS plugins + - Configures plugin settings for optimal performance + - Integrates plugins with existing configuration + - Updates content paths for plugin-specific classes + +2. **Configuration Setup** + - Configures plugin options and customizations + - Sets up plugin-specific utility classes + - Optimizes for CSS bundle size and purging + - Integrates with design system tokens + +3. **Usage Examples** + - Provides implementation examples for each plugin + - Shows best practices and common patterns + - Demonstrates responsive and interactive usage + - Includes accessibility considerations + +4. **Performance Optimization** + - Configures plugins for optimal bundle size + - Sets up effective purging strategies + - Optimizes for build performance + - Monitors plugin impact on CSS output + +## Official Plugins + +### Typography Plugin (@tailwindcss/typography) + +#### Installation and Setup + +```bash +# Install typography plugin +npm install -D @tailwindcss/typography + +# Or with yarn +yarn add -D @tailwindcss/typography +``` + +#### Configuration + +```javascript +// tailwind.config.js +module.exports = { + theme: { + extend: { + typography: ({ theme }) => ({ + // Default prose styles + DEFAULT: { + css: { + maxWidth: 'none', + color: theme('colors.gray.700'), + '[class~="lead"]': { + color: theme('colors.gray.600'), + fontSize: theme('fontSize.xl')[0], + lineHeight: theme('fontSize.xl')[1].lineHeight, + }, + a: { + color: theme('colors.blue.600'), + textDecoration: 'none', + fontWeight: theme('fontWeight.medium'), + '&:hover': { + color: theme('colors.blue.700'), + textDecoration: 'underline', + }, + }, + 'h1, h2, h3, h4, h5, h6': { + color: theme('colors.gray.900'), + fontWeight: theme('fontWeight.bold'), + }, + h1: { + fontSize: theme('fontSize.4xl')[0], + lineHeight: theme('fontSize.4xl')[1].lineHeight, + }, + h2: { + fontSize: theme('fontSize.3xl')[0], + lineHeight: theme('fontSize.3xl')[1].lineHeight, + }, + h3: { + fontSize: theme('fontSize.2xl')[0], + lineHeight: theme('fontSize.2xl')[1].lineHeight, + }, + code: { + color: theme('colors.gray.900'), + backgroundColor: theme('colors.gray.100'), + padding: theme('spacing.1'), + borderRadius: theme('borderRadius.sm'), + fontSize: theme('fontSize.sm')[0], + }, + 'pre code': { + backgroundColor: 'transparent', + padding: 0, + }, + pre: { + backgroundColor: theme('colors.gray.900'), + color: theme('colors.gray.100'), + padding: theme('spacing.4'), + borderRadius: theme('borderRadius.lg'), + overflow: 'auto', + }, + blockquote: { + borderLeftWidth: theme('borderWidth.4'), + borderLeftColor: theme('colors.gray.300'), + paddingLeft: theme('spacing.4'), + fontStyle: 'italic', + color: theme('colors.gray.600'), + }, + }, + }, + + // Dark mode typography + invert: { + css: { + '--tw-prose-body': theme('colors.gray.300'), + '--tw-prose-headings': theme('colors.gray.100'), + '--tw-prose-lead': theme('colors.gray.400'), + '--tw-prose-links': theme('colors.blue.400'), + '--tw-prose-bold': theme('colors.gray.100'), + '--tw-prose-counters': theme('colors.gray.400'), + '--tw-prose-bullets': theme('colors.gray.500'), + '--tw-prose-hr': theme('colors.gray.700'), + '--tw-prose-quotes': theme('colors.gray.200'), + '--tw-prose-quote-borders': theme('colors.gray.700'), + '--tw-prose-captions': theme('colors.gray.400'), + '--tw-prose-code': theme('colors.gray.100'), + '--tw-prose-pre-code': theme('colors.gray.100'), + '--tw-prose-pre-bg': theme('colors.gray.800'), + '--tw-prose-th-borders': theme('colors.gray.600'), + '--tw-prose-td-borders': theme('colors.gray.700'), + }, + }, + + // Size variants + sm: { + css: { + fontSize: theme('fontSize.sm')[0], + lineHeight: theme('fontSize.sm')[1].lineHeight, + h1: { fontSize: theme('fontSize.2xl')[0] }, + h2: { fontSize: theme('fontSize.xl')[0] }, + h3: { fontSize: theme('fontSize.lg')[0] }, + }, + }, + + lg: { + css: { + fontSize: theme('fontSize.lg')[0], + lineHeight: theme('fontSize.lg')[1].lineHeight, + h1: { fontSize: theme('fontSize.5xl')[0] }, + h2: { fontSize: theme('fontSize.4xl')[0] }, + h3: { fontSize: theme('fontSize.3xl')[0] }, + }, + }, + + xl: { + css: { + fontSize: theme('fontSize.xl')[0], + lineHeight: theme('fontSize.xl')[1].lineHeight, + h1: { fontSize: theme('fontSize.6xl')[0] }, + h2: { fontSize: theme('fontSize.5xl')[0] }, + h3: { fontSize: theme('fontSize.4xl')[0] }, + }, + }, + }), + }, + }, + plugins: [ + require('@tailwindcss/typography'), + ], +} +``` + +#### Usage Examples + +```html +<!-- Basic prose content --> +<article class="prose lg:prose-xl max-w-none"> + <h1>Article Title</h1> + <p class="lead">This is a lead paragraph with emphasis.</p> + <p>Regular paragraph content with <a href="#">links</a> and <strong>bold text</strong>.</p> + + <blockquote> + This is a blockquote with proper styling. + </blockquote> + + <pre><code>console.log('Code blocks are styled too')</code></pre> +</article> + +<!-- Dark mode prose --> +<article class="prose dark:prose-invert"> + <h2>Dark Mode Compatible</h2> + <p>Content that adapts to dark themes.</p> +</article> + +<!-- Size variants --> +<div class="prose prose-sm">Small typography</div> +<div class="prose prose-lg">Large typography</div> +<div class="prose prose-xl">Extra large typography</div> + +<!-- Custom prose without max-width --> +<div class="prose max-w-none"> + <p>Full width content without prose max-width constraint.</p> +</div> +``` + +### Forms Plugin (@tailwindcss/forms) + +#### Installation and Setup + +```bash +# Install forms plugin +npm install -D @tailwindcss/forms +``` + +#### Configuration + +```javascript +// tailwind.config.js +module.exports = { + plugins: [ + require('@tailwindcss/forms')({ + strategy: 'class', // 'base' or 'class' + }), + ], +} +``` + +#### Usage Examples + +```html +<!-- Form inputs with class strategy --> +<form class="space-y-4"> + <div> + <label for="name" class="block text-sm font-medium text-gray-700">Name</label> + <input + type="text" + id="name" + class="form-input mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" + /> + </div> + + <div> + <label for="email" class="block text-sm font-medium text-gray-700">Email</label> + <input + type="email" + id="email" + class="form-input mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" + /> + </div> + + <div> + <label for="message" class="block text-sm font-medium text-gray-700">Message</label> + <textarea + id="message" + rows="4" + class="form-textarea mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" + ></textarea> + </div> + + <div> + <label class="flex items-center"> + <input type="checkbox" class="form-checkbox rounded text-blue-600 focus:ring-blue-500" /> + <span class="ml-2 text-sm text-gray-700">I agree to the terms</span> + </label> + </div> + + <div> + <label class="block text-sm font-medium text-gray-700">Options</label> + <div class="mt-2 space-y-2"> + <label class="flex items-center"> + <input type="radio" name="option" value="1" class="form-radio text-blue-600 focus:ring-blue-500" /> + <span class="ml-2 text-sm text-gray-700">Option 1</span> + </label> + <label class="flex items-center"> + <input type="radio" name="option" value="2" class="form-radio text-blue-600 focus:ring-blue-500" /> + <span class="ml-2 text-sm text-gray-700">Option 2</span> + </label> + </div> + </div> + + <div> + <label for="select" class="block text-sm font-medium text-gray-700">Select</label> + <select id="select" class="form-select mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"> + <option>Option 1</option> + <option>Option 2</option> + <option>Option 3</option> + </select> + </div> + + <button type="submit" class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> + Submit + </button> +</form> +``` + +### Aspect Ratio Plugin (@tailwindcss/aspect-ratio) + +#### Installation and Setup + +```bash +# Install aspect ratio plugin +npm install -D @tailwindcss/aspect-ratio +``` + +#### Configuration + +```javascript +// tailwind.config.js +module.exports = { + plugins: [ + require('@tailwindcss/aspect-ratio'), + ], +} +``` + +#### Usage Examples + +```html +<!-- Video embed with 16:9 aspect ratio --> +<div class="aspect-w-16 aspect-h-9"> + <iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" frameborder="0" allowfullscreen></iframe> +</div> + +<!-- Square image container --> +<div class="aspect-w-1 aspect-h-1"> + <img src="image.jpg" alt="Square image" class="object-cover" /> +</div> + +<!-- Card with consistent aspect ratios --> +<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> + <div class="bg-white rounded-lg shadow-md overflow-hidden"> + <div class="aspect-w-16 aspect-h-9"> + <img src="image1.jpg" alt="Card 1" class="object-cover" /> + </div> + <div class="p-4"> + <h3 class="font-semibold">Card Title 1</h3> + </div> + </div> + + <div class="bg-white rounded-lg shadow-md overflow-hidden"> + <div class="aspect-w-16 aspect-h-9"> + <img src="image2.jpg" alt="Card 2" class="object-cover" /> + </div> + <div class="p-4"> + <h3 class="font-semibold">Card Title 2</h3> + </div> + </div> +</div> + +<!-- Modern CSS aspect-ratio property (newer alternative) --> +<div class="aspect-video"> + <iframe src="video.mp4" class="w-full h-full object-cover"></iframe> +</div> + +<div class="aspect-square"> + <img src="square-image.jpg" alt="Square" class="w-full h-full object-cover" /> +</div> + +<!-- Custom aspect ratios --> +<div class="aspect-w-4 aspect-h-3"> + <div class="bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold"> + 4:3 Aspect Ratio + </div> +</div> +``` + +### Container Queries Plugin (@tailwindcss/container-queries) + +#### Installation and Setup + +```bash +# Install container queries plugin +npm install -D @tailwindcss/container-queries +``` + +#### Configuration + +```javascript +// tailwind.config.js +module.exports = { + plugins: [ + require('@tailwindcss/container-queries'), + ], +} +``` + +#### Usage Examples + +```html +<!-- Component-level responsive design --> +<div class="@container"> + <div class="@md:flex @md:items-center @md:space-x-4"> + <img class="@md:w-24 @md:h-24 w-full h-48 object-cover rounded-lg" /> + <div class="@md:flex-1 mt-4 @md:mt-0"> + <h3 class="text-lg @lg:text-xl font-semibold">Product Title</h3> + <p class="text-gray-600 @lg:text-base text-sm">Product description</p> + <div class="@lg:flex @lg:items-center @lg:justify-between mt-2"> + <span class="font-bold @lg:text-lg">$99.99</span> + <button class="@lg:ml-4 bg-blue-600 text-white px-4 py-2 rounded"> + Add to Cart + </button> + </div> + </div> + </div> +</div> + +<!-- Card grid with container queries --> +<div class="@container"> + <div class="grid @sm:grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3 @xl:grid-cols-4 gap-4"> + <div class="bg-white rounded-lg p-4 shadow"> + <h4 class="font-semibold @lg:text-lg">Card Title</h4> + <p class="text-sm @lg:text-base text-gray-600">Card content that adapts to container size.</p> + </div> + </div> +</div> + +<!-- Sidebar with container-specific styling --> +<div class="flex"> + <aside class="@container w-64 bg-gray-100 p-4"> + <nav class="@md:space-y-4 @sm:space-y-2"> + <a class="block @md:text-base @sm:text-sm hover:text-blue-600">Navigation Item</a> + </nav> + </aside> + + <main class="flex-1 p-6"> + <div class="@container"> + <h1 class="@lg:text-4xl @md:text-3xl text-2xl font-bold">Main Content</h1> + </div> + </main> +</div> +``` + +## Popular Community Plugins + +### Line Clamp Plugin (@tailwindcss/line-clamp) + +#### Installation and Setup + +```bash +# Install line clamp plugin (now built into Tailwind v3.3+) +npm install -D @tailwindcss/line-clamp +``` + +#### Usage Examples + +```html +<!-- Clamp text to specific number of lines --> +<p class="line-clamp-3 text-sm text-gray-600"> + This is a long paragraph that will be clamped to exactly 3 lines with an ellipsis at the end when it overflows beyond the specified number of lines. +</p> + +<!-- Different line clamp values --> +<div class="space-y-4"> + <p class="line-clamp-1">Single line with ellipsis</p> + <p class="line-clamp-2">Two lines maximum with ellipsis</p> + <p class="line-clamp-4">Up to four lines with ellipsis</p> + <p class="line-clamp-none">No line clamping applied</p> +</div> + +<!-- Responsive line clamping --> +<p class="line-clamp-2 md:line-clamp-3 lg:line-clamp-4"> + Responsive line clamping that shows more lines on larger screens. +</p> +``` + +### Animations Plugin (tailwindcss-animate) + +#### Installation and Setup + +```bash +# Install animations plugin +npm install -D tailwindcss-animate +``` + +#### Configuration + +```javascript +// tailwind.config.js +module.exports = { + plugins: [ + require('tailwindcss-animate'), + ], +} +``` + +#### Usage Examples + +```html +<!-- Predefined animations --> +<div class="animate-fade-in">Fades in smoothly</div> +<div class="animate-slide-up">Slides up from bottom</div> +<div class="animate-scale-in">Scales in from center</div> +<div class="animate-bounce-in">Bounces in with spring effect</div> + +<!-- Loading animations --> +<div class="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full"></div> +<div class="animate-pulse bg-gray-300 h-4 rounded"></div> + +<!-- Hover animations --> +<button class="transform transition-transform hover:animate-bounce"> + Bounce on Hover +</button> + +<div class="group"> + <div class="transform transition-transform group-hover:animate-wiggle"> + <span>Wiggle on group hover</span> + </div> +</div> + +<!-- Staggered animations --> +<div class="space-y-2"> + <div class="animate-slide-in-left" style="animation-delay: 0ms;">Item 1</div> + <div class="animate-slide-in-left" style="animation-delay: 100ms;">Item 2</div> + <div class="animate-slide-in-left" style="animation-delay: 200ms;">Item 3</div> +</div> +``` + +### Debugging Plugin (tailwindcss-debug-screens) + +#### Installation and Setup + +```bash +# Install debug plugin (development only) +npm install -D tailwindcss-debug-screens +``` + +#### Configuration + +```javascript +// tailwind.config.js +module.exports = { + plugins: [ + process.env.NODE_ENV === 'development' && require('tailwindcss-debug-screens'), + ].filter(Boolean), +} +``` + +#### Usage + +```html +<!-- Add debug indicator to body --> +<body class="debug-screens"> + <!-- Your content --> +</body> +``` + +## Custom Plugin Development + +### Creating a Custom Plugin + +```javascript +// plugins/custom-utilities.js +const plugin = require('tailwindcss/plugin') + +module.exports = plugin(function({ addUtilities, addComponents, theme }) { + // Add custom utilities + addUtilities({ + '.text-shadow': { + textShadow: '2px 2px 4px rgba(0, 0, 0, 0.1)', + }, + '.text-shadow-lg': { + textShadow: '4px 4px 8px rgba(0, 0, 0, 0.2)', + }, + '.scrollbar-hide': { + '-ms-overflow-style': 'none', + 'scrollbar-width': 'none', + '&::-webkit-scrollbar': { + display: 'none', + }, + }, + '.backdrop-blur-xs': { + backdropFilter: 'blur(2px)', + }, + }) + + // Add custom components + addComponents({ + '.btn-primary': { + backgroundColor: theme('colors.blue.600'), + color: theme('colors.white'), + padding: `${theme('spacing.2')} ${theme('spacing.4')}`, + borderRadius: theme('borderRadius.md'), + fontWeight: theme('fontWeight.medium'), + '&:hover': { + backgroundColor: theme('colors.blue.700'), + }, + '&:focus': { + outline: 'none', + boxShadow: `0 0 0 3px ${theme('colors.blue.500')}33`, + }, + }, + '.card': { + backgroundColor: theme('colors.white'), + borderRadius: theme('borderRadius.lg'), + boxShadow: theme('boxShadow.md'), + padding: theme('spacing.6'), + }, + }) +}) +``` + +#### Using Custom Plugin + +```javascript +// tailwind.config.js +module.exports = { + plugins: [ + require('./plugins/custom-utilities'), + ], +} +``` + +### Advanced Custom Plugin with Variants + +```javascript +// plugins/advanced-utilities.js +const plugin = require('tailwindcss/plugin') + +module.exports = plugin( + function({ addUtilities, matchUtilities, theme }) { + // Static utilities + addUtilities({ + '.writing-vertical': { + 'writing-mode': 'vertical-rl', + }, + }) + + // Dynamic utilities with arbitrary values + matchUtilities( + { + 'text-shadow': (value) => ({ + textShadow: value, + }), + }, + { values: theme('textShadow') } + ) + + matchUtilities( + { + 'animation-delay': (value) => ({ + animationDelay: value, + }), + }, + { values: theme('animationDelay') } + ) + }, + { + // Extend theme + theme: { + textShadow: { + sm: '1px 1px 2px rgba(0, 0, 0, 0.1)', + DEFAULT: '2px 2px 4px rgba(0, 0, 0, 0.1)', + lg: '4px 4px 8px rgba(0, 0, 0, 0.15)', + }, + animationDelay: { + '75': '75ms', + '100': '100ms', + '150': '150ms', + '200': '200ms', + '300': '300ms', + '500': '500ms', + '700': '700ms', + '1000': '1000ms', + }, + }, + } +) +``` + +## Plugin Performance Optimization + +### Bundle Size Analysis Script + +```javascript +// scripts/analyze-plugins.js +const fs = require('fs') +const postcss = require('postcss') +const tailwindcss = require('tailwindcss') + +async function analyzePluginImpact(configPath) { + // Base configuration without plugins + const baseConfig = { + content: ['./test.html'], + plugins: [], + } + + // Configuration with plugins + const pluginConfig = require(configPath) + + // Generate CSS for both configurations + const baseCSS = await generateCSS(baseConfig) + const pluginCSS = await generateCSS(pluginConfig) + + console.log('Plugin Impact Analysis:') + console.log(`Base CSS size: ${baseCSS.length} bytes`) + console.log(`With plugins: ${pluginCSS.length} bytes`) + console.log(`Difference: ${pluginCSS.length - baseCSS.length} bytes`) + console.log(`Increase: ${(((pluginCSS.length - baseCSS.length) / baseCSS.length) * 100).toFixed(2)}%`) +} + +async function generateCSS(config) { + const result = await postcss([tailwindcss(config)]) + .process('@tailwind base; @tailwind components; @tailwind utilities;', { from: undefined }) + + return result.css +} + +analyzePluginImpact('./tailwind.config.js') +``` + +Remember: **Choose plugins based on actual needs, configure them properly, and monitor their impact on bundle size and performance!** diff --git a/ui/tailwindcss/.claude/commands/analyze-usage.md b/ui/tailwindcss/.claude/commands/analyze-usage.md new file mode 100644 index 0000000..18d5f0f --- /dev/null +++ b/ui/tailwindcss/.claude/commands/analyze-usage.md @@ -0,0 +1,545 @@ +--- +name: analyze-usage +description: Analyze TailwindCSS utility usage patterns, identify optimization opportunities, and generate usage reports +tools: Read, Bash, Grep, Glob, Write +--- + +# Analyze TailwindCSS Usage + +This command analyzes your TailwindCSS usage patterns across your codebase to identify optimization opportunities, unused utilities, and usage statistics. + +## What This Command Does + +1. **Usage Pattern Analysis** + - Scans all template files for TailwindCSS class usage + - Identifies most and least used utility patterns + - Generates usage frequency reports + - Detects potential optimization opportunities + +2. **Bundle Size Analysis** + - Analyzes generated CSS bundle size + - Identifies largest utility categories + - Compares before/after optimization results + - Tracks bundle size over time + +3. **Code Quality Insights** + - Identifies overly complex utility combinations + - Suggests component extraction opportunities + - Detects inconsistent utility usage patterns + - Highlights potential refactoring opportunities + +4. **Performance Recommendations** + - Suggests safelist optimizations + - Identifies unused CSS that can be purged + - Recommends content path improvements + - Provides bundle optimization suggestions + +## Usage Examples + +### Basic Usage Analysis + +```bash +# Analyze utility usage in all template files +grep -r "class[Name]*=" src/ --include="*.jsx" --include="*.tsx" --include="*.vue" --include="*.html" | \ +sed -E 's/.*class[Name]*=["'\''`]([^"'\''`]*)["'\''`].*/\1/' | \ +tr ' ' '\n' | \ +sort | uniq -c | sort -nr > tailwind-usage-report.txt + +# View top 20 most used utilities +head -20 tailwind-usage-report.txt + +# View least used utilities +tail -20 tailwind-usage-report.txt +``` + +### Advanced Pattern Analysis + +```bash +# Find all TailwindCSS classes in codebase +find src -name "*.{js,jsx,ts,tsx,vue,html}" -exec grep -l "class" {} \; | \ +xargs grep -o "class[Name]*=['\"][^'\"]*['\"]" | \ +sed -E 's/.*class[Name]*=["'\''`]([^"'\''`]*)["'\''`].*/\1/' | \ +tr ' ' '\n' | \ +grep -E '^[a-zA-Z]' | \ +sort | uniq -c | sort -nr +``` + +### Component Complexity Analysis + +```bash +# Find components with many utility classes (potential extraction candidates) +find src/components -name "*.{jsx,tsx}" -exec sh -c ' + for file do + count=$(grep -o "class[Name]*=['\"][^'\"]*['\"]" "$file" | \ + sed -E "s/.*class[Name]*=[\"\'\`]([^\"\'\`]*)[\"\'\`].*/\1/" | \ + tr " " "\n" | wc -l) + echo "$count $file" + done +' sh {} + | sort -nr | head -10 +``` + +## Analysis Scripts + +### Comprehensive Usage Analyzer + +```javascript +// scripts/analyze-tailwind-usage.js +const fs = require('fs') +const path = require('path') +const glob = require('glob') + +class TailwindUsageAnalyzer { + constructor(options = {}) { + this.srcPaths = options.srcPaths || ['src/**/*.{js,jsx,ts,tsx,vue,html}'] + this.outputPath = options.outputPath || './tailwind-analysis.json' + this.classPattern = /(?:class|className)(?:Name)?[`:=]\s*[`"']([^`"']*)[`"']/g + } + + async analyze() { + const files = this.getTemplateFiles() + const results = { + totalFiles: files.length, + totalClasses: 0, + utilityStats: {}, + fileStats: {}, + categoryStats: {}, + complexityStats: {}, + timestamp: new Date().toISOString() + } + + for (const file of files) { + const content = fs.readFileSync(file, 'utf8') + const fileClasses = this.extractClasses(content) + + results.fileStats[file] = { + classCount: fileClasses.length, + uniqueClasses: [...new Set(fileClasses)].length, + complexity: this.calculateComplexity(fileClasses) + } + + // Update utility stats + fileClasses.forEach(className => { + results.utilityStats[className] = (results.utilityStats[className] || 0) + 1 + results.totalClasses++ + + // Categorize utility + const category = this.categorizeUtility(className) + results.categoryStats[category] = (results.categoryStats[category] || 0) + 1 + }) + } + + // Calculate additional insights + results.insights = this.generateInsights(results) + results.recommendations = this.generateRecommendations(results) + + // Save results + fs.writeFileSync(this.outputPath, JSON.stringify(results, null, 2)) + + return results + } + + getTemplateFiles() { + const files = [] + this.srcPaths.forEach(pattern => { + files.push(...glob.sync(pattern)) + }) + return files + } + + extractClasses(content) { + const classes = [] + let match + + while ((match = this.classPattern.exec(content)) !== null) { + const classString = match[1] + const classList = classString.split(/\s+/).filter(cls => cls.length > 0) + classes.push(...classList) + } + + return classes + } + + categorizeUtility(className) { + const categories = { + layout: /^(block|inline|flex|grid|table|hidden|container)/, + spacing: /^(p|m|space)[trblxy]?-/, + sizing: /^(w|h|max-w|max-h|min-w|min-h)-/, + typography: /^(text|font|leading|tracking|whitespace)/, + colors: /^(bg|text|border|ring)-.+-(50|100|200|300|400|500|600|700|800|900|950)$/, + borders: /^(border|rounded|ring|divide)/, + effects: /^(shadow|opacity|blur)/, + filters: /^(filter|backdrop|brightness|contrast|grayscale)/, + animation: /^(animate|transition|duration|ease|delay)/, + transforms: /^(transform|scale|rotate|translate|skew)/, + interactivity: /^(cursor|select|resize|outline|appearance)/, + responsive: /^(sm|md|lg|xl|2xl):/, + state: /^(hover|focus|active|disabled|group|peer):/, + } + + for (const [category, pattern] of Object.entries(categories)) { + if (pattern.test(className)) { + return category + } + } + + return 'other' + } + + calculateComplexity(classes) { + const uniqueClasses = new Set(classes) + const responsiveClasses = classes.filter(c => /^(sm|md|lg|xl|2xl):/.test(c)) + const stateClasses = classes.filter(c => /^(hover|focus|active|group|peer):/.test(c)) + + return { + total: classes.length, + unique: uniqueClasses.size, + responsive: responsiveClasses.length, + interactive: stateClasses.length, + ratio: classes.length / uniqueClasses.size + } + } + + generateInsights(results) { + const sortedUtilities = Object.entries(results.utilityStats) + .sort(([,a], [,b]) => b - a) + + const sortedCategories = Object.entries(results.categoryStats) + .sort(([,a], [,b]) => b - a) + + const complexFiles = Object.entries(results.fileStats) + .sort(([,a], [,b]) => b.complexity.total - a.complexity.total) + .slice(0, 10) + + return { + mostUsedUtilities: sortedUtilities.slice(0, 20), + leastUsedUtilities: sortedUtilities.slice(-20), + topCategories: sortedCategories, + mostComplexFiles: complexFiles, + averageClassesPerFile: results.totalClasses / results.totalFiles, + uniqueUtilityCount: Object.keys(results.utilityStats).length + } + } + + generateRecommendations(results) { + const recommendations = [] + + // Check for overused utilities + const overusedUtilities = results.insights.mostUsedUtilities + .filter(([,count]) => count > results.totalFiles * 0.8) + + if (overusedUtilities.length > 0) { + recommendations.push({ + type: 'component-extraction', + message: 'Consider extracting components for frequently used utility combinations', + utilities: overusedUtilities.slice(0, 5).map(([name]) => name) + }) + } + + // Check for complex files + const complexFiles = results.insights.mostComplexFiles + .filter(([,stats]) => stats.complexity.total > 50) + + if (complexFiles.length > 0) { + recommendations.push({ + type: 'complexity-reduction', + message: 'These files have high utility complexity and may benefit from refactoring', + files: complexFiles.slice(0, 5).map(([file]) => file) + }) + } + + // Check for unused categories + const lowUsageCategories = Object.entries(results.categoryStats) + .filter(([,count]) => count < results.totalClasses * 0.01) + + if (lowUsageCategories.length > 0) { + recommendations.push({ + type: 'config-optimization', + message: 'Consider removing unused utility categories from your build', + categories: lowUsageCategories.map(([name]) => name) + }) + } + + return recommendations + } +} + +// Usage +const analyzer = new TailwindUsageAnalyzer({ + srcPaths: ['src/**/*.{jsx,tsx}', 'pages/**/*.{jsx,tsx}'], + outputPath: './reports/tailwind-usage.json' +}) + +analyzer.analyze().then(results => { + console.log('TailwindCSS Usage Analysis Complete!') + console.log(`Analyzed ${results.totalFiles} files`) + console.log(`Found ${results.totalClasses} utility class usages`) + console.log(`${results.insights.uniqueUtilityCount} unique utilities`) + console.log(`Average ${results.insights.averageClassesPerFile.toFixed(1)} classes per file`) + + console.log('\nTop 10 Most Used Utilities:') + results.insights.mostUsedUtilities.slice(0, 10).forEach(([name, count]) => { + console.log(` ${name}: ${count} usages`) + }) + + console.log('\nRecommendations:') + results.recommendations.forEach(rec => { + console.log(` ${rec.type}: ${rec.message}`) + }) +}) +``` + +### Bundle Size Analyzer + +```javascript +// scripts/analyze-bundle-size.js +const fs = require('fs') +const gzipSize = require('gzip-size') +const brotliSize = require('brotli-size') + +async function analyzeBundleSize(cssFilePath) { + const css = fs.readFileSync(cssFilePath, 'utf8') + const originalSize = Buffer.byteLength(css, 'utf8') + + const gzipped = await gzipSize(css) + const brotlied = await brotliSize(css) + + // Extract utility classes + const utilities = css.match(/\.[a-zA-Z][a-zA-Z0-9_-]*(?::[\w-]+)*(?:,\s*\.[a-zA-Z][a-zA-Z0-9_-]*(?::[\w-]+)*)*\s*{[^}]+}/g) || [] + + // Categorize utilities + const categories = { + layout: 0, spacing: 0, typography: 0, colors: 0, + borders: 0, effects: 0, animations: 0, responsive: 0 + } + + let categorySize = { ...categories } + + utilities.forEach(rule => { + const size = Buffer.byteLength(rule, 'utf8') + + if (/\.(flex|grid|block|inline)/.test(rule)) { + categorySize.layout += size + } else if (/\.(p|m|space)-/.test(rule)) { + categorySize.spacing += size + } else if (/\.(text|font)-/.test(rule)) { + categorySize.typography += size + } else if (/\.(bg|text|border)-.+-(50|100|200|300|400|500|600|700|800|900)/.test(rule)) { + categorySize.colors += size + } else if (/\.(border|rounded|ring)/.test(rule)) { + categorySize.borders += size + } else if (/\.(shadow|opacity|blur)/.test(rule)) { + categorySize.effects += size + } else if (/\.(animate|transition)/.test(rule)) { + categorySize.animations += size + } else if (/@media/.test(rule)) { + categorySize.responsive += size + } + }) + + return { + original: originalSize, + gzipped, + brotlied, + utilityCount: utilities.length, + categoryBreakdown: categorySize, + compressionRatio: { + gzip: (originalSize / gzipped).toFixed(2), + brotli: (originalSize / brotlied).toFixed(2) + } + } +} + +// Generate size report +async function generateSizeReport(cssPath) { + const analysis = await analyzeBundleSize(cssPath) + + console.log('CSS Bundle Size Analysis') + console.log('========================') + console.log(`Original size: ${(analysis.original / 1024).toFixed(2)} KB`) + console.log(`Gzipped size: ${(analysis.gzipped / 1024).toFixed(2)} KB (${analysis.compressionRatio.gzip}x compression)`) + console.log(`Brotli size: ${(analysis.brotlied / 1024).toFixed(2)} KB (${analysis.compressionRatio.brotli}x compression)`) + console.log(`Utility rules: ${analysis.utilityCount}`) + + console.log('\nSize by Category:') + Object.entries(analysis.categoryBreakdown) + .sort(([,a], [,b]) => b - a) + .forEach(([category, size]) => { + const percentage = ((size / analysis.original) * 100).toFixed(1) + console.log(` ${category}: ${(size / 1024).toFixed(2)} KB (${percentage}%)`) + }) +} + +// Usage: node scripts/analyze-bundle-size.js dist/styles.css +generateSizeReport(process.argv[2]) +``` + +## Usage Reports + +### HTML Report Generator + +```javascript +// scripts/generate-usage-report.js +function generateHTMLReport(analysisData) { + const html = ` +<!DOCTYPE html> +<html> +<head> + <title>TailwindCSS Usage Report</title> + <style> + body { font-family: system-ui, sans-serif; margin: 2rem; } + .card { border: 1px solid #e5e5e5; border-radius: 8px; padding: 1rem; margin: 1rem 0; } + .stat { display: inline-block; margin: 0.5rem 1rem 0.5rem 0; } + .chart { width: 100%; height: 300px; } + table { width: 100%; border-collapse: collapse; } + th, td { padding: 0.5rem; border: 1px solid #ddd; text-align: left; } + th { background-color: #f5f5f5; } + </style> +</head> +<body> + <h1>TailwindCSS Usage Analysis Report</h1> + <p>Generated on: ${analysisData.timestamp}</p> + + <div class="card"> + <h2>Overview</h2> + <div class="stat"><strong>${analysisData.totalFiles}</strong> files analyzed</div> + <div class="stat"><strong>${analysisData.totalClasses}</strong> utility usages</div> + <div class="stat"><strong>${analysisData.insights.uniqueUtilityCount}</strong> unique utilities</div> + <div class="stat"><strong>${analysisData.insights.averageClassesPerFile.toFixed(1)}</strong> avg classes/file</div> + </div> + + <div class="card"> + <h2>Top Utility Categories</h2> + <table> + <tr><th>Category</th><th>Usage Count</th><th>Percentage</th></tr> + ${analysisData.insights.topCategories.slice(0, 10).map(([cat, count]) => ` + <tr> + <td>${cat}</td> + <td>${count}</td> + <td>${((count / analysisData.totalClasses) * 100).toFixed(1)}%</td> + </tr> + `).join('')} + </table> + </div> + + <div class="card"> + <h2>Most Used Utilities</h2> + <table> + <tr><th>Utility</th><th>Usage Count</th><th>Files</th></tr> + ${analysisData.insights.mostUsedUtilities.slice(0, 20).map(([util, count]) => ` + <tr> + <td><code>${util}</code></td> + <td>${count}</td> + <td>${Math.round((count / analysisData.totalFiles) * 100)}%</td> + </tr> + `).join('')} + </table> + </div> + + <div class="card"> + <h2>Most Complex Files</h2> + <table> + <tr><th>File</th><th>Total Classes</th><th>Unique Classes</th><th>Complexity Ratio</th></tr> + ${analysisData.insights.mostComplexFiles.slice(0, 10).map(([file, stats]) => ` + <tr> + <td><code>${file}</code></td> + <td>${stats.complexity.total}</td> + <td>${stats.complexity.unique}</td> + <td>${stats.complexity.ratio.toFixed(2)}</td> + </tr> + `).join('')} + </table> + </div> + + <div class="card"> + <h2>Recommendations</h2> + <ul> + ${analysisData.recommendations.map(rec => ` + <li> + <strong>${rec.type.replace('-', ' ')}:</strong> ${rec.message} + ${rec.utilities ? `<br><small>Utilities: ${rec.utilities.join(', ')}</small>` : ''} + ${rec.files ? `<br><small>Files: ${rec.files.slice(0, 3).join(', ')}</small>` : ''} + ${rec.categories ? `<br><small>Categories: ${rec.categories.join(', ')}</small>` : ''} + </li> + `).join('')} + </ul> + </div> +</body> +</html> +` + + fs.writeFileSync('./reports/tailwind-usage-report.html', html) + console.log('HTML report generated: ./reports/tailwind-usage-report.html') +} +``` + +## Automation and Monitoring + +### CI/CD Integration + +```yaml +# .github/workflows/tailwind-analysis.yml +name: TailwindCSS Usage Analysis + +on: + pull_request: + paths: + - 'src/**/*.{js,jsx,ts,tsx}' + - 'tailwind.config.js' + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run TailwindCSS usage analysis + run: node scripts/analyze-tailwind-usage.js + + - name: Generate size analysis + run: | + npm run build:css + node scripts/analyze-bundle-size.js dist/styles.css > bundle-size-report.txt + + - name: Comment PR with analysis + uses: actions/github-script@v6 + with: + script: | + const fs = require('fs'); + const analysis = JSON.parse(fs.readFileSync('./reports/tailwind-usage.json', 'utf8')); + const sizeReport = fs.readFileSync('bundle-size-report.txt', 'utf8'); + + const body = `## 📊 TailwindCSS Analysis + + **Usage Statistics:** + - Files analyzed: ${analysis.totalFiles} + - Total utility usages: ${analysis.totalClasses} + - Unique utilities: ${analysis.insights.uniqueUtilityCount} + - Average classes per file: ${analysis.insights.averageClassesPerFile.toFixed(1)} + + **Bundle Size:** + \`\`\` + ${sizeReport} + \`\`\` + + **Top Recommendations:** + ${analysis.recommendations.slice(0, 3).map(rec => `- ${rec.message}`).join('\n')} + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); +``` + +Remember: **Regular analysis helps maintain optimal TailwindCSS usage and identifies optimization opportunities early!** diff --git a/ui/tailwindcss/.claude/commands/component.md b/ui/tailwindcss/.claude/commands/component.md new file mode 100644 index 0000000..88f6ec1 --- /dev/null +++ b/ui/tailwindcss/.claude/commands/component.md @@ -0,0 +1,18 @@ +--- +description: Generate component with Tailwind utility classes +argument-hint: "[component-name] [type]" +allowed-tools: Write, Read, Edit +--- + +Generate Tailwind component: $ARGUMENTS + +Follow utility-first principles: +1. Create component with proper utility classes +2. Include responsive design patterns +3. Add dark mode support if applicable +4. Use semantic class combinations +5. Include accessibility utilities + +Types: button, card, form, layout, navigation + +Example: `/component Button primary` or `/component Card hover` diff --git a/ui/tailwindcss/.claude/commands/create-component.md b/ui/tailwindcss/.claude/commands/create-component.md new file mode 100644 index 0000000..fab23f5 --- /dev/null +++ b/ui/tailwindcss/.claude/commands/create-component.md @@ -0,0 +1,716 @@ +--- +name: create-component +description: Create reusable components using TailwindCSS utilities with proper patterns and best practices +tools: Write, Edit, Read, Grep, Glob +--- + +# Create TailwindCSS Component + +This command helps create well-structured, reusable components using TailwindCSS utilities following best practices and design system patterns. + +## What This Command Does + +1. **Component Architecture** + - Creates component files with proper TailwindCSS utility composition + - Implements responsive design patterns + - Sets up proper TypeScript/PropTypes definitions + - Follows accessibility best practices + +2. **Utility Composition** + - Uses semantic utility class combinations + - Implements proper state management (hover, focus, active) + - Creates responsive variants using breakpoint prefixes + - Follows mobile-first methodology + +3. **Design System Integration** + - Uses design tokens from TailwindCSS configuration + - Implements consistent spacing and typography scales + - Applies proper color palette and semantic colors + - Follows component variant patterns + +4. **Performance Optimization** + - Uses efficient utility combinations + - Optimizes for CSS purging + - Implements proper class composition strategies + - Avoids unnecessary custom CSS + +## Component Templates + +### Button Component + +```jsx +// components/Button.jsx +import React from 'react' +import { cva } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + // Base styles + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes<HTMLButtonElement> { + variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' + size?: 'default' | 'sm' | 'lg' | 'icon' + loading?: boolean + leftIcon?: React.ReactNode + rightIcon?: React.ReactNode +} + +const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( + ({ className, variant, size, loading, leftIcon, rightIcon, children, ...props }, ref) => { + return ( + <button + className={cn(buttonVariants({ variant, size }), className)} + ref={ref} + disabled={loading || props.disabled} + {...props} + > + {loading ? ( + <svg className="mr-2 h-4 w-4 animate-spin" viewBox="0 0 24 24"> + <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25" /> + <path fill="currentColor" className="opacity-75" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /> + </svg> + ) : leftIcon ? ( + <span className="mr-2">{leftIcon}</span> + ) : null} + + {children} + + {rightIcon && !loading && ( + <span className="ml-2">{rightIcon}</span> + )} + </button> + ) + } +) + +Button.displayName = "Button" + +export { Button, buttonVariants } +``` + +### Card Component + +```jsx +// components/Card.jsx +import React from 'react' +import { cn } from '@/lib/utils' + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> & { + hover?: boolean + padding?: 'none' | 'sm' | 'md' | 'lg' + } +>(({ className, hover = false, padding = 'md', children, ...props }, ref) => { + const paddingMap = { + none: '', + sm: 'p-4', + md: 'p-6', + lg: 'p-8' + } + + return ( + <div + ref={ref} + className={cn( + "rounded-lg border bg-card text-card-foreground shadow-sm", + hover && "transition-shadow hover:shadow-md", + paddingMap[padding], + className + )} + {...props} + > + {children} + </div> + ) +}) + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> +>(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("flex flex-col space-y-1.5 p-6", className)} + {...props} + /> +)) + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes<HTMLHeadingElement> +>(({ className, ...props }, ref) => ( + <h3 + ref={ref} + className={cn("text-2xl font-semibold leading-none tracking-tight", className)} + {...props} + /> +)) + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> +>(({ className, ...props }, ref) => ( + <p + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> +)) + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> +>(({ className, ...props }, ref) => ( + <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> +)) + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> +>(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("flex items-center p-6 pt-0", className)} + {...props} + /> +)) + +Card.displayName = "Card" +CardHeader.displayName = "CardHeader" +CardTitle.displayName = "CardTitle" +CardDescription.displayName = "CardDescription" +CardContent.displayName = "CardContent" +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +``` + +### Input Component + +```jsx +// components/Input.jsx +import React from 'react' +import { cva } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const inputVariants = cva( + "flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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", + { + variants: { + size: { + sm: "h-8 px-2 text-xs", + default: "h-10 px-3", + lg: "h-12 px-4 text-base", + }, + state: { + default: "", + error: "border-destructive focus-visible:ring-destructive", + success: "border-green-500 focus-visible:ring-green-500", + }, + }, + defaultVariants: { + size: "default", + state: "default", + }, + } +) + +export interface InputProps + extends React.InputHTMLAttributes<HTMLInputElement> { + size?: 'sm' | 'default' | 'lg' + state?: 'default' | 'error' | 'success' + label?: string + helperText?: string + error?: string + leftIcon?: React.ReactNode + rightIcon?: React.ReactNode +} + +const Input = React.forwardRef<HTMLInputElement, InputProps>( + ({ + className, + type, + size, + state, + label, + helperText, + error, + leftIcon, + rightIcon, + ...props + }, ref) => { + const inputState = error ? 'error' : state + + return ( + <div className="space-y-1"> + {label && ( + <label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"> + {label} + </label> + )} + + <div className="relative"> + {leftIcon && ( + <div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"> + {leftIcon} + </div> + )} + + <input + type={type} + className={cn( + inputVariants({ size, state: inputState }), + leftIcon && "pl-9", + rightIcon && "pr-9", + className + )} + ref={ref} + {...props} + /> + + {rightIcon && ( + <div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"> + {rightIcon} + </div> + )} + </div> + + {(helperText || error) && ( + <p className={cn( + "text-xs", + error ? "text-destructive" : "text-muted-foreground" + )}> + {error || helperText} + </p> + )} + </div> + ) + } +) + +Input.displayName = "Input" + +export { Input, inputVariants } +``` + +### Badge Component + +```jsx +// components/Badge.jsx +import React from 'react' +import { cva } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + success: "border-transparent bg-green-500 text-white hover:bg-green-600", + warning: "border-transparent bg-yellow-500 text-white hover:bg-yellow-600", + outline: "text-foreground", + }, + size: { + sm: "px-2 py-0.5 text-xs", + default: "px-2.5 py-0.5 text-xs", + lg: "px-3 py-1 text-sm", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes<HTMLDivElement> { + variant?: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'outline' + size?: 'sm' | 'default' | 'lg' + removable?: boolean + onRemove?: () => void +} + +const Badge = React.forwardRef<HTMLDivElement, BadgeProps>( + ({ className, variant, size, removable, onRemove, children, ...props }, ref) => { + return ( + <div + className={cn(badgeVariants({ variant, size }), className)} + ref={ref} + {...props} + > + {children} + {removable && ( + <button + onClick={onRemove} + className="ml-1 -mr-1 rounded-full p-0.5 hover:bg-black/10 focus:outline-none" + aria-label="Remove badge" + > + <svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> + <path d="M18 6L6 18M6 6l12 12" /> + </svg> + </button> + )} + </div> + ) + } +) + +Badge.displayName = "Badge" + +export { Badge, badgeVariants } +``` + +### Alert Component + +```jsx +// components/Alert.jsx +import React from 'react' +import { cva } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + success: "border-green-500/50 text-green-700 dark:text-green-400 [&>svg]:text-green-600", + warning: "border-yellow-500/50 text-yellow-700 dark:text-yellow-400 [&>svg]:text-yellow-600", + info: "border-blue-500/50 text-blue-700 dark:text-blue-400 [&>svg]:text-blue-600", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> & { + variant?: 'default' | 'destructive' | 'success' | 'warning' | 'info' + dismissible?: boolean + onDismiss?: () => void + } +>(({ className, variant, dismissible, onDismiss, children, ...props }, ref) => ( + <div + ref={ref} + role="alert" + className={cn(alertVariants({ variant }), className)} + {...props} + > + {children} + {dismissible && ( + <button + onClick={onDismiss} + className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" + > + <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> + <path d="M18 6L6 18M6 6l12 12" /> + </svg> + <span className="sr-only">Close</span> + </button> + )} + </div> +)) + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes<HTMLHeadingElement> +>(({ className, ...props }, ref) => ( + <h5 + ref={ref} + className={cn("mb-1 font-medium leading-none tracking-tight", className)} + {...props} + /> +)) + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> +>(({ className, ...props }, ref) => ( + <div + ref={ref} + className={cn("text-sm [&_p]:leading-relaxed", className)} + {...props} + /> +)) + +Alert.displayName = "Alert" +AlertTitle.displayName = "AlertTitle" +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } +``` + +## Layout Components + +### Container Component + +```jsx +// components/Container.jsx +import React from 'react' +import { cn } from '@/lib/utils' + +export interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> { + size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' + padding?: boolean +} + +const Container = React.forwardRef<HTMLDivElement, ContainerProps>( + ({ className, size = 'lg', padding = true, ...props }, ref) => { + const sizeClasses = { + sm: 'max-w-2xl', + md: 'max-w-4xl', + lg: 'max-w-6xl', + xl: 'max-w-7xl', + '2xl': 'max-w-8xl', + full: 'max-w-full' + } + + return ( + <div + ref={ref} + className={cn( + 'mx-auto', + sizeClasses[size], + padding && 'px-4 sm:px-6 lg:px-8', + className + )} + {...props} + /> + ) + } +) + +Container.displayName = 'Container' + +export { Container } +``` + +### Grid Component + +```jsx +// components/Grid.jsx +import React from 'react' +import { cn } from '@/lib/utils' + +export interface GridProps extends React.HTMLAttributes<HTMLDivElement> { + cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12 + gap?: 'none' | 'sm' | 'md' | 'lg' | 'xl' + responsive?: boolean +} + +const Grid = React.forwardRef<HTMLDivElement, GridProps>( + ({ className, cols = 1, gap = 'md', responsive = true, ...props }, ref) => { + const gapClasses = { + none: 'gap-0', + sm: 'gap-2', + md: 'gap-4', + lg: 'gap-6', + xl: 'gap-8' + } + + const getResponsiveCols = (cols: number) => { + if (!responsive) return `grid-cols-${cols}` + + switch (cols) { + case 1: return 'grid-cols-1' + case 2: return 'grid-cols-1 md:grid-cols-2' + case 3: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3' + case 4: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4' + case 5: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5' + case 6: return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6' + case 12: return 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-12' + default: return `grid-cols-${cols}` + } + } + + return ( + <div + ref={ref} + className={cn( + 'grid', + getResponsiveCols(cols), + gapClasses[gap], + className + )} + {...props} + /> + ) + } +) + +Grid.displayName = 'Grid' + +export { Grid } +``` + +## Utility Functions + +### Class Name Utility + +```typescript +// lib/utils.ts +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +// Responsive utility +export function responsive( + base: string, + sm?: string, + md?: string, + lg?: string, + xl?: string, + xxl?: string +) { + return cn( + base, + sm && `sm:${sm}`, + md && `md:${md}`, + lg && `lg:${lg}`, + xl && `xl:${xl}`, + xxl && `2xl:${xxl}` + ) +} + +// Focus ring utility +export function focusRing(color: string = 'ring-primary') { + return `focus:outline-none focus:ring-2 ${color} focus:ring-offset-2` +} +``` + +## Component Generation Script + +### Auto-generate Component + +```javascript +// scripts/create-component.js +const fs = require('fs') +const path = require('path') + +function createComponent(name, type = 'basic') { + const componentName = name.charAt(0).toUpperCase() + name.slice(1) + const fileName = `${componentName}.tsx` + const componentDir = `./components/${componentName}` + + // Create component directory + if (!fs.existsSync(componentDir)) { + fs.mkdirSync(componentDir, { recursive: true }) + } + + const templates = { + basic: basicComponentTemplate, + form: formComponentTemplate, + layout: layoutComponentTemplate, + interactive: interactiveComponentTemplate + } + + const template = templates[type] || templates.basic + const componentCode = template(componentName, name) + + // Write component file + fs.writeFileSync(path.join(componentDir, fileName), componentCode) + + // Create index file + const indexContent = `export { ${componentName} } from './${componentName}'\nexport type { ${componentName}Props } from './${componentName}'` + fs.writeFileSync(path.join(componentDir, 'index.ts'), indexContent) + + console.log(`✅ Component ${componentName} created successfully!`) + console.log(`📁 Location: ${componentDir}`) + console.log(`📝 Files created:`) + console.log(` - ${fileName}`) + console.log(` - index.ts`) +} + +function basicComponentTemplate(componentName, kebabName) { + return `import React from 'react' +import { cn } from '@/lib/utils' + +export interface ${componentName}Props extends React.HTMLAttributes<HTMLDivElement> { + variant?: 'default' | 'secondary' + size?: 'sm' | 'md' | 'lg' +} + +const ${componentName} = React.forwardRef<HTMLDivElement, ${componentName}Props>( + ({ className, variant = 'default', size = 'md', children, ...props }, ref) => { + const variants = { + default: 'bg-background text-foreground', + secondary: 'bg-secondary text-secondary-foreground' + } + + const sizes = { + sm: 'p-2 text-sm', + md: 'p-4 text-base', + lg: 'p-6 text-lg' + } + + return ( + <div + ref={ref} + className={cn( + 'rounded-lg border transition-colors', + variants[variant], + sizes[size], + className + )} + {...props} + > + {children} + </div> + ) + } +) + +${componentName}.displayName = '${componentName}' + +export { ${componentName} } +` +} + +// Usage: node scripts/create-component.js MyComponent basic +const [,, name, type] = process.argv +if (!name) { + console.error('Please provide a component name') + process.exit(1) +} + +createComponent(name, type) +``` + +Remember: **Focus on utility composition, responsive design, accessibility, and performance optimization when creating TailwindCSS components!** diff --git a/ui/tailwindcss/.claude/commands/init-tailwind.md b/ui/tailwindcss/.claude/commands/init-tailwind.md new file mode 100644 index 0000000..604c47e --- /dev/null +++ b/ui/tailwindcss/.claude/commands/init-tailwind.md @@ -0,0 +1,229 @@ +--- +name: init-tailwind +description: Initialize TailwindCSS in a new project with optimal configuration +tools: Write, Edit, Bash +--- + +# Initialize TailwindCSS Project + +This command sets up a new TailwindCSS project with best practices and optimal configuration. + +## What This Command Does + +1. **Install TailwindCSS and Dependencies** + - Installs TailwindCSS, PostCSS, and Autoprefixer + - Adds common TailwindCSS plugins + - Sets up development dependencies + +2. **Create Configuration Files** + - Generates optimized `tailwind.config.js` + - Creates `postcss.config.js` + - Sets up CSS entry point with Tailwind directives + +3. **Configure Content Paths** + - Sets up content scanning for your framework + - Optimizes purging configuration + - Adds safelist for dynamic classes + +## Usage Examples + +### Next.js Project + +```bash +# Install TailwindCSS for Next.js +npm install -D tailwindcss postcss autoprefixer @tailwindcss/typography @tailwindcss/forms @tailwindcss/aspect-ratio + +# Generate config files +npx tailwindcss init -p + +# Configure for Next.js paths +``` + +### React/Vite Project + +```bash +# Install TailwindCSS for Vite +npm install -D tailwindcss postcss autoprefixer @tailwindcss/typography @tailwindcss/forms + +# Generate config +npx tailwindcss init -p + +# Configure for React/Vite paths +``` + +### Vanilla HTML Project + +```bash +# Install TailwindCSS CLI +npm install -D tailwindcss + +# Generate config +npx tailwindcss init + +# Build CSS file +npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch +``` + +## Configuration Templates + +### Optimized Tailwind Config + +```javascript +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + './src/**/*.{js,ts,jsx,tsx,mdx}', + ], + darkMode: 'class', + theme: { + extend: { + colors: { + primary: { + 50: '#eff6ff', + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + 950: '#172554', + }, + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + }, + animation: { + 'fade-in': 'fadeIn 0.5s ease-in-out', + 'slide-up': 'slideUp 0.3s ease-out', + }, + keyframes: { + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + slideUp: { + '0%': { transform: 'translateY(10px)', opacity: '0' }, + '100%': { transform: 'translateY(0)', opacity: '1' }, + }, + }, + }, + }, + plugins: [ + require('@tailwindcss/typography'), + require('@tailwindcss/forms'), + require('@tailwindcss/aspect-ratio'), + ], +} +``` + +### PostCSS Configuration + +```javascript +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} +``` + +### CSS Entry Point + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; + } + + body { + @apply bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100; + } +} + +@layer components { + .btn { + @apply inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50; + } + + .btn-primary { + @apply bg-primary-600 text-white hover:bg-primary-700 focus-visible:ring-primary-500; + } +} +``` + +## Project-Specific Optimizations + +### Next.js Optimization + +```javascript +// next.config.js +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + optimizeCss: true, + }, +} +module.exports = nextConfig +``` + +### Vite Optimization + +```javascript +// vite.config.js +import { defineConfig } from 'vite' + +export default defineConfig({ + css: { + postcss: './postcss.config.js', + }, + build: { + cssCodeSplit: true, + }, +}) +``` + +## Package.json Scripts + +```json +{ + "scripts": { + "build-css": "tailwindcss -i ./src/input.css -o ./dist/output.css", + "watch-css": "tailwindcss -i ./src/input.css -o ./dist/output.css --watch", + "build-css-prod": "tailwindcss -i ./src/input.css -o ./dist/output.css --minify" + } +} +``` + +## Best Practices Setup + +1. **Content Configuration** + - Include all template file paths + - Use specific extensions for better performance + - Exclude build directories and node_modules + +2. **Plugin Selection** + - Start with essential plugins (typography, forms) + - Add aspect-ratio for responsive images + - Consider container-queries for advanced layouts + +3. **Theme Configuration** + - Extend default theme rather than replacing + - Use semantic color names + - Define consistent spacing and typography scales + +4. **Performance** + - Enable CSS purging for production + - Use specific content paths + - Consider CSS-in-JS integration if needed + +Remember: **Start simple, extend gradually, and optimize for your specific use case!** diff --git a/ui/tailwindcss/.claude/commands/optimize-config.md b/ui/tailwindcss/.claude/commands/optimize-config.md new file mode 100644 index 0000000..f55ed0f --- /dev/null +++ b/ui/tailwindcss/.claude/commands/optimize-config.md @@ -0,0 +1,412 @@ +--- +name: optimize-config +description: Optimize TailwindCSS configuration for better performance, smaller bundle size, and efficient development workflow +tools: Read, Edit, Bash, Grep, Glob +--- + +# Optimize TailwindCSS Configuration + +This command analyzes and optimizes your TailwindCSS setup for maximum performance and minimal bundle size. + +## What This Command Does + +1. **Content Path Optimization** + - Analyzes project structure to optimize content scanning + - Configures precise file patterns for better purging + - Excludes unnecessary directories and files + +2. **Bundle Size Analysis** + - Identifies unused utilities in your CSS bundle + - Optimizes safelist configuration + - Configures effective CSS purging strategies + +3. **Build Performance** + - Optimizes PostCSS pipeline configuration + - Configures caching strategies + - Sets up development vs production optimizations + +4. **Plugin and Theme Cleanup** + - Removes unused plugins and theme extensions + - Optimizes custom utility configurations + - Cleans up redundant theme settings + +## Usage Examples + +### Analyze Current Bundle Size + +```bash +# Build CSS and analyze size +npx tailwindcss -i ./src/styles.css -o ./dist/output.css +wc -c ./dist/output.css + +# With minification +npx tailwindcss -i ./src/styles.css -o ./dist/output.css --minify +wc -c ./dist/output.css + +# Compress with Brotli +brotli -q 11 ./dist/output.css +ls -lh ./dist/output.css.br +``` + +### Content Path Optimization + +```javascript +// Before: Generic paths +module.exports = { + content: ["./src/**/*.{js,jsx,ts,tsx}"], +} + +// After: Specific optimized paths +module.exports = { + content: [ + // Be specific about directories + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx}', + './app/**/*.{js,ts,jsx,tsx}', + './lib/**/*.{js,ts}', + + // Include component libraries if used + './node_modules/@your-ui-lib/**/*.{js,ts,jsx,tsx}', + + // Exclude unnecessary files + '!./node_modules', + '!./.git', + '!./.next', + '!./dist', + '!./coverage', + ], +} +``` + +### Advanced Content Configuration + +```javascript +module.exports = { + content: [ + { + files: ['./src/**/*.{js,ts,jsx,tsx}'], + // Custom extraction for complex patterns + transform: { + js: (content) => { + // Extract classes from template literals + return content.match(/(?:class|className)(?:Name)?[`:=]\s*[`"']([^`"']*)[`"']/g) || [] + } + } + }, + { + files: ['./components/**/*.{js,ts,jsx,tsx}'], + // Extract dynamic class compositions + transform: { + jsx: (content) => { + const matches = content.match(/(?:clsx|cn|twMerge)\([^)]*\)/g) || [] + return matches.join(' ') + } + } + } + ] +} +``` + +## Performance Optimizations + +### Production Build Configuration + +```javascript +// postcss.config.js - Environment-specific optimization +module.exports = { + plugins: [ + require('tailwindcss'), + require('autoprefixer'), + + // Production-only optimizations + ...(process.env.NODE_ENV === 'production' ? [ + require('@fullhuman/postcss-purgecss')({ + content: [ + './pages/**/*.{js,ts,jsx,tsx}', + './components/**/*.{js,ts,jsx,tsx}', + ], + defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [], + safelist: { + standard: [/^hljs/, /^prose/], + deep: [/^animate-/, /^transition-/], + greedy: [/^bg-/, /^text-/, /^border-/] + } + }), + require('cssnano')({ + preset: ['advanced', { + discardComments: { removeAll: true }, + reduceIdents: false, + zindex: false, + }] + }) + ] : []) + ] +} +``` + +### Webpack/Next.js Optimization + +```javascript +// next.config.js +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + optimizeCss: true, + swcMinify: true, + }, + + webpack: (config, { dev, isServer }) => { + // CSS optimization for production + if (!dev && !isServer) { + config.optimization.splitChunks.cacheGroups.styles = { + name: 'styles', + test: /\.(css|scss)$/, + chunks: 'all', + enforce: true, + } + } + + return config + }, +} + +module.exports = nextConfig +``` + +### Vite Optimization + +```javascript +// vite.config.js +import { defineConfig } from 'vite' + +export default defineConfig({ + css: { + postcss: './postcss.config.js', + devSourcemap: true, + }, + + build: { + cssCodeSplit: true, + cssMinify: 'esbuild', + + rollupOptions: { + output: { + manualChunks: { + 'tailwind-base': ['tailwindcss/base'], + 'tailwind-components': ['tailwindcss/components'], + 'tailwind-utilities': ['tailwindcss/utilities'] + } + } + }, + + reportCompressedSize: true, + chunkSizeWarningLimit: 1000, + }, +}) +``` + +## Safelist Optimization + +### Smart Safelist Configuration + +```javascript +module.exports = { + safelist: [ + // Dynamic color variations + { + pattern: /^(bg|text|border)-(red|green|blue|yellow|purple)-(50|100|500|600|700|900)$/, + variants: ['hover', 'focus', 'active', 'disabled'], + }, + + // Animation and state classes + { + pattern: /^(opacity|scale|rotate|translate[xy]?)-(0|25|50|75|100)$/, + variants: ['group-hover', 'peer-focus', 'motion-reduce'], + }, + + // Responsive grid columns (often dynamically generated) + /^grid-cols-(1|2|3|4|6|12)$/, + + // Common state classes + /^(animate|transition)-.+/, + + // Dynamic spacing that might be calculated + { + pattern: /^(p|m|w|h)-(0|1|2|4|8|16|32|64)$/, + variants: ['sm', 'md', 'lg', 'xl', '2xl'], + }, + ], + + // Block classes that should never be included + blocklist: [ + 'container', // If using custom container + 'debug-*', // Debug utilities + ], +} +``` + +## Bundle Analysis Tools + +### CSS Analysis Script + +```javascript +// scripts/analyze-css.js +const fs = require('fs') +const path = require('path') + +function analyzeCSSBundle(filePath) { + const css = fs.readFileSync(filePath, 'utf8') + + // Extract all utility classes + const utilities = css.match(/\.[a-zA-Z][a-zA-Z0-9_-]*\s*{/g) || [] + const uniqueUtilities = [...new Set(utilities.map(u => u.replace(/\s*{$/, '')))] + + // File size analysis + const stats = fs.statSync(filePath) + const sizeKB = (stats.size / 1024).toFixed(2) + + console.log(`CSS Bundle Analysis:`) + console.log(`- File size: ${sizeKB}KB`) + console.log(`- Utility classes: ${uniqueUtilities.length}`) + console.log(`- Average bytes per utility: ${(stats.size / uniqueUtilities.length).toFixed(2)}`) + + // Most common utility patterns + const patterns = {} + uniqueUtilities.forEach(utility => { + const pattern = utility.replace(/\d+/g, '#').replace(/-(xs|sm|md|lg|xl|2xl)$/, '-*') + patterns[pattern] = (patterns[pattern] || 0) + 1 + }) + + const topPatterns = Object.entries(patterns) + .sort(([,a], [,b]) => b - a) + .slice(0, 10) + + console.log('\nTop utility patterns:') + topPatterns.forEach(([pattern, count]) => { + console.log(`- ${pattern}: ${count} variants`) + }) +} + +// Usage: node scripts/analyze-css.js dist/output.css +analyzeCSSBundle(process.argv[2]) +``` + +### Unused CSS Detection + +```bash +# Using PurgeCSS to find unused CSS +npm install -g purgecss + +# Analyze unused CSS +purgecss --css dist/styles.css \ + --content 'src/**/*.{js,jsx,ts,tsx}' \ + --output temp/ \ + --rejected + +# Compare sizes +echo "Original size:" && wc -c dist/styles.css +echo "Purged size:" && wc -c temp/styles.css +``` + +## Monitoring and Automation + +### GitHub Actions for Bundle Size Monitoring + +```yaml +# .github/workflows/css-size-check.yml +name: CSS Bundle Size Check + +on: [pull_request] + +jobs: + css-size: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build CSS + run: npm run build:css + + - name: Check bundle size + run: | + SIZE=$(wc -c < dist/styles.css) + echo "CSS bundle size: $SIZE bytes" + if [ $SIZE -gt 100000 ]; then + echo "❌ CSS bundle is too large (>100KB)" + exit 1 + else + echo "✅ CSS bundle size is acceptable" + fi + + - name: Comment PR + uses: actions/github-script@v6 + with: + script: | + const fs = require('fs'); + const size = fs.statSync('dist/styles.css').size; + const sizeKB = (size / 1024).toFixed(2); + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `📊 CSS Bundle Size: ${sizeKB}KB` + }); +``` + +### Pre-commit Hook for CSS Optimization + +```bash +#!/bin/sh +# .husky/pre-commit + +# Build CSS and check size +npm run build:css + +# Check if CSS file is too large +SIZE=$(wc -c < dist/styles.css) +if [ $SIZE -gt 100000 ]; then + echo "❌ CSS bundle is too large (${SIZE} bytes > 100KB)" + echo "Consider optimizing your Tailwind configuration" + exit 1 +fi + +echo "✅ CSS bundle size is acceptable (${SIZE} bytes)" +``` + +## Optimization Checklist + +### Performance Checklist + +- [ ] Content paths are specific and exclude unnecessary files +- [ ] Safelist includes only genuinely dynamic classes +- [ ] Unused plugins are removed from configuration +- [ ] CSS is minified in production builds +- [ ] CSS code splitting is enabled where possible +- [ ] Bundle size is monitored in CI/CD pipeline + +### Development Experience Checklist + +- [ ] Hot reload works efficiently with content changes +- [ ] Build times are optimized for development +- [ ] Source maps are available for debugging +- [ ] Error reporting is clear for configuration issues + +### Production Checklist + +- [ ] CSS is compressed (Gzip/Brotli) +- [ ] Critical CSS is inlined where beneficial +- [ ] Unused CSS is properly purged +- [ ] Bundle analysis is automated +- [ ] Performance monitoring is in place + +Remember: **Optimize for your specific use case, measure before and after, and maintain monitoring over time!** diff --git a/ui/tailwindcss/.claude/commands/setup-dark-mode.md b/ui/tailwindcss/.claude/commands/setup-dark-mode.md new file mode 100644 index 0000000..7b18b13 --- /dev/null +++ b/ui/tailwindcss/.claude/commands/setup-dark-mode.md @@ -0,0 +1,721 @@ +--- +name: setup-dark-mode +description: Set up comprehensive dark mode support with TailwindCSS using CSS variables, theme switching, and system preferences +tools: Write, Edit, Read, Bash +--- + +# Setup Dark Mode with TailwindCSS + +This command sets up a complete dark mode system using TailwindCSS with CSS variables, automatic theme detection, and smooth transitions. + +## What This Command Does + +1. **CSS Variables Configuration** + - Sets up semantic color system using CSS variables + - Configures light and dark theme variants + - Creates smooth transition system between themes + - Implements proper contrast ratios for accessibility + +2. **Theme Configuration** + - Configures TailwindCSS for class-based dark mode + - Sets up color palette using CSS variables + - Creates theme-aware utility classes + - Optimizes for design system consistency + +3. **JavaScript Theme Controller** + - Detects system theme preferences + - Provides manual theme switching functionality + - Persists user theme preferences + - Handles theme transitions smoothly + +4. **Component Integration** + - Creates theme-aware components + - Implements proper dark mode patterns + - Sets up theme toggle components + - Provides theme context for React/Vue apps + +## Configuration Setup + +### TailwindCSS Configuration + +```javascript +// tailwind.config.js +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + './src/**/*.{js,ts,jsx,tsx,mdx}', + ], + darkMode: 'class', // Enable class-based dark mode + theme: { + extend: { + colors: { + // CSS variable-based color system + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + + // Semantic colors + success: { + DEFAULT: 'hsl(var(--success))', + foreground: 'hsl(var(--success-foreground))', + }, + + warning: { + DEFAULT: 'hsl(var(--warning))', + foreground: 'hsl(var(--warning-foreground))', + }, + + info: { + DEFAULT: 'hsl(var(--info))', + foreground: 'hsl(var(--info-foreground))', + }, + }, + + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + + boxShadow: { + 'sm': 'var(--shadow-sm)', + 'DEFAULT': 'var(--shadow)', + 'md': 'var(--shadow-md)', + 'lg': 'var(--shadow-lg)', + 'xl': 'var(--shadow-xl)', + }, + }, + }, + plugins: [], +} +``` + +### CSS Variables Setup + +```css +/* globals.css */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + /* Light theme colors */ + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + /* Semantic colors */ + --success: 142.1 76.2% 36.3%; + --success-foreground: 355.7 100% 97.3%; + + --warning: 32.5 94.6% 43.7%; + --warning-foreground: 26 83.3% 14.1%; + + --info: 217.2 91.2% 59.8%; + --info-foreground: 210 40% 98%; + + /* Design tokens */ + --radius: 0.5rem; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + } + + .dark { + /* Dark theme colors */ + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 84% 4.9%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + + /* Semantic colors for dark theme */ + --success: 142.1 70.6% 45.3%; + --success-foreground: 144.9 80.4% 10%; + + --warning: 32.5 94.6% 43.7%; + --warning-foreground: 26 83.3% 14.1%; + + --info: 217.2 91.2% 59.8%; + --info-foreground: 222.2 84% 4.9%; + + /* Dark theme shadows */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4), 0 1px 2px -1px rgb(0 0 0 / 0.3); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3); + } + + /* Global base styles */ + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + font-feature-settings: "rlig" 1, "calt" 1; + } + + /* Smooth theme transitions */ + html { + transition: color-scheme 0.2s ease-in-out; + } + + * { + transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out; + } + + /* Focus styles */ + .focus-visible { + @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2; + } +} + +/* Custom scrollbar for dark mode */ +@layer utilities { + .scrollbar-thin { + scrollbar-width: thin; + } + + .scrollbar-track-transparent { + scrollbar-color: hsl(var(--muted)) transparent; + } + + .dark .scrollbar-track-transparent { + scrollbar-color: hsl(var(--muted)) transparent; + } +} +``` + +## Theme Management + +### JavaScript Theme Controller + +```javascript +// lib/theme.js +class ThemeManager { + constructor() { + this.theme = 'system' + this.systemTheme = 'light' + this.init() + } + + init() { + // Get stored theme or default to system + this.theme = localStorage.getItem('theme') || 'system' + + // Listen for system theme changes + this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + this.systemTheme = this.mediaQuery.matches ? 'dark' : 'light' + + this.mediaQuery.addEventListener('change', (e) => { + this.systemTheme = e.matches ? 'dark' : 'light' + if (this.theme === 'system') { + this.applyTheme() + } + }) + + // Apply initial theme + this.applyTheme() + } + + setTheme(theme) { + this.theme = theme + localStorage.setItem('theme', theme) + this.applyTheme() + this.notifyListeners() + } + + applyTheme() { + const root = document.documentElement + const isDark = this.theme === 'dark' || (this.theme === 'system' && this.systemTheme === 'dark') + + if (isDark) { + root.classList.add('dark') + root.style.colorScheme = 'dark' + } else { + root.classList.remove('dark') + root.style.colorScheme = 'light' + } + } + + getTheme() { + return this.theme + } + + getEffectiveTheme() { + return this.theme === 'system' ? this.systemTheme : this.theme + } + + // Event listener system + listeners = new Set() + + subscribe(callback) { + this.listeners.add(callback) + return () => this.listeners.delete(callback) + } + + notifyListeners() { + this.listeners.forEach(callback => { + callback({ + theme: this.theme, + effectiveTheme: this.getEffectiveTheme() + }) + }) + } +} + +// Create global instance +const themeManager = new ThemeManager() + +export { themeManager } +``` + +### React Theme Hook + +```jsx +// hooks/useTheme.js +import { useState, useEffect } from 'react' +import { themeManager } from '@/lib/theme' + +export function useTheme() { + const [theme, setThemeState] = useState(themeManager.getTheme()) + const [effectiveTheme, setEffectiveTheme] = useState(themeManager.getEffectiveTheme()) + + useEffect(() => { + const unsubscribe = themeManager.subscribe(({ theme, effectiveTheme }) => { + setThemeState(theme) + setEffectiveTheme(effectiveTheme) + }) + + return unsubscribe + }, []) + + const setTheme = (newTheme) => { + themeManager.setTheme(newTheme) + } + + return { + theme, + effectiveTheme, + setTheme, + themes: ['light', 'dark', 'system'] + } +} +``` + +### React Theme Provider + +```jsx +// providers/ThemeProvider.jsx +import React, { createContext, useContext, useEffect, useState } from 'react' + +const ThemeProviderContext = createContext({ + theme: 'system', + setTheme: () => null, +}) + +export function ThemeProvider({ children, defaultTheme = 'system' }) { + const [theme, setTheme] = useState(() => { + if (typeof window !== 'undefined') { + return localStorage.getItem('theme') || defaultTheme + } + return defaultTheme + }) + + useEffect(() => { + const root = window.document.documentElement + root.classList.remove('light', 'dark') + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' + root.classList.add(systemTheme) + return + } + + root.classList.add(theme) + }, [theme]) + + const value = { + theme, + setTheme: (theme) => { + localStorage.setItem('theme', theme) + setTheme(theme) + }, + } + + return ( + <ThemeProviderContext.Provider value={value}> + {children} + </ThemeProviderContext.Provider> + ) +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext) + + if (context === undefined) + throw new Error('useTheme must be used within a ThemeProvider') + + return context +} +``` + +## Theme Toggle Components + +### Simple Theme Toggle + +```jsx +// components/ThemeToggle.jsx +import React from 'react' +import { Moon, Sun } from 'lucide-react' +import { useTheme } from '@/hooks/useTheme' +import { Button } from '@/components/ui/Button' + +export function ThemeToggle() { + const { effectiveTheme, setTheme } = useTheme() + + const toggleTheme = () => { + setTheme(effectiveTheme === 'light' ? 'dark' : 'light') + } + + return ( + <Button + variant="ghost" + size="icon" + onClick={toggleTheme} + className="relative" + aria-label="Toggle theme" + > + <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> + <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> + </Button> + ) +} +``` + +### Advanced Theme Selector + +```jsx +// components/ThemeSelector.jsx +import React from 'react' +import { Monitor, Moon, Sun } from 'lucide-react' +import { useTheme } from '@/hooks/useTheme' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/DropdownMenu' +import { Button } from '@/components/ui/Button' + +export function ThemeSelector() { + const { theme, setTheme } = useTheme() + + const themes = [ + { value: 'light', label: 'Light', icon: Sun }, + { value: 'dark', label: 'Dark', icon: Moon }, + { value: 'system', label: 'System', icon: Monitor }, + ] + + const currentTheme = themes.find(t => t.value === theme) + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" className="w-full justify-start"> + <currentTheme.icon className="mr-2 h-4 w-4" /> + {currentTheme.label} + </Button> + </DropdownMenuTrigger> + + <DropdownMenuContent align="end"> + {themes.map(({ value, label, icon: Icon }) => ( + <DropdownMenuItem + key={value} + onClick={() => setTheme(value)} + className="cursor-pointer" + > + <Icon className="mr-2 h-4 w-4" /> + {label} + {theme === value && ( + <span className="ml-auto">✓</span> + )} + </DropdownMenuItem> + ))} + </DropdownMenuContent> + </DropdownMenu> + ) +} +``` + +### Animated Theme Toggle + +```jsx +// components/AnimatedThemeToggle.jsx +import React from 'react' +import { useTheme } from '@/hooks/useTheme' +import { cn } from '@/lib/utils' + +export function AnimatedThemeToggle() { + const { effectiveTheme, setTheme } = useTheme() + const isDark = effectiveTheme === 'dark' + + const toggleTheme = () => { + setTheme(isDark ? 'light' : 'dark') + } + + return ( + <button + onClick={toggleTheme} + className={cn( + 'relative inline-flex h-12 w-12 items-center justify-center rounded-full', + 'bg-background border-2 border-border shadow-lg', + 'transition-all duration-300 ease-in-out', + 'hover:scale-110 hover:shadow-xl', + 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2' + )} + aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`} + > + <div className="relative h-6 w-6 overflow-hidden"> + {/* Sun icon */} + <svg + className={cn( + 'absolute inset-0 h-6 w-6 text-yellow-500 transition-all duration-300', + isDark ? 'rotate-90 scale-0 opacity-0' : 'rotate-0 scale-100 opacity-100' + )} + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" + /> + </svg> + + {/* Moon icon */} + <svg + className={cn( + 'absolute inset-0 h-6 w-6 text-blue-400 transition-all duration-300', + isDark ? 'rotate-0 scale-100 opacity-100' : '-rotate-90 scale-0 opacity-0' + )} + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" + /> + </svg> + </div> + </button> + ) +} +``` + +## Theme-Aware Components + +### Dark Mode Image Component + +```jsx +// components/ThemeAwareImage.jsx +import React from 'react' +import { useTheme } from '@/hooks/useTheme' + +export function ThemeAwareImage({ + lightSrc, + darkSrc, + alt, + className, + ...props +}) { + const { effectiveTheme } = useTheme() + const src = effectiveTheme === 'dark' ? darkSrc : lightSrc + + return ( + <img + src={src} + alt={alt} + className={className} + {...props} + /> + ) +} +``` + +### Theme Detection Script + +```html +<!-- Add to document head for no-flash theme detection --> +<script> + (function() { + const theme = localStorage.getItem('theme') + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + + if (theme === 'dark' || (!theme && systemPrefersDark)) { + document.documentElement.classList.add('dark') + document.documentElement.style.colorScheme = 'dark' + } else { + document.documentElement.classList.remove('dark') + document.documentElement.style.colorScheme = 'light' + } + })() +</script> +``` + +## Testing Dark Mode + +### Dark Mode Test Suite + +```javascript +// tests/dark-mode.test.js +import { render, screen, fireEvent } from '@testing-library/react' +import { ThemeProvider } from '@/providers/ThemeProvider' +import { ThemeToggle } from '@/components/ThemeToggle' + +describe('Dark Mode', () => { + beforeEach(() => { + localStorage.clear() + document.documentElement.className = '' + }) + + test('applies dark mode class when theme is dark', () => { + render( + <ThemeProvider defaultTheme="dark"> + <div>Test content</div> + </ThemeProvider> + ) + + expect(document.documentElement).toHaveClass('dark') + }) + + test('toggles theme when button is clicked', () => { + render( + <ThemeProvider> + <ThemeToggle /> + </ThemeProvider> + ) + + const toggleButton = screen.getByLabelText(/toggle theme/i) + fireEvent.click(toggleButton) + + expect(document.documentElement).toHaveClass('dark') + }) + + test('persists theme preference', () => { + render( + <ThemeProvider> + <ThemeToggle /> + </ThemeProvider> + ) + + const toggleButton = screen.getByLabelText(/toggle theme/i) + fireEvent.click(toggleButton) + + expect(localStorage.getItem('theme')).toBe('dark') + }) +}) +``` + +Remember: **Dark mode should enhance user experience with proper contrast ratios, smooth transitions, and respect for user preferences!** |
