deckhearth/.convoys/scanner-desktop-layout.md
Randall Stillwell 95c2f6003b docs(convoy): flip shipped statuses (scanner-desktop-layout, reconcile-historical-add-scripts)
- scanner-desktop-layout: open -> shipped; adds As-shipped section
  (PR #165, squash 938c161, 2026-08-15) with post-merge follow-up refs.
- reconcile-historical-add-scripts: open -> shipped to match its
  existing As-shipped section and ship-readiness RESOLVED entry.
2026-08-23 22:27:54 -05:00

902 lines
54 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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: shipped
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`; ~6065% | 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: **150200ms** 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 150300ms
- [ ] 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:** 150200ms 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 5963) — 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` (~6065%) + 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 24 |
| `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 15 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 6266 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.
## As-shipped
Shipped 2026-08-15 as PR #165 (squash `938c161`, commit subject
`feat(scanner): add desktop workstation layout (#165)`). All six briefs'
surfaces landed: md+ workstation chrome, device picker, Upload Image,
Batch Scan, Auto-detect toggle, live match inspector, and the bottom
Recent Scans / Scan Queue / Duplicates strip; mobile immersive checkout
unchanged below `md`.
Related post-merge scanner fixes on adjacent convoys (not this scope):
#166 (multi-card flow + frame overlay + rate limits) and #167 (manual
tap-to-scan shutter).