docs: Motion System v1 + Button gold standard + Component Template + Decision Tree

- Motion System v1 (chapter 5):
  - Motion principles: when to animate, when NOT to animate
  - Duration scale (instant to slower: 0ms to 500ms)
  - Easing functions (linear, in, out, in-out, spring)
  - Component-specific motion: Button, Modal, Drawer, Map, Skeleton,
    Progress, Error/Success
  - Reduced motion support (prefers-reduced-motion)
  - Interruption rules (animations must be interruptible)
  - Error/success animations (shake, checkmark, pulse)

- Button gold standard (chapter 8.3.1):
  - Full specification: purpose, design goal, token dependencies,
    layout, sizes, variants, states, keyboard, touch, accessibility,
    anti-patterns, related components, DDR references
  - Sets the bar for all future component specifications

- COMPONENT_TEMPLATE.md (v1.0, LOCKED):
  - Standard template for all component specifications
  - Sections: purpose, tokens, layout, sizes, variants, states,
    keyboard, touch, accessibility, motion, acceptance criteria,
    anti-patterns, related components, DDR references

- COMPONENT_DECISION_TREE.md (v1.0, LOCKED):
  - Decision trees for: user actions, information display, user input,
    feedback, navigation, Landvex-specific
  - Quick reference table mapping needs to components
  - Prevents invention of unnecessary components

Rationale: Motion before brand (affects every interaction). Button as
reference implementation. Template ensures consistency. Decision tree
guides AI agents and developers to correct component choice.
This commit is contained in:
Bernt
2026-07-02 08:53:20 +00:00
parent 48236658e8
commit 1e40d384ca
3 changed files with 547 additions and 42 deletions
+182 -42
View File
@@ -511,29 +511,129 @@ Dedicated palette for geospatial visualization. Defined by **layer**, not indivi
## 5. Motion System
> **Status:** ⏳ Ej påbörjad
> **Status:** ✅ Specified
> **Principle:** Motion supports understanding. It never decorates.
### 5.1 Duration Scale
### 5.1 Motion Principles
**When to animate:**
- State changes that need attention (modal open, toast appear)
- Spatial relationships (drawer slide, page transition)
- Feedback on user action (button press, form submit)
- Loading and progress (skeleton, progress bar)
**When NOT to animate:**
- Static content that does not change
- Data updates that are frequent (live dashboards)
- Elements entering viewport on scroll (use Intersection Observer, not animation)
- Anything that delays user action
**Rules:**
- Animations must be interruptible
- Multiple animations must not conflict
- Motion must respect `prefers-reduced-motion`
### 5.2 Duration Scale
| Token | Value | Usage |
|-------|-------|-------|
| `duration-instant` | 0ms | No animation |
| `duration-fast` | 100ms | Hover states |
| `duration-fast` | 100ms | Hover states, focus rings |
| `duration-normal` | 200ms | Standard transitions |
| `duration-slow` | 300ms | Page transitions |
| `duration-slower` | 500ms | Complex animations |
| `duration-slow` | 300ms | Page transitions, drawers |
| `duration-slower` | 500ms | Complex animations, modals |
### 5.2 Easing Functions
### 5.3 Easing Functions
| Token | Value | Usage |
|-------|-------|-------|
| `ease-linear` | linear | Continuous motion |
| `ease-in` | cubic-bezier(0.4, 0, 1, 1) | Exit animations |
| `ease-out` | cubic-bezier(0, 0, 0.2, 1) | Enter animations |
| `ease-in-out` | cubic-bezier(0.4, 0, 0.2, 1) | Standard |
| `ease-spring` | cubic-bezier(0.34, 1.56, 0.64, 1) | Playful interactions |
| `ease-linear` | linear | Continuous motion, progress bars |
| `ease-in` | cubic-bezier(0.4, 0, 1, 1) | Exit animations, dismiss |
| `ease-out` | cubic-bezier(0, 0, 0.2, 1) | Enter animations, appear |
| `ease-in-out` | cubic-bezier(0.4, 0, 0.2, 1) | Standard, symmetric |
| `ease-spring` | cubic-bezier(0.34, 1.56, 0.64, 1) | Playful, overshoot |
### 5.3 Motion Principles
### 5.4 Component-Specific Motion
#### 5.4.1 Button
| State | Animation | Duration | Easing |
|-------|-----------|----------|--------|
| Hover | Background color shift | 100ms | ease-out |
| Press | Scale to 0.98 | 50ms | ease-in |
| Release | Scale to 1.0 | 100ms | ease-out |
| Loading | Spinner rotation | 1s | linear infinite |
| Disabled | Opacity to 0.5 | 100ms | ease-in-out |
#### 5.4.2 Modal
| State | Animation | Duration | Easing |
|-------|-----------|----------|--------|
| Open | Fade in + scale from 0.95 | 300ms | ease-out |
| Close | Fade out + scale to 0.95 | 200ms | ease-in |
| Backdrop | Fade to 50% opacity | 300ms | ease-in-out |
#### 5.4.3 Drawer
| State | Animation | Duration | Easing |
|-------|-----------|----------|--------|
| Open | Slide from edge | 300ms | ease-out |
| Close | Slide to edge | 200ms | ease-in |
| Backdrop | Fade to 50% opacity | 300ms | ease-in-out |
#### 5.4.4 Map Transitions
| State | Animation | Duration | Easing |
|-------|-----------|----------|--------|
| Pan | None (instant) | 0ms | — |
| Zoom | Smooth interpolation | 300ms | ease-in-out |
| Layer toggle | Cross-fade | 200ms | ease-in-out |
| Feature select | Highlight pulse | 300ms | ease-out |
**Rule:** Map panning is never animated. Users need instant feedback on drag.
#### 5.4.5 Skeleton
| State | Animation | Duration | Easing |
|-------|-----------|----------|--------|
| Loading | Shimmer sweep | 1.5s | ease-in-out infinite |
| Complete | Fade to content | 200ms | ease-out |
#### 5.4.6 Progress
| State | Animation | Duration | Easing |
|-------|-----------|----------|--------|
| Indeterminate | Sweep | 1s | linear infinite |
| Determinate | Width growth | 300ms | ease-out |
| Complete | Checkmark draw | 200ms | ease-out |
### 5.5 Reduced Motion
When `prefers-reduced-motion: reduce` is active:
- All animations become instant (0ms)
- Skeleton shows static placeholder (no shimmer)
- Progress shows static bar (no sweep)
- Map zoom uses instant jump (no interpolation)
- Modals and drawers appear instantly (no fade/scale)
**Exception:** Loading spinners may rotate slowly (2s per rotation) to indicate activity without motion.
### 5.6 Interruptions
**Rule:** Animations must be interruptible.
- If a modal is closing and user opens another, the first must complete or reverse instantly
- If a drawer is sliding and user taps backdrop, it must reverse direction
- No animation queue buildup
### 5.7 Error and Success Animations
| Type | Animation | Duration | Purpose |
|------|-----------|----------|---------|
| Error | Subtle shake (4px, 3 cycles) | 300ms | Indicate invalid input |
| Success | Checkmark draw + fade | 400ms | Confirm completion |
| Warning | Pulse border color | 2s | Draw attention without alarm |
---
@@ -666,46 +766,86 @@ Only then may a new component be defined.
### 8.3.1 Button
**States:**
- Default
- Hover
- Focus
- Pressed (Active)
- Loading
- Disabled
**Purpose:** Trigger primary user actions. The most used interactive element.
**Design Goal:** Immediately recognizable, consistently responsive, accessible without thought.
**Token Dependencies:**
- `button-primary-bg``brand-500`
- `button-primary-bg-hover``brand-600`
- `button-primary-text``text-inverse`
- `button-secondary-bg``surface-secondary`
- `button-secondary-border``border-default`
- `button-ghost-text``brand-500`
- `space-padding-button``space-3` (12px vertical, `space-4` horizontal)
- `radius-button``radius-2` (4px)
**Layout:**
- Height: 40px (default), 32px (small), 48px (large)
- Padding: 12px vertical, 16px horizontal
- Icon + text gap: 8px
- Border radius: 4px
**Sizes:**
| Size | Height | Padding | Font | Usage |
|------|--------|---------|------|-------|
| Small | 32px | 8px 12px | `text-sm` | Dense UIs, tables |
| Default | 40px | 12px 16px | `text-base` | Standard |
| Large | 48px | 16px 24px | `text-lg` | Hero actions, mobile |
**Variants:**
- Primary
- Secondary
- Tertiary (Ghost)
- Danger
- Icon-only
**ARIA:**
| Variant | Background | Border | Text | Usage |
|---------|------------|--------|------|-------|
| Primary | `brand-500` | None | White | Main action |
| Secondary | `surface-secondary` | `border-default` | `text-primary` | Alternative action |
| Ghost | Transparent | None | `brand-500` | Low emphasis |
| Danger | `semantic-red-500` | None | White | Destructive action |
| Icon-only | Any above | None | CurrentColor | Toolbar, compact |
**States:**
| State | Visual | Motion |
|-------|--------|--------|
| Default | As variant | None |
| Hover | Darken 10% | Background shift 100ms ease-out |
| Focus | 2px ring `border-focus` | Ring appear 100ms ease-out |
| Pressed | Scale 0.98 | Scale 50ms ease-in |
| Loading | Spinner replaces text | Spinner 1s linear infinite |
| Disabled | Opacity 0.5, no pointer | None |
**Keyboard:**
- `Tab`: Focus
- `Enter` / `Space`: Activate
- Disabled: Not focusable
**Touch:**
- Min 44x44px touch target
- Active state on press (immediate)
- No hover state on touch
**Accessibility:**
- `role="button"`
- `aria-label` for icon-only
- `aria-disabled` for disabled
- `aria-busy` for loading
- Focus ring always visible
**Keyboard:**
- `Enter` / `Space` to activate
- `Tab` to focus
**Anti-Patterns:**
- ❌ Two primary buttons in same view
- ❌ Disabled button without explanation
- ❌ Button that looks like link
- ❌ Loading button that remains clickable
**Touch:**
- Min 44x44px touch target
- Active state on press
**Related Components:**
- Split Button (extends Button)
- Icon Button (extends Button)
- Floating Action Button (extends Button)
**Animation:**
- Hover: background-color 100ms ease-out
- Press: scale(0.98) 50ms
- Loading: spinner rotation 1s linear infinite
**Acceptance Criteria:**
- [ ] All states are visually distinct
- [ ] Focus ring is visible
- [ ] Loading state prevents double-submit
- [ ] Disabled state is not focusable
- [ ] Touch target meets 44px minimum
**DDR References:**
- DDR-001: Why buttons use brand color for primary action
- DDR-002: Why ghost variant exists (low emphasis without border)
### 8.3.2 Input / Text Field