890 lines
54 KiB
Markdown
890 lines
54 KiB
Markdown
|
|
---
|
|||
|
|
name: scanner-desktop-layout
|
|||
|
|
classification: feature
|
|||
|
|
success_metric: |
|
|||
|
|
On md+ viewports, /scanner keeps the desktop app chrome (sidebar +
|
|||
|
|
top bar), shows a framed camera workstation with a real webcam
|
|||
|
|
device picker, Upload Image, Batch Scan, Auto-detect, Scanner
|
|||
|
|
Tips, a live match inspector, and a bottom strip with Recent
|
|||
|
|
Scans / Scan Queue / Duplicates — without regressing the mobile
|
|||
|
|
immersive checkout.
|
|||
|
|
skip: []
|
|||
|
|
status: open
|
|||
|
|
created: 2026-08-15
|
|||
|
|
depends_on:
|
|||
|
|
- scanner-mobile-checkout
|
|||
|
|
- scanner-rebuild
|
|||
|
|
model_policy:
|
|||
|
|
default_session: auto
|
|||
|
|
roles:
|
|||
|
|
role-conductor: composer-2.5-fast
|
|||
|
|
role-architect: composer-2.5
|
|||
|
|
role-ia-architect: composer-2.5-fast
|
|||
|
|
role-ux-reviewer: composer-2.5-fast
|
|||
|
|
role-ui-designer: composer-2.5-fast
|
|||
|
|
role-implementer: composer-2.5-fast
|
|||
|
|
role-reviewer: cursor-grok-4.5-high
|
|||
|
|
role-security-auditor: gpt-5.6-terra-medium
|
|||
|
|
role-design-system-auditor: cursor-grok-4.5-high
|
|||
|
|
role-a11y-auditor: cursor-grok-4.5-high
|
|||
|
|
role-doc-writer: auto
|
|||
|
|
escalate_to: claude-sonnet-5-thinking-medium
|
|||
|
|
escalate_to_premium: claude-4.6-opus-high-thinking
|
|||
|
|
never_premium:
|
|||
|
|
- role-reviewer
|
|||
|
|
- role-security-auditor
|
|||
|
|
- role-design-system-auditor
|
|||
|
|
- role-a11y-auditor
|
|||
|
|
- role-ui-designer
|
|||
|
|
- role-doc-writer
|
|||
|
|
design_direction:
|
|||
|
|
source: role-ui-designer
|
|||
|
|
skill: ui-ux-pro-max
|
|||
|
|
skill_version: "2.5.0"
|
|||
|
|
version: 1
|
|||
|
|
locked_at: 2026-08-15
|
|||
|
|
product_type: desktop trading card scanner workstation
|
|||
|
|
pattern: Feature-Rich Showcase (workstation variant)
|
|||
|
|
style: Liquid Glass / glassmorphism
|
|||
|
|
stack: nextjs
|
|||
|
|
layout_reference: image-1eeebe23-9983-4a96-a3e7-4e3cdfdceb5b.png
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Convoy: scanner-desktop-layout
|
|||
|
|
|
|||
|
|
Give `/scanner` a dedicated desktop workstation layout from the
|
|||
|
|
attached dark/light mock (camera + live result + history strip),
|
|||
|
|
while leaving the shipped mobile immersive checkout alone.
|
|||
|
|
|
|||
|
|
Worktree: `tcg-vault-worktrees/scanner-desktop-layout` on
|
|||
|
|
`convoy/scanner-desktop-layout` (branched from `origin/main` @
|
|||
|
|
`0d52858`). Layout reference:
|
|||
|
|
`image-1eeebe23-9983-4a96-a3e7-4e3cdfdceb5b.png`.
|
|||
|
|
|
|||
|
|
Do **not** land this on `dashboard-home-realignment` — that convoy
|
|||
|
|
owns sidebar IA + top-bar chrome. Do **not** land this on
|
|||
|
|
`scanner-identify-upgrade` — that epic owns detect/identify accuracy.
|
|||
|
|
|
|||
|
|
## Why
|
|||
|
|
|
|||
|
|
`scanner-mobile-checkout` shipped the phone job: full-bleed camera,
|
|||
|
|
local cart, checkout sheet. Desktop (`md+`) got the leftover
|
|||
|
|
composition — the same camera chrome plus a 360px cart side panel
|
|||
|
|
(`ScannerReview` `variant="side-panel"`). That is not a desk
|
|||
|
|
workstation.
|
|||
|
|
|
|||
|
|
On a laptop the user wants to see the webcam, inspect the current
|
|||
|
|
match (set, rarity, number, condition, foil, confidence), decide
|
|||
|
|
what to do with it, and keep a history/queue in view — without
|
|||
|
|
losing the app sidebar or search bar. The mock is that layout.
|
|||
|
|
Today they get a phone overlay stretched into a column.
|
|||
|
|
|
|||
|
|
## Scope
|
|||
|
|
|
|||
|
|
### In scope
|
|||
|
|
|
|||
|
|
- **Desktop-only composition (`md+`).** Keep Layout sidebar +
|
|||
|
|
TopSearchBar. Stop treating desktop as an immersive camera page
|
|||
|
|
with a bolted-on cart. Mobile (`max-md`) stays
|
|||
|
|
`chrome="immersive"` + checkout sheet.
|
|||
|
|
- **Framed camera viewport.** Large live feed with ember corner
|
|||
|
|
brackets (already in `ScannerCamera`), Auto-detect status, and
|
|||
|
|
desk controls under the frame. Camera is a panel in the page,
|
|||
|
|
not a full-bleed overlay.
|
|||
|
|
- **Real webcam device picker.** `enumerateDevices` + `deviceId`
|
|||
|
|
in `useCameraScanner` (not facing-mode swap relabeled). Persist
|
|||
|
|
the last-used device for the tab if cheap. Empty-list / denied-
|
|||
|
|
permission fallback. Mobile keeps the existing facing-mode
|
|||
|
|
toggle — do not replace the phone chrome with a device `<select>`.
|
|||
|
|
- **Upload Image + Batch Scan.** Upload Image stays single-file
|
|||
|
|
(existing gallery path). **Batch Scan** is a multi-file picker
|
|||
|
|
that runs each image through `identifyFromGalleryFile`
|
|||
|
|
**sequentially** (respect existing identify rate limits; no
|
|||
|
|
parallel Gemini storm, no new batch API). Progress and failures
|
|||
|
|
show in Scan Queue. Live multi-card on the webcam stays
|
|||
|
|
Auto-detect — do not build a new pile-in-one-frame detector.
|
|||
|
|
- **Scanner Tips.** Header control from the mock. Glass popover
|
|||
|
|
or modal (use `<Modal>` / `GlassSurface`, no custom scrim) with
|
|||
|
|
short lighting / framing / foil / auto-detect guidance. No new
|
|||
|
|
route. Copy authored in this convoy (IA + UI Designer).
|
|||
|
|
- **Live match inspector (right rail).** Current identify result:
|
|||
|
|
thumbnail, name, set, rarity, collector number, condition,
|
|||
|
|
foil, confidence, primary add, Rescan. This replaces the cart
|
|||
|
|
list as the primary right-hand surface.
|
|||
|
|
- **Bottom history / queue strip.** Three tabs over the existing
|
|||
|
|
cart + ownership model: **Recent Scans** (session history),
|
|||
|
|
**Scan Queue** (uncommitted cart), **Duplicates** (already in
|
|||
|
|
My Collection via `ownershipMap`, and/or same-session
|
|||
|
|
name+set repeats from `mergeScannedCardEntry`). Badge counts
|
|||
|
|
on Queue and Duplicates. Clear + view-all as IA/UX refine.
|
|||
|
|
- **Reuse, don't rewrite, the scan engine.** Same hooks:
|
|||
|
|
`useCameraScanner`, `useScannerIdentification`, `useScannerQueue`,
|
|||
|
|
`scanner-session`. Identify / OCR / Gemini stay out.
|
|||
|
|
|
|||
|
|
### Out of scope
|
|||
|
|
|
|||
|
|
- Identify / detection / Gemini (`lib/scanner-card-identify.js`,
|
|||
|
|
`pages/api/scan/identify.js`, OpenCV). Sibling:
|
|||
|
|
`scanner-identify-upgrade`.
|
|||
|
|
- Mobile immersive checkout, scan peek, or checkout sheet — no
|
|||
|
|
visual or interaction regression below `md`.
|
|||
|
|
- Layout nav IA, Daily Ember, notifications, profile cluster.
|
|||
|
|
Sibling: `dashboard-home-realignment`. Mock items Wishlist /
|
|||
|
|
Trades / Market / Events / Binders are **not** product nav.
|
|||
|
|
- Wishlist as a feature (no table / API). Mock "Save to Wishlist"
|
|||
|
|
maps to **Add to List** or drops — IA decides.
|
|||
|
|
- A new identify / batch-OCR API, parallel Gemini calls, or a
|
|||
|
|
pile-in-one-frame detector. Batch Scan is multi-file sequential
|
|||
|
|
identify on the existing path.
|
|||
|
|
- Schema / new tables. Cart remains client session state.
|
|||
|
|
- Changing the homepage visual-diff baseline except as a
|
|||
|
|
side-effect of `/scanner` desktop chrome (scanner surfaces only).
|
|||
|
|
|
|||
|
|
### Product-vocab lock (from mock → Deck Hearth)
|
|||
|
|
|
|||
|
|
| Mock copy | Ship as |
|
|||
|
|
| --- | --- |
|
|||
|
|
| Add to Collection | `VOCAB.ADD_TO_MY_COLLECTION` |
|
|||
|
|
| Save to Wishlist | Add to List, or omit |
|
|||
|
|
| Binders (nav) | Lists — not this convoy |
|
|||
|
|
| Collection (nav) | My Collection — not this convoy |
|
|||
|
|
|
|||
|
|
## Roles invoked
|
|||
|
|
|
|||
|
|
1. `role-ia-architect` — desktop flow vs mobile cart; inspector
|
|||
|
|
vs queue commit; Duplicates membership (`ownershipMap` vs
|
|||
|
|
session repeat); Batch Scan progress / failure; Tips content
|
|||
|
|
outline; device-picker empty/denied states.
|
|||
|
|
2. `role-ui-designer` — lock the md+ workstation against the
|
|||
|
|
attached mock using Liquid Glass tokens (`ui-ux-pro-max` is
|
|||
|
|
installed), including Tips popover, device `<select>`, Batch
|
|||
|
|
Scan progress, and the three-tab strip. Do **not** skip
|
|||
|
|
`ui-design`.
|
|||
|
|
3. `role-ux-reviewer` — inspect-then-add vs scan-all-then-checkout
|
|||
|
|
on desktop; Auto-detect off; Rescan; empty inspector; leave
|
|||
|
|
with an uncommitted queue; sequential batch cancel; duplicate
|
|||
|
|
tab actions (increment qty vs skip vs still add).
|
|||
|
|
4. `role-architect` — briefs. Likely: (1) page composition +
|
|||
|
|
Layout chrome split, (2) camera panel + `deviceId` picker,
|
|||
|
|
(3) result inspector, (4) history/queue/duplicates strip,
|
|||
|
|
(5) batch multi-file identify + Tips. `slice_dependencies`
|
|||
|
|
must mark what can run in parallel.
|
|||
|
|
5. `role-implementer` — per brief.
|
|||
|
|
6. Audit fan-out: reviewer + security-auditor + design-system-auditor
|
|||
|
|
+ a11y-auditor.
|
|||
|
|
|
|||
|
|
## Todos
|
|||
|
|
|
|||
|
|
- [x] IA: desktop screen inventory + inspector-add vs cart-commit;
|
|||
|
|
Duplicates membership; Batch Scan + Tips content
|
|||
|
|
- [x] UI Designer: lock md+ workstation (tokens, not hex) from
|
|||
|
|
the attached mock; light + dark; Tips, device picker,
|
|||
|
|
batch progress, three-tab strip
|
|||
|
|
- [x] UX: Auto-detect off, Rescan, empty state, leave-with-queue,
|
|||
|
|
keyboard on desk controls, batch cancel, duplicate actions
|
|||
|
|
- [x] Architect: briefs + `slice_dependencies`; confirm Layout
|
|||
|
|
is `chrome="default"` on md+ only
|
|||
|
|
- [ ] Desktop composition in `pages/scanner.js` (do not hide
|
|||
|
|
sidebar / top bar at md+)
|
|||
|
|
- [ ] Camera as a framed panel; `deviceId` picker + Upload +
|
|||
|
|
Batch Scan + Auto-detect; hide mobile overlay chrome at md+
|
|||
|
|
- [ ] Live match inspector wired to the latest unprocessed
|
|||
|
|
identify (condition / foil already on the cart entry)
|
|||
|
|
- [ ] Bottom strip: Recent Scans + Scan Queue + Duplicates over
|
|||
|
|
`useScannerQueue` / `ownershipMap` / `scanner-session`
|
|||
|
|
- [ ] Scanner Tips popover/modal with convoy-authored copy
|
|||
|
|
- [ ] Sequential multi-file Batch Scan through
|
|||
|
|
`identifyFromGalleryFile` (cancellable, queue progress)
|
|||
|
|
- [ ] Tests for desktop composition (inspector + queue +
|
|||
|
|
duplicates + batch enqueue, no mobile sheet) and
|
|||
|
|
no-regression on checkout sheet at `max-width: 767px`
|
|||
|
|
- [ ] Visual-diff: desktop `/scanner` surface; refresh Linux
|
|||
|
|
baselines if the page is in the visual suite
|
|||
|
|
|
|||
|
|
## What exists today (conductor survey)
|
|||
|
|
|
|||
|
|
`/scanner` is one route, two compositions, one engine.
|
|||
|
|
|
|||
|
|
| Layer | Files | Today |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| Page | `pages/scanner.js` | Auth gate; `Layout chrome="immersive"` on **all** viewports; camera column + `md:` 360px `ScannerReview` cart; mobile-only `ScannerCheckoutSheet` |
|
|||
|
|
| Layout | `components/Layout.js` | Immersive hides **mobile** nav + top bar (`max-md` only). Desktop sidebar + TopSearchBar already stay visible. |
|
|||
|
|
| Camera chrome | `components/scanner/ScannerCamera.js` | Full-bleed video, overlay top bar (back / title / gallery), bottom bar (flash / facing / status / Review N), scan peek, disambiguation. Same chrome on desktop. |
|
|||
|
|
| Cart | `lib/use-scanner-queue.js`, `lib/scanner-session.js` | Identify enqueues locally (`processed: false`). Commit via `commitSelectedToOwned` / `commitSelectedToCollection`. `sessionStorage` persist. `addSingleCardToOwned` already exists. |
|
|||
|
|
| Identify | `lib/use-scanner-identification.js`, `lib/use-camera-scanner.js` | Facing-mode swap only — **no** `deviceId` / `enumerateDevices`. Auto-detect is always on unless `verificationPausedRef` (checkout / list picker / disambiguation). |
|
|||
|
|
| Review | `components/scanner/ScannerReview.js` | Thin wrapper: desktop side panel titled "Cart" that mounts `ScannerCheckoutContent`. |
|
|||
|
|
|
|||
|
|
Mobile checkout decisions that still apply unless IA overturns them
|
|||
|
|
for desktop only: stay on camera after commit (D1), skip Setup
|
|||
|
|
(D3), My Collection + List only (D4), gallery in-scope (D5),
|
|||
|
|
cart in `sessionStorage` (D7).
|
|||
|
|
|
|||
|
|
## Conductor notes (build shape)
|
|||
|
|
|
|||
|
|
Likely file ownership for Architect to refine:
|
|||
|
|
|
|||
|
|
| Area | Files |
|
|||
|
|
| --- | --- |
|
|||
|
|
| Viewport split | `pages/scanner.js` — `chrome` default on md+, immersive on mobile; desktop grid vs mobile overlay |
|
|||
|
|
| Camera panel | `components/scanner/ScannerCamera.js` (desktop variant or `variant="workstation"`), `lib/use-camera-scanner.js` (`enumerateDevices` + `deviceId`) |
|
|||
|
|
| Inspector | new `components/scanner/ScannerResultPanel.js` — latest cart entry + condition/foil + add/rescan |
|
|||
|
|
| History strip | new `components/scanner/ScannerHistoryStrip.js` — Recent / Queue / Duplicates over `queue.scannedCards` + `ownershipMap` |
|
|||
|
|
| Batch Scan | `lib/use-scanner-identification.js` (`identifyFromGalleryFile` loop), queue progress UI |
|
|||
|
|
| Tips | new `components/scanner/ScannerTips.js` — `<Modal>` or popover, convoy copy |
|
|||
|
|
| Cart reuse | `lib/use-scanner-queue.js`, `lib/scanner-session.js`, `ScannerCheckoutSheet.js` (mobile only) |
|
|||
|
|
| Copy | `lib/collection-vocabulary.js` |
|
|||
|
|
|
|||
|
|
Do not rewrite identification. Prefer a desktop layout shell that
|
|||
|
|
**hides** mobile overlay chrome at `md+` rather than forking the
|
|||
|
|
camera hook.
|
|||
|
|
|
|||
|
|
## Decisions (post-conductor)
|
|||
|
|
|
|||
|
|
Locked 2026-08-15 from the parent session. IA / UX / Architect
|
|||
|
|
treat these as settled.
|
|||
|
|
|
|||
|
|
| # | Decision |
|
|||
|
|
| --- | --- |
|
|||
|
|
| C1 | **Batch Scan is in.** Multi-file sequential identify via the existing gallery path. No new batch API, no parallel Gemini, no new pile detector. |
|
|||
|
|
| C2 | **Duplicates tab is in.** Strip tab with a badge. Membership = already-owned (`ownershipMap`) and/or same-session name+set repeats. IA picks the exact rule and tab actions. |
|
|||
|
|
| C3 | **Scanner Tips is in.** Header control → glass popover/modal. Copy in this convoy. |
|
|||
|
|
| C4 | **Real webcam device picker is in.** `enumerateDevices` + `deviceId` on desktop. Mobile keeps facing-mode swap. |
|
|||
|
|
| C5 | **Inspector can commit this card now** *and* the queue strip remains for multi-add (same cart, two commit surfaces). Overturn only if IA finds a conflict. |
|
|||
|
|
|
|||
|
|
## Open questions (IA / product)
|
|||
|
|
|
|||
|
|
1. **Duplicates membership + actions.** Owned-in-collection only,
|
|||
|
|
session repeats only, or both? From the tab, can the user still
|
|||
|
|
add (increment qty), skip, or jump the inspector to that row?
|
|||
|
|
2. **Wishlist.** Out as a feature. Confirm "Save to Wishlist" →
|
|||
|
|
Add to List on the inspector, or omit the third action.
|
|||
|
|
3. **Auto-detect toggle.** User-facing pause of identification
|
|||
|
|
(extend `verificationPausedRef`), or just a status badge?
|
|||
|
|
4. **Batch Scan cancel / errors.** Mid-batch cancel: keep already-
|
|||
|
|
identified rows? Per-file failure: continue the rest and flag
|
|||
|
|
the row, or stop?
|
|||
|
|
5. **Tips content.** Four or five short tips (lighting, frame the
|
|||
|
|
card, foil glare, hold still, auto-detect). IA drafts; UI
|
|||
|
|
Designer locks the surface.
|
|||
|
|
|
|||
|
|
## Multitask dispatch
|
|||
|
|
|
|||
|
|
Planning is serial: IA → UI Designer → UX → Architect.
|
|||
|
|
|
|||
|
|
After architect: implementer fan-out only if briefs have
|
|||
|
|
`depends_on: []` and disjoint `files:`. Device picker
|
|||
|
|
(`use-camera-scanner.js`) and Tips (`ScannerTips.js`) are the
|
|||
|
|
best candidates to parallelize with the inspector if they do
|
|||
|
|
not both own `pages/scanner.js`. Page composition likely
|
|||
|
|
blocks the strip and Batch Scan wiring.
|
|||
|
|
|
|||
|
|
After PR draft: `/multitask` audit fan-out
|
|||
|
|
`role-reviewer + role-security-auditor + role-design-system-auditor + role-a11y-auditor`
|
|||
|
|
(group id: `audit-scanner-desktop-layout-<pr>`).
|
|||
|
|
|
|||
|
|
## IA
|
|||
|
|
|
|||
|
|
### Affected routes
|
|||
|
|
|
|||
|
|
- `/scanner` — **[modified]** Single route, two viewport compositions. Desktop (`md+`) switches to `Layout chrome="default"` (sidebar + TopSearchBar visible), framed camera workstation, live match inspector (right rail), and bottom history strip. Mobile (`max-md`) stays `chrome="immersive"` with checkout sheet — no regression.
|
|||
|
|
- `/login` — **[impacted]** Existing `returnUrl=/scanner` auth gate unchanged; desktop users land on the workstation after sign-in.
|
|||
|
|
- `/collections`, `/my-cards` — **[impacted]** Post-commit navigation targets only (Add to List picker, success flows). No route or nav IA changes in this convoy.
|
|||
|
|
|
|||
|
|
No new routes. No API route changes.
|
|||
|
|
|
|||
|
|
### User flow
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
A["/scanner (auth)"] --> B{"md+?"}
|
|||
|
|
B -->|Yes| C["Workstation"]
|
|||
|
|
B -->|No| D["Immersive mobile"]
|
|||
|
|
C --> E["Scan / Upload / Batch"]
|
|||
|
|
E --> F["Match inspector"]
|
|||
|
|
F --> G["Add or queue"]
|
|||
|
|
C --> H["Strip tabs"]
|
|||
|
|
H --> F
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Desktop path: user opens `/scanner` with app chrome → scans via webcam, single Upload Image, or Batch Scan (sequential gallery identify) → latest match appears in the right-rail inspector → commits one card via inspector **or** batches via Scan Queue strip → Duplicates tab surfaces owned + session-repeat rows for review/increment. Mobile path unchanged: full-bleed camera → checkout sheet.
|
|||
|
|
|
|||
|
|
### Screen inventory
|
|||
|
|
|
|||
|
|
| Screen | Path | New/modified | Notes |
|
|||
|
|
| --- | --- | --- | --- |
|
|||
|
|
| Scanner Desktop Workstation | `/scanner` | modified | `md+` grid: framed camera panel (device picker, Upload, Batch Scan, Auto-detect toggle, Tips), right-rail inspector, bottom strip. Replaces 360px cart side panel as primary right-hand surface. |
|
|||
|
|
| Scanner Mobile Immersive | `/scanner` | impacted (no regression) | `max-md`: `chrome="immersive"`, overlay camera chrome, `ScannerCheckoutSheet`. D1/D3/D4/D5/D7 decisions preserved. |
|
|||
|
|
| Live Match Inspector | `/scanner` | new (sub-surface) | Right rail on desktop. Shows latest unprocessed identify: thumbnail, name, set, rarity, collector #, condition, foil, confidence. Actions: `VOCAB.ADD_TO_MY_COLLECTION`, `VOCAB.ADD_TO_LIST`, Rescan. Single-card commit without opening checkout sheet. |
|
|||
|
|
| History / Queue Strip | `/scanner` | new (sub-surface) | Bottom strip on desktop. Tabs: **Recent Scans** (session history), **Scan Queue** (uncommitted cart, badge = unprocessed count), **Duplicates** (badge = duplicate row count). Row click focuses card in inspector. |
|
|||
|
|
| Scanner Tips | `/scanner` | new (sub-surface) | Header control → glass `<Modal>` or popover. Five convoy-authored tips (see Content deltas). No route change. |
|
|||
|
|
| List Picker | `/scanner` | impacted | Existing "Choose a List" `<Modal>`. Opened from inspector `VOCAB.ADD_TO_LIST` on desktop (and unchanged on mobile). |
|
|||
|
|
| Leave Scanner | `/scanner` | impacted | Existing leave-with-uncommitted-queue modal. Applies to both viewports when navigating away with queue items. |
|
|||
|
|
|
|||
|
|
### Content / data model deltas
|
|||
|
|
|
|||
|
|
**Copy (ship from `lib/collection-vocabulary.js`):**
|
|||
|
|
|
|||
|
|
- Primary add: `VOCAB.ADD_TO_MY_COLLECTION` ("Add to My Collection").
|
|||
|
|
- Secondary add: `VOCAB.ADD_TO_LIST` ("Add to List") — **not** "Save to Wishlist" (feature omitted).
|
|||
|
|
- Strip tab labels: "Recent Scans", "Scan Queue", "Duplicates".
|
|||
|
|
- Camera controls: "Upload Image", "Batch Scan", "Auto-detect" (toggle + status on/off), "Scanner Tips", "Rescan".
|
|||
|
|
- Device picker: "Camera" or "Webcam" `<select>` label; empty/denied fallback copy (UI Designer).
|
|||
|
|
|
|||
|
|
**Scanner Tips content (draft for UI Designer):**
|
|||
|
|
|
|||
|
|
1. Good lighting — avoid glare on foil cards.
|
|||
|
|
2. Fill the frame with one card; keep corners visible.
|
|||
|
|
3. Hold still until Auto-detect locks the match.
|
|||
|
|
4. Use Batch Scan for a pile of photos from your gallery.
|
|||
|
|
5. Switch webcam if the image is dark or mirrored.
|
|||
|
|
|
|||
|
|
**No schema changes.** Cart remains client `sessionStorage` via `lib/scanner-session.js`. Duplicates derive from existing `ownershipMap` (already in My Collection) and `mergeScannedCardEntry` (same-session name+set repeats) — no new tables or API fields.
|
|||
|
|
|
|||
|
|
**IA decisions (locked — formerly open questions):**
|
|||
|
|
|
|||
|
|
| Topic | Decision |
|
|||
|
|
| --- | --- |
|
|||
|
|
| Duplicates membership | **Both** sources: `ownershipMap` (already-owned) **and** same-session name+set repeats via `mergeScannedCardEntry`. Badge = count of rows in the Duplicates set. Row click focuses that card in the inspector. User can still add (increment qty) from inspector or queue. |
|
|||
|
|
| Wishlist | **Omit.** Inspector third action is `VOCAB.ADD_TO_LIST`, not wishlist. |
|
|||
|
|
| Auto-detect | **Real pause toggle** on desktop. Extends `verificationPausedRef` (user off = paused identify). Status badge reflects on/off. Mobile behavior unchanged unless UX specifies otherwise. |
|
|||
|
|
| Batch cancel / errors | Mid-batch **cancel keeps** already-identified rows. Per-file failure **continues** the rest and flags that row in Scan Queue — do not stop the batch. |
|
|||
|
|
| Inspector vs queue commit | **Both surfaces** (C5): inspector commits one card now; Scan Queue strip handles multi-add / bulk commit. Same cart, two commit paths. |
|
|||
|
|
| Webcam picker | Desktop only: `enumerateDevices` + `deviceId`. Mobile keeps facing-mode swap (C4). |
|
|||
|
|
|
|||
|
|
### Open IA questions
|
|||
|
|
|
|||
|
|
None — all five conductor questions resolved above.
|
|||
|
|
|
|||
|
|
## Design direction
|
|||
|
|
|
|||
|
|
Locked v1 against `image-1eeebe23-9983-4a96-a3e7-4e3cdfdceb5b.png`
|
|||
|
|
(dark + light side-by-side). **Incremental redesign** of an existing
|
|||
|
|
Liquid Glass surface — not a new palette. Generator run:
|
|||
|
|
`desktop trading card scanner workstation camera inspector queue
|
|||
|
|
glassmorphism` (`ui-ux-pro-max` v2.5.0).
|
|||
|
|
|
|||
|
|
### Summary
|
|||
|
|
|
|||
|
|
On `md+`, `/scanner` is a **desk workstation** inside normal app
|
|||
|
|
chrome (sidebar + TopSearchBar): a framed live-camera panel with
|
|||
|
|
ember corner brackets, a right-rail **live match inspector**, and a
|
|||
|
|
bottom **history / queue strip** — inspect one card, commit from the
|
|||
|
|
rail, or batch from the strip. Mobile stays immersive; this direction
|
|||
|
|
applies only at `md+`.
|
|||
|
|
|
|||
|
|
### Pattern + style
|
|||
|
|
|
|||
|
|
| Field | Value |
|
|||
|
|
| --- | --- |
|
|||
|
|
| Product type | Desktop trading card scanner workstation |
|
|||
|
|
| Landing / app pattern | Feature-Rich Showcase → **workstation grid** (camera + inspector + strip; not a marketing landing) |
|
|||
|
|
| UI style | **Liquid Glass** (existing Deck Hearth DS) — translucent panels, rim-light, ember accent rings |
|
|||
|
|
| Stack notes | nextjs · React 18 · Tailwind + CSS variables · `<Modal>` / `<GlassSurface>` / `<Button>` primitives |
|
|||
|
|
|
|||
|
|
**Generator vs repo (repo wins):**
|
|||
|
|
|
|||
|
|
| Generator | Locked override |
|
|||
|
|
| --- | --- |
|
|||
|
|
| Exaggerated Minimalism (oversized type, massive whitespace) | **Rejected** — match existing app density; page title `text-2xl` / `font-semibold`, subtitle `text-sm text-secondary` |
|
|||
|
|
| Palette `#1E293B` / `#2563EB` scan blue | **Rejected** — use `--accent-ember`, `--accent-flame`, `--accent-gold`, `--bg-*`, `--text-*` from `styles/globals.css` |
|
|||
|
|
| Inter typography | **Rejected** — keep system stack (`-apple-system, BlinkMacSystemFont, …`) |
|
|||
|
|
| Feature-Rich Showcase sections | **Adapted** — three functional zones (camera, inspector, strip) replace marketing feature cards |
|
|||
|
|
|
|||
|
|
### Layout authority (`md+` only)
|
|||
|
|
|
|||
|
|
Viewport split at `md` (768px). Below `md`, no changes (immersive +
|
|||
|
|
checkout sheet).
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ [Layout sidebar] │ TopSearchBar + profile │
|
|||
|
|
├──────────────────┼──────────────────────────────────────────────┤
|
|||
|
|
│ │ Card Scanner [Scanner Tips] │
|
|||
|
|
│ │ <subtitle> │
|
|||
|
|
│ ├──────────────────────────┬───────────────────┤
|
|||
|
|
│ │ ┌─ Auto-detect ON ─┐ │ Scan Result 98% │
|
|||
|
|
│ │ │ [live video] │ │ ┌────┐ metadata │
|
|||
|
|
│ │ │ ember brackets │ │ │thumb│ set/rarity │
|
|||
|
|
│ │ └──────────────────┘ │ condition ▾ foil │
|
|||
|
|
│ │ [Camera ▾][Upload][Batch] │ confidence bar │
|
|||
|
|
│ │ [Auto-detect]│ + Add to My Coll. │
|
|||
|
|
│ │ │ Rescan | Add List │
|
|||
|
|
│ ├──────────────────────────┴───────────────────┤
|
|||
|
|
│ │ Recent │ Queue (N) │ Dupes (N) Clear All│
|
|||
|
|
│ │ [chip][chip][chip]… │
|
|||
|
|
│ │ [ View All Scans ] │
|
|||
|
|
└──────────────────┴──────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Zone | Width / placement | Surface recipe |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| Page header | Full content width above grid | Flat on `--bg-primary`; Tips = `<Button variant="ghost">` + lightbulb icon |
|
|||
|
|
| Camera column | `flex-1` / `min-w-0`; ~60–65% | Viewport frame: `--glass-surface-low` + `--ember-rim-subtle` on outer frame; **ember corner brackets** on video (existing `ScannerCamera`) |
|
|||
|
|
| Desk control bar | Directly under viewport | `--glass-surface-mid` bar, `rounded-xl`, horizontal flex wrap at `md` |
|
|||
|
|
| Inspector rail | Fixed `w-[360px]` or `max-w-sm`; sticky top optional | `--glass-surface-low` panel, `--elevation-ambient`, `--rim-light-inner/outer` |
|
|||
|
|
| Bottom strip | Full content width below main grid | `--glass-surface-mid` container; tab row + chip scroller |
|
|||
|
|
|
|||
|
|
**Page copy (header):**
|
|||
|
|
|
|||
|
|
- Title: **Card Scanner**
|
|||
|
|
- Subtitle: *Identify cards with your webcam and add them to My
|
|||
|
|
Collection.*
|
|||
|
|
|
|||
|
|
### Colors
|
|||
|
|
|
|||
|
|
All values via CSS variables — **no hex in `.js`**.
|
|||
|
|
|
|||
|
|
| Role | Token | Usage on this surface |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| Page background | `--bg-primary` | Workstation canvas behind glass panels |
|
|||
|
|
| Panel fill | `--glass-surface-{low,mid,high}` | Camera frame (low), control bar + strip (mid), popovers (high) |
|
|||
|
|
| Primary text | `--text-primary` | Titles, card names, tab labels |
|
|||
|
|
| Secondary text | `--text-secondary` | Subtitle, set/rarity, timestamps |
|
|||
|
|
| CTA / primary action | `--accent-ember` | **Add to My Collection** button; active tab underline; selected chip border |
|
|||
|
|
| Accent highlight | `--accent-flame`, `--accent-gold` | Confidence bar fill; match badge when ≥90% |
|
|||
|
|
| Interactive rim | `--ember-rim-subtle` / `--ember-rim-pronounced` | Camera frame rest; primary button hover/focus |
|
|||
|
|
| Success / match | `--accent-gold` or existing success token | Auto-detect ON dot; high-confidence pill |
|
|||
|
|
| Error / batch fail | `--accent-ember` at reduced opacity or danger variant | Failed queue row indicator |
|
|||
|
|
| Border | `--border` | Control bar dividers, chip separators |
|
|||
|
|
|
|||
|
|
Light and dark both use the same token names; theme context switches
|
|||
|
|
values. Mock's cool-navy dark is approximated by existing
|
|||
|
|
`--bg-primary-dark` — do not introduce a new dark base.
|
|||
|
|
|
|||
|
|
### Typography
|
|||
|
|
|
|||
|
|
| Role | Font | Notes |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| Page title | System stack, `text-2xl font-semibold` | "Card Scanner" |
|
|||
|
|
| Subtitle | System stack, `text-sm`, `--text-secondary` | One line under title |
|
|||
|
|
| Section labels | `text-sm font-medium uppercase tracking-wide` | "Scan Result", strip tab labels |
|
|||
|
|
| Card name (inspector) | `text-lg font-semibold` | Primary identify headline |
|
|||
|
|
| Metadata | `text-sm`, `--text-secondary` | Set · rarity · collector # |
|
|||
|
|
| Chip / queue row | `text-sm` name, `text-xs` meta | Timestamps right-aligned |
|
|||
|
|
|
|||
|
|
### Camera viewport + desk controls
|
|||
|
|
|
|||
|
|
**Inside frame (overlay on video):**
|
|||
|
|
|
|||
|
|
- **Auto-detect badge** — top-left pill: green status dot +
|
|||
|
|
`Auto-detect ON` / `Auto-detect OFF` (reflects pause toggle).
|
|||
|
|
- **Ember corner brackets** — four L-shaped corners on the card
|
|||
|
|
alignment region (ship existing `ScannerCamera` bracket styling).
|
|||
|
|
- Optional alignment guide (mock dotted vertical line) — **omit in
|
|||
|
|
v1** unless implementer brief explicitly includes it; brackets +
|
|||
|
|
badge are required.
|
|||
|
|
|
|||
|
|
**Desk control bar (below frame, left → right):**
|
|||
|
|
|
|||
|
|
| Control | Component | Notes |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| Camera | `<select>` or styled native picker, label **Camera** | `enumerateDevices` video inputs; desktop only |
|
|||
|
|
| Upload Image | `<Button variant="secondary">` + upload icon | Single-file; existing gallery path |
|
|||
|
|
| Batch Scan | `<Button variant="secondary">` + stack icon | Multi-file picker; triggers sequential identify |
|
|||
|
|
| Auto-detect | Toggle switch + label **Auto-detect** | Right-aligned on wide screens; extends `verificationPausedRef` |
|
|||
|
|
|
|||
|
|
**Webcam device picker — fallback copy (locked):**
|
|||
|
|
|
|||
|
|
| State | Message | Action hint |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| Loading | Detecting cameras… | Disable `<select>` until resolved |
|
|||
|
|
| Empty list | No camera found | Connect a webcam or use **Upload Image**. |
|
|||
|
|
| Permission denied | Camera access blocked | Allow camera in your browser settings, or use **Upload Image**. |
|
|||
|
|
| Error (enumerate failed) | Couldn't list cameras | Use **Upload Image** or reload the page. |
|
|||
|
|
|
|||
|
|
When empty or denied, render the message inline below the disabled
|
|||
|
|
`<select>` (`text-sm`, `--text-secondary`); do not use a blocking
|
|||
|
|
modal for device errors.
|
|||
|
|
|
|||
|
|
### Live match inspector (right rail)
|
|||
|
|
|
|||
|
|
Header row: **Scan Result** + confidence pill (e.g. `98% Match`).
|
|||
|
|
|
|||
|
|
**Empty state** (no identify yet): centered illustration area +
|
|||
|
|
*"Point your camera at a card or upload an image to see a match."*
|
|||
|
|
|
|||
|
|
**Populated state** (latest unprocessed cart entry):
|
|||
|
|
|
|||
|
|
- Thumbnail + name, set, rarity icon, collector number
|
|||
|
|
- **Condition** — `<select>` (existing condition options)
|
|||
|
|
- **Foil** — toggle switch
|
|||
|
|
- Confidence — percentage + horizontal bar + caption (*Excellent match.*
|
|||
|
|
/ *Good match.* / *Low confidence — verify before adding.*)
|
|||
|
|
- Actions (top → bottom priority):
|
|||
|
|
1. **Primary:** `VOCAB.ADD_TO_MY_COLLECTION` (`<Button variant="primary">`, `+` icon optional)
|
|||
|
|
2. **Secondary row:** **Rescan** (ghost) · `VOCAB.ADD_TO_LIST` (ghost)
|
|||
|
|
- **Never** ship "Add to Collection", "Save to Wishlist", or "Mark
|
|||
|
|
Owned".
|
|||
|
|
|
|||
|
|
### Bottom history / queue strip
|
|||
|
|
|
|||
|
|
**Tab row:** **Recent Scans** · **Scan Queue** `(N)` · **Duplicates**
|
|||
|
|
`(N)` — badge = unprocessed count / duplicate row count per IA.
|
|||
|
|
**Clear All** — text link, right-aligned (`text-sm`, destructive on
|
|||
|
|
hover).
|
|||
|
|
|
|||
|
|
**Card chips** (horizontal scroll, `overflow-x-auto`):
|
|||
|
|
|
|||
|
|
- Thumbnail, name, set · rarity · condition snippet
|
|||
|
|
- Status: confidence % + relative time (*Just now*, *2 min ago*)
|
|||
|
|
- Active / focused row: `--ember-rim-pronounced` border (matches mock
|
|||
|
|
ember outline on leftmost chip)
|
|||
|
|
- **No `backdrop-filter` on individual chips** — solid
|
|||
|
|
`--bg-secondary` per per-card grid performance budget
|
|||
|
|
(`docs/DESIGN_TOKENS.md` § Per-card grid performance budget)
|
|||
|
|
|
|||
|
|
**View All Scans** — full-width ghost button at strip bottom; opens
|
|||
|
|
expanded queue view or scrolls strip — Architect brief decides;
|
|||
|
|
visual = translucent bar button from mock.
|
|||
|
|
|
|||
|
|
**Row click** — focuses that card in the inspector (IA-locked).
|
|||
|
|
|
|||
|
|
### Batch Scan progress (Scan Queue tab)
|
|||
|
|
|
|||
|
|
When batch is running, **Scan Queue** tab auto-focuses (or shows an
|
|||
|
|
inline banner):
|
|||
|
|
|
|||
|
|
- Progress line: **Scanning 3 of 12…** with determinate progress bar
|
|||
|
|
(`--accent-ember` fill)
|
|||
|
|
- **Cancel** — ghost button; **keeps already-identified rows** (IA)
|
|||
|
|
- Per-file failure — enqueue row with error flag, label **Identify
|
|||
|
|
failed**, `text-sm` reason if available; batch **continues** remaining
|
|||
|
|
files
|
|||
|
|
- On complete — banner dismisses; failed rows stay in queue with
|
|||
|
|
visual error state (ember left border or warning icon)
|
|||
|
|
|
|||
|
|
### Scanner Tips (surface locked)
|
|||
|
|
|
|||
|
|
**`<Modal>`** — five tips exceed popover length; use
|
|||
|
|
`components/ui/Modal` + modal panel recipe (`--glass-surface-low`,
|
|||
|
|
`--modal-scrim`). Trigger: header **Scanner Tips** button (lightbulb
|
|||
|
|
icon + label).
|
|||
|
|
|
|||
|
|
| # | Tip |
|
|||
|
|
| --- | --- |
|
|||
|
|
| 1 | **Good lighting** — avoid glare on foil cards. |
|
|||
|
|
| 2 | **Fill the frame** with one card; keep corners visible. |
|
|||
|
|
| 3 | **Hold still** until Auto-detect locks the match. |
|
|||
|
|
| 4 | **Use Batch Scan** for a pile of photos from your gallery. |
|
|||
|
|
| 5 | **Switch camera** if the image is dark or mirrored. |
|
|||
|
|
|
|||
|
|
Modal title: **Scanner Tips**. Dismiss via close control + Esc +
|
|||
|
|
scrim click (standard `<Modal>` behavior).
|
|||
|
|
|
|||
|
|
### Effects + motion
|
|||
|
|
|
|||
|
|
- Panel transitions: **150–200ms** ease on hover/focus for buttons and
|
|||
|
|
chips (`docs/MOTION_SYSTEM.md` if defined; else `transition-colors
|
|||
|
|
duration-200`)
|
|||
|
|
- Auto-detect badge dot: subtle pulse when ON; **respect
|
|||
|
|
`prefers-reduced-motion`** (static dot when reduced)
|
|||
|
|
- Confidence bar: width transition 300ms on value change; no animation
|
|||
|
|
when reduced
|
|||
|
|
- Tab switch: instant content swap (no slide); optional 150ms fade on
|
|||
|
|
chip row refresh
|
|||
|
|
- Hover: `cursor-pointer` on all clickable chips, tabs, buttons;
|
|||
|
|
`--ember-rim-subtle` → `--ember-rim-pronounced` on primary hover
|
|||
|
|
|
|||
|
|
### Anti-patterns (do not ship)
|
|||
|
|
|
|||
|
|
- Full-bleed camera overlay on desktop (mobile-only chrome)
|
|||
|
|
- Hex colors in `.js` / inline styles
|
|||
|
|
- Generator slate/blue palette or Inter font import
|
|||
|
|
- `backdrop-filter` on per-card strip chips or thumbnail tiles
|
|||
|
|
- "Add to Collection", "Save to Wishlist", "Mark Owned", "All My
|
|||
|
|
Cards" in UI copy
|
|||
|
|
- Custom modal scrim (must use `<Modal>` primitive)
|
|||
|
|
- Popover for five tips (use Modal)
|
|||
|
|
- Parallel batch identify UI implying concurrent Gemini calls
|
|||
|
|
- Device `<select>` on mobile (facing-mode toggle stays)
|
|||
|
|
|
|||
|
|
### Pre-delivery checklist
|
|||
|
|
|
|||
|
|
- [ ] No emojis as icons (SVG: Lucide / Heroicons)
|
|||
|
|
- [ ] `cursor-pointer` on clickable elements
|
|||
|
|
- [ ] Hover/focus transitions 150–300ms
|
|||
|
|
- [ ] Text contrast ≥ 4.5:1 on glass-over-flat surfaces
|
|||
|
|
- [ ] Keyboard focus visible (`--ember-rim-pronounced` focus ring)
|
|||
|
|
- [ ] `prefers-reduced-motion` respected (badge pulse, bar animate)
|
|||
|
|
- [ ] Responsive: mobile immersive unchanged; workstation at 768 /
|
|||
|
|
1024 / 1440
|
|||
|
|
- [ ] Light + dark verified against mock reference
|
|||
|
|
- [ ] Copy from `lib/collection-vocabulary.js` for add actions
|
|||
|
|
- [ ] Visual-diff: desktop `/scanner` if in suite
|
|||
|
|
|
|||
|
|
### Conflict rule
|
|||
|
|
|
|||
|
|
**Repo design tokens win** when they disagree with generator output.
|
|||
|
|
This lock intentionally overrides `ui-ux-pro-max` palette and typography
|
|||
|
|
only. Layout proportions follow the attached mock; nav items in the
|
|||
|
|
mock that are not product routes (Wishlist, Trades, etc.) are **not**
|
|||
|
|
in scope — existing Layout sidebar IA stands.
|
|||
|
|
|
|||
|
|
## UX
|
|||
|
|
|
|||
|
|
### 1. Existing components to reuse
|
|||
|
|
|
|||
|
|
| Component | Path | Use on desktop workstation |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `<Modal>` | `components/ui/Modal.js` | Leave-with-queue, List picker, **Scanner Tips** (five tips — no custom scrim) |
|
|||
|
|
| `<Button>` | `components/ui/Button.js` | Upload Image, Batch Scan, Auto-detect toggle label area, inspector primary/ghost actions, batch Cancel, View All Scans |
|
|||
|
|
| `<GlassSurface>` | `components/ui/GlassSurface.js` | Camera frame, desk control bar, inspector rail, strip container, strip chips (solid `--bg-secondary` fill inside — no per-chip blur) |
|
|||
|
|
| `<Input>` | `components/ui/Input.js` | Not required v1; strip search deferred to Architect brief |
|
|||
|
|
| `ScannerCamera` | `components/scanner/ScannerCamera.js` | Framed viewport, ember brackets, overlay Auto-detect badge; hide mobile overlay chrome at `md+` via variant prop |
|
|||
|
|
| `ScannerDisambiguation` | `components/scanner/ScannerDisambiguation.js` | Unchanged multi-match picker; pauses identify via existing `verificationPausedRef` wiring |
|
|||
|
|
| `ScannerToast` | `components/scanner/ScannerToast.js` | Post-commit confirmation, batch complete summary, identify errors |
|
|||
|
|
| `ScannerCheckoutSheet` | `components/scanner/ScannerCheckoutSheet.js` | **Mobile only** (`max-md`) — do not mount at `md+` |
|
|||
|
|
| `ScannerCountPill` | `components/scanner/ScannerCountPill.js` | **Mobile only** — desktop uses strip Queue badge instead |
|
|||
|
|
| `ReviewCardItem` | `components/scanner/ReviewCardItem.js` | **Pattern donor** for condition `<select>`, foil toggle, confidence ring/color helpers, increment/decrement — lift constants (`CONDITION_OPTIONS`, `confidencePercent`, `confidenceRingColor`) into shared export or duplicate minimally in `ScannerResultPanel` |
|
|||
|
|
| Leave modal + List picker | `pages/scanner.js` | Reuse verbatim markup and handlers; extend leave trigger to desktop back/nav paths |
|
|||
|
|
|
|||
|
|
**New surfaces** (`ScannerResultPanel`, `ScannerHistoryStrip`, `ScannerTips`) compose primitives only — no new modal shell, no hex in `.js`.
|
|||
|
|
|
|||
|
|
### 2. Design direction alignment
|
|||
|
|
|
|||
|
|
- **Liquid Glass tokens:** All panels use `--glass-surface-{low,mid,high}`, `--ember-rim-{subtle,pronounced}`, `--elevation-ambient` per Design direction layout authority table. Inspector rail = `GlassSurface` tint `low`; desk bar + strip = `mid`.
|
|||
|
|
- **Ember accent:** Primary add, active tab underline, focused chip border, Auto-detect ON dot use `--accent-ember` / `--accent-flame` — not generator slate/blue.
|
|||
|
|
- **Copy lock:** Inspector and strip actions import `VOCAB.ADD_TO_MY_COLLECTION` and `VOCAB.ADD_TO_LIST` from `lib/collection-vocabulary.js`. Wishlist omitted (IA-locked).
|
|||
|
|
- **Performance budget:** Strip chips and inspector thumbnail tiles use solid `--bg-secondary` — **no `backdrop-filter` on per-card elements** (Design direction + `docs/DESIGN_TOKENS.md`).
|
|||
|
|
- **Tips surface:** `<Modal size="md">` with glass panel recipe — matches Design direction "Popover for five tips" rejection.
|
|||
|
|
- **Motion:** 150–200ms color/rim transitions; badge pulse and confidence bar width animate only when `prefers-reduced-motion: no-preference` (see §7 interaction patterns).
|
|||
|
|
|
|||
|
|
### 3. Existing patterns to follow
|
|||
|
|
|
|||
|
|
- **Viewport split:** `pages/scanner.js` already gates mobile checkout with `md:hidden` / `hidden md:flex` — extend to `Layout chrome="default"` at `md+` only; keep `chrome="immersive"` below `md`.
|
|||
|
|
- **Pause identify:** `verificationPausedRef` in `pages/scanner.js` (lines 59–63) — extend desktop Auto-detect OFF to set ref; keep existing pauses for list picker + disambiguation + mobile checkout.
|
|||
|
|
- **Single-card commit:** `queue.addSingleCardToOwned(card)` in `lib/use-scanner-queue.js` — inspector primary action; same API path as mobile bulk commit.
|
|||
|
|
- **Leave with queue:** Existing `<Modal title="Leave scanner?">` + `clearScannerCartStorage()` — trigger from desktop back, sidebar nav away, and `router.back()` when `unprocessedCount > 0`.
|
|||
|
|
- **List commit:** Existing List picker `<Modal>` + `handleListPick` — inspector `VOCAB.ADD_TO_LIST` opens same modal scoped to **focused inspector card** (single selection), not bulk strip selection.
|
|||
|
|
- **Error alerts:** Match `ScannerCheckoutContent` / `pages/scanner.js` list picker — `role="alert"` div with `color-mix(in srgb, var(--color-error) …)` for commit and batch failures.
|
|||
|
|
- **Confidence UX:** Reuse `ReviewCardItem` thresholds: `<70%` ember warning, `≥90%` gold/high — caption text from Design direction (*Excellent match.* / *Good match.* / *Low confidence — verify before adding.*).
|
|||
|
|
- **Device picker fallbacks:** Inline `text-sm` `--text-secondary` messages below disabled `<select>` per Design direction table — not a blocking modal.
|
|||
|
|
- **Focus rings:** `Button` / `ChromeIconButton` pattern — `focus-visible:ring-2` + `'--tw-ring-color': 'var(--accent-ember)'` (`components/scanner/ScannerCamera.js`).
|
|||
|
|
|
|||
|
|
### 4. A11y constraints
|
|||
|
|
|
|||
|
|
Hand to `role-a11y-auditor`:
|
|||
|
|
|
|||
|
|
- **1.4.3 Contrast (Minimum):** All `--text-primary` / `--text-secondary` on `--glass-surface-*` over `--bg-primary` must meet **4.5:1** for body text, **3:1** for large/bold card name (`text-lg font-semibold`). Confidence bar track vs fill must meet **3:1** non-text contrast.
|
|||
|
|
- **1.4.10 Reflow:** At 768px width with 200% zoom, workstation grid stacks camera above inspector; strip tabs remain horizontally scrollable without two-dimensional scroll traps.
|
|||
|
|
- **1.4.11 Focus Not Obscured (Minimum):** Sticky inspector rail and batch progress banner must not fully hide focused desk controls or strip chips.
|
|||
|
|
- **2.1.1 Keyboard:** Every action reachable without pointer: desk controls, strip tabs (tablist), chip rows, inspector add/rescan, batch Cancel, Tips open/close.
|
|||
|
|
- **2.4.3 Focus Order:** DOM order = visual order: page header (Tips) → camera frame → desk bar (Camera → Upload → Batch → Auto-detect) → inspector → strip tabs → chip scroller → View All.
|
|||
|
|
- **2.4.7 Focus Visible:** Ember focus ring on all interactive elements; native `<select>` gets `focus-visible:ring-2` wrapper if browser default ring is suppressed.
|
|||
|
|
- **2.4.11 Focus Not Obscured:** Modal open (Tips, leave, list picker) uses existing `useFocusTrap` — no focus escape to camera video underneath.
|
|||
|
|
- **4.1.2 Name, Role, Value:**
|
|||
|
|
- Auto-detect toggle: `role="switch"`, `aria-checked={!verificationPausedRef}`, visible label **Auto-detect** associated via `htmlFor` / `aria-labelledby`.
|
|||
|
|
- Strip tabs: `role="tablist"` / `role="tab"` / `role="tabpanel"` with `aria-selected`, `aria-controls`, badge counts in `aria-label` (e.g. `Scan Queue, 3 unprocessed cards`).
|
|||
|
|
- Camera `<select>`: `<label>` **Camera** + `aria-describedby` pointing to empty/denied fallback text when present.
|
|||
|
|
- Inspector condition `<select>`: accessible name includes card name (`Condition for {card.name}`).
|
|||
|
|
- Foil toggle: `role="switch"`, `aria-checked`, label **Foil**.
|
|||
|
|
- **4.1.3 Status Messages:** Auto-detect ON/OFF badge exposes `aria-live="polite"` region; batch progress (`Scanning 3 of 12…`) uses `aria-live="polite"`; commit success via `ScannerToast` with `role="status"`.
|
|||
|
|
- **3.3.1 Error Identification:** Batch per-file failures show **Identify failed** + reason text on the queue row; commit errors in inspector use `role="alert"` (same pattern as checkout footer).
|
|||
|
|
- **2.5.3 Label in Name:** Visible button text must appear in accessible name — e.g. **Upload Image**, not icon-only without `aria-label`.
|
|||
|
|
- **Live region discipline:** Only one polite live region for batch progress; avoid duplicate announcements on tab auto-focus during batch.
|
|||
|
|
|
|||
|
|
### 5. Interaction patterns
|
|||
|
|
|
|||
|
|
#### Locked decisions (opinionated — one pattern each)
|
|||
|
|
|
|||
|
|
| Topic | Decision | Nielsen / rationale |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| **Empty inspector** | Centered muted camera/scan illustration + single line: *"Point your camera at a card or upload an image to see a match."* No duplicate Upload/Batch CTAs in the rail (controls live in desk bar). | **H8** aesthetic minimalism; **H6** recognition — one place for actions. |
|
|||
|
|
| **Leave with uncommitted queue** | Keep existing `<Modal title="Leave scanner?">` — **Keep scanning** (secondary) / **Leave** (danger). Same copy and `clearScannerCartStorage()` behavior on desktop nav-away. | **H3** user control; **H5** error prevention. |
|
|||
|
|
| **Inspector add success** | **Advance inspector to next unprocessed queue entry** (or empty state if queue clear). Show `ScannerToast` (*Added to My Collection*). Committed card appears in **Recent Scans** with processed styling. Do **not** linger on committed card in inspector. | **H2** real-world scan rhythm; **H1** toast + strip update confirm status without blocking next decision. |
|
|||
|
|
| **Auto-detect OFF** | Real pause via `verificationPausedRef` — no identify/OCR while OFF. Badge reads **Auto-detect OFF**; dot static gray. Toggle is independent of mobile checkout pause logic. | **H3** user control; **H1** badge reflects system state. |
|
|||
|
|
| **Rescan** | Clears focus card's identify metadata and re-queues same physical capture path (webcam frame or re-read gallery source if batch row) — card stays in queue unprocessed. | **H3** undo/recover from bad match. |
|
|||
|
|
| **Duplicate row click** | Focuses that card in inspector (scroll chip into view, `--ember-rim-pronounced` on active chip). **Add still allowed** — increment qty via inspector primary or existing queue increment if duplicate already unprocessed. No separate "Skip" action in v1. | **H6** recognition over recall; **H4** consistency with Queue tab. |
|
|||
|
|
| **Batch cancel** | Ghost **Cancel** in Scan Queue tab progress banner. Cancels remaining files; **keeps** already-identified rows (IA-locked). Banner dismisses; toast *Batch scan stopped* (info). | **H3** user control; **H9** recover from long batch. |
|
|||
|
|
| **Batch per-file failure** | Row stays in Scan Queue with ember left border + **Identify failed** label; batch **continues**. Row click focuses inspector for manual Rescan or remove. | **H9** graceful recovery; **H1** visible error on row. |
|
|||
|
|
| **Strip row click (all tabs)** | Sets inspector focus to that card; does not auto-commit. Recent / Queue / Duplicates share one focus model. | **H4** consistency. |
|
|||
|
|
| **Clear All** | Destructive text link — confirm via existing pattern or inline `window.confirm` only if Architect brief adds it; default: immediate clear with undo **not** required v1 (queue is session-scoped). Architect to confirm in brief. | **H5** prevent accidental loss — if no confirm, disable when batch running. |
|
|||
|
|
|
|||
|
|
#### Required
|
|||
|
|
|
|||
|
|
- **Loading — identify in flight (H1):** Inspector shows skeleton/thumbnail placeholder + *Identifying…* on desk bar Upload/Batch busy states disable repeat picks.
|
|||
|
|
- **Loading — commit (H1):** Inspector primary `<Button loading>` during `addSingleCardToOwned`; disable Rescan and secondary actions while processing.
|
|||
|
|
- **Empty strip tabs (H9):** Recent: *No scans yet this session.* Queue: *Scan queue is empty — matches appear here before you add them.* Duplicates: *No duplicates detected.*
|
|||
|
|
- **Auto-detect ON feedback (H1):** Green pulsing dot in frame badge (respect reduced motion); optional success chime already in `ScannerCamera` — desktop may keep muted default.
|
|||
|
|
- **Batch running (H1):** Auto-select **Scan Queue** tab; determinate progress bar; Cancel visible throughout.
|
|||
|
|
- **Focus management (H3):** Opening Tips/list/leave modals traps focus; closing returns focus to trigger button.
|
|||
|
|
- **Hover/focus (H4):** Chips and tabs: `--ember-rim-subtle` → `--ember-rim-pronounced` on hover/focus; `cursor-pointer` on all click targets.
|
|||
|
|
|
|||
|
|
#### Nice-to-have
|
|||
|
|
|
|||
|
|
- **Optimistic UI:** Inspector advance on commit can happen after API success only (no optimistic skip) — queue hook already marks `processed` post-success.
|
|||
|
|
- **Keyboard shortcut:** `A` to trigger inspector **Add to My Collection** when inspector populated and not processing — defer unless Architect brief adds; not v1 blocker.
|
|||
|
|
- **View All Scans:** Expanded modal or full-width strip — Architect decides; v1 may scroll chip row only.
|
|||
|
|
|
|||
|
|
### 6. Anti-patterns to avoid
|
|||
|
|
|
|||
|
|
- **Duplicate CTAs in empty inspector** — Upload/Batch already in desk bar (**H8** minimalist design).
|
|||
|
|
- **Custom modal scrim for Tips or device errors** — must use `<Modal>` or inline text only (**H4** consistency).
|
|||
|
|
- **Device `<select>` on mobile** — facing-mode swap stays (**H4**).
|
|||
|
|
- **Popover for five Scanner Tips** — too long; Modal only (**H8**).
|
|||
|
|
- **Linger on committed card in inspector after add** — blocks scan rhythm (**H2**).
|
|||
|
|
- **Stop entire batch on one file failure** — IA forbids (**H9**).
|
|||
|
|
- **Discard identified rows on batch cancel** — IA forbids (**H3**).
|
|||
|
|
- **"Save to Wishlist" / "Mark Owned" / "Add to Collection"** copy — CI forbidden strings (**H4**).
|
|||
|
|
- **`backdrop-filter` on strip chips or inspector thumbnail** — GPU budget violation.
|
|||
|
|
- **Full-bleed camera overlay on desktop** — mobile-only chrome (**H2**).
|
|||
|
|
- **Parallel batch progress implying concurrent API calls** — misleading (**H1** honest status).
|
|||
|
|
- **Icon-only desk controls without `aria-label`** — fails **2.5.3** / **4.1.2**.
|
|||
|
|
- **Auto-commit on strip row click** — user loses inspect step (**H3** user control).
|
|||
|
|
- **Slide animation on tab switch** — Design direction specifies instant swap; fade optional ≤150ms only.
|
|||
|
|
|
|||
|
|
### 7. Mobile / responsive notes
|
|||
|
|
|
|||
|
|
**Below `md` (767px) — zero regression:**
|
|||
|
|
|
|||
|
|
- Keep `Layout chrome="immersive"`, `ScannerCamera` overlay chrome (back, gallery, flash, facing, Review N pill), `ScannerCheckoutSheet`, `ScannerCountPill`, scan peek, disambiguation sheet behavior unchanged.
|
|||
|
|
- Do not render desktop workstation grid, inspector rail, history strip, device `<select>`, Batch Scan desk control, or Tips header button on mobile unless Tips is also added to mobile overlay (out of scope — **Tips = desktop header only v1**).
|
|||
|
|
- `verificationPausedRef` mobile checkout pause unchanged (`isCheckoutOpen && max-width 767px`).
|
|||
|
|
|
|||
|
|
**At `md+` (768px+):**
|
|||
|
|
|
|||
|
|
- `Layout chrome="default"` — sidebar + `TopSearchBar` visible.
|
|||
|
|
- Workstation grid: camera column `flex-1 min-w-0` (~60–65%) + inspector `w-[360px] max-w-sm` + full-width bottom strip.
|
|||
|
|
- Hide mobile-only elements: checkout sheet, Review N pill, overlay top/bottom camera bars, facing-mode toggle (replace with Camera `<select>`).
|
|||
|
|
- Desk control bar wraps at `md`; Auto-detect toggle right-aligned at `lg+`.
|
|||
|
|
- Strip chip scroller: `overflow-x-auto`, `-webkit-overflow-scrolling: touch` for trackpad/touch on hybrid devices; min chip height **44px** touch target.
|
|||
|
|
- **`prefers-reduced-motion`:** Disable Auto-detect badge pulse, confidence bar width transition, optional chip-row fade — use `@media (prefers-reduced-motion: reduce)` instant state swaps; keep color/rim hover transitions ≤150ms or disable per `docs/MOTION_SYSTEM.md` if defined.
|
|||
|
|
- **Breakpoints to verify:** 768 / 1024 / 1440 — inspector sticky top optional; strip never collapses tabs into mystery meat menu v1.
|
|||
|
|
- **Visual-diff:** Desktop `/scanner` only; mobile baseline must not change.
|
|||
|
|
|
|||
|
|
## Architecture
|
|||
|
|
|
|||
|
|
### File plan
|
|||
|
|
|
|||
|
|
| File | Action | Purpose |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `lib/use-camera-scanner.js` | modified | `enumerateDevices`, `deviceId` selection, picker status, sessionStorage persist |
|
|||
|
|
| `test/lib/use-camera-scanner.test.js` | new | Mock `mediaDevices` — device list, constraint switch, persist |
|
|||
|
|
| `components/scanner/ScannerTips.js` | new | Header trigger + five-tip `<Modal>` |
|
|||
|
|
| `test/components/ScannerTips.test.js` | new | Open/close, copy lock |
|
|||
|
|
| `components/scanner/ScannerResultPanel.js` | new | Right-rail live match inspector |
|
|||
|
|
| `test/components/ScannerResultPanel.test.js` | new | Empty/populated, add loading, vocab |
|
|||
|
|
| `components/scanner/ScannerHistoryStrip.js` | new | Recent / Queue / Duplicates strip + `getDuplicateCards` |
|
|||
|
|
| `test/components/ScannerHistoryStrip.test.js` | new | Tabs, duplicates helper, batch banner |
|
|||
|
|
| `lib/scanner-batch-identify.js` | new | Sequential `runSequentialGalleryIdentify` |
|
|||
|
|
| `test/lib/scanner-batch-identify.test.js` | new | Progress, cancel, error continuation |
|
|||
|
|
| `pages/scanner.js` | modified | md+ workstation grid, focus state, pause composite, chrome split |
|
|||
|
|
| `components/scanner/ScannerCamera.js` | modified | `workstation` variant — framed viewport, hide mobile overlay at md+ |
|
|||
|
|
| `lib/use-scanner-identification.js` | modified | Remove redundant `verificationPausedRef` overwrite effect |
|
|||
|
|
| `lib/use-scanner-queue.js` | modified | Boolean return from `addSingleCardToOwned` / `addSingleCardToCollection` |
|
|||
|
|
| `test/pages/scanner.test.js` | new | Mobile checkout regression + desktop composition smoke |
|
|||
|
|
| `test/components/ScannerCamera.test.js` | new | Workstation variant hides overlay chrome |
|
|||
|
|
|
|||
|
|
**Not touched:** `components/Layout.js` (immersive already equals default at md+),
|
|||
|
|
`lib/scanner-card-identify.js`, `pages/api/scan/identify.js`, `ScannerCheckoutSheet.js`
|
|||
|
|
(mobile only), nav IA (`dashboard-home-realignment`).
|
|||
|
|
|
|||
|
|
### API surface
|
|||
|
|
|
|||
|
|
No new or modified API routes. Client cart remains `sessionStorage` via
|
|||
|
|
`lib/scanner-session.js`. All commits reuse existing `scanner-route-api.js` helpers
|
|||
|
|
(`addScannedCardToOwned`, `addScannedCardToCollection`).
|
|||
|
|
|
|||
|
|
### Schema diff
|
|||
|
|
|
|||
|
|
None.
|
|||
|
|
|
|||
|
|
### Test plan
|
|||
|
|
|
|||
|
|
| Area | File | What to lock |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| Device picker hook | `test/lib/use-camera-scanner.test.js` | `deviceId` constraints, enumerate, persist, status messages |
|
|||
|
|
| Batch helper | `test/lib/scanner-batch-identify.test.js` | Sequential order, cancel mid-batch, error continues |
|
|||
|
|
| Tips modal | `test/components/ScannerTips.test.js` | Five tips, Modal primitive |
|
|||
|
|
| Inspector | `test/components/ScannerResultPanel.test.js` | Empty state (no duplicate CTAs), `VOCAB` labels |
|
|||
|
|
| History strip | `test/components/ScannerHistoryStrip.test.js` | `getDuplicateCards`, tab badges, batch banner |
|
|||
|
|
| Camera variant | `test/components/ScannerCamera.test.js` | Overlay hidden for `workstation` at md+ |
|
|||
|
|
| Page orchestration | `test/pages/scanner.test.js` | Checkout sheet at max-md; workstation surfaces at md+ |
|
|||
|
|
| Mobile regression | `test/components/ScannerCheckoutSheet.test.js` | Existing suite stays green — do not weaken |
|
|||
|
|
| Layout chrome | `test/components/Layout.test.js` | Existing immersive tests — no change expected |
|
|||
|
|
|
|||
|
|
**Visual-diff:** `tests/visual/` contains only `homepage.spec.ts` / `home.png` —
|
|||
|
|
no `/scanner` baseline update required for this convoy.
|
|||
|
|
|
|||
|
|
### Risk list
|
|||
|
|
|
|||
|
|
| Risk | Mitigation |
|
|||
|
|
| --- | --- |
|
|||
|
|
| `verificationPausedRef` race between page and `useScannerIdentification` | Brief 6 deletes identification hook's direct ref assignment; page owns composite pause including `isAutoDetectPaused` |
|
|||
|
|
| `addSingleCardToOwned` lacks success signal today | Brief 6 adds boolean return in `use-scanner-queue.js` |
|
|||
|
|
| `chrome="immersive"` on all viewports today | Boot finding: Layout already shows sidebar at md+; page uses explicit `default` vs `immersive` via `matchMedia` for clarity |
|
|||
|
|
| Batch identify failures invisible in queue | Page enqueues `identifyFailed` rows; strip shows ember border + label |
|
|||
|
|
| Rescan without stored capture | v1: re-identify via `scanImageUrl` fetch; else toast to rescan from camera |
|
|||
|
|
| Desktop/mobile class split regressions | Tests mock `matchMedia`; mobile paths keep `md:hidden` gates |
|
|||
|
|
| Forbidden copy in new surfaces | Import `VOCAB` from `collection-vocabulary.js`; CI `forbidden-stale-strings` |
|
|||
|
|
| Brief 6 LOC >400 | Accepted — single serializer for `pages/scanner.js`; components split across Briefs 2–4 |
|
|||
|
|
| `getDuplicateCards` over-counts | Helper uses ownershipMap + session name/set + quantity>1; tune in strip tests |
|
|||
|
|
|
|||
|
|
### Decomposition
|
|||
|
|
|
|||
|
|
| Brief # | Title | Files | Depends on | Est. PR size |
|
|||
|
|
| --- | --- | --- | --- | --- |
|
|||
|
|
| 1 | Camera device picker hook | `use-camera-scanner.js`, test | — | S (~180 LOC) |
|
|||
|
|
| 2 | Scanner Tips modal | `ScannerTips.js`, test | — | S (~100 LOC) |
|
|||
|
|
| 3 | Live match inspector | `ScannerResultPanel.js`, test | — | M (~280 LOC) |
|
|||
|
|
| 4 | History / queue strip | `ScannerHistoryStrip.js`, test | — | M (~300 LOC) |
|
|||
|
|
| 5 | Sequential batch helper | `scanner-batch-identify.js`, test | — | S (~120 LOC) |
|
|||
|
|
| 6 | Page workstation wiring | `scanner.js`, `ScannerCamera.js`, identification + queue hooks, tests | 1, 2, 3, 4, 5 | L (~450 LOC) |
|
|||
|
|
|
|||
|
|
**Estimated PRs:** 6 (Briefs 1–5 parallelizable; Brief 6 after merge or rebase).
|
|||
|
|
|
|||
|
|
### Slice dependencies (multitask-ready)
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
slice_dependencies:
|
|||
|
|
- brief: 1
|
|||
|
|
depends_on: []
|
|||
|
|
files:
|
|||
|
|
- lib/use-camera-scanner.js
|
|||
|
|
- test/lib/use-camera-scanner.test.js
|
|||
|
|
- brief: 2
|
|||
|
|
depends_on: []
|
|||
|
|
files:
|
|||
|
|
- components/scanner/ScannerTips.js
|
|||
|
|
- test/components/ScannerTips.test.js
|
|||
|
|
- brief: 3
|
|||
|
|
depends_on: []
|
|||
|
|
files:
|
|||
|
|
- components/scanner/ScannerResultPanel.js
|
|||
|
|
- test/components/ScannerResultPanel.test.js
|
|||
|
|
- brief: 4
|
|||
|
|
depends_on: []
|
|||
|
|
files:
|
|||
|
|
- components/scanner/ScannerHistoryStrip.js
|
|||
|
|
- test/components/ScannerHistoryStrip.test.js
|
|||
|
|
- brief: 5
|
|||
|
|
depends_on: []
|
|||
|
|
files:
|
|||
|
|
- lib/scanner-batch-identify.js
|
|||
|
|
- test/lib/scanner-batch-identify.test.js
|
|||
|
|
- brief: 6
|
|||
|
|
depends_on: [1, 2, 3, 4, 5]
|
|||
|
|
files:
|
|||
|
|
- pages/scanner.js
|
|||
|
|
- components/scanner/ScannerCamera.js
|
|||
|
|
- lib/use-scanner-identification.js
|
|||
|
|
- lib/use-scanner-queue.js
|
|||
|
|
- test/pages/scanner.test.js
|
|||
|
|
- test/components/ScannerCamera.test.js
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Boot-the-brief findings (fixed in briefs)
|
|||
|
|
|
|||
|
|
1. **Layout chrome:** `chrome="immersive"` already renders sidebar + TopSearchBar at
|
|||
|
|
`md+` (`Layout.js` uses `max-md:hidden` only). Brief 6 uses `matchMedia` to pass
|
|||
|
|
`chrome="default"` on desktop for semantic clarity — no `Layout.js` edit.
|
|||
|
|
2. **`verificationPausedRef` overwrite:** `useScannerIdentification` lines 62–66 set
|
|||
|
|
the ref to `Boolean(disambiguation)` only, racing page composite pause — Brief 6
|
|||
|
|
removes that effect.
|
|||
|
|
3. **`addSingleCardToOwned` return:** No success boolean today — Brief 6 extends
|
|||
|
|
`use-scanner-queue.js` to return `true`/`false`.
|
|||
|
|
4. **Visual suite:** `/scanner` not in `tests/visual/` — no Linux baseline refresh.
|
|||
|
|
5. **No new packages** — dep-set check N/A; all primitives exist (`Modal`, `GlassSurface`, `Button`).
|
|||
|
|
6. **Gallery batch path verified:** `identifyFromGalleryFile` exists in
|
|||
|
|
`use-scanner-identification.js` (line 326) — Brief 5 wraps it sequentially.
|