frontend-design
Create components and pages following the Family Gifting Dashboard "Soft Modernity" design system. Use when building UI components, pages, or implementing design specifications. Includes Apple-inspired warmth, generous radii, diffused shadows, and token-optimized styling.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install miethe-family-shopping-dashboard-frontend-design
Repository
Skill path: .claude/skills/frontend-design
Create components and pages following the Family Gifting Dashboard "Soft Modernity" design system. Use when building UI components, pages, or implementing design specifications. Includes Apple-inspired warmth, generous radii, diffused shadows, and token-optimized styling.
Open repositoryBest for
Primary workflow: Design Product.
Technical facets: Designer, Frontend.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: miethe.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install frontend-design into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/miethe/family-shopping-dashboard before adding frontend-design to shared team environments
- Use frontend-design for design workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: frontend-design
description: Create components and pages following the Family Gifting Dashboard "Soft Modernity" design system. Use when building UI components, pages, or implementing design specifications. Includes Apple-inspired warmth, generous radii, diffused shadows, and token-optimized styling.
---
# Frontend Design — Soft Modernity
Build UI components and pages following the Family Gifting Dashboard design system. This skill provides token-optimized references for the project's "Soft Modernity" aesthetic—Apple-inspired warmth, generous radii, diffused shadows, and mobile-first patterns.
## Design Philosophy: "Soft Modernity"
**Aesthetic**: Apple-inspired warmth with generous radii and diffused shadows
**Feel**: Welcoming, refined, familiar—like a well-designed iOS app
**Never**: Pure white backgrounds, harsh shadows, sharp corners, cold colors
### Core Characteristics
- **Warm Foundation**: Cream (#FAF8F5) base, never pure white
- **Generous Radii**: 10-48px border radius (cards 20-36px)
- **Diffused Shadows**: Soft, layered shadows with subtle borders
- **Warm Ink**: Brown-based text (#2D2520) instead of pure black
- **Status Colors**: Muted, harmonious (Mustard, Sage, Terracotta, Lavender)
- **Glassmorphism**: Translucent surfaces with backdrop blur (80% opacity + 12px blur)
- **Mobile-First**: 44px touch targets, safe areas, 100dvh viewport
## When to Use This Skill
**Building Components**:
- New UI components (Button, Card, Input, Modal)
- Implementing from design specs
- Creating variant systems with CVA
**Building Pages**:
- Dashboard layouts
- Responsive patterns (sidebar + bottom nav)
- Kanban columns, carousels, stat cards
**Styling Decisions**:
- Which color/spacing/radius token to use
- Component variant selection
- Mobile vs desktop layout patterns
## Quick Token Reference
### Essential Colors
```yaml
Background:
base: "#FAF8F5" # Creamy base (never pure white)
subtle: "#F5F2ED" # Slightly darker
elevated: "#FFFFFF" # Pure white (cards only)
Text (Warm Ink):
primary: "#2D2520" # Headings (warm dark brown)
secondary: "#5C534D" # Body text
tertiary: "#8A827C" # Captions
Primary (Holiday Coral):
main: "#E8846B" # Primary buttons, CTAs
hover: "#D66A51" # Hover state
Status:
idea: "#D4A853" # Mustard (Ideas, Shortlisted)
success: "#7BA676" # Sage (Purchased, Gifted)
warning: "#C97B63" # Terracotta (Urgent)
progress: "#8A78A3" # Lavender (Buying, Ordered)
Borders:
subtle: "#E8E3DC" # Soft borders
medium: "#D4CDC4" # Default borders
strong: "#B8AFA4" # Emphasized
focus: "#E8846B" # Focus rings
```
### Spacing (8px Grid)
```yaml
1: 4px # 0.25rem
2: 8px # 0.5rem
3: 12px # 0.75rem
4: 16px # 1rem
5: 20px # 1.25rem
6: 24px # 1.5rem
8: 32px # 2rem
10: 40px # 2.5rem
12: 48px # 3rem
```
### Border Radius
```yaml
small: 8px # Pills, badges, small buttons
medium: 12px # Inputs, standard buttons
large: 16px # Default cards
xlarge: 20px # Large cards
2xlarge: 24px # Hero cards, stat cards
3xlarge: 32px # Extra large containers
full: 9999px # Avatars, circular elements
```
### Shadows
```yaml
subtle: # Level 1 - Minimal cards
"0 1px 2px rgba(45, 37, 32, 0.04), 0 0 0 1px rgba(45, 37, 32, 0.02)"
low: # Level 2 - Most cards
"0 2px 8px rgba(45, 37, 32, 0.06), 0 0 0 1px rgba(45, 37, 32, 0.03)"
medium: # Level 3 - Hover, dropdowns
"0 4px 16px rgba(45, 37, 32, 0.08), 0 1px 4px rgba(45, 37, 32, 0.04)"
high: # Level 4 - Modals, sheets
"0 8px 32px rgba(45, 37, 32, 0.12), 0 2px 8px rgba(45, 37, 32, 0.06)"
```
## Component Variant Guide
### Button
**Variants**: `primary` (coral CTA) | `secondary` (warm gray) | `ghost` (transparent) | `glass` (translucent) | `destructive` (terracotta)
**Sizes**: `sm` (32px) | `md` (44px, default) | `lg` (52px) | `xl` (60px)
**Touch Target**: All meet 44px minimum
```tsx
// CVA Example
<Button variant="primary" size="md">Add Gift</Button>
<Button variant="secondary" size="sm">Cancel</Button>
<Button variant="glass" size="lg">Dashboard</Button>
```
### Card
**Variants**: `default` (elevated white) | `glass` (translucent blur) | `elevated` (higher shadow) | `interactive` (hover lift) | `stat` (gradient status) | `flat` (minimal shadow)
**Padding**: 20-32px
**Radius**: 16-24px
```tsx
<Card variant="glass" className="p-6 rounded-xlarge">
{/* Glassmorphism with backdrop blur */}
</Card>
<Card variant="stat" className="p-8 rounded-2xlarge">
{/* Stat card with gradient background */}
</Card>
```
### StatusPill
**Statuses**: `idea` | `shortlisted` | `buying` | `ordered` | `purchased` | `delivered` | `gifted` | `urgent`
**Pattern**: Dot indicator (6px) + uppercase label (12px semibold)
```tsx
<StatusPill status="idea">Idea</StatusPill>
<StatusPill status="purchased">Purchased</StatusPill>
```
### Avatar
**Sizes**: `xs` (24px) | `sm` (32px) | `md` (40px) | `lg` (56px) | `xl` (80px)
**Features**: Status rings, gift count badges
**Border**: 2px white, shadow-low
```tsx
<Avatar size="lg" status="online" badge={3} />
```
## Layout Patterns
### Desktop Sidebar + Main
```tsx
<div className="flex h-screen">
{/* Sidebar: 240px fixed */}
<aside className="hidden md:block w-60 glass-panel">
<nav>...</nav>
</aside>
{/* Main content: flex-1 */}
<main className="flex-1 overflow-auto">
{children}
</main>
</div>
```
### Mobile Bottom Nav
```tsx
<nav className="
fixed bottom-0 left-0 right-0 md:hidden
h-14 pb-safe-area-inset-bottom
glass-panel border-t border-subtle
flex items-center justify-around
z-50
">
{/* 4-5 nav items max, 44px touch targets */}
</nav>
```
### Responsive Breakpoints
```yaml
xs: 375px # iPhone SE
sm: 640px # Small tablets
md: 768px # Tablets (sidebar shows, bottom nav hides)
lg: 1024px # Laptops
xl: 1280px # Desktops
```
### Safe Areas (iOS)
```tsx
className="
pt-safe-area-inset-top
pb-safe-area-inset-bottom
pl-safe-area-inset-left
pr-safe-area-inset-right
"
```
### Viewport Height
```css
/* Always use 100dvh (dynamic) instead of 100vh */
height: 100dvh; /* Fixes iOS address bar issue */
```
## Animation Guidelines
**Use**: `anime.js` for page animations (see `./references/animejs.md`)
**Pattern**: One well-orchestrated entrance > scattered micro-interactions
**Stagger**: Use `animation-delay` for sequential reveals
```tsx
// Staggered card entrance
{cards.map((card, i) => (
<Card
key={card.id}
className="animate-slide-up-fade"
style={{ animationDelay: `${i * 100}ms` }}
>
{card.content}
</Card>
))}
```
**Durations**:
- Fast: 150ms (micro-interactions)
- Default: 200ms (most interactions)
- Slow: 300ms (page transitions)
- Spring: Use ease-spring for delight
## Detailed References
When you need more detail:
- **Token values**: `./references/soft-modernity-tokens.md` — Complete color palette, spacing scale, typography
- **Component variants**: `./references/component-variants.md` — CVA patterns, Tailwind classes, all variants
- **Layout patterns**: `./references/layout-mobile.md` — Responsive layouts, safe areas, touch targets
- **Animations**: `./references/animejs.md` — anime.js v4 reference (KEEP AS-IS)
## Project Design Docs
For comprehensive design specifications:
- `docs/designs/DESIGN-TOKENS.md` — Canonical token values
- `docs/designs/COMPONENTS.md` — Component specifications
- `docs/designs/LAYOUT-PATTERNS.md` — Page layouts
- `docs/designs/DESIGN-GUIDE.md` — Philosophy and principles
## Implementation Notes
**Tailwind Config**: All tokens pre-configured in `apps/web/tailwind.config.ts`
**CSS Variables**: Available in `apps/web/app/globals.css`
**CVA**: Use for component variants (class-variance-authority)
**Utilities**: Glassmorphism via `.glass-panel` utility class
**Mobile-First**: Default styles assume mobile, add `md:` prefix for tablet+, `lg:` for desktop
## Common Patterns
### Glassmorphism Panel
```tsx
<div className="glass-panel p-6 rounded-xlarge">
{/* 80% opacity + 12px backdrop blur */}
</div>
```
### Stat Card Grid
```tsx
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<StatCard value={12} label="Ideas" color="idea" />
<StatCard value={8} label="To Buy" color="warning" />
<StatCard value={4} label="Purchased" color="success" />
</div>
```
### Avatar Carousel
```tsx
<div className="flex gap-6 overflow-x-auto pb-2">
{members.map(member => (
<Avatar
key={member.id}
size="lg"
status={member.status}
badge={member.giftCount}
/>
))}
</div>
```
### Interactive Card with Hover Lift
```tsx
<Card
variant="interactive"
className="
transition-all duration-300
hover:shadow-medium
hover:scale-[1.01]
active:scale-[0.99]
"
>
{content}
</Card>
```
## Best Practices
1. **Always use cream base** (#FAF8F5), never pure white backgrounds
2. **44px minimum** for all touch targets
3. **Generous radii** — cards 20-24px, containers 32-48px
4. **Diffused shadows** — use layered shadows (shadow + border)
5. **Warm text** — brown-based (#2D2520), not pure black
6. **Status colors** — muted harmonious palette (mustard, sage, terracotta, lavender)
7. **Safe areas** — always include on fixed/sticky elements
8. **Dynamic viewport** — use 100dvh, not 100vh
9. **Glassmorphism** — 80% opacity + 12px backdrop blur for floating panels
10. **Staggered animations** — one orchestrated entrance beats scattered micro-interactions
## Anti-Patterns to Avoid
❌ Pure white (#FFFFFF) background
❌ Pure black (#000000) text
❌ Sharp corners (< 8px radius)
❌ Harsh shadows (no blur, high opacity)
❌ Cold colors (blues, grays without warmth)
❌ Touch targets < 44px
❌ Fixed 100vh on mobile
❌ No safe-area handling on iOS
❌ Generic AI aesthetics (Inter font, purple gradients, cookie-cutter layouts)
---
**Last Updated**: 2025-11-28
**Design System Version**: 1.0
**Project**: Family Gifting Dashboard
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/animejs.md
```markdown
# Anime.js v4 Reference Guide for AI Assistants
## 🚨 CRITICAL: ALWAYS USE ANIME.JS V4 SYNTAX 🚨
**This project uses Anime.js v4.x.x - DO NOT use v3 syntax under any circumstances**
**If you're about to write `import anime from 'animejs'` - STOP!**
**That's v3. This project uses v4. Use the correct import below.**
## 🚀 Quick Start - Essential Setup
### 1. Correct v4 Import (REQUIRED)
```javascript
// ✅ CORRECT v4 imports
import { animate, createTimeline, stagger, utils, svg, eases, engine } from 'animejs';
// ❌ WRONG v3 import - NEVER USE THIS
// import anime from 'animejs';
```
### 2. Configure Time Units to Seconds (SET ONCE IN APP ENTRY POINT)
```javascript
// ⚠️ IMPORTANT: Set this ONLY ONCE in your app's main entry point
// For React: App.js/App.tsx or index.js/index.tsx
// For Vue: main.js/main.ts
// For vanilla JS: The main script file that loads first
import { engine } from 'animejs';
// Set ONLY in the app's entry point, NOT in components
engine.timeUnit = 's';
// Now ALL durations use seconds everywhere: 1 = 1 second, 0.5 = 500ms
// DO NOT set this in individual components - it's a global setting!
```
### 3. Single-Line Format for Simple Animations (REQUIRED)
```javascript
// ✅ GOOD - Clean, readable, one line for simple tweens
animate('.element', { x: 250, duration: 1, ease: 'outQuad' });
// ❌ BAD - Unnecessary multi-line for simple tweens
animate('.element', {
x: 250,
duration: 1,
ease: 'outQuad'
});
```
## ✅ Quick Validation Checklist
Before generating anime.js code, verify:
- [ ] Using `import { animate, ... } from 'animejs'` NOT `import anime`
- [ ] Set `engine.timeUnit = 's'` ONLY ONCE in app entry point (NOT in components)
- [ ] Using seconds for all durations (1 = 1 second)
- [ ] Simple animations on ONE LINE
- [ ] Using `animate()` NOT `anime()`
- [ ] Using `createTimeline()` NOT `anime.timeline()`
- [ ] Using `ease:` NOT `easing:`
- [ ] Using `to:` for values, NOT `value:`
- [ ] Using `on` prefix for callbacks (onUpdate, onComplete)
- [ ] Using `loop` and `alternate` NOT `direction`
- [ ] Using correct v4 stagger syntax with `stagger()`
- [ ] Using shorthand properties (x, y, z) when possible
## 🎯 Core API - Most Common Patterns
### Basic Animation (single line for simple tweens)
```javascript
// Simple tween - ALWAYS one line
animate('.element', { x: 250, rotate: 180, duration: 0.8, ease: 'inOutQuad' });
// Fade in - one line
animate('.element', { opacity: [0, 1], y: [20, 0], duration: 0.6, ease: 'outQuad' });
// Scale bounce - one line
animate('.element', { scale: [0, 1], duration: 0.8, ease: 'outElastic(1, 0.5)' });
// Infinite loop - one line
animate('.element', { rotate: 360, duration: 2, loop: true, ease: 'linear' });
```
### Timeline Creation
```javascript
const tl = createTimeline({ defaults: { duration: 1, ease: 'outQuad' } });
tl.add('.element1', { x: 250 })
.add('.element2', { y: 100 }, '+=0.2') // 0.2s after previous
.add('.element3', { rotate: 180 }, '<'); // at start of previous
```
### Stagger Animations (single line)
```javascript
animate('.elements', { x: 250, delay: stagger(0.1) }); // 0.1s between each
animate('.elements', { x: 250, delay: stagger(0.1, { from: 'center' }) });
```
## ❌ Common AI Mistakes to Avoid
### MISTAKE #1: Using v3 Import Pattern
```javascript
// ❌ WRONG - This is v3
import anime from 'animejs';
anime({ targets: '.element', translateX: 250 });
// ✅ CORRECT - Always use v4
import { animate } from 'animejs';
animate('.element', { x: 250 });
```
### MISTAKE #2: Using 'targets' Property
```javascript
// ❌ WRONG - 'targets' is v3
animate({ targets: '.element', translateX: 250 });
// ✅ CORRECT - First parameter is the target
animate('.element', { x: 250 });
```
### MISTAKE #3: Using 'easing' Instead of 'ease'
```javascript
// ❌ WRONG
animate('.element', { x: 250, easing: 'easeInOutQuad' });
// ✅ CORRECT
animate('.element', { x: 250, ease: 'inOutQuad' });
```
### MISTAKE #4: Using 'value' for Animation Values
```javascript
// ❌ WRONG - 'value' is v3
animate('.element', { x: { value: 250 } });
// ✅ CORRECT - Use 'to' for values
animate('.element', { x: { to: 250 } });
```
### MISTAKE #5: Wrong Timeline Syntax
```javascript
// ❌ WRONG - anime.timeline() is v3
const tl = anime.timeline();
// ✅ CORRECT - Use createTimeline
import { createTimeline } from 'animejs';
const tl = createTimeline();
```
## 📋 Property Syntax Reference (v3 → v4)
### Animation Values
```javascript
// ✅ v4: Use 'to' for target values
{ opacity: { to: 0.5 } }
{ x: { to: [0, 100] } }
// ❌ v3: DON'T use 'value'
// { opacity: { value: 0.5 } }
```
### Easing Functions
```javascript
// ✅ v4: Use 'ease' (no 'ease' prefix)
{ ease: 'inOutQuad' }
{ ease: 'outElastic(1, 0.5)' }
{ ease: 'cubicBezier(0.4, 0, 0.2, 1)' }
// ❌ v3: DON'T use 'easing' or 'ease' prefix
// { easing: 'easeInOutQuad' }
```
### Direction & Looping
```javascript
// ✅ v4
{
loop: true, // infinite loop
loop: 3, // loop 3 times
alternate: true, // alternate direction
reversed: true // play in reverse
}
// ❌ v3: DON'T use 'direction'
// { direction: 'alternate' }
```
### Transform Properties (Shorthand Preferred)
```javascript
// ✅ Both syntaxes work in v4:
animate('.element', { x: 100, y: 50, z: 25 }); // shorthand (preferred)
animate('.element', { translateX: 100, translateY: 50, translateZ: 25 }); // explicit
```
### Callbacks (ALL prefixed with 'on')
```javascript
// ✅ v4: Simple callback - keep on one line
animate('.element', { x: 250, duration: 1, onComplete: () => console.log('Done!') });
// ✅ v4: Multiple callbacks - use multi-line
animate('.element', {
x: 250,
duration: 1,
onBegin: (anim) => console.log('Started'),
onUpdate: (anim) => console.log('Progress:', anim.progress),
onComplete: (anim) => console.log('Finished')
});
// ❌ v3: DON'T use unprefixed callbacks
// { update: () => {}, complete: () => {} }
```
## 📝 Code Formatting Guidelines
### ALWAYS Use Single-Line Format for Simple Animations
**This is mandatory for readability** - Use for animations with ≤4 properties:
```javascript
// ✅ GOOD - Clean, readable, one line
animate('.element', { x: 250, duration: 1, ease: 'outQuad' });
animate('.box', { opacity: 0.5, scale: 0.8, duration: 0.3 });
// ❌ BAD - Unnecessary multi-line for simple tweens
animate('.element', {
x: 250,
duration: 1,
ease: 'outQuad'
});
```
### Multi-Line Format (Only for Complex Animations)
Use for animations with >4 properties or callbacks:
```javascript
// Complex animation with callbacks - multi-line is appropriate
animate('.element', {
x: { to: [0, 100, 50], duration: 2 },
y: { to: [0, -50, 0], duration: 2 },
scale: [0, 1.2, 1],
ease: 'outElastic(1, 0.5)',
onComplete: () => console.log('Done!')
});
```
## 🎨 Common Animation Patterns
### Hover Animation (single line per animation)
```javascript
element.addEventListener('mouseenter', () => animate(element, { scale: 1.1, duration: 0.3, ease: 'outQuad' }));
element.addEventListener('mouseleave', () => animate(element, { scale: 1, duration: 0.3, ease: 'outQuad' }));
```
### Sequential Timeline
```javascript
const tl = createTimeline({ defaults: { duration: 0.5 } });
tl.add('.step1', { x: 100 })
.add('.step2', { y: 100 })
.add('.step3', { scale: 2 });
```
### Scroll-triggered Animation
```javascript
import { createScrollObserver } from 'animejs';
createScrollObserver({
target: '.scroll-element',
root: document.querySelector('.scroll-container'),
play: () => animate('.element', { x: 250, duration: 1 }),
visibility: 0.5
});
```
## 🔧 Advanced Features
### SVG Animations
```javascript
import { animate, svg } from 'animejs';
// Morph path (single line)
animate('#path1', { d: svg.morphTo('#path2'), duration: 1 });
// Draw SVG line
const drawable = svg.createDrawable('.svg-path');
animate(drawable, { draw: '0% 100%', duration: 2 });
// Motion path (single line for simple usage)
const motionPath = svg.createMotionPath('#motion-path');
animate('.element', { x: motionPath.translateX, y: motionPath.translateY, rotate: motionPath.rotate });
```
### Utility Functions
```javascript
import { utils } from 'animejs';
// DOM selection
const elements = utils.$('.elements');
// Get current value
const currentX = utils.get('.element', 'translateX');
// Set values immediately
utils.set('.element', { x: 100, opacity: 0.5 });
// Remove animations
utils.remove('.element');
// Math utilities
utils.random(0, 100);
utils.shuffle([1, 2, 3, 4]);
utils.lerp(0, 100, 0.5); // 50
utils.clamp(150, 0, 100); // 100
```
### TypeScript Support
```typescript
import { animate, createTimeline, JSAnimation, Timeline, AnimationParams, TimelineParams } from 'animejs';
// Single line for simple animations
const animation: JSAnimation = animate('.element', { x: 250, duration: 1 } as AnimationParams);
const timeline: Timeline = createTimeline({ defaults: { duration: 0.8 } } as TimelineParams);
```
## ⚡ Performance Tips
1. **Use transforms over position properties**
```javascript
// ✅ Good - uses transform
animate('.element', { x: 100 });
// ❌ Avoid - triggers layout
animate('.element', { left: 100 });
```
2. **Batch animations in timelines**
```javascript
// ✅ Good - single timeline
const tl = createTimeline();
elements.forEach(el => tl.add(el, { x: 100 }));
// ❌ Avoid - multiple animations
elements.forEach(el => animate(el, { x: 100 }));
```
3. **Use will-change CSS property for complex animations**
```css
.animated-element {
will-change: transform, opacity;
}
```
## 🚫 How to Identify V3 Code (DON'T USE)
If you see ANY of these patterns, it's v3 and MUST be updated:
```javascript
// All of these are V3 - NEVER USE:
anime({ ... })
anime.timeline()
anime.stagger()
anime.random()
anime.remove()
anime.get()
anime.set()
anime.running
{ targets: '...' }
{ easing: '...' }
{ value: ... }
{ direction: 'alternate' }
```
## 💡 AI Code Generation Rules
When asked to create animations with anime.js:
1. **ONLY** set `engine.timeUnit = 's'` ONCE in the app's main entry point (App.js, main.js, index.js) - NEVER in components
2. **ALWAYS** use seconds for all durations (1 = 1 second)
3. **ALWAYS** format simple animations on ONE LINE
4. **ALWAYS** start with v4 imports
5. **NEVER** use `anime()` function
6. **ALWAYS** use `animate()` for animations
7. **NEVER** include `targets` property
8. **ALWAYS** use `ease` not `easing`
9. **NEVER** use `value`, use `to` instead
10. **ALWAYS** prefix callbacks with `on`
11. **NEVER** use `direction`, use `alternate` and `reversed`
12. **ALWAYS** use `createTimeline()` for timelines
13. **PREFER** shorthand (`x`) over explicit (`translateX`)
14. **FORMAT** short animations on single line (≤4 properties)
15. **NEVER** generate v3 syntax under any circumstances
## NPM Installation
```bash
npm install animejs
```
## Version Check
```javascript
// Current version: 4.x.x
// If you see any code using anime({ targets: ... }), it's v3 and needs updating!
```
```
### references/soft-modernity-tokens.md
```markdown
# Soft Modernity Design Tokens
Token-optimized reference for the Family Gifting Dashboard design system. All values are canonical and pre-configured in `apps/web/tailwind.config.ts`.
---
## Color Palette
### Background Colors
```yaml
bg-base: "#FAF8F5" # Creamy off-white base (NEVER pure white)
bg-subtle: "#F5F2ED" # Slightly darker for contrast
bg-elevated: "#FFFFFF" # Pure white for elevated cards only
surface-primary: "#FFFFFF" # Floating cards, modals
surface-secondary: "rgba(255,255,255,0.6)" # Glassy surfaces
surface-tertiary: "rgba(242,238,230,0.5)" # Subtle backgrounds
surface-translucent: "rgba(250,248,245,0.8)" # Glassmorphism base
```
### Text Colors (Warm Ink)
```yaml
text-primary: "#2D2520" # Warm dark brown - headings
text-secondary: "#5C534D" # Medium warm grey - body
text-tertiary: "#8A827C" # Light warm grey - captions
text-disabled: "#C4BDB7" # Disabled state
text-inverse: "#FFFFFF" # Text on dark backgrounds
```
### Primary Accent (Holiday Coral)
```yaml
primary-50: "#FEF3F1"
primary-100: "#FDE5E0"
primary-200: "#FBC9BC"
primary-300: "#F5A894"
primary-400: "#EE8F76"
primary-500: "#E8846B" # Main coral - buttons, CTAs
primary-600: "#D66A51" # Hover state
primary-700: "#B95440"
primary-800: "#9A4234"
primary-900: "#7D352B"
```
### Status: Idea/Shortlisted (Muted Mustard)
```yaml
status-idea-50: "#FDF9F0"
status-idea-100: "#FAF1DC"
status-idea-200: "#F4E0B3"
status-idea-300: "#E8CC85"
status-idea-400: "#DCB85E"
status-idea-500: "#D4A853" # Mustard yellow
status-idea-600: "#B88F45"
status-idea-700: "#967538"
status-idea-800: "#735A2B"
status-idea-900: "#523F1F"
```
### Status: Purchased/Gifted (Soft Sage)
```yaml
status-success-50: "#F3F7F2"
status-success-100: "#E4EDE2"
status-success-200: "#C5D8C1"
status-success-300: "#A0BD9B"
status-success-400: "#8AAA84"
status-success-500: "#7BA676" # Sage green
status-success-600: "#668B61"
status-success-700: "#51704E"
status-success-800: "#3D543B"
status-success-900: "#2A3928"
```
### Status: Urgent/Attention (Muted Terracotta)
```yaml
status-warning-50: "#FEF5F3"
status-warning-100: "#FCE9E5"
status-warning-200: "#F6CEC5"
status-warning-300: "#EBAB9D"
status-warning-400: "#DD9179"
status-warning-500: "#C97B63" # Terracotta
status-warning-600: "#AC6350"
status-warning-700: "#8D4E40"
status-warning-800: "#6D3C31"
status-warning-900: "#4F2B23"
```
### Status: Buying/Ordered (Muted Lavender)
```yaml
status-progress-50: "#F7F5F9"
status-progress-100: "#EDE8F2"
status-progress-200: "#D6CBDF"
status-progress-300: "#B9A7C7"
status-progress-400: "#A08DB4"
status-progress-500: "#8A78A3" # Lavender
status-progress-600: "#70628A"
status-progress-700: "#594E6E"
status-progress-800: "#433B53"
status-progress-900: "#2F2A3A"
```
### Borders & Dividers
```yaml
border-subtle: "#E8E3DC" # Soft borders
border-medium: "#D4CDC4" # Default borders
border-strong: "#B8AFA4" # Emphasized borders
border-focus: "#E8846B" # Focus rings (coral)
glass-border: "rgba(255,255,255,0.4)" # Glassmorphism borders
glass-border-strong: "rgba(255,255,255,0.6)" # Emphasized glass
```
### Overlays
```yaml
overlay-light: "rgba(45,37,32,0.08)" # Subtle overlays
overlay-medium: "rgba(45,37,32,0.16)" # Modal backgrounds
overlay-strong: "rgba(45,37,32,0.48)" # Strong emphasis
```
### Neutral Warm Scale (Tailwind warm-*)
```yaml
warm-50: "#FAF8F5"
warm-100: "#F5F2ED"
warm-200: "#EBE7E0"
warm-300: "#D4CDC4"
warm-400: "#C4BDB7"
warm-500: "#A69C94"
warm-600: "#8A827C"
warm-700: "#5C534D"
warm-800: "#3D3632"
warm-900: "#2D2520"
```
---
## Typography
### Font Stack
```yaml
primary: "'SF Pro Rounded', -apple-system, BlinkMacSystemFont, system-ui, sans-serif"
fallback: "'Nunito', 'Inter', system-ui, -apple-system, sans-serif"
```
### Type Scale
```yaml
display-large: # 48px / 3rem
size: 3rem
line: 3.5rem # 56px / 1.167
weight: 800 # Heavy
tracking: -0.02em
display-medium: # 36px / 2.25rem
size: 2.25rem
line: 2.75rem # 44px / 1.222
weight: 800 # Heavy
tracking: -0.015em
display-small: # 28px / 1.75rem
size: 1.75rem
line: 2.25rem # 36px / 1.286
weight: 700 # Bold
tracking: -0.01em
heading-1: # 24px / 1.5rem
size: 1.5rem
line: 2rem # 32px / 1.333
weight: 700 # Bold
tracking: -0.01em
heading-2: # 20px / 1.25rem
size: 1.25rem
line: 1.75rem # 28px / 1.4
weight: 600 # Semibold
tracking: -0.01em
heading-3: # 18px / 1.125rem
size: 1.125rem
line: 1.625rem # 26px / 1.444
weight: 600 # Semibold
tracking: -0.005em
body-large: # 16px / 1rem
size: 1rem
line: 1.5rem # 24px / 1.5
weight: 400 # Regular
body-medium: # 14px / 0.875rem
size: 0.875rem
line: 1.25rem # 20px / 1.429
weight: 400 # Regular
body-small: # 12px / 0.75rem
size: 0.75rem
line: 1rem # 16px / 1.333
weight: 500 # Medium
label-large: # 14px / 0.875rem
size: 0.875rem
line: 1.25rem # 20px / 1.429
weight: 600 # Semibold
label-small: # 12px / 0.75rem
size: 0.75rem
line: 1rem # 16px / 1.333
weight: 600 # Semibold
tracking: 0.01em
```
---
## Spacing Scale (8px Base Grid)
```yaml
spacing-1: 0.25rem # 4px
spacing-2: 0.5rem # 8px
spacing-3: 0.75rem # 12px
spacing-4: 1rem # 16px
spacing-5: 1.25rem # 20px
spacing-6: 1.5rem # 24px
spacing-8: 2rem # 32px
spacing-10: 2.5rem # 40px
spacing-12: 3rem # 48px
spacing-16: 4rem # 64px
spacing-20: 5rem # 80px
spacing-24: 6rem # 96px
```
**Tailwind Classes**: `p-1` through `p-24`, `m-1` through `m-24`, `gap-1` through `gap-24`
---
## Border Radius
```yaml
small: 0.5rem # 8px - Pills, badges, small buttons
medium: 0.75rem # 12px - Inputs, standard buttons
large: 1rem # 16px - Default cards
xlarge: 1.25rem # 20px - Large cards
2xlarge: 1.5rem # 24px - Hero cards, stat cards
3xlarge: 2rem # 32px - Extra large containers
full: 9999px # Avatars, circular elements
```
**Tailwind Classes**: `rounded-small`, `rounded-medium`, `rounded-large`, `rounded-xlarge`, `rounded-2xlarge`, `rounded-3xlarge`, `rounded-full`
---
## Shadows (Diffused Elevation)
```yaml
subtle: # Level 1 - Minimal cards
"0 1px 2px rgba(45, 37, 32, 0.04), 0 0 0 1px rgba(45, 37, 32, 0.02)"
low: # Level 2 - Most cards
"0 2px 8px rgba(45, 37, 32, 0.06), 0 0 0 1px rgba(45, 37, 32, 0.03)"
medium: # Level 3 - Hover, dropdowns
"0 4px 16px rgba(45, 37, 32, 0.08), 0 1px 4px rgba(45, 37, 32, 0.04)"
high: # Level 4 - Modals, sheets
"0 8px 32px rgba(45, 37, 32, 0.12), 0 2px 8px rgba(45, 37, 32, 0.06)"
extra-high: # Level 5 - Fullscreen overlays
"0 16px 48px rgba(45, 37, 32, 0.16), 0 4px 16px rgba(45, 37, 32, 0.08)"
translucent: # Glassmorphism
"0 4px 24px rgba(45, 37, 32, 0.10), 0 0 0 1px rgba(45, 37, 32, 0.04), inset 0 0 0 1px rgba(255, 255, 255, 0.2)"
glass: # Glass panels
"0 8px 32px 0 rgba(31, 38, 135, 0.07)"
glass-inset: # Glass border highlight
"inset 0 0 0 1px rgba(255, 255, 255, 0.1)"
glow: # Accent glow (coral)
"0 0 20px rgba(255, 102, 77, 0.3)"
```
**Tailwind Classes**: `shadow-subtle`, `shadow-low`, `shadow-medium`, `shadow-high`, `shadow-glass`
### Elevation Hierarchy
```yaml
Level 0 (Base): no-shadow # Background
Level 1 (Flat): shadow-subtle # Minimal cards
Level 2 (Default): shadow-low # Most cards
Level 3 (Raised): shadow-medium # Hover, dropdowns
Level 4 (Floating): shadow-high # Modals, sheets
Level 5 (Overlay): shadow-extra-high # Fullscreen overlays
```
---
## Animations
### Durations
```yaml
fast: 150ms # Fast interactions (hover, press)
default: 200ms # Default interactions (most transitions)
slow: 300ms # Slower, more dramatic (page transitions)
page: 400ms # Page-level transitions
```
### Easing Functions
```yaml
ease-out: cubic-bezier(0.16, 1, 0.3, 1) # Entering elements (fast→slow)
ease-in: cubic-bezier(0.7, 0, 0.84, 0) # Exiting elements (slow→fast)
ease-in-out: cubic-bezier(0.65, 0, 0.35, 1) # Smooth both ways
ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1) # Bouncy (for delight)
```
### Keyframe Animations
```yaml
fadeIn:
from: { opacity: 0 }
to: { opacity: 1 }
slideUpFade:
from: { opacity: 0, transform: translateY(10px) }
to: { opacity: 1, transform: translateY(0) }
scaleIn:
from: { opacity: 0, transform: scale(0.95) }
to: { opacity: 1, transform: scale(1) }
springIn:
0%: { transform: scale(0.9) }
50%: { transform: scale(1.05) }
100%: { transform: scale(1) }
shimmer:
100%: { transform: translateX(100%) }
```
**Tailwind Classes**: `animate-fade-in`, `animate-slide-up-fade`, `animate-scale-in`, `animate-spring-in`, `animate-shimmer`
---
## Breakpoints
```yaml
xs: 375px # iPhone SE
sm: 640px # Small tablets
md: 768px # Tablets (sidebar shows, bottom nav hides)
lg: 1024px # Laptops
xl: 1280px # Desktops
2xl: 1536px # Large desktops
```
**Tailwind Prefixes**: `xs:`, `sm:`, `md:`, `lg:`, `xl:`, `2xl:`
---
## Layout Tokens
### Max Content Widths
```yaml
max-content-width: 1280px # 80rem - Max content width
max-reading-width: 640px # 40rem - Comfortable reading
max-form-width: 512px # 32rem - Form max width
```
### Grid Systems
```yaml
Desktop:
columns: 12
gutter: 24px
Tablet:
columns: 8
gutter: 16px
Mobile:
columns: 4
gutter: 16px
```
### Component Sizing
```yaml
Button Heights:
small: 32px
medium: 44px # Default (touch target)
large: 52px
xlarge: 60px
Input Heights:
standard: 48px # Including border
Avatar Sizes:
xs: 24px
sm: 32px
md: 40px # Default
lg: 56px
xl: 80px
Card Padding:
small: 20px # spacing-5
medium: 24px # spacing-6
large: 32px # spacing-8
Sidebar Width:
desktop: 240px # Fixed on desktop
```
---
## Touch Targets (Mobile)
```yaml
minimum: 44px # Minimum 44×44px touch area
icon-size: 24px # Standard icon size
icon-touch-area: 44px # Touch area for icon-only buttons
```
---
## Safe Areas (iOS/macOS)
```yaml
safe-area-inset-top: env(safe-area-inset-top)
safe-area-inset-right: env(safe-area-inset-right)
safe-area-inset-bottom: env(safe-area-inset-bottom)
safe-area-inset-left: env(safe-area-inset-left)
```
**Tailwind Classes**: `pt-safe-area-inset-top`, `pb-safe-area-inset-bottom`, `pl-safe-area-inset-left`, `pr-safe-area-inset-right`
---
## CSS Custom Properties
All tokens available as CSS variables in `:root`:
```css
:root {
/* Colors */
--color-bg-base: #FAF8F5;
--color-text-primary: #2D2520;
--color-primary-500: #E8846B;
/* Typography */
--font-family: 'SF Pro Rounded', -apple-system, sans-serif;
--font-size-body-large: 1rem;
/* Spacing */
--spacing-4: 1rem;
--spacing-6: 1.5rem;
/* Radius */
--radius-large: 1rem;
--radius-xlarge: 1.25rem;
/* Shadows */
--shadow-low: 0 2px 8px rgba(45, 37, 32, 0.06), 0 0 0 1px rgba(45, 37, 32, 0.03);
/* Animations */
--duration-default: 200ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
```
---
**Reference**: See `apps/web/tailwind.config.ts` for complete implementation
**Version**: 1.0
**Last Updated**: 2025-11-28
```
### references/component-variants.md
```markdown
# Component Variants — CVA Patterns
Token-optimized component variant patterns using `class-variance-authority` (CVA). All examples use Tailwind classes configured in `apps/web/tailwind.config.ts`.
---
## Button Component
### Variants
```typescript
// CVA Definition
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-medium font-semibold transition-all duration-200",
{
variants: {
variant: {
primary: "bg-primary-500 text-white hover:bg-primary-600 active:bg-primary-700 shadow-low hover:shadow-medium",
secondary: "bg-warm-100 text-warm-900 border border-warm-300 hover:bg-warm-200 hover:border-warm-400",
ghost: "bg-transparent text-warm-800 hover:bg-warm-100 active:bg-warm-200",
glass: "bg-surface-secondary backdrop-blur-xs border border-glass-border text-warm-900 hover:bg-surface-tertiary shadow-glass",
destructive: "bg-status-warning-500 text-white hover:bg-status-warning-600 active:bg-status-warning-700 shadow-low"
},
size: {
sm: "h-8 px-3 text-body-small min-w-[44px]",
md: "h-11 px-6 text-body-large min-w-[44px]",
lg: "h-13 px-8 text-heading-3 min-w-[44px]",
xl: "h-15 px-10 text-heading-2 min-w-[44px]"
}
},
defaultVariants: {
variant: "primary",
size: "md"
}
}
);
```
### Tailwind Examples
```tsx
// Primary (Coral CTA)
<button className="
inline-flex items-center justify-center
h-11 px-6 min-w-[44px]
bg-primary-500 text-white
hover:bg-primary-600 active:bg-primary-700
rounded-medium font-semibold
shadow-low hover:shadow-medium
transition-all duration-200
">
Add Gift
</button>
// Secondary (Warm Gray)
<button className="
inline-flex items-center justify-center
h-11 px-6 min-w-[44px]
bg-warm-100 text-warm-900
border border-warm-300
hover:bg-warm-200 hover:border-warm-400
rounded-medium font-semibold
transition-all duration-200
">
Cancel
</button>
// Ghost (Transparent)
<button className="
inline-flex items-center justify-center
h-11 px-6 min-w-[44px]
bg-transparent text-warm-800
hover:bg-warm-100 active:bg-warm-200
rounded-medium font-semibold
transition-all duration-200
">
Learn More
</button>
// Glass (Glassmorphism)
<button className="
inline-flex items-center justify-center
h-11 px-6 min-w-[44px]
bg-surface-secondary backdrop-blur-xs
border border-glass-border text-warm-900
hover:bg-surface-tertiary
rounded-medium font-semibold
shadow-glass
transition-all duration-200
">
Dashboard
</button>
// Destructive (Terracotta)
<button className="
inline-flex items-center justify-center
h-11 px-6 min-w-[44px]
bg-status-warning-500 text-white
hover:bg-status-warning-600 active:bg-status-warning-700
rounded-medium font-semibold
shadow-low
transition-all duration-200
">
Delete
</button>
```
### States
```tsx
// Focus (All Variants)
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
// Disabled (All Variants)
disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none
// Loading (All Variants)
// Add spinner overlay, maintain dimensions
```
---
## Card Component
### Variants
```typescript
// CVA Definition
const cardVariants = cva(
"rounded-large transition-all duration-300",
{
variants: {
variant: {
default: "bg-surface-primary border border-border-subtle shadow-low hover:shadow-medium",
glass: "bg-surface-secondary backdrop-blur-xs border border-glass-border shadow-glass",
elevated: "bg-surface-primary border border-border-subtle shadow-medium hover:shadow-high",
interactive: "bg-surface-primary border border-border-subtle shadow-low hover:shadow-medium hover:border-border-medium hover:scale-[1.01] active:scale-[0.99] cursor-pointer",
stat: "border-2 shadow-medium", // Gradient bg added via inline style
flat: "bg-surface-secondary border border-border-subtle shadow-subtle"
},
padding: {
sm: "p-5", // 20px
md: "p-6", // 24px
lg: "p-8" // 32px
}
},
defaultVariants: {
variant: "default",
padding: "md"
}
}
);
```
### Tailwind Examples
```tsx
// Default (Elevated White)
<div className="
bg-surface-primary
border border-border-subtle
rounded-large p-6
shadow-low hover:shadow-medium
transition-all duration-300
">
{content}
</div>
// Glass (Glassmorphism)
<div className="
bg-surface-secondary backdrop-blur-xs
border border-glass-border
rounded-xlarge p-6
shadow-glass
transition-all duration-300
">
{content}
</div>
// Interactive (Hover Lift)
<div className="
bg-surface-primary
border border-border-subtle
rounded-large p-6
shadow-low
hover:shadow-medium hover:border-border-medium
hover:scale-[1.01] active:scale-[0.99]
cursor-pointer
transition-all duration-300
">
{content}
</div>
// Stat (Gradient Background)
<div className="
bg-gradient-to-br from-status-idea-50 to-status-idea-100
border-2 border-status-idea-200
rounded-2xlarge p-8
shadow-medium
">
<div className="text-display-medium font-heavy text-status-idea-700">
12
</div>
<div className="text-body-small font-semibold uppercase text-status-idea-600">
Ideas
</div>
</div>
// Flat (Minimal)
<div className="
bg-surface-secondary
border border-border-subtle
rounded-large p-5
shadow-subtle
">
{content}
</div>
```
---
## StatusPill Component
### Variants
```typescript
// CVA Definition
const statusPillVariants = cva(
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-small text-label-small uppercase",
{
variants: {
status: {
idea: "bg-status-idea-100 text-status-idea-800 border border-status-idea-300",
shortlisted: "bg-status-idea-100 text-status-idea-800 border border-status-idea-300",
buying: "bg-status-progress-100 text-status-progress-800 border border-status-progress-300",
ordered: "bg-status-progress-100 text-status-progress-800 border border-status-progress-300",
purchased: "bg-status-success-100 text-status-success-800 border border-status-success-300",
delivered: "bg-status-success-100 text-status-success-800 border border-status-success-300",
gifted: "bg-status-success-100 text-status-success-800 border border-status-success-300",
urgent: "bg-status-warning-100 text-status-warning-800 border border-status-warning-300"
}
}
}
);
```
### Tailwind Examples
```tsx
// Idea (Mustard)
<span className="
inline-flex items-center gap-1.5
px-3 py-1.5
bg-status-idea-100 text-status-idea-800
border border-status-idea-300
rounded-small
text-label-small uppercase font-semibold
">
<span className="w-1.5 h-1.5 rounded-full bg-status-idea-600" />
Idea
</span>
// Purchased (Sage)
<span className="
inline-flex items-center gap-1.5
px-3 py-1.5
bg-status-success-100 text-status-success-800
border border-status-success-300
rounded-small
text-label-small uppercase font-semibold
">
<span className="w-1.5 h-1.5 rounded-full bg-status-success-600" />
Purchased
</span>
// Urgent (Terracotta)
<span className="
inline-flex items-center gap-1.5
px-3 py-1.5
bg-status-warning-100 text-status-warning-800
border border-status-warning-300
rounded-small
text-label-small uppercase font-semibold
">
<span className="w-1.5 h-1.5 rounded-full bg-status-warning-600" />
Urgent
</span>
// Buying (Lavender)
<span className="
inline-flex items-center gap-1.5
px-3 py-1.5
bg-status-progress-100 text-status-progress-800
border border-status-progress-300
rounded-small
text-label-small uppercase font-semibold
">
<span className="w-1.5 h-1.5 rounded-full bg-status-progress-600" />
Buying
</span>
```
### Status Dot Colors
```yaml
idea: bg-status-idea-600 # Mustard
shortlisted: bg-status-idea-600 # Mustard
buying: bg-status-progress-600 # Lavender
ordered: bg-status-progress-600 # Lavender
purchased: bg-status-success-600 # Sage
delivered: bg-status-success-600 # Sage
gifted: bg-status-success-600 # Sage
urgent: bg-status-warning-600 # Terracotta
```
---
## Avatar Component
### Variants
```typescript
// CVA Definition
const avatarVariants = cva(
"rounded-full border-2 border-white shadow-low",
{
variants: {
size: {
xs: "w-6 h-6", // 24px
sm: "w-8 h-8", // 32px
md: "w-10 h-10", // 40px
lg: "w-14 h-14", // 56px
xl: "w-20 h-20" // 80px
}
},
defaultVariants: {
size: "md"
}
}
);
```
### Tailwind Examples
```tsx
// Medium Avatar (40px, Default)
<div className="relative">
<img
src={avatarUrl}
className="w-10 h-10 rounded-full border-2 border-white shadow-low object-cover"
alt="User"
/>
</div>
// Large Avatar with Status Ring (56px)
<div className="relative">
<img
src={avatarUrl}
className="w-14 h-14 rounded-full border-2 border-white shadow-low object-cover"
alt="User"
/>
{/* Status ring - online */}
<div className="
absolute -bottom-0.5 -right-0.5
w-14 h-14 rounded-full
ring-3 ring-status-success-500
border-2 border-white
pointer-events-none
" />
</div>
// Avatar with Badge (Gift Count)
<div className="relative">
<img
src={avatarUrl}
className="w-14 h-14 rounded-full border-2 border-white shadow-low object-cover"
alt="User"
/>
{/* Gift count badge */}
<div className="
absolute -bottom-1 -right-1
w-6 h-6 rounded-full
bg-primary-500 text-white
border-2 border-white
flex items-center justify-center
text-xs font-bold
">
3
</div>
</div>
// Avatar Carousel (Overlap)
<div className="flex -space-x-3">
<img className="w-10 h-10 rounded-full border-2 border-white shadow-low z-30" src={url1} />
<img className="w-10 h-10 rounded-full border-2 border-white shadow-low z-20" src={url2} />
<img className="w-10 h-10 rounded-full border-2 border-white shadow-low z-10" src={url3} />
<div className="
w-10 h-10 rounded-full border-2 border-white shadow-low z-0
bg-warm-300 text-warm-800
flex items-center justify-center
text-xs font-bold
">
+5
</div>
</div>
```
### Status Ring Colors
```yaml
online: ring-status-success-500 # Sage green
busy: ring-status-warning-500 # Terracotta
away: ring-status-idea-500 # Mustard
offline: ring-warm-400 # Warm gray
```
---
## Input Component
### Variants
```typescript
// CVA Definition
const inputVariants = cva(
"w-full h-12 px-4 rounded-medium text-body-large transition-all duration-200",
{
variants: {
variant: {
default: "bg-white border-2 border-border-medium focus:border-primary-500 focus:ring-2 focus:ring-primary-200",
error: "bg-white border-2 border-status-warning-500 focus:ring-2 focus:ring-status-warning-200",
success: "bg-white border-2 border-status-success-500 focus:ring-2 focus:ring-status-success-200"
},
size: {
md: "h-12 px-4 text-body-large",
lg: "h-14 px-5 text-heading-3"
}
},
defaultVariants: {
variant: "default",
size: "md"
}
}
);
```
### Tailwind Examples
```tsx
// Text Input (Default)
<input
type="text"
className="
w-full h-12 px-4
bg-white text-warm-900
border-2 border-border-medium
focus:border-primary-500 focus:ring-2 focus:ring-primary-200
rounded-medium
text-body-large placeholder:text-warm-400
shadow-subtle
transition-all duration-200
outline-none
"
placeholder="Enter gift name..."
/>
// Textarea
<textarea
className="
w-full min-h-[120px] px-4 py-3
bg-white text-warm-900
border-2 border-border-medium
focus:border-primary-500 focus:ring-2 focus:ring-primary-200
rounded-medium
text-body-large placeholder:text-warm-400
shadow-subtle
resize-y
transition-all duration-200
outline-none
"
placeholder="Add notes..."
/>
// Select
<select className="
w-full h-12 px-4
bg-white text-warm-900
border-2 border-border-medium
focus:border-primary-500 focus:ring-2 focus:ring-primary-200
rounded-medium
text-body-large
shadow-subtle
appearance-none
bg-[url('data:image/svg+xml;base64,...')] bg-no-repeat bg-right-4 bg-[length:16px]
transition-all duration-200
outline-none
">
<option>Select status...</option>
</select>
// Error State
<input
className="
w-full h-12 px-4
bg-white text-warm-900
border-2 border-status-warning-500
focus:ring-2 focus:ring-status-warning-200
rounded-medium
text-body-large
transition-all duration-200
outline-none
"
/>
// Disabled State
<input
disabled
className="
w-full h-12 px-4
bg-warm-100 text-warm-500
border-2 border-border-subtle
rounded-medium
text-body-large
opacity-40 cursor-not-allowed
"
/>
```
---
## Modal Component
### Structure
```tsx
// Overlay
<div className="
fixed inset-0 z-40
bg-overlay-strong backdrop-blur-sm
animate-fade-in
" />
// Container
<div className="
fixed inset-0 z-50
flex items-center justify-center
p-6
">
{/* Modal Box */}
<div className="
w-full max-w-lg
bg-surface-primary
border border-border-subtle
rounded-2xlarge
shadow-extra-high
animate-scale-in
">
{/* Header */}
<div className="
px-8 py-6
border-b border-border-subtle
">
<h2 className="text-heading-1 font-bold text-warm-900">
Modal Title
</h2>
<p className="mt-1 text-body-medium text-warm-600">
Optional subtitle
</p>
</div>
{/* Content */}
<div className="px-8 py-6">
{content}
</div>
{/* Footer */}
<div className="
px-8 py-6
border-t border-border-subtle
flex items-center justify-end gap-3
">
<Button variant="secondary" size="md">Cancel</Button>
<Button variant="primary" size="md">Confirm</Button>
</div>
</div>
</div>
```
---
## Loading States
### Spinner
```tsx
<div className="
w-8 h-8
border-4 border-warm-200 border-t-primary-500
rounded-full
animate-spin
" />
```
### Skeleton Loader
```tsx
// Text Line
<div className="h-4 w-3/4 bg-warm-200 rounded-medium animate-pulse" />
// Image Block
<div className="h-48 w-full bg-warm-200 rounded-large animate-pulse" />
// Card Skeleton
<div className="bg-surface-primary border border-border-subtle rounded-large p-6 space-y-4">
<div className="h-6 w-3/4 bg-warm-200 rounded-medium animate-pulse" />
<div className="h-4 w-full bg-warm-200 rounded-medium animate-pulse" />
<div className="h-4 w-5/6 bg-warm-200 rounded-medium animate-pulse" />
</div>
```
---
## Utility Classes
### Glassmorphism Panel
```tsx
// Pre-configured utility
<div className="glass-panel p-6 rounded-xlarge">
{content}
</div>
// Manual (if utility not available)
<div className="
bg-surface-secondary backdrop-blur-xs
border border-glass-border
shadow-glass
p-6 rounded-xlarge
">
{content}
</div>
```
### Focus Ring
```tsx
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
```
### Truncate Text
```tsx
className="truncate" // Single line
className="line-clamp-2" // Two lines
className="line-clamp-3" // Three lines
```
---
**Reference**: See `apps/web/tailwind.config.ts` for complete CVA patterns
**Version**: 1.0
**Last Updated**: 2025-11-28
```
### references/layout-mobile.md
```markdown
# Layout & Mobile Patterns
Token-optimized layout patterns for responsive, mobile-first design. Covers shell layouts, safe areas, touch targets, and responsive breakpoints.
---
## App Shell Layout
### Desktop: Sidebar + Main Content
```tsx
<div className="flex h-screen">
{/* Sidebar - 240px fixed, hidden on mobile */}
<aside className="
hidden md:block
w-60 /* 240px fixed width */
bg-surface-secondary backdrop-blur-xs
border-r border-border-subtle
shadow-translucent
overflow-y-auto
">
{/* Logo */}
<div className="p-6 border-b border-border-subtle">
<Logo />
</div>
{/* Navigation */}
<nav className="p-4 space-y-2">
<NavItem href="/dashboard" icon={HomeIcon}>Dashboard</NavItem>
<NavItem href="/people" icon={UsersIcon}>People</NavItem>
<NavItem href="/occasions" icon={CalendarIcon}>Occasions</NavItem>
<NavItem href="/lists" icon={ListIcon}>Lists</NavItem>
<NavItem href="/gifts" icon={GiftIcon}>Gifts</NavItem>
</nav>
{/* User Profile (Bottom) */}
<div className="mt-auto p-4 border-t border-border-subtle">
<UserProfile />
</div>
</aside>
{/* Main Content Area - flex-1 */}
<main className="
flex-1
overflow-y-auto
bg-bg-base
">
{children}
</main>
</div>
```
### Mobile: Bottom Navigation
```tsx
{/* Main content with bottom padding for nav */}
<main className="
pb-20 md:pb-0 /* Space for bottom nav on mobile */
overflow-y-auto
bg-bg-base
">
{children}
</main>
{/* Bottom Navigation - Fixed, Mobile Only */}
<nav className="
fixed bottom-0 left-0 right-0
md:hidden /* Hide on tablet+ */
h-14 /* 56px */
pb-safe-area-inset-bottom /* iOS safe area */
bg-surface-secondary backdrop-blur-xs
border-t border-border-subtle
shadow-translucent
flex items-center justify-around
z-50
">
{/* Nav Items (4-5 max) */}
<NavButton href="/dashboard" icon={HomeIcon}>
Dashboard
</NavButton>
<NavButton href="/people" icon={UsersIcon}>
People
</NavButton>
<NavButton href="/occasions" icon={CalendarIcon}>
Occasions
</NavButton>
<NavButton href="/lists" icon={ListIcon}>
Lists
</NavButton>
</nav>
```
### NavButton Component (Mobile Bottom Nav)
```tsx
<a
href={href}
className="
flex flex-col items-center justify-center
min-w-[44px] min-h-[44px] /* Touch target */
px-2 py-1
text-warm-700
hover:text-primary-500
transition-colors duration-200
"
>
<Icon className="w-6 h-6" />
<span className="text-xs mt-0.5">{label}</span>
</a>
{/* Active State */}
<a className="
...
text-primary-500
font-semibold
">
<Icon className="w-6 h-6 fill-current" />
<span className="text-xs mt-0.5">{label}</span>
</a>
```
---
## Safe Areas (iOS/Safari)
### Fixed Header with Safe Area
```tsx
<header className="
fixed top-0 left-0 right-0
pt-safe-area-inset-top /* iOS notch padding */
pl-safe-area-inset-left
pr-safe-area-inset-right
h-14 /* + safe-area-inset-top */
bg-surface-primary
border-b border-border-subtle
shadow-low
z-40
">
<div className="h-14 px-4 flex items-center justify-between">
<Logo />
<MenuButton />
</div>
</header>
```
### Bottom Navigation with Safe Area
```tsx
<nav className="
fixed bottom-0 left-0 right-0
pb-safe-area-inset-bottom /* iOS home indicator padding */
pl-safe-area-inset-left
pr-safe-area-inset-right
h-14 /* + safe-area-inset-bottom */
...
">
{navItems}
</nav>
```
### Full-Width Background with Safe Area
```tsx
<div className="
w-screen /* Full viewport width */
bg-primary-100
pl-safe-area-inset-left /* Avoid notch/camera */
pr-safe-area-inset-right
pt-safe-area-inset-top
pb-safe-area-inset-bottom
">
{content}
</div>
```
---
## Viewport Height (100dvh Fix)
### Full-Height Container
```tsx
{/* ALWAYS use 100dvh (dynamic) instead of 100vh */}
<div className="h-dvh"> {/* Fixes iOS address bar issue */}
{content}
</div>
{/* Fallback for older browsers */}
<div className="h-screen"> {/* Tailwind default: 100vh */}
{content}
</div>
{/* Manual calculation (avoid if possible) */}
<div style={{ height: 'calc(100vh - 60px)' }}>
{content}
</div>
```
### Full-Height with Header/Footer
```tsx
<div className="flex flex-col h-dvh">
{/* Header - Fixed height */}
<header className="h-14 flex-shrink-0">
{header}
</header>
{/* Main - Flexible */}
<main className="flex-1 overflow-y-auto">
{content}
</main>
{/* Footer - Fixed height */}
<footer className="h-14 flex-shrink-0">
{footer}
</footer>
</div>
```
---
## Touch Targets (44px Minimum)
### Button Touch Target
```tsx
{/* Minimum 44x44px for all interactive elements */}
<button className="
min-h-[44px] min-w-[44px] /* Minimum touch area */
px-6 py-2 /* Visual padding */
...
">
Click Me
</button>
```
### Icon-Only Button
```tsx
<button className="
w-11 h-11 /* 44px exactly */
flex items-center justify-center
rounded-full
hover:bg-warm-100
transition-colors duration-200
">
<Icon className="w-6 h-6" /> /* 24px icon */
</button>
```
### Checkbox/Radio Touch Area
```tsx
<label className="
flex items-center
min-h-[44px] /* Touch target height */
cursor-pointer
">
<input
type="checkbox"
className="
w-5 h-5 /* Visual size */
cursor-pointer
"
/>
<span className="ml-3 text-body-large">
Accept terms
</span>
</label>
```
---
## Responsive Breakpoints
### Breakpoint Values
```yaml
xs: 375px # iPhone SE
sm: 640px # Small tablets
md: 768px # Tablets (sidebar shows, bottom nav hides)
lg: 1024px # Laptops
xl: 1280px # Desktops
2xl: 1536px # Large desktops
```
### Mobile-First Responsive Pattern
```tsx
<div className="
w-full /* Mobile: full width */
md:w-1/2 /* Tablet: 50% */
lg:w-1/3 /* Desktop: 33% */
p-4 /* Mobile: padding */
md:p-6 /* Tablet: more padding */
lg:p-8 /* Desktop: even more */
">
Content
</div>
```
### Grid Responsive Pattern
```tsx
{/* Single column mobile, 2 cols tablet, 3 cols desktop */}
<div className="
grid
grid-cols-1 /* Mobile: 1 column */
md:grid-cols-2 /* Tablet: 2 columns */
lg:grid-cols-3 /* Desktop: 3 columns */
gap-4 /* Mobile: 16px gap */
md:gap-6 /* Tablet: 24px gap */
">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
```
### Show/Hide Based on Breakpoint
```tsx
{/* Visible on mobile only */}
<div className="block md:hidden">
Mobile content
</div>
{/* Hidden on mobile, visible on tablet+ */}
<div className="hidden md:block">
Desktop content
</div>
{/* Visible on desktop only */}
<div className="hidden lg:block">
Desktop-only content
</div>
```
---
## Dashboard Layout
### Stat Cards Grid
```tsx
<div className="
grid
grid-cols-1 /* Mobile: stacked */
md:grid-cols-3 /* Desktop: 3 columns */
gap-6
mb-12
">
<StatCard value={12} label="Ideas" color="idea" />
<StatCard value={8} label="To Buy" color="warning" />
<StatCard value={4} label="Purchased" color="success" />
</div>
```
### Avatar Carousel (Horizontal Scroll)
```tsx
<div className="
bg-surface-primary
border border-border-subtle
rounded-xlarge p-6
shadow-low
">
<h3 className="text-heading-2 font-semibold mb-4">
Family Members
</h3>
{/* Horizontal scroll container */}
<div className="
flex gap-6
overflow-x-auto /* Horizontal scroll */
pb-2 /* Space for scrollbar */
scrollbar-hide /* Hide scrollbar (optional) */
">
{members.map(member => (
<div key={member.id} className="
flex-shrink-0 /* Prevent shrinking */
text-center
">
<Avatar
size="lg"
src={member.avatar}
status={member.status}
badge={member.giftCount}
/>
<p className="text-body-small font-semibold mt-2">
{member.name}
</p>
</div>
))}
</div>
</div>
```
### Kanban Columns (List Detail)
```tsx
{/* Desktop: Horizontal scroll, Mobile: Vertical stack */}
<div className="
flex
flex-col md:flex-row /* Stack mobile, row desktop */
gap-6
overflow-x-auto /* Horizontal scroll desktop */
pb-6
">
{/* Each column */}
<div className="
w-full md:w-80 /* Full mobile, 320px desktop */
flex-shrink-0 /* Prevent shrinking */
bg-surface-primary
border border-border-subtle
rounded-large
shadow-low
">
{/* Column Header */}
<div className="
px-4 py-3
bg-gradient-to-br from-status-idea-50 to-status-idea-100
border-b-4 border-status-idea-500
rounded-t-large
">
<h4 className="text-heading-3 font-semibold text-status-idea-700">
Ideas <span className="text-body-small">(5)</span>
</h4>
</div>
{/* Column Content */}
<div className="
p-4
space-y-4
min-h-[400px]
">
{items.map(item => (
<GiftCard key={item.id} {...item} />
))}
</div>
</div>
{/* Repeat for other columns */}
</div>
```
---
## Content Spacing Patterns
### Page Container
```tsx
<div className="
max-w-7xl mx-auto /* Max 1280px, centered */
px-4 md:px-6 lg:px-8 /* Responsive horizontal padding */
py-6 md:py-8 lg:py-12 /* Responsive vertical padding */
">
{pageContent}
</div>
```
### Section Spacing
```tsx
<section className="mb-12"> {/* 48px bottom margin */}
<h2 className="text-heading-1 font-bold mb-6">
Section Title
</h2>
{content}
</section>
```
### Card Grid Spacing
```tsx
<div className="
grid
grid-cols-1 md:grid-cols-2 lg:grid-cols-3
gap-4 md:gap-6 /* Responsive gap */
">
{cards}
</div>
```
---
## Z-Index Hierarchy
```yaml
z-0: Base layer (backgrounds)
z-10: Default content
z-20: Sticky headers
z-30: Dropdowns
z-40: Modal overlays
z-50: Modals, bottom nav
z-60+: Tooltips, popovers
```
### Example Z-Index Usage
```tsx
{/* Modal Overlay */}
<div className="fixed inset-0 z-40 bg-overlay-strong" />
{/* Modal Content */}
<div className="fixed inset-0 z-50 flex items-center justify-center">
{modal}
</div>
{/* Bottom Nav */}
<nav className="fixed bottom-0 left-0 right-0 z-50">
{navItems}
</nav>
```
---
## Accessibility Considerations
### Focus Visible
```tsx
{/* All interactive elements */}
focus:outline-none
focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
```
### Skip to Content Link
```tsx
<a
href="#main-content"
className="
sr-only /* Screen reader only by default */
focus:not-sr-only /* Visible when focused */
focus:absolute focus:top-4 focus:left-4
focus:z-50
focus:px-4 focus:py-2
focus:bg-primary-500 focus:text-white
focus:rounded-medium
"
>
Skip to main content
</a>
```
### Responsive Font Sizes
```tsx
{/* Heading responsive sizing */}
<h1 className="
text-display-small /* Mobile: 28px */
md:text-display-medium /* Tablet: 36px */
lg:text-display-large /* Desktop: 48px */
">
Page Title
</h1>
```
---
## Performance Optimizations
### Lazy Loading Images
```tsx
<img
src={imageUrl}
loading="lazy" /* Native lazy loading */
className="rounded-large"
/>
```
### Scroll Snap (Carousels)
```tsx
<div className="
flex gap-6
overflow-x-auto
snap-x snap-mandatory /* Snap scrolling */
">
<div className="snap-start flex-shrink-0">
{item}
</div>
</div>
```
---
## Common Layout Examples
### Modal Layout
```tsx
{/* Full pattern from component-variants.md */}
<div className="fixed inset-0 z-40 bg-overlay-strong backdrop-blur-sm" />
<div className="fixed inset-0 z-50 flex items-center justify-center p-6">
<div className="
w-full max-w-lg
bg-surface-primary
rounded-2xlarge
shadow-extra-high
">
{modalContent}
</div>
</div>
```
### Sidebar Detail Panel
```tsx
<div className="flex h-screen">
{/* Main content */}
<main className="flex-1 overflow-y-auto">
{content}
</main>
{/* Detail panel - slide in from right */}
<aside className="
w-96 /* 384px */
border-l border-border-subtle
bg-surface-primary
overflow-y-auto
transform transition-transform duration-300
translate-x-full /* Hidden by default */
data-[open=true]:translate-x-0 /* Visible when open */
">
{detailContent}
</aside>
</div>
```
---
**Reference**: See `docs/designs/LAYOUT-PATTERNS.md` for comprehensive layout specs
**Version**: 1.0
**Last Updated**: 2025-11-28
```