
Frontend Design
- 5 installs
- Updated January 23, 2026
- jwynia/teach
Helps with frontend development tasks.
About
frontend-design is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- frontend-design
- Frontend Development
- AI-coding skill
Frontend Design by the numbers
- 5 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,791 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/teach --skill frontend-designAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| Last updated | January 23, 2026 |
| Repository | jwynia/teach ↗ |
What it does
Helps with frontend development tasks.
Files
Frontend Design
Create distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. This skill combines design philosophy guidance with practical TypeScript tooling for analyzing existing designs and generating design system artifacts.
Core Principle: Choose a clear aesthetic direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
When to Use This Skill
Use when:
- Building web components, pages, or applications
- Auditing existing CSS for design inconsistencies
- Extracting design tokens from legacy codebases
- Generating color palettes and typography systems
- Creating component templates across frameworks
- Checking accessibility compliance for design elements
Don't use when:
- Simple text content changes
- Backend-only work
- Non-visual functionality
Prerequisites
- Deno runtime (for script execution)
- Optional: Existing CSS/design files for analysis
- Optional: Design tokens file for reference
Quick Start
This skill operates in three modes:
1. Analyze Mode
Audit existing styles, extract tokens, check accessibility.
# Audit CSS for design patterns and inconsistencies
deno run --allow-read scripts/analyze-styles.ts styles.css
# Extract design tokens from existing CSS
deno run --allow-read scripts/extract-tokens.ts ./src --format css
# Check accessibility (contrast, focus states)
deno run --allow-read scripts/analyze-accessibility.ts component.tsx2. Specify Mode
Create JSON specifications defining what to generate. See assets/ for schema examples.
3. Generate Mode
Create palettes, typography systems, tokens, and components.
# Generate a color palette
deno run --allow-read --allow-write scripts/generate-palette.ts --seed "#2563eb" --theme warm
# Generate typography system
deno run --allow-read --allow-write scripts/generate-typography.ts --display "Playfair Display" --body "Source Sans Pro"
# Generate design tokens file
deno run --allow-read --allow-write scripts/generate-tokens.ts tokens-spec.json ./output/
# Generate a component
deno run --allow-read --allow-write scripts/generate-component.ts --name Button --framework react --styling tailwind---
Instructions
Phase 1: Analysis (Optional but Recommended)
Before creating new designs, audit existing code to understand current patterns.
1a. Style Analysis
Analyze CSS files to identify colors, typography, spacing, and inconsistencies:
deno run --allow-read scripts/analyze-styles.ts <input> [options]
Options:
--tokens <file> Compare against existing design tokens
--pretty Pretty-print JSON output
--format <type> Output format: json (default), summaryOutput includes:
- Color usage inventory with hex normalization
- Typography patterns (fonts, sizes, weights)
- Spacing value distribution
- Inconsistencies and recommendations
1b. Token Extraction
Extract design tokens from CSS files into standardized formats:
deno run --allow-read scripts/extract-tokens.ts <input> [options]
Options:
--format <type> Output: css, scss, tailwind, style-dictionary, tokens-studio
--output-css Also output CSS variables file1c. Accessibility Audit
Check design-related accessibility issues:
deno run --allow-read scripts/analyze-accessibility.ts <input> [options]
Options:
--format <type> Output: json, summary
--level <AA|AAA> WCAG conformance level (default: AA)Checks include:
- Color contrast ratios
- Focus indicator presence
- Touch target sizes
- Motion preference respect
---
Phase 2: Generation
2a. Color Palette Generation
Generate cohesive color palettes from seed colors or themes:
deno run --allow-read --allow-write scripts/generate-palette.ts [options] <output>
Options:
--seed <color> Primary seed color (hex)
--theme <type> warm, cool, neutral, vibrant, muted, dark, light
--style <type> minimalist, bold, organic, corporate, playful
--shades Generate 50-950 shade scale
--semantic Generate success/warning/error colors
--contrast <lvl> Target contrast: AA (default), AAA
--format <type> css, scss, tailwind, tokens, jsonExample specification (palette-spec.json):
{
"seedColors": {
"primary": "#2563eb",
"accent": "#f59e0b"
},
"theme": "cool",
"generateShades": true,
"generateSemantics": true,
"contrastTarget": "AA",
"outputFormat": "css"
}2b. Typography System Generation
Generate typography systems with font stacks and scales:
deno run --allow-read --allow-write scripts/generate-typography.ts [options] <output>
Options:
--display <font> Display/heading font family
--body <font> Body text font family
--mono <font> Monospace font family
--scale <type> minor-second, major-second, minor-third, major-third, perfect-fourth, golden-ratio
--base <px> Base font size (default: 16)
--line-height tight, normal, relaxed
--responsive Generate responsive breakpoints
--format <type> css, scss, tailwind, tokensType scales:
| Scale | Ratio | Character |
|---|---|---|
| minor-second | 1.067 | Subtle, conservative |
| major-second | 1.125 | Balanced, professional |
| minor-third | 1.200 | Clear hierarchy |
| major-third | 1.250 | Strong presence |
| perfect-fourth | 1.333 | Bold, impactful |
| golden-ratio | 1.618 | Dramatic, artistic |
2c. Design Token Generation
Generate comprehensive design token files in multiple formats:
deno run --allow-read --allow-write scripts/generate-tokens.ts <spec> <output-dir>Specification structure:
{
"name": "my-design-system",
"tokens": {
"color": { "primary": "#2563eb", "secondary": "#64748b" },
"spacing": { "xs": "0.25rem", "sm": "0.5rem", "md": "1rem" },
"typography": {
"fontFamilies": { "display": "Playfair Display", "body": "Inter" },
"fontSizes": { "sm": "0.875rem", "base": "1rem", "lg": "1.125rem" }
},
"shadow": { "sm": "0 1px 2px rgba(0,0,0,0.05)" },
"border": { "radius": { "sm": "0.25rem", "md": "0.5rem" } },
"animation": { "duration": { "fast": "150ms", "normal": "300ms" } }
},
"outputFormats": ["css", "scss", "tailwind", "ts"]
}2d. Component Generation
Generate component templates with design-focused styling:
deno run --allow-read --allow-write scripts/generate-component.ts [options] <output-dir>
Options:
--name <name> Component name (required)
--type <type> button, card, input, modal, navigation, hero, custom
--framework <fw> react, vue, svelte, html
--styling <type> css, tailwind, css-modules, styled-components, emotion
--aesthetic <type> minimal, bold, organic, brutalist, glassmorphism, neumorphism
--animation <lvl> none, subtle, expressive
--dark-mode Include dark mode support
--tokens <file> Use design tokens fileFramework output examples:
React with Tailwind:
export function Button({ variant = 'primary', children }: ButtonProps) {
return (
<button className="px-4 py-2 rounded-lg font-medium transition-all">
{children}
</button>
);
}Vue with scoped CSS:
<template>
<button :class="['btn', `btn--${variant}`]"><slot /></button>
</template>
<script setup lang="ts">
defineProps<{ variant?: 'primary' | 'secondary' }>();
</script>
<style scoped>.btn { /* styles */ }</style>Svelte:
<script lang="ts">
export let variant: 'primary' | 'secondary' = 'primary';
</script>
<button class="btn btn--{variant}"><slot /></button>
<style>.btn { /* styles */ }</style>---
Examples
Example 1: Audit and Modernize Legacy CSS
# 1. Analyze existing styles
deno run --allow-read scripts/analyze-styles.ts ./legacy/styles.css --pretty > audit.json
# 2. Extract tokens from the analysis
deno run --allow-read scripts/extract-tokens.ts ./legacy/styles.css --format css > tokens.css
# 3. Generate improved palette from dominant color
deno run --allow-read --allow-write scripts/generate-palette.ts --seed "#336699" --shades --format css palette.cssExample 2: Create Design System from Scratch
# 1. Generate color palette
deno run --allow-read --allow-write scripts/generate-palette.ts \
--seed "#8b5cf6" --theme vibrant --shades --semantic --format css colors.css
# 2. Generate typography system
deno run --allow-read --allow-write scripts/generate-typography.ts \
--display "Space Grotesk" --body "Inter" --scale major-third --responsive --format css typography.css
# 3. Generate comprehensive tokens
deno run --allow-read --allow-write scripts/generate-tokens.ts design-spec.json ./tokens/Example 3: Generate Component Library
# Generate button component for React with Tailwind
deno run --allow-read --allow-write scripts/generate-component.ts \
--name Button --framework react --styling tailwind --aesthetic bold ./components/
# Generate card component for Vue with CSS modules
deno run --allow-read --allow-write scripts/generate-component.ts \
--name Card --framework vue --styling css-modules --aesthetic glassmorphism ./components/
# Generate input for Svelte
deno run --allow-read --allow-write scripts/generate-component.ts \
--name Input --framework svelte --styling css --aesthetic minimal ./components/Example 4: Accessibility Check Before Launch
# Audit all components
deno run --allow-read scripts/analyze-accessibility.ts ./src/components --level AAA --format summary---
Script Reference
| Script | Purpose | Permissions |
|---|---|---|
analyze-styles.ts | Audit CSS for patterns and inconsistencies | --allow-read |
extract-tokens.ts | Extract design tokens from CSS | --allow-read |
analyze-accessibility.ts | Check design accessibility | --allow-read |
generate-palette.ts | Generate color palettes | --allow-read --allow-write |
generate-typography.ts | Generate typography systems | --allow-read --allow-write |
generate-tokens.ts | Generate multi-format tokens | --allow-read --allow-write |
generate-component.ts | Generate framework components | --allow-read --allow-write |
---
Design Philosophy
Avoid generic "AI slop" aesthetics. See references/design-philosophy.md for detailed guidance.
Key principles:
- Typography: Choose distinctive fonts, not defaults (avoid Arial, Inter, Roboto)
- Color: Dominant colors with sharp accents, not timid palettes
- Motion: Orchestrated reveals over scattered micro-interactions
- Composition: Asymmetry, overlap, grid-breaking elements
- Texture: Gradients, noise, patterns - not flat solid colors
Never converge on:
- Purple gradients on white backgrounds
- System fonts or overused families
- Predictable card-based layouts
- Cookie-cutter Bootstrap aesthetics
---
Common Issues
"CSS parsing failed"
- Ensure valid CSS syntax
- SCSS/PostCSS may need preprocessing first
"Color contrast check failed"
- Check for CSS variables that couldn't be resolved
- Provide the actual color values or tokens file
"Unknown framework"
- Supported: react, vue, svelte, html
- Check spelling and lowercase
"Token format not supported"
- Supported: css, scss, tailwind, style-dictionary, tokens-studio, json, ts
---
Additional Resources
references/design-philosophy.md- Detailed anti-slop aesthetics guidereferences/token-formats.md- Design token format specificationsreferences/framework-templates.md- Framework-specific patternsassets/*.json- JSON schema files for specifications
---
Related Skills
web-search- Research design inspiration and trendsresearch-workflow- Deep dive into design systems
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Component Specification",
"description": "Schema for component template generation",
"type": "object",
"required": ["name", "framework"],
"properties": {
"name": {
"type": "string",
"description": "Component name (PascalCase)",
"pattern": "^[A-Z][a-zA-Z0-9]*$",
"minLength": 1,
"maxLength": 64
},
"type": {
"type": "string",
"description": "Component type for template selection",
"enum": ["button", "card", "input", "modal", "navigation", "hero", "custom"],
"default": "custom"
},
"framework": {
"type": "string",
"description": "Target framework",
"enum": ["react", "vue", "svelte", "html"]
},
"styling": {
"type": "string",
"description": "Styling approach",
"enum": ["css", "tailwind", "css-modules", "styled-components", "emotion"],
"default": "css"
},
"aesthetic": {
"type": "object",
"description": "Aesthetic configuration",
"properties": {
"style": {
"type": "string",
"description": "Visual style",
"enum": ["minimal", "bold", "organic", "brutalist", "glassmorphism", "neumorphism"]
},
"animation": {
"type": "string",
"description": "Animation level",
"enum": ["none", "subtle", "expressive"],
"default": "subtle"
},
"darkMode": {
"type": "boolean",
"description": "Include dark mode support",
"default": false
}
}
},
"tokens": {
"type": "string",
"description": "Path to design tokens file for styling"
},
"props": {
"type": "object",
"description": "Component props definition",
"additionalProperties": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "TypeScript type"
},
"default": {
"type": "string",
"description": "Default value"
},
"required": {
"type": "boolean",
"description": "Whether prop is required",
"default": false
}
},
"required": ["type"]
}
},
"slots": {
"type": "array",
"description": "Named slots (for Vue/Svelte) or children patterns (for React)",
"items": { "type": "string" }
},
"variants": {
"type": "array",
"description": "Style variants to generate",
"items": { "type": "string" }
}
},
"examples": [
{
"name": "Button",
"type": "button",
"framework": "react",
"styling": "tailwind",
"aesthetic": {
"style": "bold",
"animation": "subtle",
"darkMode": true
},
"props": {
"variant": {
"type": "'primary' | 'secondary' | 'ghost'",
"default": "'primary'"
},
"size": {
"type": "'sm' | 'md' | 'lg'",
"default": "'md'"
},
"disabled": {
"type": "boolean",
"default": "false"
}
},
"variants": ["primary", "secondary", "ghost"]
},
{
"name": "Card",
"type": "card",
"framework": "vue",
"styling": "css-modules",
"aesthetic": {
"style": "glassmorphism",
"animation": "expressive"
},
"slots": ["header", "default", "footer"],
"props": {
"elevated": {
"type": "boolean",
"default": "false"
}
}
},
{
"name": "Input",
"type": "input",
"framework": "svelte",
"styling": "css",
"aesthetic": {
"style": "minimal"
},
"props": {
"label": {
"type": "string",
"required": true
},
"placeholder": {
"type": "string"
},
"error": {
"type": "string"
}
}
}
]
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Palette Specification",
"description": "Schema for color palette generation specifications",
"type": "object",
"properties": {
"seedColors": {
"type": "object",
"description": "Seed colors to build palette from",
"properties": {
"primary": {
"type": "string",
"description": "Primary brand color (hex format)",
"pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
},
"secondary": {
"type": "string",
"description": "Secondary color (hex format)",
"pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
},
"accent": {
"type": "string",
"description": "Accent color (hex format)",
"pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
}
}
},
"theme": {
"type": "string",
"description": "Overall theme direction",
"enum": ["warm", "cool", "neutral", "vibrant", "muted", "dark", "light"]
},
"style": {
"type": "string",
"description": "Design style influence",
"enum": ["minimalist", "bold", "organic", "corporate", "playful"]
},
"generateShades": {
"type": "boolean",
"description": "Generate 50-950 shade scale for each color",
"default": false
},
"generateSemantics": {
"type": "boolean",
"description": "Generate semantic colors (success, warning, error, info)",
"default": false
},
"contrastTarget": {
"type": "string",
"description": "WCAG contrast level to target",
"enum": ["AA", "AAA"],
"default": "AA"
},
"outputFormat": {
"type": "string",
"description": "Output format for the palette",
"enum": ["css", "scss", "tailwind", "tokens", "json"],
"default": "css"
}
},
"examples": [
{
"seedColors": {
"primary": "#2563eb",
"accent": "#f59e0b"
},
"theme": "cool",
"generateShades": true,
"generateSemantics": true,
"contrastTarget": "AA",
"outputFormat": "css"
},
{
"seedColors": {
"primary": "#8b5cf6"
},
"theme": "vibrant",
"style": "playful",
"generateShades": true,
"outputFormat": "tailwind"
}
]
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Design Token Specification",
"description": "Schema for comprehensive design token generation",
"type": "object",
"required": ["name", "tokens", "outputFormats"],
"properties": {
"name": {
"type": "string",
"description": "Design system name",
"minLength": 1,
"maxLength": 64
},
"description": {
"type": "string",
"description": "Description of the design system"
},
"tokens": {
"type": "object",
"description": "Token definitions by category",
"properties": {
"color": {
"type": "object",
"description": "Color tokens",
"additionalProperties": {
"oneOf": [
{ "type": "string" },
{ "type": "object", "additionalProperties": { "type": "string" } }
]
}
},
"spacing": {
"type": "object",
"description": "Spacing scale tokens",
"additionalProperties": { "type": "string" }
},
"typography": {
"type": "object",
"properties": {
"fontFamilies": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"fontSizes": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"fontWeights": {
"type": "object",
"additionalProperties": { "type": "number" }
},
"lineHeights": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"letterSpacing": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"shadow": {
"type": "object",
"description": "Box shadow tokens",
"additionalProperties": { "type": "string" }
},
"border": {
"type": "object",
"properties": {
"radius": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"width": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"animation": {
"type": "object",
"properties": {
"duration": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"easing": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"zIndex": {
"type": "object",
"description": "Z-index scale",
"additionalProperties": { "type": "number" }
}
}
},
"outputFormats": {
"type": "array",
"description": "Output formats to generate",
"items": {
"type": "string",
"enum": ["css", "scss", "json", "js", "ts", "tailwind", "style-dictionary"]
},
"minItems": 1
}
},
"examples": [
{
"name": "my-design-system",
"description": "A modern design system for web applications",
"tokens": {
"color": {
"primary": "#2563eb",
"primary-light": "#3b82f6",
"primary-dark": "#1d4ed8",
"secondary": "#64748b",
"background": "#ffffff",
"surface": "#f8fafc",
"text": "#1e293b",
"text-muted": "#64748b"
},
"spacing": {
"xs": "0.25rem",
"sm": "0.5rem",
"md": "1rem",
"lg": "1.5rem",
"xl": "2rem",
"2xl": "3rem"
},
"typography": {
"fontFamilies": {
"display": "Playfair Display, serif",
"body": "Inter, sans-serif",
"mono": "JetBrains Mono, monospace"
},
"fontSizes": {
"xs": "0.75rem",
"sm": "0.875rem",
"base": "1rem",
"lg": "1.125rem",
"xl": "1.25rem",
"2xl": "1.5rem",
"3xl": "1.875rem",
"4xl": "2.25rem"
},
"fontWeights": {
"normal": 400,
"medium": 500,
"semibold": 600,
"bold": 700
},
"lineHeights": {
"tight": "1.25",
"normal": "1.5",
"relaxed": "1.75"
}
},
"shadow": {
"sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
"md": "0 4px 6px -1px rgb(0 0 0 / 0.1)",
"lg": "0 10px 15px -3px rgb(0 0 0 / 0.1)",
"xl": "0 20px 25px -5px rgb(0 0 0 / 0.1)"
},
"border": {
"radius": {
"sm": "0.25rem",
"md": "0.5rem",
"lg": "1rem",
"full": "9999px"
},
"width": {
"thin": "1px",
"medium": "2px",
"thick": "4px"
}
},
"animation": {
"duration": {
"fast": "150ms",
"normal": "300ms",
"slow": "500ms"
},
"easing": {
"ease-in": "cubic-bezier(0.4, 0, 1, 1)",
"ease-out": "cubic-bezier(0, 0, 0.2, 1)",
"ease-in-out": "cubic-bezier(0.4, 0, 0.2, 1)"
}
},
"zIndex": {
"dropdown": 1000,
"sticky": 1020,
"modal": 1030,
"popover": 1040,
"tooltip": 1050
}
},
"outputFormats": ["css", "scss", "tailwind", "ts"]
}
]
}
Design Philosophy: Avoiding AI Slop
This document provides detailed guidance on creating distinctive, production-grade frontend interfaces that avoid generic "AI-generated" aesthetics.
Core Principle
Choose a clear aesthetic direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Design Thinking Process
Before writing any code, understand the context and commit to a BOLD aesthetic direction:
1. Purpose
- What problem does this interface solve?
- Who uses it and in what context?
- What emotions should it evoke?
2. Aesthetic Tone
Pick a distinctive direction. Here are examples for inspiration:
| Tone | Characteristics |
|---|---|
| Brutally Minimal | Stark contrast, maximum whitespace, single accent color |
| Maximalist Chaos | Dense information, layered textures, vibrant palettes |
| Retro-Futuristic | Neon accents, dark backgrounds, geometric shapes |
| Organic/Natural | Soft curves, earthy tones, flowing animations |
| Luxury/Refined | Subtle typography, muted palettes, premium spacing |
| Playful/Toy-like | Rounded corners, bright colors, bouncy animations |
| Editorial/Magazine | Strong typography hierarchy, asymmetric layouts |
| Brutalist/Raw | Exposed structure, stark fonts, visible grids |
| Art Deco/Geometric | Bold patterns, metallic accents, symmetry |
| Soft/Pastel | Gentle gradients, light shadows, calming colors |
| Industrial/Utilitarian | Monospace fonts, muted colors, functional density |
3. Differentiation
What makes this interface UNFORGETTABLE? What's the one thing someone will remember?
---
The Five Pillars of Distinctive Design
1. Typography
Do:
- Choose fonts that are beautiful, unique, and characterful
- Pair a distinctive display font with a refined body font
- Use unexpected, personality-rich choices
- Consider the context and audience
Don't:
- Default to Arial, Inter, Roboto, or system fonts
- Use the same fonts across every project
- Ignore font pairing harmony
- Over-rely on weight for hierarchy
Distinctive Font Examples:
| Category | Options |
|---|---|
| Display | Playfair Display, Fraunces, Libre Baskerville, Cormorant |
| Sans-Serif | Space Grotesk, DM Sans, Plus Jakarta Sans, Outfit |
| Mono | JetBrains Mono, Fira Code, IBM Plex Mono |
| Unique | Anybody, Instrument Serif, Cabinet Grotesk |
2. Color & Theme
Do:
- Commit to a cohesive aesthetic
- Use CSS variables for consistency
- Choose dominant colors with sharp accents
- Consider context (brand, mood, accessibility)
Don't:
- Default to purple gradients on white
- Use timid, evenly-distributed palettes
- Ignore contrast requirements
- Copy trending palettes without adaptation
Palette Strategies:
- Monochromatic: Single hue with varied saturation/lightness
- Complementary: Opposite colors for high contrast
- Analogous: Adjacent colors for harmony
- Split-Complementary: Main color + two adjacent to its complement
- Triadic: Three evenly-spaced colors
3. Motion
Do:
- Focus on high-impact moments
- Create orchestrated page load reveals with staggered animation-delay
- Use scroll-triggered animations that surprise
- Implement hover states with personality
Don't:
- Add motion for motion's sake
- Create scattered, uncoordinated micro-interactions
- Ignore reduced-motion preferences
- Use jarring or disorienting effects
Motion Hierarchy: 1. Page transitions: Most impactful, highest attention 2. Scroll reveals: Content appearing as user explores 3. Hover states: Feedback and delight 4. Micro-interactions: Subtle polish (buttons, toggles)
CSS-First Approach:
/* Staggered reveal */
.item { animation: fadeIn 0.5s ease-out forwards; opacity: 0; }
.item:nth-child(1) { animation-delay: 0.1s; }
.item:nth-child(2) { animation-delay: 0.2s; }
.item:nth-child(3) { animation-delay: 0.3s; }
/* Respect preferences */
@media (prefers-reduced-motion: reduce) {
.item { animation: none; opacity: 1; }
}4. Spatial Composition
Do:
- Create unexpected layouts
- Use asymmetry intentionally
- Allow elements to overlap
- Employ diagonal flow and grid-breaking elements
- Balance generous negative space OR controlled density
Don't:
- Default to symmetric card grids
- Treat every element equally
- Fear whitespace (or density, if that's your direction)
- Ignore visual rhythm
Layout Techniques:
- Broken grid: Elements that break the expected grid
- Overlap: Layered elements creating depth
- Scale contrast: Large elements next to small
- Negative space: Breathing room as a design element
- Asymmetric balance: Visual weight distributed unevenly
5. Backgrounds & Visual Details
Do:
- Create atmosphere and depth
- Use contextual effects that match the aesthetic
- Layer transparencies for dimension
- Add texture where appropriate
Don't:
- Default to solid white backgrounds
- Use generic gradient presets
- Forget about dark mode considerations
- Over-apply effects
Visual Effects Palette:
- Gradient meshes
- Noise and grain textures
- Geometric patterns
- Layered transparencies
- Dramatic shadows (soft or hard)
- Decorative borders
- Custom cursors
- Glassmorphism (used intentionally)
- Neumorphism (used sparingly)
---
What to Avoid: The "AI Slop" Checklist
Typography Anti-Patterns
- [ ] Inter everywhere
- [ ] Roboto as the only choice
- [ ] Arial or system fonts
- [ ] Identical font usage across projects
Color Anti-Patterns
- [ ] Purple-to-blue gradient on white
- [ ] Generic "modern" teal
- [ ] Desaturated "safe" palettes
- [ ] Unrelated accent colors
Layout Anti-Patterns
- [ ] 3-column card grids
- [ ] Identical padding everywhere
- [ ] Symmetric hero sections
- [ ] Predictable section patterns
Effect Anti-Patterns
- [ ] Subtle gray shadows on everything
- [ ] Generic rounded corners (8px everywhere)
- [ ] Smooth but meaningless transitions
- [ ] Glass effects without purpose
---
Implementation Complexity Matching
Critical: Match implementation complexity to aesthetic vision.
Maximalist Designs
- Elaborate code with extensive animations
- Multiple layered effects
- Complex state management for interactions
- Rich color variations
Minimalist Designs
- Restraint and precision
- Careful attention to spacing
- Perfect typography
- Subtle, meaningful details
- "Less" requires more care, not less code
---
Quality Checklist
Before considering a design complete:
- [ ] Typography is distinctive and intentional
- [ ] Colors support the aesthetic direction
- [ ] Motion is orchestrated, not scattered
- [ ] Layout has visual interest and hierarchy
- [ ] Details add atmosphere without clutter
- [ ] Accessibility is preserved (contrast, motion preferences)
- [ ] The design is memorable - someone would recognize it
---
Remember
"Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision."
No two designs should look the same. Vary themes, fonts, aesthetics. The goal is intentional, memorable, production-grade interfaces.
Framework Templates Reference
This document provides framework-specific patterns and best practices for generating components with the frontend-design skill.
React
File Structure
components/
├── Button/
│ ├── Button.tsx # Component
│ ├── Button.css # Styles (if using CSS)
│ ├── Button.module.css # Styles (if using CSS Modules)
│ ├── Button.test.tsx # Tests
│ └── index.ts # Re-exportComponent Pattern
import React from 'react';
export interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
}
export function Button({
children,
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ButtonProps) {
return (
<button
className={`btn btn--${variant} btn--${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
export default Button;Styling Options
CSS Modules:
import styles from './Button.module.css';
<button className={styles.button} />Tailwind:
<button className="px-4 py-2 bg-blue-500 text-white rounded-lg" />Styled Components:
import styled from 'styled-components';
const StyledButton = styled.button`
padding: 0.5rem 1rem;
background: var(--color-primary);
`;Emotion:
import { css } from '@emotion/react';
const buttonStyle = css`
padding: 0.5rem 1rem;
background: var(--color-primary);
`;---
Vue 3
File Structure
components/
├── Button.vue # Single File Component
└── __tests__/
└── Button.spec.ts # TestsComponent Pattern (Composition API)
<template>
<button
:class="[
'btn',
`btn--${variant}`,
`btn--${size}`,
{ 'btn--disabled': disabled }
]"
:disabled="disabled"
@click="emit('click')"
>
<slot />
</button>
</template>
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
disabled: false,
});
const emit = defineEmits<{
click: [];
}>();
</script>
<style scoped>
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn--primary {
background: var(--color-primary);
color: white;
}
.btn--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>Styling Options
Scoped CSS (default):
<style scoped>
.button { /* styles */ }
</style>CSS Modules:
<style module>
.button { /* styles */ }
</style>
<template>
<button :class="$style.button" />
</template>Tailwind:
<template>
<button class="px-4 py-2 bg-blue-500 text-white rounded-lg" />
</template>---
Svelte
File Structure
components/
├── Button.svelte # Component
└── Button.test.ts # TestsComponent Pattern
<script lang="ts">
export let variant: 'primary' | 'secondary' | 'ghost' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let disabled = false;
</script>
<button
class="btn btn--{variant} btn--{size}"
class:btn--disabled={disabled}
{disabled}
on:click
>
<slot />
</button>
<style>
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn--primary {
background: var(--color-primary);
color: white;
}
.btn--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>Styling Options
Component Styles (default):
<style>
/* Scoped by default */
.button { /* styles */ }
</style>Global Styles:
<style global>
.button { /* global styles */ }
</style>Tailwind:
<button class="px-4 py-2 bg-blue-500 text-white rounded-lg" />---
HTML (Vanilla)
File Structure
components/
├── button.html # Component HTML
├── button.css # Component CSS
└── button.js # Optional JavaScriptComponent Pattern
<!-- button.html -->
<button class="btn btn--primary btn--md">
Button Text
</button>/* button.css */
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.btn--primary {
background: var(--color-primary, #2563eb);
color: white;
}
.btn--primary:hover {
background: var(--color-primary-dark, #1d4ed8);
}
.btn--secondary {
background: var(--color-gray-100, #f3f4f6);
color: var(--color-gray-900, #111827);
}
.btn--ghost {
background: transparent;
color: var(--color-gray-700, #374151);
}
.btn--sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.btn--md {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.btn--lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.btn:disabled,
.btn--disabled {
opacity: 0.5;
cursor: not-allowed;
}---
Common Patterns
Variant Classes
All frameworks use a consistent variant pattern:
.component--variant
.component--size
.component--stateExamples:
.btn--primary,.btn--secondary,.btn--ghost.btn--sm,.btn--md,.btn--lg.btn--disabled,.btn--loading
Prop/Attribute Naming
| Concept | React | Vue | Svelte |
|---|---|---|---|
| Style variant | variant | variant | variant |
| Size | size | size | size |
| Disabled | disabled | disabled | disabled |
| Click handler | onClick | @click | on:click |
| Children | children | <slot /> | <slot /> |
Accessibility
All generated components include:
1. Proper semantics - Use native elements (<button>, <input>) 2. Keyboard support - Focus styles, keyboard handlers 3. ARIA attributes - When native semantics insufficient 4. Focus management - Visible focus indicators
Dark Mode Support
When --dark-mode is enabled:
/* Light mode (default) */
.btn {
background: white;
color: #111827;
}
/* Dark mode */
.dark .btn,
[data-theme="dark"] .btn {
background: #1f2937;
color: white;
}
/* Or using media query */
@media (prefers-color-scheme: dark) {
.btn {
background: #1f2937;
color: white;
}
}Animation Levels
| Level | Description | Usage |
|---|---|---|
none | No animations | Accessibility, performance |
subtle | Micro-interactions | Most components |
expressive | Bold animations | Hero sections, CTAs |
/* Subtle */
.btn {
transition: all 0.2s ease-out;
}
/* Expressive */
.btn {
transition: all 0.3s ease-out;
}
.btn:hover {
transform: scale(1.05);
}
.btn:active {
transform: scale(0.95);
}---
Best Practices
1. Use semantic HTML - <button> for buttons, <a> for links 2. Include TypeScript types - For React and Vue 3. Support variants - Primary, secondary, ghost at minimum 4. Include sizes - sm, md, lg 5. Handle disabled state - Visual and functional 6. Add focus styles - Visible focus indicators 7. Respect motion preferences - prefers-reduced-motion 8. Use CSS custom properties - For theming flexibility
Design Token Formats Reference
This document describes the various design token output formats supported by the frontend-design skill tools.
Overview
Design tokens are the atomic values of a design system - colors, typography, spacing, etc. stored in a format that can be consumed by different platforms and tools.
Supported Formats
1. CSS Custom Properties
File extension: .css
Standard CSS variables that work in any modern browser.
:root {
/* Colors */
--color-primary-500: #2563eb;
--color-primary-600: #1d4ed8;
/* Spacing */
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
/* Typography */
--font-body: Inter, sans-serif;
--text-base: 1rem;
}Usage:
.button {
background: var(--color-primary-500);
padding: var(--spacing-sm) var(--spacing-md);
font-family: var(--font-body);
}Pros:
- Native browser support
- Runtime theming (dark mode, etc.)
- No build step required
Cons:
- No type safety
- Limited IDE support
---
2. SCSS Variables
File extension: .scss
SCSS/Sass variables with maps for iteration.
// Colors
$color-primary-500: #2563eb;
$color-primary-600: #1d4ed8;
// Spacing
$spacing-sm: 0.5rem;
$spacing-md: 1rem;
// Maps for iteration
$colors: (
'primary-500': #2563eb,
'primary-600': #1d4ed8,
);
$spacing: (
'sm': 0.5rem,
'md': 1rem,
);Usage:
.button {
background: $color-primary-500;
padding: $spacing-sm $spacing-md;
}
// Generate utility classes
@each $name, $value in $spacing {
.p-#{$name} { padding: $value; }
}Pros:
- Compile-time optimization
- Programmatic generation with loops
- Mature ecosystem
Cons:
- Requires build step
- No runtime theming without CSS vars
---
3. Tailwind Config
File extension: .tailwind.js
Extension object for Tailwind CSS configuration.
module.exports = {
colors: {
primary: {
500: '#2563eb',
600: '#1d4ed8',
},
},
spacing: {
sm: '0.5rem',
md: '1rem',
},
fontFamily: {
body: ['Inter', 'sans-serif'],
},
};Usage (in tailwind.config.js):
const tokens = require('./tokens.tailwind.js');
module.exports = {
theme: {
extend: tokens,
},
};Pros:
- Direct Tailwind integration
- Utility class generation
- PurgeCSS optimization
Cons:
- Tailwind-specific
- Requires Tailwind setup
---
4. JSON
File extension: .json
Raw JSON format for maximum portability.
{
"name": "my-design-system",
"tokens": {
"color": {
"primary": {
"500": "#2563eb",
"600": "#1d4ed8"
}
},
"spacing": {
"sm": "0.5rem",
"md": "1rem"
}
}
}Usage:
- Import in any language
- Transform with custom tools
- Use as source of truth
Pros:
- Universal format
- Easy to parse
- Platform agnostic
Cons:
- Requires transformation for use
- No direct runtime usage
---
5. JavaScript Module
File extension: .js
ES module with exports.
export const tokens = {
color: {
primary: {
500: '#2563eb',
600: '#1d4ed8',
},
},
spacing: {
sm: '0.5rem',
md: '1rem',
},
};
export const flatTokens = {
'color-primary-500': '#2563eb',
'color-primary-600': '#1d4ed8',
'spacing-sm': '0.5rem',
'spacing-md': '1rem',
};
export default tokens;Usage:
import { tokens, flatTokens } from './tokens.js';
const primaryColor = tokens.color.primary[500];Pros:
- Direct JavaScript import
- Tree-shakeable
- Works with any bundler
Cons:
- JavaScript only
- No type information
---
6. TypeScript Module
File extension: .ts
TypeScript module with full type definitions.
export interface DesignTokens {
color: {
primary: Record<string, string>;
};
spacing: Record<string, string>;
}
export const tokens: DesignTokens = {
color: {
primary: {
500: '#2563eb',
600: '#1d4ed8',
},
},
spacing: {
sm: '0.5rem',
md: '1rem',
},
} as const;
export type FlatToken = string | number;
export const flatTokens: Record<string, FlatToken> = {
'color-primary-500': '#2563eb',
'spacing-sm': '0.5rem',
};
export default tokens;Usage:
import tokens, { DesignTokens } from './tokens';
// Full type safety
const color: string = tokens.color.primary[500];Pros:
- Full type safety
- IDE autocomplete
- Compile-time validation
Cons:
- TypeScript only
- Requires compilation
---
7. Style Dictionary
File extension: .tokens.json
Format compatible with Amazon's Style Dictionary tool.
{
"color": {
"primary": {
"500": { "value": "#2563eb" },
"600": { "value": "#1d4ed8" }
}
},
"spacing": {
"sm": { "value": "0.5rem" },
"md": { "value": "1rem" }
}
}Usage: Use with Style Dictionary to generate platform-specific outputs (iOS, Android, CSS, etc.).
Pros:
- Multi-platform support
- Powerful transformation system
- Industry standard
Cons:
- Requires Style Dictionary setup
- Additional tooling
---
Format Selection Guide
| Use Case | Recommended Format |
|---|---|
| Web-only, runtime theming | CSS |
| Sass/SCSS project | SCSS |
| Tailwind CSS project | Tailwind |
| Multi-platform design system | Style Dictionary |
| React/JS application | TypeScript |
| Universal source of truth | JSON |
| Quick prototyping | CSS |
Token Categories
All formats support these token categories:
| Category | Examples |
|---|---|
color | Primary, secondary, semantic colors |
spacing | Margins, paddings, gaps |
typography | Font families, sizes, weights, line heights |
shadow | Box shadows, text shadows |
border | Radius, width |
animation | Duration, easing functions |
zIndex | Stacking layers |
Best Practices
1. Use semantic names - color-primary not color-blue 2. Maintain consistency - Same structure across all formats 3. Include metadata - Name, description, version 4. Version control - Track token changes 5. Single source of truth - Generate all formats from one spec
#!/usr/bin/env -S deno run --allow-read
/**
* analyze-accessibility.ts - Design Accessibility Auditor
*
* Checks HTML/component files for design-related accessibility issues
* including color contrast, focus states, touch targets, and motion.
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { basename, extname, resolve } from "jsr:@std/path@1.0.8";
import { walk } from "jsr:@std/fs@1.0.8/walk";
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "analyze-accessibility";
// WCAG contrast ratios
const CONTRAST_AA_NORMAL = 4.5;
const CONTRAST_AA_LARGE = 3.0;
const CONTRAST_AAA_NORMAL = 7.0;
const CONTRAST_AAA_LARGE = 4.5;
// === Types ===
export interface A11yIssue {
severity: "error" | "warning" | "info";
category: "contrast" | "focus" | "motion" | "touch-target" | "semantics" | "images";
element: string;
line?: number;
description: string;
wcagCriteria: string;
suggestion: string;
}
export interface A11yCheck {
category: string;
description: string;
passed: boolean;
}
export interface AccessibilityAuditResult {
filename: string;
issues: A11yIssue[];
warnings: A11yIssue[];
passed: A11yCheck[];
score: number;
}
interface AnalyzeOptions {
input: string;
level: "AA" | "AAA";
format: "json" | "summary";
pretty: boolean;
}
// === Color Utilities ===
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) {
// Try short hex
const short = /^#?([a-f\d])([a-f\d])([a-f\d])$/i.exec(hex);
if (short) {
return {
r: parseInt(short[1] + short[1], 16),
g: parseInt(short[2] + short[2], 16),
b: parseInt(short[3] + short[3], 16),
};
}
return null;
}
return {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
};
}
function getLuminance(r: number, g: number, b: number): number {
const [rs, gs, bs] = [r, g, b].map((c) => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function getContrastRatio(color1: string, color2: string): number | null {
const rgb1 = hexToRgb(color1);
const rgb2 = hexToRgb(color2);
if (!rgb1 || !rgb2) return null;
const l1 = getLuminance(rgb1.r, rgb1.g, rgb1.b);
const l2 = getLuminance(rgb2.r, rgb2.g, rgb2.b);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// === Analysis Patterns ===
interface AnalysisPattern {
pattern: RegExp;
check: (match: RegExpMatchArray, content: string, lineNum: number) => A11yIssue | null;
}
const HTML_PATTERNS: AnalysisPattern[] = [
// Missing alt text on images
{
pattern: /<img[^>]*(?!alt=)[^>]*>/gi,
check: (match, _content, lineNum) => {
if (!match[0].includes("alt=")) {
return {
severity: "error",
category: "images",
element: match[0].substring(0, 50) + "...",
line: lineNum,
description: "Image missing alt attribute",
wcagCriteria: "1.1.1 Non-text Content",
suggestion: 'Add alt="" for decorative images or descriptive alt text for informative images',
};
}
return null;
},
},
// Empty alt text should be intentional (for decorative images)
{
pattern: /<img[^>]*alt=""[^>]*>/gi,
check: (match, _content, lineNum) => {
// Check if it has role="presentation" or aria-hidden
if (!match[0].includes('role="presentation"') && !match[0].includes("aria-hidden")) {
return {
severity: "info",
category: "images",
element: match[0].substring(0, 50) + "...",
line: lineNum,
description: "Image has empty alt - ensure this is decorative",
wcagCriteria: "1.1.1 Non-text Content",
suggestion: 'For decorative images, also add role="presentation" or aria-hidden="true"',
};
}
return null;
},
},
// Links without accessible names
{
pattern: /<a[^>]*>[\s]*<\/a>/gi,
check: (match, _content, lineNum) => ({
severity: "error",
category: "semantics",
element: match[0],
line: lineNum,
description: "Empty link - no accessible name",
wcagCriteria: "2.4.4 Link Purpose",
suggestion: "Add text content or aria-label to the link",
}),
},
// Buttons without accessible names
{
pattern: /<button[^>]*>[\s]*<\/button>/gi,
check: (match, _content, lineNum) => ({
severity: "error",
category: "semantics",
element: match[0],
line: lineNum,
description: "Empty button - no accessible name",
wcagCriteria: "4.1.2 Name, Role, Value",
suggestion: "Add text content or aria-label to the button",
}),
},
// Form inputs without labels
{
pattern: /<input[^>]*type=["'](?:text|email|password|tel|url|search|number)[^>]*>/gi,
check: (match, content, lineNum) => {
const idMatch = match[0].match(/id=["']([^"']+)["']/);
if (idMatch) {
const labelPattern = new RegExp(`for=["']${idMatch[1]}["']`);
if (labelPattern.test(content)) {
return null;
}
}
if (match[0].includes("aria-label") || match[0].includes("aria-labelledby")) {
return null;
}
return {
severity: "error",
category: "semantics",
element: match[0].substring(0, 50) + "...",
line: lineNum,
description: "Form input missing associated label",
wcagCriteria: "1.3.1 Info and Relationships",
suggestion: "Add a <label> with matching for attribute, or use aria-label",
};
},
},
// Check for outline:none without alternative focus styles
{
pattern: /outline:\s*(?:none|0);?/gi,
check: (match, _content, lineNum) => ({
severity: "warning",
category: "focus",
element: "CSS rule",
line: lineNum,
description: "outline:none detected - ensure alternative focus indicator exists",
wcagCriteria: "2.4.7 Focus Visible",
suggestion: "Replace with visible focus styles (box-shadow, border, etc.)",
}),
},
// Check for :focus without styles
{
pattern: /:focus\s*\{[^}]*\}/gi,
check: (match, _content, lineNum) => {
// Check if focus rule has meaningful styles
const rule = match[0];
if (
rule.includes("outline") ||
rule.includes("box-shadow") ||
rule.includes("border") ||
rule.includes("background")
) {
return null; // Has focus styling
}
return {
severity: "info",
category: "focus",
element: ":focus rule",
line: lineNum,
description: "Focus rule may lack visible indicator",
wcagCriteria: "2.4.7 Focus Visible",
suggestion: "Ensure :focus includes visible changes (outline, shadow, border)",
};
},
},
// Check for animations without reduced motion support
{
pattern: /@keyframes\s+[\w-]+/gi,
check: (match, content, _lineNum) => {
if (!content.includes("prefers-reduced-motion")) {
return {
severity: "warning",
category: "motion",
element: match[0],
line: undefined,
description: "Animation detected without prefers-reduced-motion support",
wcagCriteria: "2.3.3 Animation from Interactions",
suggestion:
"Add @media (prefers-reduced-motion: reduce) to disable/reduce animations",
};
}
return null;
},
},
// Check for small touch targets
{
pattern: /(?:width|height|min-width|min-height):\s*(\d+)(px|rem)/gi,
check: (match, _content, lineNum) => {
const value = parseInt(match[1]);
const unit = match[2];
// Convert to px for comparison
let pxValue = value;
if (unit === "rem") {
pxValue = value * 16;
}
// Only flag if it looks like a button/link target
if (pxValue < 44) {
return {
severity: "info",
category: "touch-target",
element: `size: ${match[0]}`,
line: lineNum,
description: `Small dimension (${pxValue}px) - verify touch target is at least 44x44px`,
wcagCriteria: "2.5.5 Target Size",
suggestion: "Ensure interactive elements have at least 44x44px touch target",
};
}
return null;
},
},
// Check for text in SVGs without title/desc
{
pattern: /<svg[^>]*>[\s\S]*?<\/svg>/gi,
check: (match, _content, lineNum) => {
const svg = match[0];
if (svg.includes("<text") && !svg.includes("<title") && !svg.includes("aria-label")) {
return {
severity: "warning",
category: "images",
element: "SVG with text",
line: lineNum,
description: "SVG with text content but no accessible name",
wcagCriteria: "1.1.1 Non-text Content",
suggestion: "Add <title> element or aria-label to SVG",
};
}
return null;
},
},
// Check for autoplay on video/audio
{
pattern: /<(?:video|audio)[^>]*autoplay[^>]*>/gi,
check: (match, _content, lineNum) => {
if (!match[0].includes("muted")) {
return {
severity: "error",
category: "motion",
element: match[0].substring(0, 50) + "...",
line: lineNum,
description: "Autoplay media without muted attribute",
wcagCriteria: "1.4.2 Audio Control",
suggestion: 'Add muted attribute or provide pause controls',
};
}
return null;
},
},
];
// === Analysis Functions ===
function getLineNumber(content: string, index: number): number {
return content.substring(0, index).split("\n").length;
}
function analyzeContent(content: string, filename: string): A11yIssue[] {
const issues: A11yIssue[] = [];
for (const { pattern, check } of HTML_PATTERNS) {
// Reset regex
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(content)) !== null) {
const lineNum = getLineNumber(content, match.index);
const issue = check(match, content, lineNum);
if (issue) {
issues.push(issue);
}
}
}
return issues;
}
function analyzeColorContrast(
content: string,
level: "AA" | "AAA"
): A11yIssue[] {
const issues: A11yIssue[] = [];
// Extract color pairs from inline styles
const colorPairs: Array<{ fg: string; bg: string; context: string }> = [];
// Look for color and background-color in same rule
const ruleRegex = /\{([^}]+)\}/g;
let match;
while ((match = ruleRegex.exec(content)) !== null) {
const rule = match[1];
const colorMatch = rule.match(/(?:^|;)\s*color:\s*(#[a-f0-9]{3,6})/i);
const bgMatch = rule.match(/background(?:-color)?:\s*(#[a-f0-9]{3,6})/i);
if (colorMatch && bgMatch) {
colorPairs.push({
fg: colorMatch[1],
bg: bgMatch[1],
context: match[0].substring(0, 50),
});
}
}
// Check contrast for each pair
const minRatio = level === "AAA" ? CONTRAST_AAA_NORMAL : CONTRAST_AA_NORMAL;
for (const pair of colorPairs) {
const ratio = getContrastRatio(pair.fg, pair.bg);
if (ratio && ratio < minRatio) {
issues.push({
severity: "error",
category: "contrast",
element: pair.context,
description: `Insufficient contrast ratio: ${ratio.toFixed(2)}:1 (requires ${minRatio}:1 for ${level})`,
wcagCriteria: "1.4.3 Contrast (Minimum)",
suggestion: `Increase contrast between ${pair.fg} and ${pair.bg}`,
});
}
}
return issues;
}
function generatePassedChecks(issues: A11yIssue[]): A11yCheck[] {
const checks: A11yCheck[] = [];
const categories = ["contrast", "focus", "motion", "touch-target", "semantics", "images"];
for (const category of categories) {
const categoryIssues = issues.filter((i) => i.category === category);
const hasErrors = categoryIssues.some((i) => i.severity === "error");
if (!hasErrors) {
checks.push({
category,
description: `No ${category} errors detected`,
passed: true,
});
}
}
return checks;
}
function calculateScore(issues: A11yIssue[]): number {
// Start with 100, deduct points for issues
let score = 100;
for (const issue of issues) {
switch (issue.severity) {
case "error":
score -= 15;
break;
case "warning":
score -= 5;
break;
case "info":
score -= 1;
break;
}
}
return Math.max(0, Math.min(100, score));
}
// === Core Analysis Function ===
export async function analyzeAccessibility(
options: AnalyzeOptions
): Promise<AccessibilityAuditResult[]> {
const results: AccessibilityAuditResult[] = [];
const inputPath = resolve(options.input);
// Supported extensions
const exts = [".html", ".htm", ".tsx", ".jsx", ".vue", ".svelte", ".css"];
// Collect files
const files: string[] = [];
try {
const stat = await Deno.stat(inputPath);
if (stat.isDirectory) {
for await (const entry of walk(inputPath, {
exts: exts.map((e) => e.slice(1)),
includeDirs: false,
})) {
files.push(entry.path);
}
} else if (stat.isFile && exts.includes(extname(inputPath))) {
files.push(inputPath);
} else {
throw new Error(`Input must be a supported file or directory: ${inputPath}`);
}
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
throw new Error(`File or directory not found: ${inputPath}`);
}
throw error;
}
if (files.length === 0) {
throw new Error(`No supported files found in: ${inputPath}`);
}
// Analyze each file
for (const file of files) {
const content = await Deno.readTextFile(file);
const issues = [
...analyzeContent(content, basename(file)),
...analyzeColorContrast(content, options.level),
];
const errors = issues.filter((i) => i.severity === "error");
const warnings = issues.filter((i) => i.severity === "warning" || i.severity === "info");
const passed = generatePassedChecks(issues);
const score = calculateScore(issues);
results.push({
filename: basename(file),
issues: errors,
warnings,
passed,
score,
});
}
return results;
}
// === Output Formatting ===
function formatSummary(results: AccessibilityAuditResult[]): string {
const lines: string[] = [];
for (const result of results) {
lines.push(`\n${"=".repeat(60)}`);
lines.push(`FILE: ${result.filename}`);
lines.push(`SCORE: ${result.score}/100`);
lines.push("=".repeat(60));
if (result.issues.length > 0) {
lines.push("\nERRORS:");
for (const issue of result.issues) {
lines.push(` [${issue.category.toUpperCase()}] ${issue.description}`);
if (issue.line) lines.push(` Line: ${issue.line}`);
lines.push(` WCAG: ${issue.wcagCriteria}`);
lines.push(` Fix: ${issue.suggestion}`);
}
}
if (result.warnings.length > 0) {
lines.push("\nWARNINGS:");
for (const warning of result.warnings) {
lines.push(` [${warning.category.toUpperCase()}] ${warning.description}`);
if (warning.line) lines.push(` Line: ${warning.line}`);
lines.push(` Fix: ${warning.suggestion}`);
}
}
if (result.passed.length > 0) {
lines.push("\nPASSED:");
for (const check of result.passed) {
lines.push(` [OK] ${check.description}`);
}
}
}
// Summary
const totalErrors = results.reduce((sum, r) => sum + r.issues.length, 0);
const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
const avgScore = results.reduce((sum, r) => sum + r.score, 0) / results.length;
lines.push(`\n${"=".repeat(60)}`);
lines.push("SUMMARY");
lines.push("=".repeat(60));
lines.push(`Files analyzed: ${results.length}`);
lines.push(`Total errors: ${totalErrors}`);
lines.push(`Total warnings: ${totalWarnings}`);
lines.push(`Average score: ${avgScore.toFixed(1)}/100`);
return lines.join("\n");
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Design Accessibility Auditor
Checks HTML/component files for design-related accessibility issues
including color contrast, focus states, touch targets, and motion.
Usage:
deno run --allow-read scripts/${SCRIPT_NAME}.ts <input> [options]
Arguments:
<input> HTML/component file or directory
Options:
-h, --help Show this help message
-v, --version Show version
--level <AA|AAA> WCAG conformance level (default: AA)
--format <type> Output format: json (default), summary
--pretty Pretty-print JSON output
Supported Files:
.html, .htm, .tsx, .jsx, .vue, .svelte, .css
Checks Performed:
- Color contrast ratios
- Focus indicator presence
- Touch target sizes
- Motion and animation accessibility
- Image alt text
- Form label associations
- Semantic structure
Examples:
# Audit a component file
deno run --allow-read scripts/${SCRIPT_NAME}.ts Button.tsx
# Audit all components with AAA level
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src/components --level AAA
# Get a human-readable summary
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --format summary
`);
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "version", "pretty"],
string: ["level", "format"],
alias: { h: "help", v: "version" },
default: { level: "AA", format: "json", pretty: false },
});
if (parsed.help) {
printHelp();
Deno.exit(0);
}
if (parsed.version) {
console.log(`${SCRIPT_NAME} v${VERSION}`);
Deno.exit(0);
}
const input = parsed._[0] as string;
if (!input) {
console.error("Error: Input file or directory required");
console.error("Use --help for usage information");
Deno.exit(1);
}
if (!["AA", "AAA"].includes(parsed.level)) {
console.error(`Error: Invalid level '${parsed.level}'. Use AA or AAA.`);
Deno.exit(1);
}
const options: AnalyzeOptions = {
input,
level: parsed.level as "AA" | "AAA",
format: parsed.format as "json" | "summary",
pretty: parsed.pretty,
};
try {
const results = await analyzeAccessibility(options);
if (options.format === "summary") {
console.log(formatSummary(results));
} else {
const output = options.pretty
? JSON.stringify(results, null, 2)
: JSON.stringify(results);
console.log(output);
}
// Exit with error code if there are issues
const hasErrors = results.some((r) => r.issues.length > 0);
if (hasErrors) {
Deno.exit(1);
}
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read
/**
* analyze-styles.ts - CSS Style Auditor
*
* Analyzes CSS files to extract color, typography, spacing patterns
* and identify design inconsistencies.
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { basename, extname, resolve } from "jsr:@std/path@1.0.8";
import { walk } from "jsr:@std/fs@1.0.8/walk";
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "analyze-styles";
// === Types ===
export interface ColorUsage {
value: string;
normalizedHex: string;
count: number;
locations: string[];
suggestedTokenName?: string;
}
export interface TypographyUsage {
property: "font-family" | "font-size" | "font-weight" | "line-height";
value: string;
count: number;
locations: string[];
}
export interface SpacingUsage {
property: string;
value: string;
count: number;
locations: string[];
}
export interface StyleInconsistency {
type: "color" | "font" | "spacing" | "z-index";
description: string;
values: string[];
suggestion: string;
}
export interface StyleAuditResult {
filename: string;
summary: {
totalSelectors: number;
totalDeclarations: number;
uniqueColors: number;
uniqueFonts: number;
uniqueSpacingValues: number;
};
colors: ColorUsage[];
typography: TypographyUsage[];
spacing: SpacingUsage[];
inconsistencies: StyleInconsistency[];
recommendations: string[];
}
interface AnalyzeOptions {
input: string;
tokensFile?: string;
pretty: boolean;
format: "json" | "summary";
}
// === Color Utilities ===
const COLOR_REGEX =
/#(?:[0-9a-fA-F]{3}){1,2}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)|(?:transparent|currentColor|inherit)\b/gi;
const NAMED_COLORS: Record<string, string> = {
black: "#000000",
white: "#ffffff",
red: "#ff0000",
green: "#008000",
blue: "#0000ff",
yellow: "#ffff00",
cyan: "#00ffff",
magenta: "#ff00ff",
gray: "#808080",
grey: "#808080",
};
function normalizeColor(color: string): string {
const lower = color.toLowerCase().trim();
// Handle named colors
if (NAMED_COLORS[lower]) {
return NAMED_COLORS[lower];
}
// Handle hex shorthand
if (/^#[0-9a-f]{3}$/i.test(lower)) {
const r = lower[1];
const g = lower[2];
const b = lower[3];
return `#${r}${r}${g}${g}${b}${b}`;
}
// Handle rgb/rgba
const rgbMatch = lower.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (rgbMatch) {
const r = parseInt(rgbMatch[1]).toString(16).padStart(2, "0");
const g = parseInt(rgbMatch[2]).toString(16).padStart(2, "0");
const b = parseInt(rgbMatch[3]).toString(16).padStart(2, "0");
return `#${r}${g}${b}`;
}
// Already normalized hex or other
return lower;
}
function suggestTokenName(hex: string, index: number): string {
// Simple heuristic based on luminance
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
if (luminance > 0.9) return `color-light-${index}`;
if (luminance < 0.1) return `color-dark-${index}`;
if (r > g && r > b) return `color-red-${index}`;
if (g > r && g > b) return `color-green-${index}`;
if (b > r && b > g) return `color-blue-${index}`;
return `color-neutral-${index}`;
}
// === CSS Parsing ===
interface Declaration {
property: string;
value: string;
selector: string;
}
function parseCSS(css: string): Declaration[] {
const declarations: Declaration[] = [];
let currentSelector = "";
// Remove comments
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, "");
// Simple regex-based parser (handles most common cases)
const ruleRegex = /([^{]+)\{([^}]+)\}/g;
let match;
while ((match = ruleRegex.exec(cleaned)) !== null) {
currentSelector = match[1].trim();
const body = match[2];
// Parse declarations
const declRegex = /([a-z-]+)\s*:\s*([^;]+);?/gi;
let declMatch;
while ((declMatch = declRegex.exec(body)) !== null) {
declarations.push({
property: declMatch[1].toLowerCase().trim(),
value: declMatch[2].trim(),
selector: currentSelector,
});
}
}
return declarations;
}
// === Analysis Functions ===
function analyzeColors(declarations: Declaration[]): ColorUsage[] {
const colorMap = new Map<string, ColorUsage>();
const colorProperties = [
"color",
"background-color",
"background",
"border-color",
"border",
"border-top-color",
"border-right-color",
"border-bottom-color",
"border-left-color",
"outline-color",
"box-shadow",
"text-shadow",
"fill",
"stroke",
];
for (const decl of declarations) {
if (colorProperties.some((p) => decl.property.includes(p))) {
const colors = decl.value.match(COLOR_REGEX) || [];
for (const color of colors) {
if (color === "transparent" || color === "currentColor" || color === "inherit") {
continue;
}
const normalized = normalizeColor(color);
const location = `${decl.selector}:${decl.property}`;
if (colorMap.has(normalized)) {
const existing = colorMap.get(normalized)!;
existing.count++;
if (!existing.locations.includes(location)) {
existing.locations.push(location);
}
} else {
colorMap.set(normalized, {
value: color,
normalizedHex: normalized,
count: 1,
locations: [location],
});
}
}
}
}
// Add suggested token names
const colors = Array.from(colorMap.values()).sort((a, b) => b.count - a.count);
colors.forEach((c, i) => {
c.suggestedTokenName = suggestTokenName(c.normalizedHex, i + 1);
});
return colors;
}
function analyzeTypography(declarations: Declaration[]): TypographyUsage[] {
const typoMap = new Map<string, TypographyUsage>();
const typoProperties = ["font-family", "font-size", "font-weight", "line-height"];
for (const decl of declarations) {
if (typoProperties.includes(decl.property)) {
const key = `${decl.property}:${decl.value}`;
const location = `${decl.selector}:${decl.property}`;
if (typoMap.has(key)) {
const existing = typoMap.get(key)!;
existing.count++;
if (!existing.locations.includes(location)) {
existing.locations.push(location);
}
} else {
typoMap.set(key, {
property: decl.property as TypographyUsage["property"],
value: decl.value,
count: 1,
locations: [location],
});
}
}
}
return Array.from(typoMap.values()).sort((a, b) => b.count - a.count);
}
function analyzeSpacing(declarations: Declaration[]): SpacingUsage[] {
const spacingMap = new Map<string, SpacingUsage>();
const spacingProperties = [
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"gap",
"row-gap",
"column-gap",
];
for (const decl of declarations) {
if (spacingProperties.includes(decl.property)) {
const key = `${decl.property}:${decl.value}`;
const location = `${decl.selector}:${decl.property}`;
if (spacingMap.has(key)) {
const existing = spacingMap.get(key)!;
existing.count++;
if (!existing.locations.includes(location)) {
existing.locations.push(location);
}
} else {
spacingMap.set(key, {
property: decl.property,
value: decl.value,
count: 1,
locations: [location],
});
}
}
}
return Array.from(spacingMap.values()).sort((a, b) => b.count - a.count);
}
function findInconsistencies(
colors: ColorUsage[],
typography: TypographyUsage[],
spacing: SpacingUsage[]
): StyleInconsistency[] {
const inconsistencies: StyleInconsistency[] = [];
// Check for similar but different colors
const hexColors = colors.map((c) => c.normalizedHex);
for (let i = 0; i < hexColors.length; i++) {
for (let j = i + 1; j < hexColors.length; j++) {
const c1 = hexColors[i];
const c2 = hexColors[j];
if (c1 && c2 && areColorsSimilar(c1, c2)) {
inconsistencies.push({
type: "color",
description: `Similar colors found that could be consolidated`,
values: [c1, c2],
suggestion: `Consider using a single color token for these similar values`,
});
}
}
}
// Check for too many unique font families
const fontFamilies = typography.filter((t) => t.property === "font-family");
if (fontFamilies.length > 3) {
inconsistencies.push({
type: "font",
description: `Too many font families (${fontFamilies.length}) - consider limiting to 2-3`,
values: fontFamilies.map((f) => f.value),
suggestion: "Use a display font, body font, and optionally a monospace font",
});
}
// Check for inconsistent spacing values
const spacingValues = [...new Set(spacing.map((s) => s.value))];
if (spacingValues.length > 10) {
inconsistencies.push({
type: "spacing",
description: `Many unique spacing values (${spacingValues.length}) - consider a consistent scale`,
values: spacingValues.slice(0, 10),
suggestion: "Use a spacing scale (4px, 8px, 12px, 16px, 24px, 32px, etc.)",
});
}
return inconsistencies;
}
function areColorsSimilar(hex1: string, hex2: string): boolean {
if (!hex1.startsWith("#") || !hex2.startsWith("#")) return false;
const r1 = parseInt(hex1.slice(1, 3), 16);
const g1 = parseInt(hex1.slice(3, 5), 16);
const b1 = parseInt(hex1.slice(5, 7), 16);
const r2 = parseInt(hex2.slice(1, 3), 16);
const g2 = parseInt(hex2.slice(3, 5), 16);
const b2 = parseInt(hex2.slice(5, 7), 16);
const distance = Math.sqrt(
Math.pow(r1 - r2, 2) + Math.pow(g1 - g2, 2) + Math.pow(b1 - b2, 2)
);
// Colors within distance of 30 are considered similar
return distance > 0 && distance < 30;
}
function generateRecommendations(
result: Omit<StyleAuditResult, "recommendations">
): string[] {
const recommendations: string[] = [];
// Color recommendations
if (result.colors.length > 15) {
recommendations.push(
`Consider reducing color palette from ${result.colors.length} to 10-15 colors with a design token system`
);
}
// Typography recommendations
const genericFonts = result.typography.filter(
(t) =>
t.property === "font-family" &&
/arial|helvetica|roboto|inter|system-ui/i.test(t.value)
);
if (genericFonts.length > 0) {
recommendations.push(
"Replace generic fonts (Arial, Helvetica, Roboto, Inter) with distinctive alternatives"
);
}
// Spacing recommendations
const pixelSpacing = result.spacing.filter((s) => /^\d+px$/.test(s.value));
if (pixelSpacing.length > 5) {
recommendations.push(
"Convert pixel-based spacing to rem units for better accessibility"
);
}
// Inconsistency-based recommendations
if (result.inconsistencies.length > 0) {
recommendations.push(
`Address ${result.inconsistencies.length} design inconsistencies identified in the audit`
);
}
return recommendations;
}
// === Core Analysis Function ===
export async function analyzeStyles(options: AnalyzeOptions): Promise<StyleAuditResult[]> {
const results: StyleAuditResult[] = [];
const inputPath = resolve(options.input);
// Collect CSS files
const cssFiles: string[] = [];
try {
const stat = await Deno.stat(inputPath);
if (stat.isDirectory) {
for await (const entry of walk(inputPath, {
exts: [".css"],
includeDirs: false,
})) {
cssFiles.push(entry.path);
}
} else if (stat.isFile && extname(inputPath) === ".css") {
cssFiles.push(inputPath);
} else {
throw new Error(`Input must be a CSS file or directory: ${inputPath}`);
}
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
throw new Error(`File or directory not found: ${inputPath}`);
}
throw error;
}
if (cssFiles.length === 0) {
throw new Error(`No CSS files found in: ${inputPath}`);
}
// Analyze each file
for (const file of cssFiles) {
const css = await Deno.readTextFile(file);
const declarations = parseCSS(css);
const colors = analyzeColors(declarations);
const typography = analyzeTypography(declarations);
const spacing = analyzeSpacing(declarations);
const inconsistencies = findInconsistencies(colors, typography, spacing);
const uniqueSelectors = new Set(declarations.map((d) => d.selector));
const partialResult = {
filename: basename(file),
summary: {
totalSelectors: uniqueSelectors.size,
totalDeclarations: declarations.length,
uniqueColors: colors.length,
uniqueFonts: typography.filter((t) => t.property === "font-family").length,
uniqueSpacingValues: spacing.length,
},
colors,
typography,
spacing,
inconsistencies,
};
results.push({
...partialResult,
recommendations: generateRecommendations(partialResult),
});
}
return results;
}
// === Output Formatting ===
function formatSummary(results: StyleAuditResult[]): string {
const lines: string[] = [];
for (const result of results) {
lines.push(`\n${"=".repeat(60)}`);
lines.push(`FILE: ${result.filename}`);
lines.push("=".repeat(60));
lines.push("\nSUMMARY:");
lines.push(` Selectors: ${result.summary.totalSelectors}`);
lines.push(` Declarations: ${result.summary.totalDeclarations}`);
lines.push(` Unique Colors: ${result.summary.uniqueColors}`);
lines.push(` Unique Fonts: ${result.summary.uniqueFonts}`);
lines.push(` Unique Spacing: ${result.summary.uniqueSpacingValues}`);
if (result.colors.length > 0) {
lines.push("\nTOP COLORS:");
result.colors.slice(0, 5).forEach((c) => {
lines.push(` ${c.normalizedHex} (${c.count}x) → ${c.suggestedTokenName}`);
});
}
if (result.typography.length > 0) {
lines.push("\nTYPOGRAPHY:");
result.typography
.filter((t) => t.property === "font-family")
.slice(0, 3)
.forEach((t) => {
lines.push(` ${t.value} (${t.count}x)`);
});
}
if (result.inconsistencies.length > 0) {
lines.push("\nINCONSISTENCIES:");
result.inconsistencies.forEach((i) => {
lines.push(` [${i.type.toUpperCase()}] ${i.description}`);
});
}
if (result.recommendations.length > 0) {
lines.push("\nRECOMMENDATIONS:");
result.recommendations.forEach((r) => {
lines.push(` • ${r}`);
});
}
}
return lines.join("\n");
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - CSS Style Auditor
Analyzes CSS files to extract color, typography, spacing patterns
and identify design inconsistencies.
Usage:
deno run --allow-read scripts/${SCRIPT_NAME}.ts <input> [options]
Arguments:
<input> CSS file or directory containing CSS files
Options:
-h, --help Show this help message
-v, --version Show version
--tokens <file> Compare against existing design tokens file
--pretty Pretty-print JSON output
--format <type> Output format: json (default), summary
Examples:
# Analyze a single CSS file
deno run --allow-read scripts/${SCRIPT_NAME}.ts styles.css
# Analyze all CSS in a directory with pretty output
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --pretty
# Get a human-readable summary
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --format summary
# Compare against existing tokens
deno run --allow-read scripts/${SCRIPT_NAME}.ts styles.css --tokens tokens.json
`);
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "version", "pretty"],
string: ["tokens", "format"],
alias: { h: "help", v: "version" },
default: { format: "json", pretty: false },
});
if (parsed.help) {
printHelp();
Deno.exit(0);
}
if (parsed.version) {
console.log(`${SCRIPT_NAME} v${VERSION}`);
Deno.exit(0);
}
const input = parsed._[0] as string;
if (!input) {
console.error("Error: Input file or directory required");
console.error("Use --help for usage information");
Deno.exit(1);
}
const options: AnalyzeOptions = {
input,
tokensFile: parsed.tokens,
pretty: parsed.pretty,
format: parsed.format as "json" | "summary",
};
try {
const results = await analyzeStyles(options);
if (options.format === "summary") {
console.log(formatSummary(results));
} else {
const output = options.pretty
? JSON.stringify(results, null, 2)
: JSON.stringify(results);
console.log(output);
}
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read
/**
* extract-tokens.ts - Design Token Extractor
*
* Extracts design tokens from CSS files containing CSS custom properties
* and outputs in various standardized formats.
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { basename, extname, resolve } from "jsr:@std/path@1.0.8";
import { walk } from "jsr:@std/fs@1.0.8/walk";
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "extract-tokens";
type OutputFormat = "css" | "scss" | "tailwind" | "style-dictionary" | "tokens-studio" | "json";
// === Types ===
export interface TokenValue {
value: string;
type: string;
description?: string;
usageCount?: number;
}
export interface ExtractedTokens {
source: string;
format: OutputFormat;
tokens: {
color: Record<string, TokenValue>;
typography: Record<string, TokenValue>;
spacing: Record<string, TokenValue>;
shadow: Record<string, TokenValue>;
border: Record<string, TokenValue>;
animation: Record<string, TokenValue>;
other: Record<string, TokenValue>;
};
unmapped: Array<{
property: string;
value: string;
count: number;
suggestion: string;
}>;
}
interface ExtractOptions {
input: string;
format: OutputFormat;
outputCss: boolean;
pretty: boolean;
}
// === Token Classification ===
const TOKEN_PATTERNS: Record<string, RegExp[]> = {
color: [
/color/i,
/background/i,
/border-color/i,
/fill/i,
/stroke/i,
/shadow/i,
/bg/i,
/fg/i,
/text(?!-)/i,
],
typography: [
/font/i,
/text-/i,
/line-height/i,
/letter-spacing/i,
/word-spacing/i,
],
spacing: [
/space/i,
/gap/i,
/margin/i,
/padding/i,
/inset/i,
],
shadow: [
/shadow/i,
/elevation/i,
],
border: [
/border/i,
/radius/i,
/outline/i,
],
animation: [
/duration/i,
/timing/i,
/easing/i,
/delay/i,
/transition/i,
/animation/i,
],
};
function classifyToken(name: string, value: string): string {
// Check name patterns
for (const [category, patterns] of Object.entries(TOKEN_PATTERNS)) {
if (patterns.some((p) => p.test(name))) {
return category;
}
}
// Check value patterns
if (/^#|^rgb|^hsl|^oklch|transparent|currentColor/i.test(value)) {
return "color";
}
if (/^\d+(\.\d+)?(px|rem|em|%)$/.test(value)) {
if (/radius/i.test(name)) return "border";
return "spacing";
}
if (/^\d+(\.\d+)?(ms|s)$/.test(value)) {
return "animation";
}
if (/^(sans-serif|serif|monospace|cursive|fantasy|system-ui)/i.test(value)) {
return "typography";
}
return "other";
}
function inferTokenType(category: string, value: string): string {
if (category === "color") return "color";
if (category === "typography") {
if (/^\d/.test(value)) return "dimension";
return "fontFamily";
}
if (category === "spacing") return "dimension";
if (category === "shadow") return "shadow";
if (category === "border") return "dimension";
if (category === "animation") return "duration";
return "other";
}
// === CSS Parsing ===
interface CSSVariable {
name: string;
value: string;
count: number;
}
function extractCSSVariables(css: string): CSSVariable[] {
const variables = new Map<string, CSSVariable>();
// Remove comments
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, "");
// Find variable definitions (--name: value)
const varDefRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
let match;
while ((match = varDefRegex.exec(cleaned)) !== null) {
const name = match[1].trim();
const value = match[2].trim();
if (variables.has(name)) {
variables.get(name)!.count++;
} else {
variables.set(name, { name, value, count: 1 });
}
}
return Array.from(variables.values());
}
function extractRepeatedValues(css: string): Map<string, number> {
const valueCount = new Map<string, number>();
// Remove comments
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, "");
// Find all property values
const declRegex = /[a-z-]+\s*:\s*([^;{}]+);/gi;
let match;
while ((match = declRegex.exec(cleaned)) !== null) {
const value = match[1].trim();
// Skip var() references and very short values
if (!value.startsWith("var(") && value.length > 2) {
valueCount.set(value, (valueCount.get(value) || 0) + 1);
}
}
return valueCount;
}
// === Token Extraction ===
export async function extractTokens(options: ExtractOptions): Promise<ExtractedTokens> {
const inputPath = resolve(options.input);
// Collect CSS files
const cssContents: string[] = [];
let sourceName = "";
try {
const stat = await Deno.stat(inputPath);
if (stat.isDirectory) {
sourceName = basename(inputPath);
for await (const entry of walk(inputPath, {
exts: [".css"],
includeDirs: false,
})) {
cssContents.push(await Deno.readTextFile(entry.path));
}
} else if (stat.isFile && extname(inputPath) === ".css") {
sourceName = basename(inputPath);
cssContents.push(await Deno.readTextFile(inputPath));
} else {
throw new Error(`Input must be a CSS file or directory: ${inputPath}`);
}
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
throw new Error(`File or directory not found: ${inputPath}`);
}
throw error;
}
if (cssContents.length === 0) {
throw new Error(`No CSS files found in: ${inputPath}`);
}
const combinedCSS = cssContents.join("\n");
// Extract CSS variables
const cssVariables = extractCSSVariables(combinedCSS);
// Extract repeated values that could become tokens
const repeatedValues = extractRepeatedValues(combinedCSS);
// Classify tokens
const tokens: ExtractedTokens["tokens"] = {
color: {},
typography: {},
spacing: {},
shadow: {},
border: {},
animation: {},
other: {},
};
for (const variable of cssVariables) {
const category = classifyToken(variable.name, variable.value);
const tokenName = variable.name.replace(/^--/, "");
tokens[category as keyof typeof tokens][tokenName] = {
value: variable.value,
type: inferTokenType(category, variable.value),
usageCount: variable.count,
};
}
// Find unmapped repeated values
const unmapped: ExtractedTokens["unmapped"] = [];
for (const [value, count] of repeatedValues) {
if (count >= 3) {
// Value used 3+ times
const category = classifyToken("", value);
const suggestion = `--${category}-${unmapped.length + 1}`;
// Check if this value is already a token
const isTokenized = Object.values(tokens).some((cat) =>
Object.values(cat).some((t) => t.value === value)
);
if (!isTokenized) {
unmapped.push({
property: category,
value,
count,
suggestion,
});
}
}
}
return {
source: sourceName,
format: options.format,
tokens,
unmapped: unmapped.sort((a, b) => b.count - a.count).slice(0, 20),
};
}
// === Output Formatting ===
function formatAsCSS(tokens: ExtractedTokens): string {
const lines: string[] = ["/* Design Tokens - Generated by extract-tokens */", "", ":root {"];
for (const [category, categoryTokens] of Object.entries(tokens.tokens)) {
const entries = Object.entries(categoryTokens);
if (entries.length === 0) continue;
lines.push(` /* ${category} */`);
for (const [name, token] of entries) {
lines.push(` --${name}: ${token.value};`);
}
lines.push("");
}
lines.push("}");
return lines.join("\n");
}
function formatAsSCSS(tokens: ExtractedTokens): string {
const lines: string[] = ["// Design Tokens - Generated by extract-tokens", ""];
for (const [category, categoryTokens] of Object.entries(tokens.tokens)) {
const entries = Object.entries(categoryTokens);
if (entries.length === 0) continue;
lines.push(`// ${category}`);
for (const [name, token] of entries) {
const scssName = name.replace(/-/g, "_");
lines.push(`$${scssName}: ${token.value};`);
}
lines.push("");
}
return lines.join("\n");
}
function formatAsTailwind(tokens: ExtractedTokens): string {
const config: Record<string, Record<string, string>> = {
colors: {},
spacing: {},
fontFamily: {},
fontSize: {},
boxShadow: {},
borderRadius: {},
transitionDuration: {},
};
for (const [name, token] of Object.entries(tokens.tokens.color)) {
config.colors[name] = token.value;
}
for (const [name, token] of Object.entries(tokens.tokens.spacing)) {
config.spacing[name] = token.value;
}
for (const [name, token] of Object.entries(tokens.tokens.typography)) {
if (token.type === "fontFamily") {
config.fontFamily[name] = token.value;
} else {
config.fontSize[name] = token.value;
}
}
for (const [name, token] of Object.entries(tokens.tokens.shadow)) {
config.boxShadow[name] = token.value;
}
for (const [name, token] of Object.entries(tokens.tokens.border)) {
config.borderRadius[name] = token.value;
}
for (const [name, token] of Object.entries(tokens.tokens.animation)) {
config.transitionDuration[name] = token.value;
}
// Remove empty categories
for (const key of Object.keys(config)) {
if (Object.keys(config[key]).length === 0) {
delete config[key];
}
}
return `// Tailwind Config Extension - Generated by extract-tokens
// Add this to your tailwind.config.js theme.extend
module.exports = {
theme: {
extend: ${JSON.stringify(config, null, 6).replace(/"([^"]+)":/g, "$1:")}
}
};`;
}
function formatAsStyleDictionary(tokens: ExtractedTokens): string {
const sdTokens: Record<string, unknown> = {};
for (const [category, categoryTokens] of Object.entries(tokens.tokens)) {
if (Object.keys(categoryTokens).length === 0) continue;
sdTokens[category] = {};
for (const [name, token] of Object.entries(categoryTokens)) {
(sdTokens[category] as Record<string, unknown>)[name] = {
value: token.value,
type: token.type,
};
}
}
return JSON.stringify(sdTokens, null, 2);
}
function formatAsTokensStudio(tokens: ExtractedTokens): string {
const tsTokens: Record<string, unknown> = {};
for (const [category, categoryTokens] of Object.entries(tokens.tokens)) {
if (Object.keys(categoryTokens).length === 0) continue;
tsTokens[category] = {};
for (const [name, token] of Object.entries(categoryTokens)) {
(tsTokens[category] as Record<string, unknown>)[name] = {
value: token.value,
type: token.type,
$extensions: {
"studio.tokens": {
modify: {},
},
},
};
}
}
return JSON.stringify(tsTokens, null, 2);
}
function formatOutput(tokens: ExtractedTokens, options: ExtractOptions): string {
switch (options.format) {
case "css":
return formatAsCSS(tokens);
case "scss":
return formatAsSCSS(tokens);
case "tailwind":
return formatAsTailwind(tokens);
case "style-dictionary":
return formatAsStyleDictionary(tokens);
case "tokens-studio":
return formatAsTokensStudio(tokens);
case "json":
default:
return options.pretty
? JSON.stringify(tokens, null, 2)
: JSON.stringify(tokens);
}
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Design Token Extractor
Extracts design tokens from CSS files containing CSS custom properties
and outputs in various standardized formats.
Usage:
deno run --allow-read scripts/${SCRIPT_NAME}.ts <input> [options]
Arguments:
<input> CSS file or directory containing CSS files
Options:
-h, --help Show this help message
-v, --version Show version
--format <type> Output format: css, scss, tailwind, style-dictionary,
tokens-studio, json (default: json)
--output-css Also output CSS variables (with json format)
--pretty Pretty-print JSON output
Formats:
css CSS custom properties (:root { --token: value; })
scss SCSS variables ($token: value;)
tailwind Tailwind config extension object
style-dictionary Style Dictionary JSON format
tokens-studio Tokens Studio JSON format
json Raw JSON with all metadata
Examples:
# Extract tokens as CSS variables
deno run --allow-read scripts/${SCRIPT_NAME}.ts styles.css --format css
# Extract to Tailwind config
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --format tailwind
# Extract to Style Dictionary format
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --format style-dictionary
# Get raw JSON with unmapped values
deno run --allow-read scripts/${SCRIPT_NAME}.ts styles.css --pretty
`);
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "version", "pretty", "output-css"],
string: ["format"],
alias: { h: "help", v: "version" },
default: { format: "json", pretty: false },
});
if (parsed.help) {
printHelp();
Deno.exit(0);
}
if (parsed.version) {
console.log(`${SCRIPT_NAME} v${VERSION}`);
Deno.exit(0);
}
const input = parsed._[0] as string;
if (!input) {
console.error("Error: Input file or directory required");
console.error("Use --help for usage information");
Deno.exit(1);
}
const validFormats: OutputFormat[] = [
"css",
"scss",
"tailwind",
"style-dictionary",
"tokens-studio",
"json",
];
if (!validFormats.includes(parsed.format as OutputFormat)) {
console.error(`Error: Invalid format '${parsed.format}'`);
console.error(`Valid formats: ${validFormats.join(", ")}`);
Deno.exit(1);
}
const options: ExtractOptions = {
input,
format: parsed.format as OutputFormat,
outputCss: parsed["output-css"],
pretty: parsed.pretty,
};
try {
const tokens = await extractTokens(options);
console.log(formatOutput(tokens, options));
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read --allow-write
/**
* generate-component.ts - Component Template Generator
*
* Generates component templates with design-focused styling
* for multiple frameworks (React, Vue, Svelte, HTML).
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { resolve, join } from "jsr:@std/path@1.0.8";
import { ensureDir } from "jsr:@std/fs@1.0.8/ensure-dir";
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "generate-component";
type Framework = "react" | "vue" | "svelte" | "html";
type Styling = "css" | "tailwind" | "css-modules" | "styled-components" | "emotion";
type Aesthetic = "minimal" | "bold" | "organic" | "brutalist" | "glassmorphism" | "neumorphism";
type Animation = "none" | "subtle" | "expressive";
type ComponentType = "button" | "card" | "input" | "modal" | "navigation" | "hero" | "custom";
// === Types ===
export interface ComponentSpec {
name: string;
type: ComponentType;
framework: Framework;
styling: Styling;
aesthetic?: {
style?: Aesthetic;
animation?: Animation;
darkMode?: boolean;
};
tokens?: string;
props?: Record<string, { type: string; default?: string; required?: boolean }>;
slots?: string[];
variants?: string[];
}
interface GenerateOptions {
specFile?: string;
name: string;
type: ComponentType;
framework: Framework;
styling: Styling;
aesthetic?: Aesthetic;
animation: Animation;
darkMode: boolean;
tokens?: string;
outputDir: string;
}
// === Style Templates ===
const AESTHETIC_STYLES: Record<Aesthetic, Record<string, string>> = {
minimal: {
base: "border border-gray-200 bg-white text-gray-900",
hover: "hover:border-gray-300",
focus: "focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2",
active: "active:bg-gray-50",
transition: "transition-colors duration-150",
},
bold: {
base: "bg-gradient-to-r from-purple-600 to-pink-600 text-white font-bold",
hover: "hover:from-purple-700 hover:to-pink-700 hover:scale-105",
focus: "focus:outline-none focus:ring-4 focus:ring-purple-300",
active: "active:scale-95",
transition: "transition-all duration-200 transform",
},
organic: {
base: "bg-amber-50 text-amber-900 border-2 border-amber-200 rounded-3xl",
hover: "hover:bg-amber-100 hover:border-amber-300",
focus: "focus:outline-none focus:ring-2 focus:ring-amber-400",
active: "active:bg-amber-200",
transition: "transition-all duration-300 ease-out",
},
brutalist: {
base: "bg-black text-white border-4 border-black font-mono uppercase tracking-wider",
hover: "hover:bg-white hover:text-black",
focus: "focus:outline-none focus:ring-0 focus:border-yellow-400",
active: "active:translate-x-1 active:translate-y-1",
transition: "transition-all duration-100",
},
glassmorphism: {
base: "bg-white/20 backdrop-blur-lg border border-white/30 text-white shadow-xl",
hover: "hover:bg-white/30",
focus: "focus:outline-none focus:ring-2 focus:ring-white/50",
active: "active:bg-white/40",
transition: "transition-all duration-200",
},
neumorphism: {
base: "bg-gray-100 text-gray-700 rounded-xl shadow-[8px_8px_16px_#d1d1d1,-8px_-8px_16px_#ffffff]",
hover: "hover:shadow-[4px_4px_8px_#d1d1d1,-4px_-4px_8px_#ffffff]",
focus: "focus:outline-none focus:shadow-[inset_4px_4px_8px_#d1d1d1,inset_-4px_-4px_8px_#ffffff]",
active: "active:shadow-[inset_4px_4px_8px_#d1d1d1,inset_-4px_-4px_8px_#ffffff]",
transition: "transition-shadow duration-200",
},
};
const ANIMATION_STYLES: Record<Animation, string> = {
none: "",
subtle: "transition-all duration-200 ease-out",
expressive: "transition-all duration-300 ease-out hover:scale-105 active:scale-95",
};
// === Component Templates ===
function getButtonTemplate(options: GenerateOptions): { styles: string; component: string } {
const aesthetic = options.aesthetic || "minimal";
const styles = AESTHETIC_STYLES[aesthetic];
const animation = ANIMATION_STYLES[options.animation];
const cssClasses = [
"px-4 py-2 rounded-lg font-medium",
styles.base,
styles.hover,
styles.focus,
styles.active,
animation || styles.transition,
].join(" ");
if (options.styling === "tailwind") {
return {
styles: "",
component: cssClasses,
};
}
// CSS version
const cssStyles = `
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
${aesthetic === "minimal" ? `
border: 1px solid #e5e7eb;
background: white;
color: #111827;
` : ""}
${aesthetic === "bold" ? `
background: linear-gradient(to right, #9333ea, #ec4899);
color: white;
font-weight: 700;
border: none;
` : ""}
transition: all 0.2s ease-out;
}
.btn:hover {
transform: translateY(-1px);
}
.btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
}
.btn:active {
transform: translateY(0);
}
${options.darkMode ? `
.dark .btn {
background: #1f2937;
color: white;
border-color: #374151;
}
` : ""}`;
return { styles: cssStyles, component: "btn" };
}
function getCardTemplate(options: GenerateOptions): { styles: string; component: string } {
const aesthetic = options.aesthetic || "minimal";
if (options.styling === "tailwind") {
const classes: Record<Aesthetic, string> = {
minimal: "bg-white border border-gray-200 rounded-lg shadow-sm p-6",
bold: "bg-gradient-to-br from-indigo-500 to-purple-600 text-white rounded-2xl p-8 shadow-2xl",
organic: "bg-amber-50 border-2 border-amber-200 rounded-3xl p-8",
brutalist: "bg-white border-4 border-black p-6",
glassmorphism: "bg-white/20 backdrop-blur-lg border border-white/30 rounded-2xl p-6 shadow-xl",
neumorphism: "bg-gray-100 rounded-2xl p-6 shadow-[8px_8px_16px_#d1d1d1,-8px_-8px_16px_#ffffff]",
};
return { styles: "", component: classes[aesthetic] };
}
const cssStyles = `
.card {
padding: 1.5rem;
border-radius: 0.5rem;
background: white;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.card-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.card-body {
color: #374151;
}
.card-footer {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
}
${options.darkMode ? `
.dark .card {
background: #1f2937;
border-color: #374151;
color: white;
}
` : ""}`;
return { styles: cssStyles, component: "card" };
}
function getInputTemplate(options: GenerateOptions): { styles: string; component: string } {
if (options.styling === "tailwind") {
return {
styles: "",
component: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent",
};
}
const cssStyles = `
.input-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.input-label {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
}
.input {
width: 100%;
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.input-error {
border-color: #ef4444;
}
.input-error-message {
font-size: 0.875rem;
color: #ef4444;
}
${options.darkMode ? `
.dark .input {
background: #1f2937;
border-color: #4b5563;
color: white;
}
` : ""}`;
return { styles: cssStyles, component: "input" };
}
// === Framework Generators ===
function generateReact(options: GenerateOptions, template: { styles: string; component: string }): string {
const { name } = options;
const propsInterface = `export interface ${name}Props {
children?: React.ReactNode;
className?: string;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
}`;
if (options.styling === "tailwind") {
return `import React from 'react';
${propsInterface}
const variants = {
primary: '${template.component}',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'bg-transparent hover:bg-gray-100',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
export function ${name}({
children,
className = '',
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ${name}Props) {
return (
<button
className={\`\${variants[variant]} \${sizes[size]} \${className} \${disabled ? 'opacity-50 cursor-not-allowed' : ''}\`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
export default ${name};
`;
}
if (options.styling === "css-modules") {
return `import React from 'react';
import styles from './${name}.module.css';
${propsInterface}
export function ${name}({
children,
className = '',
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ${name}Props) {
const classes = [
styles.${template.component},
styles[variant],
styles[size],
disabled ? styles.disabled : '',
className,
].filter(Boolean).join(' ');
return (
<button className={classes} disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
export default ${name};
`;
}
// Default CSS
return `import React from 'react';
import './${name}.css';
${propsInterface}
export function ${name}({
children,
className = '',
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ${name}Props) {
const classes = [
'${template.component}',
\`${template.component}--\${variant}\`,
\`${template.component}--\${size}\`,
disabled ? '${template.component}--disabled' : '',
className,
].filter(Boolean).join(' ');
return (
<button className={classes} disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
export default ${name};
`;
}
function generateVue(options: GenerateOptions, template: { styles: string; component: string }): string {
const { name } = options;
if (options.styling === "tailwind") {
return `<template>
<button
:class="[
'${template.component}',
variants[variant],
sizes[size],
{ 'opacity-50 cursor-not-allowed': disabled }
]"
:disabled="disabled"
@click="$emit('click')"
>
<slot />
</button>
</template>
<script setup lang="ts">
defineProps<{
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}>();
defineEmits<{
click: [];
}>();
const variants = {
primary: '',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'bg-transparent hover:bg-gray-100',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
</script>
`;
}
return `<template>
<button
:class="[
'${template.component}',
'${template.component}--' + variant,
'${template.component}--' + size,
{ '${template.component}--disabled': disabled }
]"
:disabled="disabled"
@click="$emit('click')"
>
<slot />
</button>
</template>
<script setup lang="ts">
defineProps<{
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}>();
defineEmits<{
click: [];
}>();
</script>
<style scoped>
${template.styles}
.${template.component}--secondary {
background: #f3f4f6;
color: #111827;
}
.${template.component}--ghost {
background: transparent;
border: none;
}
.${template.component}--sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.${template.component}--lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.${template.component}--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
`;
}
function generateSvelte(options: GenerateOptions, template: { styles: string; component: string }): string {
const { name: _name } = options;
if (options.styling === "tailwind") {
return `<script lang="ts">
export let variant: 'primary' | 'secondary' | 'ghost' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let disabled = false;
const variants = {
primary: '${template.component}',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'bg-transparent hover:bg-gray-100',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
</script>
<button
class="{variants[variant]} {sizes[size]} {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
{disabled}
on:click
>
<slot />
</button>
`;
}
return `<script lang="ts">
export let variant: 'primary' | 'secondary' | 'ghost' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let disabled = false;
</script>
<button
class="${template.component} ${template.component}--{variant} ${template.component}--{size}"
class:${template.component}--disabled={disabled}
{disabled}
on:click
>
<slot />
</button>
<style>
${template.styles}
.${template.component}--secondary {
background: #f3f4f6;
color: #111827;
}
.${template.component}--ghost {
background: transparent;
border: none;
}
.${template.component}--sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.${template.component}--lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.${template.component}--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
`;
}
function generateHTML(options: GenerateOptions, template: { styles: string; component: string }): string {
const { name } = options;
if (options.styling === "tailwind") {
return `<!-- ${name} Component -->
<!-- Add Tailwind CSS to your project for these styles to work -->
<button class="${template.component}">
Button Text
</button>
<!-- Variants -->
<button class="${template.component}">Primary</button>
<button class="bg-gray-100 text-gray-900 hover:bg-gray-200 px-4 py-2 rounded-lg">Secondary</button>
<button class="bg-transparent hover:bg-gray-100 px-4 py-2 rounded-lg">Ghost</button>
`;
}
return `<!-- ${name} Component -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${name} Component</title>
<style>
${template.styles}
.${template.component}--secondary {
background: #f3f4f6;
color: #111827;
}
.${template.component}--ghost {
background: transparent;
border: none;
}
.${template.component}--sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.${template.component}--lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.${template.component}--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<button class="${template.component}">Primary Button</button>
<button class="${template.component} ${template.component}--secondary">Secondary Button</button>
<button class="${template.component} ${template.component}--ghost">Ghost Button</button>
</body>
</html>
`;
}
// === File Extension Mapping ===
function getFileExtension(framework: Framework, styling: Styling): string {
switch (framework) {
case "react":
return styling === "styled-components" || styling === "emotion" ? "tsx" : "tsx";
case "vue":
return "vue";
case "svelte":
return "svelte";
case "html":
return "html";
default:
return "txt";
}
}
// === Core Generation Function ===
export async function generateComponent(options: GenerateOptions): Promise<Map<string, string>> {
const outputs = new Map<string, string>();
// Get template based on component type
let template: { styles: string; component: string };
switch (options.type) {
case "button":
template = getButtonTemplate(options);
break;
case "card":
template = getCardTemplate(options);
break;
case "input":
template = getInputTemplate(options);
break;
default:
template = getButtonTemplate(options); // Default to button
}
// Generate component based on framework
let componentCode: string;
switch (options.framework) {
case "react":
componentCode = generateReact(options, template);
break;
case "vue":
componentCode = generateVue(options, template);
break;
case "svelte":
componentCode = generateSvelte(options, template);
break;
case "html":
componentCode = generateHTML(options, template);
break;
default:
throw new Error(`Unknown framework: ${options.framework}`);
}
const ext = getFileExtension(options.framework, options.styling);
const fileName = `${options.name}.${ext}`;
outputs.set(fileName, componentCode);
// Generate separate CSS file if needed
if (options.styling === "css" && template.styles && options.framework !== "html") {
outputs.set(`${options.name}.css`, template.styles);
}
if (options.styling === "css-modules" && template.styles) {
outputs.set(`${options.name}.module.css`, template.styles);
}
return outputs;
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Component Template Generator
Generates component templates with design-focused styling
for multiple frameworks (React, Vue, Svelte, HTML).
Usage:
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts [options] <output-dir>
Arguments:
<output-dir> Directory for output files
Options:
-h, --help Show this help message
-v, --version Show version
--spec <file> JSON specification file
--name <name> Component name (PascalCase)
--type <type> Component type: button, card, input, modal, navigation, hero
--framework <fw> Framework: react, vue, svelte, html
--styling <type> Styling: css, tailwind, css-modules, styled-components, emotion
--aesthetic <type> Aesthetic: minimal, bold, organic, brutalist, glassmorphism, neumorphism
--animation <level> Animation: none, subtle, expressive
--dark-mode Include dark mode support
--tokens <file> Use design tokens file
Examples:
# Generate React button with Tailwind
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
--name Button --framework react --styling tailwind --aesthetic bold ./components/
# Generate Vue card with CSS modules
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
--name Card --type card --framework vue --styling css-modules ./components/
# Generate Svelte input with minimal aesthetic
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
--name Input --type input --framework svelte --aesthetic minimal ./components/
# Generate from spec file
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
--spec component-spec.json ./components/
`);
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "version", "dark-mode"],
string: ["spec", "name", "type", "framework", "styling", "aesthetic", "animation", "tokens"],
alias: { h: "help", v: "version" },
default: {
type: "button",
framework: "react",
styling: "css",
animation: "subtle",
"dark-mode": false,
},
});
if (parsed.help) {
printHelp();
Deno.exit(0);
}
if (parsed.version) {
console.log(`${SCRIPT_NAME} v${VERSION}`);
Deno.exit(0);
}
const outputDir = parsed._[0] as string;
if (!outputDir) {
console.error("Error: Output directory required");
console.error("Use --help for usage information");
Deno.exit(1);
}
if (!parsed.spec && !parsed.name) {
console.error("Error: Component name required (--name or --spec)");
console.error("Use --help for usage information");
Deno.exit(1);
}
const validFrameworks: Framework[] = ["react", "vue", "svelte", "html"];
const validStyling: Styling[] = ["css", "tailwind", "css-modules", "styled-components", "emotion"];
const validAesthetics: Aesthetic[] = ["minimal", "bold", "organic", "brutalist", "glassmorphism", "neumorphism"];
const validTypes: ComponentType[] = ["button", "card", "input", "modal", "navigation", "hero", "custom"];
const validAnimations: Animation[] = ["none", "subtle", "expressive"];
if (!validFrameworks.includes(parsed.framework as Framework)) {
console.error(`Error: Invalid framework '${parsed.framework}'`);
console.error(`Valid frameworks: ${validFrameworks.join(", ")}`);
Deno.exit(1);
}
if (!validStyling.includes(parsed.styling as Styling)) {
console.error(`Error: Invalid styling '${parsed.styling}'`);
console.error(`Valid styling options: ${validStyling.join(", ")}`);
Deno.exit(1);
}
if (parsed.aesthetic && !validAesthetics.includes(parsed.aesthetic as Aesthetic)) {
console.error(`Error: Invalid aesthetic '${parsed.aesthetic}'`);
console.error(`Valid aesthetics: ${validAesthetics.join(", ")}`);
Deno.exit(1);
}
if (!validTypes.includes(parsed.type as ComponentType)) {
console.error(`Error: Invalid type '${parsed.type}'`);
console.error(`Valid types: ${validTypes.join(", ")}`);
Deno.exit(1);
}
if (!validAnimations.includes(parsed.animation as Animation)) {
console.error(`Error: Invalid animation '${parsed.animation}'`);
console.error(`Valid animations: ${validAnimations.join(", ")}`);
Deno.exit(1);
}
const options: GenerateOptions = {
specFile: parsed.spec,
name: parsed.name || "Component",
type: parsed.type as ComponentType,
framework: parsed.framework as Framework,
styling: parsed.styling as Styling,
aesthetic: parsed.aesthetic as Aesthetic | undefined,
animation: parsed.animation as Animation,
darkMode: parsed["dark-mode"],
tokens: parsed.tokens,
outputDir,
};
try {
const outputs = await generateComponent(options);
const outPath = resolve(outputDir);
await ensureDir(outPath);
for (const [fileName, content] of outputs) {
const filePath = join(outPath, fileName);
await Deno.writeTextFile(filePath, content);
console.log(`Generated: ${filePath}`);
}
console.log(`\nSuccessfully generated ${outputs.size} file(s)`);
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}