style: format all files with prettier

This commit is contained in:
Seth Hobson
2026-01-19 17:07:03 -05:00
parent 8d37048deb
commit 56848874a2
355 changed files with 15215 additions and 10241 deletions

View File

@@ -5,6 +5,7 @@ Comprehensive UI/UX design plugin covering mobile (iOS, Android, React Native) a
## Features
### Core Capabilities
- **Design Systems**: Token architecture, theming, multi-brand systems
- **Accessibility**: WCAG 2.2 compliance, inclusive design patterns
- **Responsive Design**: Container queries, fluid layouts, breakpoints
@@ -14,34 +15,34 @@ Comprehensive UI/UX design plugin covering mobile (iOS, Android, React Native) a
## Skills
| Skill | Description |
|-------|-------------|
| `design-system-patterns` | Design tokens, theming, component architecture |
| `accessibility-compliance` | WCAG 2.2, mobile a11y, inclusive design |
| `responsive-design` | Container queries, fluid layouts, breakpoints |
| `mobile-ios-design` | iOS Human Interface Guidelines, SwiftUI patterns |
| `mobile-android-design` | Material Design 3, Jetpack Compose patterns |
| `react-native-design` | React Native styling, navigation, animations |
| `web-component-design` | React/Vue/Svelte component patterns, CSS-in-JS |
| `interaction-design` | Microinteractions, motion design, transitions |
| `visual-design-foundations` | Typography, color theory, spacing, iconography |
| Skill | Description |
| --------------------------- | ------------------------------------------------ |
| `design-system-patterns` | Design tokens, theming, component architecture |
| `accessibility-compliance` | WCAG 2.2, mobile a11y, inclusive design |
| `responsive-design` | Container queries, fluid layouts, breakpoints |
| `mobile-ios-design` | iOS Human Interface Guidelines, SwiftUI patterns |
| `mobile-android-design` | Material Design 3, Jetpack Compose patterns |
| `react-native-design` | React Native styling, navigation, animations |
| `web-component-design` | React/Vue/Svelte component patterns, CSS-in-JS |
| `interaction-design` | Microinteractions, motion design, transitions |
| `visual-design-foundations` | Typography, color theory, spacing, iconography |
## Agents
| Agent | Description |
|-------|-------------|
| `ui-designer` | Proactive UI design, component creation, layout optimization |
| `accessibility-expert` | A11y analysis, WCAG compliance, remediation |
| `design-system-architect` | Design token systems, component libraries, theming |
| Agent | Description |
| ------------------------- | ------------------------------------------------------------ |
| `ui-designer` | Proactive UI design, component creation, layout optimization |
| `accessibility-expert` | A11y analysis, WCAG compliance, remediation |
| `design-system-architect` | Design token systems, component libraries, theming |
## Commands
| Command | Description |
|---------|-------------|
| `/ui-design:design-review` | Review existing UI for issues and improvements |
| `/ui-design:create-component` | Guided component creation with proper patterns |
| `/ui-design:accessibility-audit` | Audit UI code for WCAG compliance |
| `/ui-design:design-system-setup` | Initialize a design system with tokens |
| Command | Description |
| -------------------------------- | ---------------------------------------------- |
| `/ui-design:design-review` | Review existing UI for issues and improvements |
| `/ui-design:create-component` | Guided component creation with proper patterns |
| `/ui-design:accessibility-audit` | Audit UI code for WCAG compliance |
| `/ui-design:design-system-setup` | Initialize a design system with tokens |
## Installation
@@ -52,21 +53,25 @@ Comprehensive UI/UX design plugin covering mobile (iOS, Android, React Native) a
## Usage Examples
### Design Review
```
/ui-design:design-review --file src/components/Button.tsx
```
### Create Component
```
/ui-design:create-component Card --platform react
```
### Accessibility Audit
```
/ui-design:accessibility-audit --level AA
```
### Design System Setup
```
/ui-design:design-system-setup --name "Acme Design System"
```
@@ -74,22 +79,26 @@ Comprehensive UI/UX design plugin covering mobile (iOS, Android, React Native) a
## Key Technologies Covered
### Web
- CSS Grid, Flexbox, Container Queries
- Tailwind CSS, CSS-in-JS (Styled Components, Emotion)
- React, Vue, Svelte component patterns
- Framer Motion, GSAP animations
### Mobile
- **iOS**: SwiftUI, UIKit, Human Interface Guidelines
- **Android**: Jetpack Compose, Material Design 3
- **React Native**: StyleSheet, Reanimated, React Navigation
### Design Systems
- Design tokens (Style Dictionary, Figma Variables)
- Component libraries (Storybook documentation)
- Multi-brand theming
### Accessibility
- WCAG 2.2 AA/AAA compliance
- ARIA patterns and semantic HTML
- Screen reader compatibility

View File

@@ -8,11 +8,13 @@ color: green
You are an expert accessibility specialist dedicated to creating inclusive digital experiences that work for all users regardless of ability.
## Purpose
Expert accessibility specialist with deep knowledge of WCAG guidelines, assistive technologies, and inclusive design principles. Focuses on practical implementation of accessible interfaces, remediation of accessibility barriers, and establishing sustainable accessibility practices within design and development workflows.
## Capabilities
### WCAG Compliance & Standards
- WCAG 2.1 and 2.2 guidelines: Level A, AA, and AAA criteria
- Understanding success criteria and their technical requirements
- WCAG 3.0 (Silver) emerging guidelines and future considerations
@@ -23,6 +25,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- ACR (Accessibility Conformance Report) and VPAT documentation
### Screen Reader Optimization
- ARIA (Accessible Rich Internet Applications) implementation
- ARIA roles, states, and properties for custom components
- Live regions for dynamic content announcements (aria-live, aria-atomic)
@@ -33,6 +36,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- Image alt text strategies: decorative, informative, functional, complex
### Keyboard Navigation & Focus Management
- Tab order and focus flow optimization
- Focus trapping for modals and dialogs
- Skip links and landmark navigation
@@ -43,6 +47,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- Focus restoration after dynamic content changes
### Color & Visual Accessibility
- Color contrast analysis: WCAG AA (4.5:1) and AAA (7:1) ratios
- Color blindness considerations: protanopia, deuteranopia, tritanopia
- Non-color indicators for conveying information
@@ -53,6 +58,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- Font sizing and zoom support up to 200%
### Cognitive Accessibility
- Clear and simple language guidelines
- Consistent navigation and predictable behavior
- Error prevention and recovery mechanisms
@@ -63,6 +69,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- Clear instructions and helpful error messages
### Assistive Technology Compatibility
- Screen reader compatibility testing and optimization
- Voice control software: Dragon NaturallySpeaking, Voice Control
- Switch access and alternative input devices
@@ -73,6 +80,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- Alternative pointer devices and mouth sticks
### Automated & Manual Testing
- Automated testing tools: axe-core, WAVE, Lighthouse, Pa11y
- Integration testing with jest-axe, cypress-axe
- Manual testing checklists and procedures
@@ -83,6 +91,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- User testing with people with disabilities
### Remediation & Implementation
- Accessibility audit report creation and prioritization
- Remediation planning with severity and impact assessment
- Quick wins vs. long-term architectural improvements
@@ -93,6 +102,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- PDF and document accessibility requirements
## Behavioral Traits
- Advocates for users with disabilities throughout the design process
- Balances compliance requirements with genuine usability
- Provides practical, implementable solutions rather than theoretical ideals
@@ -105,6 +115,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- Approaches accessibility as an ongoing practice, not a one-time checklist
## Knowledge Base
- Complete WCAG 2.1/2.2 success criteria and techniques
- ARIA Authoring Practices Guide (APG) patterns
- Assistive technology behavior and compatibility quirks
@@ -117,6 +128,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
- Emerging accessibility technologies and standards
## Response Approach
1. **Assess the accessibility context** including user needs and compliance requirements
2. **Identify specific WCAG criteria** and success criteria relevant to the issue
3. **Analyze current implementation** for accessibility barriers
@@ -127,6 +139,7 @@ Expert accessibility specialist with deep knowledge of WCAG guidelines, assistiv
8. **Document accessibility requirements** for future reference
## Example Interactions
- "Audit this component for WCAG 2.1 AA compliance and provide a remediation plan"
- "Make this custom dropdown accessible with proper keyboard navigation and screen reader support"
- "Review our color palette for sufficient contrast ratios across all combinations"

View File

@@ -8,11 +8,13 @@ color: magenta
You are an expert design system architect specializing in building scalable, maintainable design systems that bridge design and development.
## Purpose
Expert design system architect with deep expertise in token-based design, component library architecture, and theming infrastructure. Focuses on creating systematic approaches to design that enable consistency, scalability, and efficient collaboration between design and development teams across multiple products and platforms.
## Capabilities
### Design Token Architecture
- Token taxonomy: primitive, semantic, and component-level tokens
- Token naming conventions and organizational strategies
- Color token systems: palette, semantic (success, warning, error), component-specific
@@ -25,6 +27,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Token aliasing and referencing strategies
### Token Tooling & Transformation
- Style Dictionary configuration and custom transforms
- Tokens Studio (Figma Tokens) integration and workflows
- Token transformation to CSS custom properties
@@ -35,6 +38,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Multi-format output: CSS, SCSS, JSON, JavaScript, Swift, Kotlin
### Component Library Architecture
- Component API design principles and prop patterns
- Compound component patterns for flexible composition
- Headless component architecture (Radix, Headless UI patterns)
@@ -45,6 +49,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Default prop strategies and sensible defaults
### Multi-Brand & Theming Systems
- Theme architecture for multiple brands and products
- CSS custom property-based theming
- Theme switching and persistence strategies
@@ -55,6 +60,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Runtime theme generation and modification
### Design-Development Workflow
- Design-to-code handoff processes and tooling
- Figma component structure mirroring code architecture
- Design token synchronization between Figma and code
@@ -65,6 +71,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Change management and deprecation strategies
### Scalable Component Patterns
- Primitive components as building blocks
- Layout components: Box, Stack, Flex, Grid
- Typography components with semantic variants
@@ -75,6 +82,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Overlay components: modals, popovers, tooltips
### Documentation & Governance
- Component documentation structure and standards
- Usage guidelines and best practices documentation
- Do's and don'ts with visual examples
@@ -85,6 +93,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Design system roadmap and versioning
### Performance & Optimization
- Tree-shaking and bundle size optimization
- CSS optimization: critical CSS, code splitting
- Component lazy loading strategies
@@ -95,6 +104,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Monitoring design system adoption and usage
## Behavioral Traits
- Thinks systematically about design decisions and their cascading effects
- Balances flexibility with consistency in component APIs
- Prioritizes developer experience alongside design quality
@@ -107,6 +117,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Collaborates effectively across design and engineering disciplines
## Knowledge Base
- Industry design systems: Material Design, Carbon, Spectrum, Polaris, Atlassian
- Token specification formats: W3C Design Tokens, Style Dictionary
- Component library frameworks: React, Vue, Web Components, Svelte
@@ -119,6 +130,7 @@ Expert design system architect with deep expertise in token-based design, compon
- Emerging standards: CSS layers, container queries, view transitions
## Response Approach
1. **Understand the system scope** including products, platforms, and team structure
2. **Analyze existing design patterns** and identify systematization opportunities
3. **Design token architecture** with appropriate abstraction levels
@@ -129,6 +141,7 @@ Expert design system architect with deep expertise in token-based design, compon
8. **Recommend tooling and automation** for sustainable maintenance
## Example Interactions
- "Design a token architecture for a multi-brand enterprise application with dark mode support"
- "Create a component library structure for a React-based design system with Storybook documentation"
- "Build a theming system that supports white-labeling for SaaS customer customization"

View File

@@ -8,11 +8,13 @@ color: cyan
You are an expert UI designer specializing in creating beautiful, functional, and user-centered interface designs with a focus on practical implementation.
## Purpose
Expert UI designer combining visual design expertise with implementation knowledge. Masters modern design systems, responsive layouts, and component-driven architecture. Focuses on creating interfaces that are visually appealing, functionally effective, and technically feasible to implement.
## Capabilities
### Component Design & Creation
- Atomic design methodology: atoms, molecules, organisms, templates, pages
- Component composition patterns for maximum reusability
- State-driven component design: default, hover, active, focus, disabled, error
@@ -23,6 +25,7 @@ Expert UI designer combining visual design expertise with implementation knowled
- Skeleton loaders and empty states for loading experiences
### Layout Systems & Grid Design
- CSS Grid and Flexbox layout architecture
- Responsive grid systems: 12-column, fluid, and custom grids
- Breakpoint strategy and mobile-first design approach
@@ -33,6 +36,7 @@ Expert UI designer combining visual design expertise with implementation knowled
- Z-index management and layering strategies
### Visual Design Fundamentals
- Color theory: palette creation, contrast ratios, color harmony
- Typography systems: type scale, font pairing, hierarchical organization
- Iconography: icon systems, sizing, consistency guidelines
@@ -43,6 +47,7 @@ Expert UI designer combining visual design expertise with implementation knowled
- Dark mode design with appropriate color transformations
### Responsive & Adaptive Design
- Mobile-first design strategy and progressive enhancement
- Touch-friendly target sizing (minimum 44x44px)
- Responsive typography with fluid scaling (clamp, viewport units)
@@ -53,6 +58,7 @@ Expert UI designer combining visual design expertise with implementation knowled
- Print stylesheet considerations for document-heavy interfaces
### Design-to-Code Implementation
- Design token translation to CSS custom properties
- Component specification documentation for developers
- Tailwind CSS utility-first implementation patterns
@@ -63,6 +69,7 @@ Expert UI designer combining visual design expertise with implementation knowled
- SVG optimization and implementation for icons and illustrations
### Prototyping & Interaction Design
- Low-fidelity wireframing for rapid concept exploration
- High-fidelity prototyping with realistic interactions
- Interaction patterns: drag-and-drop, swipe gestures, pull-to-refresh
@@ -73,6 +80,7 @@ Expert UI designer combining visual design expertise with implementation knowled
- Error state handling and recovery patterns
## Behavioral Traits
- Prioritizes user needs and usability over aesthetic preferences
- Creates designs that are technically feasible and performant
- Maintains consistency through systematic design decisions
@@ -85,6 +93,7 @@ Expert UI designer combining visual design expertise with implementation knowled
- Focuses on solving real user problems through thoughtful design
## Knowledge Base
- Modern CSS capabilities: container queries, has(), layers, subgrid
- Design system best practices from industry leaders (Material, Carbon, Spectrum)
- Component library patterns: Radix, shadcn/ui, Headless UI
@@ -97,6 +106,7 @@ Expert UI designer combining visual design expertise with implementation knowled
- Emerging design patterns and interaction models
## Response Approach
1. **Understand the design problem** and user needs being addressed
2. **Analyze existing design context** including brand, system, and constraints
3. **Propose design solutions** with clear rationale and alternatives considered
@@ -107,6 +117,7 @@ Expert UI designer combining visual design expertise with implementation knowled
8. **Recommend testing approaches** for validating design effectiveness
## Example Interactions
- "Design a card component system for an e-commerce product listing with hover states and responsive behavior"
- "Create a dashboard layout with collapsible sidebar navigation and responsive grid for widgets"
- "Build a multi-step form wizard with progress indication and validation feedback"

View File

@@ -113,17 +113,20 @@ For each file, check against WCAG criteria:
#### Perceivable (WCAG 1.x)
**1.1 Text Alternatives:**
- [ ] Images have `alt` attributes
- [ ] Decorative images use `alt=""` or `role="presentation"`
- [ ] Complex images have extended descriptions
- [ ] Icon buttons have accessible names
**1.2 Time-based Media:**
- [ ] Videos have captions
- [ ] Audio has transcripts
- [ ] Media players are keyboard accessible
**1.3 Adaptable:**
- [ ] Semantic HTML structure (headings, lists, landmarks)
- [ ] Proper heading hierarchy (h1 > h2 > h3)
- [ ] Form inputs have associated labels
@@ -131,6 +134,7 @@ For each file, check against WCAG criteria:
- [ ] Reading order is logical
**1.4 Distinguishable:**
- [ ] Color contrast meets requirements (4.5:1 normal, 3:1 large)
- [ ] Color is not sole means of conveying information
- [ ] Text can be resized to 200%
@@ -140,21 +144,25 @@ For each file, check against WCAG criteria:
#### Operable (WCAG 2.x)
**2.1 Keyboard Accessible:**
- [ ] All interactive elements are keyboard accessible
- [ ] No keyboard traps
- [ ] Focus order is logical
- [ ] Custom widgets follow ARIA patterns
**2.2 Enough Time:**
- [ ] Time limits can be extended/disabled
- [ ] Auto-updating content can be paused
- [ ] No content times out unexpectedly
**2.3 Seizures:**
- [ ] No content flashes more than 3 times/second
- [ ] Animations can be disabled (prefers-reduced-motion)
**2.4 Navigable:**
- [ ] Skip links present
- [ ] Page has descriptive title
- [ ] Focus visible on all elements
@@ -162,6 +170,7 @@ For each file, check against WCAG criteria:
- [ ] Multiple ways to find pages
**2.5 Input Modalities:**
- [ ] Touch targets are at least 44x44px (AAA: 44px, AA: 24px)
- [ ] Functionality not dependent on motion
- [ ] Dragging has alternative
@@ -169,17 +178,20 @@ For each file, check against WCAG criteria:
#### Understandable (WCAG 3.x)
**3.1 Readable:**
- [ ] Language is specified (`lang` attribute)
- [ ] Unusual words are defined
- [ ] Abbreviations are expanded
**3.2 Predictable:**
- [ ] Focus doesn't trigger unexpected changes
- [ ] Input doesn't trigger unexpected changes
- [ ] Navigation is consistent
- [ ] Components behave consistently
**3.3 Input Assistance:**
- [ ] Error messages are descriptive
- [ ] Labels or instructions provided
- [ ] Error suggestions provided
@@ -188,6 +200,7 @@ For each file, check against WCAG criteria:
#### Robust (WCAG 4.x)
**4.1 Compatible:**
- [ ] HTML validates (no duplicate IDs)
- [ ] Custom components have proper ARIA
- [ ] Status messages announced to screen readers
@@ -253,7 +266,7 @@ Check ARIA usage:
Generate audit report in `.ui-design/audits/{audit_id}.md`:
```markdown
````markdown
# Accessibility Audit Report
**Audit ID:** {audit_id}
@@ -267,7 +280,7 @@ Generate audit report in `.ui-design/audits/{audit_id}.md`:
**Compliance Status:** {Passing | Needs Improvement | Failing}
| Severity | Count | % of Issues |
|----------|-------|-------------|
| -------- | ----- | ----------- |
| Critical | {n} | {%} |
| Serious | {n} | {%} |
| Moderate | {n} | {%} |
@@ -298,6 +311,7 @@ These issues prevent users with disabilities from using the interface.
{Step-by-step fix instructions}
**Code Fix:**
```{language}
// Before
{current_code}
@@ -305,8 +319,10 @@ These issues prevent users with disabilities from using the interface.
// After
{fixed_code}
```
````
**Testing:**
- Manual: {how to manually verify}
- Automated: {suggested test}
@@ -336,11 +352,11 @@ These are best practice improvements.
The following WCAG criteria passed:
| Criterion | Name | Level |
|-----------|------|-------|
| 1.1.1 | Non-text Content | A |
| 1.3.1 | Info and Relationships | A |
| ... | ... | ... |
| Criterion | Name | Level |
| --------- | ---------------------- | ----- |
| 1.1.1 | Non-text Content | A |
| 1.3.1 | Info and Relationships | A |
| ... | ... | ... |
## Recommendations
@@ -366,11 +382,11 @@ Add these tests to catch regressions:
```javascript
// Example jest-axe test
import { axe, toHaveNoViolations } from 'jest-axe';
import { axe, toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);
test('component has no accessibility violations', async () => {
test("component has no accessibility violations", async () => {
const { container } = render(<Component />);
const results = await axe(container);
expect(results).toHaveNoViolations();
@@ -397,7 +413,8 @@ test('component has no accessibility violations', async () => {
_Generated by UI Design Accessibility Audit_
_WCAG Reference: https://www.w3.org/WAI/WCAG21/quickref/_
```
````
## Completion
@@ -415,7 +432,7 @@ Update `audit_state.json`:
"minor": 3
}
}
```
````
Display summary:

View File

@@ -24,6 +24,7 @@ Guided workflow for creating new UI components following established patterns an
- Check for existing component conventions
4. If no framework detected:
```
I couldn't detect a UI framework. What are you using?
@@ -278,6 +279,7 @@ export interface {ComponentName}Props extends HTMLAttributes<HTMLDivElement> {
Based on styling approach:
**CSS Modules:**
```css
/* {ComponentName}.styles.module.css */
.root {
@@ -294,6 +296,7 @@ Based on styling approach:
```
**Tailwind:**
```tsx
// Inline in component
className={cn(

View File

@@ -130,24 +130,28 @@ Read and analyze the target files:
Check for:
**Spacing & Layout:**
- Inconsistent margins/padding
- Misaligned elements
- Unbalanced whitespace
- Magic numbers vs. design tokens
**Typography:**
- Font size consistency
- Line height appropriateness
- Text contrast ratios
- Font weight usage
**Colors:**
- Color contrast accessibility
- Consistent color usage
- Semantic color application
- Dark mode support (if applicable)
**Visual Hierarchy:**
- Clear primary actions
- Appropriate emphasis
- Scannable content structure
@@ -157,6 +161,7 @@ Check for:
Check for:
**Interaction Patterns:**
- Clear clickable/tappable areas
- Appropriate hover/focus states
- Loading state indicators
@@ -164,12 +169,14 @@ Check for:
- Empty state handling
**User Flow:**
- Logical tab order
- Clear call-to-action
- Predictable behavior
- Feedback on actions
**Cognitive Load:**
- Information density
- Progressive disclosure
- Clear labels and instructions
@@ -180,18 +187,21 @@ Check for:
Check for:
**Component Patterns:**
- Single responsibility
- Prop drilling depth
- State management appropriateness
- Component reusability
**Styling Patterns:**
- Consistent naming conventions
- Reusable style definitions
- Media query organization
- CSS specificity issues
**Maintainability:**
- Clear component boundaries
- Documentation/comments
- Test coverage
@@ -202,12 +212,14 @@ Check for:
Check for:
**Render Optimization:**
- Unnecessary re-renders
- Missing memoization
- Large component trees
- Expensive computations in render
**Asset Optimization:**
- Image sizes and formats
- Icon implementation
- Font loading strategy
@@ -217,7 +229,7 @@ Check for:
Generate review report in `.ui-design/reviews/{review_id}.md`:
```markdown
````markdown
# Design Review: {Component/File Name}
**Review ID:** {review_id}
@@ -230,6 +242,7 @@ Generate review report in `.ui-design/reviews/{review_id}.md`:
{2-3 sentence overview of findings}
**Issues Found:** {total_count}
- Critical: {count}
- Major: {count}
- Minor: {count}
@@ -253,6 +266,7 @@ Generate review report in `.ui-design/reviews/{review_id}.md`:
{Specific fix suggestion}
**Code Example:**
```{language}
// Before
{current_code}
@@ -260,22 +274,26 @@ Generate review report in `.ui-design/reviews/{review_id}.md`:
// After
{suggested_code}
```
````
---
## Major Issues
### Issue 2: {Title}
...
## Minor Issues
### Issue 3: {Title}
...
## Suggestions
### Suggestion 1: {Title}
...
## Positive Observations
@@ -294,6 +312,7 @@ Generate review report in `.ui-design/reviews/{review_id}.md`:
---
_Generated by UI Design Review. Run `/ui-design:design-review` again after fixes._
```
## Completion
@@ -307,22 +326,26 @@ After generating report:
2. Display summary:
```
Design Review Complete!
Target: {component/file}
Issues Found: {total}
- {critical} Critical
- {major} Major
- {minor} Minor
- {suggestions} Suggestions
- {critical} Critical
- {major} Major
- {minor} Minor
- {suggestions} Suggestions
Full report: .ui-design/reviews/{review_id}.md
What would you like to do next?
1. View detailed findings for a specific issue
2. Start implementing fixes
3. Export report (markdown/JSON)
4. Review another component
```
## Follow-up Actions
@@ -330,6 +353,7 @@ What would you like to do next?
If user selects "Start implementing fixes":
```
Which issues would you like to address?
1. All critical issues first
@@ -338,6 +362,7 @@ Which issues would you like to address?
4. Generate a fix plan for all issues
Enter choice:
```
Guide user through fixes one at a time, updating the review report as issues are resolved.
@@ -347,3 +372,4 @@ Guide user through fixes one at a time, updating the review report as issues are
- If target file not found: Suggest similar files, offer to search
- If file is not UI code: Explain and ask for correct target
- If review fails mid-way: Save partial results, offer to resume
```

View File

@@ -25,6 +25,7 @@ Initialize a design system with design tokens, component patterns, and documenta
- Detect TypeScript usage
4. If existing design system detected:
```
I detected an existing design system configuration:
@@ -385,27 +386,27 @@ Create `.ui-design/tokens/tokens.css`:
:root {
/* Colors - Primary */
--color-primary-50: #EFF6FF;
--color-primary-100: #DBEAFE;
--color-primary-500: #3B82F6;
--color-primary-600: #2563EB;
--color-primary-700: #1D4ED8;
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
/* Colors - Neutral */
--color-neutral-50: #F9FAFB;
--color-neutral-100: #F3F4F6;
--color-neutral-500: #6B7280;
--color-neutral-50: #f9fafb;
--color-neutral-100: #f3f4f6;
--color-neutral-500: #6b7280;
--color-neutral-900: #111827;
/* Colors - Semantic */
--color-success: #22C55E;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #3b82f6;
/* Typography */
--font-family-sans: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
--font-family-mono: ui-monospace, 'Fira Code', monospace;
--font-family-mono: ui-monospace, "Fira Code", monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
@@ -442,17 +443,17 @@ Create `.ui-design/tokens/tokens.css`:
@media (prefers-color-scheme: dark) {
:root {
--color-neutral-50: #111827;
--color-neutral-100: #1F2937;
--color-neutral-500: #9CA3AF;
--color-neutral-900: #F9FAFB;
--color-neutral-100: #1f2937;
--color-neutral-500: #9ca3af;
--color-neutral-900: #f9fafb;
}
}
[data-theme="dark"] {
--color-neutral-50: #111827;
--color-neutral-100: #1F2937;
--color-neutral-500: #9CA3AF;
--color-neutral-900: #F9FAFB;
--color-neutral-100: #1f2937;
--color-neutral-500: #9ca3af;
--color-neutral-900: #f9fafb;
}
```
@@ -470,23 +471,23 @@ module.exports = {
extend: {
colors: {
primary: {
50: '#EFF6FF',
100: '#DBEAFE',
200: '#BFDBFE',
300: '#93C5FD',
400: '#60A5FA',
500: '#3B82F6',
600: '#2563EB',
700: '#1D4ED8',
800: '#1E40AF',
900: '#1E3A8A',
950: '#172554',
50: "#EFF6FF",
100: "#DBEAFE",
200: "#BFDBFE",
300: "#93C5FD",
400: "#60A5FA",
500: "#3B82F6",
600: "#2563EB",
700: "#1D4ED8",
800: "#1E40AF",
900: "#1E3A8A",
950: "#172554",
},
// ... other colors
},
fontFamily: {
sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
mono: ['ui-monospace', 'Fira Code', 'monospace'],
sans: ["Inter", "-apple-system", "BlinkMacSystemFont", "sans-serif"],
mono: ["ui-monospace", "Fira Code", "monospace"],
},
// ... other tokens
},
@@ -504,7 +505,7 @@ Create `.ui-design/tokens/tokens.ts`:
export const colors = {
primary: {
50: '#EFF6FF',
50: "#EFF6FF",
// ... full palette
},
// ... other color groups
@@ -512,17 +513,17 @@ export const colors = {
export const typography = {
fontFamily: {
sans: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif',
sans: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
mono: "ui-monospace, 'Fira Code', monospace",
},
fontSize: {
xs: '0.75rem',
xs: "0.75rem",
// ... full scale
},
} as const;
export const spacing = {
1: '0.25rem',
1: "0.25rem",
// ... full scale
} as const;
@@ -535,7 +536,7 @@ export type SpacingToken = keyof typeof spacing;
Create `.ui-design/docs/design-system.md`:
```markdown
````markdown
# Design System Documentation
## Overview
@@ -545,33 +546,37 @@ This design system provides the foundation for consistent UI development.
## Colors
### Primary Palette
| Token | Value | Usage |
|-------|-------|-------|
| Token | Value | Usage |
| ----------- | ------- | ---------------------- |
| primary-500 | #3B82F6 | Primary actions, links |
| primary-600 | #2563EB | Hover state |
| primary-700 | #1D4ED8 | Active state |
| primary-600 | #2563EB | Hover state |
| primary-700 | #1D4ED8 | Active state |
### Semantic Colors
| Token | Value | Usage |
|-------|-------|-------|
| success | #22C55E | Success messages, positive actions |
| warning | #F59E0B | Warning messages, caution |
| error | #EF4444 | Error messages, destructive actions |
| Token | Value | Usage |
| ------- | ------- | ----------------------------------- |
| success | #22C55E | Success messages, positive actions |
| warning | #F59E0B | Warning messages, caution |
| error | #EF4444 | Error messages, destructive actions |
## Typography
### Scale
| Name | Size | Usage |
|------|------|-------|
| xs | 0.75rem | Captions, labels |
| sm | 0.875rem | Secondary text |
| base | 1rem | Body text |
| lg | 1.125rem | Emphasized body |
| xl | 1.25rem | Subheadings |
| Name | Size | Usage |
| ---- | -------- | ---------------- |
| xs | 0.75rem | Captions, labels |
| sm | 0.875rem | Secondary text |
| base | 1rem | Body text |
| lg | 1.125rem | Emphasized body |
| xl | 1.25rem | Subheadings |
## Spacing
Use spacing tokens for consistent margins and padding:
- `spacing-1` (4px): Tight spacing
- `spacing-2` (8px): Compact spacing
- `spacing-4` (16px): Default spacing
@@ -581,6 +586,7 @@ Use spacing tokens for consistent margins and padding:
## Usage
### CSS Custom Properties
```css
.button {
background: var(--color-primary-500);
@@ -588,13 +594,14 @@ Use spacing tokens for consistent margins and padding:
border-radius: var(--radius-md);
}
```
````
### Tailwind
```html
<button class="bg-primary-500 px-4 py-2 rounded-md">
Click me
</button>
<button class="bg-primary-500 px-4 py-2 rounded-md">Click me</button>
```
```
## Completion
@@ -602,26 +609,31 @@ Use spacing tokens for consistent margins and padding:
Update state and display summary:
```
Design System Setup Complete!
Created files:
- .ui-design/design-system.json (master configuration)
- .ui-design/tokens/tokens.css (CSS custom properties)
- .ui-design/tokens/tailwind.config.js (Tailwind extension)
- .ui-design/tokens/tokens.ts (TypeScript module)
- .ui-design/docs/design-system.md (documentation)
- .ui-design/design-system.json (master configuration)
- .ui-design/tokens/tokens.css (CSS custom properties)
- .ui-design/tokens/tailwind.config.js (Tailwind extension)
- .ui-design/tokens/tokens.ts (TypeScript module)
- .ui-design/docs/design-system.md (documentation)
Quick start:
1. CSS: @import '.ui-design/tokens/tokens.css';
2. Tailwind: Spread in your tailwind.config.js
3. TypeScript: import { colors } from '.ui-design/tokens/tokens';
Next steps:
1. Review and customize tokens as needed
2. Run /ui-design:create-component to build with your design system
3. Run /ui-design:design-review to validate existing UI against tokens
Need to modify tokens? Run /ui-design:design-system-setup --preset {preset}
```
## Error Handling
@@ -630,3 +642,4 @@ Need to modify tokens? Run /ui-design:design-system-setup --preset {preset}
- If file write fails: Report error, suggest manual creation
- If color generation fails: Provide manual palette input option
- If tailwind not detected: Skip tailwind output, inform user
```

View File

@@ -21,30 +21,35 @@ Master accessibility implementation to create inclusive experiences that work fo
## Core Capabilities
### 1. WCAG 2.2 Guidelines
- Perceivable: Content must be presentable in different ways
- Operable: Interface must be navigable with keyboard and assistive tech
- Understandable: Content and operation must be clear
- Robust: Content must work with current and future assistive technologies
### 2. ARIA Patterns
- Roles: Define element purpose (button, dialog, navigation)
- States: Indicate current condition (expanded, selected, disabled)
- Properties: Describe relationships and additional info (labelledby, describedby)
- Live regions: Announce dynamic content changes
### 3. Keyboard Navigation
- Focus order and tab sequence
- Focus indicators and visible focus states
- Keyboard shortcuts and hotkeys
- Focus trapping for modals and dialogs
### 4. Screen Reader Support
- Semantic HTML structure
- Alternative text for images
- Proper heading hierarchy
- Skip links and landmarks
### 5. Mobile Accessibility
- Touch target sizing (44x44dp minimum)
- VoiceOver and TalkBack compatibility
- Gesture alternatives
@@ -54,18 +59,18 @@ Master accessibility implementation to create inclusive experiences that work fo
### WCAG 2.2 Success Criteria Checklist
| Level | Criterion | Description |
|-------|-----------|-------------|
| A | 1.1.1 | Non-text content has text alternatives |
| A | 1.3.1 | Info and relationships programmatically determinable |
| A | 2.1.1 | All functionality keyboard accessible |
| A | 2.4.1 | Skip to main content mechanism |
| AA | 1.4.3 | Contrast ratio 4.5:1 (text), 3:1 (large text) |
| AA | 1.4.11 | Non-text contrast 3:1 |
| AA | 2.4.7 | Focus visible |
| AA | 2.5.8 | Target size minimum 24x24px (NEW in 2.2) |
| AAA | 1.4.6 | Enhanced contrast 7:1 |
| AAA | 2.5.5 | Target size minimum 44x44px |
| Level | Criterion | Description |
| ----- | --------- | ---------------------------------------------------- |
| A | 1.1.1 | Non-text content has text alternatives |
| A | 1.3.1 | Info and relationships programmatically determinable |
| A | 2.1.1 | All functionality keyboard accessible |
| A | 2.4.1 | Skip to main content mechanism |
| AA | 1.4.3 | Contrast ratio 4.5:1 (text), 3:1 (large text) |
| AA | 1.4.11 | Non-text contrast 3:1 |
| AA | 2.4.7 | Focus visible |
| AA | 2.5.8 | Target size minimum 24x24px (NEW in 2.2) |
| AAA | 1.4.6 | Enhanced contrast 7:1 |
| AAA | 2.5.5 | Target size minimum 44x44px |
## Key Patterns
@@ -73,13 +78,13 @@ Master accessibility implementation to create inclusive experiences that work fo
```tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
variant?: "primary" | "secondary";
isLoading?: boolean;
}
function AccessibleButton({
children,
variant = 'primary',
variant = "primary",
isLoading = false,
disabled,
...props
@@ -94,11 +99,11 @@ function AccessibleButton({
aria-disabled={disabled || isLoading}
className={cn(
// Visible focus ring
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
// Minimum touch target size (44x44px)
'min-h-[44px] min-w-[44px]',
variant === 'primary' && 'bg-primary text-primary-foreground',
(disabled || isLoading) && 'opacity-50 cursor-not-allowed'
"min-h-[44px] min-w-[44px]",
variant === "primary" && "bg-primary text-primary-foreground",
(disabled || isLoading) && "opacity-50 cursor-not-allowed",
)}
{...props}
>
@@ -118,8 +123,8 @@ function AccessibleButton({
### Pattern 2: Accessible Modal Dialog
```tsx
import * as React from 'react';
import { FocusTrap } from '@headlessui/react';
import * as React from "react";
import { FocusTrap } from "@headlessui/react";
interface DialogProps {
isOpen: boolean;
@@ -135,21 +140,21 @@ function AccessibleDialog({ isOpen, onClose, title, children }: DialogProps) {
// Close on Escape key
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
if (e.key === "Escape" && isOpen) {
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
// Prevent body scroll when open
React.useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
document.body.style.overflow = "hidden";
}
return () => {
document.body.style.overflow = '';
document.body.style.overflow = "";
};
}, [isOpen]);
@@ -227,7 +232,9 @@ function AccessibleForm() {
<div className="space-y-2">
<label htmlFor="email" className="block font-medium">
Email address
<span aria-hidden="true" className="text-destructive ml-1">*</span>
<span aria-hidden="true" className="text-destructive ml-1">
*
</span>
<span className="sr-only">(required)</span>
</label>
<input
@@ -237,10 +244,10 @@ function AccessibleForm() {
required
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : 'email-hint'}
aria-describedby={errors.email ? "email-error" : "email-hint"}
className={cn(
'w-full px-3 py-2 border rounded-md',
errors.email && 'border-destructive'
"w-full px-3 py-2 border rounded-md",
errors.email && "border-destructive",
)}
/>
{errors.email ? (
@@ -271,10 +278,10 @@ function SkipLink() {
href="#main-content"
className={cn(
// Hidden by default, visible on focus
'sr-only focus:not-sr-only',
'focus:absolute focus:top-4 focus:left-4 focus:z-50',
'focus:bg-background focus:px-4 focus:py-2 focus:rounded-md',
'focus:ring-2 focus:ring-primary'
"sr-only focus:not-sr-only",
"focus:absolute focus:top-4 focus:left-4 focus:z-50",
"focus:bg-background focus:px-4 focus:py-2 focus:rounded-md",
"focus:ring-2 focus:ring-primary",
)}
>
Skip to main content
@@ -302,12 +309,15 @@ function Layout({ children }) {
```tsx
function useAnnounce() {
const [message, setMessage] = React.useState('');
const [message, setMessage] = React.useState("");
const announce = React.useCallback((text: string, priority: 'polite' | 'assertive' = 'polite') => {
setMessage(''); // Clear first to ensure re-announcement
setTimeout(() => setMessage(text), 100);
}, []);
const announce = React.useCallback(
(text: string, priority: "polite" | "assertive" = "polite") => {
setMessage(""); // Clear first to ensure re-announcement
setTimeout(() => setMessage(text), 100);
},
[],
);
const Announcer = () => (
<div

View File

@@ -73,7 +73,7 @@ function Accordion({ items }) {
onClick={() => setOpenIndex(isOpen ? -1 : index)}
>
{item.title}
<span aria-hidden="true">{isOpen ? '' : '+'}</span>
<span aria-hidden="true">{isOpen ? "" : "+"}</span>
</button>
</h3>
<div
@@ -103,16 +103,16 @@ function Tabs({ tabs }) {
let newIndex = index;
switch (e.key) {
case 'ArrowRight':
case "ArrowRight":
newIndex = (index + 1) % tabs.length;
break;
case 'ArrowLeft':
case "ArrowLeft":
newIndex = (index - 1 + tabs.length) % tabs.length;
break;
case 'Home':
case "Home":
newIndex = 0;
break;
case 'End':
case "End":
newIndex = tabs.length - 1;
break;
default:
@@ -172,7 +172,7 @@ function MenuButton({ label, items }) {
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowDown':
case "ArrowDown":
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
@@ -181,16 +181,16 @@ function MenuButton({ label, items }) {
setActiveIndex((prev) => Math.min(prev + 1, items.length - 1));
}
break;
case 'ArrowUp':
case "ArrowUp":
e.preventDefault();
setActiveIndex((prev) => Math.max(prev - 1, 0));
break;
case 'Escape':
case "Escape":
setIsOpen(false);
buttonRef.current?.focus();
break;
case 'Enter':
case ' ':
case "Enter":
case " ":
if (isOpen && activeIndex >= 0) {
e.preventDefault();
items[activeIndex].onClick();
@@ -253,36 +253,36 @@ function MenuButton({ label, items }) {
```tsx
function Combobox({ options, onSelect, placeholder }) {
const [inputValue, setInputValue] = useState('');
const [inputValue, setInputValue] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const inputRef = useRef(null);
const listboxId = useId();
const filteredOptions = options.filter((opt) =>
opt.toLowerCase().includes(inputValue.toLowerCase())
opt.toLowerCase().includes(inputValue.toLowerCase()),
);
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowDown':
case "ArrowDown":
e.preventDefault();
setIsOpen(true);
setActiveIndex((prev) =>
Math.min(prev + 1, filteredOptions.length - 1)
Math.min(prev + 1, filteredOptions.length - 1),
);
break;
case 'ArrowUp':
case "ArrowUp":
e.preventDefault();
setActiveIndex((prev) => Math.max(prev - 1, 0));
break;
case 'Enter':
case "Enter":
if (activeIndex >= 0) {
e.preventDefault();
selectOption(filteredOptions[activeIndex]);
}
break;
case 'Escape':
case "Escape":
setIsOpen(false);
setActiveIndex(-1);
break;
@@ -396,16 +396,16 @@ function Toolbar({ items }) {
let newIndex = activeIndex;
switch (e.key) {
case 'ArrowRight':
case "ArrowRight":
newIndex = (activeIndex + 1) % items.length;
break;
case 'ArrowLeft':
case "ArrowLeft":
newIndex = (activeIndex - 1 + items.length) % items.length;
break;
case 'Home':
case "Home":
newIndex = 0;
break;
case 'End':
case "End":
newIndex = items.length - 1;
break;
default:
@@ -414,7 +414,7 @@ function Toolbar({ items }) {
e.preventDefault();
setActiveIndex(newIndex);
toolbarRef.current?.querySelectorAll('button')[newIndex]?.focus();
toolbarRef.current?.querySelectorAll("button")[newIndex]?.focus();
};
return (

View File

@@ -34,12 +34,11 @@ Mobile accessibility ensures apps work for users with disabilities on iOS and An
// Ensure adequate spacing between touch targets
function ButtonGroup({ buttons }) {
return (
<div className="flex gap-3"> {/* 12px minimum gap */}
<div className="flex gap-3">
{" "}
{/* 12px minimum gap */}
{buttons.map((btn) => (
<button
key={btn.id}
className="min-w-[44px] min-h-[44px] px-4 py-2"
>
<button key={btn.id} className="min-w-[44px] min-h-[44px] px-4 py-2">
{btn.label}
</button>
))}
@@ -66,7 +65,7 @@ function IconButton({ icon, label, onClick }) {
### React Native Accessibility Props
```tsx
import { View, Text, TouchableOpacity, AccessibilityInfo } from 'react-native';
import { View, Text, TouchableOpacity, AccessibilityInfo } from "react-native";
// Basic accessible button
function AccessibleButton({ onPress, title, hint }) {
@@ -91,15 +90,15 @@ function ProductCard({ product }) {
accessibilityLabel={`${product.name}, ${product.price}, ${product.rating} stars`}
accessibilityRole="button"
accessibilityActions={[
{ name: 'activate', label: 'View details' },
{ name: 'addToCart', label: 'Add to cart' },
{ name: "activate", label: "View details" },
{ name: "addToCart", label: "Add to cart" },
]}
onAccessibilityAction={(event) => {
switch (event.nativeEvent.actionName) {
case 'addToCart':
case "addToCart":
addToCart(product);
break;
case 'activate':
case "activate":
viewDetails(product);
break;
}
@@ -356,11 +355,9 @@ function SwipeableCard({ item, onDelete }) {
return (
<View
accessible={true}
accessibilityActions={[
{ name: 'delete', label: 'Delete item' },
]}
accessibilityActions={[{ name: "delete", label: "Delete item" }]}
onAccessibilityAction={(event) => {
if (event.nativeEvent.actionName === 'delete') {
if (event.nativeEvent.actionName === "delete") {
onDelete(item);
}
}}
@@ -382,7 +379,7 @@ function SwipeableCard({ item, onDelete }) {
<TouchableOpacity
accessibilityLabel={`Delete ${item.title}`}
onPress={() => onDelete(item)}
style={{ position: 'absolute', right: 0 }}
style={{ position: "absolute", right: 0 }}
>
<Text>Delete</Text>
</TouchableOpacity>
@@ -395,7 +392,7 @@ function SwipeableCard({ item, onDelete }) {
```tsx
// Respect reduced motion preference
import { AccessibilityInfo } from 'react-native';
import { AccessibilityInfo } from "react-native";
function AnimatedComponent() {
const [reduceMotion, setReduceMotion] = useState(false);
@@ -404,8 +401,8 @@ function AnimatedComponent() {
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
const subscription = AccessibilityInfo.addEventListener(
'reduceMotionChanged',
setReduceMotion
"reduceMotionChanged",
setReduceMotion,
);
return () => subscription.remove();
@@ -414,9 +411,7 @@ function AnimatedComponent() {
return (
<Animated.View
style={{
transform: reduceMotion
? []
: [{ translateX: animatedValue }],
transform: reduceMotion ? [] : [{ translateX: animatedValue }],
opacity: reduceMotion ? 1 : animatedOpacity,
}}
>
@@ -503,6 +498,7 @@ const scaledFontSize = (size: number) => {
```markdown
## VoiceOver (iOS) Testing
- [ ] All interactive elements have labels
- [ ] Swipe navigation covers all content in logical order
- [ ] Custom actions available for complex interactions
@@ -511,6 +507,7 @@ const scaledFontSize = (size: number) => {
- [ ] Images have appropriate descriptions or are hidden
## TalkBack (Android) Testing
- [ ] Focus order is logical
- [ ] Touch exploration works correctly
- [ ] Custom actions available
@@ -519,12 +516,14 @@ const scaledFontSize = (size: number) => {
- [ ] Grouped content read together
## Motor Accessibility
- [ ] Touch targets at least 44x44 points
- [ ] Adequate spacing between targets (8dp minimum)
- [ ] Alternatives to complex gestures
- [ ] No time-limited interactions
## Visual Accessibility
- [ ] Text scales to 200% without loss
- [ ] Content visible in high contrast mode
- [ ] Color not sole indicator

View File

@@ -259,7 +259,7 @@ function Tooltip({ content, children }) {
<div
role="tooltip"
// Dismissible: user can close without moving pointer
onKeyDown={(e) => e.key === 'Escape' && setIsVisible(false)}
onKeyDown={(e) => e.key === "Escape" && setIsVisible(false)}
// Hoverable: content stays visible when pointer moves to it
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
@@ -292,7 +292,7 @@ function CustomButton({ onClick, children }) {
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onClick();
}
@@ -331,10 +331,10 @@ function Modal({ isOpen, onClose, children }) {
// Allow Escape to close
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if (e.key === "Escape") onClose();
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
return (
@@ -357,12 +357,18 @@ function Modal({ isOpen, onClose, children }) {
```tsx
// Skip links
<body>
<a href="#main" className="skip-link">Skip to main content</a>
<a href="#nav" className="skip-link">Skip to navigation</a>
<a href="#main" className="skip-link">
Skip to main content
</a>
<a href="#nav" className="skip-link">
Skip to navigation
</a>
<header>...</header>
<nav id="nav" aria-label="Main">...</nav>
<nav id="nav" aria-label="Main">
...
</nav>
<main id="main" tabIndex={-1}>
{/* Main content */}
@@ -455,8 +461,12 @@ Content and interface must be understandable.
```html
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>...</body>
<head>
...
</head>
<body>
...
</body>
</html>
```
@@ -571,7 +581,7 @@ function CustomCheckbox({ checked, onChange, label }) {
aria-label={label}
onClick={() => onChange(!checked)}
>
{checked ? '✓' : '○'} {label}
{checked ? "✓" : "○"} {label}
</button>
);
}
@@ -587,8 +597,8 @@ function CustomSlider({ value, min, max, label, onChange }) {
aria-label={label}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'ArrowRight') onChange(Math.min(value + 1, max));
if (e.key === 'ArrowLeft') onChange(Math.max(value - 1, min));
if (e.key === "ArrowRight") onChange(Math.min(value + 1, max));
if (e.key === "ArrowLeft") onChange(Math.max(value - 1, min));
}}
>
<div style={{ width: `${((value - min) / (max - min)) * 100}%` }} />
@@ -601,6 +611,7 @@ function CustomSlider({ value, min, max, label, onChange }) {
```markdown
## Keyboard Testing
- [ ] All interactive elements focusable with Tab
- [ ] Focus order matches visual order
- [ ] Focus indicator always visible
@@ -609,6 +620,7 @@ function CustomSlider({ value, min, max, label, onChange }) {
- [ ] Enter/Space activates buttons and links
## Screen Reader Testing
- [ ] All images have alt text
- [ ] Form inputs have labels
- [ ] Headings in logical order
@@ -617,6 +629,7 @@ function CustomSlider({ value, min, max, label, onChange }) {
- [ ] Error messages announced
## Visual Testing
- [ ] Text contrast at least 4.5:1
- [ ] UI component contrast at least 3:1
- [ ] Works at 200% zoom

View File

@@ -20,6 +20,7 @@ Master design system architecture to create consistent, maintainable, and scalab
## Core Capabilities
### 1. Design Tokens
- Primitive tokens (raw values: colors, sizes, fonts)
- Semantic tokens (contextual meaning: text-primary, surface-elevated)
- Component tokens (specific usage: button-bg, card-border)
@@ -27,6 +28,7 @@ Master design system architecture to create consistent, maintainable, and scalab
- Multi-platform token generation (CSS, iOS, Android)
### 2. Theming Infrastructure
- CSS custom properties architecture
- Theme context providers in React
- Dynamic theme switching
@@ -35,6 +37,7 @@ Master design system architecture to create consistent, maintainable, and scalab
- Reduced motion and high contrast modes
### 3. Component Architecture
- Compound component patterns
- Polymorphic components (as prop)
- Variant and size systems
@@ -43,6 +46,7 @@ Master design system architecture to create consistent, maintainable, and scalab
- Style props and responsive variants
### 4. Token Pipeline
- Figma to code synchronization
- Style Dictionary configuration
- Token transformation and formatting
@@ -56,32 +60,32 @@ const tokens = {
colors: {
// Primitive tokens
gray: {
50: '#fafafa',
100: '#f5f5f5',
900: '#171717',
50: "#fafafa",
100: "#f5f5f5",
900: "#171717",
},
blue: {
500: '#3b82f6',
600: '#2563eb',
500: "#3b82f6",
600: "#2563eb",
},
},
// Semantic tokens (reference primitives)
semantic: {
light: {
'text-primary': 'var(--color-gray-900)',
'text-secondary': 'var(--color-gray-600)',
'surface-default': 'var(--color-white)',
'surface-elevated': 'var(--color-gray-50)',
'border-default': 'var(--color-gray-200)',
'interactive-primary': 'var(--color-blue-500)',
"text-primary": "var(--color-gray-900)",
"text-secondary": "var(--color-gray-600)",
"surface-default": "var(--color-white)",
"surface-elevated": "var(--color-gray-50)",
"border-default": "var(--color-gray-200)",
"interactive-primary": "var(--color-blue-500)",
},
dark: {
'text-primary': 'var(--color-gray-50)',
'text-secondary': 'var(--color-gray-400)',
'surface-default': 'var(--color-gray-900)',
'surface-elevated': 'var(--color-gray-800)',
'border-default': 'var(--color-gray-700)',
'interactive-primary': 'var(--color-blue-400)',
"text-primary": "var(--color-gray-50)",
"text-secondary": "var(--color-gray-400)",
"surface-default": "var(--color-gray-900)",
"surface-elevated": "var(--color-gray-800)",
"border-default": "var(--color-gray-700)",
"interactive-primary": "var(--color-blue-400)",
},
},
};
@@ -135,13 +139,13 @@ const tokens = {
### Pattern 2: Theme Switching with React
```tsx
import { createContext, useContext, useEffect, useState } from 'react';
import { createContext, useContext, useEffect, useState } from "react";
type Theme = 'light' | 'dark' | 'system';
type Theme = "light" | "dark" | "system";
interface ThemeContextValue {
theme: Theme;
resolvedTheme: 'light' | 'dark';
resolvedTheme: "light" | "dark";
setTheme: (theme: Theme) => void;
}
@@ -149,37 +153,37 @@ const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
if (typeof window !== 'undefined') {
return (localStorage.getItem('theme') as Theme) || 'system';
if (typeof window !== "undefined") {
return (localStorage.getItem("theme") as Theme) || "system";
}
return 'system';
return "system";
});
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("light");
useEffect(() => {
const root = document.documentElement;
const applyTheme = (isDark: boolean) => {
root.classList.remove('light', 'dark');
root.classList.add(isDark ? 'dark' : 'light');
setResolvedTheme(isDark ? 'dark' : 'light');
root.classList.remove("light", "dark");
root.classList.add(isDark ? "dark" : "light");
setResolvedTheme(isDark ? "dark" : "light");
};
if (theme === 'system') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (theme === "system") {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
applyTheme(mediaQuery.matches);
const handler = (e: MediaQueryListEvent) => applyTheme(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
mediaQuery.addEventListener("change", handler);
return () => mediaQuery.removeEventListener("change", handler);
} else {
applyTheme(theme === 'dark');
applyTheme(theme === "dark");
}
}, [theme]);
useEffect(() => {
localStorage.setItem('theme', theme);
localStorage.setItem("theme", theme);
}, [theme]);
return (
@@ -191,7 +195,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
};
```
@@ -199,45 +203,52 @@ export const useTheme = () => {
### Pattern 3: Variant System with CVA
```tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
// Base styles
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
sm: 'h-9 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-11 px-8 text-base',
icon: 'h-10 w-10',
sm: "h-9 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-11 px-8 text-base",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: 'default',
size: 'md',
variant: "default",
size: "md",
},
}
},
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size, className }))} {...props} />
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
```
@@ -247,44 +258,52 @@ export function Button({ className, variant, size, ...props }: ButtonProps) {
```javascript
// style-dictionary.config.js
module.exports = {
source: ['tokens/**/*.json'],
source: ["tokens/**/*.json"],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'dist/css/',
files: [{
destination: 'variables.css',
format: 'css/variables',
options: {
outputReferences: true, // Preserve token references
transformGroup: "css",
buildPath: "dist/css/",
files: [
{
destination: "variables.css",
format: "css/variables",
options: {
outputReferences: true, // Preserve token references
},
},
}],
],
},
scss: {
transformGroup: 'scss',
buildPath: 'dist/scss/',
files: [{
destination: '_variables.scss',
format: 'scss/variables',
}],
transformGroup: "scss",
buildPath: "dist/scss/",
files: [
{
destination: "_variables.scss",
format: "scss/variables",
},
],
},
ios: {
transformGroup: 'ios-swift',
buildPath: 'dist/ios/',
files: [{
destination: 'DesignTokens.swift',
format: 'ios-swift/class.swift',
className: 'DesignTokens',
}],
transformGroup: "ios-swift",
buildPath: "dist/ios/",
files: [
{
destination: "DesignTokens.swift",
format: "ios-swift/class.swift",
className: "DesignTokens",
},
],
},
android: {
transformGroup: 'android',
buildPath: 'dist/android/',
files: [{
destination: 'colors.xml',
format: 'android/colors',
filter: { attributes: { category: 'color' } },
}],
transformGroup: "android",
buildPath: "dist/android/",
files: [
{
destination: "colors.xml",
format: "android/colors",
filter: { attributes: { category: "color" } },
},
],
},
},
};

View File

@@ -10,20 +10,22 @@ Compound components share implicit state through React context, allowing flexibl
```tsx
// Compound component pattern
import * as React from 'react';
import * as React from "react";
interface AccordionContextValue {
openItems: Set<string>;
toggle: (id: string) => void;
type: 'single' | 'multiple';
type: "single" | "multiple";
}
const AccordionContext = React.createContext<AccordionContextValue | null>(null);
const AccordionContext = React.createContext<AccordionContextValue | null>(
null,
);
function useAccordionContext() {
const context = React.useContext(AccordionContext);
if (!context) {
throw new Error('Accordion components must be used within an Accordion');
throw new Error("Accordion components must be used within an Accordion");
}
return context;
}
@@ -31,13 +33,17 @@ function useAccordionContext() {
// Root component
interface AccordionProps {
children: React.ReactNode;
type?: 'single' | 'multiple';
type?: "single" | "multiple";
defaultOpen?: string[];
}
function Accordion({ children, type = 'single', defaultOpen = [] }: AccordionProps) {
function Accordion({
children,
type = "single",
defaultOpen = [],
}: AccordionProps) {
const [openItems, setOpenItems] = React.useState<Set<string>>(
new Set(defaultOpen)
new Set(defaultOpen),
);
const toggle = React.useCallback(
@@ -47,7 +53,7 @@ function Accordion({ children, type = 'single', defaultOpen = [] }: AccordionPro
if (next.has(id)) {
next.delete(id);
} else {
if (type === 'single') {
if (type === "single") {
next.clear();
}
next.add(id);
@@ -55,7 +61,7 @@ function Accordion({ children, type = 'single', defaultOpen = [] }: AccordionPro
return next;
});
},
[type]
[type],
);
return (
@@ -93,7 +99,7 @@ function AccordionTrigger({ children }: { children: React.ReactNode }) {
>
{children}
<ChevronDown
className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
className={`h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
/>
</button>
);
@@ -120,7 +126,7 @@ export const AccordionCompound = Object.assign(Accordion, {
// Usage
function Example() {
return (
<AccordionCompound type="single" defaultOpen={['item-1']}>
<AccordionCompound type="single" defaultOpen={["item-1"]}>
<AccordionCompound.Item id="item-1">
<AccordionCompound.Trigger>Is it accessible?</AccordionCompound.Trigger>
<AccordionCompound.Content>
@@ -144,7 +150,7 @@ Polymorphic components can render as different HTML elements or other components
```tsx
// Polymorphic component with proper TypeScript support
import * as React from 'react';
import * as React from "react";
type AsProp<C extends React.ElementType> = {
as?: C;
@@ -154,64 +160,71 @@ type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
Props = {},
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>['ref'];
React.ComponentPropsWithRef<C>["ref"];
type PolymorphicComponentPropWithRef<
C extends React.ElementType,
Props = {}
Props = {},
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };
// Button component
interface ButtonOwnProps {
variant?: 'default' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
variant?: "default" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
}
type ButtonProps<C extends React.ElementType = 'button'> =
type ButtonProps<C extends React.ElementType = "button"> =
PolymorphicComponentPropWithRef<C, ButtonOwnProps>;
const Button = React.forwardRef(
<C extends React.ElementType = 'button'>(
{ as, variant = 'default', size = 'md', className, children, ...props }: ButtonProps<C>,
ref?: PolymorphicRef<C>
<C extends React.ElementType = "button">(
{
as,
variant = "default",
size = "md",
className,
children,
...props
}: ButtonProps<C>,
ref?: PolymorphicRef<C>,
) => {
const Component = as || 'button';
const Component = as || "button";
const variantClasses = {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
};
const sizeClasses = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base",
};
return (
<Component
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
variantClasses[variant],
sizeClasses[size],
className
className,
)}
{...props}
>
{children}
</Component>
);
}
},
);
Button.displayName = 'Button';
Button.displayName = "Button";
// Usage
function Example() {
@@ -242,31 +255,31 @@ Slots allow users to replace default elements with custom implementations.
```tsx
// Slot pattern for customizable components
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
variant?: 'default' | 'outline';
variant?: "default" | "outline";
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, variant = 'default', className, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
({ asChild = false, variant = "default", className, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md font-medium',
variant === 'default' && 'bg-primary text-primary-foreground',
variant === 'outline' && 'border border-input bg-background',
className
"inline-flex items-center justify-center rounded-md font-medium",
variant === "default" && "bg-primary text-primary-foreground",
variant === "outline" && "border border-input bg-background",
className,
)}
{...props}
/>
);
}
},
);
// Usage - Button styles applied to child element
@@ -285,7 +298,7 @@ Headless components provide behavior without styling, enabling complete visual c
```tsx
// Headless toggle hook
import * as React from 'react';
import * as React from "react";
interface UseToggleProps {
defaultPressed?: boolean;
@@ -315,8 +328,8 @@ function useToggle({
pressed,
toggle,
buttonProps: {
role: 'switch' as const,
'aria-checked': pressed,
role: "switch" as const,
"aria-checked": pressed,
onClick: toggle,
},
};
@@ -334,7 +347,8 @@ function useListbox<T>({
defaultSelectedIndex = -1,
onSelect,
}: UseListboxProps<T>) {
const [selectedIndex, setSelectedIndex] = React.useState(defaultSelectedIndex);
const [selectedIndex, setSelectedIndex] =
React.useState(defaultSelectedIndex);
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
const select = React.useCallback(
@@ -342,40 +356,40 @@ function useListbox<T>({
setSelectedIndex(index);
onSelect?.(items[index], index);
},
[items, onSelect]
[items, onSelect],
);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent) => {
switch (event.key) {
case 'ArrowDown':
case "ArrowDown":
event.preventDefault();
setHighlightedIndex((prev) =>
prev < items.length - 1 ? prev + 1 : prev
prev < items.length - 1 ? prev + 1 : prev,
);
break;
case 'ArrowUp':
case "ArrowUp":
event.preventDefault();
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev));
break;
case 'Enter':
case ' ':
case "Enter":
case " ":
event.preventDefault();
if (highlightedIndex >= 0) {
select(highlightedIndex);
}
break;
case 'Home':
case "Home":
event.preventDefault();
setHighlightedIndex(0);
break;
case 'End':
case "End":
event.preventDefault();
setHighlightedIndex(items.length - 1);
break;
}
},
[items.length, highlightedIndex, select]
[items.length, highlightedIndex, select],
);
return {
@@ -384,13 +398,13 @@ function useListbox<T>({
select,
setHighlightedIndex,
listboxProps: {
role: 'listbox' as const,
role: "listbox" as const,
tabIndex: 0,
onKeyDown: handleKeyDown,
},
getOptionProps: (index: number) => ({
role: 'option' as const,
'aria-selected': index === selectedIndex,
role: "option" as const,
"aria-selected": index === selectedIndex,
onClick: () => select(index),
onMouseEnter: () => setHighlightedIndex(index),
}),
@@ -461,28 +475,28 @@ function Badge({ className, variant, size, ...props }: BadgeProps) {
## Responsive Variants
```tsx
import { cva } from 'class-variance-authority';
import { cva } from "class-variance-authority";
// Responsive variant configuration
const containerVariants = cva('mx-auto w-full px-4', {
const containerVariants = cva("mx-auto w-full px-4", {
variants: {
size: {
sm: 'max-w-screen-sm',
md: 'max-w-screen-md',
lg: 'max-w-screen-lg',
xl: 'max-w-screen-xl',
full: 'max-w-full',
sm: "max-w-screen-sm",
md: "max-w-screen-md",
lg: "max-w-screen-lg",
xl: "max-w-screen-xl",
full: "max-w-full",
},
padding: {
none: 'px-0',
sm: 'px-4 md:px-6',
md: 'px-4 md:px-8 lg:px-12',
lg: 'px-6 md:px-12 lg:px-20',
none: "px-0",
sm: "px-4 md:px-6",
md: "px-4 md:px-8 lg:px-12",
lg: "px-6 md:px-12 lg:px-20",
},
},
defaultVariants: {
size: 'lg',
padding: 'md',
size: "lg",
padding: "md",
},
});
@@ -498,23 +512,23 @@ interface ResponsiveValue<T> {
function getResponsiveClasses<T extends string>(
prop: T | ResponsiveValue<T> | undefined,
classMap: Record<T, string>,
responsiveClassMap: Record<string, Record<T, string>>
responsiveClassMap: Record<string, Record<T, string>>,
): string {
if (!prop) return '';
if (!prop) return "";
if (typeof prop === 'string') {
if (typeof prop === "string") {
return classMap[prop];
}
return Object.entries(prop)
.map(([breakpoint, value]) => {
if (breakpoint === 'base') {
if (breakpoint === "base") {
return classMap[value as T];
}
return responsiveClassMap[breakpoint]?.[value as T];
})
.filter(Boolean)
.join(' ');
.join(" ");
}
```
@@ -555,7 +569,7 @@ function DataList<T>({
keyExtractor={(user) => user.id}
renderItem={(user) => <UserCard user={user} />}
renderEmpty={() => <EmptyState message="No users found" />}
/>
/>;
```
### Children as Function
@@ -577,11 +591,11 @@ function Disclosure({ children, defaultOpen = false }: DisclosureProps) {
<Disclosure>
{({ isOpen, toggle }) => (
<>
<button onClick={toggle}>{isOpen ? 'Close' : 'Open'}</button>
<button onClick={toggle}>{isOpen ? "Close" : "Open"}</button>
{isOpen && <div>Content</div>}
</>
)}
</Disclosure>
</Disclosure>;
```
## Best Practices

View File

@@ -129,9 +129,15 @@ Design tokens are the atomic values of a design system - the smallest pieces tha
{
"shadow": {
"sm": { "value": "0 1px 2px 0 rgb(0 0 0 / 0.05)" },
"md": { "value": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)" },
"lg": { "value": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)" },
"xl": { "value": "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)" }
"md": {
"value": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
},
"lg": {
"value": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
},
"xl": {
"value": "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
}
},
"radius": {
"none": { "value": "0" },
@@ -302,13 +308,13 @@ Examples:
### Style Dictionary Transforms
```javascript
const StyleDictionary = require('style-dictionary');
const StyleDictionary = require("style-dictionary");
// Custom transform for px to rem
StyleDictionary.registerTransform({
name: 'size/pxToRem',
type: 'value',
matcher: (token) => token.attributes.category === 'size',
name: "size/pxToRem",
type: "value",
matcher: (token) => token.attributes.category === "size",
transformer: (token) => {
const value = parseFloat(token.value);
return `${value / 16}rem`;
@@ -317,14 +323,14 @@ StyleDictionary.registerTransform({
// Custom format for CSS custom properties
StyleDictionary.registerFormat({
name: 'css/customProperties',
formatter: function({ dictionary, options }) {
const tokens = dictionary.allTokens.map(token => {
const name = token.name.replace(/\./g, '-');
name: "css/customProperties",
formatter: function ({ dictionary, options }) {
const tokens = dictionary.allTokens.map((token) => {
const name = token.name.replace(/\./g, "-");
return ` --${name}: ${token.value};`;
});
return `:root {\n${tokens.join('\n')}\n}`;
return `:root {\n${tokens.join("\n")}\n}`;
},
});
```
@@ -399,10 +405,10 @@ interface TokenValidation {
function validateContrast(
foreground: string,
background: string,
level: 'AA' | 'AAA' = 'AA'
level: "AA" | "AAA" = "AA",
): boolean {
const ratio = getContrastRatio(foreground, background);
return level === 'AA' ? ratio >= 4.5 : ratio >= 7;
return level === "AA" ? ratio >= 4.5 : ratio >= 7;
}
```

View File

@@ -16,7 +16,7 @@ A robust theming system enables applications to support multiple visual appearan
/* Base tokens that don't change */
--font-sans: Inter, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--font-mono: "JetBrains Mono", monospace;
/* Animation tokens */
--duration-fast: 150ms;
@@ -34,7 +34,7 @@ A robust theming system enables applications to support multiple visual appearan
/* 2. Light theme (default) */
:root,
[data-theme='light'] {
[data-theme="light"] {
--color-bg: #ffffff;
--color-bg-subtle: #f8fafc;
--color-bg-muted: #f1f5f9;
@@ -57,7 +57,7 @@ A robust theming system enables applications to support multiple visual appearan
}
/* 3. Dark theme */
[data-theme='dark'] {
[data-theme="dark"] {
--color-bg: #0f172a;
--color-bg-subtle: #1e293b;
--color-bg-muted: #334155;
@@ -81,7 +81,7 @@ A robust theming system enables applications to support multiple visual appearan
/* 4. System preference detection */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
:root:not([data-theme="light"]) {
/* Inherit dark theme values */
--color-bg: #0f172a;
/* ... other dark values */
@@ -129,16 +129,16 @@ A robust theming system enables applications to support multiple visual appearan
```tsx
// theme-provider.tsx
import * as React from 'react';
import * as React from "react";
type Theme = 'light' | 'dark' | 'system';
type ResolvedTheme = 'light' | 'dark';
type Theme = "light" | "dark" | "system";
type ResolvedTheme = "light" | "dark";
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
attribute?: 'class' | 'data-theme';
attribute?: "class" | "data-theme";
enableSystem?: boolean;
disableTransitionOnChange?: boolean;
}
@@ -150,31 +150,32 @@ interface ThemeProviderState {
toggleTheme: () => void;
}
const ThemeProviderContext = React.createContext<ThemeProviderState | undefined>(
undefined
);
const ThemeProviderContext = React.createContext<
ThemeProviderState | undefined
>(undefined);
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'theme',
attribute = 'data-theme',
defaultTheme = "system",
storageKey = "theme",
attribute = "data-theme",
enableSystem = true,
disableTransitionOnChange = false,
}: ThemeProviderProps) {
const [theme, setThemeState] = React.useState<Theme>(() => {
if (typeof window === 'undefined') return defaultTheme;
if (typeof window === "undefined") return defaultTheme;
return (localStorage.getItem(storageKey) as Theme) || defaultTheme;
});
const [resolvedTheme, setResolvedTheme] = React.useState<ResolvedTheme>('light');
const [resolvedTheme, setResolvedTheme] =
React.useState<ResolvedTheme>("light");
// Get system preference
const getSystemTheme = React.useCallback((): ResolvedTheme => {
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}, []);
// Apply theme to DOM
@@ -184,11 +185,11 @@ export function ThemeProvider({
// Disable transitions temporarily
if (disableTransitionOnChange) {
const css = document.createElement('style');
const css = document.createElement("style");
css.appendChild(
document.createTextNode(
`*,*::before,*::after{transition:none!important}`
)
`*,*::before,*::after{transition:none!important}`,
),
);
document.head.appendChild(css);
@@ -202,8 +203,8 @@ export function ThemeProvider({
}
// Apply attribute
if (attribute === 'class') {
root.classList.remove('light', 'dark');
if (attribute === "class") {
root.classList.remove("light", "dark");
root.classList.add(newTheme);
} else {
root.setAttribute(attribute, newTheme);
@@ -214,27 +215,27 @@ export function ThemeProvider({
setResolvedTheme(newTheme);
},
[attribute, disableTransitionOnChange]
[attribute, disableTransitionOnChange],
);
// Handle theme changes
React.useEffect(() => {
const resolved = theme === 'system' ? getSystemTheme() : theme;
const resolved = theme === "system" ? getSystemTheme() : theme;
applyTheme(resolved);
}, [theme, applyTheme, getSystemTheme]);
// Listen for system theme changes
React.useEffect(() => {
if (!enableSystem || theme !== 'system') return;
if (!enableSystem || theme !== "system") return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
applyTheme(getSystemTheme());
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme, enableSystem, applyTheme, getSystemTheme]);
// Persist to localStorage
@@ -243,11 +244,11 @@ export function ThemeProvider({
localStorage.setItem(storageKey, newTheme);
setThemeState(newTheme);
},
[storageKey]
[storageKey],
);
const toggleTheme = React.useCallback(() => {
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
setTheme(resolvedTheme === "light" ? "dark" : "light");
}, [resolvedTheme, setTheme]);
const value = React.useMemo(
@@ -257,7 +258,7 @@ export function ThemeProvider({
setTheme,
toggleTheme,
}),
[theme, resolvedTheme, setTheme, toggleTheme]
[theme, resolvedTheme, setTheme, toggleTheme],
);
return (
@@ -270,7 +271,7 @@ export function ThemeProvider({
export function useTheme() {
const context = React.useContext(ThemeProviderContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
@@ -280,8 +281,8 @@ export function useTheme() {
```tsx
// theme-toggle.tsx
import { Moon, Sun, Monitor } from 'lucide-react';
import { useTheme } from './theme-provider';
import { Moon, Sun, Monitor } from "lucide-react";
import { useTheme } from "./theme-provider";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
@@ -289,27 +290,27 @@ export function ThemeToggle() {
return (
<div className="flex items-center gap-1 rounded-lg bg-muted p-1">
<button
onClick={() => setTheme('light')}
onClick={() => setTheme("light")}
className={`rounded-md p-2 ${
theme === 'light' ? 'bg-background shadow-sm' : ''
theme === "light" ? "bg-background shadow-sm" : ""
}`}
aria-label="Light theme"
>
<Sun className="h-4 w-4" />
</button>
<button
onClick={() => setTheme('dark')}
onClick={() => setTheme("dark")}
className={`rounded-md p-2 ${
theme === 'dark' ? 'bg-background shadow-sm' : ''
theme === "dark" ? "bg-background shadow-sm" : ""
}`}
aria-label="Dark theme"
>
<Moon className="h-4 w-4" />
</button>
<button
onClick={() => setTheme('system')}
onClick={() => setTheme("system")}
className={`rounded-md p-2 ${
theme === 'system' ? 'bg-background shadow-sm' : ''
theme === "system" ? "bg-background shadow-sm" : ""
}`}
aria-label="System theme"
>
@@ -326,42 +327,42 @@ export function ThemeToggle() {
```css
/* Brand A - Corporate Blue */
[data-brand='corporate'] {
[data-brand="corporate"] {
--brand-primary: #0066cc;
--brand-primary-hover: #0052a3;
--brand-secondary: #f0f7ff;
--brand-accent: #00a3e0;
--brand-font-heading: 'Helvetica Neue', sans-serif;
--brand-font-body: 'Open Sans', sans-serif;
--brand-font-heading: "Helvetica Neue", sans-serif;
--brand-font-body: "Open Sans", sans-serif;
--brand-radius: 0.25rem;
--brand-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Brand B - Modern Startup */
[data-brand='startup'] {
[data-brand="startup"] {
--brand-primary: #7c3aed;
--brand-primary-hover: #6d28d9;
--brand-secondary: #faf5ff;
--brand-accent: #f472b6;
--brand-font-heading: 'Poppins', sans-serif;
--brand-font-body: 'Inter', sans-serif;
--brand-font-heading: "Poppins", sans-serif;
--brand-font-body: "Inter", sans-serif;
--brand-radius: 1rem;
--brand-shadow: 0 4px 12px rgba(124, 58, 237, 0.15);
}
/* Brand C - Minimal */
[data-brand='minimal'] {
[data-brand="minimal"] {
--brand-primary: #171717;
--brand-primary-hover: #404040;
--brand-secondary: #fafafa;
--brand-accent: #171717;
--brand-font-heading: 'Space Grotesk', sans-serif;
--brand-font-body: 'IBM Plex Sans', sans-serif;
--brand-font-heading: "Space Grotesk", sans-serif;
--brand-font-body: "IBM Plex Sans", sans-serif;
--brand-radius: 0;
--brand-shadow: none;
@@ -402,7 +403,7 @@ export function ThemeToggle() {
--color-accent: #0000ee;
}
[data-theme='dark'] {
[data-theme="dark"] {
--color-text: #ffffff;
--color-text-muted: #ffffff;
--color-bg: #000000;
@@ -470,9 +471,9 @@ export default function RootLayout({ children }) {
```tsx
// theme.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider, useTheme } from './theme-provider';
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ThemeProvider, useTheme } from "./theme-provider";
function TestComponent() {
const { theme, setTheme, resolvedTheme } = useTheme();
@@ -480,34 +481,34 @@ function TestComponent() {
<div>
<span data-testid="theme">{theme}</span>
<span data-testid="resolved">{resolvedTheme}</span>
<button onClick={() => setTheme('dark')}>Set Dark</button>
<button onClick={() => setTheme("dark")}>Set Dark</button>
</div>
);
}
describe('ThemeProvider', () => {
it('should default to system theme', () => {
describe("ThemeProvider", () => {
it("should default to system theme", () => {
render(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
</ThemeProvider>,
);
expect(screen.getByTestId('theme')).toHaveTextContent('system');
expect(screen.getByTestId("theme")).toHaveTextContent("system");
});
it('should switch to dark theme', async () => {
it("should switch to dark theme", async () => {
const user = userEvent.setup();
render(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
</ThemeProvider>,
);
await user.click(screen.getByText('Set Dark'));
expect(screen.getByTestId('theme')).toHaveTextContent('dark');
expect(document.documentElement).toHaveAttribute('data-theme', 'dark');
await user.click(screen.getByText("Set Dark"));
expect(screen.getByTestId("theme")).toHaveTextContent("dark");
expect(document.documentElement).toHaveAttribute("data-theme", "dark");
});
});
```

View File

@@ -23,6 +23,7 @@ Create engaging, intuitive interactions through motion, feedback, and thoughtful
### 1. Purposeful Motion
Motion should communicate, not decorate:
- **Feedback**: Confirm user actions occurred
- **Orientation**: Show where elements come from/go to
- **Focus**: Direct attention to important changes
@@ -30,27 +31,27 @@ Motion should communicate, not decorate:
### 2. Timing Guidelines
| Duration | Use Case |
|----------|----------|
| 100-150ms | Micro-feedback (hovers, clicks) |
| 200-300ms | Small transitions (toggles, dropdowns) |
| Duration | Use Case |
| --------- | ----------------------------------------- |
| 100-150ms | Micro-feedback (hovers, clicks) |
| 200-300ms | Small transitions (toggles, dropdowns) |
| 300-500ms | Medium transitions (modals, page changes) |
| 500ms+ | Complex choreographed animations |
| 500ms+ | Complex choreographed animations |
### 3. Easing Functions
```css
/* Common easings */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* Decelerate - entering */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* Accelerate - exiting */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* Both - moving between */
--spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* Overshoot - playful */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* Decelerate - entering */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* Accelerate - exiting */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* Both - moving between */
--spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* Overshoot - playful */
```
## Quick Start: Button Microinteraction
```tsx
import { motion } from 'framer-motion';
import { motion } from "framer-motion";
export function InteractiveButton({ children, onClick }) {
return (
@@ -58,7 +59,7 @@ export function InteractiveButton({ children, onClick }) {
onClick={onClick}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
>
{children}
@@ -72,6 +73,7 @@ export function InteractiveButton({ children, onClick }) {
### 1. Loading States
**Skeleton Screens**: Preserve layout while loading
```tsx
function CardSkeleton() {
return (
@@ -85,6 +87,7 @@ function CardSkeleton() {
```
**Progress Indicators**: Show determinate progress
```tsx
function ProgressBar({ progress }: { progress: number }) {
return (
@@ -93,7 +96,7 @@ function ProgressBar({ progress }: { progress: number }) {
className="h-full bg-blue-600"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ ease: 'easeOut' }}
transition={{ ease: "easeOut" }}
/>
</div>
);
@@ -103,6 +106,7 @@ function ProgressBar({ progress }: { progress: number }) {
### 2. State Transitions
**Toggle with smooth transition**:
```tsx
function Toggle({ checked, onChange }) {
return (
@@ -112,13 +116,13 @@ function Toggle({ checked, onChange }) {
onClick={() => onChange(!checked)}
className={`
relative w-12 h-6 rounded-full transition-colors duration-200
${checked ? 'bg-blue-600' : 'bg-gray-300'}
${checked ? "bg-blue-600" : "bg-gray-300"}
`}
>
<motion.span
className="absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow"
animate={{ x: checked ? 24 : 0 }}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</button>
);
@@ -128,8 +132,9 @@ function Toggle({ checked, onChange }) {
### 3. Page Transitions
**Framer Motion layout animations**:
```tsx
import { AnimatePresence, motion } from 'framer-motion';
import { AnimatePresence, motion } from "framer-motion";
function PageTransition({ children, key }) {
return (
@@ -151,6 +156,7 @@ function PageTransition({ children, key }) {
### 4. Feedback Patterns
**Ripple effect on click**:
```tsx
function RippleButton({ children, onClick }) {
const [ripples, setRipples] = useState([]);
@@ -162,9 +168,9 @@ function RippleButton({ children, onClick }) {
y: e.clientY - rect.top,
id: Date.now(),
};
setRipples(prev => [...prev, ripple]);
setRipples((prev) => [...prev, ripple]);
setTimeout(() => {
setRipples(prev => prev.filter(r => r.id !== ripple.id));
setRipples((prev) => prev.filter((r) => r.id !== ripple.id));
}, 600);
onClick?.(e);
};
@@ -172,7 +178,7 @@ function RippleButton({ children, onClick }) {
return (
<button onClick={handleClick} className="relative overflow-hidden">
{children}
{ripples.map(ripple => (
{ripples.map((ripple) => (
<span
key={ripple.id}
className="absolute bg-white/30 rounded-full animate-ripple"
@@ -187,6 +193,7 @@ function RippleButton({ children, onClick }) {
### 5. Gesture Interactions
**Swipe to dismiss**:
```tsx
function SwipeCard({ children, onDismiss }) {
return (
@@ -212,29 +219,50 @@ function SwipeCard({ children, onDismiss }) {
```css
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.animate-fadeIn { animation: fadeIn 0.3s ease-out; }
.animate-pulse { animation: pulse 2s ease-in-out infinite; }
.animate-spin { animation: spin 1s linear infinite; }
.animate-fadeIn {
animation: fadeIn 0.3s ease-out;
}
.animate-pulse {
animation: pulse 2s ease-in-out infinite;
}
.animate-spin {
animation: spin 1s linear infinite;
}
```
### CSS Transitions
```css
.card {
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
transition:
transform 0.2s ease-out,
box-shadow 0.2s ease-out;
}
.card:hover {
@@ -248,7 +276,9 @@ function SwipeCard({ children, onDismiss }) {
```css
/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
@@ -259,7 +289,7 @@ function SwipeCard({ children, onDismiss }) {
```tsx
function AnimatedComponent() {
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
"(prefers-reduced-motion: reduce)",
).matches;
return (

View File

@@ -7,7 +7,7 @@ The most popular React animation library with declarative API.
### Basic Animations
```tsx
import { motion, AnimatePresence } from 'framer-motion';
import { motion, AnimatePresence } from "framer-motion";
// Simple animation
function FadeIn({ children }) {
@@ -29,7 +29,7 @@ function InteractiveCard() {
<motion.div
whileHover={{ scale: 1.02, y: -4 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="p-6 bg-white rounded-lg shadow"
>
Hover or tap me
@@ -44,9 +44,9 @@ function PulseButton() {
animate={{
scale: [1, 1.05, 1],
boxShadow: [
'0 0 0 0 rgba(59, 130, 246, 0.5)',
'0 0 0 10px rgba(59, 130, 246, 0)',
'0 0 0 0 rgba(59, 130, 246, 0)',
"0 0 0 0 rgba(59, 130, 246, 0.5)",
"0 0 0 10px rgba(59, 130, 246, 0)",
"0 0 0 0 rgba(59, 130, 246, 0)",
],
}}
transition={{ duration: 2, repeat: Infinity }}
@@ -61,7 +61,7 @@ function PulseButton() {
### Layout Animations
```tsx
import { motion, LayoutGroup } from 'framer-motion';
import { motion, LayoutGroup } from "framer-motion";
// Shared layout animation
function TabIndicator({ activeTab, tabs }) {
@@ -78,7 +78,7 @@ function TabIndicator({ activeTab, tabs }) {
<motion.div
layoutId="activeTab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
)}
</button>
@@ -131,11 +131,7 @@ const itemVariants = {
function StaggeredList({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
{items.map((item) => (
<motion.li key={item.id} variants={itemVariants}>
{item.content}
@@ -149,8 +145,8 @@ function StaggeredList({ items }) {
### Page Transitions
```tsx
import { AnimatePresence, motion } from 'framer-motion';
import { useRouter } from 'next/router';
import { AnimatePresence, motion } from "framer-motion";
import { useRouter } from "next/router";
const pageVariants = {
initial: { opacity: 0, x: -20 },
@@ -185,8 +181,8 @@ Industry-standard animation library for complex, performant animations.
### Basic Timeline
```tsx
import { useRef, useLayoutEffect } from 'react';
import gsap from 'gsap';
import { useRef, useLayoutEffect } from "react";
import gsap from "gsap";
function AnimatedHero() {
const containerRef = useRef<HTMLDivElement>(null);
@@ -195,7 +191,7 @@ function AnimatedHero() {
useLayoutEffect(() => {
const ctx = gsap.context(() => {
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
const tl = gsap.timeline({ defaults: { ease: "power3.out" } });
tl.from(titleRef.current, {
y: 50,
@@ -209,9 +205,9 @@ function AnimatedHero() {
opacity: 0,
duration: 0.6,
},
'-=0.4' // Start 0.4s before previous ends
"-=0.4", // Start 0.4s before previous ends
)
.from('.cta-button', {
.from(".cta-button", {
scale: 0.8,
opacity: 0,
duration: 0.4,
@@ -234,9 +230,9 @@ function AnimatedHero() {
### ScrollTrigger
```tsx
import { useLayoutEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
@@ -249,24 +245,24 @@ function ParallaxSection() {
// Parallax image
gsap.to(imageRef.current, {
yPercent: -20,
ease: 'none',
ease: "none",
scrollTrigger: {
trigger: sectionRef.current,
start: 'top bottom',
end: 'bottom top',
start: "top bottom",
end: "bottom top",
scrub: true,
},
});
// Fade in content
gsap.from('.content-block', {
gsap.from(".content-block", {
opacity: 0,
y: 50,
stagger: 0.2,
scrollTrigger: {
trigger: sectionRef.current,
start: 'top 80%',
end: 'top 20%',
start: "top 80%",
end: "top 20%",
scrub: 1,
},
});
@@ -290,9 +286,9 @@ function ParallaxSection() {
### Text Animation
```tsx
import { useLayoutEffect, useRef } from 'react';
import gsap from 'gsap';
import { SplitText } from 'gsap/SplitText';
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);
@@ -301,8 +297,8 @@ function AnimatedHeadline({ text }) {
useLayoutEffect(() => {
const split = new SplitText(textRef.current, {
type: 'chars,words',
charsClass: 'char',
type: "chars,words",
charsClass: "char",
});
gsap.from(split.chars, {
@@ -311,7 +307,7 @@ function AnimatedHeadline({ text }) {
rotateX: -90,
stagger: 0.02,
duration: 0.8,
ease: 'back.out(1.7)',
ease: "back.out(1.7)",
});
return () => split.revert();
@@ -362,7 +358,7 @@ Native browser animation API for simple animations.
function useWebAnimation(
ref: RefObject<HTMLElement>,
keyframes: Keyframe[],
options: KeyframeAnimationOptions
options: KeyframeAnimationOptions,
) {
useEffect(() => {
if (!ref.current) return;
@@ -380,14 +376,14 @@ function SlideIn({ children }) {
useWebAnimation(
elementRef,
[
{ transform: 'translateX(-100%)', opacity: 0 },
{ transform: 'translateX(0)', opacity: 1 },
{ transform: "translateX(-100%)", opacity: 0 },
{ transform: "translateX(0)", opacity: 1 },
],
{
duration: 300,
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
fill: 'forwards',
}
easing: "cubic-bezier(0.16, 1, 0.3, 1)",
fill: "forwards",
},
);
return <div ref={elementRef}>{children}</div>;
@@ -400,7 +396,7 @@ Native browser API for page transitions.
```tsx
// Check support
const supportsViewTransitions = 'startViewTransition' in document;
const supportsViewTransitions = "startViewTransition" in document;
// Simple page transition
async function navigateTo(url: string) {
@@ -456,7 +452,9 @@ function ProductCard({ product }) {
/* Only animate transform and opacity for 60fps */
.smooth {
transition: transform 0.3s ease, opacity 0.3s ease;
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
/* Avoid animating these (cause reflow) */
@@ -472,12 +470,12 @@ function useReducedMotion() {
const [prefersReduced, setPrefersReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
setPrefersReduced(mq.matches);
const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, []);
return prefersReduced;

View File

@@ -5,7 +5,7 @@
### Loading Button
```tsx
import { motion, AnimatePresence } from 'framer-motion';
import { motion, AnimatePresence } from "framer-motion";
interface LoadingButtonProps {
isLoading: boolean;
@@ -71,17 +71,19 @@ function Spinner({ className }: { className?: string }) {
```tsx
function SubmitButton({ onSubmit }: { onSubmit: () => Promise<void> }) {
const [state, setState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [state, setState] = useState<"idle" | "loading" | "success" | "error">(
"idle",
);
const handleClick = async () => {
setState('loading');
setState("loading");
try {
await onSubmit();
setState('success');
setTimeout(() => setState('idle'), 2000);
setState("success");
setTimeout(() => setState("idle"), 2000);
} catch {
setState('error');
setTimeout(() => setState('idle'), 2000);
setState("error");
setTimeout(() => setState("idle"), 2000);
}
};
@@ -93,18 +95,20 @@ function SubmitButton({ onSubmit }: { onSubmit: () => Promise<void> }) {
};
const colors = {
idle: 'bg-blue-600 hover:bg-blue-700',
loading: 'bg-blue-600',
success: 'bg-green-600',
error: 'bg-red-600',
idle: "bg-blue-600 hover:bg-blue-700",
loading: "bg-blue-600",
success: "bg-green-600",
error: "bg-red-600",
};
return (
<motion.button
onClick={handleClick}
disabled={state === 'loading'}
disabled={state === "loading"}
className={`flex items-center gap-2 px-4 py-2 text-white rounded-lg transition-colors ${colors[state]}`}
animate={{ scale: state === 'success' || state === 'error' ? [1, 1.05, 1] : 1 }}
animate={{
scale: state === "success" || state === "error" ? [1, 1.05, 1] : 1,
}}
>
<AnimatePresence mode="wait">
{icons[state] && (
@@ -118,10 +122,10 @@ function SubmitButton({ onSubmit }: { onSubmit: () => Promise<void> }) {
</motion.span>
)}
</AnimatePresence>
{state === 'idle' && 'Submit'}
{state === 'loading' && 'Submitting...'}
{state === 'success' && 'Done!'}
{state === 'error' && 'Failed'}
{state === "idle" && "Submit"}
{state === "loading" && "Submitting..."}
{state === "success" && "Done!"}
{state === "error" && "Failed"}
</motion.button>
);
}
@@ -132,10 +136,16 @@ function SubmitButton({ onSubmit }: { onSubmit: () => Promise<void> }) {
### Floating Label Input
```tsx
import { useState, useId } from 'react';
import { useState, useId } from "react";
function FloatingInput({ label, type = 'text' }: { label: string; type?: string }) {
const [value, setValue] = useState('');
function FloatingInput({
label,
type = "text",
}: {
label: string;
type?: string;
}) {
const [value, setValue] = useState("");
const [isFocused, setIsFocused] = useState(false);
const id = useId();
@@ -156,9 +166,10 @@ function FloatingInput({ label, type = 'text' }: { label: string; type?: string
<label
htmlFor={id}
className={`absolute left-4 transition-all duration-200 pointer-events-none
${isFloating
? 'top-0 -translate-y-1/2 text-xs bg-white px-1 text-blue-600'
: 'top-1/2 -translate-y-1/2 text-gray-500'
${
isFloating
? "top-0 -translate-y-1/2 text-xs bg-white px-1 text-blue-600"
: "top-1/2 -translate-y-1/2 text-gray-500"
}`}
>
{label}
@@ -171,7 +182,7 @@ function FloatingInput({ label, type = 'text' }: { label: string; type?: string
### Shake on Error
```tsx
import { motion, useAnimation } from 'framer-motion';
import { motion, useAnimation } from "framer-motion";
function ShakeInput({ error, ...props }: InputProps & { error?: string }) {
const controls = useAnimation();
@@ -190,7 +201,7 @@ function ShakeInput({ error, ...props }: InputProps & { error?: string }) {
<input
{...props}
className={`w-full px-4 py-2 border rounded-lg ${
error ? 'border-red-500' : 'border-gray-300'
error ? "border-red-500" : "border-gray-300"
}`}
/>
{error && (
@@ -211,7 +222,7 @@ function ShakeInput({ error, ...props }: InputProps & { error?: string }) {
```tsx
function TextareaWithCount({ maxLength = 280 }: { maxLength?: number }) {
const [value, setValue] = useState('');
const [value, setValue] = useState("");
const remaining = maxLength - value.length;
const isNearLimit = remaining <= 20;
const isOverLimit = remaining < 0;
@@ -226,7 +237,11 @@ function TextareaWithCount({ maxLength = 280 }: { maxLength?: number }) {
/>
<motion.span
className={`absolute bottom-2 right-2 text-sm ${
isOverLimit ? 'text-red-500' : isNearLimit ? 'text-yellow-500' : 'text-gray-400'
isOverLimit
? "text-red-500"
: isNearLimit
? "text-yellow-500"
: "text-gray-400"
}`}
animate={{ scale: isNearLimit ? [1, 1.1, 1] : 1 }}
transition={{ duration: 0.2 }}
@@ -243,23 +258,23 @@ function TextareaWithCount({ maxLength = 280 }: { maxLength?: number }) {
### Toast Notifications
```tsx
import { motion, AnimatePresence } from 'framer-motion';
import { createContext, useContext, useState, useCallback } from 'react';
import { motion, AnimatePresence } from "framer-motion";
import { createContext, useContext, useState, useCallback } from "react";
interface Toast {
id: string;
message: string;
type: 'success' | 'error' | 'info';
type: "success" | "error" | "info";
}
const ToastContext = createContext<{
addToast: (message: string, type: Toast['type']) => void;
addToast: (message: string, type: Toast["type"]) => void;
} | null>(null);
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((message: string, type: Toast['type']) => {
const addToast = useCallback((message: string, type: Toast["type"]) => {
const id = Date.now().toString();
setToasts((prev) => [...prev, { id, message, type }]);
setTimeout(() => {
@@ -279,8 +294,11 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, x: 100, scale: 0.95 }}
className={`px-4 py-3 rounded-lg shadow-lg ${
toast.type === 'success' ? 'bg-green-600' :
toast.type === 'error' ? 'bg-red-600' : 'bg-blue-600'
toast.type === "success"
? "bg-green-600"
: toast.type === "error"
? "bg-red-600"
: "bg-blue-600"
} text-white`}
>
{toast.message}
@@ -294,7 +312,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
export function useToast() {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be within ToastProvider');
if (!context) throw new Error("useToast must be within ToastProvider");
return context;
}
```
@@ -304,7 +322,7 @@ export function useToast() {
```tsx
function ConfirmButton({
onConfirm,
confirmText = 'Click again to confirm',
confirmText = "Click again to confirm",
children,
}: {
onConfirm: () => void;
@@ -333,13 +351,13 @@ function ConfirmButton({
<motion.button
onClick={handleClick}
className={`px-4 py-2 rounded-lg transition-colors ${
isPending ? 'bg-red-600 text-white' : 'bg-gray-200 text-gray-800'
isPending ? "bg-red-600 text-white" : "bg-gray-200 text-gray-800"
}`}
animate={{ scale: isPending ? [1, 1.02, 1] : 1 }}
>
<AnimatePresence mode="wait">
<motion.span
key={isPending ? 'confirm' : 'idle'}
key={isPending ? "confirm" : "idle"}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
@@ -357,8 +375,8 @@ function ConfirmButton({
### Active Link Indicator
```tsx
import { motion } from 'framer-motion';
import { usePathname } from 'next/navigation';
import { motion } from "framer-motion";
import { usePathname } from "next/navigation";
function Navigation({ items }: { items: { href: string; label: string }[] }) {
const pathname = usePathname();
@@ -372,14 +390,14 @@ function Navigation({ items }: { items: { href: string; label: string }[] }) {
key={item.href}
href={item.href}
className={`relative px-4 py-2 text-sm font-medium ${
isActive ? 'text-white' : 'text-gray-600 hover:text-gray-900'
isActive ? "text-white" : "text-gray-600 hover:text-gray-900"
}`}
>
{isActive && (
<motion.div
layoutId="activeNav"
className="absolute inset-0 bg-blue-600 rounded-md"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
)}
<span className="relative z-10">{item.label}</span>
@@ -400,9 +418,9 @@ function MenuIcon({ isOpen }: { isOpen: boolean }) {
<motion.span
className="absolute left-0 h-0.5 w-6 bg-current"
animate={{
top: isOpen ? '50%' : '25%',
top: isOpen ? "50%" : "25%",
rotate: isOpen ? 45 : 0,
translateY: isOpen ? '-50%' : 0,
translateY: isOpen ? "-50%" : 0,
}}
transition={{ duration: 0.2 }}
/>
@@ -414,9 +432,9 @@ function MenuIcon({ isOpen }: { isOpen: boolean }) {
<motion.span
className="absolute left-0 h-0.5 w-6 bg-current"
animate={{
bottom: isOpen ? '50%' : '25%',
bottom: isOpen ? "50%" : "25%",
rotate: isOpen ? -45 : 0,
translateY: isOpen ? '50%' : 0,
translateY: isOpen ? "50%" : 0,
}}
transition={{ duration: 0.2 }}
/>
@@ -453,7 +471,7 @@ function LikeButton({ postId, initialLiked, initialCount }) {
<motion.button
onClick={handleLike}
whileTap={{ scale: 0.9 }}
className={`flex items-center gap-2 ${liked ? 'text-red-500' : 'text-gray-500'}`}
className={`flex items-center gap-2 ${liked ? "text-red-500" : "text-gray-500"}`}
>
<motion.span
animate={{ scale: liked ? [1, 1.3, 1] : 1 }}
@@ -479,7 +497,7 @@ function LikeButton({ postId, initialLiked, initialCount }) {
### Pull to Refresh
```tsx
import { motion, useMotionValue, useTransform } from 'framer-motion';
import { motion, useMotionValue, useTransform } from "framer-motion";
function PullToRefresh({ onRefresh, children }) {
const y = useMotionValue(0);

View File

@@ -3,7 +3,7 @@
## Intersection Observer Hook
```tsx
import { useEffect, useRef, useState, type RefObject } from 'react';
import { useEffect, useRef, useState, type RefObject } from "react";
interface UseInViewOptions {
threshold?: number | number[];
@@ -13,7 +13,7 @@ interface UseInViewOptions {
function useInView<T extends HTMLElement>({
threshold = 0,
rootMargin = '0px',
rootMargin = "0px",
triggerOnce = false,
}: UseInViewOptions = {}): [RefObject<T>, boolean] {
const ref = useRef<T>(null);
@@ -31,7 +31,7 @@ function useInView<T extends HTMLElement>({
observer.unobserve(element);
}
},
{ threshold, rootMargin }
{ threshold, rootMargin },
);
observer.observe(element);
@@ -49,7 +49,7 @@ function FadeInSection({ children }) {
<div
ref={ref}
className={`transition-all duration-700 ${
isInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'
isInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
>
{children}
@@ -61,7 +61,7 @@ function FadeInSection({ children }) {
## Scroll Progress Indicator
```tsx
import { motion, useScroll, useSpring } from 'framer-motion';
import { motion, useScroll, useSpring } from "framer-motion";
function ScrollProgress() {
const { scrollYProgress } = useScroll();
@@ -104,26 +104,23 @@ function ScrollProgress() {
### Framer Motion Parallax
```tsx
import { motion, useScroll, useTransform } from 'framer-motion';
import { motion, useScroll, useTransform } from "framer-motion";
function ParallaxHero() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start start', 'end start'],
offset: ["start start", "end start"],
});
const y = useTransform(scrollYProgress, [0, 1], ['0%', '50%']);
const y = useTransform(scrollYProgress, [0, 1], ["0%", "50%"]);
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [1, 1.2]);
return (
<section ref={ref} className="relative h-screen overflow-hidden">
{/* Background image with parallax */}
<motion.div
style={{ y, scale }}
className="absolute inset-0"
>
<motion.div style={{ y, scale }} className="absolute inset-0">
<img src="/hero-bg.jpg" alt="" className="w-full h-full object-cover" />
</motion.div>
@@ -148,7 +145,7 @@ function ScrollAnimation() {
const containerRef = useRef(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start end', 'end start'],
offset: ["start end", "end start"],
});
// Different transformations based on scroll progress
@@ -157,7 +154,7 @@ function ScrollAnimation() {
const backgroundColor = useTransform(
scrollYProgress,
[0, 0.5, 1],
['#3b82f6', '#8b5cf6', '#ec4899']
["#3b82f6", "#8b5cf6", "#ec4899"],
);
return (
@@ -180,13 +177,13 @@ function HorizontalScroll({ items }) {
const containerRef = useRef(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start start', 'end end'],
offset: ["start start", "end end"],
});
const x = useTransform(
scrollYProgress,
[0, 1],
['0%', `-${(items.length - 1) * 100}%`]
["0%", `-${(items.length - 1) * 100}%`],
);
return (
@@ -239,7 +236,7 @@ function StaggeredList({ items }) {
```tsx
function TextReveal({ text }) {
const [ref, isInView] = useInView({ threshold: 0.5, triggerOnce: true });
const words = text.split(' ');
const words = text.split(" ");
return (
<p ref={ref} className="text-4xl font-bold">
@@ -268,8 +265,8 @@ function ClipReveal({ children }) {
return (
<motion.div
ref={ref}
initial={{ clipPath: 'inset(0 100% 0 0)' }}
animate={isInView ? { clipPath: 'inset(0 0% 0 0)' } : {}}
initial={{ clipPath: "inset(0 100% 0 0)" }}
animate={isInView ? { clipPath: "inset(0 0% 0 0)" } : {}}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
>
{children}
@@ -285,7 +282,7 @@ function StickySection({ title, content, image }) {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start start', 'end start'],
offset: ["start start", "end start"],
});
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [1, 1, 0]);
@@ -373,7 +370,10 @@ function FullPageScroll({ sections }) {
return (
<div className="snap-container">
{sections.map((section, i) => (
<section key={i} className="snap-section flex items-center justify-center">
<section
key={i}
className="snap-section flex items-center justify-center"
>
{section}
</section>
))}
@@ -403,14 +403,14 @@ function useThrottledScroll(callback, delay = 16) {
}
};
window.addEventListener('scroll', handler, { passive: true });
return () => window.removeEventListener('scroll', handler);
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, [callback, delay]);
}
// Use transform instead of top/left
// Good
const goodAnimation = { transform: 'translateY(100px)' };
const goodAnimation = { transform: "translateY(100px)" };
// Bad (causes reflow)
const badAnimation = { top: '100px' };
const badAnimation = { top: "100px" };
```

View File

@@ -27,6 +27,7 @@ Master Material Design 3 (Material You) and Jetpack Compose to build modern, ada
**Large Screens**: Responsive layouts for tablets and foldables
**Material Components:**
- Cards, Buttons, FABs, Chips
- Navigation (rail, drawer, bottom nav)
- Text fields, Dialogs, Sheets
@@ -35,6 +36,7 @@ Master Material Design 3 (Material You) and Jetpack Compose to build modern, ada
### 2. Jetpack Compose Layout System
**Column and Row:**
```kotlin
// Vertical arrangement with alignment
Column(
@@ -69,6 +71,7 @@ Row(
```
**Lazy Lists and Grids:**
```kotlin
// Lazy column with sticky headers
LazyColumn {
@@ -105,6 +108,7 @@ LazyVerticalGrid(
### 3. Navigation Patterns
**Bottom Navigation:**
```kotlin
@Composable
fun MainScreen() {
@@ -151,6 +155,7 @@ fun MainScreen() {
```
**Navigation Drawer:**
```kotlin
@Composable
fun DrawerNavigation() {
@@ -205,6 +210,7 @@ fun DrawerNavigation() {
### 4. Material 3 Theming
**Color Scheme:**
```kotlin
// Dynamic color (Android 12+)
val dynamicColorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -231,6 +237,7 @@ private val LightColorScheme = lightColorScheme(
```
**Typography:**
```kotlin
val AppTypography = Typography(
displayLarge = TextStyle(
@@ -269,6 +276,7 @@ val AppTypography = Typography(
### 5. Component Examples
**Cards:**
```kotlin
@Composable
fun FeatureCard(
@@ -312,6 +320,7 @@ fun FeatureCard(
```
**Buttons:**
```kotlin
// Filled button (primary action)
Button(onClick = { }) {

View File

@@ -27,6 +27,7 @@ Master iOS Human Interface Guidelines (HIG) and SwiftUI patterns to build polish
**Depth**: Visual layers and motion convey hierarchy and enable navigation
**Platform Considerations:**
- **iOS**: Touch-first, compact displays, portrait orientation
- **iPadOS**: Larger canvas, multitasking, pointer support
- **visionOS**: Spatial computing, eye/hand input
@@ -34,6 +35,7 @@ Master iOS Human Interface Guidelines (HIG) and SwiftUI patterns to build polish
### 2. SwiftUI Layout System
**Stack-Based Layouts:**
```swift
// Vertical stack with alignment
VStack(alignment: .leading, spacing: 12) {
@@ -55,6 +57,7 @@ HStack {
```
**Grid Layouts:**
```swift
// Adaptive grid that fills available width
LazyVGrid(columns: [
@@ -80,6 +83,7 @@ LazyVGrid(columns: [
### 3. Navigation Patterns
**NavigationStack (iOS 16+):**
```swift
struct ContentView: View {
@State private var path = NavigationPath()
@@ -101,6 +105,7 @@ struct ContentView: View {
```
**TabView:**
```swift
struct MainTabView: View {
@State private var selectedTab = 0
@@ -132,6 +137,7 @@ struct MainTabView: View {
### 4. System Integration
**SF Symbols:**
```swift
// Basic symbol
Image(systemName: "heart.fill")
@@ -150,6 +156,7 @@ Image(systemName: "bell.fill")
```
**Dynamic Type:**
```swift
// Use semantic fonts
Text("Headline")
@@ -166,6 +173,7 @@ Text("Custom")
### 5. Visual Design
**Colors and Materials:**
```swift
// Semantic colors that adapt to light/dark mode
Text("Primary")
@@ -185,6 +193,7 @@ Text("Overlay")
```
**Shadows and Depth:**
```swift
// Standard card shadow
RoundedRectangle(cornerRadius: 16)

View File

@@ -3,6 +3,7 @@
## Lists and Collections
### Basic List
```swift
struct ItemListView: View {
@State private var items: [Item] = []
@@ -32,6 +33,7 @@ struct ItemListView: View {
```
### Sectioned List
```swift
struct SectionedListView: View {
let groupedItems: [String: [Item]]
@@ -52,6 +54,7 @@ struct SectionedListView: View {
```
### Search Integration
```swift
struct SearchableListView: View {
@State private var searchText = ""
@@ -85,6 +88,7 @@ struct SearchableListView: View {
## Forms and Input
### Settings Form
```swift
struct SettingsView: View {
@AppStorage("notifications") private var notificationsEnabled = true
@@ -125,6 +129,7 @@ struct SettingsView: View {
```
### Custom Input Fields
```swift
struct ValidatedTextField: View {
let title: String
@@ -171,6 +176,7 @@ struct ValidatedTextField: View {
## Buttons and Actions
### Button Styles
```swift
// Primary filled button
Button("Continue") {
@@ -202,6 +208,7 @@ struct ScaleButtonStyle: ButtonStyle {
```
### Menu and Context Menu
```swift
// Menu button
Menu {
@@ -226,6 +233,7 @@ Text("Long press me")
## Sheets and Modals
### Sheet Presentation
```swift
struct ParentView: View {
@State private var showSettings = false
@@ -269,6 +277,7 @@ struct SettingsSheet: View {
```
### Confirmation Dialog
```swift
struct DeleteConfirmationView: View {
@State private var showConfirmation = false
@@ -296,6 +305,7 @@ struct DeleteConfirmationView: View {
## Loading and Progress
### Progress Indicators
```swift
// Indeterminate spinner
ProgressView()
@@ -334,6 +344,7 @@ struct LoadingOverlay: View {
```
### Skeleton Loading
```swift
struct SkeletonRow: View {
@State private var isAnimating = false
@@ -366,6 +377,7 @@ struct SkeletonRow: View {
## Async Content Loading
### AsyncImage
```swift
AsyncImage(url: imageURL) { phase in
switch phase {
@@ -387,6 +399,7 @@ AsyncImage(url: imageURL) { phase in
```
### Task-Based Loading
```swift
struct AsyncContentView: View {
@State private var items: [Item] = []
@@ -435,6 +448,7 @@ struct AsyncContentView: View {
## Animations
### Implicit Animations
```swift
struct AnimatedCard: View {
@State private var isExpanded = false
@@ -461,6 +475,7 @@ struct AnimatedCard: View {
```
### Custom Transitions
```swift
extension AnyTransition {
static var slideAndFade: AnyTransition {
@@ -477,6 +492,7 @@ extension AnyTransition {
```
### Phase Animator (iOS 17+)
```swift
struct PulsingButton: View {
var body: some View {
@@ -495,6 +511,7 @@ struct PulsingButton: View {
## Gestures
### Drag Gesture
```swift
struct DraggableCard: View {
@State private var offset = CGSize.zero
@@ -524,6 +541,7 @@ struct DraggableCard: View {
```
### Simultaneous Gestures
```swift
struct ZoomableImage: View {
@State private var scale: CGFloat = 1.0

View File

@@ -23,6 +23,7 @@ Master React Native styling patterns, React Navigation, and Reanimated 3 to buil
### 1. StyleSheet and Styling
**Basic StyleSheet:**
```typescript
import { StyleSheet, View, Text } from 'react-native';
@@ -56,6 +57,7 @@ function Card() {
```
**Dynamic Styles:**
```typescript
interface CardProps {
variant: 'primary' | 'secondary';
@@ -99,30 +101,31 @@ const styles = StyleSheet.create({
### 2. Flexbox Layout
**Row and Column Layouts:**
```typescript
const styles = StyleSheet.create({
// Vertical stack (column)
column: {
flexDirection: 'column',
flexDirection: "column",
gap: 12,
},
// Horizontal stack (row)
row: {
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
gap: 8,
},
// Space between items
spaceBetween: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
// Centered content
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
},
// Fill remaining space
fill: {
@@ -134,6 +137,7 @@ const styles = StyleSheet.create({
### 3. React Navigation Setup
**Stack Navigator:**
```typescript
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
@@ -177,6 +181,7 @@ function AppNavigator() {
```
**Tab Navigator:**
```typescript
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
@@ -216,6 +221,7 @@ function TabNavigator() {
### 4. Reanimated 3 Basics
**Animated Values:**
```typescript
import Animated, {
useSharedValue,
@@ -248,6 +254,7 @@ function AnimatedBox() {
```
**Gesture Handler Integration:**
```typescript
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
@@ -290,14 +297,14 @@ function DraggableCard() {
### 5. Platform-Specific Styling
```typescript
import { Platform, StyleSheet } from 'react-native';
import { Platform, StyleSheet } from "react-native";
const styles = StyleSheet.create({
container: {
padding: 16,
...Platform.select({
ios: {
shadowColor: '#000',
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
@@ -308,14 +315,14 @@ const styles = StyleSheet.create({
}),
},
text: {
fontFamily: Platform.OS === 'ios' ? 'SF Pro Text' : 'Roboto',
fontFamily: Platform.OS === "ios" ? "SF Pro Text" : "Roboto",
fontSize: 16,
},
});
// Platform-specific components
import { Platform } from 'react-native';
const StatusBarHeight = Platform.OS === 'ios' ? 44 : 0;
import { Platform } from "react-native";
const StatusBarHeight = Platform.OS === "ios" ? 44 : 0;
```
## Quick Start Component

View File

@@ -18,9 +18,12 @@ npm install react-native-screens react-native-safe-area-context
```typescript
// navigation/types.ts
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native';
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { BottomTabScreenProps } from "@react-navigation/bottom-tabs";
import {
CompositeScreenProps,
NavigatorScreenParams,
} from "@react-navigation/native";
// Define param lists for each navigator
export type RootStackParamList = {
@@ -63,9 +66,9 @@ declare global {
```typescript
// hooks/useAppNavigation.ts
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from './types';
import { useNavigation, useRoute, RouteProp } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { RootStackParamList } from "./types";
export function useAppNavigation() {
return useNavigation<NativeStackNavigationProp<RootStackParamList>>();
@@ -464,9 +467,9 @@ function App() {
### Handling Deep Links
```typescript
import { useEffect } from 'react';
import { Linking } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useEffect } from "react";
import { Linking } from "react-native";
import { useNavigation } from "@react-navigation/native";
function useDeepLinkHandler() {
const navigation = useNavigation();
@@ -481,7 +484,7 @@ function useDeepLinkHandler() {
};
// Handle URL changes
const subscription = Linking.addEventListener('url', ({ url }) => {
const subscription = Linking.addEventListener("url", ({ url }) => {
handleDeepLink(url);
});

View File

@@ -712,9 +712,12 @@ function BottomSheet({ children }) {
```typescript
// Memoize animated style when dependencies don't change
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}), []); // Empty deps if no external dependencies
const animatedStyle = useAnimatedStyle(
() => ({
transform: [{ translateX: translateX.value }],
}),
[],
); // Empty deps if no external dependencies
// Use useMemo for complex calculations outside worklets
const threshold = useMemo(() => calculateThreshold(screenWidth), [screenWidth]);
@@ -725,7 +728,7 @@ const threshold = useMemo(() => calculateThreshold(screenWidth), [screenWidth]);
```typescript
// Do: Keep worklets simple
const simpleWorklet = () => {
'worklet';
"worklet";
return scale.value * 2;
};
@@ -738,7 +741,7 @@ const onComplete = () => {
};
opacity.value = withTiming(1, {}, (finished) => {
'worklet';
"worklet";
if (finished) {
runOnJS(onComplete)();
}

View File

@@ -5,7 +5,7 @@
### Creating Styles
```typescript
import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native';
import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from "react-native";
// Typed styles for better IDE support
interface Styles {
@@ -18,12 +18,12 @@ const styles = StyleSheet.create<Styles>({
container: {
flex: 1,
padding: 16,
backgroundColor: '#ffffff',
backgroundColor: "#ffffff",
},
title: {
fontSize: 24,
fontWeight: '700',
color: '#1f2937',
fontWeight: "700",
color: "#1f2937",
},
image: {
width: 100,
@@ -425,13 +425,10 @@ export function Spacer({ size, flex }: SpacerProps) {
### Cross-Platform Shadows
```typescript
import { Platform, ViewStyle } from 'react-native';
import { Platform, ViewStyle } from "react-native";
export function createShadow(
elevation: number,
color = '#000000'
): ViewStyle {
if (Platform.OS === 'android') {
export function createShadow(elevation: number, color = "#000000"): ViewStyle {
if (Platform.OS === "android") {
return { elevation };
}
@@ -483,7 +480,7 @@ export const shadows = {
// Usage
const styles = StyleSheet.create({
card: {
backgroundColor: '#ffffff',
backgroundColor: "#ffffff",
borderRadius: 12,
padding: 16,
...shadows.md,

View File

@@ -21,24 +21,28 @@ Master modern responsive design techniques to create interfaces that adapt seaml
## Core Capabilities
### 1. Container Queries
- Component-level responsiveness independent of viewport
- Container query units (cqi, cqw, cqh)
- Style queries for conditional styling
- Fallbacks for browser support
### 2. Fluid Typography & Spacing
- CSS clamp() for fluid scaling
- Viewport-relative units (vw, vh, dvh)
- Fluid type scales with min/max bounds
- Responsive spacing systems
### 3. Layout Patterns
- CSS Grid for 2D layouts
- Flexbox for 1D distribution
- Intrinsic layouts (content-based sizing)
- Subgrid for nested grid alignment
### 4. Breakpoint Strategy
- Mobile-first media queries
- Content-based breakpoints
- Design token integration
@@ -51,11 +55,21 @@ Master modern responsive design techniques to create interfaces that adapt seaml
```css
/* Mobile-first breakpoints */
/* Base: Mobile (< 640px) */
@media (min-width: 640px) { /* sm: Landscape phones, small tablets */ }
@media (min-width: 768px) { /* md: Tablets */ }
@media (min-width: 1024px) { /* lg: Laptops, small desktops */ }
@media (min-width: 1280px) { /* xl: Desktops */ }
@media (min-width: 1536px) { /* 2xl: Large desktops */ }
@media (min-width: 640px) {
/* sm: Landscape phones, small tablets */
}
@media (min-width: 768px) {
/* md: Tablets */
}
@media (min-width: 1024px) {
/* lg: Laptops, small desktops */
}
@media (min-width: 1280px) {
/* xl: Desktops */
}
@media (min-width: 1536px) {
/* 2xl: Large desktops */
}
/* Tailwind CSS equivalent */
/* sm: @media (min-width: 640px) */
@@ -148,10 +162,18 @@ function ResponsiveCard({ title, image, description }) {
}
/* Usage */
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
p { font-size: var(--text-base); }
h1 {
font-size: var(--text-4xl);
}
h2 {
font-size: var(--text-3xl);
}
h3 {
font-size: var(--text-2xl);
}
p {
font-size: var(--text-base);
}
/* Fluid spacing scale */
:root {
@@ -165,7 +187,12 @@ p { font-size: var(--text-base); }
```tsx
// Utility function for fluid values
function fluidValue(minSize: number, maxSize: number, minWidth = 320, maxWidth = 1280) {
function fluidValue(
minSize: number,
maxSize: number,
minWidth = 320,
maxWidth = 1280,
) {
const slope = (maxSize - minSize) / (maxWidth - minWidth);
const yAxisIntersection = -minWidth * slope + minSize;
@@ -178,7 +205,7 @@ const fluidTypeScale = {
base: fluidValue(1, 1.125),
lg: fluidValue(1.25, 1.5),
xl: fluidValue(1.5, 2),
'2xl': fluidValue(2, 3),
"2xl": fluidValue(2, 3),
};
```
@@ -230,19 +257,23 @@ const fluidTypeScale = {
}
}
.header { grid-area: header; }
.main { grid-area: main; }
.sidebar { grid-area: sidebar; }
.footer { grid-area: footer; }
.header {
grid-area: header;
}
.main {
grid-area: main;
}
.sidebar {
grid-area: sidebar;
}
.footer {
grid-area: footer;
}
```
```tsx
// Responsive grid component
function ResponsiveGrid({
children,
minItemWidth = '250px',
gap = '1.5rem',
}) {
function ResponsiveGrid({ children, minItemWidth = "250px", gap = "1.5rem" }) {
return (
<div
className="grid"
@@ -292,12 +323,12 @@ function ResponsiveNav({ items }) {
id="nav-menu"
className={cn(
// Base: hidden on mobile
'absolute top-full left-0 right-0 bg-background border-b',
'flex flex-col',
"absolute top-full left-0 right-0 bg-background border-b",
"flex flex-col",
// Mobile: slide down
isOpen ? 'flex' : 'hidden',
isOpen ? "flex" : "hidden",
// Desktop: always visible, horizontal
'lg:static lg:flex lg:flex-row lg:border-0 lg:bg-transparent'
"lg:static lg:flex lg:flex-row lg:border-0 lg:bg-transparent",
)}
>
{items.map((item) => (
@@ -305,9 +336,9 @@ function ResponsiveNav({ items }) {
<a
href={item.href}
className={cn(
'block px-4 py-3',
'lg:px-3 lg:py-2',
'hover:bg-muted lg:hover:bg-transparent lg:hover:text-primary'
"block px-4 py-3",
"lg:px-3 lg:py-2",
"hover:bg-muted lg:hover:bg-transparent lg:hover:text-primary",
)}
>
{item.label}

View File

@@ -52,11 +52,21 @@ Start with the smallest screen, then progressively enhance for larger screens.
/* xl: 1280px - Desktops */
/* 2xl: 1536px - Large desktops */
@media (min-width: 640px) { /* sm */ }
@media (min-width: 768px) { /* md */ }
@media (min-width: 1024px) { /* lg */ }
@media (min-width: 1280px) { /* xl */ }
@media (min-width: 1536px) { /* 2xl */ }
@media (min-width: 640px) {
/* sm */
}
@media (min-width: 768px) {
/* md */
}
@media (min-width: 1024px) {
/* lg */
}
@media (min-width: 1280px) {
/* xl */
}
@media (min-width: 1536px) {
/* 2xl */
}
```
### Bootstrap 5
@@ -69,11 +79,21 @@ Start with the smallest screen, then progressively enhance for larger screens.
/* xl: 1200px */
/* xxl: 1400px */
@media (min-width: 576px) { /* sm */ }
@media (min-width: 768px) { /* md */ }
@media (min-width: 992px) { /* lg */ }
@media (min-width: 1200px) { /* xl */ }
@media (min-width: 1400px) { /* xxl */ }
@media (min-width: 576px) {
/* sm */
}
@media (min-width: 768px) {
/* md */
}
@media (min-width: 992px) {
/* lg */
}
@media (min-width: 1200px) {
/* xl */
}
@media (min-width: 1400px) {
/* xxl */
}
```
### Minimalist Scale
@@ -89,8 +109,12 @@ Start with the smallest screen, then progressively enhance for larger screens.
--bp-lg: 1024px;
}
@media (min-width: 600px) { /* Medium */ }
@media (min-width: 1024px) { /* Large */ }
@media (min-width: 600px) {
/* Medium */
}
@media (min-width: 1024px) {
/* Large */
}
```
## Content-Based Breakpoints
@@ -101,7 +125,9 @@ Instead of using device-based breakpoints, identify where your content naturally
```css
/* Bad: Device-based thinking */
@media (min-width: 768px) { /* iPad breakpoint */ }
@media (min-width: 768px) {
/* iPad breakpoint */
}
/* Good: Content-based thinking */
/* Breakpoint where sidebar fits comfortably next to content */
@@ -133,7 +159,7 @@ function findBreakpoints(selector) {
// Check for overflow, wrapping, or layout issues
if (element.scrollWidth > element.clientWidth) {
breakpoints.push({ width, issue: 'overflow' });
breakpoints.push({ width, issue: "overflow" });
}
}
@@ -179,7 +205,7 @@ export const breakpoints = {
md: 768,
lg: 1024,
xl: 1280,
'2xl': 1536,
"2xl": 1536,
} as const;
// Media query hook
@@ -191,8 +217,8 @@ function useMediaQuery(query: string): boolean {
setMatches(media.matches);
const listener = () => setMatches(media.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
media.addEventListener("change", listener);
return () => media.removeEventListener("change", listener);
}, [query]);
return matches;
@@ -209,7 +235,15 @@ function useBreakpoint() {
isMobile: !isSmall,
isTablet: isSmall && !isLarge,
isDesktop: isLarge,
current: isXLarge ? 'xl' : isLarge ? 'lg' : isMedium ? 'md' : isSmall ? 'sm' : 'base',
current: isXLarge
? "xl"
: isLarge
? "lg"
: isMedium
? "md"
: isSmall
? "sm"
: "base",
};
}
```
@@ -444,11 +478,14 @@ function useBreakpoint() {
}
/* Handle page breaks */
h1, h2, h3 {
h1,
h2,
h3 {
page-break-after: avoid;
}
img, table {
img,
table {
page-break-inside: avoid;
}
@@ -519,13 +556,16 @@ async function testBreakpoints(page, breakpoints) {
// Check for horizontal overflow
const hasOverflow = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
return (
document.documentElement.scrollWidth >
document.documentElement.clientWidth
);
});
// Check for elements going off-screen
const offscreenElements = await page.evaluate(() => {
const elements = document.querySelectorAll('*');
return Array.from(elements).filter(el => {
const elements = document.querySelectorAll("*");
return Array.from(elements).filter((el) => {
const rect = el.getBoundingClientRect();
return rect.right > window.innerWidth || rect.left < 0;
}).length;

View File

@@ -63,22 +63,30 @@ Container queries have excellent modern browser support (Chrome 105+, Firefox 11
/* Minimum width */
@container (min-width: 300px) {
.element { /* styles */ }
.element {
/* styles */
}
}
/* Maximum width */
@container (max-width: 500px) {
.element { /* styles */ }
.element {
/* styles */
}
}
/* Range syntax */
@container (300px <= width <= 600px) {
.element { /* styles */ }
.element {
/* styles */
}
}
/* Exact width */
@container (width: 400px) {
.element { /* styles */ }
.element {
/* styles */
}
}
```
@@ -87,17 +95,23 @@ Container queries have excellent modern browser support (Chrome 105+, Firefox 11
```css
/* AND condition */
@container (min-width: 400px) and (max-width: 800px) {
.element { /* styles */ }
.element {
/* styles */
}
}
/* OR condition */
@container (max-width: 300px) or (min-width: 800px) {
.element { /* styles */ }
.element {
/* styles */
}
}
/* NOT condition */
@container not (min-width: 400px) {
.element { /* styles */ }
.element {
/* styles */
}
}
```
@@ -403,9 +417,7 @@ Style queries allow querying CSS custom property values. Currently limited suppo
// Tailwind v3.2+ supports container queries
// tailwind.config.js
module.exports = {
plugins: [
require('@tailwindcss/container-queries'),
],
plugins: [require("@tailwindcss/container-queries")],
};
// Component usage
@@ -437,13 +449,9 @@ function Dashboard() {
return (
<div className="@container/main">
<aside className="@container/sidebar">
<nav className="flex flex-col @lg/sidebar:flex-row">
{/* ... */}
</nav>
<nav className="flex flex-col @lg/sidebar:flex-row">{/* ... */}</nav>
</aside>
<main className="@lg/main:grid @lg/main:grid-cols-2">
{/* ... */}
</main>
<main className="@lg/main:grid @lg/main:grid-cols-2">{/* ... */}</main>
</div>
);
}
@@ -506,10 +514,18 @@ function Dashboard() {
```css
/* Avoid over-nesting containers */
/* Bad: Too many nested containers */
.level-1 { container-type: inline-size; }
.level-2 { container-type: inline-size; }
.level-3 { container-type: inline-size; }
.level-4 { container-type: inline-size; }
.level-1 {
container-type: inline-size;
}
.level-2 {
container-type: inline-size;
}
.level-3 {
container-type: inline-size;
}
.level-4 {
container-type: inline-size;
}
/* Good: Strategic container placement */
.component-wrapper {
@@ -528,16 +544,16 @@ function Dashboard() {
```javascript
// Test container query support
const supportsContainerQueries = CSS.supports('container-type', 'inline-size');
const supportsContainerQueries = CSS.supports("container-type", "inline-size");
// Resize observer for testing
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log('Container width:', entry.contentRect.width);
console.log("Container width:", entry.contentRect.width);
}
});
observer.observe(document.querySelector('.container'));
observer.observe(document.querySelector(".container"));
```
## Resources

View File

@@ -61,9 +61,9 @@ const typeScale = {
base: fluidType({ minFontSize: 16, maxFontSize: 18 }),
lg: fluidType({ minFontSize: 18, maxFontSize: 20 }),
xl: fluidType({ minFontSize: 20, maxFontSize: 24 }),
'2xl': fluidType({ minFontSize: 24, maxFontSize: 32 }),
'3xl': fluidType({ minFontSize: 30, maxFontSize: 48 }),
'4xl': fluidType({ minFontSize: 36, maxFontSize: 60 }),
"2xl": fluidType({ minFontSize: 24, maxFontSize: 32 }),
"3xl": fluidType({ minFontSize: 30, maxFontSize: 48 }),
"4xl": fluidType({ minFontSize: 36, maxFontSize: 60 }),
};
```
@@ -98,14 +98,34 @@ body {
line-height: var(--leading-normal);
}
h1 { font-size: var(--text-4xl); line-height: var(--leading-tight); }
h2 { font-size: var(--text-3xl); line-height: var(--leading-tight); }
h3 { font-size: var(--text-2xl); line-height: var(--leading-tight); }
h4 { font-size: var(--text-xl); line-height: var(--leading-normal); }
h5 { font-size: var(--text-lg); line-height: var(--leading-normal); }
h6 { font-size: var(--text-base); line-height: var(--leading-normal); }
h1 {
font-size: var(--text-4xl);
line-height: var(--leading-tight);
}
h2 {
font-size: var(--text-3xl);
line-height: var(--leading-tight);
}
h3 {
font-size: var(--text-2xl);
line-height: var(--leading-tight);
}
h4 {
font-size: var(--text-xl);
line-height: var(--leading-normal);
}
h5 {
font-size: var(--text-lg);
line-height: var(--leading-normal);
}
h6 {
font-size: var(--text-base);
line-height: var(--leading-normal);
}
small { font-size: var(--text-sm); }
small {
font-size: var(--text-sm);
}
```
## Fluid Spacing
@@ -184,10 +204,7 @@ small { font-size: var(--text-sm); }
/* Grid that fills available space */
.auto-grid {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, 250px), 1fr)
);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
gap: var(--space-md);
}
@@ -338,8 +355,8 @@ small { font-size: var(--text-sm); }
}
/* Limit columns */
.switcher > :nth-last-child(n+4),
.switcher > :nth-last-child(n+4) ~ * {
.switcher > :nth-last-child(n + 4),
.switcher > :nth-last-child(n + 4) ~ * {
flex-basis: 100%;
}
```
@@ -405,10 +422,7 @@ small { font-size: var(--text-sm); }
.card-grid {
/* Each card at least 200px, fill available space */
grid-template-columns: repeat(
auto-fit,
minmax(max(200px, 100%/4), 1fr)
);
grid-template-columns: repeat(auto-fit, minmax(max(200px, 100%/4), 1fr));
}
```
@@ -473,21 +487,47 @@ small { font-size: var(--text-sm); }
```css
/* Tailwind-style fluid utilities */
.text-fluid-sm { font-size: var(--text-sm); }
.text-fluid-base { font-size: var(--text-base); }
.text-fluid-lg { font-size: var(--text-lg); }
.text-fluid-xl { font-size: var(--text-xl); }
.text-fluid-2xl { font-size: var(--text-2xl); }
.text-fluid-3xl { font-size: var(--text-3xl); }
.text-fluid-4xl { font-size: var(--text-4xl); }
.text-fluid-sm {
font-size: var(--text-sm);
}
.text-fluid-base {
font-size: var(--text-base);
}
.text-fluid-lg {
font-size: var(--text-lg);
}
.text-fluid-xl {
font-size: var(--text-xl);
}
.text-fluid-2xl {
font-size: var(--text-2xl);
}
.text-fluid-3xl {
font-size: var(--text-3xl);
}
.text-fluid-4xl {
font-size: var(--text-4xl);
}
.p-fluid-sm { padding: var(--space-sm); }
.p-fluid-md { padding: var(--space-md); }
.p-fluid-lg { padding: var(--space-lg); }
.p-fluid-sm {
padding: var(--space-sm);
}
.p-fluid-md {
padding: var(--space-md);
}
.p-fluid-lg {
padding: var(--space-lg);
}
.gap-fluid-sm { gap: var(--space-sm); }
.gap-fluid-md { gap: var(--space-md); }
.gap-fluid-lg { gap: var(--space-lg); }
.gap-fluid-sm {
gap: var(--space-sm);
}
.gap-fluid-md {
gap: var(--space-md);
}
.gap-fluid-lg {
gap: var(--space-lg);
}
```
## Resources

View File

@@ -23,17 +23,18 @@ Build cohesive, accessible visual systems using typography, color, spacing, and
### 1. Typography Scale
**Modular Scale** (ratio-based sizing):
```css
:root {
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-lg: 1.125rem; /* 18px */
--font-size-xl: 1.25rem; /* 20px */
--font-size-2xl: 1.5rem; /* 24px */
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-lg: 1.125rem; /* 18px */
--font-size-xl: 1.25rem; /* 20px */
--font-size-2xl: 1.5rem; /* 24px */
--font-size-3xl: 1.875rem; /* 30px */
--font-size-4xl: 2.25rem; /* 36px */
--font-size-5xl: 3rem; /* 48px */
--font-size-4xl: 2.25rem; /* 36px */
--font-size-5xl: 3rem; /* 48px */
}
```
@@ -47,24 +48,26 @@ Build cohesive, accessible visual systems using typography, color, spacing, and
### 2. Spacing System
**8-point grid** (industry standard):
```css
:root {
--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-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 */
}
```
### 3. Color System
**Semantic color tokens**:
```css
:root {
/* Brand */
@@ -100,29 +103,29 @@ module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
sans: ["Inter", "system-ui", "sans-serif"],
mono: ["JetBrains Mono", "monospace"],
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.25rem' }],
base: ['1rem', { lineHeight: '1.5rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
xs: ["0.75rem", { lineHeight: "1rem" }],
sm: ["0.875rem", { lineHeight: "1.25rem" }],
base: ["1rem", { lineHeight: "1.5rem" }],
lg: ["1.125rem", { lineHeight: "1.75rem" }],
xl: ["1.25rem", { lineHeight: "1.75rem" }],
"2xl": ["1.5rem", { lineHeight: "2rem" }],
},
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
50: "#eff6ff",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
},
},
spacing: {
// Extends default with custom values
'18': '4.5rem',
'88': '22rem',
18: "4.5rem",
88: "22rem",
},
},
},
@@ -134,6 +137,7 @@ module.exports = {
### Font Pairing
**Safe combinations**:
- Heading: **Inter** / Body: **Inter** (single family)
- Heading: **Playfair Display** / Body: **Source Sans Pro** (contrast)
- Heading: **Space Grotesk** / Body: **IBM Plex Sans** (geometric)
@@ -159,8 +163,8 @@ p {
```css
/* Prevent layout shift */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter.woff2') format('woff2');
font-family: "Inter";
src: url("/fonts/Inter.woff2") format("woff2");
font-display: swap;
font-weight: 400 700;
}
@@ -170,12 +174,12 @@ p {
### Contrast Requirements (WCAG)
| Element | Minimum Ratio |
|---------|---------------|
| Body text | 4.5:1 (AA) |
| Large text (18px+) | 3:1 (AA) |
| UI components | 3:1 (AA) |
| Enhanced | 7:1 (AAA) |
| Element | Minimum Ratio |
| ------------------ | ------------- |
| Body text | 4.5:1 (AA) |
| Large text (18px+) | 3:1 (AA) |
| UI components | 3:1 (AA) |
| Enhanced | 7:1 (AAA) |
### Dark Mode Strategy
@@ -204,7 +208,7 @@ p {
function getContrastRatio(foreground: string, background: string): number {
const getLuminance = (hex: string) => {
const rgb = hexToRgb(hex);
const [r, g, b] = rgb.map(c => {
const [r, g, b] = rgb.map((c) => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
@@ -268,7 +272,7 @@ Icon-text gap: 8px (--space-2)
```tsx
interface IconProps {
name: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
size?: "xs" | "sm" | "md" | "lg" | "xl";
className?: string;
}
@@ -280,12 +284,12 @@ const sizeMap = {
xl: 32,
};
export function Icon({ name, size = 'md', className }: IconProps) {
export function Icon({ name, size = "md", className }: IconProps) {
return (
<svg
width={sizeMap[size]}
height={sizeMap[size]}
className={cn('inline-block flex-shrink-0', className)}
className={cn("inline-block flex-shrink-0", className)}
aria-hidden="true"
>
<use href={`/icons.svg#${name}`} />

View File

@@ -15,7 +15,7 @@ Using OKLCH for perceptually uniform color scales:
--blue-200: oklch(86% 0.08 250);
--blue-300: oklch(75% 0.12 250);
--blue-400: oklch(65% 0.16 250);
--blue-500: oklch(55% 0.20 250); /* Primary */
--blue-500: oklch(55% 0.2 250); /* Primary */
--blue-600: oklch(48% 0.18 250);
--blue-700: oklch(40% 0.16 250);
--blue-800: oklch(32% 0.12 250);
@@ -27,26 +27,29 @@ Using OKLCH for perceptually uniform color scales:
### Programmatic Scale Generation
```tsx
function generateColorScale(hue: number, saturation: number = 100): Record<string, string> {
function generateColorScale(
hue: number,
saturation: number = 100,
): Record<string, string> {
const lightnessStops = [
{ name: '50', l: 97 },
{ name: '100', l: 93 },
{ name: '200', l: 85 },
{ name: '300', l: 75 },
{ name: '400', l: 65 },
{ name: '500', l: 55 },
{ name: '600', l: 45 },
{ name: '700', l: 35 },
{ name: '800', l: 25 },
{ name: '900', l: 18 },
{ name: '950', l: 12 },
{ name: "50", l: 97 },
{ name: "100", l: 93 },
{ name: "200", l: 85 },
{ name: "300", l: 75 },
{ name: "400", l: 65 },
{ name: "500", l: 55 },
{ name: "600", l: 45 },
{ name: "700", l: 35 },
{ name: "800", l: 25 },
{ name: "900", l: 18 },
{ name: "950", l: 12 },
];
return Object.fromEntries(
lightnessStops.map(({ name, l }) => [
name,
`hsl(${hue}, ${saturation}%, ${l}%)`,
])
]),
);
}
@@ -167,49 +170,50 @@ const error = generateColorScale(0); // Red
### React Theme Context
```tsx
import { createContext, useContext, useEffect, useState } from 'react';
import { createContext, useContext, useEffect, useState } from "react";
type Theme = 'light' | 'dark' | 'system';
type Theme = "light" | "dark" | "system";
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: 'light' | 'dark';
resolvedTheme: "light" | "dark";
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('system');
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
const [theme, setTheme] = useState<Theme>("system");
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("light");
useEffect(() => {
const root = document.documentElement;
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
setResolvedTheme(systemTheme);
root.setAttribute('data-theme', systemTheme);
root.setAttribute("data-theme", systemTheme);
} else {
setResolvedTheme(theme);
root.setAttribute('data-theme', theme);
root.setAttribute("data-theme", theme);
}
}, [theme]);
useEffect(() => {
if (theme !== 'system') return;
if (theme !== "system") return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handler = (e: MediaQueryListEvent) => {
const newTheme = e.matches ? 'dark' : 'light';
const newTheme = e.matches ? "dark" : "light";
setResolvedTheme(newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
document.documentElement.setAttribute("data-theme", newTheme);
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
mediaQuery.addEventListener("change", handler);
return () => mediaQuery.removeEventListener("change", handler);
}, [theme]);
return (
@@ -221,7 +225,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be within ThemeProvider');
if (!context) throw new Error("useTheme must be within ThemeProvider");
return context;
}
```
@@ -233,7 +237,7 @@ export function useTheme() {
```tsx
function hexToRgb(hex: string): [number, number, number] {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) throw new Error('Invalid hex color');
if (!result) throw new Error("Invalid hex color");
return [
parseInt(result[1], 16),
parseInt(result[2], 16),
@@ -265,8 +269,8 @@ function getContrastRatio(hex1: string, hex2: string): number {
function meetsWCAG(
foreground: string,
background: string,
size: 'normal' | 'large' = 'normal',
level: 'AA' | 'AAA' = 'AA'
size: "normal" | "large" = "normal",
level: "AA" | "AAA" = "AA",
): boolean {
const ratio = getContrastRatio(foreground, background);
@@ -279,8 +283,8 @@ function meetsWCAG(
}
// Usage
meetsWCAG('#ffffff', '#3b82f6'); // true (4.5:1 for AA normal)
meetsWCAG('#ffffff', '#60a5fa'); // false (below 4.5:1)
meetsWCAG("#ffffff", "#3b82f6"); // true (4.5:1 for AA normal)
meetsWCAG("#ffffff", "#60a5fa"); // false (below 4.5:1)
```
### Accessible Color Pairs
@@ -292,14 +296,14 @@ function getAccessibleTextColor(backgroundColor: string): string {
const luminance = getLuminance(r, g, b);
// Use white text on dark backgrounds, black on light
return luminance > 0.179 ? '#111827' : '#ffffff';
return luminance > 0.179 ? "#111827" : "#ffffff";
}
// Find the nearest accessible shade
function findAccessibleShade(
textColor: string,
backgroundScale: string[],
minContrast: number = 4.5
minContrast: number = 4.5,
): string | null {
for (const shade of backgroundScale) {
if (getContrastRatio(textColor, shade) >= minContrast) {
@@ -315,26 +319,22 @@ function findAccessibleShade(
### Harmony Functions
```tsx
type HarmonyType = 'complementary' | 'triadic' | 'analogous' | 'split-complementary';
type HarmonyType =
| "complementary"
| "triadic"
| "analogous"
| "split-complementary";
function generateHarmony(baseHue: number, type: HarmonyType): number[] {
switch (type) {
case 'complementary':
case "complementary":
return [baseHue, (baseHue + 180) % 360];
case 'triadic':
case "triadic":
return [baseHue, (baseHue + 120) % 360, (baseHue + 240) % 360];
case 'analogous':
return [
(baseHue - 30 + 360) % 360,
baseHue,
(baseHue + 30) % 360,
];
case 'split-complementary':
return [
baseHue,
(baseHue + 150) % 360,
(baseHue + 210) % 360,
];
case "analogous":
return [(baseHue - 30 + 360) % 360, baseHue, (baseHue + 30) % 360];
case "split-complementary":
return [baseHue, (baseHue + 150) % 360, (baseHue + 210) % 360];
default:
return [baseHue];
}
@@ -343,13 +343,13 @@ function generateHarmony(baseHue: number, type: HarmonyType): number[] {
// Generate palette from harmony
function generateHarmoniousPalette(
baseHue: number,
type: HarmonyType
type: HarmonyType,
): Record<string, string> {
const hues = generateHarmony(baseHue, type);
const names = ['primary', 'secondary', 'tertiary'];
const names = ["primary", "secondary", "tertiary"];
return Object.fromEntries(
hues.map((hue, i) => [names[i] || `color-${i}`, `hsl(${hue}, 70%, 50%)`])
hues.map((hue, i) => [names[i] || `color-${i}`, `hsl(${hue}, 70%, 50%)`]),
);
}
```
@@ -358,7 +358,7 @@ function generateHarmoniousPalette(
```tsx
// Simulate color blindness
type ColorBlindnessType = 'protanopia' | 'deuteranopia' | 'tritanopia';
type ColorBlindnessType = "protanopia" | "deuteranopia" | "tritanopia";
// Matrix transforms for common types
const colorBlindnessMatrices: Record<ColorBlindnessType, number[][]> = {

View File

@@ -14,28 +14,28 @@ The 8-point grid is the industry standard for consistent spacing.
/* Spacing scale */
--space-0: 0;
--space-px: 1px;
--space-0-5: calc(var(--space-unit) * 0.5); /* 2px */
--space-1: var(--space-unit); /* 4px */
--space-1-5: calc(var(--space-unit) * 1.5); /* 6px */
--space-2: calc(var(--space-unit) * 2); /* 8px */
--space-2-5: calc(var(--space-unit) * 2.5); /* 10px */
--space-3: calc(var(--space-unit) * 3); /* 12px */
--space-3-5: calc(var(--space-unit) * 3.5); /* 14px */
--space-4: calc(var(--space-unit) * 4); /* 16px */
--space-5: calc(var(--space-unit) * 5); /* 20px */
--space-6: calc(var(--space-unit) * 6); /* 24px */
--space-7: calc(var(--space-unit) * 7); /* 28px */
--space-8: calc(var(--space-unit) * 8); /* 32px */
--space-9: calc(var(--space-unit) * 9); /* 36px */
--space-10: calc(var(--space-unit) * 10); /* 40px */
--space-11: calc(var(--space-unit) * 11); /* 44px */
--space-12: calc(var(--space-unit) * 12); /* 48px */
--space-14: calc(var(--space-unit) * 14); /* 56px */
--space-16: calc(var(--space-unit) * 16); /* 64px */
--space-20: calc(var(--space-unit) * 20); /* 80px */
--space-24: calc(var(--space-unit) * 24); /* 96px */
--space-28: calc(var(--space-unit) * 28); /* 112px */
--space-32: calc(var(--space-unit) * 32); /* 128px */
--space-0-5: calc(var(--space-unit) * 0.5); /* 2px */
--space-1: var(--space-unit); /* 4px */
--space-1-5: calc(var(--space-unit) * 1.5); /* 6px */
--space-2: calc(var(--space-unit) * 2); /* 8px */
--space-2-5: calc(var(--space-unit) * 2.5); /* 10px */
--space-3: calc(var(--space-unit) * 3); /* 12px */
--space-3-5: calc(var(--space-unit) * 3.5); /* 14px */
--space-4: calc(var(--space-unit) * 4); /* 16px */
--space-5: calc(var(--space-unit) * 5); /* 20px */
--space-6: calc(var(--space-unit) * 6); /* 24px */
--space-7: calc(var(--space-unit) * 7); /* 28px */
--space-8: calc(var(--space-unit) * 8); /* 32px */
--space-9: calc(var(--space-unit) * 9); /* 36px */
--space-10: calc(var(--space-unit) * 10); /* 40px */
--space-11: calc(var(--space-unit) * 11); /* 44px */
--space-12: calc(var(--space-unit) * 12); /* 48px */
--space-14: calc(var(--space-unit) * 14); /* 56px */
--space-16: calc(var(--space-unit) * 16); /* 64px */
--space-20: calc(var(--space-unit) * 20); /* 80px */
--space-24: calc(var(--space-unit) * 24); /* 96px */
--space-28: calc(var(--space-unit) * 28); /* 112px */
--space-32: calc(var(--space-unit) * 32); /* 128px */
}
```
@@ -44,20 +44,20 @@ The 8-point grid is the industry standard for consistent spacing.
```css
:root {
/* Component-level spacing */
--spacing-xs: var(--space-1); /* 4px - tight spacing */
--spacing-sm: var(--space-2); /* 8px - compact spacing */
--spacing-md: var(--space-4); /* 16px - default spacing */
--spacing-lg: var(--space-6); /* 24px - comfortable spacing */
--spacing-xl: var(--space-8); /* 32px - loose spacing */
--spacing-2xl: var(--space-12); /* 48px - generous spacing */
--spacing-3xl: var(--space-16); /* 64px - section spacing */
--spacing-xs: var(--space-1); /* 4px - tight spacing */
--spacing-sm: var(--space-2); /* 8px - compact spacing */
--spacing-md: var(--space-4); /* 16px - default spacing */
--spacing-lg: var(--space-6); /* 24px - comfortable spacing */
--spacing-xl: var(--space-8); /* 32px - loose spacing */
--spacing-2xl: var(--space-12); /* 48px - generous spacing */
--spacing-3xl: var(--space-16); /* 64px - section spacing */
/* Specific use cases */
--spacing-inline: var(--space-2); /* Between inline elements */
--spacing-stack: var(--space-4); /* Between stacked elements */
--spacing-inset: var(--space-4); /* Padding inside containers */
--spacing-section: var(--space-16); /* Between major sections */
--spacing-page: var(--space-24); /* Page margins */
--spacing-inline: var(--space-2); /* Between inline elements */
--spacing-stack: var(--space-4); /* Between stacked elements */
--spacing-inset: var(--space-4); /* Padding inside containers */
--spacing-section: var(--space-16); /* Between major sections */
--spacing-page: var(--space-24); /* Page margins */
}
```
@@ -67,18 +67,17 @@ The 8-point grid is the industry standard for consistent spacing.
// Tailwind-like spacing scale generator
function createSpacingScale(baseUnit: number = 4): Record<string, string> {
const scale: Record<string, string> = {
'0': '0',
'px': '1px',
"0": "0",
px: "1px",
};
const multipliers = [
0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10,
11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48,
52, 56, 60, 64, 72, 80, 96,
0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24,
28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96,
];
for (const m of multipliers) {
const key = m % 1 === 0 ? String(m) : String(m).replace('.', '-');
const key = m % 1 === 0 ? String(m) : String(m).replace(".", "-");
scale[key] = `${baseUnit * m}px`;
}
@@ -140,15 +139,15 @@ function createSpacingScale(baseUnit: number = 4): Record<string, string> {
```css
:root {
/* Icon sizes aligned to spacing grid */
--icon-xs: 12px; /* Inline decorators */
--icon-sm: 16px; /* Small UI elements */
--icon-md: 20px; /* Default size */
--icon-lg: 24px; /* Emphasis */
--icon-xl: 32px; /* Large displays */
--icon-2xl: 48px; /* Hero icons */
--icon-xs: 12px; /* Inline decorators */
--icon-sm: 16px; /* Small UI elements */
--icon-md: 20px; /* Default size */
--icon-lg: 24px; /* Emphasis */
--icon-xl: 32px; /* Large displays */
--icon-2xl: 48px; /* Hero icons */
/* Touch target sizes */
--touch-target-min: 44px; /* WCAG minimum */
--touch-target-min: 44px; /* WCAG minimum */
--touch-target-comfortable: 48px;
}
```
@@ -156,11 +155,11 @@ function createSpacingScale(baseUnit: number = 4): Record<string, string> {
### SVG Icon Component
```tsx
import { forwardRef, type SVGProps } from 'react';
import { forwardRef, type SVGProps } from "react";
interface IconProps extends SVGProps<SVGSVGElement> {
name: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
label?: string;
}
@@ -170,11 +169,11 @@ const sizeMap = {
md: 20,
lg: 24,
xl: 32,
'2xl': 48,
"2xl": 48,
};
export const Icon = forwardRef<SVGSVGElement, IconProps>(
({ name, size = 'md', label, className, ...props }, ref) => {
({ name, size = "md", label, className, ...props }, ref) => {
const pixelSize = sizeMap[size];
return (
@@ -185,16 +184,16 @@ export const Icon = forwardRef<SVGSVGElement, IconProps>(
className={`inline-block flex-shrink-0 ${className}`}
aria-hidden={!label}
aria-label={label}
role={label ? 'img' : undefined}
role={label ? "img" : undefined}
{...props}
>
<use href={`/icons.svg#${name}`} />
</svg>
);
}
},
);
Icon.displayName = 'Icon';
Icon.displayName = "Icon";
```
### Icon Button Patterns
@@ -203,27 +202,27 @@ Icon.displayName = 'Icon';
interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
icon: string;
label: string;
size?: 'sm' | 'md' | 'lg';
variant?: 'solid' | 'ghost' | 'outline';
size?: "sm" | "md" | "lg";
variant?: "solid" | "ghost" | "outline";
}
const sizeClasses = {
sm: 'p-1.5', /* 32px total with 16px icon */
md: 'p-2', /* 40px total with 20px icon */
lg: 'p-2.5', /* 48px total with 24px icon */
sm: "p-1.5" /* 32px total with 16px icon */,
md: "p-2" /* 40px total with 20px icon */,
lg: "p-2.5" /* 48px total with 24px icon */,
};
const iconSizes = {
sm: 'sm' as const,
md: 'md' as const,
lg: 'lg' as const,
sm: "sm" as const,
md: "md" as const,
lg: "lg" as const,
};
export function IconButton({
icon,
label,
size = 'md',
variant = 'ghost',
size = "md",
variant = "ghost",
className,
...props
}: IconButtonProps) {
@@ -233,9 +232,9 @@ export function IconButton({
inline-flex items-center justify-center rounded-lg
transition-colors focus-visible:outline-none focus-visible:ring-2
${sizeClasses[size]}
${variant === 'solid' && 'bg-blue-600 text-white hover:bg-blue-700'}
${variant === 'ghost' && 'hover:bg-gray-100'}
${variant === 'outline' && 'border border-gray-300 hover:bg-gray-50'}
${variant === "solid" && "bg-blue-600 text-white hover:bg-blue-700"}
${variant === "ghost" && "hover:bg-gray-100"}
${variant === "outline" && "border border-gray-300 hover:bg-gray-50"}
${className}
`}
aria-label={label}
@@ -251,62 +250,62 @@ export function IconButton({
```tsx
// Build script for SVG sprite
import { readdir, readFile, writeFile } from 'fs/promises';
import { optimize } from 'svgo';
import { readdir, readFile, writeFile } from "fs/promises";
import { optimize } from "svgo";
async function buildIconSprite(iconDir: string, outputPath: string) {
const files = await readdir(iconDir);
const svgFiles = files.filter((f) => f.endsWith('.svg'));
const svgFiles = files.filter((f) => f.endsWith(".svg"));
const symbols = await Promise.all(
svgFiles.map(async (file) => {
const content = await readFile(`${iconDir}/${file}`, 'utf-8');
const name = file.replace('.svg', '');
const content = await readFile(`${iconDir}/${file}`, "utf-8");
const name = file.replace(".svg", "");
// Optimize SVG
const result = optimize(content, {
plugins: [
'removeDoctype',
'removeXMLProcInst',
'removeComments',
'removeMetadata',
'removeTitle',
'removeDesc',
'removeUselessDefs',
'removeEditorsNSData',
'removeEmptyAttrs',
'removeHiddenElems',
'removeEmptyText',
'removeEmptyContainers',
'convertStyleToAttrs',
'convertColors',
'convertPathData',
'convertTransform',
'removeUnknownsAndDefaults',
'removeNonInheritableGroupAttrs',
'removeUselessStrokeAndFill',
'removeUnusedNS',
'cleanupNumericValues',
'cleanupListOfValues',
'moveElemsAttrsToGroup',
'moveGroupAttrsToElems',
'collapseGroups',
'mergePaths',
"removeDoctype",
"removeXMLProcInst",
"removeComments",
"removeMetadata",
"removeTitle",
"removeDesc",
"removeUselessDefs",
"removeEditorsNSData",
"removeEmptyAttrs",
"removeHiddenElems",
"removeEmptyText",
"removeEmptyContainers",
"convertStyleToAttrs",
"convertColors",
"convertPathData",
"convertTransform",
"removeUnknownsAndDefaults",
"removeNonInheritableGroupAttrs",
"removeUselessStrokeAndFill",
"removeUnusedNS",
"cleanupNumericValues",
"cleanupListOfValues",
"moveElemsAttrsToGroup",
"moveGroupAttrsToElems",
"collapseGroups",
"mergePaths",
],
});
// Extract viewBox and content
const viewBoxMatch = result.data.match(/viewBox="([^"]+)"/);
const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24';
const viewBox = viewBoxMatch ? viewBoxMatch[1] : "0 0 24 24";
const innerContent = result.data
.replace(/<svg[^>]*>/, '')
.replace(/<\/svg>/, '');
.replace(/<svg[^>]*>/, "")
.replace(/<\/svg>/, "");
return `<symbol id="${name}" viewBox="${viewBox}">${innerContent}</symbol>`;
})
}),
);
const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">${symbols.join('')}</svg>`;
const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">${symbols.join("")}</svg>`;
await writeFile(outputPath, sprite);
console.log(`Generated sprite with ${symbols.length} icons`);
@@ -317,7 +316,7 @@ async function buildIconSprite(iconDir: string, outputPath: string) {
```tsx
// Lucide React
import { Home, Settings, User, Search } from 'lucide-react';
import { Home, Settings, User, Search } from "lucide-react";
function Navigation() {
return (
@@ -331,8 +330,8 @@ function Navigation() {
}
// Heroicons
import { HomeIcon, Cog6ToothIcon } from '@heroicons/react/24/outline';
import { HomeIcon as HomeIconSolid } from '@heroicons/react/24/solid';
import { HomeIcon, Cog6ToothIcon } from "@heroicons/react/24/outline";
import { HomeIcon as HomeIconSolid } from "@heroicons/react/24/solid";
function ToggleIcon({ active }: { active: boolean }) {
const Icon = active ? HomeIconSolid : HomeIcon;
@@ -340,7 +339,7 @@ function ToggleIcon({ active }: { active: boolean }) {
}
// Radix Icons
import { HomeIcon, GearIcon } from '@radix-ui/react-icons';
import { HomeIcon, GearIcon } from "@radix-ui/react-icons";
```
## Sizing Systems
@@ -350,29 +349,29 @@ import { HomeIcon, GearIcon } from '@radix-ui/react-icons';
```css
:root {
/* Fixed sizes */
--size-4: 1rem; /* 16px */
--size-5: 1.25rem; /* 20px */
--size-6: 1.5rem; /* 24px */
--size-8: 2rem; /* 32px */
--size-10: 2.5rem; /* 40px */
--size-12: 3rem; /* 48px */
--size-14: 3.5rem; /* 56px */
--size-16: 4rem; /* 64px */
--size-20: 5rem; /* 80px */
--size-24: 6rem; /* 96px */
--size-32: 8rem; /* 128px */
--size-4: 1rem; /* 16px */
--size-5: 1.25rem; /* 20px */
--size-6: 1.5rem; /* 24px */
--size-8: 2rem; /* 32px */
--size-10: 2.5rem; /* 40px */
--size-12: 3rem; /* 48px */
--size-14: 3.5rem; /* 56px */
--size-16: 4rem; /* 64px */
--size-20: 5rem; /* 80px */
--size-24: 6rem; /* 96px */
--size-32: 8rem; /* 128px */
/* Component heights */
--height-input-sm: var(--size-8); /* 32px */
--height-input-md: var(--size-10); /* 40px */
--height-input-lg: var(--size-12); /* 48px */
--height-input-sm: var(--size-8); /* 32px */
--height-input-md: var(--size-10); /* 40px */
--height-input-lg: var(--size-12); /* 48px */
/* Avatar sizes */
--avatar-xs: var(--size-6); /* 24px */
--avatar-sm: var(--size-8); /* 32px */
--avatar-md: var(--size-10); /* 40px */
--avatar-lg: var(--size-12); /* 48px */
--avatar-xl: var(--size-16); /* 64px */
--avatar-xs: var(--size-6); /* 24px */
--avatar-sm: var(--size-8); /* 32px */
--avatar-md: var(--size-10); /* 40px */
--avatar-lg: var(--size-12); /* 48px */
--avatar-xl: var(--size-16); /* 64px */
--avatar-2xl: var(--size-24); /* 96px */
}
```
@@ -407,13 +406,13 @@ import { HomeIcon, GearIcon } from '@radix-ui/react-icons';
```css
:root {
--radius-none: 0;
--radius-sm: 0.125rem; /* 2px */
--radius-sm: 0.125rem; /* 2px */
--radius-default: 0.25rem; /* 4px */
--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-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;
/* Component-specific */

View File

@@ -9,17 +9,21 @@ A modular scale creates harmonious relationships between font sizes using a math
```tsx
// Common ratios
const RATIOS = {
minorSecond: 1.067, // 16:15
majorSecond: 1.125, // 9:8
minorThird: 1.2, // 6:5
majorThird: 1.25, // 5:4
perfectFourth: 1.333, // 4:3
minorSecond: 1.067, // 16:15
majorSecond: 1.125, // 9:8
minorThird: 1.2, // 6:5
majorThird: 1.25, // 5:4
perfectFourth: 1.333, // 4:3
augmentedFourth: 1.414, // √2
perfectFifth: 1.5, // 3:2
goldenRatio: 1.618, // φ
perfectFifth: 1.5, // 3:2
goldenRatio: 1.618, // φ
};
function generateScale(baseSize: number, ratio: number, steps: number): number[] {
function generateScale(
baseSize: number,
ratio: number,
steps: number,
): number[] {
const scale: number[] = [];
for (let i = -2; i <= steps; i++) {
scale.push(Math.round(baseSize * Math.pow(ratio, i) * 100) / 100);
@@ -37,17 +41,17 @@ const typeScale = generateScale(16, RATIOS.perfectFourth, 6);
```css
:root {
/* Base scale using perfect fourth (1.333) */
--font-size-2xs: 0.563rem; /* ~9px */
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-md: 1.125rem; /* 18px */
--font-size-lg: 1.333rem; /* ~21px */
--font-size-xl: 1.5rem; /* 24px */
--font-size-2xl: 1.777rem; /* ~28px */
--font-size-3xl: 2.369rem; /* ~38px */
--font-size-4xl: 3.157rem; /* ~50px */
--font-size-5xl: 4.209rem; /* ~67px */
--font-size-2xs: 0.563rem; /* ~9px */
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-md: 1.125rem; /* 18px */
--font-size-lg: 1.333rem; /* ~21px */
--font-size-xl: 1.5rem; /* 24px */
--font-size-2xl: 1.777rem; /* ~28px */
--font-size-3xl: 2.369rem; /* ~38px */
--font-size-4xl: 3.157rem; /* ~50px */
--font-size-5xl: 4.209rem; /* ~67px */
/* Font weights */
--font-weight-normal: 400;
@@ -79,8 +83,8 @@ const typeScale = generateScale(16, RATIOS.perfectFourth, 6);
```css
/* Use font-display to control loading behavior */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.woff2') format('woff2-variations');
font-family: "Inter";
src: url("/fonts/Inter-Variable.woff2") format("woff2-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap; /* Show fallback immediately, swap when loaded */
@@ -88,8 +92,8 @@ const typeScale = generateScale(16, RATIOS.perfectFourth, 6);
/* Optional: size-adjust for better fallback matching */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%; /* Adjust to match Inter metrics */
ascent-override: 90%;
descent-override: 22%;
@@ -97,7 +101,7 @@ const typeScale = generateScale(16, RATIOS.perfectFourth, 6);
}
body {
font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;
font-family: "Inter", "Inter Fallback", system-ui, sans-serif;
}
```
@@ -121,15 +125,17 @@ body {
```css
/* Variable font with weight and width axes */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.woff2') format('woff2');
font-family: "Inter";
src: url("/fonts/Inter-Variable.woff2") format("woff2");
font-weight: 100 900;
font-stretch: 75% 125%;
}
/* Use font-variation-settings for fine control */
.custom-weight {
font-variation-settings: 'wght' 450, 'wdth' 95;
font-variation-settings:
"wght" 450,
"wdth" 95;
}
/* Or use standard properties */
@@ -169,9 +175,8 @@ p {
--max-vw: 1200;
line-height: calc(
var(--min-line-height) +
(var(--max-line-height) - var(--min-line-height)) *
((100vw - var(--min-vw) * 1px) / (var(--max-vw) - var(--min-vw)))
var(--min-line-height) + (var(--max-line-height) - var(--min-line-height)) *
((100vw - var(--min-vw) * 1px) / (var(--max-vw) - var(--min-vw)))
);
}
```
@@ -183,15 +188,15 @@ p {
module.exports = {
theme: {
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.25rem' }],
base: ['1rem', { lineHeight: '1.5rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
'5xl': ['3rem', { lineHeight: '1' }],
xs: ["0.75rem", { lineHeight: "1rem" }],
sm: ["0.875rem", { lineHeight: "1.25rem" }],
base: ["1rem", { lineHeight: "1.5rem" }],
lg: ["1.125rem", { lineHeight: "1.75rem" }],
xl: ["1.25rem", { lineHeight: "1.75rem" }],
"2xl": ["1.5rem", { lineHeight: "2rem" }],
"3xl": ["1.875rem", { lineHeight: "2.25rem" }],
"4xl": ["2.25rem", { lineHeight: "2.5rem" }],
"5xl": ["3rem", { lineHeight: "1" }],
},
},
};
@@ -268,7 +273,9 @@ p {
}
/* Balance headings */
h1, h2, h3 {
h1,
h2,
h3 {
text-wrap: balance;
}
@@ -292,20 +299,20 @@ h1, h2, h3 {
```css
/* Serif heading + Sans body */
:root {
--font-heading: 'Playfair Display', Georgia, serif;
--font-body: 'Source Sans Pro', -apple-system, sans-serif;
--font-heading: "Playfair Display", Georgia, serif;
--font-body: "Source Sans Pro", -apple-system, sans-serif;
}
/* Geometric heading + Humanist body */
:root {
--font-heading: 'Space Grotesk', sans-serif;
--font-body: 'IBM Plex Sans', sans-serif;
--font-heading: "Space Grotesk", sans-serif;
--font-body: "IBM Plex Sans", sans-serif;
}
/* Modern sans heading + Classic serif body */
:root {
--font-heading: 'Inter', system-ui, sans-serif;
--font-body: 'Georgia', Times, serif;
--font-heading: "Inter", system-ui, sans-serif;
--font-body: "Georgia", Times, serif;
}
```
@@ -314,7 +321,7 @@ h1, h2, h3 {
```css
/* Single variable font family for all uses */
:root {
--font-family: 'Inter', system-ui, sans-serif;
--font-family: "Inter", system-ui, sans-serif;
}
h1 {
@@ -405,7 +412,7 @@ p {
font-variant-numeric: tabular-nums lining-nums;
/* Fractions */
font-feature-settings: 'frac' 1;
font-feature-settings: "frac" 1;
}
/* Tabular numbers for aligned columns */

View File

@@ -22,6 +22,7 @@ Build reusable, maintainable UI components using modern frameworks with clean co
### 1. Component Composition Patterns
**Compound Components**: Related components that work together
```tsx
// Usage
<Select value={value} onChange={setValue}>
@@ -34,15 +35,17 @@ Build reusable, maintainable UI components using modern frameworks with clean co
```
**Render Props**: Delegate rendering to parent
```tsx
<DataFetcher url="/api/users">
{({ data, loading, error }) => (
{({ data, loading, error }) =>
loading ? <Spinner /> : <UserList users={data} />
)}
}
</DataFetcher>
```
**Slots (Vue/Svelte)**: Named content injection points
```vue
<template>
<Card>
@@ -55,20 +58,20 @@ Build reusable, maintainable UI components using modern frameworks with clean co
### 2. CSS-in-JS Approaches
| Solution | Approach | Best For |
|----------|----------|----------|
| **Tailwind CSS** | Utility classes | Rapid prototyping, design systems |
| **CSS Modules** | Scoped CSS files | Existing CSS, gradual adoption |
| **styled-components** | Template literals | React, dynamic styling |
| **Emotion** | Object/template styles | Flexible, SSR-friendly |
| **Vanilla Extract** | Zero-runtime | Performance-critical apps |
| Solution | Approach | Best For |
| --------------------- | ---------------------- | --------------------------------- |
| **Tailwind CSS** | Utility classes | Rapid prototyping, design systems |
| **CSS Modules** | Scoped CSS files | Existing CSS, gradual adoption |
| **styled-components** | Template literals | React, dynamic styling |
| **Emotion** | Object/template styles | Flexible, SSR-friendly |
| **Vanilla Extract** | Zero-runtime | Performance-critical apps |
### 3. Component API Design
```tsx
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
variant?: "primary" | "secondary" | "ghost";
size?: "sm" | "md" | "lg";
isLoading?: boolean;
isDisabled?: boolean;
leftIcon?: React.ReactNode;
@@ -79,6 +82,7 @@ interface ButtonProps {
```
**Principles**:
- Use semantic prop names (`isLoading` vs `loading`)
- Provide sensible defaults
- Support composition via `children`
@@ -87,34 +91,35 @@ interface ButtonProps {
## Quick Start: React Component with Tailwind
```tsx
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { forwardRef, type ComponentPropsWithoutRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'hover:bg-gray-100 hover:text-gray-900',
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",
ghost: "hover:bg-gray-100 hover:text-gray-900",
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base",
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
variant: "primary",
size: "md",
},
}
},
);
interface ButtonProps
extends ComponentPropsWithoutRef<'button'>,
extends
ComponentPropsWithoutRef<"button">,
VariantProps<typeof buttonVariants> {
isLoading?: boolean;
}
@@ -130,9 +135,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
{isLoading && <Spinner className="mr-2 h-4 w-4" />}
{children}
</button>
)
),
);
Button.displayName = 'Button';
Button.displayName = "Button";
```
## Framework Patterns
@@ -140,7 +145,7 @@ Button.displayName = 'Button';
### React: Compound Components
```tsx
import { createContext, useContext, useState, type ReactNode } from 'react';
import { createContext, useContext, useState, type ReactNode } from "react";
interface AccordionContextValue {
openItems: Set<string>;
@@ -151,7 +156,7 @@ const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordion() {
const context = useContext(AccordionContext);
if (!context) throw new Error('Must be used within Accordion');
if (!context) throw new Error("Must be used within Accordion");
return context;
}
@@ -159,7 +164,7 @@ export function Accordion({ children }: { children: ReactNode }) {
const [openItems, setOpenItems] = useState<Set<string>>(new Set());
const toggle = (id: string) => {
setOpenItems(prev => {
setOpenItems((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
@@ -176,8 +181,12 @@ export function Accordion({ children }: { children: ReactNode }) {
Accordion.Item = function AccordionItem({
id,
title,
children
}: { id: string; title: string; children: ReactNode }) {
children,
}: {
id: string;
title: string;
children: ReactNode;
}) {
const { openItems, toggle } = useAccordion();
const isOpen = openItems.has(id);
@@ -196,20 +205,22 @@ Accordion.Item = function AccordionItem({
```vue
<script setup lang="ts">
import { ref, computed, provide, inject, type InjectionKey } from 'vue';
import { ref, computed, provide, inject, type InjectionKey } from "vue";
interface TabsContext {
activeTab: Ref<string>;
setActive: (id: string) => void;
}
const TabsKey: InjectionKey<TabsContext> = Symbol('tabs');
const TabsKey: InjectionKey<TabsContext> = Symbol("tabs");
// Parent component
const activeTab = ref('tab-1');
const activeTab = ref("tab-1");
provide(TabsKey, {
activeTab,
setActive: (id: string) => { activeTab.value = id; }
setActive: (id: string) => {
activeTab.value = id;
},
});
// Child component usage

View File

@@ -5,8 +5,8 @@
### Modal Dialog
```tsx
import { useEffect, useRef, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useEffect, useRef, type ReactNode } from "react";
import { createPortal } from "react-dom";
interface ModalProps {
isOpen: boolean;
@@ -23,27 +23,27 @@ export function Modal({ isOpen, onClose, title, children }: ModalProps) {
if (isOpen) {
previousActiveElement.current = document.activeElement;
dialogRef.current?.focus();
document.body.style.overflow = 'hidden';
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = '';
document.body.style.overflow = "";
(previousActiveElement.current as HTMLElement)?.focus();
}
return () => {
document.body.style.overflow = '';
document.body.style.overflow = "";
};
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if (e.key === 'Tab') trapFocus(e, dialogRef.current);
if (e.key === "Escape") onClose();
if (e.key === "Tab") trapFocus(e, dialogRef.current);
};
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
document.addEventListener("keydown", handleKeyDown);
}
return () => document.removeEventListener('keydown', handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
@@ -82,7 +82,7 @@ export function Modal({ isOpen, onClose, title, children }: ModalProps) {
<div className="mt-4">{children}</div>
</div>
</div>,
document.body
document.body,
);
}
@@ -90,7 +90,7 @@ function trapFocus(e: KeyboardEvent, container: HTMLElement | null) {
if (!container) return;
const focusableElements = container.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
@@ -108,7 +108,7 @@ function trapFocus(e: KeyboardEvent, container: HTMLElement | null) {
### Dropdown Menu
```tsx
import { useState, useRef, useEffect, type ReactNode } from 'react';
import { useState, useRef, useEffect, type ReactNode } from "react";
interface DropdownProps {
trigger: ReactNode;
@@ -124,22 +124,25 @@ export function Dropdown({ trigger, children, label }: DropdownProps) {
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'Escape':
case "Escape":
setIsOpen(false);
triggerRef.current?.focus();
break;
case 'ArrowDown':
case "ArrowDown":
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
@@ -147,17 +150,17 @@ export function Dropdown({ trigger, children, label }: DropdownProps) {
focusNextItem(menuRef.current, 1);
}
break;
case 'ArrowUp':
case "ArrowUp":
e.preventDefault();
if (isOpen) {
focusNextItem(menuRef.current, -1);
}
break;
case 'Home':
case "Home":
e.preventDefault();
focusFirstItem(menuRef.current);
break;
case 'End':
case "End":
e.preventDefault();
focusLastItem(menuRef.current);
break;
@@ -177,7 +180,7 @@ export function Dropdown({ trigger, children, label }: DropdownProps) {
{trigger}
<ChevronDownIcon
aria-hidden="true"
className={`transition-transform ${isOpen ? 'rotate-180' : ''}`}
className={`transition-transform ${isOpen ? "rotate-180" : ""}`}
/>
</button>
@@ -217,18 +220,26 @@ export function MenuItem({ children, onClick, disabled }: MenuItemProps) {
function focusNextItem(menu: HTMLElement | null, direction: 1 | -1) {
if (!menu) return;
const items = menu.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])');
const currentIndex = Array.from(items).indexOf(document.activeElement as HTMLElement);
const items = menu.querySelectorAll<HTMLElement>(
'[role="menuitem"]:not([disabled])',
);
const currentIndex = Array.from(items).indexOf(
document.activeElement as HTMLElement,
);
const nextIndex = (currentIndex + direction + items.length) % items.length;
items[nextIndex]?.focus();
}
function focusFirstItem(menu: HTMLElement | null) {
menu?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus();
menu
?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')
?.focus();
}
function focusLastItem(menu: HTMLElement | null) {
const items = menu?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])');
const items = menu?.querySelectorAll<HTMLElement>(
'[role="menuitem"]:not([disabled])',
);
items?.[items.length - 1]?.focus();
}
```
@@ -236,7 +247,13 @@ function focusLastItem(menu: HTMLElement | null) {
### Combobox / Autocomplete
```tsx
import { useState, useRef, useId, type ChangeEvent, type KeyboardEvent } from 'react';
import {
useState,
useRef,
useId,
type ChangeEvent,
type KeyboardEvent,
} from "react";
interface Option {
value: string;
@@ -259,7 +276,7 @@ export function Combobox({
placeholder,
}: ComboboxProps) {
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState('');
const [inputValue, setInputValue] = useState("");
const [activeIndex, setActiveIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
@@ -268,7 +285,7 @@ export function Combobox({
const listboxId = useId();
const filteredOptions = options.filter((option) =>
option.label.toLowerCase().includes(inputValue.toLowerCase())
option.label.toLowerCase().includes(inputValue.toLowerCase()),
);
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
@@ -286,27 +303,27 @@ export function Combobox({
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
case "ArrowDown":
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setActiveIndex((prev) =>
prev < filteredOptions.length - 1 ? prev + 1 : prev
prev < filteredOptions.length - 1 ? prev + 1 : prev,
);
}
break;
case 'ArrowUp':
case "ArrowUp":
e.preventDefault();
setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev));
break;
case 'Enter':
case "Enter":
e.preventDefault();
if (activeIndex >= 0 && filteredOptions[activeIndex]) {
handleSelect(filteredOptions[activeIndex]);
}
break;
case 'Escape':
case "Escape":
setIsOpen(false);
break;
}
@@ -353,8 +370,8 @@ export function Combobox({
aria-selected={activeIndex === index}
onClick={() => handleSelect(option)}
className={`cursor-pointer px-3 py-2 ${
activeIndex === index ? 'bg-blue-100' : 'hover:bg-gray-100'
} ${value === option.value ? 'font-medium' : ''}`}
activeIndex === index ? "bg-blue-100" : "hover:bg-gray-100"
} ${value === option.value ? "font-medium" : ""}`}
>
{option.label}
</li>
@@ -375,7 +392,7 @@ export function Combobox({
### Form Validation
```tsx
import { useId, type FormEvent } from 'react';
import { useId, type FormEvent } from "react";
interface FormFieldProps {
label: string;
@@ -383,12 +400,17 @@ interface FormFieldProps {
required?: boolean;
children: (props: {
id: string;
'aria-describedby': string | undefined;
'aria-invalid': boolean;
"aria-describedby": string | undefined;
"aria-invalid": boolean;
}) => ReactNode;
}
export function FormField({ label, error, required, children }: FormFieldProps) {
export function FormField({
label,
error,
required,
children,
}: FormFieldProps) {
const id = useId();
const errorId = `${id}-error`;
@@ -405,8 +427,8 @@ export function FormField({ label, error, required, children }: FormFieldProps)
{children({
id,
'aria-describedby': error ? errorId : undefined,
'aria-invalid': !!error,
"aria-describedby": error ? errorId : undefined,
"aria-invalid": !!error,
})}
{error && (
@@ -436,13 +458,16 @@ function ContactForm() {
type="email"
required
className={`w-full rounded border px-3 py-2 ${
props['aria-invalid'] ? 'border-red-500' : 'border-gray-300'
props["aria-invalid"] ? "border-red-500" : "border-gray-300"
}`}
/>
)}
</FormField>
<button type="submit" className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">
<button
type="submit"
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
>
Submit
</button>
</form>
@@ -476,19 +501,22 @@ export function SkipLinks() {
## Live Regions
```tsx
import { useState, useEffect } from 'react';
import { useState, useEffect } from "react";
interface LiveAnnouncerProps {
message: string;
politeness?: 'polite' | 'assertive';
politeness?: "polite" | "assertive";
}
export function LiveAnnouncer({ message, politeness = 'polite' }: LiveAnnouncerProps) {
const [announcement, setAnnouncement] = useState('');
export function LiveAnnouncer({
message,
politeness = "polite",
}: LiveAnnouncerProps) {
const [announcement, setAnnouncement] = useState("");
useEffect(() => {
// Clear first, then set - ensures screen readers pick up the change
setAnnouncement('');
setAnnouncement("");
const timer = setTimeout(() => setAnnouncement(message), 100);
return () => clearTimeout(timer);
}, [message]);
@@ -506,9 +534,15 @@ export function LiveAnnouncer({ message, politeness = 'polite' }: LiveAnnouncerP
}
// Usage in a search component
function SearchResults({ results, loading }: { results: Item[]; loading: boolean }) {
function SearchResults({
results,
loading,
}: {
results: Item[];
loading: boolean;
}) {
const message = loading
? 'Loading results...'
? "Loading results..."
: `${results.length} results found`;
return (
@@ -544,12 +578,14 @@ function useFocusTrap(containerRef: RefObject<HTMLElement>, isActive: boolean) {
if (!isActive || !containerRef.current) return;
const container = containerRef.current;
const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const focusableSelector =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.key !== "Tab") return;
const focusableElements = container.querySelectorAll<HTMLElement>(focusableSelector);
const focusableElements =
container.querySelectorAll<HTMLElement>(focusableSelector);
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
@@ -562,8 +598,8 @@ function useFocusTrap(containerRef: RefObject<HTMLElement>, isActive: boolean) {
}
};
container.addEventListener('keydown', handleKeyDown);
return () => container.removeEventListener('keydown', handleKeyDown);
container.addEventListener("keydown", handleKeyDown);
return () => container.removeEventListener("keydown", handleKeyDown);
}, [containerRef, isActive]);
}
```
@@ -595,8 +631,12 @@ function getContrastRatio(fg: string, bg: string): number {
return (lighter + 0.05) / (darker + 0.05);
}
function meetsWCAG(fg: string, bg: string, level: 'AA' | 'AAA' = 'AA'): boolean {
function meetsWCAG(
fg: string,
bg: string,
level: "AA" | "AAA" = "AA",
): boolean {
const ratio = getContrastRatio(fg, bg);
return level === 'AAA' ? ratio >= 7 : ratio >= 4.5;
return level === "AAA" ? ratio >= 7 : ratio >= 4.5;
}
```

View File

@@ -15,7 +15,7 @@ import {
type ReactNode,
type Dispatch,
type SetStateAction,
} from 'react';
} from "react";
// Types
interface TabsContextValue {
@@ -51,7 +51,7 @@ const TabsContext = createContext<TabsContextValue | null>(null);
function useTabs() {
const context = useContext(TabsContext);
if (!context) {
throw new Error('Tabs components must be used within <Tabs>');
throw new Error("Tabs components must be used within <Tabs>");
}
return context;
}
@@ -62,11 +62,11 @@ export function Tabs({ defaultValue, children, onChange }: TabsProps) {
const handleChange: Dispatch<SetStateAction<string>> = useCallback(
(value) => {
const newValue = typeof value === 'function' ? value(activeTab) : value;
const newValue = typeof value === "function" ? value(activeTab) : value;
setActiveTab(newValue);
onChange?.(newValue);
},
[activeTab, onChange]
[activeTab, onChange],
);
return (
@@ -100,10 +100,12 @@ Tabs.Tab = function Tab({ value, children, disabled }: TabProps) {
onClick={() => setActiveTab(value)}
className={`
px-4 py-2 font-medium transition-colors
${isActive
? 'border-b-2 border-blue-600 text-blue-600'
: 'text-gray-600 hover:text-gray-900'}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${
isActive
? "border-b-2 border-blue-600 text-blue-600"
: "text-gray-600 hover:text-gray-900"
}
${disabled ? "opacity-50 cursor-not-allowed" : ""}
`}
>
{children}
@@ -138,7 +140,9 @@ Tabs.Panel = function TabPanel({ value, children }: TabPanelProps) {
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="features">Features</Tabs.Tab>
<Tabs.Tab value="pricing" disabled>Pricing</Tabs.Tab>
<Tabs.Tab value="pricing" disabled>
Pricing
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">
<h2>Product Overview</h2>
@@ -180,14 +184,14 @@ function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
});
const fetchData = useCallback(async () => {
setState(prev => ({ ...prev, loading: true, error: null }));
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Fetch failed');
if (!response.ok) throw new Error("Fetch failed");
const data = await response.json();
setState({ data, loading: false, error: null });
} catch (error) {
setState(prev => ({ ...prev, loading: false, error: error as Error }));
setState((prev) => ({ ...prev, loading: false, error: error as Error }));
}
}, [url]);
@@ -205,7 +209,7 @@ function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
return <UserList users={data!} />;
}}
</DataLoader>
</DataLoader>;
```
## Polymorphic Components
@@ -343,18 +347,10 @@ function Card({ children, header, footer, media }: CardProps) {
return (
<article className="rounded-lg border bg-white shadow-sm">
{media && (
<div className="aspect-video overflow-hidden rounded-t-lg">
{media}
</div>
<div className="aspect-video overflow-hidden rounded-t-lg">{media}</div>
)}
{header && (
<header className="border-b px-4 py-3">
{header}
</header>
)}
<div className="px-4 py-4">
{children}
</div>
{header && <header className="border-b px-4 py-3">{header}</header>}
<div className="px-4 py-4">{children}</div>
{footer && (
<footer className="border-t px-4 py-3 bg-gray-50 rounded-b-lg">
{footer}
@@ -371,7 +367,7 @@ function Card({ children, header, footer, media }: CardProps) {
footer={<Button>Action</Button>}
>
<p>Card content goes here.</p>
</Card>
</Card>;
```
## Forward Ref Pattern
@@ -379,7 +375,7 @@ function Card({ children, header, footer, media }: CardProps) {
Allow parent components to access the underlying DOM node.
```tsx
import { forwardRef, useRef, useImperativeHandle } from 'react';
import { forwardRef, useRef, useImperativeHandle } from "react";
interface InputHandle {
focus: () => void;
@@ -399,9 +395,9 @@ const FancyInput = forwardRef<InputHandle, FancyInputProps>(
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) inputRef.current.value = '';
if (inputRef.current) inputRef.current.value = "";
},
getValue: () => inputRef.current?.value ?? '',
getValue: () => inputRef.current?.value ?? "",
}));
return (
@@ -415,10 +411,10 @@ const FancyInput = forwardRef<InputHandle, FancyInputProps>(
/>
</div>
);
}
},
);
FancyInput.displayName = 'FancyInput';
FancyInput.displayName = "FancyInput";
// Usage
function Form() {

View File

@@ -2,13 +2,13 @@
## Comparison Matrix
| Approach | Runtime | Bundle Size | Learning Curve | Dynamic Styles | SSR |
|----------|---------|-------------|----------------|----------------|-----|
| CSS Modules | None | Minimal | Low | Limited | Yes |
| Tailwind | None | Small (purged) | Medium | Via classes | Yes |
| styled-components | Yes | Medium | Medium | Full | Yes* |
| Emotion | Yes | Medium | Medium | Full | Yes |
| Vanilla Extract | None | Minimal | High | Limited | Yes |
| Approach | Runtime | Bundle Size | Learning Curve | Dynamic Styles | SSR |
| ----------------- | ------- | -------------- | -------------- | -------------- | ----- |
| CSS Modules | None | Minimal | Low | Limited | Yes |
| Tailwind | None | Small (purged) | Medium | Via classes | Yes |
| styled-components | Yes | Medium | Medium | Full | Yes\* |
| Emotion | Yes | Medium | Medium | Full | Yes |
| Vanilla Extract | None | Minimal | High | Limited | Yes |
## CSS Modules
@@ -56,19 +56,19 @@ Scoped CSS with zero runtime overhead.
```tsx
// Button.tsx
import styles from './Button.module.css';
import { clsx } from 'clsx';
import styles from "./Button.module.css";
import { clsx } from "clsx";
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
variant?: "primary" | "secondary";
size?: "small" | "medium" | "large";
children: React.ReactNode;
onClick?: () => void;
}
export function Button({
variant = 'primary',
size = 'medium',
variant = "primary",
size = "medium",
children,
onClick,
}: ButtonProps) {
@@ -77,7 +77,7 @@ export function Button({
className={clsx(
styles.button,
styles[variant],
size !== 'medium' && styles[size]
size !== "medium" && styles[size],
)}
onClick={onClick}
>
@@ -104,7 +104,7 @@ export function Button({
/* Button.module.css */
.srOnly {
composes: visuallyHidden from './base.module.css';
composes: visuallyHidden from "./base.module.css";
}
```
@@ -115,38 +115,42 @@ Utility-first CSS with design system constraints.
### Class Variance Authority (CVA)
```tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
// Base styles
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: 'default',
size: 'default',
variant: "default",
size: "default",
},
}
},
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
@@ -160,7 +164,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{...props}
/>
);
}
},
);
```
@@ -168,48 +172,48 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
```tsx
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Usage - handles conflicting classes
cn('px-4 py-2', 'px-6'); // => 'py-2 px-6'
cn('text-red-500', condition && 'text-blue-500'); // => 'text-blue-500' if condition
cn("px-4 py-2", "px-6"); // => 'py-2 px-6'
cn("text-red-500", condition && "text-blue-500"); // => 'text-blue-500' if condition
```
### Custom Plugin
```js
// tailwind.config.js
const plugin = require('tailwindcss/plugin');
const plugin = require("tailwindcss/plugin");
module.exports = {
plugins: [
plugin(function({ addUtilities, addComponents, theme }) {
plugin(function ({ addUtilities, addComponents, theme }) {
// Add utilities
addUtilities({
'.text-balance': {
'text-wrap': 'balance',
".text-balance": {
"text-wrap": "balance",
},
'.scrollbar-hide': {
'-ms-overflow-style': 'none',
'scrollbar-width': 'none',
'&::-webkit-scrollbar': {
display: 'none',
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
// Add components
addComponents({
'.card': {
backgroundColor: theme('colors.white'),
borderRadius: theme('borderRadius.lg'),
padding: theme('spacing.6'),
boxShadow: theme('boxShadow.md'),
".card": {
backgroundColor: theme("colors.white"),
borderRadius: theme("borderRadius.lg"),
padding: theme("spacing.6"),
boxShadow: theme("boxShadow.md"),
},
});
}),
@@ -222,7 +226,7 @@ module.exports = {
CSS-in-JS with template literals.
```tsx
import styled, { css, keyframes } from 'styled-components';
import styled, { css, keyframes } from "styled-components";
// Keyframes
const fadeIn = keyframes`
@@ -232,8 +236,8 @@ const fadeIn = keyframes`
// Base button with variants
interface ButtonProps {
$variant?: 'primary' | 'secondary' | 'ghost';
$size?: 'sm' | 'md' | 'lg';
$variant?: "primary" | "secondary" | "ghost";
$size?: "sm" | "md" | "lg";
$isLoading?: boolean;
}
@@ -287,8 +291,8 @@ const Button = styled.button<ButtonProps>`
transition: all 0.2s ease;
animation: ${fadeIn} 0.3s ease;
${({ $size = 'md' }) => sizeStyles[$size]}
${({ $variant = 'primary' }) => variantStyles[$variant]}
${({ $size = "md" }) => sizeStyles[$size]}
${({ $variant = "primary" }) => variantStyles[$variant]}
&:disabled {
opacity: 0.5;
@@ -312,12 +316,12 @@ const IconButton = styled(Button)`
// Theme provider
const theme = {
colors: {
primary: '#2563eb',
primaryHover: '#1d4ed8',
secondary: '#f3f4f6',
secondaryHover: '#e5e7eb',
ghost: 'rgba(0, 0, 0, 0.05)',
text: '#1f2937',
primary: "#2563eb",
primaryHover: "#1d4ed8",
secondary: "#f3f4f6",
secondaryHover: "#e5e7eb",
ghost: "rgba(0, 0, 0, 0.05)",
text: "#1f2937",
},
};
@@ -326,7 +330,7 @@ const theme = {
<Button $variant="primary" $size="lg">
Click me
</Button>
</ThemeProvider>
</ThemeProvider>;
```
## Emotion
@@ -335,11 +339,11 @@ Flexible CSS-in-JS with object and template syntax.
```tsx
/** @jsxImportSource @emotion/react */
import { css, Theme, ThemeProvider, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { css, Theme, ThemeProvider, useTheme } from "@emotion/react";
import styled from "@emotion/styled";
// Theme typing
declare module '@emotion/react' {
declare module "@emotion/react" {
export interface Theme {
colors: {
primary: string;
@@ -352,20 +356,21 @@ declare module '@emotion/react' {
const theme: Theme = {
colors: {
primary: '#2563eb',
background: '#ffffff',
text: '#1f2937',
primary: "#2563eb",
background: "#ffffff",
text: "#1f2937",
},
spacing: (factor: number) => `${factor * 0.25}rem`,
};
// Object syntax
const cardStyles = (theme: Theme) => css({
backgroundColor: theme.colors.background,
padding: theme.spacing(4),
borderRadius: '0.5rem',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
});
const cardStyles = (theme: Theme) =>
css({
backgroundColor: theme.colors.background,
padding: theme.spacing(4),
borderRadius: "0.5rem",
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)",
});
// Template literal syntax
const buttonStyles = css`
@@ -407,7 +412,7 @@ function Alert({ children }: { children: React.ReactNode }) {
<Card>
<Alert>Important message</Alert>
</Card>
</ThemeProvider>
</ThemeProvider>;
```
## Vanilla Extract
@@ -416,26 +421,26 @@ Zero-runtime CSS-in-JS with full type safety.
```tsx
// styles.css.ts
import { style, styleVariants, createTheme } from '@vanilla-extract/css';
import { recipe, type RecipeVariants } from '@vanilla-extract/recipes';
import { style, styleVariants, createTheme } from "@vanilla-extract/css";
import { recipe, type RecipeVariants } from "@vanilla-extract/recipes";
// Theme contract
export const [themeClass, vars] = createTheme({
color: {
primary: '#2563eb',
secondary: '#64748b',
background: '#ffffff',
text: '#1f2937',
primary: "#2563eb",
secondary: "#64748b",
background: "#ffffff",
text: "#1f2937",
},
space: {
small: '0.5rem',
medium: '1rem',
large: '1.5rem',
small: "0.5rem",
medium: "1rem",
large: "1.5rem",
},
radius: {
small: '0.25rem',
medium: '0.375rem',
large: '0.5rem',
small: "0.25rem",
medium: "0.375rem",
large: "0.5rem",
},
});
@@ -455,54 +460,54 @@ export const text = styleVariants({
// Recipe (like CVA)
export const button = recipe({
base: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 500,
borderRadius: vars.radius.medium,
transition: 'background-color 0.2s',
cursor: 'pointer',
border: 'none',
':disabled': {
transition: "background-color 0.2s",
cursor: "pointer",
border: "none",
":disabled": {
opacity: 0.5,
cursor: 'not-allowed',
cursor: "not-allowed",
},
},
variants: {
variant: {
primary: {
backgroundColor: vars.color.primary,
color: 'white',
':hover': {
backgroundColor: '#1d4ed8',
color: "white",
":hover": {
backgroundColor: "#1d4ed8",
},
},
secondary: {
backgroundColor: '#f3f4f6',
backgroundColor: "#f3f4f6",
color: vars.color.text,
':hover': {
backgroundColor: '#e5e7eb',
":hover": {
backgroundColor: "#e5e7eb",
},
},
},
size: {
small: {
padding: '0.25rem 0.75rem',
fontSize: '0.875rem',
padding: "0.25rem 0.75rem",
fontSize: "0.875rem",
},
medium: {
padding: '0.5rem 1rem',
fontSize: '1rem',
padding: "0.5rem 1rem",
fontSize: "1rem",
},
large: {
padding: '0.75rem 1.5rem',
fontSize: '1.125rem',
padding: "0.75rem 1.5rem",
fontSize: "1.125rem",
},
},
},
defaultVariants: {
variant: 'primary',
size: 'medium',
variant: "primary",
size: "medium",
},
});
@@ -511,7 +516,7 @@ export type ButtonVariants = RecipeVariants<typeof button>;
```tsx
// Button.tsx
import { button, type ButtonVariants, themeClass } from './styles.css';
import { button, type ButtonVariants, themeClass } from "./styles.css";
interface ButtonProps extends ButtonVariants {
children: React.ReactNode;
@@ -545,8 +550,8 @@ function App() {
```tsx
// Next.js with styled-components
// pages/_document.tsx
import Document, { DocumentContext } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
import Document, { DocumentContext } from "next/document";
import { ServerStyleSheet } from "styled-components";
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
@@ -576,9 +581,9 @@ export default class MyDocument extends Document {
```tsx
// Dynamically import heavy styled components
import dynamic from 'next/dynamic';
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import('./HeavyChart'), {
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <Skeleton height={400} />,
ssr: false,
});