---
stepsCompleted: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
inputDocuments: ['prd.md', 'architecture.md']
workflowType: 'ux-design'
project_name: 'pictureFrame'
user_name: 'Matt.edholm'
date: '2026-04-27'
---

# UX Design Specification - pictureFrame

**Author:** Matt.edholm
**Date:** 2026-04-27

---

<!-- UX design content will be appended sequentially through collaborative workflow steps -->

## Executive Summary

### Project Vision

pictureFrame is a handcrafted e-ink digital picture frame ecosystem built as a meaningful gift. The physical frame is hand-built; the companion web app is where the frame's content comes to life — uploading photos, approving shares from family, curating what someone you love wakes up to every morning.

The app is not a monitoring dashboard. It is an image curation tool with a warm, personal, playful character — simple and obvious in its interactions, fun in its personality. Future directions (image stickers, overlays) are consistent with this tone: the app should feel like a place where you make something for someone, not a place where you manage a device.

### Target Users

**Margaret (recipient)** — non-technical gift recipient. Approves photos shared by family members. Her primary interaction is clicking an email approve link and choosing which frame to add a photo to — no login required for that flow. When she does open the app, it should be immediately obvious what to do.

**Matt (builder/admin)** — gift giver and super admin. Uploads the initial photo collection, approves images per device, configures device settings, monitors the fleet. Power user comfort without admin visual weight — the controls should feel purposeful, not bureaucratic.

**Sarah (contributor)** — family member. Uploads photos and shares them to a recipient. Should never need to understand approvals, device settings, or the rotation engine. Her job is: pick a photo, send it to someone.

### Key Design Challenges

1. **Non-technical provisioning** — Two-phase QR flow must be self-explanatory and self-healing. Every failure state resets automatically. No step can require interpretation.

2. **Email-first approval** — The share email is a primary UX surface, not just a notification. It must work on any email client, any device, with no login. The device-selection page after approval is a critical no-auth screen.

3. **Three users, one app** — Matt wants control and visibility. Sarah wants to pick a photo and go. The app must not expose contributor-level users to admin complexity, while giving Matt the full picture.

### Design Opportunities

1. **Curation as the core emotional experience** — Choosing which photos appear on someone's frame is an act of care. The image upload and approval flows should feel considered and personal, not transactional. Image detail views should feel like a canvas — ready for future sticker/overlay features without a redesign.

2. **The gift moment** — When a device is first linked via setup QR, there is an opportunity to frame the experience as "you just gave someone this" rather than "device provisioning complete."

3. **Warm, playful personality** — Simple and obvious as the baseline; fun as the texture. The visual and copy language should reflect that this is for family, not for IT departments.

## Core User Experience

### Defining Experience

The heart of pictureFrame is the upload-to-frame flow: pick a photo, make it yours, see it in the frame, put it there. Everything else — sharing, approvals, device settings — orbits this moment.

The editing experience is the product's personality. Crop/fit and pre-baked sticker overlays are first-class, not afterthoughts. Images with stickers retain their edit state and can be re-edited at any time — sticker placement, sizing, and removal is always available on images you own.

### Platform Strategy

Mobile-first web app. iOS Safari and Android Chrome are the primary targets; all flows are designed touch-first. Laptop is a supported secondary use case. No native app.

Touch interactions drive all design decisions: tap targets sized for thumbs, drag-to-position for sticker placement, tap-to-remove (×) on placed stickers.

### Upload & Edit Flow

The primary upload flow is a linear funnel on mobile:

1. **Pick** — select from camera roll or files
2. **Crop/Fit** — frame-shaped crop UI (device aspect ratio always visible; landscape or portrait per device)
3. **Stickers** — add pre-baked sticker overlays; drag to position, resize, tap × to delete; multiple stickers supported
4. **Add to Frame** — choose which device(s) this goes to (own devices only; sharing to others uses the share flow)
5. **Done** — return to home page

Images with stickers can be re-edited from the library at any time. The original and edit state (crop parameters, sticker positions/sizes) are stored separately from the rendered output.

### Sharing & Approval

Sharing is initiated from the UI (library or image detail). The email approve flow is the primary approval mechanism for recipients — one tap, no login required, device-selection page after approval.

Users can also approve or decline images directly within the app UI (equivalent action to the email flow, both are first-class paths).

### Critical Success Moments

- **First photo on the frame** — the upload-edit-add funnel completes and the image enters rotation. This should feel like sending something.
- **Sticker placement** — drag-and-drop on mobile should feel immediate and fun; the frame preview keeps the recipient's experience in frame.
- **Email approval** — one tap, confirmation, done. No friction for non-technical recipients.
- **Re-editing** — returning to a photo with stickers and adjusting it should feel natural, not like a special mode.

### Experience Principles

1. **The frame is always in view** — the device's aspect ratio is visible throughout crop and sticker editing. You are always making something for someone.
2. **Edit state is never lost** — sticker compositions and crop settings are preserved and re-editable indefinitely.
3. **Fun is prominent** — stickers and editing are surfaced in the primary flow, not buried in an overflow menu.
4. **One flow, complete** — upload leads directly to frame assignment; no orphaned library items unless the user explicitly chooses not to add to a device.
5. **Approve your way** — email tap or UI action: both paths are first-class and produce identical results.

## Desired Emotional Response

### Primary Emotional Goals

- **Warmth** — sharing and curating photos should feel like an act of care, not a task. The product is for family; the UI should reflect that.
- **Playfulness** — the sticker/editing experience is where personality lives. It should make you smile. A funny edit spreads because someone saw it on a frame and said "send me that one."
- **Connection** — the frame is the social surface. You see a photo cycling on someone's wall, you want it, you ask for it. The app is how you send it; it should be fast.
- **Confidence** — every step obvious and safe, no dead ends.

### Emotional Journey Mapping

| Moment | Target feeling |
|---|---|
| First open | "This feels friendly" |
| Upload + crop | "Easy — and I'm making something" |
| Sticker placement | "Haha, yes" |
| Add to own frame | Immediate satisfaction — no approval, no wait |
| See photo on someone's frame in person | "Send me that one" |
| Owner finds it, shares it | Quick, obvious, done |
| Email approve (recipient) | "Oh that was easy" |
| Frame cycles to new photo | The payoff — warmth without the app |

### Micro-Emotions

- **Delight** — sticker interactions, fun copy, small animations on completion
- **Accomplishment** — the upload-edit-add funnel ends with something on the frame, not in a queue
- **Trust** — no approval needed for your own devices; the system does what you expect
- **Connection** — the frame in the room drives the social loop; the app just closes it

### Design Implications

- **Warmth** → rounded UI, named devices ("Dad's Frame", not "Device #3"), friendly copy throughout
- **Playfulness** → stickers prominent in edit flow; small celebratory feedback on actions
- **Connection** → finding and sharing a photo should take seconds; the share flow is the product's social handshake
- **Confidence** → one obvious action per screen; your own devices need no approval gate; clear feedback on every tap

### Emotions to Avoid

- Tech anxiety — especially for non-technical recipients
- Overwhelm — settings and admin complexity never leak into contributor or recipient flows
- Transactional — nothing should feel like filing paperwork
- Waiting — adding to your own frame is instant; no approval limbo for the uploader

## UX Pattern Analysis & Inspiration

### Inspiring Products Analysis

**Snapchat — sticker/overlay editing**
The gold standard for tap-to-place sticker UX on mobile. Users already know it: tap a sticker from a tray to place it on the canvas, drag to reposition, pinch to resize, tap to select, × to delete. Zero learning curve. The sticker tray scrolls horizontally and stays out of the way of the canvas. This is the exact pattern to replicate for pictureFrame's sticker editor.

**iMessage / Apple Photos — sharing and warmth**
Named recipients, not abstract IDs. Sharing feels like handing something to a specific person. Confirmation is quiet — a small animation, not a modal. The visual language is warm and human without being childish.

**Instagram — mobile upload funnel**
Crop-first design: you see the frame before you decide anything else. The aspect ratio is always visible. The upload flow is linear and committed — you move forward, not sideways. One obvious action per step.

**Canva (simplified) — pre-baked element tray**
A scrollable horizontal tray of categorized elements (stickers, in our case) that opens from the bottom of the screen. Doesn't take over the canvas. Categories let you browse without overwhelming. Works well for non-technical users who just want to pick something fun.

### Transferable UX Patterns

**Sticker interaction (from Snapchat):**
Tap from tray → placed on canvas → drag to move → pinch to resize → tap to re-select → × to delete. Applied directly to pictureFrame's sticker editor. Users arrive already knowing this pattern.

**Crop-first framing (from Instagram):**
Show the device aspect ratio before anything else in the edit flow. The frame shape is the first thing you see — you are always designing for a specific physical object, not cropping an abstract photo.

**Named, human recipients (from iMessage):**
Devices are always shown by their given name ("Margaret's Frame") in every context — upload, share, approve. Never a MAC address, device ID, or generic label.

**Bottom sheet element tray (from Canva/Snapchat):**
Sticker categories and sticker items in a bottom sheet that slides up without covering the full canvas. Scrolls horizontally within categories. Dismisses by tapping the canvas.

**Quiet confirmation (from Apple Photos):**
Completing an action (add to frame, approve, share) should feel settled and calm — a small visual acknowledgment, then return to context. No success modal that demands a tap to dismiss.

### Anti-Patterns to Avoid

- **Approval-first design** — adding a photo to your own frame should never require an approval step. Approval friction belongs only in the cross-user sharing flow.
- **Settings leaking into primary flows** — device configuration, rotation frequency, uniqueness windows are admin concerns. They should never appear in the upload, sticker, or share flows.
- **Buried stickers** — if stickers require more than one tap to reach from the edit screen, they won't be used. They must be a first-class element of the editing UI.
- **Empty canvas anxiety** — the crop/edit screen should never look blank or intimidating. The device frame shape, a clear "Add sticker" affordance, and the photo itself should fill the space confidently.
- **Generic confirmation copy** — "Success" or "Done" is wasted space. Every completion moment should say something specific and warm.

### Design Inspiration Strategy

**Adopt directly:**
- Snapchat sticker interaction model — tap, drag, pinch, ×
- Instagram crop-first funnel — frame shape before anything else
- Bottom sheet sticker tray — non-intrusive, dismissable

**Adapt for pictureFrame:**
- Canva element tray → simplified to stickers only, mobile-optimized, warmer visual style
- iMessage sharing warmth → applied to the share flow and email design, not just in-app interactions

**Avoid entirely:**
- Any UX that treats the frame as a device to be configured rather than a gift to be curated
- Multi-step confirmation flows for actions the user has clear intent on

## Design System Foundation

### Design System Choice

**Authenticated app:** Vue 3 SPA (TypeScript strict, Vite, Vue Router, Pinia) with SCSS modules scoped per SFC component and Konva.js + Vue-Konva for the sticker canvas editor.

**Public flows** (email approve/decline, provisioning setup page): Symfony + Twig, minimal SCSS, no Vue dependency.

### Rationale for Selection

- **Vue 3 over React** — Composition API is clean, bundle size is smaller, TypeScript integration is excellent, appropriate for this application's complexity level.
- **SCSS modules over Tailwind** — semantic, authored CSS produces more maintainable component styles; complex canvas states (sticker selected, dragging, pinch-active) express more clearly as authored classes than utility strings. No utility class noise in templates.
- **No DaisyUI** — components are written properly from scratch in Vue SFCs with scoped SCSS; a pre-built component library is unnecessary.
- **Konva.js** over Fabric.js — lighter, superior mobile touch event handling, maintained Vue-Konva wrapper for reactive sticker state.
- **TypeScript strict mode** — API response interfaces mirror Symfony entities; compiler surfaces contract drift before it reaches deployed devices. Non-negotiable given the no-breaking-changes firmware constraint.

### Implementation Approach

- Vue app scaffolded with Vite + TypeScript template
- `src/types/` directory holds interfaces mirroring every Symfony API response shape: `Device`, `Image`, `StickerLayer`, `RenderedAsset`, `Token`
- Symfony serves the Vue app's `index.html` shell for all authenticated routes; Vue Router handles client-side navigation
- Public Symfony routes (`/setup/{mac}`, `/token/{uuid}/approve`) remain outside the Vue app entirely

### Customization Strategy

- Design tokens defined as SCSS variables: color palette, spacing scale, border radius, typography — warm/playful values set once, used everywhere
- Component SCSS lives in each SFC `<style scoped lang="scss">`
- Global styles (reset, typography, base layout) in `src/styles/global.scss`

## Visual Design Foundation

### Color System

Six user-selectable themes, all shipping in V1. The user picks their preferred theme in app settings; the selection persists per account. All themes share the same semantic token structure — only the values change.

| Theme | Primary | Accent | Character |
|---|---|---|---|
| 🪵 Warm Craft | #c4622a | #f5c842 | Handmade, amber, walnut |
| 🎉 Playful Pop | #d94f6e | #ffb347 | Bold, energetic, fun |
| 🌿 Sage & Cream | #3d7a5a | #e8965a | Calm, natural, approachable |
| 🌸 Dusty Mauve | #8a4a7a | #d4a843 | Whimsical, warm, personal |
| 🌊 Ocean Dusk | #2a6878 | #f0875a | Refined, calm, premium |
| 🍯 Honey & Slate | #5a5068 | #e8a830 | Sophisticated, warm-neutral |

**Semantic tokens** (same names across all themes):
`--color-primary`, `--color-primary-soft`, `--color-accent`, `--color-surface`, `--color-surface-raised`, `--color-text`, `--color-text-muted`, `--color-border`

All themes meet WCAG AA contrast on their respective backgrounds.

### Typography System

**Typeface:** Nunito (Google Fonts, variable weight)
- Rounded terminals — warm and approachable without being childish
- Excellent mobile legibility at small sizes
- Variable weight supports the full scale without multiple font files

**Type scale:**

| Role | Size | Weight |
|---|---|---|
| Display | 28px | 800 |
| Heading 1 | 22px | 700 |
| Heading 2 | 18px | 700 |
| Body | 15px | 400 |
| Body strong | 15px | 600 |
| Label | 13px | 600 |
| Caption | 11px | 700 (tracked +0.08em) |

**Line heights:** 1.2 for headings, 1.6 for body, 1 for labels/captions.

### Spacing & Layout Foundation

**Base unit:** 8px
**Scale:** 4 · 8 · 12 · 16 · 24 · 32 · 48 · 64

**Touch targets:** 44px minimum height (iOS HIG)
**Card padding:** 16px mobile, 24px desktop
**Screen padding:** 16px mobile, 32px desktop
**Border radius:** 12px cards, 14px large cards, 100px pills/buttons

**Layout:** Single-column mobile (max 480px content width). Desktop widens to a centered 960px container — two-column where appropriate (library grid, device list).

### Accessibility Considerations

- All 6 themes validated at WCAG AA contrast (4.5:1 for body text)
- Touch targets ≥ 44px on all interactive elements
- Nunito at 15px body size exceeds readability minimums for older users (important for Margaret as a primary user)
- Theme preference stored per account, not per device

## Design Direction Decision

### Design Directions Explored

Five layout directions were evaluated:
1. **Photo Grid** — gallery-first, frame as secondary navigation
2. **Frame-Centric** — frames as top-level, navigate into each to manage photos
3. **Upload-First** — prominent upload CTA with recent activity below
4. **Activity Feed** — chronological feed with inline approvals
5. **Minimal Card** — frame card with current photo and Add action together

### Chosen Direction

**Direction 5 — Minimal Card**

Home screen shows each frame as a card: current photo preview, frame name, photo count, and a prominent "+ Add Photo" button — all in one view. One frame: large featured card with library strip below. Two or more frames: stacked cards, each self-contained with their own action.

### Design Rationale

Direction 5 is the most honest representation of the product's mental model: the frame is the hero, the photo is the content, the action is obvious. No secondary navigation required to accomplish the primary job.

It scales cleanly from a single-frame user (Matt gifting one frame) to a multi-frame household without a layout change — just more cards.

The library strip on the single-frame home gives power users a hint of their collection without making it the focus. The Library tab handles full browsing.

### Implementation Approach

- Home screen: `FrameList.vue` — renders `FrameCard.vue` per device
- Single frame state: large card with aspect-ratio photo preview, status chip, next-cycle info, full-width "+ Add Photo" CTA, library strip below
- Multi-frame state: compact stacked cards, each with thumbnail, name, count, and "+ Add" pill button
- Empty state (no frames yet): single card-shaped prompt to provision a frame via QR
- "+ Add Photo" always opens the upload → crop → sticker → add funnel

## User Journey Flows

### 1. Upload → Edit → Add to Frame

The primary flow. Triggered from "+ Add Photo" on any frame card.

```mermaid
flowchart TD
    A([Tap + Add Photo]) --> B[System photo picker opens]
    B --> C{Photo selected?}
    C -- No --> Z([Cancelled — back to Home])
    C -- Yes --> D[Crop screen\nDevice aspect ratio visible\nPinch/drag to fit]
    D --> E[Tap Next]
    E --> F[Sticker screen\nCanvas with sticker tray icon]
    F --> G{Add stickers?}
    G -- Yes --> H[Tap sticker tray\nBottom sheet opens]
    H --> I[Tap sticker → placed on canvas\nDrag/pinch/× to manage]
    I --> G
    G -- Done --> J[Tap Add to Frame]
    J --> K[Device picker bottom sheet\nFrames shown by name with thumbnail]
    K --> L{Select frames}
    L --> M[Tap Done]
    M --> N[Quiet confirmation animation]
    N --> O([Back to Home\nPhoto in rotation])
```

**Key UX decisions:**
- Crop screen shows the destination device's aspect ratio border — named ("Margaret's Frame") if coming from a specific frame card
- Stickers step is always shown but skippable — one tap to proceed without adding any
- Device picker pre-selects the frame the user came from (if any)
- No success modal — quiet animation, return to home

### 2. Device Provisioning (Two-Phase QR)

Triggered by the empty state on Home ("Set up your first frame") or the "+ Add a frame" action.

```mermaid
flowchart TD
    A([Frame powered on\nOr button held 5s to reset]) --> B[E-ink: QR code for AP\n"Scan to connect"]
    B --> C[User scans QR\nPhone joins PictureFrame AP]
    C --> D[Captive portal opens\nWiFi SSID + password entry]
    D --> E{WiFi connects?}
    E -- No --> F[E-ink: full red screen\nAP reactivates\nProvisioning QR redisplays]
    F --> B
    E -- Yes --> G[E-ink: success QR\n"Scan to finish setup"]
    G --> H[User scans QR\nOpens /setup/mac in browser]
    H --> I{Has account?}
    I -- No --> J[Registration screen\nEmail + password]
    J --> K[Account created\nDevice linked]
    I -- Yes --> L[Login screen]
    L --> K
    K --> M[App: name your frame\nOrientation · frequency]
    M --> N([Frame reboots\nBegins image cycle])
```

**Key UX decisions:**
- Phase 1 (AP + captive portal) has zero server dependency — works with no internet
- Every failure state is self-healing; user never needs to manually retry
- Setup page (`/setup/mac`) is a Symfony Twig page — outside the Vue SPA
- Frame naming happens immediately after linking — before the user leaves the setup flow

### 3. Share → Email Approve (Sarah → Margaret)

Triggered from image detail → Share action in the library.

```mermaid
flowchart TD
    A([Sarah: tap Share on image]) --> B[Share sheet\nRecipient search by name/email]
    B --> C[Sarah selects Margaret\nTaps Send]
    C --> D[Server sends email to Margaret\nImage preview + Approve button]
    D --> E{Margaret taps?}
    E -- Approve --> F[Device selection page\nNo login required\nShows Margaret's frames by name]
    F --> G[Margaret taps a frame\nTaps Done]
    G --> H[Image enters approved pool\nConfirmation shown]
    H --> I([Next cycle: photo appears on frame])
    E -- Decline --> J[Decline confirmed\nImage not added]
    E -- Ignores --> K[Token expires after TTL\nNo action taken]
```

**Key UX decisions:**
- Device selection page is a public Symfony Twig page — no login, no Vue app
- Margaret sees frames by their human name, not technical identifiers
- Approval is single-use — tapping Approve again after TTL shows a friendly expired message
- Sarah sees no confirmation until Margaret approves — she shared it, not approved it

### 4. In-App Approve / Decline

For users who approve shares through the app rather than email.

```mermaid
flowchart TD
    A([Notification or Library badge\n"1 photo waiting"]) --> B[Library → Shared tab\nPhoto shown with Approve/Decline]
    B --> C{User action}
    C -- Approve --> D[Device picker bottom sheet\nSame as upload flow]
    D --> E[Select frame → Done]
    E --> F[Image enters rotation\nQuiet confirmation]
    C -- Decline --> G[Confirm: Decline this photo?]
    G -- Yes --> H[Photo removed from Shared tab]
    G -- No --> B
```

**Key UX decisions:**
- In-app approve produces identical outcome to email approve — same device picker, same pool entry
- Decline requires one confirmation tap to prevent accidental dismissal
- Approved/declined photos leave the Shared tab immediately

### Journey Patterns

**Entry via context:** When "+ Add Photo" is tapped from a specific frame card, that frame is pre-selected throughout the upload funnel. When tapped from a general context, the device picker shows all frames with none pre-selected.

**Bottom sheet for selection:** Device picking, sticker tray, and share recipient search all use the same bottom sheet pattern — slides up, dismisses on tap outside or swipe down.

**Quiet completion:** No flow ends with a success modal. Completion is signalled by a brief animation and return to the previous context.

**Self-healing errors:** Every error state in provisioning resets to a retry-ready state automatically. No dead ends.

### Flow Optimization Principles

1. **Pre-selection reduces decisions** — context determines defaults; the user confirms rather than chooses from scratch
2. **No orphaned steps** — every flow has a clear completion state; nothing leaves the user stranded mid-flow
3. **Public flows stay simple** — approve/decline email pages and provisioning setup are Symfony Twig, not Vue
4. **Destructive actions confirm once** — decline, delete, and reset all require one confirmation tap; no double-confirmation

## Component Strategy

### Foundation Components

Built once, used everywhere. SCSS tokens drive all theming.

| Component | Purpose | Key states |
|---|---|---|
| `BaseButton` | Primary, secondary, pill, destructive variants | default, hover, active, disabled, loading |
| `BaseInput` | Text fields, password, email | default, focused, error, disabled |
| `BaseBottomSheet` | Slides up from bottom, dismisses on tap outside or swipe down | closed, open, loading |
| `BaseCard` | Rounded surface with shadow, padding variants | default, pressable |
| `BaseChip` | Inline label/tag with color variants | default, removable |
| `BaseToast` | Quiet completion animation — appears briefly, no dismiss required | success, info |
| `BottomNav` | 4-item fixed bottom navigation | item active/inactive states |

### Custom Components

**`FrameCard`**
The hero component. Shows frame's current photo, name, status, next-cycle info, and "+ Add Photo" CTA. Two layouts: featured (single frame) and compact (multi-frame list).
- States: `featured`, `compact`, `empty` (no photo yet), `offline` (red border), `sync-fail` (yellow border)
- Actions: tap photo → frame detail; tap "+ Add Photo" → upload funnel with frame pre-selected
- Accessibility: frame name as aria-label, status communicated via both color and text

**`CropEditor`**
Frame-shaped crop UI. Shows device aspect ratio as a bordered overlay. User pinches/drags the photo behind it to fit.
- Built on native CSS with touch event handlers (no canvas needed for crop)
- Shows destination frame name in corner ("Margaret's Frame")
- Orientation toggle (landscape/portrait) where device supports both
- States: `idle`, `dragging`, `pinching`

**`StickerCanvas`**
Konva.js stage wrapping the cropped photo with a sticker layer on top. Manages collection of placed stickers reactively via Pinia.
- Sticker object: `{ id, type, x, y, scale, rotation }`
- Touch: tap to place/select, drag to move, pinch to resize, tap × to delete
- Re-editable: loads saved sticker state when editing an existing photo
- States: `idle`, `sticker-selected` (shows × handle), `dragging`

**`StickerTray`**
Bottom sheet containing categorised sticker library. Scrolls horizontally within categories. Tap a sticker to place it on the `StickerCanvas`.
- Categories: Seasonal · Holidays · Fun · Family · Nature
- Dismisses by tapping the canvas or swiping the sheet down
- Pre-baked sticker set (SVG assets in `src/assets/stickers/`)

**`DevicePicker`**
Bottom sheet listing user's frames by name with current photo thumbnail. Used in: upload funnel (Add to Frame step), in-app approve flow.
- Single-select or multi-select mode
- Pre-selects the frame from context (if launched from a FrameCard)
- "All Frames" option in multi-select mode
- Empty state: "You don't have any frames yet" with link to provisioning

**`PhotoThumb`**
Square/rectangular thumbnail for library grids. Shows sticker indicator badge if photo has sticker composition saved.
- Sizes: `sm` (64px), `md` (80px), `lg` (120px)
- States: `default`, `selected` (checkbox overlay), `has-stickers` (badge)

**`ShareSheet`**
Bottom sheet for sharing a photo to another user. Text input searches connected family members by name or email. Tap to select, tap Send.
- States: `search`, `selected`, `sending`, `sent`

**`ApproveCard`**
Used in Library → Shared tab. Shows incoming shared photo with Approve and Decline actions inline.
- States: `pending`, `approved`, `declined`

### Implementation Priority

**Phase 1 — Core flows (ship nothing without these):**
`FrameCard` · `CropEditor` · `StickerCanvas` · `StickerTray` · `DevicePicker` · `BaseBottomSheet` · `BaseButton` · `BottomNav`

**Phase 2 — Library and sharing:**
`PhotoThumb` · `ShareSheet` · `ApproveCard` · `BaseChip` · `BaseCard`

**Phase 3 — Polish:**
`BaseToast` · `BaseInput` · sticker indicator animations · FrameCard offline/sync-fail states

## Defining Experience

### The Core Interaction

pictureFrame: "Make a photo and put it on someone's frame."

Pick a photo. Crop it to the frame's shape. Add something fun. Send it there. That's the product. Everything else — sharing, approvals, device settings, admin — supports this moment without appearing in it.

### User Mental Model

Users arrive thinking of this like making a card or decorating a photo for someone — a creative act with a specific recipient in mind. The physical frame is always the destination. Unlike Instagram (destination: feed) or Google Photos (destination: archive), the mental model here is: *I am making something that will sit on a wall in someone's home.*

That framing changes everything. The crop UI isn't "adjust composition" — it's "fit this photo to the frame on Margaret's mantle." The sticker isn't decoration for a post — it's a santa hat you put on Dad because it'll make her laugh every time it cycles up.

### Success Criteria

- The device frame shape is visible from the first moment of editing. The user always knows what they're making and for what.
- Sticker placement responds immediately to touch — tap to place, drag to move, no perceptible lag on mobile.
- "Add to frame" is the natural last step of every upload. No photo lands in a library without a destination unless the user explicitly skips it.
- The path from home screen to editing a photo is one tap.
- Completion feels like sending something, not saving a file.

### Pattern Analysis

**Established patterns used:**
- Instagram crop-first funnel — show the frame before any other option
- Snapchat sticker interaction model — tap, drag, pinch, × to delete
- iMessage-style quiet confirmation — small acknowledgment, return to context

**Novel combination:**
The crop UI shows the *physical device's aspect ratio*, not an abstract crop box. You are fitting a photo to a specific object. This framing is new — no mainstream app crops to a named physical recipient's device. The sticker tray appears within that same frame-shaped canvas. The entire editing experience is spatially oriented toward the destination.

No user education required — the interactions are familiar. The *context* (making something for a physical frame for a specific person) is what's new, and the UI makes that context obvious rather than explaining it.

### Experience Mechanics

**1. Initiation**
Prominent "+" or photo icon on the home screen. One tap. Opens the device camera roll / file picker immediately — no intermediate screen.

**2. Crop & Fit**
Photo loads into a frame-shaped crop UI (device aspect ratio: landscape 800×480 or portrait 480×800). User pinches/drags to fit. The frame border is styled to suggest the physical e-ink frame — the destination is visible from the first interaction.

**3. Stickers**
A sticker tray icon is visible at the bottom of the canvas. Tap to open a bottom sheet with sticker categories. Tap a sticker to place it on the canvas. Drag to reposition, pinch to resize, tap to select, tap × to delete. Tap outside the tray to dismiss it. Multiple stickers supported. Canvas auto-saves sticker state.

**4. Add to Frame**
"Add to Frame" CTA always visible. Tapping it slides up a device picker — shows all user's own frames by name with a small thumbnail of the current display. User taps a frame (or multiple). Taps Done.

**5. Completion**
Small, warm confirmation animation. Returns to home screen. No modal to dismiss. The photo is now in rotation on that frame.

## UX Consistency Patterns

### Button Hierarchy

One primary action per screen. Secondary and tertiary actions are visually subordinate — never compete with the primary.

| Variant | Use | Visual |
|---|---|---|
| **Primary** | The one obvious action (Add to Frame, Approve, Send) | Full-width pill, `--color-primary` fill, Nunito 600 |
| **Secondary** | Alternative action at same level (Skip, Cancel) | Pill outline, `--color-primary` stroke, transparent fill |
| **Ghost** | Low-priority in-context action (Edit, View details) | No border, `--color-text-muted`, min 44px tap target |
| **Destructive** | Irreversible actions (Decline, Delete, Remove from frame) | Pill, `--color-error` fill — never in the same visual group as primary |
| **Icon pill** | Floating action in canvas contexts (sticker tray toggle) | Circle, `--color-surface-raised`, 48px diameter |

**Rules:**
- Primary button is always full-width on mobile. Never two full-width buttons side by side.
- Destructive actions are never the first option presented. They appear below or after the primary action.
- Disabled state: 40% opacity, `cursor: not-allowed`, `aria-disabled="true"` — never hidden.

### Feedback Patterns

Quiet by default. Loud only for blocking errors.

| Type | Trigger | Treatment |
|---|---|---|
| **Success / completion** | Add to frame, approve, share sent | `BaseToast` — slides up from bottom, holds 2.5s, disappears; warm copy ("Photo on its way to Margaret's frame") |
| **Info** | Informational prompt (e.g., "Token expires in 24h") | `BaseToast` with `--color-accent` indicator — same slide/dismiss behavior |
| **Warning** | Non-blocking issue (frame offline, photo duplicate) | Inline warning chip on the relevant component — no toast, no modal |
| **Error** | Blocking failure (upload failed, network error) | Inline below the triggering action — red text + retry affordance; no modal unless the entire screen is broken |
| **Validation** | Form field constraint not met | Inline below the field — never above it, never in a banner |

**Rules:**
- Toasts never require a tap to dismiss. They are not modals.
- Error text never says "Error" as the first word. Describe what happened and what to do: "Couldn't save — tap to try again."
- Never show multiple toasts at once. Queue them; show the most recent.
- Confirmations for destructive actions use an inline confirmation pattern (confirm button appears in place of the action), not a modal dialog.

### Form Patterns

**Field states (BaseInput):**
- `default` — `--color-border` outline, `--color-text-muted` placeholder
- `focused` — `--color-primary` outline (2px), label floats up
- `filled` — label floated, text visible
- `error` — `--color-error` outline, error message below
- `disabled` — 40% opacity, not interactable

**Rules:**
- Labels are always visible (floating label pattern — not disappearing placeholders).
- Validation fires on blur, not on keystroke — don't punish the user mid-typing.
- Required fields are not marked with asterisks. If every field is required, say nothing. If a field is optional, mark it "(optional)".
- Password fields always include a show/hide toggle.
- Submit button is disabled until minimum required fields are filled.

### Navigation Patterns

`BottomNav` — 4 items, always visible in the authenticated app.

| Tab | Icon | Label |
|---|---|---|
| Home | House icon | Home |
| Library | Grid icon | Library |
| Shared | Heart icon (badge for pending) | Shared |
| Settings | Gear icon | Settings |

**Rules:**
- Active tab uses `--color-primary` fill, inactive tabs use `--color-text-muted`.
- The Shared tab shows a numeric badge when pending approvals exist. Max display: "9+" for 10+.
- `BottomNav` is hidden during full-screen editing flows (CropEditor, StickerCanvas) — the canvas owns the full screen.
- Back navigation in multi-step flows (upload funnel) uses an in-flow back button (top-left chevron), not the bottom nav.
- The upload funnel is a modal flow — launched over the current screen, closed on cancel or completion; bottom nav does not appear.

### Bottom Sheet Pattern

The primary overlay pattern. Used for: DevicePicker, StickerTray, ShareSheet, any in-context selection.

**Behavior rules:**
- Slides up with a 250ms ease-out animation.
- Handle pill (32px × 4px, `--color-border`) at top center — tap or swipe down to dismiss.
- Tap outside (on the darkened overlay) also dismisses.
- Scroll within the sheet when content exceeds ~60vh — the sheet does not expand beyond 85vh.
- Loading state within an open sheet shows a skeleton, not a spinner — never collapses the sheet to show a spinner.
- Bottom nav is hidden behind the overlay but still present — it reappears on sheet dismiss.

Full-page modals are not used for selection actions. Modals are reserved for destructive confirmations only — and even those use the inline confirmation pattern where possible.

### Empty States

Every screen with variable content has a defined empty state.

| Context | Empty state treatment |
|---|---|
| Home — no frames | Illustrated prompt card: frame silhouette + "Set up your first frame" + QR setup CTA |
| Home — frame with no photos | FrameCard shows device photo area as a soft dashed rect with "+ Add a photo" overlaid |
| Library — no uploads | Centered illustration + "No photos yet" + "Upload your first photo" button |
| Library Shared — no pending | Centered quiet message: "Nothing waiting to approve" — no illustration needed |
| Device picker — no frames | "You don't have any frames yet" + "Add a frame" link |
| Search — no results | "No photos matching '[term]'" — no illustration, just clear text |

**Rules:**
- Empty states always include one action. Never strand the user with only an explanation.
- Empty states never use the word "empty" or "nothing found" as the headline. Be specific to context.

### Loading States

Loading is communicated inline; the app never shows a full-screen spinner.

| Context | Loading treatment |
|---|---|
| Page/route transition | Top-of-screen progress bar (`--color-primary`, 3px) — no content blocking |
| Image grid loading | Skeleton cards — same size as `PhotoThumb`, shimmer animation |
| FrameCard photo loading | Skeleton in the photo area — same aspect ratio as the device |
| Bottom sheet content | Skeleton rows inside the open sheet |
| Button action in progress | `BaseButton` loading state: spinner replaces label, button disabled, width does not change |
| Upload progress | Progress ring in the upload sheet — percentage visible |

**Rules:**
- Skeletons match the shape of the content they're loading. No generic spinner where a card will appear.
- Button loading state never changes the button width — prevents layout shift.
- Loading states have a 200ms delay before appearing — instantaneous actions should not flash a skeleton.
- If loading exceeds 10 seconds, show a "Taking longer than expected — tap to cancel" inline option.

### Search and Filtering

Used in: Library (search by photo metadata/date), ShareSheet (recipient search).

**Library search:**
- Search bar appears below the page title — not in the nav bar.
- Real-time filter as the user types (300ms debounce).
- Active search shows a "× Clear" inline in the search field.
- Library tabs (All / Mine / Shared) remain visible below search — filtering is additive (tab + search combined).

**ShareSheet recipient search:**
- Inline within the bottom sheet — field is auto-focused when sheet opens.
- Searches by name and email simultaneously.
- Results appear as tappable rows with avatar initial + name.
- Selected recipient shows as a chip above the search field — tap chip to deselect.

**Rules:**
- Search never navigates away from the current screen — it filters the visible content in place.
- No "Search" button — results update live.
- Search field placeholder is context-specific: "Search your photos" / "Find someone…"

## Responsive Design & Accessibility

### Responsive Strategy

**Mobile is the primary platform.** iOS Safari and Android Chrome are the targets; every layout decision starts from a phone. Desktop is a supported second screen — useful for Matt managing his fleet, but Margaret and Sarah will be on their phones.

**Desktop gets more room, not a different product.** The same flows and components scale to a wider container — no feature gating by device, no reorganized information architecture. Desktop widens to a 960px centered container with some two-column opportunities (library grid, settings + device list side-by-side).

**Tablet is phone layout at a larger scale.** No dedicated tablet layout — mobile layout centered with max-width 640px on the content. Tablets are uncommon enough in this use case to not warrant a dedicated design tier.

**Canvas contexts are mobile-only in V1.** The sticker editor (CropEditor + StickerCanvas) is not optimized for desktop — pinch-to-resize doesn't exist with a mouse. Desktop shows the crop/sticker UI in a centered modal, with drag-to-move functional and scroll-to-resize as a fallback.

### Breakpoint Strategy

Mobile-first SCSS. Media queries add layout complexity at wider sizes — they never remove it.

```scss
// Defined once in src/styles/_breakpoints.scss
$bp-tablet: 640px;   // tablet and up
$bp-desktop: 960px;  // desktop and up

@mixin tablet { @media (min-width: #{$bp-tablet}) { @content; } }
@mixin desktop { @media (min-width: #{$bp-desktop}) { @content; } }
```

**What changes at each breakpoint:**

| Element | Mobile (< 640px) | Tablet (640–959px) | Desktop (960px+) |
|---|---|---|---|
| Content width | 100% (16px padding) | 100% (32px padding) | 960px centered |
| Library grid | 2 columns | 3 columns | 4 columns |
| FrameCard (multi) | Full-width stacked | Full-width stacked | Two-column grid |
| Settings | Single column | Single column | Two-column (nav + content) |
| Bottom nav | Fixed bottom | Fixed bottom | Hidden — top nav instead |
| Upload funnel | Full screen | Full screen | Centered modal (480px wide) |

No JavaScript for breakpoints. All layout adaptation is pure CSS/SCSS. JS `matchMedia` is used only for canvas resize events in the sticker editor.

### Accessibility Strategy

**Target: WCAG 2.1 AA.** Margaret is the most accessibility-sensitive user — she may be older, less familiar with apps, potentially using system large text. The email approve flow is a public-facing page that must work for anyone who receives it, with any assistive technology.

**Specific requirements:**

| Requirement | Standard | Implementation |
|---|---|---|
| Color contrast (body text) | 4.5:1 minimum | All 6 themes validated at AA |
| Color contrast (large text ≥18px) | 3:1 minimum | All theme headings verified |
| Touch target size | 44×44px minimum | Enforced via BaseButton and all interactive elements |
| Focus indicators | Visible, 3:1 contrast | `--color-primary` 2px ring on all interactive elements |
| Text resize | 200% zoom without horizontal scroll | Single-column mobile layout handles this naturally |
| Screen reader labels | All interactive elements labeled | `aria-label` on icon-only buttons; `aria-live` on toasts |
| Color not sole indicator | Status communicated beyond color | FrameCard offline: red border + text label; share badge: color + number |
| Skip navigation | Skip-to-content link | First element in DOM: `<a class="skip-link">Skip to main content</a>` |
| Form labels | Always visible | Floating label pattern — never placeholder-only |
| Error identification | Described in text | Error messages describe the issue, not just highlight the field red |

### Focus Management

- **Bottom sheet open:** Focus moves to the first interactive element inside the sheet.
- **Bottom sheet close:** Focus returns to the trigger element that opened the sheet.
- **Upload funnel (modal flow):** Focus trapped within the funnel steps. Escape key dismisses.
- **Toast notifications:** Announced via `aria-live="polite"` — screen readers read them without interrupting current action.
- **Route transitions:** On navigation, focus moves to the page `<h1>` — prevents focus loss between route changes.

### Testing Strategy

**Responsive:**
- Primary test devices: iPhone SE (smallest supported), iPhone 15 Pro Max (largest common), Android mid-range (Pixel 7a)
- Browser targets: Safari iOS, Chrome Android, Chrome/Firefox desktop, Safari desktop
- Viewport testing range: 375px – 1440px
- Real device testing for touch interactions — simulator does not reliably replicate pinch/drag behavior

**Accessibility:**
- **Automated:** axe-core via `@axe-core/vue` in development mode — surfaces AA violations in browser console during development
- **Color contrast:** All theme token combinations validated before any theme ships
- **Screen reader:** VoiceOver (iOS) for mobile flows; VoiceOver (macOS) + NVDA (Windows) for desktop and email flows
- **Keyboard navigation:** Full keyboard walkthrough of all primary flows — tab order, focus rings, enter/space on custom controls
- **Touch target audit:** Visual overlay tool to verify 44px minimum on every release

**Email flows (special case):**
- Approve/decline email tested in: Gmail (mobile + web), Apple Mail (iOS + macOS), Outlook (web)
- No Vue, no JavaScript required — Twig-rendered HTML must function with images disabled, CSS disabled, and screen reader only
- Plain text email fallback required

### Implementation Guidelines

**SCSS responsive approach:**
```scss
// Mobile-first — base styles are mobile; mixin adds desktop behavior
.library-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;

  @include tablet { grid-template-columns: repeat(3, 1fr); }
  @include desktop { grid-template-columns: repeat(4, 1fr); }
}
```

**Semantic HTML first:**
- Page structure: `<main>`, `<nav>`, `<header>`, `<section>` — not `<div>` soup
- Button elements for actions, `<a>` for navigation — no `<div>` click handlers
- Lists (`<ul>/<li>`) for library grids and device pickers
- `<figure>/<figcaption>` for photo thumbnails where a label is meaningful

**ARIA usage rules:**
- Use native HTML semantics before reaching for ARIA
- `aria-label` on icon-only buttons: `<button aria-label="Close sticker tray">`
- `aria-live="polite"` on toast container
- `aria-expanded` on BottomSheet trigger buttons
- `aria-selected` on BottomNav active tab
- `aria-invalid` + `aria-describedby` wiring on BaseInput error state

**Vue-specific accessibility:**
- `v-focus` directive for programmatic focus management (on sheet open, route change)
- Route change handler in `App.vue` — on every `$route` change, focus the `<h1>` of the new view
- Canvas context (Konva stage): keyboard-accessible alternatives for all sticker actions — sticker selection available via visible control buttons, not drag-only
