deckhearth/.convoys/scanner-desktop-layout.md
varutasu 938c161a26
feat(scanner): add desktop workstation layout (#165)
Give /scanner a md+ camera, live match inspector, and history strip
(with device picker, batch scan, and tips) without regressing the
mobile immersive checkout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-15 17:21:23 -05:00

54 KiB
Raw Blame History

name classification success_metric skip status created depends_on model_policy design_direction
scanner-desktop-layout feature 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.
open 2026-08-15
scanner-mobile-checkout
scanner-rebuild
default_session roles escalate_to escalate_to_premium never_premium
auto
role-conductor role-architect role-ia-architect role-ux-reviewer role-ui-designer role-implementer role-reviewer role-security-auditor role-design-system-auditor role-a11y-auditor role-doc-writer
composer-2.5-fast composer-2.5 composer-2.5-fast composer-2.5-fast composer-2.5-fast composer-2.5-fast cursor-grok-4.5-high gpt-5.6-terra-medium cursor-grok-4.5-high cursor-grok-4.5-high auto
claude-sonnet-5-thinking-medium claude-4.6-opus-high-thinking
role-reviewer
role-security-auditor
role-design-system-auditor
role-a11y-auditor
role-ui-designer
role-doc-writer
source skill skill_version version locked_at product_type pattern style stack layout_reference
role-ui-designer ui-ux-pro-max 2.5.0 1 2026-08-15 desktop trading card scanner workstation Feature-Rich Showcase (workstation variant) Liquid Glass / glassmorphism nextjs 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

  • IA: desktop screen inventory + inspector-add vs cart-commit; Duplicates membership; Batch Scan + Tips content
  • UI Designer: lock md+ workstation (tokens, not hex) from the attached mock; light + dark; Tips, device picker, batch progress, three-tab strip
  • UX: Auto-detect off, Rescan, empty state, leave-with-queue, keyboard on desk controls, batch cancel, duplicate actions
  • 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.jschrome 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

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-secondaryno 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)

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.