Compare commits

...

2 Commits

Author SHA1 Message Date
football2801 94dae685e2 docs: update architecture to reflect Vue 3 SPA frontend decision
Replace AssetMapper + Stimulus + Turbo with Vue 3 SPA (Vite,
TypeScript strict, SCSS modules, Konva.js). Authenticated app is
now a full SPA served by Symfony catch-all; public flows (provisioning,
email approve/decline) remain Symfony + Twig. Add JSON API controllers
for SPA, SpaController catch-all, updated directory structure, and
revised implementation sequence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 21:57:06 -04:00
football2801 2a2c8ae343 docs: add UX design specification and visual explorers
Complete 14-step UX design spec covering core experience, emotional
response, design system (Vue 3 + TypeScript + SCSS + Konva.js), 6
color themes, Direction 5 (Minimal Card), user journeys, component
strategy, UX patterns, and responsive/accessibility requirements.
Includes interactive theme explorer and design direction mockups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 21:54:01 -04:00
5 changed files with 1994 additions and 70 deletions
-3
View File
@@ -1,6 +1,3 @@
# AI tool output (planning artifacts are excluded from git)
_bmad-output/
# PlatformIO build artifacts
.pio/
+110 -67
View File
@@ -72,7 +72,7 @@ symfony new pictureframe --webapp
**Language & Runtime:** PHP 8.4+, Symfony 8.0
**Templating:** Twig with Stimulus and UX Turbo (Hotwire) — lightweight interactivity without a JS build step
**Templating:** Twig — used only for public flows (provisioning setup, email approve/decline pages). Authenticated app is a Vue 3 SPA; Twig is not used there.
**ORM & Database:** Doctrine ORM, PostgreSQL 16 (DDEV local), migrations via `doctrine/migrations`
@@ -80,17 +80,19 @@ symfony new pictureframe --webapp
**Email:** Symfony Mailer — handles sharing flows and hard-delete confirmations
**Build Tooling:** AssetMapper (no Webpack/Node build step required)
**Build Tooling:** Vite (Vue SPA, outputs to `public/build/`). AssetMapper not used — no Stimulus, no Turbo, no `importmap.php`.
**Testing:** PHPUnit via `symfony/test-pack`
**Code Organization:** Standard Symfony structure — `src/Entity`, `src/Controller`, `src/Repository`, `src/Service`, `templates/`
**Code Organization:** Standard Symfony structure — `src/Entity`, `src/Controller`, `src/Repository`, `src/Service`, `templates/` (public flows only) + `frontend/` (Vue SPA source)
**Additional packages required:**
- `symfony/messenger` + Doctrine transport — async image processing worker
- `symfony/scheduler` — rotation engine and scheduled cleanup
- Image processing library (TBD step 4: GD vs Imagick)
**Post-scaffold cleanup:** After `symfony new pictureframe --webapp`, remove `symfony/stimulus-bundle`, `symfony/ux-turbo`, and AssetMapper. Initialize the Vue SPA in `frontend/` with `npm create vite@latest frontend -- --template vue-ts`.
**Local Dev:** DDEV configured to mirror aqua-iq (PHP 8.4, Nginx-FPM, PostgreSQL 16)
**Note:** Project initialization using this command is the first implementation story.
@@ -141,7 +143,9 @@ symfony new pictureframe --webapp
**Device API:** Single endpoint — `GET /api/device/{mac}/image` — returns raw binary (`application/octet-stream`). This is the only machine-to-machine API surface. No versioning scheme beyond URL stability guarantee (no breaking changes in V1).
**Web Controllers:** Standard Symfony controllers returning Twig responses. No JSON API for the web application.
**Web Controllers (authenticated app):** Symfony controllers return JSON responses for all authenticated app API calls under the `/api/` prefix. No Twig rendering for authenticated routes — Symfony serves the SPA shell only. Controllers use `JsonResponse` or Symfony Serializer.
**Web Controllers (public flows):** `/setup/{mac}`, `/token/{uuid}/approve`, `/token/{uuid}/decline` return Twig responses. These are the only controllers that render HTML directly.
**Email:** Symfony Mailer. Transactional emails: image share notification (with approve link), hard-delete confirmation. Authorization links embedded as tokenized URLs pointing to Symfony routes.
@@ -153,13 +157,26 @@ symfony new pictureframe --webapp
### Frontend Architecture
**Templating:** Twig. Identical pattern to aqua-iq.
**Authenticated App — Vue 3 SPA:**
All authenticated routes are served by a Vue 3 SPA built with Vite + TypeScript strict mode. Vue Router handles client-side navigation; Pinia manages shared state (current user, device list, upload funnel state). SCSS modules scoped per SFC for component styles. Konva.js + Vue-Konva for the sticker canvas editor. No Stimulus, no Turbo, no AssetMapper.
**Interactivity:** Stimulus controllers + Turbo Drive (Hotwire). No SPA, no build step required.
Symfony serves the SPA shell (`public/build/index.html`) via a catch-all route for all authenticated paths. Vue Router takes over client-side navigation from that point.
**Forms:** Symfony Form component.
**Public Flows — Symfony + Twig:**
Three routes remain as Symfony Twig pages with no Vue dependency:
- `/setup/{mac}` — device provisioning setup (post-QR scan)
- `/token/{uuid}/approve` — email approve page (no login required)
- `/token/{uuid}/decline` — email decline page (no login required)
**Assets:** AssetMapper (no Webpack/Node).
These must work with images disabled, CSS disabled, and screen reader only.
**TypeScript:** Strict mode. `frontend/src/types/` holds interfaces mirroring every Symfony API response shape: `Device`, `Image`, `StickerLayer`, `RenderedAsset`, `Token`. The compiler surfaces API contract drift before it reaches deployed devices.
**SCSS:** Modules scoped per SFC (`<style scoped lang="scss">`). Global tokens in `frontend/src/styles/global.scss`. No Tailwind, no utility CSS framework, no pre-built component library.
**Build:** Vite outputs to `public/build/`. `vite.config.ts` sets `outDir: '../public/build'`. The Symfony catch-all controller renders `public/build/index.html` directly.
**Forms:** Symfony Form component is not used for authenticated app forms — Vue handles all form logic. Symfony Form is only used in Twig public flows if needed.
### Infrastructure & Deployment
@@ -178,14 +195,15 @@ symfony new pictureframe --webapp
### Decision Impact Analysis
**Implementation Sequence:**
1. DDEV setup + Symfony scaffold (`symfony new pictureframe --webapp`)
2. Add Messenger, Scheduler, Imagick
3. Domain + Nginx config on VPS
4. Core entities (User, Device, Image, RenderedAsset, Token)
5. Image processing worker
6. Device pull endpoint
7. Web application features (library, approval, sharing, admin)
8. Firmware (after domain + API contract confirmed)
1. DDEV setup + Symfony scaffold (`symfony new pictureframe --webapp`); remove Stimulus/Turbo/AssetMapper
2. Vue SPA scaffold in `frontend/` (`npm create vite@latest frontend -- --template vue-ts`); configure `vite.config.ts` to output to `public/build/`
3. Add Messenger, Scheduler, Imagick to Symfony
4. Domain + Nginx config on VPS
5. Core entities (User, Device, Image, RenderedAsset, Token)
6. Image processing worker
7. Symfony JSON API endpoints + SpaController catch-all
8. Vue SPA features (library, upload funnel, sticker editor, approvals, admin)
9. Firmware (after domain + API contract confirmed)
**Cross-Component Dependencies:**
- Firmware cannot be finalized until `pictureframe.edholm.me` is live and API endpoint format is confirmed
@@ -302,13 +320,13 @@ Mandatory rules for all code in this project:
### Requirements to Structure Mapping
**User & Account Management**`src/Controller/SecurityController.php`, `src/Entity/User.php`, `src/Repository/UserRepository.php`, `templates/security/`
**User & Account Management**`src/Controller/SecurityController.php`, `src/Controller/Api/UserApiController.php`, `src/Entity/User.php`, `src/Repository/UserRepository.php`
**Device Management**`src/Controller/Device/`, `src/Entity/Device.php`, `src/Service/DeviceService.php`, `src/Repository/DeviceRepository.php`, `templates/device/`
**Device Management**`src/Controller/Api/DeviceApiController.php`, `src/Entity/Device.php`, `src/Service/DeviceService.php`, `src/Repository/DeviceRepository.php`, `frontend/src/views/HomeView.vue`
**Image Library**`src/Controller/Image/`, `src/Entity/Image.php`, `src/Entity/RenderedAsset.php`, `src/Service/ImageService.php`, `src/Service/ImageProcessingService.php`, `templates/image/`
**Image Library**`src/Controller/Api/ImageApiController.php`, `src/Entity/Image.php`, `src/Entity/RenderedAsset.php`, `src/Service/ImageService.php`, `src/Service/ImageProcessingService.php`, `frontend/src/views/LibraryView.vue`
**Image Approval & Sharing**`src/Controller/Token/`, `src/Entity/Token.php`, `src/Service/TokenService.php`, `templates/token/`
**Image Approval & Sharing**`src/Controller/Token/TokenActionController.php`, `src/Entity/Token.php`, `src/Service/TokenService.php`, `templates/token/` (email approve/decline pages), `frontend/src/components/ApproveCard.vue`
**Device Provisioning**`src/Controller/Device/DeviceProvisionController.php`, `templates/device/provision.html.twig`
@@ -316,7 +334,7 @@ Mandatory rules for all code in this project:
**Display & Status (device pull endpoint)**`src/Controller/Api/DeviceImageController.php`
**Admin & Moderation**`src/Controller/Admin/`, `templates/admin/`
**Admin & Moderation**`src/Controller/Admin/`, `frontend/src/views/AdminView.vue` (if applicable)
**Cross-Cutting: Async Processing**`src/Message/ProcessImageMessage.php`, `src/MessageHandler/ProcessImageMessageHandler.php`, `config/packages/messenger.yaml`
@@ -333,14 +351,55 @@ pictureframe/
│ └── docker-compose.imagick.yaml ← adds Imagick to web container
├── .gitea/
│ └── workflows/
│ └── ci.yml ← Gitea Actions: lint + test on push
├── assets/
│ ├── app.js ← AssetMapper entry, imports Stimulus + Turbo
│ ├── controllers/
│ │ ├── image_upload_controller.js
│ │ ── device_status_controller.js
└── styles/
└── app.css
│ └── ci.yml ← Gitea Actions: lint + test + vite build on push
├── frontend/ ← Vue 3 SPA source (Vite + TypeScript)
│ ├── src/
│ ├── assets/
│ │ │ └── stickers/ ← SVG sticker assets (Seasonal, Holidays, Fun, Family, Nature)
│ │ ── components/
│ │ ├── base/
│ │ │ ├── BaseButton.vue
│ │ │ │ ├── BaseInput.vue
│ │ │ │ ├── BaseBottomSheet.vue
│ │ │ │ ├── BaseCard.vue
│ │ │ │ ├── BaseChip.vue
│ │ │ │ └── BaseToast.vue
│ │ │ ├── FrameCard.vue
│ │ │ ├── CropEditor.vue
│ │ │ ├── StickerCanvas.vue
│ │ │ ├── StickerTray.vue
│ │ │ ├── DevicePicker.vue
│ │ │ ├── PhotoThumb.vue
│ │ │ ├── ShareSheet.vue
│ │ │ ├── ApproveCard.vue
│ │ │ └── BottomNav.vue
│ │ ├── router/
│ │ │ └── index.ts ← Vue Router; catch-all handled by Symfony
│ │ ├── stores/
│ │ │ ├── auth.ts ← current user, login state
│ │ │ ├── devices.ts ← device list, current device
│ │ │ └── upload.ts ← upload funnel state, sticker composition
│ │ ├── styles/
│ │ │ ├── _breakpoints.scss ← $bp-tablet: 640px, $bp-desktop: 960px
│ │ │ ├── _tokens.scss ← CSS custom properties for all 6 themes
│ │ │ └── global.scss ← reset, typography, base layout
│ │ ├── types/
│ │ │ ├── Device.ts
│ │ │ ├── Image.ts
│ │ │ ├── StickerLayer.ts ← { id, type, x, y, scale, rotation }
│ │ │ ├── RenderedAsset.ts
│ │ │ └── Token.ts
│ │ ├── views/
│ │ │ ├── HomeView.vue ← FrameList — FrameCard per device
│ │ │ ├── LibraryView.vue ← photo grid, search, tabs (All/Mine/Shared)
│ │ │ ├── UploadView.vue ← funnel: crop → sticker → device picker
│ │ │ └── SettingsView.vue
│ │ ├── App.vue
│ │ └── main.ts
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.json
│ └── vite.config.ts ← outDir: '../public/build'
├── bin/
│ └── console
├── config/
@@ -352,30 +411,30 @@ pictureframe/
│ │ ├── scheduler.yaml
│ │ ├── security.yaml ← form_login, remember_me, ROLE_SUPER_ADMIN hierarchy
│ │ └── twig.yaml
│ ├── routes.yaml
│ ├── routes.yaml ← catch-all route → SpaController for authenticated paths
│ ├── routes/
│ │ └── api.yaml ← /api/device/{mac}/image route
│ │ └── api.yaml ← /api/device/{mac}/image + /api/* app endpoints
│ └── services.yaml
├── migrations/
├── public/
│ ├── build/ ← gitignored; Vite output (index.html + hashed assets)
│ └── index.php
├── src/
│ ├── Controller/
│ │ ├── Api/
│ │ │ ── DeviceImageController.php ← GET /api/device/{mac}/image → 200/204/404
│ │ │ ── DeviceImageController.php ← GET /api/device/{mac}/image → 200/204/404 (binary)
│ │ │ ├── DeviceApiController.php ← JSON CRUD for devices (Vue SPA)
│ │ │ ├── ImageApiController.php ← JSON CRUD for images, upload, share (Vue SPA)
│ │ │ └── UserApiController.php ← current user, settings (Vue SPA)
│ │ ├── Admin/
│ │ │ ├── AdminDashboardController.php
│ │ │ └── AdminModerationController.php
│ │ ├── SpaController.php ← catch-all; renders public/build/index.html
│ │ ├── SecurityController.php ← login/logout (form POST, JSON response)
│ │ ├── Device/
│ │ │ ── DeviceController.php
│ │ │ └── DeviceProvisionController.php
│ │ ├── Image/
│ │ │ ├── ImageLibraryController.php
│ │ │ ├── ImageUploadController.php
│ │ │ └── ImageShareController.php
│ │ ├── SecurityController.php
│ │ │ ── DeviceProvisionController.php ← GET/POST /setup/{mac} → Twig
│ │ └── Token/
│ │ └── TokenActionController.php ← consume approve/decline/hard-delete tokens
│ │ └── TokenActionController.php ← /token/{uuid}/approve|decline → Twig
│ ├── Entity/
│ │ ├── Device.php
│ │ ├── Image.php
@@ -386,10 +445,6 @@ pictureframe/
│ │ ├── Orientation.php
│ │ ├── RenderStatus.php
│ │ └── TokenType.php
│ ├── Form/
│ │ ├── DeviceType.php
│ │ ├── ImageUploadType.php
│ │ └── RegistrationType.php
│ ├── Message/
│ │ └── ProcessImageMessage.php ← DTO: imageId, deviceModel, orientation
│ ├── MessageHandler/
@@ -413,31 +468,20 @@ pictureframe/
├── storage/
│ └── images/ ← gitignored; STORAGE_PATH points here
├── templates/
│ ├── admin/
│ │ ├── dashboard.html.twig
│ │ └── moderation.html.twig
│ ├── device/
│ │ ── index.html.twig
│ │ ├── provision.html.twig
│ │ └── show.html.twig
│ ├── image/
│ │ ├── library.html.twig
│ │ ├── share.html.twig
│ │ └── upload.html.twig
│ ├── security/
│ │ └── login.html.twig
│ │ ── provision.html.twig ← /setup/{mac} public provisioning page
│ ├── token/
│ │ ├── approve.html.twig
│ │ └── decline.html.twig
│ └── base.html.twig
│ │ ├── approve.html.twig ← email approve page (no login)
│ │ └── decline.html.twig ← email decline page (no login)
│ └── base_public.html.twig ← minimal base for public Twig pages only
├── tests/
│ ├── Functional/
│ │ ├── Api/
│ │ │ ── DeviceImageControllerTest.php
│ │ ├── Device/
│ │ │ └── DeviceControllerTest.php
│ │ └── Image/
│ │ └── ImageLibraryControllerTest.php
│ │ │ ── DeviceImageControllerTest.php
│ │ ├── DeviceApiControllerTest.php
│ │ │ └── ImageApiControllerTest.php
│ │ └── Token/
│ │ └── TokenActionControllerTest.php
│ ├── Integration/
│ │ ├── MessageHandler/
│ │ │ └── ProcessImageMessageHandlerTest.php
@@ -461,7 +505,6 @@ pictureframe/
├── .gitignore
├── composer.json
├── composer.lock
├── importmap.php
├── phpunit.xml.dist
└── symfony.lock
```
@@ -472,7 +515,7 @@ pictureframe/
`GET /api/device/{mac}/image` is the only machine-to-machine surface. MAC address validated against `Device` entity before serving. Returns `application/octet-stream`, 204, or 404. Isolated in `config/routes/api.yaml`.
**Web Application Boundary**
All other routes behind Symfony form-login firewall. Twig responses only. `ROLE_SUPER_ADMIN` gates admin controllers.
Authenticated routes are gated by Symfony form-login firewall. `SpaController` serves `public/build/index.html` as the catch-all — Vue Router handles client-side navigation from there. All authenticated data flows through `/api/*` JSON endpoints. `ROLE_SUPER_ADMIN` gates admin API controllers. Public flows (`/setup/{mac}`, `/token/{uuid}/approve|decline`) are outside the firewall and return Twig responses — no Vue involved.
**Async Processing Boundary**
`ImageService` → Messenger bus → `ProcessImageMessageHandler`. Handler is the only component writing to `storage/images/`. `RenderedAsset.status` is the only signal crossing this boundary.
@@ -513,7 +556,7 @@ User soft-deletes image → Image.deleted_at set (excluded by findActive*)
### Coherence Validation ✅
**Decision Compatibility:** PHP 8.4 + Symfony 8.0 + Doctrine ORM + Messenger + Scheduler + PostgreSQL 16 is a fully supported combination. Imagick is a standalone PHP extension with no framework conflicts. AssetMapper + Stimulus + Turbo has first-party Symfony UX support and requires no build tooling.
**Decision Compatibility:** PHP 8.4 + Symfony 8.0 + Doctrine ORM + Messenger + Scheduler + PostgreSQL 16 is a fully supported combination. Imagick is a standalone PHP extension with no framework conflicts. Vue 3 + Vite + TypeScript strict + SCSS modules + Konva.js is a standard, well-supported SPA stack. The Symfony/Vue boundary is clean: Symfony serves the SPA shell and JSON API; Vue owns all authenticated UI rendering. No build-step conflicts — Vite and Symfony operate independently.
**Pattern Consistency:** `findActive*` naming, `storage/images/` path convention, exclusive Messenger dispatch points, and backed enum usage are consistently applied. One contradiction (`var/images/` in Data Architecture section) found during validation and corrected — all references now point to `storage/images/`.
@@ -0,0 +1,507 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pictureFrame — Design Directions</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', -apple-system, sans-serif; background: #1a1614; color: #f0ece8; padding: 24px 16px 48px; }
h1 { text-align: center; font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.subtitle { text-align: center; color: #8a7e78; font-size: 13px; margin-bottom: 32px; }
.dir-tabs { display: flex; gap: 8px; justify-content: center; margin-bottom: 28px; flex-wrap: wrap; }
.dir-tab { padding: 8px 18px; border-radius: 100px; border: 2px solid #3a3230; background: #2a2220; color: #c0b0a8; cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s; }
.dir-tab:hover { border-color: #c4622a; color: #f0ece8; }
.dir-tab.active { background: #c4622a; border-color: #c4622a; color: white; }
.dir-section { display: none; }
.dir-section.active { display: block; }
.dir-header { text-align: center; margin-bottom: 24px; }
.dir-header h2 { font-size: 20px; font-weight: 800; margin-bottom: 6px; }
.dir-header p { font-size: 14px; color: #a09088; max-width: 480px; margin: 0 auto; line-height: 1.6; }
/* Phone shell */
.phone-row { display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; margin-bottom: 32px; }
.phone-wrap { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.phone-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: #8a7e78; }
.phone { width: 260px; background: #111; border-radius: 36px; padding: 12px; box-shadow: 0 8px 40px rgba(0,0,0,0.6), inset 0 0 0 2px #333; }
.phone-screen { background: #fff8f0; border-radius: 26px; overflow: hidden; min-height: 480px; display: flex; flex-direction: column; }
/* Shared UI atoms */
.status-bar { display: flex; justify-content: space-between; padding: 8px 16px 4px; font-size: 10px; font-weight: 700; color: #8a7060; }
.screen-body { flex: 1; overflow: hidden; }
.bottom-nav { display: flex; justify-content: space-around; padding: 8px 0 10px; border-top: 1px solid #f0e0d4; background: white; }
.nav-item { display: flex; flex-direction: column; align-items: center; gap: 2px; font-size: 9px; font-weight: 700; color: #c0a090; }
.nav-item.on { color: #c4622a; }
.nav-icon { font-size: 18px; }
/* Atoms */
.px { padding: 0 16px; }
.screen-title { font-size: 22px; font-weight: 800; color: #2a1e16; padding: 14px 16px 8px; }
.screen-sub { font-size: 12px; color: #a08070; padding: 0 16px 12px; }
.chip { display: inline-block; padding: 3px 10px; border-radius: 100px; font-size: 10px; font-weight: 700; background: #fde8da; color: #c4622a; margin: 0 2px; }
.chip-g { background: #daeee4; color: #2a5a3e; }
.photo-thumb { border-radius: 10px; background: #e8d8c8; display: flex; align-items: center; justify-content: center; font-size: 22px; }
.card-white { background: white; border-radius: 12px; padding: 12px; box-shadow: 0 1px 6px rgba(196,98,42,0.08); }
.fab { width: 52px; height: 52px; border-radius: 50%; background: #c4622a; display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; box-shadow: 0 4px 16px rgba(196,98,42,0.4); }
.section-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: #c0a090; padding: 0 16px 6px; }
.row { display: flex; align-items: center; }
.divider { height: 1px; background: #f0e0d4; margin: 8px 16px; }
/* Insight boxes */
.insights { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; max-width: 560px; margin: 0 auto; }
@media (max-width: 500px) { .insights { grid-template-columns: 1fr; } }
.insight { background: #2a2220; border-radius: 12px; padding: 14px 16px; border-left: 3px solid #c4622a; }
.insight h4 { font-size: 12px; font-weight: 700; color: #f0ece8; margin-bottom: 4px; }
.insight p { font-size: 12px; color: #907870; line-height: 1.5; }
.insight.pro { border-color: #4aaa70; }
.insight.pro h4 { color: #7adda0; }
.insight.con { border-color: #d06050; }
.insight.con h4 { color: #f09080; }
</style>
</head>
<body>
<h1>pictureFrame — Design Directions</h1>
<p class="subtitle">5 layout approaches · Warm Craft theme · tap to explore</p>
<div class="dir-tabs">
<button class="dir-tab active" onclick="showDir(1)">1 · Photo Grid</button>
<button class="dir-tab" onclick="showDir(2)">2 · Frame-Centric</button>
<button class="dir-tab" onclick="showDir(3)">3 · Upload-First</button>
<button class="dir-tab" onclick="showDir(4)">4 · Activity Feed</button>
<button class="dir-tab" onclick="showDir(5)">5 · Minimal Card</button>
</div>
<!-- ════ DIRECTION 1: Photo Grid ════ -->
<div class="dir-section active" id="dir1">
<div class="dir-header">
<h2>1 · Photo Grid</h2>
<p>Home is a mosaic of your photos. The frame assignment is a secondary action. Feels like a personal gallery that happens to connect to a frame.</p>
</div>
<div class="phone-row">
<div class="phone-wrap">
<div class="phone-label">Home</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div class="screen-title">Your Photos</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;padding:0 12px 12px;">
<div class="photo-thumb" style="height:80px;">🏔️</div>
<div class="photo-thumb" style="height:80px;grid-row:span 2;height:163px;">👨‍👩‍👧</div>
<div class="photo-thumb" style="height:80px;">🌅</div>
<div class="photo-thumb" style="height:80px;">🎄</div>
<div class="photo-thumb" style="height:80px;">🐕</div>
<div class="photo-thumb" style="height:80px;">🎂</div>
<div class="photo-thumb" style="height:80px;">🏖️</div>
<div class="photo-thumb" style="height:80px;">❄️</div>
</div>
<div style="position:absolute;bottom:72px;right:20px;" class="fab" style="position:relative;">+</div>
<div class="bottom-nav">
<div class="nav-item on"><span class="nav-icon">📷</span><span>Photos</span></div>
<div class="nav-item"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">↗️</span><span>Shared</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
<div class="phone-wrap">
<div class="phone-label">Photo Detail</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div class="photo-thumb" style="height:220px;border-radius:0;font-size:64px;">🏔️</div>
<div style="padding:14px 16px 0;">
<div style="font-size:15px;font-weight:700;color:#2a1e16;margin-bottom:4px;">Lake Trip 2024</div>
<div style="font-size:12px;color:#a08070;margin-bottom:12px;">Added 3 days ago · On 2 frames</div>
<div style="display:flex;gap:8px;margin-bottom:14px;">
<span class="chip">Margaret's Frame</span>
<span class="chip">Living Room</span>
</div>
<div style="display:flex;gap:8px;">
<div style="flex:1;background:#c4622a;color:white;border-radius:100px;padding:10px;text-align:center;font-size:12px;font-weight:700;">Edit & Add</div>
<div style="flex:1;background:#f0e8df;color:#c4622a;border-radius:100px;padding:10px;text-align:center;font-size:12px;font-weight:700;">Share</div>
</div>
</div>
<div class="bottom-nav" style="margin-top:auto;">
<div class="nav-item on"><span class="nav-icon">📷</span><span>Photos</span></div>
<div class="nav-item"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">↗️</span><span>Shared</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="insights">
<div class="insight pro"><h4>✓ Familiar</h4><p>Feels like Apple Photos or Google Photos — users know how to navigate it immediately.</p></div>
<div class="insight pro"><h4>✓ Photo-forward</h4><p>The content (photos) is the hero. Great for libraries with many images.</p></div>
<div class="insight con"><h4>✗ Frame is secondary</h4><p>The physical frame — the whole point — is buried under a tab, not the first thing you see.</p></div>
<div class="insight con"><h4>✗ Upload entry unclear</h4><p>Floating "+" button competes with the grid. Upload vs. camera roll confusion likely.</p></div>
</div>
</div>
<!-- ════ DIRECTION 2: Frame-Centric ════ -->
<div class="dir-section" id="dir2">
<div class="dir-header">
<h2>2 · Frame-Centric</h2>
<p>Home shows your frames first. Each frame has a current photo preview. You navigate into a frame to add or manage photos for it. The frame is always the destination.</p>
</div>
<div class="phone-row">
<div class="phone-wrap">
<div class="phone-label">Home</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div class="screen-title">Your Frames</div>
<div style="padding:0 12px 12px;display:flex;flex-direction:column;gap:10px;">
<div class="card-white" style="display:flex;gap:12px;align-items:center;">
<div class="photo-thumb" style="width:64px;height:40px;flex-shrink:0;">👨‍👩‍👧</div>
<div style="flex:1;">
<div style="font-size:13px;font-weight:700;color:#2a1e16;">Margaret's Frame</div>
<div style="font-size:11px;color:#a08070;">14 photos · Updates daily</div>
</div>
<span class="chip-g chip" style="font-size:9px;">Live</span>
</div>
<div class="card-white" style="display:flex;gap:12px;align-items:center;">
<div class="photo-thumb" style="width:64px;height:40px;flex-shrink:0;">🏠</div>
<div style="flex:1;">
<div style="font-size:13px;font-weight:700;color:#2a1e16;">Living Room</div>
<div style="font-size:11px;color:#a08070;">6 photos · Updates weekly</div>
</div>
<span class="chip-g chip" style="font-size:9px;">Live</span>
</div>
<div style="border:2px dashed #e0c8b8;border-radius:12px;padding:14px;text-align:center;">
<div style="font-size:12px;color:#c0a090;font-weight:600;">+ Add a frame</div>
</div>
</div>
<div class="bottom-nav">
<div class="nav-item on"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">↗️</span><span>Shared</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
<div class="phone-wrap">
<div class="phone-label">Frame Detail</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div style="padding:14px 16px 6px;display:flex;align-items:center;gap:10px;">
<div style="font-size:12px;color:#a08070;">← Frames</div>
</div>
<div style="font-size:18px;font-weight:800;color:#2a1e16;padding:0 16px 4px;">Margaret's Frame</div>
<div style="font-size:11px;color:#a08070;padding:0 16px 12px;">14 photos · Updates daily</div>
<div style="display:flex;gap:8px;padding:0 12px 12px;">
<div style="flex:1;background:#c4622a;color:white;border-radius:100px;padding:9px;text-align:center;font-size:12px;font-weight:700;">+ Add Photo</div>
<div style="background:#f0e8df;color:#c4622a;border-radius:100px;padding:9px 14px;font-size:12px;font-weight:700;">Settings</div>
</div>
<div class="section-label">On this frame</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;padding:0 12px;">
<div class="photo-thumb" style="height:72px;">🏔️</div>
<div class="photo-thumb" style="height:72px;">👨‍👩‍👧</div>
<div class="photo-thumb" style="height:72px;">🌅</div>
<div class="photo-thumb" style="height:72px;">🎄</div>
<div class="photo-thumb" style="height:72px;">🐕</div>
<div class="photo-thumb" style="height:72px;">🎂</div>
</div>
<div class="bottom-nav" style="margin-top:auto;">
<div class="nav-item on"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">↗️</span><span>Shared</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="insights">
<div class="insight pro"><h4>✓ Frame is the hero</h4><p>Every interaction starts with "which frame?" — keeps the physical gift front and center.</p></div>
<div class="insight pro"><h4>✓ Clear mental model</h4><p>Navigate into a frame → add photos to it. Mirrors how users think about the product.</p></div>
<div class="insight con"><h4>✗ Library is secondary</h4><p>Your full photo library is a tab away. Users who want to browse all photos have to look for it.</p></div>
<div class="insight con"><h4>✗ Multi-frame add is extra steps</h4><p>Adding one photo to multiple frames requires navigating between frame detail screens.</p></div>
</div>
</div>
<!-- ════ DIRECTION 3: Upload-First ════ -->
<div class="dir-section" id="dir3">
<div class="dir-header">
<h2>3 · Upload-First</h2>
<p>The upload action IS the home screen. A big prominent upload button with recent activity below. The app communicates: the main thing you do here is put photos on frames.</p>
</div>
<div class="phone-row">
<div class="phone-wrap">
<div class="phone-label">Home</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div style="padding:20px 16px 16px;">
<div style="font-size:22px;font-weight:800;color:#2a1e16;margin-bottom:4px;">pictureFrame</div>
<div style="font-size:12px;color:#a08070;">Hey Matt 👋</div>
</div>
<div style="margin:0 12px 16px;background:#c4622a;border-radius:16px;padding:20px;text-align:center;color:white;">
<div style="font-size:32px;margin-bottom:8px;">📷</div>
<div style="font-size:15px;font-weight:800;margin-bottom:4px;">Add a photo</div>
<div style="font-size:11px;opacity:0.8;">Crop, add stickers, put it on a frame</div>
</div>
<div class="section-label">Recent</div>
<div style="display:flex;flex-direction:column;gap:8px;padding:0 12px;">
<div style="display:flex;gap:10px;align-items:center;background:white;border-radius:10px;padding:10px;">
<div class="photo-thumb" style="width:40px;height:40px;flex-shrink:0;font-size:18px;">🏔️</div>
<div style="flex:1;">
<div style="font-size:12px;font-weight:700;color:#2a1e16;">Lake Trip</div>
<div style="font-size:10px;color:#a08070;">Margaret's Frame · 2 days ago</div>
</div>
</div>
<div style="display:flex;gap:10px;align-items:center;background:white;border-radius:10px;padding:10px;">
<div class="photo-thumb" style="width:40px;height:40px;flex-shrink:0;font-size:18px;">🎄</div>
<div style="flex:1;">
<div style="font-size:12px;font-weight:700;color:#2a1e16;">Christmas 2024</div>
<div style="font-size:10px;color:#a08070;">Living Room · 5 days ago</div>
</div>
</div>
</div>
<div class="bottom-nav" style="margin-top:auto;">
<div class="nav-item on"><span class="nav-icon">🏠</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
<div class="phone-wrap">
<div class="phone-label">Upload Flow Step 1</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div style="padding:14px 16px 6px;display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:13px;color:#a08070;">Cancel</div>
<div style="font-size:14px;font-weight:700;color:#2a1e16;">Crop Photo</div>
<div style="font-size:13px;font-weight:700;color:#c4622a;">Next →</div>
</div>
<div style="margin:8px 12px;background:#f0e8df;border-radius:14px;aspect-ratio:5/3;display:flex;align-items:center;justify-content:center;border:3px solid #c4622a;position:relative;">
<div style="font-size:56px;">🏔️</div>
<div style="position:absolute;bottom:8px;right:8px;background:rgba(196,98,42,0.15);border-radius:6px;padding:3px 7px;font-size:9px;font-weight:700;color:#c4622a;">Margaret's Frame</div>
</div>
<div style="padding:12px 16px 0;font-size:11px;color:#a08070;text-align:center;">Pinch and drag to fit the frame shape</div>
<div style="padding:10px 16px 0;display:flex;gap:8px;justify-content:center;">
<div style="background:#f0e8df;color:#c4622a;border-radius:100px;padding:8px 16px;font-size:11px;font-weight:700;">Landscape</div>
<div style="background:#c4622a;color:white;border-radius:100px;padding:8px 16px;font-size:11px;font-weight:700;">Portrait</div>
</div>
<div class="bottom-nav" style="margin-top:auto;">
<div class="nav-item on"><span class="nav-icon">🏠</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="insights">
<div class="insight pro"><h4>✓ Intent is unmistakable</h4><p>First thing you see is "add a photo." The app's purpose is communicated before anything else.</p></div>
<div class="insight pro"><h4>✓ Upload funnel is the flow</h4><p>Tapping the big button goes straight into crop → sticker → add to frame. No detours.</p></div>
<div class="insight con"><h4>✗ Repeat visitors see the same home</h4><p>After the 10th upload, the big prompt feels redundant. No sense of the growing collection.</p></div>
<div class="insight con"><h4>✗ Library/frames are tabs</h4><p>Managing existing content requires navigation rather than being surfaced on home.</p></div>
</div>
</div>
<!-- ════ DIRECTION 4: Activity Feed ════ -->
<div class="dir-section" id="dir4">
<div class="dir-header">
<h2>4 · Activity Feed</h2>
<p>Home is a chronological feed of what's happened — photos added, shares received, approvals needed. Upload lives as a persistent top button. Feels connected and social.</p>
</div>
<div class="phone-row">
<div class="phone-wrap">
<div class="phone-label">Home</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div style="padding:14px 16px 8px;display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:18px;font-weight:800;color:#2a1e16;">Recent</div>
<div style="background:#c4622a;color:white;border-radius:100px;padding:7px 14px;font-size:12px;font-weight:700;">+ Photo</div>
</div>
<div style="display:flex;flex-direction:column;gap:1px;padding:0 12px 12px;">
<div style="background:white;border-radius:12px 12px 4px 4px;padding:12px;margin-bottom:2px;">
<div style="font-size:10px;color:#a08070;margin-bottom:6px;">Sarah shared a photo · 1h ago</div>
<div style="display:flex;gap:10px;align-items:center;">
<div class="photo-thumb" style="width:48px;height:48px;flex-shrink:0;font-size:20px;">🎂</div>
<div style="flex:1;">
<div style="font-size:12px;font-weight:700;color:#2a1e16;">Birthday Party</div>
<div style="font-size:10px;color:#a08070;">Tap to approve for a frame</div>
</div>
<div style="background:#fde8da;color:#c4622a;border-radius:100px;padding:5px 10px;font-size:10px;font-weight:700;">Approve</div>
</div>
</div>
<div style="background:white;border-radius:4px;padding:12px;margin-bottom:2px;">
<div style="font-size:10px;color:#a08070;margin-bottom:6px;">You added a photo · 2 days ago</div>
<div style="display:flex;gap:10px;align-items:center;">
<div class="photo-thumb" style="width:48px;height:48px;flex-shrink:0;font-size:20px;">🏔️</div>
<div style="flex:1;">
<div style="font-size:12px;font-weight:700;color:#2a1e16;">Lake Trip 2024</div>
<div style="font-size:10px;color:#a08070;">On Margaret's Frame</div>
</div>
</div>
</div>
<div style="background:white;border-radius:4px 4px 12px 12px;padding:12px;">
<div style="font-size:10px;color:#a08070;margin-bottom:6px;">Frame updated · 5 days ago</div>
<div style="font-size:12px;font-weight:600;color:#2a1e16;">Margaret's Frame cycled to a new photo</div>
</div>
</div>
<div class="bottom-nav">
<div class="nav-item on"><span class="nav-icon">📋</span><span>Feed</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
<div class="phone-wrap">
<div class="phone-label">Approve Flow</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div style="padding:14px 16px 6px;display:flex;align-items:center;gap:8px;">
<div style="font-size:12px;color:#a08070;">← Feed</div>
</div>
<div style="font-size:16px;font-weight:700;color:#2a1e16;padding:0 16px 4px;">Sarah shared a photo</div>
<div style="font-size:11px;color:#a08070;padding:0 16px 12px;">Add it to one of your frames?</div>
<div class="photo-thumb" style="height:160px;margin:0 12px 14px;font-size:64px;">🎂</div>
<div class="section-label">Choose a frame</div>
<div style="display:flex;flex-direction:column;gap:8px;padding:0 12px 12px;">
<div style="display:flex;gap:10px;align-items:center;background:white;border-radius:10px;padding:10px;border:2px solid #c4622a;">
<div class="photo-thumb" style="width:40px;height:25px;flex-shrink:0;font-size:14px;">👵</div>
<div style="font-size:12px;font-weight:700;color:#2a1e16;">Margaret's Frame</div>
<div style="margin-left:auto;font-size:16px;"></div>
</div>
<div style="display:flex;gap:10px;align-items:center;background:white;border-radius:10px;padding:10px;">
<div class="photo-thumb" style="width:40px;height:25px;flex-shrink:0;font-size:14px;">🏠</div>
<div style="font-size:12px;font-weight:700;color:#2a1e16;">Living Room</div>
</div>
</div>
<div style="padding:0 12px;"><div style="background:#c4622a;color:white;border-radius:100px;padding:12px;text-align:center;font-size:13px;font-weight:700;">Add to Margaret's Frame</div></div>
<div class="bottom-nav" style="margin-top:auto;">
<div class="nav-item on"><span class="nav-icon">📋</span><span>Feed</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="insights">
<div class="insight pro"><h4>✓ Approvals surface naturally</h4><p>Shared photos that need approval appear inline in the feed — no hunting for a notification.</p></div>
<div class="insight pro"><h4>✓ Feels alive</h4><p>The home screen changes as photos are added and frames cycle. Gives the app a sense of activity.</p></div>
<div class="insight con"><h4>✗ Complexity for simple users</h4><p>Margaret opening the app sees a feed with notifications and actions. Might be overwhelming.</p></div>
<div class="insight con"><h4>✗ Upload is a button, not the hero</h4><p>The core action (add a photo) competes with the feed content for attention.</p></div>
</div>
</div>
<!-- ════ DIRECTION 5: Minimal Card ════ -->
<div class="dir-section" id="dir5">
<div class="dir-header">
<h2>5 · Minimal Card</h2>
<p>Home is clean and spacious — one frame card prominently displayed with its current photo, a clear "Add Photo" action, and simple navigation. Maximum focus, minimum noise.</p>
</div>
<div class="phone-row">
<div class="phone-wrap">
<div class="phone-label">Home (1 frame)</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div style="padding:16px 16px 8px;display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:18px;font-weight:800;color:#2a1e16;">pictureFrame</div>
<div style="font-size:20px;">👤</div>
</div>
<div style="margin:0 12px 16px;background:white;border-radius:16px;overflow:hidden;box-shadow:0 2px 12px rgba(196,98,42,0.1);">
<div class="photo-thumb" style="height:140px;border-radius:0;font-size:56px;">👨‍👩‍👧</div>
<div style="padding:14px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
<div style="font-size:15px;font-weight:700;color:#2a1e16;">Margaret's Frame</div>
<span class="chip-g chip" style="font-size:9px;">Live</span>
</div>
<div style="font-size:11px;color:#a08070;margin-bottom:12px;">14 photos · Next cycle in 6 hours</div>
<div style="background:#c4622a;color:white;border-radius:100px;padding:11px;text-align:center;font-size:13px;font-weight:700;">+ Add a Photo</div>
</div>
</div>
<div class="section-label">Library</div>
<div style="display:flex;gap:6px;padding:0 12px;overflow:hidden;">
<div class="photo-thumb" style="width:64px;height:64px;flex-shrink:0;font-size:22px;">🏔️</div>
<div class="photo-thumb" style="width:64px;height:64px;flex-shrink:0;font-size:22px;">🌅</div>
<div class="photo-thumb" style="width:64px;height:64px;flex-shrink:0;font-size:22px;">🎄</div>
<div class="photo-thumb" style="width:64px;height:64px;flex-shrink:0;font-size:22px;">🐕</div>
</div>
<div class="bottom-nav" style="margin-top:auto;">
<div class="nav-item on"><span class="nav-icon">🏠</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
<div class="phone-wrap">
<div class="phone-label">Home (2+ frames)</div>
<div class="phone">
<div class="phone-screen">
<div class="status-bar"><span>9:41</span><span>●●●</span></div>
<div style="padding:16px 16px 8px;font-size:18px;font-weight:800;color:#2a1e16;">pictureFrame</div>
<div style="display:flex;flex-direction:column;gap:10px;padding:0 12px 12px;">
<div style="background:white;border-radius:14px;overflow:hidden;box-shadow:0 2px 8px rgba(196,98,42,0.08);">
<div class="photo-thumb" style="height:80px;border-radius:0;font-size:36px;">👨‍👩‍👧</div>
<div style="padding:10px 12px;display:flex;align-items:center;justify-content:space-between;">
<div>
<div style="font-size:13px;font-weight:700;color:#2a1e16;">Margaret's Frame</div>
<div style="font-size:10px;color:#a08070;">14 photos</div>
</div>
<div style="background:#c4622a;color:white;border-radius:100px;padding:6px 12px;font-size:11px;font-weight:700;">+ Add</div>
</div>
</div>
<div style="background:white;border-radius:14px;overflow:hidden;box-shadow:0 2px 8px rgba(196,98,42,0.08);">
<div class="photo-thumb" style="height:80px;border-radius:0;font-size:36px;">🏠</div>
<div style="padding:10px 12px;display:flex;align-items:center;justify-content:space-between;">
<div>
<div style="font-size:13px;font-weight:700;color:#2a1e16;">Living Room</div>
<div style="font-size:10px;color:#a08070;">6 photos</div>
</div>
<div style="background:#c4622a;color:white;border-radius:100px;padding:6px 12px;font-size:11px;font-weight:700;">+ Add</div>
</div>
</div>
</div>
<div class="bottom-nav" style="margin-top:auto;">
<div class="nav-item on"><span class="nav-icon">🏠</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">🖼️</span><span>Frames</span></div>
<div class="nav-item"><span class="nav-icon">👤</span><span>Me</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="insights">
<div class="insight pro"><h4>✓ Frame + action in one glance</h4><p>You see the frame's current photo AND the "Add Photo" button together. Purpose is immediate.</p></div>
<div class="insight pro"><h4>✓ Scales cleanly</h4><p>One frame: big featured card. Two+ frames: stacked cards, each with their own Add button.</p></div>
<div class="insight pro"><h4>✓ Closest to the mental model</h4><p>The frame is the hero, the photo is the content, the action is obvious. No secondary navigation needed for the primary job.</p></div>
<div class="insight con"><h4>✗ Library is below the fold</h4><p>The photo library strip is a hint, not a feature. Power users with large libraries need to go to the Library tab.</p></div>
</div>
</div>
<script>
function showDir(n) {
document.querySelectorAll('.dir-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.dir-tab').forEach(t => t.classList.remove('active'));
document.getElementById('dir' + n).classList.add('active');
document.querySelectorAll('.dir-tab')[n-1].classList.add('active');
}
</script>
</body>
</html>
@@ -0,0 +1,827 @@
---
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 (640959px) | 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
@@ -0,0 +1,550 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pictureFrame — Theme Explorer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0ece8; padding: 24px 16px; }
h1 { text-align: center; font-size: 22px; color: #2a2420; margin-bottom: 6px; font-weight: 700; }
.subtitle { text-align: center; color: #7a6e68; font-size: 14px; margin-bottom: 32px; }
.theme-tabs { display: flex; gap: 8px; justify-content: center; margin-bottom: 28px; flex-wrap: wrap; }
.tab { padding: 10px 20px; border-radius: 100px; border: 2px solid transparent; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; }
.tab.active { border-color: currentColor; }
.theme-section { display: none; }
.theme-section.active { display: block; }
.palette-row { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
.swatch { flex: 1; min-width: 80px; height: 72px; border-radius: 12px; display: flex; flex-direction: column; justify-content: flex-end; padding: 8px; font-size: 10px; font-weight: 600; }
.preview-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
@media (max-width: 600px) { .preview-grid { grid-template-columns: 1fr; } }
.btn { display: inline-block; padding: 12px 24px; border-radius: 100px; font-size: 15px; font-weight: 700; border: none; cursor: pointer; margin-right: 8px; margin-bottom: 8px; }
.btn-sm { padding: 8px 16px; font-size: 13px; }
.card { border-radius: 14px; padding: 16px; margin-bottom: 12px; }
.card-title { font-size: 15px; font-weight: 700; margin-bottom: 4px; }
.card-sub { font-size: 13px; opacity: 0.65; }
.tag { display: inline-block; padding: 4px 12px; border-radius: 100px; font-size: 12px; font-weight: 600; margin-right: 6px; }
.frame-preview { border-radius: 8px; aspect-ratio: 5/3; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; opacity: 0.7; border: 3px solid; }
.sticker-demo { font-size: 28px; background: white; border-radius: 50%; width: 52px; height: 52px; display: inline-flex; align-items: center; justify-content: center; margin: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
.section-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.45; margin-bottom: 12px; }
.nav-bar { display: flex; justify-content: space-around; padding: 12px 0 4px; border-radius: 0 0 16px 16px; }
.nav-item { display: flex; flex-direction: column; align-items: center; gap: 3px; font-size: 10px; font-weight: 600; opacity: 0.5; cursor: pointer; }
.nav-item.active { opacity: 1; }
.nav-icon { font-size: 20px; }
/* ── THEME 1: Warm Craft ── */
.t1-tab { background: #fff8f0; color: #c4622a; }
.t1-tab.active { background: #c4622a; color: white; }
.t1-text { color: #2a1e16; } .t1-subtext { color: #7a5e4e; }
.t1-btn-primary { background: #c4622a; color: white; }
.t1-btn-secondary { background: #f0e8df; color: #c4622a; }
.t1-btn-accent { background: #f5c842; color: #2a1e16; }
.t1-card { background: white; box-shadow: 0 2px 12px rgba(196,98,42,0.08); }
.t1-tag-primary { background: #fde8da; color: #c4622a; }
.t1-tag-accent { background: #fef8d8; color: #8a6a00; }
.t1-frame-border { border-color: #c4622a; color: #c4622a; background: #fde8da; }
/* ── THEME 2: Playful Pop ── */
.t2-tab { background: #fff0f0; color: #d94f6e; }
.t2-tab.active { background: #d94f6e; color: white; }
.t2-text { color: #26101a; } .t2-subtext { color: #7a4558; }
.t2-btn-primary { background: #d94f6e; color: white; }
.t2-btn-secondary { background: #fce8ed; color: #d94f6e; }
.t2-btn-accent { background: #ffb347; color: #26101a; }
.t2-card { background: white; box-shadow: 0 2px 12px rgba(217,79,110,0.08); }
.t2-tag-primary { background: #fce8ed; color: #d94f6e; }
.t2-tag-accent { background: #fff0d8; color: #a05a00; }
.t2-frame-border { border-color: #d94f6e; color: #d94f6e; background: #fce8ed; }
/* ── THEME 3: Sage & Cream ── */
.t3-tab { background: #f0f7f2; color: #3d7a5a; }
.t3-tab.active { background: #3d7a5a; color: white; }
.t3-text { color: #1a2e24; } .t3-subtext { color: #5a7868; }
.t3-btn-primary { background: #3d7a5a; color: white; }
.t3-btn-secondary { background: #eaf2ed; color: #3d7a5a; }
.t3-btn-accent { background: #e8965a; color: white; }
.t3-card { background: white; box-shadow: 0 2px 12px rgba(61,122,90,0.08); }
.t3-tag-primary { background: #daeee4; color: #2a5a3e; }
.t3-tag-accent { background: #fde8d4; color: #a0521a; }
.t3-frame-border { border-color: #3d7a5a; color: #3d7a5a; background: #daeee4; }
/* ── THEME 4: Dusty Mauve ── */
.t4-tab { background: #f7f0f7; color: #8a4a7a; }
.t4-tab.active { background: #8a4a7a; color: white; }
.t4-text { color: #221220; } .t4-subtext { color: #6e4866; }
.t4-btn-primary { background: #8a4a7a; color: white; }
.t4-btn-secondary { background: #f2e4f0; color: #8a4a7a; }
.t4-btn-accent { background: #d4a843; color: #221220; }
.t4-card { background: white; box-shadow: 0 2px 12px rgba(138,74,122,0.08); }
.t4-tag-primary { background: #f2e4f0; color: #8a4a7a; }
.t4-tag-accent { background: #fef4d8; color: #8a6000; }
.t4-frame-border { border-color: #8a4a7a; color: #8a4a7a; background: #f2e4f0; }
/* ── THEME 5: Ocean Dusk ── */
.t5-tab { background: #eef4f7; color: #2a6878; }
.t5-tab.active { background: #2a6878; color: white; }
.t5-text { color: #0e1f26; } .t5-subtext { color: #4a6e78; }
.t5-btn-primary { background: #2a6878; color: white; }
.t5-btn-secondary { background: #deedf2; color: #2a6878; }
.t5-btn-accent { background: #f0875a; color: white; }
.t5-card { background: white; box-shadow: 0 2px 12px rgba(42,104,120,0.08); }
.t5-tag-primary { background: #deedf2; color: #1a5060; }
.t5-tag-accent { background: #fde8da; color: #a04020; }
.t5-frame-border { border-color: #2a6878; color: #2a6878; background: #deedf2; }
/* ── THEME 6: Honey & Slate ── */
.t6-tab { background: #f7f4ee; color: #5a5068; }
.t6-tab.active { background: #5a5068; color: white; }
.t6-text { color: #1e1a28; } .t6-subtext { color: #6a6278; }
.t6-btn-primary { background: #5a5068; color: white; }
.t6-btn-secondary { background: #eeeaf6; color: #5a5068; }
.t6-btn-accent { background: #e8a830; color: #1e1a28; }
.t6-card { background: white; box-shadow: 0 2px 12px rgba(90,80,104,0.08); }
.t6-tag-primary { background: #eeeaf6; color: #4a4058; }
.t6-tag-accent { background: #fef3d4; color: #8a6000; }
.t6-frame-border { border-color: #5a5068; color: #5a5068; background: #eeeaf6; }
</style>
</head>
<body>
<h1>pictureFrame</h1>
<p class="subtitle">Theme Explorer — click a theme to preview</p>
<div style="position:fixed;top:16px;right:16px;display:flex;align-items:center;gap:8px;background:white;padding:8px 14px;border-radius:100px;box-shadow:0 2px 12px rgba(0,0,0,0.12);z-index:100;">
<label for="fav-select" style="font-size:12px;font-weight:600;color:#8a7a70;white-space:nowrap;">❤️ Fav:</label>
<select id="fav-select" onchange="saveFavorite(this.value)" style="padding:2px 6px;border:none;font-size:12px;font-weight:600;color:#2a2420;background:transparent;cursor:pointer;outline:none;max-width:130px;">
<option value="">— pick —</option>
<option value="1">🪵 Warm Craft</option>
<option value="2">🎉 Playful Pop</option>
<option value="3">🌿 Sage & Cream</option>
<option value="4">🌸 Dusty Mauve</option>
<option value="5">🌊 Ocean Dusk</option>
<option value="6">🍯 Honey & Slate</option>
</select>
<span id="fav-label" style="font-size:11px;color:#3a9a5a;display:none;font-weight:700;"></span>
</div>
<div class="theme-tabs">
<button class="tab t1-tab active" id="tab1" onclick="showTheme(1)">🪵 Warm Craft</button>
<button class="tab t2-tab" id="tab2" onclick="showTheme(2)">🎉 Playful Pop</button>
<button class="tab t3-tab" id="tab3" onclick="showTheme(3)">🌿 Sage & Cream</button>
<button class="tab t4-tab" id="tab4" onclick="showTheme(4)">🌸 Dusty Mauve</button>
<button class="tab t5-tab" id="tab5" onclick="showTheme(5)">🌊 Ocean Dusk</button>
<button class="tab t6-tab" id="tab6" onclick="showTheme(6)">🍯 Honey & Slate</button>
</div>
<!-- ════ THEME 1: Warm Craft ════ -->
<div class="theme-section active" id="theme1">
<div style="background:#fff8f0;border-radius:20px;padding:24px;">
<p class="section-label t1-text">Color Palette</p>
<div class="palette-row">
<div class="swatch" style="background:#c4622a;color:white"><span>Primary</span><span>#c4622a</span></div>
<div class="swatch" style="background:#e8956a;color:white"><span>Secondary</span><span>#e8956a</span></div>
<div class="swatch" style="background:#f5c842;color:#2a1e16"><span>Accent</span><span>#f5c842</span></div>
<div class="swatch" style="background:#f0e8df;color:#7a5e4e"><span>Surface</span><span>#f0e8df</span></div>
<div class="swatch" style="background:#2a1e16;color:#f0e8df"><span>Text</span><span>#2a1e16</span></div>
</div>
<p class="section-label t1-text">Concept</p>
<div class="t1-card card" style="margin-bottom:16px;"><p class="t1-text" style="font-size:14px;line-height:1.6;">Inspired by the walnut frame itself. Warm amber and terracotta feel handmade and intentional — like something that belongs on a shelf, not a screen. The golden accent echoes the e-ink's yellow. Feels like a gift, not an app.</p></div>
<p class="section-label t1-text">Buttons</p>
<div style="margin-bottom:20px;">
<button class="btn t1-btn-primary">Add to Frame</button>
<button class="btn t1-btn-secondary">Share Photo</button>
<button class="btn t1-btn-accent">Approve</button>
<button class="btn btn-sm t1-btn-secondary">Decline</button>
</div>
<div class="preview-grid">
<div>
<p class="section-label t1-text">Library Card</p>
<div class="t1-card card">
<div style="background:#f0e8df;border-radius:10px;height:100px;margin-bottom:12px;display:flex;align-items:center;justify-content:center;font-size:32px;">🏔️</div>
<div class="card-title t1-text">Lake Trip 2024</div>
<div class="card-sub t1-subtext">Added 3 days ago</div>
<div style="margin-top:10px;"><span class="tag t1-tag-primary">Margaret's Frame</span></div>
</div>
</div>
<div>
<p class="section-label t1-text">Device Card</p>
<div class="t1-card card">
<div class="frame-preview t1-frame-border">Margaret's Frame</div>
<div class="card-title t1-text" style="margin-top:12px;">Margaret's Frame</div>
<div class="card-sub t1-subtext">12 photos · Rotates daily</div>
<div style="margin-top:10px;"><span class="tag t1-tag-accent">Online</span></div>
</div>
</div>
</div>
<p class="section-label t1-text">Sticker Editor</p>
<div class="t1-card card" style="text-align:center;padding:24px;">
<div style="background:#f0e8df;border-radius:14px;padding:20px;margin-bottom:16px;position:relative;">
<div style="font-size:48px;margin-bottom:8px;">🏔️</div>
<div style="font-size:11px;opacity:0.5;">Photo canvas</div>
<div class="sticker-demo" style="position:absolute;top:8px;right:8px;">🎅</div>
</div>
<div><span class="sticker-demo">🎅</span><span class="sticker-demo"></span><span class="sticker-demo">🎉</span><span class="sticker-demo">❤️</span><span class="sticker-demo">🌟</span></div>
</div>
<p class="section-label t1-text" style="margin-top:20px;">Navigation</p>
<div class="t1-card card" style="padding:0;">
<div class="nav-bar t1-text">
<div class="nav-item active"><span class="nav-icon">🖼️</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📷</span><span>Upload</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">⚙️</span><span>Frames</span></div>
</div>
</div>
</div>
</div>
<!-- ════ THEME 2: Playful Pop ════ -->
<div class="theme-section" id="theme2">
<div style="background:#fef6f8;border-radius:20px;padding:24px;">
<p class="section-label t2-text">Color Palette</p>
<div class="palette-row">
<div class="swatch" style="background:#d94f6e;color:white"><span>Primary</span><span>#d94f6e</span></div>
<div class="swatch" style="background:#f47b93;color:white"><span>Secondary</span><span>#f47b93</span></div>
<div class="swatch" style="background:#ffb347;color:#26101a"><span>Accent</span><span>#ffb347</span></div>
<div class="swatch" style="background:#fce8ed;color:#7a4558"><span>Surface</span><span>#fce8ed</span></div>
<div class="swatch" style="background:#26101a;color:#fef6f8"><span>Text</span><span>#26101a</span></div>
</div>
<p class="section-label t2-text">Concept</p>
<div class="t2-card card" style="margin-bottom:16px;"><p class="t2-text" style="font-size:14px;line-height:1.6;">Bold, warm coral with a sunny amber accent. Feels energetic and fun — the kind of app you're happy to open. The sticker editor will feel at home here. Expressive and youthful while staying warm and personal rather than cold and techy.</p></div>
<p class="section-label t2-text">Buttons</p>
<div style="margin-bottom:20px;">
<button class="btn t2-btn-primary">Add to Frame</button>
<button class="btn t2-btn-secondary">Share Photo</button>
<button class="btn t2-btn-accent">Approve</button>
<button class="btn btn-sm t2-btn-secondary">Decline</button>
</div>
<div class="preview-grid">
<div>
<p class="section-label t2-text">Library Card</p>
<div class="t2-card card">
<div style="background:#fce8ed;border-radius:10px;height:100px;margin-bottom:12px;display:flex;align-items:center;justify-content:center;font-size:32px;">🏔️</div>
<div class="card-title t2-text">Lake Trip 2024</div>
<div class="card-sub t2-subtext">Added 3 days ago</div>
<div style="margin-top:10px;"><span class="tag t2-tag-primary">Margaret's Frame</span></div>
</div>
</div>
<div>
<p class="section-label t2-text">Device Card</p>
<div class="t2-card card">
<div class="frame-preview t2-frame-border">Margaret's Frame</div>
<div class="card-title t2-text" style="margin-top:12px;">Margaret's Frame</div>
<div class="card-sub t2-subtext">12 photos · Rotates daily</div>
<div style="margin-top:10px;"><span class="tag t2-tag-accent">Online</span></div>
</div>
</div>
</div>
<p class="section-label t2-text">Sticker Editor</p>
<div class="t2-card card" style="text-align:center;padding:24px;">
<div style="background:#fce8ed;border-radius:14px;padding:20px;margin-bottom:16px;position:relative;">
<div style="font-size:48px;margin-bottom:8px;">🏔️</div>
<div style="font-size:11px;opacity:0.5;">Photo canvas</div>
<div class="sticker-demo" style="position:absolute;top:8px;right:8px;">🎅</div>
</div>
<div><span class="sticker-demo">🎅</span><span class="sticker-demo"></span><span class="sticker-demo">🎉</span><span class="sticker-demo">❤️</span><span class="sticker-demo">🌟</span></div>
</div>
<p class="section-label t2-text" style="margin-top:20px;">Navigation</p>
<div class="t2-card card" style="padding:0;">
<div class="nav-bar t2-text">
<div class="nav-item active"><span class="nav-icon">🖼️</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📷</span><span>Upload</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">⚙️</span><span>Frames</span></div>
</div>
</div>
</div>
</div>
<!-- ════ THEME 3: Sage & Cream ════ -->
<div class="theme-section" id="theme3">
<div style="background:#f7f5f0;border-radius:20px;padding:24px;">
<p class="section-label t3-text">Color Palette</p>
<div class="palette-row">
<div class="swatch" style="background:#3d7a5a;color:white"><span>Primary</span><span>#3d7a5a</span></div>
<div class="swatch" style="background:#6aab87;color:white"><span>Secondary</span><span>#6aab87</span></div>
<div class="swatch" style="background:#e8965a;color:white"><span>Accent</span><span>#e8965a</span></div>
<div class="swatch" style="background:#eaf2ed;color:#5a7868"><span>Surface</span><span>#eaf2ed</span></div>
<div class="swatch" style="background:#1a2e24;color:#f7f5f0"><span>Text</span><span>#1a2e24</span></div>
</div>
<p class="section-label t3-text">Concept</p>
<div class="t3-card card" style="margin-bottom:16px;"><p class="t3-text" style="font-size:14px;line-height:1.6;">Sage green with a warm terracotta accent — calm and natural, like something grown rather than designed. Welcoming and trustworthy without being corporate. The most approachable for Margaret; the terracotta keeps it from feeling clinical.</p></div>
<p class="section-label t3-text">Buttons</p>
<div style="margin-bottom:20px;">
<button class="btn t3-btn-primary">Add to Frame</button>
<button class="btn t3-btn-secondary">Share Photo</button>
<button class="btn t3-btn-accent">Approve</button>
<button class="btn btn-sm t3-btn-secondary">Decline</button>
</div>
<div class="preview-grid">
<div>
<p class="section-label t3-text">Library Card</p>
<div class="t3-card card">
<div style="background:#eaf2ed;border-radius:10px;height:100px;margin-bottom:12px;display:flex;align-items:center;justify-content:center;font-size:32px;">🏔️</div>
<div class="card-title t3-text">Lake Trip 2024</div>
<div class="card-sub t3-subtext">Added 3 days ago</div>
<div style="margin-top:10px;"><span class="tag t3-tag-primary">Margaret's Frame</span></div>
</div>
</div>
<div>
<p class="section-label t3-text">Device Card</p>
<div class="t3-card card">
<div class="frame-preview t3-frame-border">Margaret's Frame</div>
<div class="card-title t3-text" style="margin-top:12px;">Margaret's Frame</div>
<div class="card-sub t3-subtext">12 photos · Rotates daily</div>
<div style="margin-top:10px;"><span class="tag t3-tag-accent">Online</span></div>
</div>
</div>
</div>
<p class="section-label t3-text">Sticker Editor</p>
<div class="t3-card card" style="text-align:center;padding:24px;">
<div style="background:#eaf2ed;border-radius:14px;padding:20px;margin-bottom:16px;position:relative;">
<div style="font-size:48px;margin-bottom:8px;">🏔️</div>
<div style="font-size:11px;opacity:0.5;">Photo canvas</div>
<div class="sticker-demo" style="position:absolute;top:8px;right:8px;">🎅</div>
</div>
<div><span class="sticker-demo">🎅</span><span class="sticker-demo"></span><span class="sticker-demo">🎉</span><span class="sticker-demo">❤️</span><span class="sticker-demo">🌟</span></div>
</div>
<p class="section-label t3-text" style="margin-top:20px;">Navigation</p>
<div class="t3-card card" style="padding:0;">
<div class="nav-bar t3-text">
<div class="nav-item active"><span class="nav-icon">🖼️</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📷</span><span>Upload</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">⚙️</span><span>Frames</span></div>
</div>
</div>
</div>
</div>
<!-- ════ THEME 4: Dusty Mauve ════ -->
<div class="theme-section" id="theme4">
<div style="background:#f9f2f8;border-radius:20px;padding:24px;">
<p class="section-label t4-text">Color Palette</p>
<div class="palette-row">
<div class="swatch" style="background:#8a4a7a;color:white"><span>Primary</span><span>#8a4a7a</span></div>
<div class="swatch" style="background:#b87aaa;color:white"><span>Secondary</span><span>#b87aaa</span></div>
<div class="swatch" style="background:#d4a843;color:#221220"><span>Accent</span><span>#d4a843</span></div>
<div class="swatch" style="background:#f2e4f0;color:#6e4866"><span>Surface</span><span>#f2e4f0</span></div>
<div class="swatch" style="background:#221220;color:#f9f2f8"><span>Text</span><span>#221220</span></div>
</div>
<p class="section-label t4-text">Concept</p>
<div class="t4-card card" style="margin-bottom:16px;"><p class="t4-text" style="font-size:14px;line-height:1.6;">Muted dusty rose and plum with a warm gold accent. Whimsical and personal without being loud — the kind of palette that feels handpicked rather than designed. Pairs beautifully with family photos and gives the sticker editor a slightly magical quality.</p></div>
<p class="section-label t4-text">Buttons</p>
<div style="margin-bottom:20px;">
<button class="btn t4-btn-primary">Add to Frame</button>
<button class="btn t4-btn-secondary">Share Photo</button>
<button class="btn t4-btn-accent">Approve</button>
<button class="btn btn-sm t4-btn-secondary">Decline</button>
</div>
<div class="preview-grid">
<div>
<p class="section-label t4-text">Library Card</p>
<div class="t4-card card">
<div style="background:#f2e4f0;border-radius:10px;height:100px;margin-bottom:12px;display:flex;align-items:center;justify-content:center;font-size:32px;">🏔️</div>
<div class="card-title t4-text">Lake Trip 2024</div>
<div class="card-sub t4-subtext">Added 3 days ago</div>
<div style="margin-top:10px;"><span class="tag t4-tag-primary">Margaret's Frame</span></div>
</div>
</div>
<div>
<p class="section-label t4-text">Device Card</p>
<div class="t4-card card">
<div class="frame-preview t4-frame-border">Margaret's Frame</div>
<div class="card-title t4-text" style="margin-top:12px;">Margaret's Frame</div>
<div class="card-sub t4-subtext">12 photos · Rotates daily</div>
<div style="margin-top:10px;"><span class="tag t4-tag-accent">Online</span></div>
</div>
</div>
</div>
<p class="section-label t4-text">Sticker Editor</p>
<div class="t4-card card" style="text-align:center;padding:24px;">
<div style="background:#f2e4f0;border-radius:14px;padding:20px;margin-bottom:16px;position:relative;">
<div style="font-size:48px;margin-bottom:8px;">🏔️</div>
<div style="font-size:11px;opacity:0.5;">Photo canvas</div>
<div class="sticker-demo" style="position:absolute;top:8px;right:8px;">🎅</div>
</div>
<div><span class="sticker-demo">🎅</span><span class="sticker-demo"></span><span class="sticker-demo">🎉</span><span class="sticker-demo">❤️</span><span class="sticker-demo">🌟</span></div>
</div>
<p class="section-label t4-text" style="margin-top:20px;">Navigation</p>
<div class="t4-card card" style="padding:0;">
<div class="nav-bar t4-text">
<div class="nav-item active"><span class="nav-icon">🖼️</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📷</span><span>Upload</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">⚙️</span><span>Frames</span></div>
</div>
</div>
</div>
</div>
<!-- ════ THEME 5: Ocean Dusk ════ -->
<div class="theme-section" id="theme5">
<div style="background:#eef5f8;border-radius:20px;padding:24px;">
<p class="section-label t5-text">Color Palette</p>
<div class="palette-row">
<div class="swatch" style="background:#2a6878;color:white"><span>Primary</span><span>#2a6878</span></div>
<div class="swatch" style="background:#5498aa;color:white"><span>Secondary</span><span>#5498aa</span></div>
<div class="swatch" style="background:#f0875a;color:white"><span>Accent</span><span>#f0875a</span></div>
<div class="swatch" style="background:#deedf2;color:#4a6e78"><span>Surface</span><span>#deedf2</span></div>
<div class="swatch" style="background:#0e1f26;color:#eef5f8"><span>Text</span><span>#0e1f26</span></div>
</div>
<p class="section-label t5-text">Concept</p>
<div class="t5-card card" style="margin-bottom:16px;"><p class="t5-text" style="font-size:14px;line-height:1.6;">Deep teal with a warm peach-coral accent — calm and considered, like late afternoon light on water. Feels premium and intentional. The warmth of the accent stops it from feeling cold. A more refined option that still carries personality and pairs beautifully with photography.</p></div>
<p class="section-label t5-text">Buttons</p>
<div style="margin-bottom:20px;">
<button class="btn t5-btn-primary">Add to Frame</button>
<button class="btn t5-btn-secondary">Share Photo</button>
<button class="btn t5-btn-accent">Approve</button>
<button class="btn btn-sm t5-btn-secondary">Decline</button>
</div>
<div class="preview-grid">
<div>
<p class="section-label t5-text">Library Card</p>
<div class="t5-card card">
<div style="background:#deedf2;border-radius:10px;height:100px;margin-bottom:12px;display:flex;align-items:center;justify-content:center;font-size:32px;">🏔️</div>
<div class="card-title t5-text">Lake Trip 2024</div>
<div class="card-sub t5-subtext">Added 3 days ago</div>
<div style="margin-top:10px;"><span class="tag t5-tag-primary">Margaret's Frame</span></div>
</div>
</div>
<div>
<p class="section-label t5-text">Device Card</p>
<div class="t5-card card">
<div class="frame-preview t5-frame-border">Margaret's Frame</div>
<div class="card-title t5-text" style="margin-top:12px;">Margaret's Frame</div>
<div class="card-sub t5-subtext">12 photos · Rotates daily</div>
<div style="margin-top:10px;"><span class="tag t5-tag-accent">Online</span></div>
</div>
</div>
</div>
<p class="section-label t5-text">Sticker Editor</p>
<div class="t5-card card" style="text-align:center;padding:24px;">
<div style="background:#deedf2;border-radius:14px;padding:20px;margin-bottom:16px;position:relative;">
<div style="font-size:48px;margin-bottom:8px;">🏔️</div>
<div style="font-size:11px;opacity:0.5;">Photo canvas</div>
<div class="sticker-demo" style="position:absolute;top:8px;right:8px;">🎅</div>
</div>
<div><span class="sticker-demo">🎅</span><span class="sticker-demo"></span><span class="sticker-demo">🎉</span><span class="sticker-demo">❤️</span><span class="sticker-demo">🌟</span></div>
</div>
<p class="section-label t5-text" style="margin-top:20px;">Navigation</p>
<div class="t5-card card" style="padding:0;">
<div class="nav-bar t5-text">
<div class="nav-item active"><span class="nav-icon">🖼️</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📷</span><span>Upload</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">⚙️</span><span>Frames</span></div>
</div>
</div>
</div>
</div>
<!-- ════ THEME 6: Honey & Slate ════ -->
<div class="theme-section" id="theme6">
<div style="background:#f5f3f8;border-radius:20px;padding:24px;">
<p class="section-label t6-text">Color Palette</p>
<div class="palette-row">
<div class="swatch" style="background:#5a5068;color:white"><span>Primary</span><span>#5a5068</span></div>
<div class="swatch" style="background:#8a80a0;color:white"><span>Secondary</span><span>#8a80a0</span></div>
<div class="swatch" style="background:#e8a830;color:#1e1a28"><span>Accent</span><span>#e8a830</span></div>
<div class="swatch" style="background:#eeeaf6;color:#6a6278"><span>Surface</span><span>#eeeaf6</span></div>
<div class="swatch" style="background:#1e1a28;color:#f5f3f8"><span>Text</span><span>#1e1a28</span></div>
</div>
<p class="section-label t6-text">Concept</p>
<div class="t6-card card" style="margin-bottom:16px;"><p class="t6-text" style="font-size:14px;line-height:1.6;">Cool lavender-slate with a rich honey gold accent. The most sophisticated of the set — the slate reads as calm and considered while the honey accent brings warmth and personality. Photos look stunning against this palette. Feels like a frame that belongs in a thoughtfully decorated home.</p></div>
<p class="section-label t6-text">Buttons</p>
<div style="margin-bottom:20px;">
<button class="btn t6-btn-primary">Add to Frame</button>
<button class="btn t6-btn-secondary">Share Photo</button>
<button class="btn t6-btn-accent">Approve</button>
<button class="btn btn-sm t6-btn-secondary">Decline</button>
</div>
<div class="preview-grid">
<div>
<p class="section-label t6-text">Library Card</p>
<div class="t6-card card">
<div style="background:#eeeaf6;border-radius:10px;height:100px;margin-bottom:12px;display:flex;align-items:center;justify-content:center;font-size:32px;">🏔️</div>
<div class="card-title t6-text">Lake Trip 2024</div>
<div class="card-sub t6-subtext">Added 3 days ago</div>
<div style="margin-top:10px;"><span class="tag t6-tag-primary">Margaret's Frame</span></div>
</div>
</div>
<div>
<p class="section-label t6-text">Device Card</p>
<div class="t6-card card">
<div class="frame-preview t6-frame-border">Margaret's Frame</div>
<div class="card-title t6-text" style="margin-top:12px;">Margaret's Frame</div>
<div class="card-sub t6-subtext">12 photos · Rotates daily</div>
<div style="margin-top:10px;"><span class="tag t6-tag-accent">Online</span></div>
</div>
</div>
</div>
<p class="section-label t6-text">Sticker Editor</p>
<div class="t6-card card" style="text-align:center;padding:24px;">
<div style="background:#eeeaf6;border-radius:14px;padding:20px;margin-bottom:16px;position:relative;">
<div style="font-size:48px;margin-bottom:8px;">🏔️</div>
<div style="font-size:11px;opacity:0.5;">Photo canvas</div>
<div class="sticker-demo" style="position:absolute;top:8px;right:8px;">🎅</div>
</div>
<div><span class="sticker-demo">🎅</span><span class="sticker-demo"></span><span class="sticker-demo">🎉</span><span class="sticker-demo">❤️</span><span class="sticker-demo">🌟</span></div>
</div>
<p class="section-label t6-text" style="margin-top:20px;">Navigation</p>
<div class="t6-card card" style="padding:0;">
<div class="nav-bar t6-text">
<div class="nav-item active"><span class="nav-icon">🖼️</span><span>Home</span></div>
<div class="nav-item"><span class="nav-icon">📷</span><span>Upload</span></div>
<div class="nav-item"><span class="nav-icon">📚</span><span>Library</span></div>
<div class="nav-item"><span class="nav-icon">⚙️</span><span>Frames</span></div>
</div>
</div>
</div>
</div>
<script>
const names = ['', '🪵 Warm Craft', '🎉 Playful Pop', '🌿 Sage & Cream', '🌸 Dusty Mauve', '🌊 Ocean Dusk', '🍯 Honey & Slate'];
function showTheme(n) {
document.querySelectorAll('.theme-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.getElementById('theme' + n).classList.add('active');
document.getElementById('tab' + n).classList.add('active');
}
function saveFavorite(val) {
if (!val) return;
localStorage.setItem('pf-fav-theme', val);
showTheme(parseInt(val));
const label = document.getElementById('fav-label');
label.style.display = 'inline';
updateFavStars();
setTimeout(() => { label.style.display = 'none'; }, 2000);
}
function updateFavStars() {
const fav = localStorage.getItem('pf-fav-theme');
for (let i = 1; i <= 6; i++) {
const tab = document.getElementById('tab' + i);
const base = names[i];
tab.textContent = (fav == i) ? base + ' ❤️' : base;
}
}
window.onload = function() {
const fav = localStorage.getItem('pf-fav-theme');
if (fav) {
document.getElementById('fav-select').value = fav;
updateFavStars();
}
};
</script>
</body>
</html>