docs(mockups): static HTML wireframes for Library scaling concepts
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
Renders the five design concepts from _bmad-output/planning-artifacts/library-many-frames-design-ideas.md as standalone HTML so Matt can compare them in a browser without spinning up the SPA. Mockups live at /mockups/library/ and reuse the project's design tokens for visual consistency: index.html landing page with concept links current.html the chip-explosion state we're shipping today concept-a.html photo + status badge → DevicePicker sheet (recommended) concept-b.html device-first tab concept-c.html multi-select bulk action bar concept-d.html device chip filter + photo dots Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
# Library page — scaling to many frames
|
||||
|
||||
**Date:** 2026-05-14
|
||||
**Facilitated by:** BMAD design team
|
||||
**Active personas:** 🎨 Sally (UX), 🎨 Maya (Design Thinking), 📋 John (PM), 🏗️ Winston (Architect)
|
||||
**Brief from {{user_name}}:** Library doesn't scale beyond 1-2 frames. Need design ideas.
|
||||
|
||||
---
|
||||
|
||||
## 1. The problem, stated by the user reading the screen
|
||||
|
||||
🎨 **Sally:** *"Picture Aunt Carol. She's gifted PictureFrame units to her two grown kids and three grandkids — five frames in the wild. She opens the Library page, ready to approve last weekend's birthday photos. Each photo card stacks five 'Living Room', 'Kitchen', 'Bedroom', 'Mom's Place', 'Cabin' chips PLUS five lock chips. With twenty photos on screen that's two hundred buttons. She squints, taps the wrong one, undoes it. The page feels broken — and Aunt Carol is the literal target user."*
|
||||
|
||||
The current per-photo layout (`<button v-for="device in devicesStore.devices">` rendered **twice** — approval row + lock row) has O(photos × devices) chips on the page. Two of the most important user-facing screens (Library "My Photos" tab) breaks down at N=4+ frames.
|
||||
|
||||
---
|
||||
|
||||
## 2. Reframing — what job is the user really hiring this page for?
|
||||
|
||||
📋 **John:** *Cuts to the chase.* "Three jobs we keep conflating in the same view:
|
||||
|
||||
| # | Job | Frequency | Trigger |
|
||||
|---|-----|-----------|---------|
|
||||
| J1 | 'Send THIS new photo to ALL my family frames' | high | just uploaded |
|
||||
| J2 | 'Send THIS photo to ONE specific frame' | medium | birthday card for Mom |
|
||||
| J3 | 'What's on Grandma's frame right now? Lock it.' | medium | curating one frame |
|
||||
| J4 | 'Remove this old photo from my frames' | low | cleanup |
|
||||
|
||||
J1 is the bulk action — currently requires N taps. J3 is the per-frame view — currently requires scanning the chip column. The Library only directly serves J2 well, and only at small N."
|
||||
|
||||
🎨 **Maya:** *Sketching in the air.* "We're solving a **many-to-many curation** problem with a **per-cell grid of toggles**. Toggle grids are the worst answer at scale — visual noise scales with N×M. We should split the matrix: either device-first, photo-first-with-summary, or operation-first (bulk)."
|
||||
|
||||
---
|
||||
|
||||
## 3. Five design concepts
|
||||
|
||||
### 🅐 Photo card → photo-only + status badge (recommended baseline)
|
||||
|
||||
🎨 **Sally:** "Strip the card to the photo, plus a single line of status: `✓ on 3 of 5 frames · 1 lock`. Tap the photo opens a bottom sheet with the full grid of approvals + locks. The DevicePicker bottom sheet we already use during upload IS this exact UI — reuse it."
|
||||
|
||||
```
|
||||
┌───────────────┐
|
||||
│ │
|
||||
│ [ photo ] │
|
||||
│ │
|
||||
└───────────────┘
|
||||
✓ 3/5 frames · 🔒 Mom's
|
||||
[edit] [delete]
|
||||
```
|
||||
|
||||
- **Pros:** O(photos) widgets, not O(photos × devices). Reuses existing DevicePicker. Grid scales arbitrarily.
|
||||
- **Cons:** One more tap for the "approve to one specific frame" job (J2). One-frame and two-frame users see a regression.
|
||||
- **Effort:** Small. Mostly CSS + reusing DevicePicker. ~1 day.
|
||||
|
||||
### 🅑 Device-first column ("Per frame" tab)
|
||||
|
||||
📋 **John:** "Add a top-level toggle to the Library: `My Photos` / `By Frame`. The `By Frame` view is a horizontal swipe-able stack — one card per frame, each card shows its approved photos and the locked photo highlighted. Lets users curate per-frame without scanning a chip column."
|
||||
|
||||
```
|
||||
┌── Living Room (5 approved · 🔒 Birthday) ──┐
|
||||
│ [photo][photo][photo][photo][photo] [+] │
|
||||
└────────────────────────────────────────────┘
|
||||
┌── Kitchen (12 approved · 🔒 none) ─────┐
|
||||
│ [photo][photo][photo][photo][photo] [+] │
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Pros:** Native answer to J3 ("what's on Grandma's frame?"). Each card is bounded — no matrix blow-up.
|
||||
- **Cons:** Doesn't help J1 (one photo → all frames) without leaving this view. Extra navigation cost.
|
||||
- **Effort:** Medium. New route/component + API filter param. ~2-3 days.
|
||||
|
||||
### 🅒 Multi-select + bulk action bar
|
||||
|
||||
🎨 **Maya:** *Jazz hands.* "Tap-and-hold a photo to enter selection mode. Top bar morphs into 'N selected — Send to… / Lock to… / Delete'. Tap-and-pick more photos. One trip to DevicePicker covers any batch. J1 in two gestures."
|
||||
|
||||
```
|
||||
[grid of photos with selection bubbles in corners]
|
||||
─────────────────────────────────────────
|
||||
3 selected [Send to →] [Cancel]
|
||||
```
|
||||
|
||||
- **Pros:** Solves J1 efficiently. Familiar pattern (iOS Photos, Google Photos).
|
||||
- **Cons:** Discoverability — first-time users won't know to long-press. Probably worth a hint on first run.
|
||||
- **Effort:** Medium. Selection-state in store + bulk API endpoint. ~2 days.
|
||||
|
||||
### 🅓 Device chip filter + photo dots
|
||||
|
||||
🎨 **Sally:** "Top of the Library page: a horizontal scrolling row of device chips. Tap one → grid filters to photos approved on that frame. Each photo's bottom shows tiny initial-dots for the OTHER frames it's on. Tap a photo to manage."
|
||||
|
||||
```
|
||||
[All] [Living Room ✓] [Kitchen] [Bedroom] [Mom's] [Cabin]
|
||||
─────────────────────────────────────────
|
||||
[photo · ●LR ●KT] [photo · ●LR]
|
||||
[photo · ●LR ●KT] [photo · ●LR ●MP]
|
||||
```
|
||||
|
||||
- **Pros:** Helpful for J3 by filter. Compact status without buttons. Works at any N.
|
||||
- **Cons:** Filter is implicit — user might not realise. Initial-dots crowd at high N.
|
||||
- **Effort:** Medium. Filter is purely client-side; dots are styling. ~1.5 days.
|
||||
|
||||
### 🅔 Device groups ("Family", "Kids", "All")
|
||||
|
||||
📋 **John:** *Skeptical.* "If users actually have 8 frames, they probably cluster: 'all kids' rooms', 'parents' places'. Let them define one-tap groups. Photo approval/lock takes group as a unit."
|
||||
|
||||
```
|
||||
Groups: [All Frames] [Kids' Rooms] [Parents] [+ Group]
|
||||
```
|
||||
|
||||
- **Pros:** Aligns to the user's mental model when they have many frames.
|
||||
- **Cons:** New entity to model (Group → Device many-to-many). New CRUD. Premature without evidence of cluster usage.
|
||||
- **Effort:** Large. Schema change + UI for group management. ~4-5 days.
|
||||
|
||||
---
|
||||
|
||||
## 4. Architect's gut-check
|
||||
|
||||
🏗️ **Winston:** *Pragmatic.* "From the data side:
|
||||
- **🅐 (photo-only card + sheet)** — zero schema changes, zero API changes. Pure frontend refactor. Ships fastest. Lowest risk to existing 7.3" beta units.
|
||||
- **🅑 (device-first tab)** — needs a `GET /api/devices/{id}/photos` endpoint. Trivial to add.
|
||||
- **🅒 (multi-select bulk)** — needs `PATCH /api/images/bulk-approve` with `[imageIds]` × `[deviceIds]`. Small server lift.
|
||||
- **🅓ⓔ** — purely frontend.
|
||||
|
||||
If we ship one thing, **🅐 is the foundation everything else builds on** — once the per-photo card is compact, layering bulk-select (🅒) or filtering (🅓) on top is additive, not a rewrite.
|
||||
|
||||
I would NOT do 🅔 yet — premature abstraction. Wait for actual users with 8+ frames asking for it."
|
||||
|
||||
---
|
||||
|
||||
## 5. Where the team lands
|
||||
|
||||
🎨 **Sally + 📋 John + 🏗️ Winston, in unison:**
|
||||
|
||||
**Ship 🅐 first.** It's the smallest change that unblocks scale and **costs nothing** for the existing 1-2-frame users (their card just looks cleaner). The DevicePicker sheet is already built — point the new "manage" tap at it.
|
||||
|
||||
**Then 🅒 (multi-select)** as a fast-follow for J1 efficiency. Tap-hold to enter selection, top action bar, batch send. Doesn't need a server endpoint immediately — can client-loop the existing per-image PATCH.
|
||||
|
||||
**Defer 🅑 (device-first tab)** until a user explicitly asks "how do I see what's on Grandma's frame?". The compact photo card + DevicePicker handles J3 well enough at N≤5.
|
||||
|
||||
**Defer 🅓 (chip filter) and 🅔 (groups)** until usage data says they're worth it.
|
||||
|
||||
---
|
||||
|
||||
## 6. Open questions for {{user_name}}
|
||||
|
||||
🎨 **Sally:** "Before we wireframe — three quick ones:
|
||||
|
||||
1. **Status badge wording.** `✓ on 3 of 5 frames` or `3 frames`? Subtle but it sets tone (`✓ on` reads as confirmation; bare `3 frames` reads as info).
|
||||
2. **Lock indicator placement.** Inside the badge line (`✓ 3/5 · 🔒 Mom's`) or as a corner overlay on the photo (lock icon top-right)?
|
||||
3. **Tap target.** Does tapping the *photo* open manage, or do we want a dedicated `Manage ▸` chip below it? Photo-tap is friendlier on mobile; dedicated chip is more discoverable."
|
||||
|
||||
🎨 **Maya:** "And one for after we wireframe — what's the maximum frame count we should *actually* design for? If it's 'family of five = five frames', design priorities differ from 'church group of forty'."
|
||||
|
||||
---
|
||||
|
||||
## 7. Next step
|
||||
|
||||
If you greenlight 🅐: I'll draft the wireframe + interaction spec, then implement. Estimated 1 working session for the refactor + DevicePicker rewiring.
|
||||
|
||||
If you want to riff on the concepts above first, just say the word — we can pull in Carson for a wild-ideas round, or Victor (Innovation Strategist) for a disruptive frame.
|
||||
@@ -0,0 +1,196 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Concept 🅐 — Photo + badge</title>
|
||||
<link rel="stylesheet" href="shared.css">
|
||||
<style>
|
||||
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-3); padding: var(--space-4); }
|
||||
.item { display: flex; flex-direction: column; gap: var(--space-1); }
|
||||
.item__photo { position: relative; }
|
||||
.item__photo .corner-lock {
|
||||
position: absolute; top: var(--space-2); right: var(--space-2);
|
||||
background: var(--color-lock); color: white; border-radius: var(--radius-full);
|
||||
width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 14px; box-shadow: 0 1px 2px rgba(0,0,0,0.25);
|
||||
}
|
||||
.item__status {
|
||||
display: flex; align-items: center; gap: var(--space-2);
|
||||
padding: var(--space-1) var(--space-1);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.item__status b { color: var(--color-text); font-weight: 600; }
|
||||
.item__status .lock { color: #6b5210; }
|
||||
.item__row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 2px var(--space-1);
|
||||
}
|
||||
.item__manage {
|
||||
background: transparent; border: 0;
|
||||
color: var(--color-primary); font-size: var(--text-xs); font-weight: 600;
|
||||
}
|
||||
|
||||
/* Sheet mock — pinned bottom panel showing the DevicePicker reuse */
|
||||
.sheet {
|
||||
position: fixed; bottom: 0; left: 50%; transform: translateX(-50%);
|
||||
width: 100%; max-width: 420px;
|
||||
background: var(--color-surface);
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
box-shadow: 0 -8px 24px rgba(58,46,34,0.18);
|
||||
padding: var(--space-4);
|
||||
transform: translate(-50%, 0);
|
||||
transition: transform var(--duration-fast);
|
||||
}
|
||||
.sheet__handle {
|
||||
width: 36px; height: 4px; background: var(--color-border);
|
||||
border-radius: 2px; margin: 0 auto var(--space-4);
|
||||
}
|
||||
.sheet__title {
|
||||
font-weight: 700; margin: 0 0 var(--space-3); font-size: var(--text-md);
|
||||
}
|
||||
.sheet__row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: var(--space-3) 0; border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.sheet__row:last-child { border-bottom: 0; }
|
||||
.sheet__device { font-weight: 600; }
|
||||
.sheet__device small { color: var(--color-text-muted); font-weight: 400; }
|
||||
.toggle {
|
||||
width: 44px; height: 26px; background: var(--color-border);
|
||||
border-radius: var(--radius-full); position: relative; transition: background 0.15s;
|
||||
border: 0;
|
||||
}
|
||||
.toggle::after {
|
||||
content: ''; position: absolute; top: 3px; left: 3px;
|
||||
width: 20px; height: 20px; border-radius: 50%; background: white;
|
||||
transition: left 0.15s;
|
||||
}
|
||||
.toggle--on { background: var(--color-primary); }
|
||||
.toggle--on::after { left: 21px; }
|
||||
.lock-toggle {
|
||||
background: transparent; border: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted); padding: 4px 10px;
|
||||
border-radius: var(--radius-full); font-size: var(--text-xs);
|
||||
}
|
||||
.lock-toggle--on { background: var(--color-lock); color: white; border-color: var(--color-lock); }
|
||||
.sheet__done {
|
||||
background: var(--color-primary); color: var(--color-primary-fg);
|
||||
width: 100%; border: 0; padding: 12px;
|
||||
border-radius: var(--radius-full); font-weight: 700;
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<header class="topbar">
|
||||
<h1>Library</h1>
|
||||
<div class="topbar__tabs">
|
||||
<button class="topbar__tab topbar__tab--active">My Photos</button>
|
||||
<button class="topbar__tab">Shared</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="note">
|
||||
<b>🅐</b> Each photo card is just the image + a one-line status. The lock
|
||||
is a corner badge on the photo (lock-icon). Tap any photo to open the
|
||||
bottom sheet with device approvals + per-device lock — same UI we already
|
||||
use for upload.
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p1"></div>
|
||||
<div class="corner-lock" title="Locked on Living Room">🔒</div>
|
||||
</div>
|
||||
<div class="item__row">
|
||||
<span class="item__status"><b>3</b>/5 frames</span>
|
||||
<button class="item__manage">Manage ▸</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p2"></div>
|
||||
</div>
|
||||
<div class="item__row">
|
||||
<span class="item__status"><b>2</b>/5 frames</span>
|
||||
<button class="item__manage">Manage ▸</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p3"></div>
|
||||
</div>
|
||||
<div class="item__row">
|
||||
<span class="item__status"><b>5</b>/5 frames</span>
|
||||
<button class="item__manage">Manage ▸</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p4"></div>
|
||||
<div class="corner-lock" title="Locked on Mom's Place">🔒</div>
|
||||
</div>
|
||||
<div class="item__row">
|
||||
<span class="item__status"><b>2</b>/5 frames</span>
|
||||
<button class="item__manage">Manage ▸</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p5"></div>
|
||||
</div>
|
||||
<div class="item__row">
|
||||
<span class="item__status"><b>2</b>/5 frames</span>
|
||||
<button class="item__manage">Manage ▸</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p6"></div>
|
||||
</div>
|
||||
<div class="item__row">
|
||||
<span class="item__status"><b>3</b>/5 frames</span>
|
||||
<button class="item__manage">Manage ▸</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sheet mock (always visible to demo reuse) -->
|
||||
<div class="sheet">
|
||||
<div class="sheet__handle"></div>
|
||||
<div class="sheet__title">Manage frames for this photo</div>
|
||||
<div class="sheet__row">
|
||||
<div class="sheet__device">Living Room<br><small>Online · last sync 2m ago</small></div>
|
||||
<button class="lock-toggle lock-toggle--on">🔒 Locked</button>
|
||||
<button class="toggle toggle--on" aria-label="Approved"></button>
|
||||
</div>
|
||||
<div class="sheet__row">
|
||||
<div class="sheet__device">Kitchen<br><small>Online · last sync 5m ago</small></div>
|
||||
<button class="lock-toggle">🔓 Rotate</button>
|
||||
<button class="toggle toggle--on" aria-label="Approved"></button>
|
||||
</div>
|
||||
<div class="sheet__row">
|
||||
<div class="sheet__device">Bedroom<br><small>Online · last sync 1m ago</small></div>
|
||||
<span></span>
|
||||
<button class="toggle" aria-label="Not approved"></button>
|
||||
</div>
|
||||
<div class="sheet__row">
|
||||
<div class="sheet__device">Mom's Place<br><small>Online · last sync 3m ago</small></div>
|
||||
<button class="lock-toggle">🔓 Rotate</button>
|
||||
<button class="toggle toggle--on" aria-label="Approved"></button>
|
||||
</div>
|
||||
<div class="sheet__row">
|
||||
<div class="sheet__device">Cabin<br><small>Offline · last seen 2h ago</small></div>
|
||||
<span></span>
|
||||
<button class="toggle" aria-label="Not approved"></button>
|
||||
</div>
|
||||
<button class="sheet__done">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Concept 🅑 — Device-first</title>
|
||||
<link rel="stylesheet" href="shared.css">
|
||||
<style>
|
||||
.scope-tabs {
|
||||
display: flex; gap: var(--space-2); padding: 0 var(--space-4) var(--space-3);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.scope-tabs button {
|
||||
background: transparent; border: 0;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-weight: 600; font-size: var(--text-sm);
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.scope-tabs button.active {
|
||||
color: var(--color-text); border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.frame-row {
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.frame-row__header {
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.frame-row__name { font-weight: 700; font-size: var(--text-md); }
|
||||
.frame-row__meta { font-size: var(--text-xs); color: var(--color-text-muted); }
|
||||
.frame-row__meta .lock-meta { color: #6b5210; font-weight: 600; }
|
||||
|
||||
.frame-row__strip {
|
||||
display: grid; grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.strip-photo { position: relative; }
|
||||
.strip-photo .locked-flag {
|
||||
position: absolute; top: 4px; right: 4px;
|
||||
background: var(--color-lock); color: white; border-radius: var(--radius-full);
|
||||
width: 20px; height: 20px; font-size: 11px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.strip-photo--add {
|
||||
border: 2px dashed var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
aspect-ratio: 1/1;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 24px;
|
||||
}
|
||||
.frame-row__count {
|
||||
margin-top: var(--space-2);
|
||||
font-size: var(--text-xs); color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<header class="topbar">
|
||||
<h1>Library</h1>
|
||||
<div class="topbar__tabs">
|
||||
<button class="topbar__tab topbar__tab--active">My Photos</button>
|
||||
<button class="topbar__tab">Shared</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="scope-tabs">
|
||||
<button>By Photo</button>
|
||||
<button class="active">By Frame</button>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<b>🅑</b> Group photos by frame, not the other way around. Each row is a
|
||||
frame — name, status, locked photo, scroll of approved photos, and a
|
||||
big <b>+</b> button to add more. Answers "what's on Grandma's frame?"
|
||||
in one glance.
|
||||
</div>
|
||||
|
||||
<div class="frame-row">
|
||||
<div class="frame-row__header">
|
||||
<div>
|
||||
<div class="frame-row__name">Living Room</div>
|
||||
<div class="frame-row__meta">8 photos · <span class="lock-meta">🔒 Birthday cake</span></div>
|
||||
</div>
|
||||
<button class="chip chip--device">Settings</button>
|
||||
</div>
|
||||
<div class="frame-row__strip">
|
||||
<div class="strip-photo"><div class="photo photo--p1"></div><div class="locked-flag">🔒</div></div>
|
||||
<div class="strip-photo"><div class="photo photo--p2"></div></div>
|
||||
<div class="strip-photo"><div class="photo photo--p3"></div></div>
|
||||
<div class="strip-photo strip-photo--add">+</div>
|
||||
</div>
|
||||
<div class="frame-row__count">+ 5 more</div>
|
||||
</div>
|
||||
|
||||
<div class="frame-row">
|
||||
<div class="frame-row__header">
|
||||
<div>
|
||||
<div class="frame-row__name">Kitchen</div>
|
||||
<div class="frame-row__meta">12 photos · rotating</div>
|
||||
</div>
|
||||
<button class="chip chip--device">Settings</button>
|
||||
</div>
|
||||
<div class="frame-row__strip">
|
||||
<div class="strip-photo"><div class="photo photo--p4"></div></div>
|
||||
<div class="strip-photo"><div class="photo photo--p5"></div></div>
|
||||
<div class="strip-photo"><div class="photo photo--p6"></div></div>
|
||||
<div class="strip-photo strip-photo--add">+</div>
|
||||
</div>
|
||||
<div class="frame-row__count">+ 9 more</div>
|
||||
</div>
|
||||
|
||||
<div class="frame-row">
|
||||
<div class="frame-row__header">
|
||||
<div>
|
||||
<div class="frame-row__name">Bedroom</div>
|
||||
<div class="frame-row__meta">3 photos · rotating</div>
|
||||
</div>
|
||||
<button class="chip chip--device">Settings</button>
|
||||
</div>
|
||||
<div class="frame-row__strip">
|
||||
<div class="strip-photo"><div class="photo photo--p7"></div></div>
|
||||
<div class="strip-photo"><div class="photo photo--p8"></div></div>
|
||||
<div class="strip-photo"><div class="photo photo--p2"></div></div>
|
||||
<div class="strip-photo strip-photo--add">+</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="frame-row">
|
||||
<div class="frame-row__header">
|
||||
<div>
|
||||
<div class="frame-row__name">Mom's Place</div>
|
||||
<div class="frame-row__meta">6 photos · <span class="lock-meta">🔒 Family dinner</span></div>
|
||||
</div>
|
||||
<button class="chip chip--device">Settings</button>
|
||||
</div>
|
||||
<div class="frame-row__strip">
|
||||
<div class="strip-photo"><div class="photo photo--p4"></div><div class="locked-flag">🔒</div></div>
|
||||
<div class="strip-photo"><div class="photo photo--p1"></div></div>
|
||||
<div class="strip-photo"><div class="photo photo--p9"></div></div>
|
||||
<div class="strip-photo strip-photo--add">+</div>
|
||||
</div>
|
||||
<div class="frame-row__count">+ 3 more</div>
|
||||
</div>
|
||||
|
||||
<div class="frame-row">
|
||||
<div class="frame-row__header">
|
||||
<div>
|
||||
<div class="frame-row__name">Cabin</div>
|
||||
<div class="frame-row__meta">2 photos · offline 2h</div>
|
||||
</div>
|
||||
<button class="chip chip--device">Settings</button>
|
||||
</div>
|
||||
<div class="frame-row__strip">
|
||||
<div class="strip-photo"><div class="photo photo--p5"></div></div>
|
||||
<div class="strip-photo"><div class="photo photo--p10"></div></div>
|
||||
<div class="strip-photo strip-photo--add">+</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="bottomnav">
|
||||
<div class="bottomnav__item">🏠<span>Home</span></div>
|
||||
<div class="bottomnav__item bottomnav__item--active">📷<span>Library</span></div>
|
||||
<div class="bottomnav__item">⚙️<span>Settings</span></div>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Concept 🅒 — Multi-select bulk</title>
|
||||
<link rel="stylesheet" href="shared.css">
|
||||
<style>
|
||||
.selection-bar {
|
||||
position: sticky; top: 0; z-index: 6;
|
||||
background: var(--color-primary); color: var(--color-primary-fg);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.selection-bar__count { font-weight: 700; }
|
||||
.selection-bar__actions { display: flex; gap: var(--space-2); }
|
||||
.selection-bar__btn {
|
||||
background: rgba(255,255,255,0.15); color: white; border: 0;
|
||||
padding: 6px 12px; border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm); font-weight: 600;
|
||||
}
|
||||
.selection-bar__btn--primary { background: white; color: var(--color-primary); }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-3); padding: var(--space-4); }
|
||||
.item { position: relative; }
|
||||
.item__photo { position: relative; }
|
||||
.item__check {
|
||||
position: absolute; top: var(--space-2); left: var(--space-2);
|
||||
width: 26px; height: 26px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.85);
|
||||
border: 2px solid white;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; color: var(--color-primary);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.25);
|
||||
}
|
||||
.item--selected .item__photo { outline: 3px solid var(--color-primary); outline-offset: -3px; border-radius: var(--radius-sm); }
|
||||
.item--selected .item__check { background: var(--color-primary); color: white; border-color: white; }
|
||||
.item__status {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
padding: var(--space-1) var(--space-1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<div class="selection-bar">
|
||||
<span class="selection-bar__count">3 selected</span>
|
||||
<div class="selection-bar__actions">
|
||||
<button class="selection-bar__btn selection-bar__btn--primary">Send to ▸</button>
|
||||
<button class="selection-bar__btn">🔒</button>
|
||||
<button class="selection-bar__btn">🗑</button>
|
||||
<button class="selection-bar__btn">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<b>🅒</b> Long-press any photo to enter selection mode (this mockup is
|
||||
pre-entered). Top bar morphs to action bar. Tick multiple photos, hit
|
||||
"Send to ▸" — opens the DevicePicker once for the batch. J1 ("send to
|
||||
all my frames") becomes two gestures instead of N × 5.
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item item--selected">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p1"></div>
|
||||
<div class="item__check">✓</div>
|
||||
</div>
|
||||
<div class="item__status">3/5 frames</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p2"></div>
|
||||
<div class="item__check"></div>
|
||||
</div>
|
||||
<div class="item__status">2/5 frames</div>
|
||||
</div>
|
||||
<div class="item item--selected">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p3"></div>
|
||||
<div class="item__check">✓</div>
|
||||
</div>
|
||||
<div class="item__status">5/5 frames</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p4"></div>
|
||||
<div class="item__check"></div>
|
||||
</div>
|
||||
<div class="item__status">2/5 frames</div>
|
||||
</div>
|
||||
<div class="item item--selected">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p5"></div>
|
||||
<div class="item__check">✓</div>
|
||||
</div>
|
||||
<div class="item__status">2/5 frames</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p6"></div>
|
||||
<div class="item__check"></div>
|
||||
</div>
|
||||
<div class="item__status">3/5 frames</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p7"></div>
|
||||
<div class="item__check"></div>
|
||||
</div>
|
||||
<div class="item__status">1/5 frames</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="item__photo">
|
||||
<div class="photo photo--p8"></div>
|
||||
<div class="item__check"></div>
|
||||
</div>
|
||||
<div class="item__status">4/5 frames</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="bottomnav">
|
||||
<div class="bottomnav__item">🏠<span>Home</span></div>
|
||||
<div class="bottomnav__item bottomnav__item--active">📷<span>Library</span></div>
|
||||
<div class="bottomnav__item">⚙️<span>Settings</span></div>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Concept 🅓 — Chip filter + dots</title>
|
||||
<link rel="stylesheet" href="shared.css">
|
||||
<style>
|
||||
.filter-row {
|
||||
overflow-x: auto;
|
||||
display: flex; gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
.filter-row::-webkit-scrollbar { display: none; }
|
||||
.filter-row button { white-space: nowrap; }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-3); padding: var(--space-4); }
|
||||
.item__row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: var(--space-1);
|
||||
}
|
||||
.item__dots {
|
||||
display: flex; gap: 4px;
|
||||
}
|
||||
.dot {
|
||||
width: 18px; height: 18px; border-radius: 50%;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 9px; font-weight: 700;
|
||||
background: var(--color-surface-2); color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
.dot--on { background: var(--color-primary); color: white; border-color: var(--color-primary); }
|
||||
.dot--lock { background: var(--color-lock); color: white; border-color: var(--color-lock); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<header class="topbar">
|
||||
<h1>Library</h1>
|
||||
<div class="topbar__tabs">
|
||||
<button class="topbar__tab topbar__tab--active">My Photos</button>
|
||||
<button class="topbar__tab">Shared</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="filter-row">
|
||||
<button class="chip chip--on">All</button>
|
||||
<button class="chip">Living Room</button>
|
||||
<button class="chip">Kitchen</button>
|
||||
<button class="chip">Bedroom</button>
|
||||
<button class="chip">Mom's Place</button>
|
||||
<button class="chip">Cabin</button>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<b>🅓</b> A horizontally-scrolling chip bar at the top filters the grid
|
||||
by frame. Each photo shows little colour dots representing the frames it's
|
||||
on (initials, lock-flag for the locked frame). Tap a photo to manage in
|
||||
a sheet. Works at any N because the dots can scroll-truncate.
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item">
|
||||
<div class="photo photo--p1"></div>
|
||||
<div class="item__row">
|
||||
<div class="item__dots">
|
||||
<span class="dot dot--on dot--lock" title="Locked on Living Room">LR</span>
|
||||
<span class="dot dot--on">KT</span>
|
||||
<span class="dot dot--on">MP</span>
|
||||
<span class="dot">BR</span>
|
||||
<span class="dot">CA</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p2"></div>
|
||||
<div class="item__row">
|
||||
<div class="item__dots">
|
||||
<span class="dot dot--on dot--lock">LR</span>
|
||||
<span class="dot dot--on">BR</span>
|
||||
<span class="dot">KT</span>
|
||||
<span class="dot">MP</span>
|
||||
<span class="dot">CA</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p3"></div>
|
||||
<div class="item__row">
|
||||
<div class="item__dots">
|
||||
<span class="dot dot--on">LR</span>
|
||||
<span class="dot dot--on">KT</span>
|
||||
<span class="dot dot--on">BR</span>
|
||||
<span class="dot dot--on">MP</span>
|
||||
<span class="dot dot--on">CA</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p4"></div>
|
||||
<div class="item__row">
|
||||
<div class="item__dots">
|
||||
<span class="dot dot--on dot--lock">MP</span>
|
||||
<span class="dot dot--on">KT</span>
|
||||
<span class="dot">LR</span>
|
||||
<span class="dot">BR</span>
|
||||
<span class="dot">CA</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p5"></div>
|
||||
<div class="item__row">
|
||||
<div class="item__dots">
|
||||
<span class="dot dot--on">LR</span>
|
||||
<span class="dot dot--on">CA</span>
|
||||
<span class="dot">KT</span>
|
||||
<span class="dot">BR</span>
|
||||
<span class="dot">MP</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p6"></div>
|
||||
<div class="item__row">
|
||||
<div class="item__dots">
|
||||
<span class="dot dot--on">LR</span>
|
||||
<span class="dot dot--on">KT</span>
|
||||
<span class="dot dot--on">BR</span>
|
||||
<span class="dot">MP</span>
|
||||
<span class="dot">CA</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="bottomnav">
|
||||
<div class="bottomnav__item">🏠<span>Home</span></div>
|
||||
<div class="bottomnav__item bottomnav__item--active">📷<span>Library</span></div>
|
||||
<div class="bottomnav__item">⚙️<span>Settings</span></div>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,129 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Library — current state</title>
|
||||
<link rel="stylesheet" href="shared.css">
|
||||
<style>
|
||||
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-3); padding: var(--space-4); }
|
||||
.item { display: flex; flex-direction: column; gap: var(--space-2); }
|
||||
.approvals, .locks { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<header class="topbar">
|
||||
<h1>Library</h1>
|
||||
<div class="topbar__tabs">
|
||||
<button class="topbar__tab topbar__tab--active">My Photos</button>
|
||||
<button class="topbar__tab">Shared</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="note">
|
||||
<b>Pain point:</b> each photo card stacks two rows of 5 chips
|
||||
(approve · lock). At 10 photos × 5 frames that's 100 buttons on screen.
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- one item × 10 -->
|
||||
<div class="item">
|
||||
<div class="photo photo--p1"></div>
|
||||
<div class="approvals">
|
||||
<button class="chip chip--device chip--on">Living Room</button>
|
||||
<button class="chip chip--device chip--on">Kitchen</button>
|
||||
<button class="chip chip--device">Bedroom</button>
|
||||
<button class="chip chip--device chip--on">Mom's Place</button>
|
||||
<button class="chip chip--device">Cabin</button>
|
||||
</div>
|
||||
<div class="locks">
|
||||
<button class="chip chip--lock chip--device">🔒 Living Room</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Kitchen</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Mom's Place</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p2"></div>
|
||||
<div class="approvals">
|
||||
<button class="chip chip--device chip--on">Living Room</button>
|
||||
<button class="chip chip--device">Kitchen</button>
|
||||
<button class="chip chip--device chip--on">Bedroom</button>
|
||||
<button class="chip chip--device">Mom's Place</button>
|
||||
<button class="chip chip--device">Cabin</button>
|
||||
</div>
|
||||
<div class="locks">
|
||||
<button class="chip chip--lock chip--device chip--lock-on">🔒 Living Room</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Bedroom</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p3"></div>
|
||||
<div class="approvals">
|
||||
<button class="chip chip--device chip--on">Living Room</button>
|
||||
<button class="chip chip--device chip--on">Kitchen</button>
|
||||
<button class="chip chip--device chip--on">Bedroom</button>
|
||||
<button class="chip chip--device chip--on">Mom's Place</button>
|
||||
<button class="chip chip--device chip--on">Cabin</button>
|
||||
</div>
|
||||
<div class="locks">
|
||||
<button class="chip chip--lock chip--device">🔒 Living Room</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Kitchen</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Bedroom</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Mom's Place</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Cabin</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p4"></div>
|
||||
<div class="approvals">
|
||||
<button class="chip chip--device">Living Room</button>
|
||||
<button class="chip chip--device chip--on">Kitchen</button>
|
||||
<button class="chip chip--device">Bedroom</button>
|
||||
<button class="chip chip--device chip--on">Mom's Place</button>
|
||||
<button class="chip chip--device">Cabin</button>
|
||||
</div>
|
||||
<div class="locks">
|
||||
<button class="chip chip--lock chip--device">🔒 Kitchen</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Mom's Place</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p5"></div>
|
||||
<div class="approvals">
|
||||
<button class="chip chip--device chip--on">Living Room</button>
|
||||
<button class="chip chip--device">Kitchen</button>
|
||||
<button class="chip chip--device">Bedroom</button>
|
||||
<button class="chip chip--device">Mom's Place</button>
|
||||
<button class="chip chip--device chip--on">Cabin</button>
|
||||
</div>
|
||||
<div class="locks">
|
||||
<button class="chip chip--lock chip--device">🔒 Living Room</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Cabin</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="photo photo--p6"></div>
|
||||
<div class="approvals">
|
||||
<button class="chip chip--device chip--on">Living Room</button>
|
||||
<button class="chip chip--device chip--on">Kitchen</button>
|
||||
<button class="chip chip--device chip--on">Bedroom</button>
|
||||
<button class="chip chip--device">Mom's Place</button>
|
||||
<button class="chip chip--device">Cabin</button>
|
||||
</div>
|
||||
<div class="locks">
|
||||
<button class="chip chip--lock chip--device">🔒 Living Room</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Kitchen</button>
|
||||
<button class="chip chip--lock chip--device">🔒 Bedroom</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="bottomnav">
|
||||
<div class="bottomnav__item">🏠<span>Home</span></div>
|
||||
<div class="bottomnav__item bottomnav__item--active">📷<span>Library</span></div>
|
||||
<div class="bottomnav__item">⚙️<span>Settings</span></div>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Library — design concepts</title>
|
||||
<link rel="stylesheet" href="shared.css">
|
||||
<style>
|
||||
body { padding: var(--space-6); }
|
||||
.wrap { max-width: 720px; margin: 0 auto; }
|
||||
h1 { font-size: var(--text-2xl); margin: 0 0 var(--space-2); }
|
||||
p { color: var(--color-text-muted); line-height: 1.5; }
|
||||
ul.concepts { list-style: none; padding: 0; margin: var(--space-6) 0 0; display: grid; gap: var(--space-3); }
|
||||
ul.concepts a {
|
||||
display: block; padding: var(--space-4); border-radius: var(--radius-md);
|
||||
background: var(--color-surface); border: 1px solid var(--color-border);
|
||||
text-decoration: none; color: inherit;
|
||||
}
|
||||
ul.concepts a:hover { border-color: var(--color-primary); }
|
||||
ul.concepts h2 { margin: 0 0 var(--space-1); font-size: var(--text-lg); }
|
||||
ul.concepts small { color: var(--color-text-muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Library page — scaling to many frames</h1>
|
||||
<p>Static HTML wireframes for the five concepts in the BMAD design memo. Each
|
||||
uses the same design tokens as the real Vue app, fake content set at five
|
||||
devices and ten photos so chip-count pain is visible.</p>
|
||||
|
||||
<ul class="concepts">
|
||||
<li><a href="current.html">
|
||||
<h2>🚫 Current state (for reference)</h2>
|
||||
<small>What's shipping today. Five chip-buttons per photo × twice
|
||||
(approve + lock) at N=5 frames.</small>
|
||||
</a></li>
|
||||
<li><a href="concept-a.html">
|
||||
<h2>🅐 Photo card + status badge → DevicePicker sheet (recommended)</h2>
|
||||
<small>Strip the card to image + one-line summary. Tap photo opens the
|
||||
existing DevicePicker bottom sheet.</small>
|
||||
</a></li>
|
||||
<li><a href="concept-b.html">
|
||||
<h2>🅑 Device-first tab</h2>
|
||||
<small><code>My Photos</code> / <code>By Frame</code> toggle. By-Frame
|
||||
groups photos by device row.</small>
|
||||
</a></li>
|
||||
<li><a href="concept-c.html">
|
||||
<h2>🅒 Multi-select + bulk action bar</h2>
|
||||
<small>Long-press a photo to enter selection mode. Top bar morphs to
|
||||
Send-to/Lock/Delete.</small>
|
||||
</a></li>
|
||||
<li><a href="concept-d.html">
|
||||
<h2>🅓 Device chip filter + photo dots</h2>
|
||||
<small>Top row of device chips filters the grid. Each photo shows tiny
|
||||
dots for the other frames it's on.</small>
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,109 @@
|
||||
/* Design tokens cloned from frontend/src/styles/global.scss so mockups
|
||||
feel native without the full Vue app shell. */
|
||||
:root {
|
||||
--text-xs: 11px; --text-sm: 13px; --text-base: 15px; --text-md: 17px;
|
||||
--text-lg: 20px; --text-xl: 24px; --text-2xl: 28px;
|
||||
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
|
||||
--space-5: 20px; --space-6: 24px; --space-8: 32px;
|
||||
--radius-sm: 6px; --radius-md: 12px; --radius-lg: 20px; --radius-full: 9999px;
|
||||
--color-bg: #fdf6ee; --color-surface: #fff9f2; --color-surface-2: #f5ead8;
|
||||
--color-border: #e8d9c4; --color-text: #3a2e22; --color-text-muted: #8a7060;
|
||||
--color-primary: #c97c3a; --color-primary-fg: #ffffff;
|
||||
--color-secondary: #e8d9c4; --color-destructive: #c0392b;
|
||||
--color-lock: #c49a20;
|
||||
--shadow-card: 0 2px 4px rgba(58, 46, 34, 0.06);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; padding: 0; background: var(--color-bg); color: var(--color-text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
button { font-family: inherit; cursor: pointer; }
|
||||
|
||||
/* Mac-friendly viewport — show the mobile width centered on a wide screen */
|
||||
.frame {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
min-height: 100dvh;
|
||||
background: var(--color-bg);
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-right: 1px solid var(--color-border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Topbar */
|
||||
.topbar {
|
||||
position: sticky; top: 0; z-index: 5;
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: var(--space-4) var(--space-4) var(--space-2);
|
||||
}
|
||||
.topbar h1 { font-size: var(--text-xl); margin: 0; font-weight: 700; }
|
||||
.topbar__tabs { display: flex; gap: var(--space-2); margin-top: var(--space-3); }
|
||||
.topbar__tab {
|
||||
background: transparent; border: 0;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm); font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.topbar__tab--active { color: var(--color-text); background: var(--color-surface-2); }
|
||||
|
||||
/* Generic chip / button */
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-surface-2);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: var(--text-xs); font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.chip--on { background: var(--color-primary); color: var(--color-primary-fg); border-color: var(--color-primary); }
|
||||
.chip--lock { background: #fff4d1; border-color: var(--color-lock); color: #6b5210; }
|
||||
.chip--lock-on { background: var(--color-lock); color: white; border-color: var(--color-lock); }
|
||||
.chip--device { padding: 3px 8px; font-size: var(--text-xs); }
|
||||
|
||||
/* Photo placeholder — colored gradient so each one is visually distinct */
|
||||
.photo {
|
||||
display: block; width: 100%; aspect-ratio: 1/1;
|
||||
border-radius: var(--radius-sm);
|
||||
background: linear-gradient(135deg, #d4a37e, #8c5a3a);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.photo--p1 { background: linear-gradient(135deg, #c97c3a, #6b3e1e); }
|
||||
.photo--p2 { background: linear-gradient(135deg, #3a8a6e, #1f4d3d); }
|
||||
.photo--p3 { background: linear-gradient(135deg, #8c6fc1, #4c3a82); }
|
||||
.photo--p4 { background: linear-gradient(135deg, #e0a85c, #a5602c); }
|
||||
.photo--p5 { background: linear-gradient(135deg, #5e9bb0, #2f5f72); }
|
||||
.photo--p6 { background: linear-gradient(135deg, #d4a37e, #8c5a3a); }
|
||||
.photo--p7 { background: linear-gradient(135deg, #6f8c5e, #3e5630); }
|
||||
.photo--p8 { background: linear-gradient(135deg, #c1715a, #6e3a2d); }
|
||||
.photo--p9 { background: linear-gradient(135deg, #a08fb1, #62506f); }
|
||||
.photo--p10{ background: linear-gradient(135deg, #d3a87b, #84583a); }
|
||||
|
||||
/* Portrait-aspect variant for the photo simulating a portrait crop */
|
||||
.photo--portrait { aspect-ratio: 3/4; }
|
||||
|
||||
/* Footer nav (decorative, just to look like the real PWA) */
|
||||
.bottomnav {
|
||||
position: sticky; bottom: 0;
|
||||
background: var(--color-surface);
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding: var(--space-3); display: flex; gap: var(--space-4);
|
||||
justify-content: space-around; font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.bottomnav__item { display: flex; flex-direction: column; align-items: center; gap: 2px; }
|
||||
.bottomnav__item--active { color: var(--color-primary); }
|
||||
|
||||
/* Mockup note panel */
|
||||
.note {
|
||||
background: #fff5dc;
|
||||
border: 1px dashed var(--color-lock);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--space-3); margin: var(--space-3) var(--space-4);
|
||||
font-size: var(--text-sm); color: #6b5210;
|
||||
}
|
||||
.note b { color: #3a2e22; }
|
||||
Reference in New Issue
Block a user