Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:49:21 +08:00
commit 29665c13f5
11 changed files with 5611 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "frontend-designer",
"description": "Build accessible, responsive, and performant frontend components with design system best practices, modern CSS, and framework-agnostic patterns.",
"version": "0.0.0-2025.11.28",
"author": {
"name": "James Rochabrun",
"email": "jamesrochabrun@gmail.com"
},
"skills": [
"./skills/frontend-designer"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# frontend-designer
Build accessible, responsive, and performant frontend components with design system best practices, modern CSS, and framework-agnostic patterns.

72
plugin.lock.json Normal file
View File

@@ -0,0 +1,72 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:jamesrochabrun/skills:frontend-designer",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "25006303dc46aeb276e3867b2a6969938f5f9360",
"treeHash": "3ddcd83c606e56801426300ea6cd6b05a7be6b81613e9ef392a5e02500315daf",
"generatedAt": "2025-11-28T10:17:48.628984Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "frontend-designer",
"description": "Build accessible, responsive, and performant frontend components with design system best practices, modern CSS, and framework-agnostic patterns."
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "6843e1e7e42d80b31a1a6486aa749240fea67755a98772e2715769b945f02b62"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "97ca00c41e61c927b515383b129c3ece2b0e33c1629d2252cfec6e6fcbc8f25d"
},
{
"path": "skills/frontend-designer/SKILL.md",
"sha256": "f144c92b819f8cca125c710b3085cee3cf8d3093512aada40ab2e0f7e2125705"
},
{
"path": "skills/frontend-designer/references/accessibility_checklist.md",
"sha256": "16eecde5bc6e0ad4045f1be67eedaa9587ba56360036250a87853681c596d4fb"
},
{
"path": "skills/frontend-designer/references/responsive_patterns.md",
"sha256": "06250863cb7fef0e25d3272dc17a84a7e777a82878138778fd9feddce17b8f60"
},
{
"path": "skills/frontend-designer/references/component_library.md",
"sha256": "709b28d81b6c0505f96d08b6ad168aeadbc660c01c703639e018b7095994d93d"
},
{
"path": "skills/frontend-designer/references/design_tokens.md",
"sha256": "fbba3f6d2d0b6fa0af11bec39532db48489646d16b11fcc6b7ec2314bb993c77"
},
{
"path": "skills/frontend-designer/scripts/setup_design_system.sh",
"sha256": "50ed5f7a8e7cdb823aa42a6f549ba988333450e2cfdab3bafb0cca237d67506f"
},
{
"path": "skills/frontend-designer/scripts/audit_accessibility.sh",
"sha256": "3c86c0d8e2fab2c9f9a49931b7952361413ff8e8f432da903e5f7cf578a9053d"
},
{
"path": "skills/frontend-designer/scripts/generate_component.sh",
"sha256": "f7c2fb178721672f7e8dfee6a57456877c791dd59fbfb2ce34fce491b5a76e14"
}
],
"dirSha256": "3ddcd83c606e56801426300ea6cd6b05a7be6b81613e9ef392a5e02500315daf"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,862 @@
---
name: frontend-designer
description: Build accessible, responsive, and performant frontend components with design system best practices, modern CSS, and framework-agnostic patterns.
---
# Frontend Designer
A comprehensive skill for frontend designers and developers to build beautiful, accessible, and performant user interfaces with modern best practices.
## What This Skill Does
Helps frontend designers/developers with:
- **Component Design & Development** - Build reusable, accessible components
- **Design Systems** - Implement tokens, patterns, and documentation
- **Responsive Design** - Mobile-first, fluid layouts
- **Accessibility (WCAG 2.1)** - Inclusive design patterns
- **Modern CSS** - Flexbox, Grid, custom properties
- **Performance Optimization** - Fast, efficient frontends
- **Framework Patterns** - React, Vue, Svelte best practices
- **Design-to-Code** - Figma to production workflows
## Why This Skill Matters
**Without systematic approach:**
- Inconsistent component implementations
- Accessibility issues
- Poor responsive behavior
- Duplicate code and styles
- Hard to maintain
- Performance problems
**With this skill:**
- Consistent, reusable components
- WCAG AA compliant by default
- Mobile-first responsive design
- Design system aligned
- Maintainable codebase
- Fast, optimized delivery
## Core Principles
### 1. Accessibility First
- WCAG 2.1 AA minimum
- Semantic HTML
- Keyboard navigation
- Screen reader support
- Focus management
- Color contrast
### 2. Mobile-First Responsive
- Start with mobile (320px)
- Progressive enhancement
- Fluid typography
- Flexible layouts
- Touch-friendly targets
### 3. Performance by Default
- Minimal CSS/JS
- Lazy loading
- Optimized images
- Critical CSS
- Tree shaking
### 4. Component-Driven
- Atomic design methodology
- Reusable components
- Props-based customization
- Composition over inheritance
### 5. Design System Aligned
- Design tokens
- Consistent spacing
- Typography scale
- Color palette
- Component library
## Component Patterns
### Button Component
**Accessible, flexible button pattern:**
```tsx
// React example
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
children,
onClick,
...props
}) => {
return (
<button
className={`btn btn--${variant} btn--${size}`}
disabled={disabled || loading}
onClick={onClick}
aria-busy={loading}
{...props}
>
{loading ? <Spinner /> : children}
</button>
);
};
```
**CSS (with design tokens):**
```css
.btn {
/* Base styles */
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-family: var(--font-sans);
font-weight: 600;
text-decoration: none;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
/* Accessibility */
min-height: 44px; /* WCAG touch target */
&:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
/* Variants */
.btn--primary {
background: var(--color-primary);
color: var(--color-on-primary);
&:hover:not(:disabled) {
background: var(--color-primary-hover);
}
}
.btn--secondary {
background: var(--color-secondary);
color: var(--color-on-secondary);
}
.btn--ghost {
background: transparent;
color: var(--color-primary);
border: 1px solid currentColor;
}
/* Sizes */
.btn--sm {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
}
.btn--md {
padding: var(--space-3) var(--space-4);
font-size: var(--text-base);
}
.btn--lg {
padding: var(--space-4) var(--space-6);
font-size: var(--text-lg);
}
```
### Card Component
**Flexible, accessible card:**
```tsx
interface CardProps {
variant?: 'elevated' | 'outlined' | 'filled';
padding?: 'none' | 'sm' | 'md' | 'lg';
interactive?: boolean;
children: React.ReactNode;
}
export const Card: React.FC<CardProps> = ({
variant = 'elevated',
padding = 'md',
interactive = false,
children,
}) => {
const Component = interactive ? 'button' : 'div';
return (
<Component
className={`
card
card--${variant}
card--padding-${padding}
${interactive ? 'card--interactive' : ''}
`}
{...(interactive && { role: 'button', tabIndex: 0 })}
>
{children}
</Component>
);
};
```
**CSS:**
```css
.card {
border-radius: var(--radius-lg);
background: var(--color-surface);
}
.card--elevated {
box-shadow: var(--shadow-md);
}
.card--outlined {
border: 1px solid var(--color-border);
}
.card--filled {
background: var(--color-surface-variant);
}
.card--interactive {
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
&:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
}
.card--padding-sm { padding: var(--space-3); }
.card--padding-md { padding: var(--space-4); }
.card--padding-lg { padding: var(--space-6); }
```
### Form Input Component
**Accessible form input:**
```tsx
interface InputProps {
label: string;
error?: string;
hint?: string;
required?: boolean;
type?: 'text' | 'email' | 'password' | 'number';
}
export const Input: React.FC<InputProps> = ({
label,
error,
hint,
required = false,
type = 'text',
...props
}) => {
const id = useId();
const hintId = `${id}-hint`;
const errorId = `${id}-error`;
return (
<div className="input-wrapper">
<label htmlFor={id} className="input-label">
{label}
{required && <span aria-label="required">*</span>}
</label>
{hint && (
<p id={hintId} className="input-hint">
{hint}
</p>
)}
<input
id={id}
type={type}
className={`input ${error ? 'input--error' : ''}`}
aria-required={required}
aria-invalid={!!error}
aria-describedby={error ? errorId : hint ? hintId : undefined}
{...props}
/>
{error && (
<p id={errorId} className="input-error" role="alert">
{error}
</p>
)}
</div>
);
};
```
## Design Tokens
**CSS Custom Properties for design system:**
```css
:root {
/* Colors - Primary */
--color-primary: #0066FF;
--color-primary-hover: #0052CC;
--color-on-primary: #FFFFFF;
/* Colors - Surface */
--color-surface: #FFFFFF;
--color-surface-variant: #F5F5F5;
--color-on-surface: #1A1A1A;
/* Colors - Borders */
--color-border: #E0E0E0;
--color-border-hover: #BDBDBD;
/* Colors - Semantic */
--color-error: #D32F2F;
--color-success: #388E3C;
--color-warning: #F57C00;
--color-info: #1976D2;
/* Spacing Scale (8px base) */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.5rem; /* 24px */
--space-6: 2rem; /* 32px */
--space-8: 3rem; /* 48px */
--space-10: 4rem; /* 64px */
/* Typography Scale */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
--text-4xl: 2.25rem; /* 36px */
/* Font Families */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-mono: "SF Mono", Monaco, "Cascadia Code", monospace;
/* Line Heights */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
/* Border Radius */
--radius-sm: 0.25rem; /* 4px */
--radius-md: 0.5rem; /* 8px */
--radius-lg: 1rem; /* 16px */
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
/* Focus Ring */
--color-focus: #0066FF;
/* Transitions */
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-surface: #1A1A1A;
--color-surface-variant: #2A2A2A;
--color-on-surface: #FFFFFF;
--color-border: #3A3A3A;
}
}
```
## Responsive Design Patterns
### Mobile-First Breakpoints
```css
/* Mobile-first approach */
.container {
padding: var(--space-4);
/* Tablet: 768px and up */
@media (min-width: 48rem) {
padding: var(--space-6);
}
/* Desktop: 1024px and up */
@media (min-width: 64rem) {
padding: var(--space-8);
max-width: 1200px;
margin: 0 auto;
}
}
```
### Fluid Typography
```css
/* Responsive typography */
h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
line-height: var(--leading-tight);
}
h2 {
font-size: clamp(1.5rem, 4vw, 2.5rem);
}
p {
font-size: clamp(1rem, 2vw, 1.125rem);
line-height: var(--leading-normal);
}
```
### Grid Layouts
```css
/* Responsive grid */
.grid {
display: grid;
gap: var(--space-4);
/* Auto-fit columns (min 280px) */
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
/* 12-column grid system */
.grid-12 {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: var(--space-4);
}
.col-span-4 {
grid-column: span 4;
}
/* Stack on mobile */
@media (max-width: 48rem) {
.col-span-4 {
grid-column: span 12;
}
}
```
## Accessibility Patterns
### Skip Links
```tsx
export const SkipLink = () => (
<a href="#main-content" className="skip-link">
Skip to main content
</a>
);
```
```css
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--color-primary);
color: var(--color-on-primary);
padding: var(--space-2) var(--space-4);
text-decoration: none;
z-index: 100;
&:focus {
top: 0;
}
}
```
### Focus Management
```tsx
// Modal with focus trap
export const Modal = ({ isOpen, onClose, children }) => {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Save currently focused element
const previouslyFocused = document.activeElement;
// Focus first focusable element in modal
const firstFocusable = modalRef.current?.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
// Restore focus on close
return () => previouslyFocused?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
className="modal-overlay"
>
<div className="modal">
{children}
</div>
</div>
);
};
```
### ARIA Labels
```tsx
// Icon button with accessible label
export const IconButton = ({ icon, label, ...props }) => (
<button
aria-label={label}
className="icon-button"
{...props}
>
<span aria-hidden="true">{icon}</span>
</button>
);
// Loading state
export const LoadingButton = ({ loading, children, ...props }) => (
<button
aria-busy={loading}
aria-live="polite"
disabled={loading}
{...props}
>
{loading ? 'Loading...' : children}
</button>
);
```
## Performance Optimization
### Critical CSS
```html
<!-- Inline critical CSS -->
<style>
/* Above-the-fold styles */
body { margin: 0; font-family: var(--font-sans); }
.header { /* critical header styles */ }
.hero { /* critical hero styles */ }
</style>
<!-- Load full stylesheet async -->
<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
```
### Lazy Loading Images
```tsx
export const LazyImage = ({ src, alt, ...props }) => (
<img
src={src}
alt={alt}
loading="lazy"
decoding="async"
{...props}
/>
);
```
### Code Splitting
```tsx
// React lazy loading
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
);
}
```
## Modern CSS Techniques
### Container Queries
```css
.card {
container-type: inline-size;
}
.card-content {
display: flex;
flex-direction: column;
/* Switch to row layout when container > 400px */
@container (min-width: 400px) {
flex-direction: row;
}
}
```
### CSS Grid Auto-Fit
```css
.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--space-4);
}
```
### Custom Properties for Theming
```css
/* Light theme (default) */
:root {
--bg: #ffffff;
--text: #000000;
}
/* Dark theme */
[data-theme="dark"] {
--bg: #000000;
--text: #ffffff;
}
body {
background: var(--bg);
color: var(--text);
}
```
## Component Library Structure
```
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.css
│ │ ├── Button.test.tsx
│ │ └── Button.stories.tsx
│ ├── Card/
│ ├── Input/
│ └── index.ts
├── tokens/
│ ├── colors.css
│ ├── spacing.css
│ ├── typography.css
│ └── index.css
├── utils/
│ ├── a11y.ts
│ └── responsive.ts
└── index.ts
```
## Using This Skill
### Generate Component
```bash
./scripts/generate_component.sh Button
```
Creates component with:
- TypeScript/JSX file
- CSS module
- Test file
- Storybook story
- Accessibility checks
### Design System Setup
```bash
./scripts/setup_design_system.sh
```
Creates:
- Design tokens (CSS custom properties)
- Base styles
- Component templates
- Documentation structure
### Accessibility Audit
```bash
./scripts/audit_accessibility.sh
```
Checks:
- Color contrast ratios
- Keyboard navigation
- ARIA attributes
- Semantic HTML
- Focus management
## Best Practices
### Component Design
**DO:**
- Use semantic HTML
- Make components keyboard accessible
- Provide ARIA labels
- Support both light and dark modes
- Make touch targets 44x44px minimum
- Use proper heading hierarchy
- Handle loading and error states
**DON'T:**
- Use divs for buttons
- Forget focus styles
- Hard-code colors
- Ignore mobile viewports
- Skip alt text on images
- Create inaccessible modals
### CSS Best Practices
**DO:**
- Use design tokens
- Mobile-first responsive
- BEM or CSS modules for naming
- Logical properties (inline-start vs left)
- Modern layout (flexbox, grid)
**DON'T:**
- Use !important
- Deep nesting (> 3 levels)
- Fixed pixel values everywhere
- Browser-specific hacks
- Inline styles
### Performance
**DO:**
- Lazy load images
- Code split routes
- Minimize CSS
- Use system fonts
- Optimize images (WebP)
- Critical CSS inline
**DON'T:**
- Load unused CSS
- Large JavaScript bundles
- Unoptimized images
- Blocking resources
- Too many web fonts
## Framework-Specific Patterns
### React
```tsx
// Composition pattern
export const Card = ({ children }) => (
<div className="card">{children}</div>
);
export const CardHeader = ({ children }) => (
<div className="card-header">{children}</div>
);
// Usage
<Card>
<CardHeader>Title</CardHeader>
<CardBody>Content</CardBody>
</Card>
```
### Vue
```vue
<!-- Composable component -->
<template>
<div :class="cardClasses">
<slot />
</div>
</template>
<script setup>
const props = defineProps({
variant: String,
padding: String
});
const cardClasses = computed(() => [
'card',
`card--${props.variant}`,
`card--padding-${props.padding}`
]);
</script>
```
## Resources
All reference materials included:
- Design token systems
- Accessibility checklist (WCAG 2.1)
- Responsive design patterns
- Component library templates
- Performance optimization guide
## Summary
This skill provides:
- **Accessible components** - WCAG AA by default
- **Responsive design** - Mobile-first approach
- **Design systems** - Token-based consistency
- **Modern CSS** - Flexbox, Grid, custom properties
- **Performance** - Optimized delivery
- **Best practices** - Production-ready patterns
**Use this skill to build beautiful, accessible, performant frontends.**
---
**"Good design is accessible design."**

View File

@@ -0,0 +1,622 @@
# WCAG 2.1 AA Accessibility Checklist
Comprehensive checklist for ensuring your frontend meets WCAG 2.1 Level AA compliance.
## Perceivable
Information and user interface components must be presentable to users in ways they can perceive.
### 1.1 Text Alternatives
**1.1.1 Non-text Content (Level A)**
- [ ] All images have appropriate alt text
- [ ] Decorative images use empty alt (`alt=""`)
- [ ] Complex images have detailed descriptions
- [ ] Icons have text alternatives or aria-label
- [ ] Charts/graphs have text descriptions
- [ ] CAPTCHAs have alternative forms
```html
<!-- Good examples -->
<img src="logo.png" alt="Company Name">
<img src="decorative.png" alt="" role="presentation">
<button aria-label="Close dialog"><span aria-hidden="true">×</span></button>
```
### 1.2 Time-based Media
**1.2.1 Audio-only and Video-only (Level A)**
- [ ] Audio-only content has transcripts
- [ ] Video-only content has transcripts or audio description
**1.2.2 Captions (Level A)**
- [ ] All pre-recorded videos have captions
- [ ] Captions are synchronized and accurate
**1.2.3 Audio Description or Media Alternative (Level A)**
- [ ] Videos have audio descriptions or text alternative
**1.2.4 Captions (Live) (Level AA)**
- [ ] Live videos have captions
**1.2.5 Audio Description (Level AA)**
- [ ] All pre-recorded videos have audio descriptions
```html
<video controls>
<source src="video.mp4" type="video/mp4">
<track kind="captions" src="captions.vtt" srclang="en" label="English">
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="English descriptions">
</video>
```
### 1.3 Adaptable
**1.3.1 Info and Relationships (Level A)**
- [ ] Semantic HTML used correctly (headings, lists, tables)
- [ ] Form labels properly associated with inputs
- [ ] Related form controls are grouped
- [ ] Visual presentation matches code structure
- [ ] ARIA roles used when needed
```html
<!-- Semantic structure -->
<main>
<h1>Main Heading</h1>
<section>
<h2>Section Heading</h2>
<p>Content</p>
</section>
</main>
<!-- Proper form labels -->
<label for="email">Email address</label>
<input type="email" id="email" name="email">
<!-- Grouped controls -->
<fieldset>
<legend>Contact preferences</legend>
<label><input type="checkbox" name="email"> Email</label>
<label><input type="checkbox" name="phone"> Phone</label>
</fieldset>
```
**1.3.2 Meaningful Sequence (Level A)**
- [ ] Reading order is logical
- [ ] Tab order follows visual flow
- [ ] CSS positioning doesn't disrupt reading order
**1.3.3 Sensory Characteristics (Level A)**
- [ ] Instructions don't rely solely on shape
- [ ] Instructions don't rely solely on size
- [ ] Instructions don't rely solely on location
- [ ] Instructions don't rely solely on sound
```html
<!-- ❌ Bad -->
<p>Click the blue button on the right</p>
<!-- ✅ Good -->
<p>Click the "Submit" button to continue</p>
```
**1.3.4 Orientation (Level AA)**
- [ ] Content works in portrait and landscape
- [ ] No orientation restrictions unless essential
**1.3.5 Identify Input Purpose (Level AA)**
- [ ] Input fields use autocomplete attribute when appropriate
```html
<input type="email" name="email" autocomplete="email">
<input type="tel" name="phone" autocomplete="tel">
<input type="text" name="address" autocomplete="street-address">
```
### 1.4 Distinguishable
**1.4.1 Use of Color (Level A)**
- [ ] Color not used as only visual means of conveying information
- [ ] Color not used as only way to distinguish interactive elements
- [ ] Links are distinguishable without color alone
```css
/* ✅ Good - underline + color */
a {
color: blue;
text-decoration: underline;
}
/* Or use icons, borders, etc. */
.error {
color: red;
border-left: 4px solid red;
padding-left: 12px;
}
.error::before {
content: "⚠ ";
}
```
**1.4.2 Audio Control (Level A)**
- [ ] Auto-playing audio can be paused
- [ ] Auto-playing audio stops after 3 seconds
- [ ] Volume controls available
**1.4.3 Contrast (Minimum) (Level AA)**
- [ ] Normal text: 4.5:1 contrast ratio
- [ ] Large text (18pt+): 3:1 contrast ratio
- [ ] UI components: 3:1 contrast ratio
- [ ] Graphical objects: 3:1 contrast ratio
```css
/* Check with contrast checkers */
.text {
color: #595959; /* 7:1 on white ✅ */
background: #FFFFFF;
}
.button {
color: #FFFFFF;
background: #0066FF; /* 4.6:1 ✅ */
border: 2px solid #0052CC; /* 3:1 ✅ */
}
```
**1.4.4 Resize Text (Level AA)**
- [ ] Text can be resized to 200% without loss of content
- [ ] No horizontal scrolling at 200% zoom
- [ ] Use relative units (rem, em)
```css
/* ✅ Good */
body {
font-size: 1rem; /* Respects user preferences */
}
h1 {
font-size: 2.5rem; /* Scales with body */
}
/* ❌ Avoid */
.text {
font-size: 14px; /* Fixed size */
}
```
**1.4.5 Images of Text (Level AA)**
- [ ] Text is text, not images
- [ ] Exception: logos, essential presentations
**1.4.10 Reflow (Level AA)**
- [ ] Content reflows at 320px viewport width
- [ ] No horizontal scrolling (except tables, diagrams)
- [ ] Responsive design implemented
```css
/* Mobile-first responsive */
.container {
width: 100%;
max-width: 1200px;
padding: 1rem;
}
@media (min-width: 768px) {
.container {
padding: 2rem;
}
}
```
**1.4.11 Non-text Contrast (Level AA)**
- [ ] UI components: 3:1 contrast against background
- [ ] Graphical objects: 3:1 contrast
- [ ] Focus indicators: 3:1 contrast
**1.4.12 Text Spacing (Level AA)**
- [ ] Content adapts to increased text spacing
- [ ] Line height: at least 1.5x font size
- [ ] Paragraph spacing: at least 2x font size
- [ ] Letter spacing: at least 0.12x font size
- [ ] Word spacing: at least 0.16x font size
```css
/* Ensure content doesn't break */
body {
line-height: 1.5;
}
p {
margin-bottom: 2em;
}
```
**1.4.13 Content on Hover or Focus (Level AA)**
- [ ] Additional content (tooltips, dropdowns) is dismissible
- [ ] Hoverable content stays visible when hovering over it
- [ ] Content remains visible until dismissed or no longer relevant
```css
/* Tooltip stays visible when hovering over it */
.tooltip:hover .tooltip-content,
.tooltip .tooltip-content:hover {
display: block;
}
```
## Operable
User interface components and navigation must be operable.
### 2.1 Keyboard Accessible
**2.1.1 Keyboard (Level A)**
- [ ] All functionality available via keyboard
- [ ] No keyboard traps
- [ ] Logical tab order
- [ ] Custom controls are keyboard accessible
```html
<!-- Custom button needs tabindex and keyboard handlers -->
<div role="button" tabindex="0"
onclick="handleClick()"
onkeydown="if(event.key==='Enter'||event.key===' ') handleClick()">
Click me
</div>
```
**2.1.2 No Keyboard Trap (Level A)**
- [ ] Focus can move away from all components
- [ ] Instructions provided if non-standard exit method
**2.1.4 Character Key Shortcuts (Level A)**
- [ ] Single-key shortcuts can be turned off
- [ ] Or remapped by user
- [ ] Or only active when component has focus
### 2.2 Enough Time
**2.2.1 Timing Adjustable (Level A)**
- [ ] Time limits can be turned off, adjusted, or extended
- [ ] User warned before time expires
- [ ] At least 20 seconds to extend
**2.2.2 Pause, Stop, Hide (Level A)**
- [ ] Moving content can be paused
- [ ] Auto-updating content can be paused/stopped
- [ ] Blinking content can be stopped
```html
<!-- Provide controls -->
<div class="carousel">
<button aria-label="Pause carousel"></button>
<button aria-label="Play carousel"></button>
</div>
```
### 2.3 Seizures and Physical Reactions
**2.3.1 Three Flashes or Below Threshold (Level A)**
- [ ] No content flashes more than 3 times per second
- [ ] Or flashes are below general flash/red flash thresholds
### 2.4 Navigable
**2.4.1 Bypass Blocks (Level A)**
- [ ] Skip navigation link provided
- [ ] Landmark regions defined
- [ ] Headings structure content
```html
<!-- Skip link (visually hidden until focused) -->
<a href="#main" class="skip-link">Skip to main content</a>
<header>
<nav aria-label="Main navigation">...</nav>
</header>
<main id="main">
<h1>Page Title</h1>
</main>
```
```css
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
```
**2.4.2 Page Titled (Level A)**
- [ ] Page has descriptive title
- [ ] Title identifies page content
```html
<title>Contact Us - Company Name</title>
```
**2.4.3 Focus Order (Level A)**
- [ ] Focus order is logical and intuitive
- [ ] Matches visual order
- [ ] No positive tabindex values
```css
/* ❌ Avoid */
.element { tabindex: 5; }
/* ✅ Use */
.element { tabindex: 0; } /* In natural order */
.element { tabindex: -1; } /* Programmatic focus only */
```
**2.4.4 Link Purpose (Level A)**
- [ ] Link text describes destination
- [ ] Context is clear
- [ ] Avoid "click here" or "read more"
```html
<!-- ❌ Bad -->
<a href="/report.pdf">Click here</a>
<!-- ✅ Good -->
<a href="/report.pdf">Download 2024 Annual Report (PDF, 2MB)</a>
```
**2.4.5 Multiple Ways (Level AA)**
- [ ] Multiple ways to find pages (menu, search, sitemap)
- [ ] Exception: pages that are steps in a process
**2.4.6 Headings and Labels (Level AA)**
- [ ] Headings describe content
- [ ] Labels describe purpose
- [ ] Headings and labels are clear
**2.4.7 Focus Visible (Level AA)**
- [ ] Keyboard focus indicator is visible
- [ ] Sufficient contrast (3:1)
- [ ] Clearly indicates focused element
```css
/* ✅ Strong focus indicator */
*:focus-visible {
outline: 2px solid #0066FF;
outline-offset: 2px;
}
/* Or custom focus styles */
button:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.5);
}
```
### 2.5 Input Modalities
**2.5.1 Pointer Gestures (Level A)**
- [ ] Complex gestures have single-pointer alternative
- [ ] Path-based gestures have simple alternative
**2.5.2 Pointer Cancellation (Level A)**
- [ ] Actions triggered on up-event (not down)
- [ ] Or can be aborted/undone
**2.5.3 Label in Name (Level A)**
- [ ] Visible label matches accessible name
- [ ] Accessible name starts with visible text
```html
<!-- ✅ Good - matches -->
<button aria-label="Submit form">Submit</button>
<!-- ❌ Bad - doesn't match -->
<button aria-label="Send">Submit</button>
```
**2.5.4 Motion Actuation (Level A)**
- [ ] Device motion triggers have UI alternative
- [ ] Can disable motion actuation
## Understandable
Information and user interface operation must be understandable.
### 3.1 Readable
**3.1.1 Language of Page (Level A)**
- [ ] Page language is identified
```html
<html lang="en">
```
**3.1.2 Language of Parts (Level AA)**
- [ ] Language changes are marked
```html
<p>The French phrase <span lang="fr">c'est la vie</span> means "that's life".</p>
```
### 3.2 Predictable
**3.2.1 On Focus (Level A)**
- [ ] Focusing an element doesn't trigger context change
- [ ] No automatic form submission on focus
**3.2.2 On Input (Level A)**
- [ ] Changing settings doesn't automatically cause context change
- [ ] User is warned of automatic changes
**3.2.3 Consistent Navigation (Level AA)**
- [ ] Navigation order is consistent across pages
- [ ] Repeated navigation in same order
**3.2.4 Consistent Identification (Level AA)**
- [ ] Components with same functionality are identified consistently
- [ ] Icons mean the same thing throughout
### 3.3 Input Assistance
**3.3.1 Error Identification (Level A)**
- [ ] Errors are identified in text
- [ ] Error is described to user
```html
<input type="email" aria-invalid="true" aria-describedby="email-error">
<span id="email-error" role="alert">Please enter a valid email address</span>
```
**3.3.2 Labels or Instructions (Level A)**
- [ ] Labels provided for input
- [ ] Instructions provided when needed
```html
<label for="password">
Password (must be at least 8 characters)
</label>
<input type="password" id="password" required minlength="8">
```
**3.3.3 Error Suggestion (Level AA)**
- [ ] Errors suggest how to fix
- [ ] Specific, actionable feedback
```html
<span role="alert">
Password must contain at least one uppercase letter,
one number, and be at least 8 characters long.
</span>
```
**3.3.4 Error Prevention (Level AA)**
- [ ] Legal/financial transactions are reversible
- [ ] Data is checked and confirmed before submission
- [ ] User can review and correct before submitting
```html
<!-- Confirmation step -->
<div role="region" aria-labelledby="review-heading">
<h2 id="review-heading">Review Your Order</h2>
<!-- Show all details -->
<button>Edit Order</button>
<button>Confirm Purchase</button>
</div>
```
## Robust
Content must be robust enough to be interpreted by a wide variety of user agents, including assistive technologies.
### 4.1 Compatible
**4.1.1 Parsing (Level A)** *[Obsolete in WCAG 2.2]*
- [ ] Valid HTML (no duplicate IDs, proper nesting)
**4.1.2 Name, Role, Value (Level A)**
- [ ] All UI components have accessible name
- [ ] Roles are appropriate
- [ ] States communicated to assistive tech
```html
<!-- Custom checkbox -->
<div role="checkbox"
aria-checked="false"
aria-labelledby="label-id"
tabindex="0">
</div>
<span id="label-id">Accept terms</span>
<!-- Button states -->
<button aria-pressed="false" aria-label="Mute">🔊</button>
<button aria-pressed="true" aria-label="Mute">🔇</button>
```
**4.1.3 Status Messages (Level AA)**
- [ ] Status messages can be perceived by assistive tech
- [ ] Use aria-live, role="status", role="alert"
```html
<!-- Success message -->
<div role="status" aria-live="polite">
Form submitted successfully!
</div>
<!-- Error message -->
<div role="alert" aria-live="assertive">
Error: Connection lost. Please try again.
</div>
<!-- Loading state -->
<div aria-live="polite" aria-busy="true">
Loading content...
</div>
```
## Testing Checklist
### Automated Testing
- [ ] Run axe DevTools
- [ ] Run Lighthouse accessibility audit
- [ ] Run WAVE browser extension
- [ ] HTML validator (W3C)
- [ ] Color contrast checker
### Manual Testing
- [ ] Keyboard-only navigation
- [ ] Screen reader testing (NVDA, JAWS, VoiceOver)
- [ ] Zoom to 200% (text resize)
- [ ] Test with browser zoom (page zoom)
- [ ] Test in high contrast mode
- [ ] Test with dark mode
- [ ] Test responsive breakpoints
### Screen Reader Testing
- [ ] NVDA (Windows, free)
- [ ] JAWS (Windows, paid)
- [ ] VoiceOver (Mac/iOS, built-in)
- [ ] TalkBack (Android, built-in)
- [ ] ORCA (Linux, free)
### Browser Testing
- [ ] Chrome + screen reader
- [ ] Firefox + screen reader
- [ ] Safari + VoiceOver
- [ ] Edge + screen reader
## Quick Reference
### Critical Items (Must Fix)
1. ✅ Images have alt text
2. ✅ Form inputs have labels
3. ✅ Sufficient color contrast (4.5:1 text, 3:1 UI)
4. ✅ Keyboard accessible (all functionality)
5. ✅ Focus indicators visible
6. ✅ No keyboard traps
7. ✅ Semantic HTML (headings, landmarks)
8. ✅ ARIA used correctly
9. ✅ Page has title
10. ✅ Language identified
### Tools
- [axe DevTools](https://www.deque.com/axe/devtools/)
- [Lighthouse](https://developers.google.com/web/tools/lighthouse)
- [WAVE](https://wave.webaim.org/)
- [Color Contrast Analyzer](https://www.tpgi.com/color-contrast-checker/)
- [NVDA Screen Reader](https://www.nvaccess.org/)
### Resources
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
- [WebAIM](https://webaim.org/)
- [The A11Y Project](https://www.a11yproject.com/)
- [MDN Accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility)
---
**"Accessibility is not a feature, it's a fundamental right."**

View File

@@ -0,0 +1,937 @@
# Component Library Architecture
Complete guide to building, organizing, and maintaining a scalable component library.
## Component Architecture Principles
### 1. Atomic Design
Organize components hierarchically from smallest to largest.
**Hierarchy:**
```
Atoms → Molecules → Organisms → Templates → Pages
```
**Example Structure:**
```
components/
├── atoms/
│ ├── Button/
│ ├── Input/
│ ├── Icon/
│ └── Label/
├── molecules/
│ ├── FormField/
│ ├── SearchBar/
│ └── Card/
├── organisms/
│ ├── Header/
│ ├── LoginForm/
│ └── ProductCard/
├── templates/
│ ├── DashboardLayout/
│ └── AuthLayout/
└── pages/
├── HomePage/
└── ProfilePage/
```
### 2. Component Types
**Presentational (Dumb) Components:**
- Focus on how things look
- No state management
- Receive data via props
- Highly reusable
```tsx
// Presentational Button
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
children,
onClick,
}) => {
return (
<button
className={`btn btn--${variant} btn--${size}`}
onClick={onClick}
>
{children}
</button>
);
};
```
**Container (Smart) Components:**
- Focus on how things work
- Manage state
- Connect to data sources
- Use presentational components
```tsx
// Container Component
export const UserProfileContainer: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser().then(data => {
setUser(data);
setLoading(false);
});
}, []);
if (loading) return <LoadingSpinner />;
if (!user) return <ErrorMessage />;
return <UserProfile user={user} />;
};
```
### 3. Component Composition
**Build complex components from simple ones.**
```tsx
// Atoms
const Avatar: React.FC<AvatarProps> = ({ src, alt }) => (
<img className="avatar" src={src} alt={alt} />
);
const Badge: React.FC<BadgeProps> = ({ children }) => (
<span className="badge">{children}</span>
);
// Molecule (composed of atoms)
const UserBadge: React.FC<UserBadgeProps> = ({ user }) => (
<div className="user-badge">
<Avatar src={user.avatar} alt={user.name} />
<span className="user-badge__name">{user.name}</span>
<Badge>{user.role}</Badge>
</div>
);
// Organism (composed of molecules)
const UserList: React.FC<UserListProps> = ({ users }) => (
<ul className="user-list">
{users.map(user => (
<li key={user.id}>
<UserBadge user={user} />
</li>
))}
</ul>
);
```
## File Structure
### Standard Component Structure
```
Button/
├── Button.tsx # Main component
├── Button.test.tsx # Tests
├── Button.stories.tsx # Storybook stories
├── Button.module.css # Styles (CSS Modules)
├── Button.types.ts # TypeScript types
└── index.ts # Barrel export
```
### Example Files
**Button/Button.tsx**
```tsx
import React from 'react';
import styles from './Button.module.css';
import { ButtonProps } from './Button.types';
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
children,
onClick,
...props
}) => {
const className = [
styles.button,
styles[`button--${variant}`],
styles[`button--${size}`],
disabled && styles['button--disabled'],
loading && styles['button--loading'],
].filter(Boolean).join(' ');
return (
<button
className={className}
disabled={disabled || loading}
onClick={onClick}
aria-busy={loading}
{...props}
>
{loading && <span className={styles.spinner} />}
{children}
</button>
);
};
```
**Button/Button.types.ts**
```tsx
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Visual style variant */
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
/** Size variant */
size?: 'sm' | 'md' | 'lg';
/** Disabled state */
disabled?: boolean;
/** Loading state */
loading?: boolean;
/** Button content */
children: React.ReactNode;
/** Click handler */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
```
**Button/Button.test.tsx**
```tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders children correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('applies variant classes', () => {
render(<Button variant="secondary">Click me</Button>);
const button = screen.getByText('Click me');
expect(button).toHaveClass('button--secondary');
});
it('disables interaction when loading', () => {
const handleClick = jest.fn();
render(<Button loading onClick={handleClick}>Click me</Button>);
const button = screen.getByText('Click me');
expect(button).toBeDisabled();
expect(button).toHaveAttribute('aria-busy', 'true');
});
});
```
**Button/Button.stories.tsx**
```tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost', 'danger'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};
export const Loading: Story = {
args: {
loading: true,
children: 'Loading...',
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: 'Disabled Button',
},
};
```
**Button/index.ts**
```tsx
export { Button } from './Button';
export type { ButtonProps } from './Button.types';
```
## Common Components
### 1. Button Component
```tsx
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
children: React.ReactNode;
onClick?: () => void;
}
```
### 2. Input Component
```tsx
interface InputProps {
type?: 'text' | 'email' | 'password' | 'number' | 'tel';
label?: string;
placeholder?: string;
value?: string;
error?: string;
helpText?: string;
required?: boolean;
disabled?: boolean;
onChange?: (value: string) => void;
}
export const Input: React.FC<InputProps> = ({
type = 'text',
label,
placeholder,
value,
error,
helpText,
required,
disabled,
onChange,
}) => {
const id = useId();
const errorId = `${id}-error`;
const helpId = `${id}-help`;
return (
<div className="input-wrapper">
{label && (
<label htmlFor={id} className="input-label">
{label}
{required && <span aria-label="required">*</span>}
</label>
)}
<input
id={id}
type={type}
className={`input ${error ? 'input--error' : ''}`}
placeholder={placeholder}
value={value}
disabled={disabled}
required={required}
aria-invalid={!!error}
aria-describedby={error ? errorId : helpText ? helpId : undefined}
onChange={(e) => onChange?.(e.target.value)}
/>
{error && (
<span id={errorId} className="input-error" role="alert">
{error}
</span>
)}
{helpText && !error && (
<span id={helpId} className="input-help">
{helpText}
</span>
)}
</div>
);
};
```
### 3. Card Component
```tsx
interface CardProps {
variant?: 'default' | 'outlined' | 'elevated';
padding?: 'none' | 'sm' | 'md' | 'lg';
clickable?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
export const Card: React.FC<CardProps> = ({
variant = 'default',
padding = 'md',
clickable = false,
children,
onClick,
}) => {
const Component = clickable ? 'button' : 'div';
return (
<Component
className={`card card--${variant} card--padding-${padding}`}
onClick={onClick}
role={clickable ? 'button' : undefined}
tabIndex={clickable ? 0 : undefined}
>
{children}
</Component>
);
};
```
### 4. Modal Component
```tsx
interface ModalProps {
open: boolean;
title?: string;
size?: 'sm' | 'md' | 'lg' | 'full';
children: React.ReactNode;
onClose: () => void;
}
export const Modal: React.FC<ModalProps> = ({
open,
title,
size = 'md',
children,
onClose,
}) => {
useEffect(() => {
if (open) {
// Trap focus in modal
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = '';
};
}
}, [open]);
if (!open) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div
className={`modal modal--${size}`}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby={title ? 'modal-title' : undefined}
>
{title && (
<div className="modal__header">
<h2 id="modal-title">{title}</h2>
<button
className="modal__close"
onClick={onClose}
aria-label="Close dialog"
>
×
</button>
</div>
)}
<div className="modal__body">{children}</div>
</div>
</div>,
document.body
);
};
```
### 5. Dropdown Component
```tsx
interface DropdownProps {
trigger: React.ReactNode;
children: React.ReactNode;
align?: 'left' | 'right';
}
export const Dropdown: React.FC<DropdownProps> = ({
trigger,
children,
align = 'left',
}) => {
const [open, setOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="dropdown" ref={dropdownRef}>
<button
className="dropdown__trigger"
onClick={() => setOpen(!open)}
aria-expanded={open}
aria-haspopup="true"
>
{trigger}
</button>
{open && (
<div className={`dropdown__menu dropdown__menu--${align}`} role="menu">
{children}
</div>
)}
</div>
);
};
```
## Component Patterns
### 1. Compound Components
**Allow components to work together while sharing implicit state.**
```tsx
// Context for shared state
const AccordionContext = createContext<{
activeIndex: number | null;
setActiveIndex: (index: number | null) => void;
} | null>(null);
// Parent component
export const Accordion: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
return (
<AccordionContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
};
// Child component
export const AccordionItem: React.FC<{
index: number;
title: string;
children: React.ReactNode;
}> = ({ index, title, children }) => {
const context = useContext(AccordionContext);
if (!context) throw new Error('AccordionItem must be used within Accordion');
const { activeIndex, setActiveIndex } = context;
const isOpen = activeIndex === index;
return (
<div className="accordion-item">
<button
className="accordion-item__header"
onClick={() => setActiveIndex(isOpen ? null : index)}
aria-expanded={isOpen}
>
{title}
</button>
{isOpen && <div className="accordion-item__content">{children}</div>}
</div>
);
};
// Usage
<Accordion>
<AccordionItem index={0} title="Section 1">Content 1</AccordionItem>
<AccordionItem index={1} title="Section 2">Content 2</AccordionItem>
</Accordion>
```
### 2. Render Props
**Pass rendering logic as a prop.**
```tsx
interface DataFetcherProps<T> {
url: string;
children: (data: {
data: T | null;
loading: boolean;
error: Error | null;
}) => React.ReactNode;
}
export function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return <>{children({ data, loading, error })}</>;
}
// Usage
<DataFetcher<User> url="/api/user">
{({ data, loading, error }) => {
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!data) return null;
return <UserProfile user={data} />;
}}
</DataFetcher>
```
### 3. Custom Hooks Pattern
**Extract reusable logic into custom hooks.**
```tsx
// useToggle hook
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// Usage in component
export const Modal: React.FC<ModalProps> = ({ children }) => {
const { value: isOpen, setTrue: open, setFalse: close } = useToggle();
return (
<>
<button onClick={open}>Open Modal</button>
{isOpen && (
<div className="modal">
{children}
<button onClick={close}>Close</button>
</div>
)}
</>
);
};
```
## Styling Strategies
### 1. CSS Modules
```css
/* Button.module.css */
.button {
display: inline-flex;
align-items: center;
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
}
.button--primary {
background: var(--color-primary);
color: var(--color-white);
}
.button--secondary {
background: transparent;
color: var(--color-primary);
border: 1px solid var(--color-primary);
}
```
```tsx
import styles from './Button.module.css';
export const Button = ({ variant }) => (
<button className={`${styles.button} ${styles[`button--${variant}`]}`}>
Click me
</button>
);
```
### 2. CSS-in-JS (Styled Components)
```tsx
import styled from 'styled-components';
const StyledButton = styled.button<{ variant: string }>`
display: inline-flex;
align-items: center;
padding: ${({ theme }) => theme.space[3]} ${({ theme }) => theme.space[4]};
background: ${({ theme, variant }) =>
variant === 'primary' ? theme.colors.primary : 'transparent'};
color: ${({ theme, variant }) =>
variant === 'primary' ? theme.colors.white : theme.colors.primary};
`;
export const Button = ({ variant, children }) => (
<StyledButton variant={variant}>{children}</StyledButton>
);
```
### 3. Tailwind CSS
```tsx
import clsx from 'clsx';
export const Button: React.FC<ButtonProps> = ({ variant, size, children }) => {
return (
<button
className={clsx(
'inline-flex items-center rounded-md font-medium',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-transparent text-blue-600 border border-blue-600': variant === 'secondary',
},
{
'px-3 py-2 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
}
)}
>
{children}
</button>
);
};
```
## Documentation
### Component Documentation Template
```tsx
/**
* Button component for triggering actions and navigation.
*
* @example
* ```tsx
* <Button variant="primary" onClick={handleClick}>
* Click me
* </Button>
* ```
*
* @see {@link https://design-system.example.com/button | Design System Docs}
*/
export const Button: React.FC<ButtonProps> = ({ ... }) => {
// Implementation
};
```
### Storybook Documentation
```tsx
import type { Meta } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
docs: {
description: {
component: `
Button component for triggering actions.
## Usage
\`\`\`tsx
import { Button } from '@/components/Button';
<Button variant="primary">Click me</Button>
\`\`\`
## Accessibility
- Keyboard accessible
- Screen reader friendly
- WCAG 2.1 AA compliant
`,
},
},
},
};
```
## Testing Strategies
### Unit Tests
```tsx
describe('Button', () => {
it('renders with correct variant', () => {
render(<Button variant="primary">Test</Button>);
expect(screen.getByRole('button')).toHaveClass('button--primary');
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Test</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
```
### Accessibility Tests
```tsx
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('has no accessibility violations', async () => {
const { container } = render(<Button>Test</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
```
### Visual Regression Tests
```tsx
// Using Chromatic or Percy
it('matches snapshot', () => {
const { container } = render(<Button variant="primary">Test</Button>);
expect(container).toMatchSnapshot();
});
```
## Performance Optimization
### Code Splitting
```tsx
// Lazy load heavy components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
export const App = () => (
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>
);
```
### Memoization
```tsx
// Memoize expensive components
export const ExpensiveComponent = memo(({ data }) => {
// Expensive rendering logic
return <div>{processData(data)}</div>;
});
// Memoize callbacks
const handleClick = useCallback(() => {
// Handle click
}, [dependencies]);
// Memoize values
const expensiveValue = useMemo(() => {
return computeExpensiveValue(data);
}, [data]);
```
## Version Control
### Semantic Versioning
```
MAJOR.MINOR.PATCH
1.0.0 → Initial release
1.1.0 → New feature (backwards compatible)
1.1.1 → Bug fix (backwards compatible)
2.0.0 → Breaking change
```
### Changelog
```markdown
# Changelog
## [2.0.0] - 2024-01-15
### Breaking Changes
- Removed `type` prop from Button (use `variant` instead)
### Added
- New `loading` state for Button
- Icon support in Button component
### Fixed
- Button focus indicator contrast ratio
## [1.1.0] - 2024-01-01
### Added
- New Input component
- Card component variants
```
## Resources
- [React Component Patterns](https://kentcdodds.com/blog/compound-components-with-react-hooks)
- [Design Systems Handbook](https://www.designbetter.co/design-systems-handbook)
- [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/)
- [Storybook Best Practices](https://storybook.js.org/docs/react/writing-docs/introduction)
---
**"Good components are reusable, accessible, and well-documented."**

View File

@@ -0,0 +1,608 @@
# Design Tokens Reference
Complete guide to implementing and using design tokens in your design system.
## What Are Design Tokens?
Design tokens are the visual design atoms of your design system — specifically, they are named entities that store visual design attributes. They're used in place of hard-coded values to maintain a scalable and consistent visual system.
## Benefits
**Consistency**
- Single source of truth for design values
- Ensures brand consistency across platforms
- Easy to maintain and update
**Scalability**
- Change values in one place, update everywhere
- Support multiple themes (light/dark)
- Platform-agnostic (web, iOS, Android)
**Developer-Designer Communication**
- Shared vocabulary between teams
- Design decisions are codified
- Reduces ambiguity
## Token Categories
### 1. Color Tokens
```css
:root {
/* Brand Colors */
--color-primary: #0066FF;
--color-primary-hover: #0052CC;
--color-primary-active: #003D99;
--color-primary-subtle: #E6F0FF;
--color-secondary: #6B7280;
--color-secondary-hover: #4B5563;
/* Semantic Colors */
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
/* Neutral Palette */
--color-white: #FFFFFF;
--color-black: #000000;
--color-gray-50: #F9FAFB;
--color-gray-100: #F3F4F6;
/* ... through gray-900 */
/* Surface Colors */
--color-surface: var(--color-white);
--color-background: var(--color-white);
/* Text Colors */
--color-text: var(--color-gray-900);
--color-text-secondary: var(--color-gray-600);
--color-text-tertiary: var(--color-gray-400);
/* Border Colors */
--color-border: var(--color-gray-200);
--color-border-hover: var(--color-gray-300);
/* Focus */
--color-focus: var(--color-primary);
}
```
**Naming Convention:**
```
--color-{category}-{variant}-{state}
Examples:
--color-primary (base brand color)
--color-primary-hover (interactive state)
--color-success-subtle (background usage)
--color-text-secondary (content hierarchy)
```
### 2. Typography Tokens
```css
:root {
/* Font Families */
--font-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-heading: var(--font-base);
--font-mono: "SF Mono", Monaco, "Cascadia Code", monospace;
/* Font Sizes - Fluid Typography */
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--text-sm: clamp(0.875rem, 0.825rem + 0.25vw, 1rem);
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--text-lg: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem);
--text-xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
--text-2xl: clamp(1.5rem, 1.35rem + 0.75vw, 1.875rem);
--text-3xl: clamp(1.875rem, 1.65rem + 1.125vw, 2.25rem);
--text-4xl: clamp(2.25rem, 1.95rem + 1.5vw, 3rem);
--text-5xl: clamp(3rem, 2.55rem + 2.25vw, 3.75rem);
/* Font Weights */
--font-light: 300;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
/* Line Heights */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
--leading-loose: 2;
/* Letter Spacing */
--tracking-tight: -0.025em;
--tracking-normal: 0;
--tracking-wide: 0.025em;
}
```
**Usage:**
```css
.heading-1 {
font-family: var(--font-heading);
font-size: var(--text-4xl);
font-weight: var(--font-bold);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
}
.body-text {
font-family: var(--font-base);
font-size: var(--text-base);
font-weight: var(--font-normal);
line-height: var(--leading-normal);
}
```
### 3. Spacing Tokens
```css
:root {
/* Spacing Scale (4px base) */
--space-0: 0;
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-20: 5rem; /* 80px */
--space-24: 6rem; /* 96px */
}
```
**Usage:**
```css
.card {
padding: var(--space-4);
margin-bottom: var(--space-6);
gap: var(--space-3);
}
.section {
padding-block: var(--space-12);
}
```
### 4. Shadow Tokens
```css
:root {
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
/* Special Shadows */
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
--shadow-focus: 0 0 0 3px rgba(0, 102, 255, 0.5);
}
```
**Usage:**
```css
.card {
box-shadow: var(--shadow-md);
}
.card:hover {
box-shadow: var(--shadow-lg);
}
button:focus-visible {
box-shadow: var(--shadow-focus);
}
```
### 5. Border Tokens
```css
:root {
/* Border Radius */
--radius-sm: 0.125rem; /* 2px */
--radius-md: 0.375rem; /* 6px */
--radius-lg: 0.5rem; /* 8px */
--radius-xl: 0.75rem; /* 12px */
--radius-2xl: 1rem; /* 16px */
--radius-full: 9999px; /* Pills/circles */
/* Border Widths */
--border-1: 1px;
--border-2: 2px;
--border-4: 4px;
}
```
**Usage:**
```css
.button {
border-radius: var(--radius-md);
border: var(--border-1) solid var(--color-border);
}
.avatar {
border-radius: var(--radius-full);
}
```
### 6. Z-Index Tokens
```css
:root {
/* Layer Management */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
```
**Usage:**
```css
.modal-backdrop {
z-index: var(--z-modal-backdrop);
}
.modal {
z-index: var(--z-modal);
}
```
### 7. Animation/Transition Tokens
```css
:root {
/* Duration */
--duration-instant: 0ms;
--duration-fast: 150ms;
--duration-base: 250ms;
--duration-slow: 350ms;
--duration-slower: 500ms;
/* Easing */
--ease-linear: linear;
--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);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
```
**Usage:**
```css
.button {
transition: all var(--duration-base) var(--ease-out);
}
.modal {
animation: slideIn var(--duration-slow) var(--ease-out);
}
```
## Semantic vs Reference Tokens
### Reference Tokens (Raw Values)
```css
:root {
--blue-500: #0066FF;
--gray-100: #F3F4F6;
--spacing-4: 1rem;
}
```
### Semantic Tokens (Meaningful Names)
```css
:root {
--color-primary: var(--blue-500);
--color-surface: var(--gray-100);
--button-padding: var(--spacing-4);
}
```
**Always use semantic tokens in components:**
```css
/* ❌ Don't */
.button {
background: var(--blue-500);
padding: var(--spacing-4);
}
/* ✅ Do */
.button {
background: var(--color-primary);
padding: var(--button-padding);
}
```
## Theming with Tokens
### Light/Dark Mode
```css
:root {
/* Light theme (default) */
--color-surface: #FFFFFF;
--color-text: #111827;
--color-border: #E5E7EB;
}
@media (prefers-color-scheme: dark) {
:root {
--color-surface: #1F2937;
--color-text: #F9FAFB;
--color-border: #4B5563;
}
}
/* Or class-based theming */
[data-theme="dark"] {
--color-surface: #1F2937;
--color-text: #F9FAFB;
--color-border: #4B5563;
}
```
### Brand Theming
```css
:root {
--color-primary: var(--brand-blue);
}
[data-brand="tech"] {
--brand-blue: #0066FF;
}
[data-brand="health"] {
--brand-blue: #10B981;
}
[data-brand="finance"] {
--brand-blue: #6366F1;
}
```
## Component-Specific Tokens
```css
:root {
/* Button */
--button-padding-sm: var(--space-2) var(--space-3);
--button-padding-md: var(--space-3) var(--space-4);
--button-padding-lg: var(--space-4) var(--space-6);
--button-radius: var(--radius-md);
--button-font-weight: var(--font-medium);
/* Input */
--input-height-sm: 36px;
--input-height-md: 44px;
--input-height-lg: 52px;
--input-border-color: var(--color-border);
--input-focus-color: var(--color-primary);
/* Card */
--card-padding: var(--space-6);
--card-radius: var(--radius-lg);
--card-shadow: var(--shadow-md);
}
```
## Platform-Specific Tokens
### CSS Custom Properties (Web)
```css
:root {
--color-primary: #0066FF;
}
.button {
background: var(--color-primary);
}
```
### JSON (React Native, iOS, Android)
```json
{
"color": {
"primary": "#0066FF",
"surface": "#FFFFFF",
"text": "#111827"
},
"spacing": {
"4": 16,
"6": 24,
"8": 32
}
}
```
### JavaScript/TypeScript
```typescript
export const tokens = {
color: {
primary: '#0066FF',
surface: '#FFFFFF',
text: '#111827',
},
spacing: {
4: '1rem',
6: '1.5rem',
8: '2rem',
},
} as const;
// Usage
const Button = styled.button`
background: ${tokens.color.primary};
padding: ${tokens.spacing.4};
`;
```
## Best Practices
### Naming Conventions
**Do:**
- Use descriptive, semantic names
- Follow consistent patterns
- Group related tokens
```css
/* ✅ Good */
--color-primary
--color-primary-hover
--color-primary-subtle
--button-padding-md
--shadow-card
```
**Don't:**
- Use arbitrary or cryptic names
- Mix naming conventions
- Use values in names
```css
/* ❌ Bad */
--blue
--primary-color-hover-state
--padding16px
--btn-pad
```
### Organization
Group tokens by category:
```
tokens/
├── colors.css
├── typography.css
├── spacing.css
├── shadows.css
├── borders.css
├── zindex.css
└── index.css (imports all)
```
### Documentation
Document token usage:
```css
/**
* Primary brand color
* Used for: primary buttons, links, focus states
* Contrast ratio: 4.6:1 (WCAG AA compliant)
*/
--color-primary: #0066FF;
```
### Accessibility
Ensure proper contrast:
```css
:root {
--color-primary: #0066FF;
--color-text-on-primary: #FFFFFF;
/* Contrast ratio: 4.5:1 ✅ */
}
```
### Performance
Use CSS variables efficiently:
```css
/* ✅ Define once, use everywhere */
:root {
--color-primary: #0066FF;
}
/* ❌ Don't redefine in every selector */
.button { --color-primary: #0066FF; }
.link { --color-primary: #0066FF; }
```
## Migration Strategy
### Step 1: Audit Current Values
```bash
# Find all color values
grep -r "#[0-9A-Fa-f]\{6\}" src/
# Find all spacing values
grep -r "padding:\|margin:" src/
```
### Step 2: Create Token Definitions
```css
:root {
/* Extract unique values */
--color-primary: #0066FF;
--space-4: 1rem;
}
```
### Step 3: Replace Hard-Coded Values
```css
/* Before */
.button {
background: #0066FF;
padding: 1rem;
}
/* After */
.button {
background: var(--color-primary);
padding: var(--space-4);
}
```
### Step 4: Test & Validate
- Visual regression testing
- Accessibility testing
- Cross-browser testing
- Dark mode verification
## Tools
**Design Token Tools:**
- Style Dictionary (transforms tokens to multiple formats)
- Theo (Salesforce's token transformer)
- Design Tokens CLI
**Browser DevTools:**
- Chrome: Inspect computed custom properties
- Firefox: CSS Variables viewer
**Example Script:**
```javascript
// Extract all CSS custom properties
const properties = Array.from(document.styleSheets)
.flatMap(sheet => Array.from(sheet.cssRules))
.filter(rule => rule.style)
.flatMap(rule => Array.from(rule.style))
.filter(prop => prop.startsWith('--'));
console.log([...new Set(properties)]);
```
## Resources
- [Design Tokens W3C Spec](https://design-tokens.github.io/community-group/format/)
- [Style Dictionary](https://amzn.github.io/style-dictionary/)
- [Design Tokens in Figma](https://www.figma.com/community/plugin/888356646278934516/Design-Tokens)
---
**"Design tokens are the translation layer between design decisions and code."**

View File

@@ -0,0 +1,839 @@
# Responsive Design Patterns
Modern responsive design patterns and techniques for creating flexible, accessible layouts that work across all devices.
## Mobile-First Approach
Start with mobile design and enhance for larger screens.
### Why Mobile-First?
**Benefits:**
- Forces focus on essential content
- Better performance (smaller base CSS)
- Progressive enhancement mindset
- Easier to add features than remove them
**Basic Pattern:**
```css
/* Base (mobile) styles - no media query */
.container {
width: 100%;
padding: 1rem;
}
/* Tablet and up */
@media (min-width: 768px) {
.container {
padding: 2rem;
}
}
/* Desktop and up */
@media (min-width: 1024px) {
.container {
max-width: 1200px;
margin: 0 auto;
padding: 3rem;
}
}
```
## Breakpoints
### Standard Breakpoints
```css
:root {
/* Mobile first - these are MIN widths */
--breakpoint-sm: 640px; /* Small tablets, large phones */
--breakpoint-md: 768px; /* Tablets */
--breakpoint-lg: 1024px; /* Laptops, desktops */
--breakpoint-xl: 1280px; /* Large desktops */
--breakpoint-2xl: 1536px; /* Extra large screens */
}
/* Usage */
@media (min-width: 768px) {
/* Tablet and up */
}
@media (min-width: 1024px) {
/* Desktop and up */
}
```
### Custom Breakpoints
```css
/* Content-based breakpoints */
@media (min-width: 400px) {
/* When content needs it, not arbitrary device size */
}
/* Prefer rem-based breakpoints for accessibility */
@media (min-width: 48rem) { /* 768px at 16px base */
/* Scales with user's font size preferences */
}
```
### Container Queries (Modern)
```css
/* Respond to container size, not viewport */
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
```
## Responsive Typography
### Fluid Typography
```css
/* Scales smoothly between min and max */
h1 {
font-size: clamp(2rem, 5vw + 1rem, 4rem);
/* Min: 2rem (32px) */
/* Preferred: 5vw + 1rem */
/* Max: 4rem (64px) */
}
/* More examples */
.text-sm {
font-size: clamp(0.875rem, 0.85rem + 0.125vw, 1rem);
}
.text-base {
font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
}
.text-lg {
font-size: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem);
}
```
### Responsive Line Height
```css
body {
/* Tighter on mobile, looser on desktop */
line-height: 1.5;
}
@media (min-width: 768px) {
body {
line-height: 1.6;
}
}
```
## Layout Patterns
### 1. Stack Layout
**Everything stacks vertically on mobile, side-by-side on larger screens.**
```css
.stack {
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (min-width: 768px) {
.stack {
flex-direction: row;
}
}
```
### 2. Sidebar Layout
**Sidebar stacks on mobile, side-by-side on desktop.**
```css
.sidebar-layout {
display: grid;
gap: 2rem;
}
@media (min-width: 768px) {
.sidebar-layout {
grid-template-columns: 250px 1fr;
}
}
/* Flexible sidebar */
@media (min-width: 768px) {
.sidebar-layout--flexible {
grid-template-columns: minmax(200px, 300px) 1fr;
}
}
```
### 3. Grid Layout
**Responsive column count.**
```css
.grid {
display: grid;
gap: 1rem;
/* Mobile: 1 column */
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.grid {
/* Tablet: 2 columns */
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.grid {
/* Desktop: 3-4 columns */
grid-template-columns: repeat(4, 1fr);
}
}
/* Auto-responsive grid (no media queries!) */
.grid-auto {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
/* Creates as many columns as fit, minimum 250px each */
}
```
### 4. Holy Grail Layout
**Classic three-column layout that adapts to mobile.**
```css
.holy-grail {
display: grid;
gap: 1rem;
min-height: 100vh;
/* Mobile: stack everything */
grid-template:
"header" auto
"main" 1fr
"sidebar1" auto
"sidebar2" auto
"footer" auto
/ 1fr;
}
@media (min-width: 768px) {
.holy-grail {
/* Desktop: traditional layout */
grid-template:
"header header header" auto
"sidebar1 main sidebar2" 1fr
"footer footer footer" auto
/ 200px 1fr 200px;
}
}
.header { grid-area: header; }
.sidebar-1 { grid-area: sidebar1; }
.main { grid-area: main; }
.sidebar-2 { grid-area: sidebar2; }
.footer { grid-area: footer; }
```
### 5. Card Layout
**Responsive cards that adapt their internal layout.**
```css
.card {
display: grid;
gap: 1rem;
/* Mobile: stack image and content */
grid-template:
"image" auto
"content" 1fr
/ 1fr;
}
@media (min-width: 640px) {
.card {
/* Tablet+: side-by-side */
grid-template:
"image content" 1fr
/ 200px 1fr;
}
}
.card__image { grid-area: image; }
.card__content { grid-area: content; }
```
### 6. Switcher Pattern
**Switch between horizontal and vertical based on available space.**
```css
.switcher {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.switcher > * {
/* Grow to fill, but switch to vertical when < 400px */
flex-grow: 1;
flex-basis: calc((400px - 100%) * 999);
/* Clever calc that breaks at threshold */
}
```
### 7. Pancake Stack
**Header, main, footer layout that adapts height.**
```css
.pancake {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.header { /* auto height */ }
.main { /* fills available space */ }
.footer { /* auto height */ }
```
## Responsive Images
### 1. Flexible Images
```css
img {
max-width: 100%;
height: auto;
display: block;
}
```
### 2. Art Direction (Different Images per Breakpoint)
```html
<picture>
<source media="(min-width: 1024px)" srcset="large.jpg">
<source media="(min-width: 768px)" srcset="medium.jpg">
<img src="small.jpg" alt="Description">
</picture>
```
### 3. Resolution Switching (Same Image, Different Sizes)
```html
<img
srcset="
small.jpg 400w,
medium.jpg 800w,
large.jpg 1200w
"
sizes="
(min-width: 1024px) 800px,
(min-width: 768px) 600px,
100vw
"
src="medium.jpg"
alt="Description"
>
```
### 4. Background Images
```css
.hero {
background-image: url('small.jpg');
background-size: cover;
background-position: center;
}
@media (min-width: 768px) {
.hero {
background-image: url('medium.jpg');
}
}
@media (min-width: 1024px) {
.hero {
background-image: url('large.jpg');
}
}
@media (min-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) {
.hero {
background-image: url('large@2x.jpg');
}
}
```
### 5. Aspect Ratio
```css
/* Modern aspect ratio */
.image-container {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Fallback for older browsers */
.image-container-fallback {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
}
.image-container-fallback img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
```
## Responsive Navigation
### 1. Mobile Menu (Hamburger)
```html
<nav class="nav">
<div class="nav__brand">Logo</div>
<button class="nav__toggle" aria-expanded="false" aria-controls="nav-menu">
<span class="sr-only">Menu</span>
<span class="hamburger"></span>
</button>
<ul class="nav__menu" id="nav-menu">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
```
```css
.nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
}
/* Mobile: hidden menu */
.nav__menu {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
flex-direction: column;
padding: 1rem;
}
.nav__menu[aria-expanded="true"] {
display: flex;
}
.nav__toggle {
display: block;
}
/* Desktop: visible menu */
@media (min-width: 768px) {
.nav__menu {
display: flex;
position: static;
flex-direction: row;
gap: 2rem;
}
.nav__toggle {
display: none;
}
}
```
### 2. Priority+ Navigation
**Show important items, collapse others into "More" menu.**
```css
.priority-nav {
display: flex;
gap: 1rem;
}
.priority-nav__item {
flex-shrink: 0; /* Don't shrink */
}
.priority-nav__more {
margin-left: auto; /* Push to end */
}
/* Hide items that don't fit */
@media (max-width: 768px) {
.priority-nav__item:nth-child(n+4) {
display: none; /* Hide items 4+ */
}
}
```
## Responsive Tables
### 1. Horizontal Scroll
```css
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
}
table {
min-width: 600px;
width: 100%;
}
```
### 2. Stacked Table (Card View)
```html
<table class="responsive-table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Name">John Doe</td>
<td data-label="Email">john@example.com</td>
<td data-label="Role">Developer</td>
</tr>
</tbody>
</table>
```
```css
/* Desktop: normal table */
@media (min-width: 768px) {
.responsive-table {
display: table;
}
}
/* Mobile: stacked cards */
@media (max-width: 767px) {
.responsive-table,
.responsive-table thead,
.responsive-table tbody,
.responsive-table tr,
.responsive-table th,
.responsive-table td {
display: block;
}
.responsive-table thead {
display: none; /* Hide table header */
}
.responsive-table tr {
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 0.5rem;
padding: 1rem;
}
.responsive-table td {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid #eee;
}
.responsive-table td:last-child {
border-bottom: none;
}
.responsive-table td::before {
content: attr(data-label);
font-weight: bold;
margin-right: 1rem;
}
}
```
## Responsive Forms
### 1. Single Column to Multi-Column
```css
.form {
display: grid;
gap: 1rem;
/* Mobile: single column */
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.form {
/* Desktop: two columns */
grid-template-columns: repeat(2, 1fr);
}
.form__field--full {
/* Some fields span both columns */
grid-column: 1 / -1;
}
}
```
### 2. Touch-Friendly Inputs
```css
input,
button,
select,
textarea {
/* Minimum 44x44px touch target */
min-height: 44px;
padding: 0.75rem 1rem;
font-size: 1rem; /* Prevents zoom on iOS */
}
@media (min-width: 768px) {
input,
button,
select,
textarea {
/* Can be smaller on desktop */
min-height: 40px;
font-size: 0.875rem;
}
}
```
## Responsive Utilities
### Show/Hide at Breakpoints
```css
/* Hide on mobile */
.hidden-mobile {
display: none;
}
@media (min-width: 768px) {
.hidden-mobile {
display: block;
}
}
/* Show only on mobile */
.visible-mobile {
display: block;
}
@media (min-width: 768px) {
.visible-mobile {
display: none;
}
}
/* Hide on desktop */
@media (min-width: 1024px) {
.hidden-desktop {
display: none;
}
}
```
### Responsive Spacing
```css
.section {
/* Mobile: smaller padding */
padding: 2rem 1rem;
}
@media (min-width: 768px) {
.section {
/* Tablet: medium padding */
padding: 4rem 2rem;
}
}
@media (min-width: 1024px) {
.section {
/* Desktop: larger padding */
padding: 6rem 3rem;
}
}
/* Or use fluid spacing */
.section-fluid {
padding: clamp(2rem, 5vw, 6rem) clamp(1rem, 3vw, 3rem);
}
```
## Testing Responsive Design
### Browser DevTools
```javascript
// Common viewport sizes to test
const viewports = [
{ width: 375, height: 667, name: 'iPhone SE' },
{ width: 390, height: 844, name: 'iPhone 12 Pro' },
{ width: 428, height: 926, name: 'iPhone 14 Pro Max' },
{ width: 768, height: 1024, name: 'iPad' },
{ width: 1024, height: 768, name: 'iPad Landscape' },
{ width: 1280, height: 720, name: 'Desktop' },
{ width: 1920, height: 1080, name: 'Full HD' },
];
```
### Responsive Testing Checklist
- [ ] Test all breakpoints
- [ ] Test between breakpoints (awkward sizes)
- [ ] Test portrait and landscape
- [ ] Test zoom levels (100%, 200%, 400%)
- [ ] Test with real devices when possible
- [ ] Test touch interactions on mobile
- [ ] Test with different font sizes
- [ ] Test with slow network (images, fonts)
## Performance Considerations
### Lazy Loading
```html
<!-- Lazy load images below the fold -->
<img src="image.jpg" loading="lazy" alt="Description">
<!-- Eager load above-the-fold images -->
<img src="hero.jpg" loading="eager" alt="Hero image">
```
### Conditional Loading
```javascript
// Load component only on larger screens
if (window.matchMedia('(min-width: 768px)').matches) {
import('./DesktopComponent.js').then(module => {
// Initialize desktop component
});
}
```
### Font Loading
```css
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* Show fallback while loading */
}
```
## Modern CSS Features
### 1. CSS Grid Auto-Fill
```css
.grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
/* Automatically creates columns, minimum 250px */
}
```
### 2. Flexbox Gap
```css
.flex-container {
display: flex;
flex-wrap: wrap;
gap: 1rem; /* No more margin hacks! */
}
```
### 3. Container Queries
```css
.card {
container-type: inline-size;
}
@container (min-width: 400px) {
.card__title {
font-size: 1.5rem;
}
}
```
### 4. Aspect Ratio
```css
.video-container {
aspect-ratio: 16 / 9;
}
```
### 5. Logical Properties
```css
/* Better for RTL/LTR support */
.element {
margin-block-start: 1rem; /* margin-top */
margin-block-end: 1rem; /* margin-bottom */
margin-inline-start: 1rem; /* margin-left in LTR, margin-right in RTL */
margin-inline-end: 1rem; /* margin-right in LTR, margin-left in RTL */
}
```
## Resources
- [MDN: Responsive Design](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Responsive_Design)
- [This is Responsive](https://bradfrost.github.io/this-is-responsive/)
- [Responsive Design Patterns](https://responsivedesign.is/patterns/)
- [CSS-Tricks: Complete Guide to Grid](https://css-tricks.com/snippets/css/complete-guide-grid/)
- [CSS-Tricks: Complete Guide to Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/)
---
**"The best design is the one that works everywhere, for everyone."**

View File

@@ -0,0 +1,467 @@
#!/bin/bash
# Frontend Designer - Accessibility Audit
# Comprehensive WCAG 2.1 AA compliance checker
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
NC='\033[0m' # No Color
# Counters
PASS_COUNT=0
FAIL_COUNT=0
WARNING_COUNT=0
# Helper functions
print_success() {
echo -e "${GREEN}✓ PASS${NC} $1"
((PASS_COUNT++))
}
print_error() {
echo -e "${RED}✗ FAIL${NC} $1"
((FAIL_COUNT++))
}
print_warning() {
echo -e "${YELLOW}⚠ WARN${NC} $1"
((WARNING_COUNT++))
}
print_info() {
echo -e "${BLUE} INFO${NC} $1"
}
print_section() {
echo ""
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${MAGENTA}$1${NC}"
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
}
# Banner
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ Frontend Designer - Accessibility Audit ║"
echo "║ WCAG 2.1 AA Compliance ║"
echo "║ ║"
echo "╚════════════════════════════════════════════════════════════╝"
echo ""
# Get target
if [ -z "$1" ]; then
print_info "Usage: $0 <file.html|directory>"
print_info "Example: $0 index.html"
print_info "Example: $0 src/components/"
exit 1
fi
TARGET="$1"
# Check if target exists
if [ ! -e "$TARGET" ]; then
print_error "Target not found: $TARGET"
exit 1
fi
# Section 1: HTML Structure
print_section "1. HTML STRUCTURE & SEMANTICS"
check_html_lang() {
if grep -q '<html[^>]*\slang=' "$1"; then
print_success "HTML lang attribute present"
else
print_error "Missing lang attribute on <html>"
echo " Fix: <html lang=\"en\">"
fi
}
check_page_title() {
if grep -q '<title>' "$1"; then
print_success "Page title present"
else
print_error "Missing <title> element"
echo " Fix: Add <title>Page Title</title>"
fi
}
check_main_landmark() {
if grep -q '<main' "$1" || grep -q 'role="main"' "$1"; then
print_success "Main landmark present"
else
print_warning "No <main> landmark found"
echo " Tip: Use <main> for primary content"
fi
}
check_heading_structure() {
if grep -q '<h1' "$1"; then
print_success "H1 heading found"
# Check for heading hierarchy
local h1_count=$(grep -o '<h1' "$1" | wc -l)
if [ "$h1_count" -eq 1 ]; then
print_success "Single H1 (recommended)"
else
print_warning "Multiple H1 headings found ($h1_count)"
echo " Tip: Use single H1 per page"
fi
else
print_error "No H1 heading found"
echo " Fix: Add <h1> for main page heading"
fi
}
check_semantic_html() {
local semantic_tags=("nav" "header" "footer" "article" "section" "aside")
local found=false
for tag in "${semantic_tags[@]}"; do
if grep -q "<$tag" "$1"; then
found=true
break
fi
done
if [ "$found" = true ]; then
print_success "Semantic HTML elements used"
else
print_warning "Consider using semantic HTML (nav, header, footer, etc.)"
fi
}
# Section 2: Images & Media
print_section "2. IMAGES & MEDIA"
check_img_alt() {
local img_count=$(grep -o '<img' "$1" | wc -l)
if [ "$img_count" -eq 0 ]; then
print_info "No images found"
else
local alt_count=$(grep '<img' "$1" | grep -c 'alt=')
if [ "$alt_count" -eq "$img_count" ]; then
print_success "All images have alt attributes ($img_count/$img_count)"
else
print_error "Images missing alt attributes ($alt_count/$img_count)"
echo " Fix: Add alt=\"description\" to all images"
echo " For decorative images use alt=\"\""
fi
fi
}
check_video_captions() {
if grep -q '<video' "$1"; then
if grep -q '<track' "$1"; then
print_success "Video has captions/subtitles"
else
print_error "Video missing captions"
echo " Fix: Add <track kind=\"captions\" src=\"captions.vtt\">"
fi
fi
}
# Section 3: Forms
print_section "3. FORMS & INPUTS"
check_form_labels() {
local input_count=$(grep -o '<input' "$1" | wc -l)
if [ "$input_count" -eq 0 ]; then
print_info "No form inputs found"
else
# Check for labels
local label_count=$(grep -o '<label' "$1" | wc -l)
local aria_label_count=$(grep '<input' "$1" | grep -c 'aria-label')
local aria_labelledby_count=$(grep '<input' "$1" | grep -c 'aria-labelledby')
local labeled_count=$((label_count + aria_label_count + aria_labelledby_count))
if [ "$labeled_count" -ge "$input_count" ]; then
print_success "All inputs have labels"
else
print_error "Some inputs missing labels"
echo " Fix: Use <label for=\"id\"> or aria-label"
fi
# Check for required fields
if grep -q 'required' "$1"; then
if grep -q 'aria-required="true"' "$1"; then
print_success "Required fields marked with aria-required"
else
print_warning "Consider adding aria-required=\"true\" to required fields"
fi
fi
fi
}
check_error_messages() {
if grep -q 'aria-describedby' "$1"; then
print_success "Error messages linked with aria-describedby"
elif grep -q 'error' "$1"; then
print_warning "Error handling present, verify aria-describedby usage"
fi
}
# Section 4: Interactive Elements
print_section "4. INTERACTIVE ELEMENTS"
check_button_text() {
# Check for empty buttons
if grep -q '<button[^>]*></button>' "$1"; then
print_error "Empty button found"
echo " Fix: Add text or aria-label to button"
else
print_success "No empty buttons found"
fi
}
check_link_text() {
# Check for generic link text
if grep -qi 'click here\|read more\|more' "$1"; then
print_warning "Generic link text found (click here, read more)"
echo " Tip: Use descriptive link text"
else
print_success "No generic link text detected"
fi
}
check_skip_links() {
if grep -q 'skip.*content\|skip.*navigation' "$1"; then
print_success "Skip navigation link present"
else
print_warning "No skip navigation link found"
echo " Tip: Add skip link for keyboard users"
echo " <a href=\"#main\" class=\"skip-link\">Skip to content</a>"
fi
}
# Section 5: ARIA
print_section "5. ARIA ATTRIBUTES"
check_aria_roles() {
if grep -q 'role=' "$1"; then
print_success "ARIA roles found"
# Check for button roles on non-button elements
if grep -q '<div[^>]*role="button"' "$1" || grep -q '<span[^>]*role="button"' "$1"; then
if grep -q 'tabindex=' "$1"; then
print_success "Custom buttons have tabindex"
else
print_error "role=\"button\" without tabindex"
echo " Fix: Add tabindex=\"0\" to custom buttons"
fi
fi
fi
}
check_aria_labels() {
if grep -q 'aria-label=' "$1"; then
print_success "ARIA labels used for context"
fi
# Check for redundant aria-label
if grep -q '<button[^>]*aria-label.*>[^<]*</button>' "$1"; then
print_warning "Possible redundant aria-label on button with text"
echo " Tip: Use aria-label when button has no visible text"
fi
}
check_aria_live() {
if grep -q 'aria-live' "$1"; then
print_success "Live regions defined"
fi
}
# Section 6: Keyboard Navigation
print_section "6. KEYBOARD NAVIGATION"
check_tabindex() {
# Check for positive tabindex
if grep -q 'tabindex="[1-9]' "$1"; then
print_error "Positive tabindex values found"
echo " Fix: Use tabindex=\"0\" or \"-1\" only"
echo " Positive values disrupt natural tab order"
else
print_success "No positive tabindex values (good)"
fi
}
check_focus_indicators() {
# This would need CSS analysis
print_info "Manual check: Verify focus indicators are visible"
echo " Test: Tab through page, ensure focus is visible"
echo " CSS: :focus-visible { outline: 2px solid; }"
}
# Section 7: Color & Contrast
print_section "7. COLOR & CONTRAST"
print_info "Manual checks required for color/contrast:"
echo ""
echo " Required contrast ratios (WCAG AA):"
echo " ✓ Normal text: 4.5:1"
echo " ✓ Large text (18pt+): 3:1"
echo " ✓ UI components: 3:1"
echo ""
echo " Tools for testing:"
echo " - Chrome DevTools (Lighthouse)"
echo " - WebAIM Contrast Checker"
echo " - axe DevTools"
echo ""
check_color_only() {
if grep -qi 'color:.*red\|color:.*green' "$1"; then
print_warning "Color usage detected - ensure not used as only indicator"
echo " Tip: Don't rely on color alone (add icons, text, patterns)"
fi
}
# Section 8: Responsive & Mobile
print_section "8. RESPONSIVE & MOBILE"
check_viewport() {
if grep -q 'viewport' "$1"; then
print_success "Viewport meta tag present"
else
print_error "Missing viewport meta tag"
echo " Fix: <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
fi
}
check_touch_targets() {
print_info "Manual check: Touch targets minimum 44x44px"
echo " Test: Verify buttons/links meet minimum size"
echo " CSS: min-height: 44px; min-width: 44px;"
}
# Section 9: Content
print_section "9. CONTENT & READABILITY"
check_lang_changes() {
if grep -q '\slang=' "$1"; then
local lang_count=$(grep -o '\slang=' "$1" | wc -l)
if [ "$lang_count" -gt 1 ]; then
print_success "Language changes marked ($lang_count instances)"
fi
fi
}
check_abbreviations() {
if grep -q '<abbr' "$1"; then
print_success "Abbreviations use <abbr> element"
fi
}
# Section 10: Motion & Animation
print_section "10. MOTION & ANIMATIONS"
print_info "Manual check: Respect prefers-reduced-motion"
echo ""
echo " CSS:"
echo " @media (prefers-reduced-motion: reduce) {"
echo " * { animation: none !important; }"
echo " }"
echo ""
# Run checks on files
if [ -f "$TARGET" ]; then
# Single file
check_html_lang "$TARGET"
check_page_title "$TARGET"
check_main_landmark "$TARGET"
check_heading_structure "$TARGET"
check_semantic_html "$TARGET"
check_img_alt "$TARGET"
check_video_captions "$TARGET"
check_form_labels "$TARGET"
check_error_messages "$TARGET"
check_button_text "$TARGET"
check_link_text "$TARGET"
check_skip_links "$TARGET"
check_aria_roles "$TARGET"
check_aria_labels "$TARGET"
check_aria_live "$TARGET"
check_tabindex "$TARGET"
check_focus_indicators "$TARGET"
check_color_only "$TARGET"
check_viewport "$TARGET"
check_touch_targets "$TARGET"
check_lang_changes "$TARGET"
check_abbreviations "$TARGET"
elif [ -d "$TARGET" ]; then
# Directory - find HTML files
html_files=$(find "$TARGET" -name "*.html" -o -name "*.htm")
if [ -z "$html_files" ]; then
print_error "No HTML files found in $TARGET"
exit 1
fi
for file in $html_files; do
print_info "Checking: $file"
check_html_lang "$file"
check_page_title "$file"
check_main_landmark "$file"
check_heading_structure "$file"
check_img_alt "$file"
check_form_labels "$file"
echo ""
done
fi
# Summary
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ Audit Summary ║"
echo "╚════════════════════════════════════════════════════════════╝"
echo ""
echo -e "${GREEN}✓ Passed: $PASS_COUNT${NC}"
echo -e "${RED}✗ Failed: $FAIL_COUNT${NC}"
echo -e "${YELLOW}⚠ Warnings: $WARNING_COUNT${NC}"
echo ""
# Calculate score
TOTAL=$((PASS_COUNT + FAIL_COUNT))
if [ $TOTAL -gt 0 ]; then
SCORE=$(( (PASS_COUNT * 100) / TOTAL ))
echo "Score: $SCORE%"
echo ""
if [ $SCORE -ge 90 ]; then
echo -e "${GREEN}Excellent! Your site is highly accessible.${NC}"
elif [ $SCORE -ge 70 ]; then
echo -e "${YELLOW}Good, but needs improvements.${NC}"
else
echo -e "${RED}Needs significant accessibility improvements.${NC}"
fi
fi
echo ""
print_info "Additional Testing Recommended:"
echo " 1. Screen reader testing (NVDA, JAWS, VoiceOver)"
echo " 2. Keyboard-only navigation"
echo " 3. Automated tools (axe, Lighthouse, WAVE)"
echo " 4. Color contrast analyzer"
echo " 5. Real user testing with assistive technologies"
echo ""
print_info "Resources:"
echo " - WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/"
echo " - WebAIM: https://webaim.org/"
echo " - a11y Project: https://www.a11yproject.com/"
echo ""
# Exit code based on failures
if [ $FAIL_COUNT -gt 0 ]; then
exit 1
else
exit 0
fi

View File

@@ -0,0 +1,619 @@
#!/bin/bash
# Frontend Designer - Component Generator
# Generates accessible, responsive components with design tokens
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
prompt_input() {
local prompt="$1"
local var_name="$2"
local required="${3:-false}"
while true; do
echo -e "${BLUE}${prompt}${NC}"
read -r input
if [ -z "$input" ] && [ "$required" = true ]; then
print_error "This field is required."
continue
fi
eval "$var_name='$input'"
break
done
}
prompt_select() {
local prompt="$1"
local var_name="$2"
shift 2
local options=("$@")
echo -e "${BLUE}${prompt}${NC}"
PS3="Select (1-${#options[@]}): "
select opt in "${options[@]}"; do
if [ -n "$opt" ]; then
eval "$var_name='$opt'"
break
else
print_error "Invalid selection. Try again."
fi
done
}
# Banner
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ Frontend Designer - Component Generator ║"
echo "║ ║"
echo "╚════════════════════════════════════════════════════════════╝"
echo ""
# Step 1: Component Type
print_info "Step 1/6: Component Type"
prompt_select "What type of component?" COMPONENT_TYPE \
"Button" \
"Input" \
"Card" \
"Modal" \
"Dropdown" \
"Navigation" \
"Form" \
"List" \
"Custom"
# Step 2: Component Name
print_info "Step 2/6: Component Name"
prompt_input "Component name (PascalCase, e.g., UserProfile):" COMPONENT_NAME true
# Step 3: Framework
print_info "Step 3/6: Framework"
prompt_select "Which framework?" FRAMEWORK \
"React" \
"Vue" \
"Vanilla JS" \
"Web Components"
# Step 4: Features
print_info "Step 4/6: Features (comma-separated)"
echo -e "${BLUE}Select features to include (e.g., variants,loading,disabled):${NC}"
echo " - variants (different visual styles)"
echo " - sizes (sm, md, lg)"
echo " - loading (loading state)"
echo " - disabled (disabled state)"
echo " - icons (icon support)"
echo " - responsive (responsive behavior)"
read -r FEATURES
# Step 5: Accessibility
print_info "Step 5/6: Accessibility Requirements"
prompt_select "WCAG compliance level?" A11Y_LEVEL \
"AA (recommended)" \
"AAA (strict)" \
"Basic"
# Step 6: Output Directory
print_info "Step 6/6: Output Location"
prompt_input "Output directory (default: ./components):" OUTPUT_DIR
OUTPUT_DIR=${OUTPUT_DIR:-"./components"}
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Generate based on framework
case $FRAMEWORK in
"React")
generate_react_component
;;
"Vue")
generate_vue_component
;;
"Vanilla JS")
generate_vanilla_component
;;
"Web Components")
generate_web_component
;;
esac
# Generate component based on selected framework
generate_react_component() {
local file_path="$OUTPUT_DIR/$COMPONENT_NAME.tsx"
cat > "$file_path" << 'EOF'
import React from 'react';
import './COMPONENT_NAME.css';
interface COMPONENT_NAMEProps {
children?: React.ReactNode;
className?: string;
VARIANT_PROP
SIZE_PROP
DISABLED_PROP
LOADING_PROP
onClick?: () => void;
}
export const COMPONENT_NAME: React.FC<COMPONENT_NAMEProps> = ({
children,
className = '',
VARIANT_DEFAULT
SIZE_DEFAULT
DISABLED_DEFAULT
LOADING_DEFAULT
onClick,
}) => {
const baseClass = 'COMPONENT_CLASS';
const variantClass = `${baseClass}--${variant}`;
const sizeClass = `${baseClass}--${size}`;
const classes = `${baseClass} ${variantClass} ${sizeClass} ${className}`;
return (
<COMPONENT_ELEMENT
className={classes}
onClick={onClick}
disabled={disabled || loading}
aria-busy={loading}
ARIA_ATTRIBUTES
>
LOADING_SPINNER
{children}
</COMPONENT_ELEMENT>
);
};
EOF
# Replace placeholders based on features
sed -i "s/COMPONENT_NAME/$COMPONENT_NAME/g" "$file_path"
sed -i "s/COMPONENT_CLASS/$(echo "$COMPONENT_NAME" | sed 's/\([A-Z]\)/-\L\1/g' | sed 's/^-//')/g" "$file_path"
if [[ $FEATURES == *"variants"* ]]; then
sed -i "s/VARIANT_PROP/variant?: 'primary' | 'secondary' | 'ghost';/" "$file_path"
sed -i "s/VARIANT_DEFAULT/variant = 'primary',/" "$file_path"
else
sed -i "/VARIANT_PROP/d" "$file_path"
sed -i "/VARIANT_DEFAULT/d" "$file_path"
fi
if [[ $FEATURES == *"sizes"* ]]; then
sed -i "s/SIZE_PROP/size?: 'sm' | 'md' | 'lg';/" "$file_path"
sed -i "s/SIZE_DEFAULT/size = 'md',/" "$file_path"
else
sed -i "/SIZE_PROP/d" "$file_path"
sed -i "/SIZE_DEFAULT/d" "$file_path"
fi
if [[ $FEATURES == *"disabled"* ]]; then
sed -i "s/DISABLED_PROP/disabled?: boolean;/" "$file_path"
sed -i "s/DISABLED_DEFAULT/disabled = false,/" "$file_path"
else
sed -i "/DISABLED_PROP/d" "$file_path"
sed -i "/DISABLED_DEFAULT/d" "$file_path"
fi
if [[ $FEATURES == *"loading"* ]]; then
sed -i "s/LOADING_PROP/loading?: boolean;/" "$file_path"
sed -i "s/LOADING_DEFAULT/loading = false,/" "$file_path"
sed -i "s|LOADING_SPINNER|{loading \&\& <span className=\"spinner\" aria-hidden=\"true\" />}|" "$file_path"
else
sed -i "/LOADING_PROP/d" "$file_path"
sed -i "/LOADING_DEFAULT/d" "$file_path"
sed -i "/LOADING_SPINNER/d" "$file_path"
fi
# Determine element type
case $COMPONENT_TYPE in
"Button")
sed -i "s/COMPONENT_ELEMENT/button/" "$file_path"
sed -i "s/ARIA_ATTRIBUTES//" "$file_path"
;;
"Input")
sed -i "s/COMPONENT_ELEMENT/input/" "$file_path"
sed -i "s/ARIA_ATTRIBUTES/aria-label=\"\" aria-describedby=\"\"/" "$file_path"
;;
"Card")
sed -i "s/COMPONENT_ELEMENT/div/" "$file_path"
sed -i "s/ARIA_ATTRIBUTES/role=\"article\"/" "$file_path"
;;
*)
sed -i "s/COMPONENT_ELEMENT/div/" "$file_path"
sed -i "s/ARIA_ATTRIBUTES//" "$file_path"
;;
esac
print_success "Created React component: $file_path"
generate_css
generate_test_file
}
generate_vue_component() {
local file_path="$OUTPUT_DIR/$COMPONENT_NAME.vue"
cat > "$file_path" << 'EOF'
<template>
<COMPONENT_ELEMENT
:class="classes"
@click="onClick"
:disabled="disabled || loading"
:aria-busy="loading"
>
<span v-if="loading" class="spinner" aria-hidden="true"></span>
<slot></slot>
</COMPONENT_ELEMENT>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
disabled: false,
loading: false,
});
const emit = defineEmits<{
click: [];
}>();
const baseClass = 'COMPONENT_CLASS';
const classes = computed(() => [
baseClass,
`${baseClass}--${props.variant}`,
`${baseClass}--${props.size}`,
]);
const onClick = () => {
if (!props.disabled && !props.loading) {
emit('click');
}
};
</script>
<style scoped>
@import './COMPONENT_NAME.css';
</style>
EOF
sed -i "s/COMPONENT_NAME/$COMPONENT_NAME/g" "$file_path"
sed -i "s/COMPONENT_CLASS/$(echo "$COMPONENT_NAME" | sed 's/\([A-Z]\)/-\L\1/g' | sed 's/^-//')/g" "$file_path"
case $COMPONENT_TYPE in
"Button")
sed -i "s/COMPONENT_ELEMENT/button/" "$file_path"
;;
"Input")
sed -i "s/COMPONENT_ELEMENT/input/" "$file_path"
;;
*)
sed -i "s/COMPONENT_ELEMENT/div/" "$file_path"
;;
esac
print_success "Created Vue component: $file_path"
generate_css
}
generate_css() {
local css_file="$OUTPUT_DIR/$COMPONENT_NAME.css"
local class_name=$(echo "$COMPONENT_NAME" | sed 's/\([A-Z]\)/-\L\1/g' | sed 's/^-//')
cat > "$css_file" << EOF
/* $COMPONENT_NAME Component Styles */
.$class_name {
/* Design Tokens */
--component-bg: var(--color-surface);
--component-text: var(--color-text);
--component-border: var(--color-border);
--component-radius: var(--radius-md);
--component-shadow: var(--shadow-sm);
/* Base Styles */
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background-color: var(--component-bg);
color: var(--component-text);
border: 1px solid var(--component-border);
border-radius: var(--component-radius);
font-family: var(--font-base);
font-size: var(--text-base);
font-weight: 500;
line-height: 1.5;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
/* Accessibility */
min-height: 44px; /* WCAG touch target */
min-width: 44px;
}
/* Variants */
.$class_name--primary {
--component-bg: var(--color-primary);
--component-text: var(--color-white);
--component-border: var(--color-primary);
}
.$class_name--primary:hover:not(:disabled) {
--component-bg: var(--color-primary-hover);
--component-border: var(--color-primary-hover);
box-shadow: var(--shadow-md);
}
.$class_name--secondary {
--component-bg: transparent;
--component-text: var(--color-primary);
--component-border: var(--color-primary);
}
.$class_name--secondary:hover:not(:disabled) {
--component-bg: var(--color-primary-subtle);
}
.$class_name--ghost {
--component-bg: transparent;
--component-text: var(--color-text);
--component-border: transparent;
}
.$class_name--ghost:hover:not(:disabled) {
--component-bg: var(--color-surface-hover);
}
/* Sizes */
.$class_name--sm {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
min-height: 36px;
}
.$class_name--md {
padding: var(--space-3) var(--space-4);
font-size: var(--text-base);
min-height: 44px;
}
.$class_name--lg {
padding: var(--space-4) var(--space-6);
font-size: var(--text-lg);
min-height: 52px;
}
/* States */
.$class_name:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
.$class_name:active:not(:disabled) {
transform: scale(0.98);
}
.$class_name:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.$class_name[aria-busy="true"] {
cursor: wait;
}
/* Loading Spinner */
.spinner {
display: inline-block;
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Responsive */
@media (max-width: 768px) {
.$class_name {
width: 100%;
}
}
/* Dark Mode */
@media (prefers-color-scheme: dark) {
.$class_name {
--component-bg: var(--color-surface-dark);
--component-text: var(--color-text-dark);
--component-border: var(--color-border-dark);
}
}
/* High Contrast Mode */
@media (prefers-contrast: high) {
.$class_name {
border-width: 2px;
}
.$class_name:focus-visible {
outline-width: 3px;
}
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
.$class_name,
.spinner {
animation: none;
transition: none;
}
}
EOF
print_success "Created CSS file: $css_file"
}
generate_test_file() {
if [ "$FRAMEWORK" != "React" ]; then
return
fi
local test_file="$OUTPUT_DIR/$COMPONENT_NAME.test.tsx"
cat > "$test_file" << 'EOF'
import { render, screen, fireEvent } from '@testing-library/react';
import { COMPONENT_NAME } from './COMPONENT_NAME';
describe('COMPONENT_NAME', () => {
it('renders children correctly', () => {
render(<COMPONENT_NAME>Click me</COMPONENT_NAME>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<COMPONENT_NAME onClick={handleClick}>Click me</COMPONENT_NAME>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('renders with different variants', () => {
const { rerender } = render(<COMPONENT_NAME variant="primary">Primary</COMPONENT_NAME>);
expect(screen.getByText('Primary')).toHaveClass('COMPONENT_CLASS--primary');
rerender(<COMPONENT_NAME variant="secondary">Secondary</COMPONENT_NAME>);
expect(screen.getByText('Secondary')).toHaveClass('COMPONENT_CLASS--secondary');
});
it('renders with different sizes', () => {
const { rerender } = render(<COMPONENT_NAME size="sm">Small</COMPONENT_NAME>);
expect(screen.getByText('Small')).toHaveClass('COMPONENT_CLASS--sm');
rerender(<COMPONENT_NAME size="lg">Large</COMPONENT_NAME>);
expect(screen.getByText('Large')).toHaveClass('COMPONENT_CLASS--lg');
});
it('disables interaction when disabled', () => {
const handleClick = jest.fn();
render(<COMPONENT_NAME disabled onClick={handleClick}>Disabled</COMPONENT_NAME>);
const element = screen.getByText('Disabled');
expect(element).toBeDisabled();
fireEvent.click(element);
expect(handleClick).not.toHaveBeenCalled();
});
it('shows loading state', () => {
render(<COMPONENT_NAME loading>Loading</COMPONENT_NAME>);
const element = screen.getByText('Loading');
expect(element).toHaveAttribute('aria-busy', 'true');
expect(element).toBeDisabled();
});
it('is keyboard accessible', () => {
const handleClick = jest.fn();
render(<COMPONENT_NAME onClick={handleClick}>Accessible</COMPONENT_NAME>);
const element = screen.getByText('Accessible');
element.focus();
expect(element).toHaveFocus();
});
it('has proper ARIA attributes', () => {
render(<COMPONENT_NAME loading>ARIA Test</COMPONENT_NAME>);
const element = screen.getByText('ARIA Test');
expect(element).toHaveAttribute('aria-busy', 'true');
});
});
EOF
sed -i "s/COMPONENT_NAME/$COMPONENT_NAME/g" "$test_file"
sed -i "s/COMPONENT_CLASS/$(echo "$COMPONENT_NAME" | sed 's/\([A-Z]\)/-\L\1/g' | sed 's/^-//')/g" "$test_file"
print_success "Created test file: $test_file"
}
# Summary
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ Generation Complete ║"
echo "╚════════════════════════════════════════════════════════════╝"
echo ""
print_success "Component: $COMPONENT_NAME"
print_success "Type: $COMPONENT_TYPE"
print_success "Framework: $FRAMEWORK"
print_success "Location: $OUTPUT_DIR"
echo ""
print_info "Files created:"
case $FRAMEWORK in
"React")
echo " - $COMPONENT_NAME.tsx (component)"
echo " - $COMPONENT_NAME.css (styles)"
echo " - $COMPONENT_NAME.test.tsx (tests)"
;;
"Vue")
echo " - $COMPONENT_NAME.vue (component)"
echo " - $COMPONENT_NAME.css (styles)"
;;
*)
echo " - $COMPONENT_NAME.js (component)"
echo " - $COMPONENT_NAME.css (styles)"
;;
esac
echo ""
print_info "Next steps:"
echo " 1. Review generated files"
echo " 2. Customize component logic"
echo " 3. Add to your component library"
echo " 4. Run tests (npm test)"
echo " 5. Test accessibility (npm run a11y)"
echo ""

View File

@@ -0,0 +1,570 @@
#!/bin/bash
# Frontend Designer - Design System Setup
# Initialize a complete design system with tokens, components, and documentation
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
prompt_input() {
local prompt="$1"
local var_name="$2"
local default="${3:-}"
echo -e "${BLUE}${prompt}${NC}"
if [ -n "$default" ]; then
echo -e "${YELLOW}(default: $default)${NC}"
fi
read -r input
if [ -z "$input" ]; then
input="$default"
fi
eval "$var_name='$input'"
}
prompt_confirm() {
local prompt="$1"
echo -e "${BLUE}${prompt} (y/n):${NC}"
read -r response
[[ "$response" =~ ^[Yy]$ ]]
}
# Banner
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ Frontend Designer - Design System Setup ║"
echo "║ ║"
echo "╚════════════════════════════════════════════════════════════╝"
echo ""
# Configuration
print_info "Step 1/5: Project Configuration"
prompt_input "Design system name:" DS_NAME "design-system"
prompt_input "Primary brand color (hex):" PRIMARY_COLOR "#0066FF"
prompt_input "Base font size (px):" BASE_FONT_SIZE "16"
prompt_input "Output directory:" OUTPUT_DIR "./design-system"
# Create directory structure
print_info "Step 2/5: Creating Directory Structure"
mkdir -p "$OUTPUT_DIR"/{tokens,components,utilities,docs,examples}
print_success "Created directory structure"
# Generate design tokens
print_info "Step 3/5: Generating Design Tokens"
generate_design_tokens
# Generate base components
print_info "Step 4/5: Generating Base Components"
if prompt_confirm "Generate base component library?"; then
generate_base_components
fi
# Generate documentation
print_info "Step 5/5: Generating Documentation"
generate_documentation
# Summary
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ Setup Complete! ║"
echo "╚════════════════════════════════════════════════════════════╝"
echo ""
print_success "Design system created: $OUTPUT_DIR"
echo ""
print_info "Next steps:"
echo " 1. Review tokens in $OUTPUT_DIR/tokens/"
echo " 2. Customize brand colors and typography"
echo " 3. Import tokens in your app"
echo " 4. Use components from $OUTPUT_DIR/components/"
echo " 5. Read documentation in $OUTPUT_DIR/docs/"
echo ""
generate_design_tokens() {
# Color tokens
cat > "$OUTPUT_DIR/tokens/colors.css" << EOF
/**
* Color Tokens
* Generated by Frontend Designer
*/
:root {
/* Brand Colors */
--color-primary: $PRIMARY_COLOR;
--color-primary-hover: color-mix(in srgb, var(--color-primary) 85%, black);
--color-primary-active: color-mix(in srgb, var(--color-primary) 75%, black);
--color-primary-subtle: color-mix(in srgb, var(--color-primary) 10%, white);
--color-secondary: #6B7280;
--color-secondary-hover: #4B5563;
--color-secondary-subtle: #F3F4F6;
/* Semantic Colors */
--color-success: #10B981;
--color-success-hover: #059669;
--color-success-subtle: #D1FAE5;
--color-warning: #F59E0B;
--color-warning-hover: #D97706;
--color-warning-subtle: #FEF3C7;
--color-error: #EF4444;
--color-error-hover: #DC2626;
--color-error-subtle: #FEE2E2;
--color-info: #3B82F6;
--color-info-hover: #2563EB;
--color-info-subtle: #DBEAFE;
/* Neutral Colors */
--color-white: #FFFFFF;
--color-black: #000000;
--color-gray-50: #F9FAFB;
--color-gray-100: #F3F4F6;
--color-gray-200: #E5E7EB;
--color-gray-300: #D1D5DB;
--color-gray-400: #9CA3AF;
--color-gray-500: #6B7280;
--color-gray-600: #4B5563;
--color-gray-700: #374151;
--color-gray-800: #1F2937;
--color-gray-900: #111827;
/* Surface Colors */
--color-surface: var(--color-white);
--color-surface-hover: var(--color-gray-50);
--color-surface-subtle: var(--color-gray-100);
--color-background: var(--color-white);
--color-background-alt: var(--color-gray-50);
/* Text Colors */
--color-text: var(--color-gray-900);
--color-text-secondary: var(--color-gray-600);
--color-text-tertiary: var(--color-gray-400);
--color-text-inverse: var(--color-white);
/* Border Colors */
--color-border: var(--color-gray-200);
--color-border-hover: var(--color-gray-300);
--color-border-focus: var(--color-primary);
/* Focus Ring */
--color-focus: var(--color-primary);
--color-focus-ring: color-mix(in srgb, var(--color-primary) 50%, transparent);
}
/* Dark Mode */
@media (prefers-color-scheme: dark) {
:root {
--color-surface: var(--color-gray-800);
--color-surface-hover: var(--color-gray-700);
--color-surface-subtle: var(--color-gray-900);
--color-background: var(--color-gray-900);
--color-background-alt: var(--color-gray-800);
--color-text: var(--color-gray-50);
--color-text-secondary: var(--color-gray-300);
--color-text-tertiary: var(--color-gray-500);
--color-border: var(--color-gray-700);
--color-border-hover: var(--color-gray-600);
}
}
EOF
# Typography tokens
cat > "$OUTPUT_DIR/tokens/typography.css" << EOF
/**
* Typography Tokens
* Generated by Frontend Designer
*/
:root {
/* Font Families */
--font-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-heading: var(--font-base);
--font-mono: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
/* Font Sizes - Fluid Typography */
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--text-sm: clamp(0.875rem, 0.825rem + 0.25vw, 1rem);
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--text-lg: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem);
--text-xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
--text-2xl: clamp(1.5rem, 1.35rem + 0.75vw, 1.875rem);
--text-3xl: clamp(1.875rem, 1.65rem + 1.125vw, 2.25rem);
--text-4xl: clamp(2.25rem, 1.95rem + 1.5vw, 3rem);
--text-5xl: clamp(3rem, 2.55rem + 2.25vw, 3.75rem);
/* Font Weights */
--font-light: 300;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
--font-extrabold: 800;
/* Line Heights */
--leading-none: 1;
--leading-tight: 1.25;
--leading-snug: 1.375;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
--leading-loose: 2;
/* Letter Spacing */
--tracking-tighter: -0.05em;
--tracking-tight: -0.025em;
--tracking-normal: 0;
--tracking-wide: 0.025em;
--tracking-wider: 0.05em;
--tracking-widest: 0.1em;
}
EOF
# Spacing tokens
cat > "$OUTPUT_DIR/tokens/spacing.css" << EOF
/**
* Spacing Tokens
* Generated by Frontend Designer
*/
:root {
/* Base spacing scale */
--space-0: 0;
--space-px: 1px;
--space-0-5: 0.125rem; /* 2px */
--space-1: 0.25rem; /* 4px */
--space-1-5: 0.375rem; /* 6px */
--space-2: 0.5rem; /* 8px */
--space-2-5: 0.625rem; /* 10px */
--space-3: 0.75rem; /* 12px */
--space-3-5: 0.875rem; /* 14px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-7: 1.75rem; /* 28px */
--space-8: 2rem; /* 32px */
--space-9: 2.25rem; /* 36px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-14: 3.5rem; /* 56px */
--space-16: 4rem; /* 64px */
--space-20: 5rem; /* 80px */
--space-24: 6rem; /* 96px */
--space-32: 8rem; /* 128px */
/* Container widths */
--container-xs: 20rem; /* 320px */
--container-sm: 24rem; /* 384px */
--container-md: 28rem; /* 448px */
--container-lg: 32rem; /* 512px */
--container-xl: 36rem; /* 576px */
--container-2xl: 42rem; /* 672px */
--container-3xl: 48rem; /* 768px */
--container-4xl: 56rem; /* 896px */
--container-5xl: 64rem; /* 1024px */
--container-6xl: 72rem; /* 1152px */
--container-7xl: 80rem; /* 1280px */
}
EOF
# Shadow tokens
cat > "$OUTPUT_DIR/tokens/shadows.css" << EOF
/**
* Shadow Tokens
* Generated by Frontend Designer
*/
:root {
/* Box Shadows */
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
/* Inner Shadow */
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
/* Focus Shadow */
--shadow-focus: 0 0 0 3px var(--color-focus-ring);
}
/* Dark Mode Shadows */
@media (prefers-color-scheme: dark) {
:root {
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.4);
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.5), 0 1px 2px 0 rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4);
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
}
}
EOF
# Border radius tokens
cat > "$OUTPUT_DIR/tokens/borders.css" << EOF
/**
* Border Tokens
* Generated by Frontend Designer
*/
:root {
/* Border Radius */
--radius-none: 0;
--radius-sm: 0.125rem; /* 2px */
--radius-md: 0.375rem; /* 6px */
--radius-lg: 0.5rem; /* 8px */
--radius-xl: 0.75rem; /* 12px */
--radius-2xl: 1rem; /* 16px */
--radius-3xl: 1.5rem; /* 24px */
--radius-full: 9999px;
/* Border Widths */
--border-0: 0;
--border-1: 1px;
--border-2: 2px;
--border-4: 4px;
--border-8: 8px;
}
EOF
# Z-index tokens
cat > "$OUTPUT_DIR/tokens/zindex.css" << EOF
/**
* Z-Index Tokens
* Generated by Frontend Designer
*/
:root {
--z-0: 0;
--z-10: 10;
--z-20: 20;
--z-30: 30;
--z-40: 40;
--z-50: 50;
/* Semantic z-index */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
EOF
# Main tokens file
cat > "$OUTPUT_DIR/tokens/index.css" << EOF
/**
* Design Tokens - $DS_NAME
* Generated by Frontend Designer
*
* Import this file in your app to use all design tokens
*/
@import './colors.css';
@import './typography.css';
@import './spacing.css';
@import './shadows.css';
@import './borders.css';
@import './zindex.css';
/* Base Reset */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html {
font-size: ${BASE_FONT_SIZE}px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font-base);
font-size: var(--text-base);
line-height: var(--leading-normal);
color: var(--color-text);
background-color: var(--color-background);
}
/* Accessibility */
:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
EOF
print_success "Generated design tokens"
}
generate_base_components() {
# This would generate base components
# For now, just create placeholder
cat > "$OUTPUT_DIR/components/README.md" << EOF
# Components
Base component library for $DS_NAME.
## Usage
Import components from this directory:
\`\`\`tsx
import { Button } from './components/Button';
import { Card } from './components/Card';
\`\`\`
## Available Components
- Button (multiple variants and sizes)
- Card (container component)
- Input (form input with validation)
- Modal (accessible dialog)
- Dropdown (accessible select)
Generate components using:
\`\`\`bash
./scripts/generate_component.sh
\`\`\`
EOF
print_success "Created components directory"
}
generate_documentation() {
cat > "$OUTPUT_DIR/docs/README.md" << EOF
# $DS_NAME Documentation
Complete design system documentation and guidelines.
## Getting Started
1. **Install design tokens**
\`\`\`css
@import 'design-system/tokens/index.css';
\`\`\`
2. **Use design tokens**
\`\`\`css
.my-component {
color: var(--color-primary);
padding: var(--space-4);
border-radius: var(--radius-md);
}
\`\`\`
3. **Import components**
\`\`\`tsx
import { Button } from 'design-system/components';
\`\`\`
## Design Tokens
### Colors
- **Brand**: \`--color-primary\`, \`--color-secondary\`
- **Semantic**: \`--color-success\`, \`--color-warning\`, \`--color-error\`
- **Neutral**: \`--color-gray-*\` (50-900)
### Typography
- **Font families**: \`--font-base\`, \`--font-heading\`, \`--font-mono\`
- **Sizes**: \`--text-xs\` through \`--text-5xl\`
- **Weights**: \`--font-light\` through \`--font-extrabold\`
### Spacing
- **Scale**: \`--space-0\` through \`--space-32\`
- **Containers**: \`--container-xs\` through \`--container-7xl\`
### Shadows
- **Elevation**: \`--shadow-xs\` through \`--shadow-2xl\`
- **Focus**: \`--shadow-focus\`
### Border Radius
- **Sizes**: \`--radius-sm\` through \`--radius-3xl\`
- **Full**: \`--radius-full\` (pills/circles)
## Accessibility
This design system follows WCAG 2.1 AA guidelines:
- ✅ Color contrast ratios: 4.5:1 for text, 3:1 for UI
- ✅ Touch targets: 44x44px minimum
- ✅ Keyboard navigation support
- ✅ Focus indicators
- ✅ Reduced motion support
- ✅ Dark mode support
- ✅ High contrast mode support
## Browser Support
- Chrome (last 2 versions)
- Firefox (last 2 versions)
- Safari (last 2 versions)
- Edge (last 2 versions)
## Contributing
See CONTRIBUTING.md for contribution guidelines.
EOF
print_success "Created documentation"
}