Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "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
3
README.md
Normal 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
72
plugin.lock.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
862
skills/frontend-designer/SKILL.md
Normal file
862
skills/frontend-designer/SKILL.md
Normal 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."**
|
||||||
622
skills/frontend-designer/references/accessibility_checklist.md
Normal file
622
skills/frontend-designer/references/accessibility_checklist.md
Normal 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."**
|
||||||
937
skills/frontend-designer/references/component_library.md
Normal file
937
skills/frontend-designer/references/component_library.md
Normal 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."**
|
||||||
608
skills/frontend-designer/references/design_tokens.md
Normal file
608
skills/frontend-designer/references/design_tokens.md
Normal 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."**
|
||||||
839
skills/frontend-designer/references/responsive_patterns.md
Normal file
839
skills/frontend-designer/references/responsive_patterns.md
Normal 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."**
|
||||||
467
skills/frontend-designer/scripts/audit_accessibility.sh
Executable file
467
skills/frontend-designer/scripts/audit_accessibility.sh
Executable 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
|
||||||
619
skills/frontend-designer/scripts/generate_component.sh
Executable file
619
skills/frontend-designer/scripts/generate_component.sh
Executable 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 ""
|
||||||
570
skills/frontend-designer/scripts/setup_design_system.sh
Executable file
570
skills/frontend-designer/scripts/setup_design_system.sh
Executable 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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user