UX Principles¶
Sapari's interface is built on a set of deliberate design decisions. This page explains the philosophy behind them.
Dual-Experience Design¶
Mobile and desktop are completely separate interfaces, not responsive adaptations of the same layout. They share backend state through React Query but render entirely different component trees.
Dashboard.tsx (line ~1305)
├── isMobile = true → <MobileDashboard /> (20 components, touch-first)
└── isMobile = false → <Desktop layout /> (sidebar + timeline + video)
Why not responsive? Video editing has fundamentally different interaction models on touch vs. mouse. A responsive layout would compromise both experiences. Instead:
- Desktop optimizes for precision: multi-track waveform editing, keyboard shortcuts, drag handles, pixel-level control
- Mobile optimizes for speed: swipe gestures, wizard flows, tap-to-decide, one-handed use
The breakpoint is 768px (UI.MOBILE_BREAKPOINT). Mobile renders when the device supports touch AND the shorter viewport dimension is below 768px. Desktop browsers resized narrow stay in desktop mode.
Brutalist Design System¶
Sapari uses a brutalist aesthetic — intentionally raw, direct, and functional. No soft gradients, no rounded corners, no decorative elements.
Visual Tokens¶
| Token | Value | Usage |
|---|---|---|
| Border weight | border-4 border-black |
Primary containers |
| Border radius | rounded-none |
Everything — sharp edges only |
| Shadow | shadow-hard |
Hard-offset box shadow (no blur) |
| Typography | Monospace, ALL-CAPS | Labels, headers, UI text |
| Primary color | #FF3300 (Sapari Orange) |
Accents, active states, CTAs |
| Dark background | #0d0d0d |
Dark mode base |
Button System¶
Four variants, all with hard shadows and press animations:
| Variant | Style | Use |
|---|---|---|
primary |
White bg, black text | Default actions |
black |
Black bg, orange text | Emphasis |
secondary |
Gray bg, black text | Secondary actions |
orange |
Orange bg, white text | Primary CTAs |
Interaction micro-animations:
- Hover:
hover:-translate-y-1 hover:-translate-x-1(lift effect) - Press:
active:translate-x-[2px] active:translate-y-[2px](push into surface) - Transitions:
transition-all duration-200
Dark Mode¶
Dark mode removes shadows (dark:shadow-none) and replaces black borders with semi-transparent orange (dark:border-orange-500/50). The brutalist structure remains, but the contrast shifts from black-on-white to orange-on-dark.
Shared State, Separate Rendering¶
Both mobile and desktop read from the same React Query cache. This means:
- Edits made on desktop appear instantly on mobile (and vice versa)
- No separate "mobile API" — same hooks, same query keys
- Optimistic updates work identically across both surfaces
Shared hooks (used by both mobile and desktop):
useEdits()— edit list with mutationsuseClips()— clip datauseCaptionLines()— caption stateuseProject()— project metadatauseAssets()— asset library
Separate hooks exist where interaction models diverge (e.g., useTimelineViewport for desktop scroll/zoom vs. mobile's pinch-zoom in FocusMode).
Design Decisions¶
Why monospace?¶
Monospace text makes timestamps, durations, and technical values scan faster. Since Sapari's UI is dense with time codes and numeric values, monospace creates visual consistency and predictable column alignment.
Why no border radius?¶
Sharp edges reinforce the tool-like, professional feel. Rounded corners suggest friendliness; hard edges suggest precision. Sapari is a power tool, not a consumer toy.
Why hard shadows?¶
Hard shadows (no blur, fixed offset) create a clear visual hierarchy without ambiguity. Elements either cast a shadow or they don't — there's no gradient of "how elevated" something is.
Why separate mobile/desktop?¶
A timeline editor cannot be made responsive. On desktop, you need:
- Multi-track waveform visualization
- Pixel-precise drag handles
- Horizontal scroll with zoom
- Right-click context menus
- Keyboard shortcuts (Space, E, Cmd+Z)
On mobile, you need:
- Swipe-to-decide card stacks
- Pinch-to-zoom with inertia
- Bottom sheets instead of sidebars
- Large touch targets (44px minimum, per WCAG 2.5.5)
- One-handed reachability
These are incompatible interaction models. Trying to merge them would make both worse.
Constants Reference¶
Key layout values from shared/config/constants.ts:
| Constant | Value | Context |
|---|---|---|
MOBILE_BREAKPOINT |
768px |
Mobile/desktop split |
TABLET_BREAKPOINT |
1024px |
Tablet detection |
MIN_TOUCH_TARGET |
44px |
WCAG minimum tap size |
UI_DEBOUNCE_MS |
100ms |
Resize event throttling |
SEARCH_DEBOUNCE_MS |
300ms |
Search input delay |
AUTO_SAVE_DELAY_MS |
1000ms |
Edit auto-save |
SETTINGS_SAVE_DEBOUNCE_MS |
500ms |
Settings persistence |
SUCCESS_NOTIFICATION_MS |
2000ms |
Success toast duration |
ERROR_NOTIFICATION_MS |
3000ms |
Error toast duration |
CLICK_THRESHOLD_MS |
200ms |
Quick-click vs long-press |