Align toast, sheet, and Review N pill with GlassSurface tokens, use solid cart rows to avoid stacked blurs, and delete pre-rebuild scanner components no longer referenced by /scanner. Co-authored-by: Cursor <cursoragent@cursor.com>
644 lines
46 KiB
Markdown
644 lines
46 KiB
Markdown
---
|
||
name: scanner-mobile-checkout
|
||
classification: feature
|
||
success_metric: |
|
||
On a phone, opening /scanner goes straight into a full-bleed camera;
|
||
each successful identify lands in a local cart (not the database);
|
||
the user can open a checkout sheet, select cards, and commit them
|
||
to My Collection or a List without leaving the session.
|
||
skip: []
|
||
status: shipped
|
||
created: 2026-08-14
|
||
depends_on:
|
||
- scanner-rebuild
|
||
- redesign-scanner-flow
|
||
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-14
|
||
product_type: mobile camera scanner checkout cart trading cards glassmorphism
|
||
pattern: Immersive camera overlay + bottom-sheet checkout
|
||
style: Liquid Glass (repo canonical)
|
||
stack: nextjs
|
||
layout_reference: image-93781104-69e3-4f0b-b25f-df7c750ef046.png
|
||
---
|
||
|
||
# Convoy: scanner-mobile-checkout
|
||
|
||
Turn the scanner into a mobile checkout: scan everything first, then
|
||
commit the cart. Layout reference is the last attached mock (full-bleed
|
||
camera, glass overlays, review as a bottom sheet).
|
||
|
||
## Why
|
||
|
||
The overnight `scanner-rebuild` got us a three-phase machine (Setup →
|
||
Scanning → Review), but the phone experience is still chrome-heavy:
|
||
Layout's sidebar, top search bar, and bottom nav compete with the
|
||
viewfinder; a destination form blocks the camera; and `handleCardScanned`
|
||
writes each match to the database immediately (rebuild D3). That fights
|
||
the actual table-scan job — hold the phone, sweep cards, then check out
|
||
once.
|
||
|
||
Users want supermarket-checkout semantics: open scanner → scan all the
|
||
things → open the cart → add selected cards to My Collection or a List
|
||
→ leave. No per-card "Add" tap. No setup screen in the way.
|
||
|
||
## Scope
|
||
|
||
### In scope
|
||
|
||
- **Immersive scan chrome (mobile).** Hide Layout sidebar, TopSearchBar,
|
||
and MobileNavigation while the camera is live. Camera is full-bleed.
|
||
Overlay only: back, title, optional gallery, detection brackets +
|
||
status around the tracked card, bottom glass bar (flash, scan status,
|
||
Review N).
|
||
- **Cart, not auto-route.** Successful identifies enqueue locally.
|
||
Nothing POSTs to `user_cards` / collections / decks until checkout.
|
||
Reverse rebuild D3 for this flow.
|
||
- **Scan peek.** After a match, a compact non-interactive preview slides
|
||
up briefly (name + thumbnail + confidence) and auto-dismisses. Tapping
|
||
it opens the cart. No condition / foil / Add buttons on the peek.
|
||
- **Checkout sheet.** Bottom sheet over the still-live (but paused)
|
||
camera. List of cart items with selection, qty, confidence, overflow
|
||
menu (remove / condition / foil). Primary: add selected to My
|
||
Collection. Secondary: add selected to a List (picker). Copy from
|
||
`lib/collection-vocabulary.js` — never "binder" as a button label
|
||
(mock says "Save to binder"; product vocab is List).
|
||
- **Camera swap.** Front / rear toggle in the bottom bar. Flash stays
|
||
rear-only and hides when `torch` is unsupported (iOS Safari).
|
||
- **Desktop.** Same cart semantics. At `md+`, cart can be a persistent
|
||
side panel instead of a sheet; do not hide the desktop sidebar.
|
||
|
||
### Out of scope
|
||
|
||
- Identify / OCR / Gemini pipeline (`lib/scanner-card-identify.js`,
|
||
`pages/api/scan/identify.js`, OpenCV detection). Layer UI on the
|
||
existing hooks.
|
||
- Schema / new tables. Cart is client-side session state.
|
||
- Deck Mode, game pre-filter, scan-history list on the setup screen
|
||
(rebuild Setup). Revisit as a later convoy if needed.
|
||
- Estimated total value row from the mock, unless identify payloads
|
||
already include `market_price` with no extra fetch.
|
||
- Deleting leftover `components/CameraScanner.js` /
|
||
`ScannerPageView.js` (pre-rebuild). Separate cleanup.
|
||
- Changing desktop visual-diff homepage baseline except as a
|
||
side-effect of Layout's new immersive prop.
|
||
|
||
## Roles invoked
|
||
|
||
1. `role-ia-architect` — scan → peek → cart → commit flow; where
|
||
destination choice lives now that Setup is gone.
|
||
2. `role-ui-designer` — lock the mobile overlay + checkout sheet against
|
||
the last mock, using Liquid Glass tokens (ui-ux-pro-max is installed).
|
||
3. `role-ux-reviewer` — cart selection, low-confidence handling, back
|
||
with unsaved cart, pause-vs-kill camera under the sheet.
|
||
4. `role-architect` — briefs. Likely: (1) Layout immersive + camera
|
||
chrome, (2) stop auto-route + cart model, (3) checkout sheet +
|
||
destinations. `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: route/flow/screen inventory for immersive scan + checkout sheet
|
||
- [x] UI Designer: lock overlay recipe (tokens, not hex) from last mock
|
||
- [x] UX: cart selection, low-confidence, back-guard, camera pause
|
||
- [x] Architect: briefs + whether Layout gets `chrome="immersive"`
|
||
- [ ] Stop auto-route in `use-scanner-queue.js`; scans stay `processed: false` until checkout
|
||
- [ ] Full-bleed camera; hide app chrome on small viewports
|
||
- [ ] Bottom bar: flash, status, Review N; add facingMode swap
|
||
- [ ] Non-interactive scan peek; checkout sheet with bulk + per-card commit
|
||
- [ ] Pause identification while the sheet is open (`verificationPausedRef`)
|
||
- [ ] Tests for cart enqueue (no network) and checkout commit paths
|
||
- [ ] Visual-diff: scanner surfaces + Layout immersive; refresh baselines if chrome changes
|
||
|
||
## Predecessor
|
||
|
||
`scanner-rebuild` (2026-06-13, overnight) shipped the phase machine,
|
||
flash hook, toasts, count pill, and review list. This convoy keeps those
|
||
hooks and **replaces the UX contract**: skip Setup, immersive camera,
|
||
cart-then-commit instead of auto-route.
|
||
|
||
## Conductor notes (build shape)
|
||
|
||
Likely file ownership for Architect to refine:
|
||
|
||
| Area | Files |
|
||
| --- | --- |
|
||
| Immersive shell | `pages/scanner.js`, `components/Layout.js`, `components/MobileNavigation.js` |
|
||
| Camera chrome | `components/scanner/ScannerCamera.js`, `lib/use-camera-scanner.js`, `lib/use-scanner-flash.js` |
|
||
| Cart model | `lib/use-scanner-queue.js`, `lib/scanner-session.js` |
|
||
| Peek + sheet | new `ScannerScanPeek.js`, rewrite `ScannerReview.js` as a sheet; reuse `ScannerDisambiguation.js` |
|
||
| Copy | `lib/collection-vocabulary.js` (`ADD_TO_MY_COLLECTION`, `ADD_TO_LIST`) |
|
||
|
||
Do not rewrite identification. `handleCardScanned` should enqueue only.
|
||
|
||
## Multitask dispatch
|
||
|
||
Planning is serial: IA → UI Designer → UX → Architect.
|
||
|
||
After architect: implementer fan-out only if briefs have `depends_on: []`
|
||
and disjoint `files:`. Cart-model (stop auto-route) likely blocks the
|
||
checkout sheet; immersive Layout may be parallel with cart-model if
|
||
they do not both own `pages/scanner.js`.
|
||
|
||
After PR draft: `/multitask` audit fan-out
|
||
`role-reviewer + role-security-auditor + role-design-system-auditor + role-a11y-auditor`
|
||
(group id: `audit-scanner-mobile-checkout-<pr>`).
|
||
|
||
## IA
|
||
|
||
### Affected routes
|
||
|
||
- `[modified]` `/scanner` — sole scanner surface. Entry skips Setup and lands on full-bleed camera; cart and checkout live as in-page overlays/sheets on this route (no new URLs). Phase machine collapses from setup → scanning → review into scanning-first with optional cart sheet open/closed.
|
||
- `[impacted]` `/login` — unauthenticated `/scanner` visits still redirect here (unchanged gate in `pages/scanner.js`). Post-login return target is TBD (see open questions).
|
||
- `[impacted]` `/dashboard`, Layout sidebar, command palette — entry links to `/scanner` unchanged; users now arrive directly in camera instead of Setup.
|
||
- `[impacted]` `/my-cards`, `/collections` — not part of the scan flow, but natural post-checkout destinations if the user navigates away after commit. No route or nav changes required.
|
||
|
||
No `[new]` routes. Destination choice (My Collection vs List) moves from pre-scan Setup into the checkout sheet on `/scanner` only — **no route changes**.
|
||
|
||
### User flow
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["Open /scanner"] --> B["Live camera"]
|
||
B --> C["Card identified"]
|
||
C --> D["Scan peek"]
|
||
D --> B
|
||
B --> E["Review N"]
|
||
E --> F["Checkout sheet"]
|
||
F --> G["Add to My Collection / List"]
|
||
G --> H["Exit scanner"]
|
||
```
|
||
|
||
### Screen inventory
|
||
|
||
| Screen | Path | New/modified | Notes |
|
||
| --- | --- | --- | --- |
|
||
| Scanner — live camera | `/scanner` | modified | Default view on open (mobile: immersive, no Layout chrome). Bottom glass bar: flash, status, camera swap, Review N. Detection brackets + status around tracked card. |
|
||
| Scan peek | `/scanner` (overlay) | new | Brief non-interactive slide-up after identify (name, thumbnail, confidence). Auto-dismiss; tap opens checkout sheet. No per-card Add / condition / foil on peek. |
|
||
| Checkout cart sheet | `/scanner` (overlay) | modified | Replaces full-page `ScannerReview` as primary cart UI on mobile. Camera stays mounted but paused under sheet. Selection, qty, confidence, overflow (remove / condition / foil). Primary: Add to My Collection; secondary: Add to List (picker). |
|
||
| Disambiguation sheet | `/scanner` (overlay) | impacted | Existing `ScannerDisambiguation` stays in overlay stack above camera; blocks identify until resolved, then card enqueues to cart. |
|
||
| Scanner Setup | `/scanner` (phase) | impacted | Removed from default entry path (out of scope to delete component). Game filter, deck mode, pre-scan destination, and scan history no longer gate camera start. |
|
||
| Auth loading | `/scanner` | impacted | Spinner while `useAuth` resolves; immersive chrome applies once authenticated (see open question on loading shell). |
|
||
|
||
Desktop (`md+`): same `/scanner` route; cart may render as persistent side panel instead of sheet; Layout sidebar remains visible.
|
||
|
||
### Content / data model deltas
|
||
|
||
- **Copy (write / wire):** checkout CTAs from `lib/collection-vocabulary.js` — `ADD_TO_MY_COLLECTION`, `ADD_TO_LIST` / `ADD_TO_LISTS`; bottom-bar "Review N" (N = cart count); flash and camera-swap `aria-label`s; empty-cart and commit-success toasts. Never use "binder" or "Save to binder" as a button label.
|
||
- **Copy (retire from entry path):** Setup destination picker, game pre-filter, deck-mode selector, and scan-history list as pre-scan gate copy (component may remain for later convoy).
|
||
- **Client session state:** cart is in-memory queue on `/scanner` — cards stay `processed: false` until checkout commit. Reverses rebuild D3 auto-route; no persistence across refresh or navigation away.
|
||
- **API calls (commit-time only, unchanged endpoints):** `POST /api/user-cards` (My Collection), `POST /api/collections/:id/cards` (List), existing `GET /api/collections` + `GET /api/decks` for destination lists. Optional `GET` batch-ownership during cart review. No schema / table changes.
|
||
- **Immersive Layout:** new chrome mode (likely `chrome="immersive"` on `Layout`) affects how `/scanner` composes global nav — content delta only on this page, not a new route.
|
||
|
||
### Open IA questions
|
||
|
||
1. **Post-checkout exit:** Product intent says "then exit" — does the user return to the prior page (`router.back()`), land on `/my-cards` or `/collections`, or stay on `/scanner` with an empty cart and live camera?
|
||
2. **Post-login deep link:** If an unauthenticated user hits `/scanner`, should login return them to `/scanner` (camera) or a safer default (`/dashboard`)?
|
||
3. **Desktop Setup:** Is Setup skipped on `md+` as well, or only on mobile viewports?
|
||
4. **Deck destination:** Deck mode and add-to-deck are out of scope — confirm deck is fully absent from checkout destinations (not just hidden behind Setup).
|
||
5. **Gallery import:** Convoy mentions "optional gallery" in the top overlay — is this in-scope as a file-picker overlay on `/scanner`, or deferred?
|
||
6. **Partial commit:** After adding a subset to My Collection / a List, does the user remain in the sheet with remaining items, or does any successful commit close the session?
|
||
|
||
## Decisions (post-IA)
|
||
|
||
Locked 2026-08-14 from product intent (parent session). UX / UI / Architect treat these as settled.
|
||
|
||
| # | Decision |
|
||
| --- | --- |
|
||
| D1 | **Stay on camera after commit.** Successful checkout removes committed rows from the cart. If the cart is empty, close the sheet and resume scanning. Back (header) is the only exit from `/scanner`. |
|
||
| D2 | **Login returns to `/scanner`.** Keep the existing auth gate; after login, send the user back to the camera, not `/dashboard`. |
|
||
| D3 | **Skip Setup on all viewports.** Desktop uses the same scanning-first entry; cart is a side panel at `md+`, not a return of the Setup form. |
|
||
| D4 | **No deck destination in checkout.** My Collection and List only. Deck Mode stays out of this convoy. |
|
||
| D5 | **Gallery is in-scope.** Top-right control opens a file picker; chosen image goes through the existing identify path and lands in the cart like a live scan. |
|
||
| D6 | **Partial commit stays in the sheet.** Uncommitted rows remain. Sheet closes only when the cart is empty or the user dismisses it. |
|
||
| D7 | **Persist cart in `sessionStorage`.** Survive refresh within the tab; do not persist across browser sessions. |
|
||
|
||
## Design direction
|
||
|
||
Locked v1 — 2026-08-14. Layout authority: last mock
|
||
(`image-93781104-69e3-4f0b-b25f-df7c750ef046.png`). Generator query:
|
||
*"mobile camera scanner checkout cart trading cards glassmorphism"*.
|
||
|
||
### Summary
|
||
|
||
Deck Hearth's mobile scanner should feel like a **supermarket checkout lane
|
||
over a live viewfinder**: full-bleed camera, ember-bracketed detection,
|
||
glass overlays that float above card art, a brief non-interactive peek after
|
||
each match, and a bottom-sheet cart for bulk commit. Warm ember/flame accents
|
||
cast light onto glass — never fill entire panels with brand orange.
|
||
|
||
### Pattern + style
|
||
|
||
| Field | Value |
|
||
| --- | --- |
|
||
| Product type | Mobile TCG collection scanner with session cart + checkout |
|
||
| App pattern | **Immersive camera overlay + bottom-sheet checkout** (generator's "Bento Grid Showcase" rejected — marketing grid, not camera UX) |
|
||
| UI style | **Liquid Glass** — repo canonical (`docs/DESIGN_TOKENS.md`). Generator's "Exaggerated Minimalism" / oversized typography rejected. |
|
||
| Stack notes | Next.js Pages router, React 18, Tailwind + CSS variables. Next.js stack query returned 0 rows — follow existing scanner components + `components/ui/` primitives. |
|
||
| Desktop (`md+`) | Same cart semantics; cart as persistent side panel; Layout sidebar stays visible. Immersive chrome is mobile-only. |
|
||
|
||
### Screen 1 — Live camera (mobile default)
|
||
|
||
Full-bleed `<video>` under all chrome. Layout sidebar, TopSearchBar, and
|
||
MobileNavigation hidden via `chrome="immersive"` on `/scanner`.
|
||
|
||
| Zone | Composition | Tokens / notes |
|
||
| --- | --- | --- |
|
||
| **Top bar** | Back (←), title "Scan Cards", gallery (file picker, D5) | `--glass-surface-mid` + `blur(--glass-blur-mid)` + rim stack. Safe-area inset top. 44×44 tap targets. |
|
||
| **Viewfinder brackets** | Four L-shaped corner brackets framing tracked card | Stroke `--accent-flame` / `--accent-ember`; no filled box. Animated scan line: horizontal ember gradient sweep (`--gradient-primary-*`), respect `prefers-reduced-motion`. |
|
||
| **Scan status** | Centered label below brackets ("Scanning… Hold steady") | `--text-primary` on semi-opaque pill or direct over darkened letterbox — ensure 4.5:1. |
|
||
| **Success toast** | Glass pill: check + "{Card name} added" + dismiss × | `--glass-surface-high` + rim; green check via success token (not emoji). Auto-dismiss ~2s. `role="status"`. |
|
||
| **Scan peek** | Compact slide-up after identify (name, thumb, confidence ring) | `--glass-surface-mid`; **non-interactive** except tap-to-open-sheet. No Add / condition / foil controls. Auto-dismiss ~3s. |
|
||
| **Bottom bar** | Three zones: Flash \| status \| **Review N** pill | Bar: `--glass-surface-mid` + blur mid + `--elevation-ambient`. Flash + camera-swap icons (rear-only torch; hide flash when unsupported). **Review N**: gradient pill `background: var(--gradient-primary-*)` + `--ember-rim-pronounced`; label `Review {N}` with chevron. Min 44px height. |
|
||
|
||
Camera overlays **may** use `backdrop-filter` (exception to per-card grid
|
||
rule — overlays sit above the viewfinder, not on grid items).
|
||
|
||
### Screen 2 — Checkout sheet (mobile)
|
||
|
||
Bottom sheet over **paused** (not unmounted) camera. Scrim + sheet follow
|
||
Modal primitive recipes.
|
||
|
||
| Zone | Composition | Tokens / notes |
|
||
| --- | --- | --- |
|
||
| **Scrim** | Dim live camera | `--modal-scrim` + `blur(--glass-blur-high)` |
|
||
| **Sheet panel** | Rounded top (~16px), grab handle, scrollable list, sticky footer | Panel: `--glass-surface-low` inside scrim + blur mid + `--elevation-pronounced`. Handle: 36×4px `--border` pill, centered. |
|
||
| **Header** | "{N} cards scanned" + low-confidence subline (orange dot) + Edit | Subline only when any row < threshold. Edit toggles bulk-select mode (UX brief). |
|
||
| **Cart rows** | Checkbox, thumb, name/set/rarity, confidence ring, qty stepper (− N +), overflow ⋮ | Row surface: `--glass-surface-high` or solid `--bg-secondary` inner panel over sheet (legibility over busy thumbs). Confidence ring: green ≥85%, amber 70–84%, orange <70% using `--accent-ember` / success tokens — not hardcoded hex. |
|
||
| **Row overflow** | Remove, condition, foil | Popover: `--glass-surface-high` recipe. |
|
||
| **Footer CTAs** | Primary gradient + secondary text button | Primary: `VOCAB.ADD_TO_MY_COLLECTION` — full-width gradient button (`var(--gradient-primary-*)`, `--ember-rim-pronounced`). Secondary: `VOCAB.ADD_TO_LIST` — ghost / link style with list icon; opens picker. **Never** "Save to binder" / "Add all to collection" without vocab import. |
|
||
| **Out of scope row** | "Total value (est.)" from mock | Omit unless identify payload already includes `market_price` (convoy scope). |
|
||
|
||
Partial commit (D6): committed rows leave the list; sheet stays open until
|
||
cart empty or user dismisses. Empty cart → close sheet, resume scanning (D1).
|
||
|
||
### Colors
|
||
|
||
**Repo tokens win.** Generator palette (slate `#1E293B`, scan-blue `#2563EB`)
|
||
is **not adopted**.
|
||
|
||
| Role | Token | Usage in this flow |
|
||
| --- | --- | --- |
|
||
| Primary accent | `--accent-ember` | Brackets, focus rings, low-confidence warnings, primary CTA gradient stop |
|
||
| Secondary accent | `--accent-flame` | Scan line, gradient stops, Review N pill |
|
||
| Gold (rare) | `--accent-gold` | Rarity badges only |
|
||
| Glass fills | `--glass-surface-{low,mid,high}` | Sheet panel / top+bottom bars / toast / peek |
|
||
| Blur | `--glass-blur-{low,mid,high}`, `--glass-saturate` | All glass overlays |
|
||
| Rims | `--rim-light-{inner,outer}`, `--ember-rim-{subtle,pronounced}` | Interactive pills and primary buttons get pronounced ember rim |
|
||
| Scrim | `--modal-scrim` | Sheet backdrop |
|
||
| Text | `--text-primary`, `--text-secondary` | All copy on glass |
|
||
| Background | `--bg-primary`, `--bg-secondary` | Row inner panels when glass-over-art fails contrast |
|
||
|
||
No hex in `.js` files. Use `var(--token)` or existing utility classes
|
||
(`.glass-panel`, gradient vars in `globals.css`).
|
||
|
||
### Typography
|
||
|
||
| Role | Font | Notes |
|
||
| --- | --- | --- |
|
||
| Display / title | System / Inter (existing) | Top bar "Scan Cards" — `font-semibold`, ~17px. No generator "clamp(3rem…)" oversized type. |
|
||
| Body | System / Inter | Row names `font-medium`; set/rarity `text-sm` `--text-secondary` |
|
||
| CTA | System / Inter | Primary button `font-semibold`; secondary `font-medium` |
|
||
|
||
### Effects + motion
|
||
|
||
| Effect | Spec |
|
||
| --- | --- |
|
||
| Transitions | 150–300ms ease on toast, peek slide, sheet open/close |
|
||
| Scan line | Looping horizontal sweep over brackets; disable loop when `prefers-reduced-motion: reduce` |
|
||
| Sheet | Slide up from bottom; scrim fade in. Focus trap while open. |
|
||
| Hover / focus | Primary controls: `--ember-rim-pronounced` on focus-visible; `cursor-pointer` on all clickables |
|
||
| Bracket pulse | Subtle ember glow on active track — `--ember-rim-subtle`, not full-panel fill |
|
||
|
||
See `docs/MOTION_SYSTEM.md` for shared motion taxonomy when implementing.
|
||
|
||
### Copy (mandatory)
|
||
|
||
Import from `lib/collection-vocabulary.js`:
|
||
|
||
| UI surface | Constant |
|
||
| --- | --- |
|
||
| Primary checkout | `VOCAB.ADD_TO_MY_COLLECTION` |
|
||
| Secondary checkout | `VOCAB.ADD_TO_LIST` |
|
||
| Bottom bar | `Review {N}` (N = cart count) |
|
||
| Toast | `{Card name} added` (success) |
|
||
|
||
Retired: "Save to binder", "Mark Owned", "Add all to collection" (use vocab
|
||
or "Add selected to My Collection" when subset selected).
|
||
|
||
### Anti-patterns (do not ship)
|
||
|
||
- Generator's light slate background + blue CTA palette on scanner chrome
|
||
- Opaque drawer replacing bottom sheet (see deprecated mock
|
||
`image-be4e787b-…` — too many actions on peek)
|
||
- Per-card Add / condition / foil on scan peek
|
||
- `backdrop-filter` on cart **thumbnail grid items** (card grid perf budget)
|
||
- Hardcoded hex in JSX; `--accent-blue` / `--accent-purple` legacy aliases
|
||
- Emoji icons (toast check must be SVG or existing glyph pattern)
|
||
- Filling overlay panels with solid ember orange (brand is rim/glow/gradient only)
|
||
- Desktop dual-theme mock layout (`image-6654f196-…`) as mobile reference
|
||
|
||
### Pre-delivery checklist
|
||
|
||
- [ ] No emojis as icons (SVG: Lucide / Heroicons)
|
||
- [ ] `cursor-pointer` on clickable elements
|
||
- [ ] Hover/focus transitions 150–300ms
|
||
- [ ] Text contrast ≥ 4.5:1 on all glass copy (use inner opaque panel if over card art)
|
||
- [ ] Keyboard focus visible (`--accent-ember` ring)
|
||
- [ ] `prefers-reduced-motion` respected (scan line, peek, sheet)
|
||
- [ ] Responsive: 375 (immersive mobile), 768, 1024 (side panel), 1440
|
||
- [ ] Flash hidden when `torch` unsupported; camera-swap `aria-label`s
|
||
- [ ] Checkout copy from `collection-vocabulary.js` only
|
||
|
||
### Token overrides vs generator
|
||
|
||
| Generator output | Locked override |
|
||
| --- | --- |
|
||
| Pattern: Bento Grid Showcase | Immersive camera + bottom-sheet checkout |
|
||
| Style: Exaggerated Minimalism | Liquid Glass (shipped) |
|
||
| Primary `#1E293B`, accent `#2563EB` | `--accent-ember`, `--accent-flame`, `--gradient-primary-*` |
|
||
| Background `#F8FAFC` | Full-bleed camera; glass overlays use `--glass-surface-*` |
|
||
| Typography: massive display type | Existing body scale; scanner chrome stays compact |
|
||
| Key effects: 10vw headings | Scan-line animation + sheet slide only |
|
||
|
||
### Conflict rule
|
||
|
||
**Repo design tokens win** when they disagree with generator output. This
|
||
lock intentionally overrides ui-ux-pro-max palette/pattern for every
|
||
scanner surface. File a convoy note + bump `design_direction.version` only
|
||
if product approves a token change.
|
||
|
||
## UX
|
||
|
||
### Existing components to reuse
|
||
|
||
- `<GlassSurface>` (`components/ui/GlassSurface.js`) — top bar, bottom bar, scan peek, checkout sheet panel, row inner panels. Use `tint="mid"` for chrome bars, `tint="low"` for sheet body, `rim="ember-pronounced"` on Review N pill and primary CTA.
|
||
- `<Modal>` (`components/ui/Modal.js`) — back-guard confirm ("Leave scanner?"), List picker (`size="md"`), nested above checkout sheet z-index stack. Do **not** roll a custom scrim for these dialogs.
|
||
- `<Button>` (`components/ui/Button.js`) — footer primary (`variant="primary"`, `loading` during commit) and secondary (`variant="ghost"`) CTAs; back-guard actions.
|
||
- `<Input>` (`components/ui/Input.js`) — optional filter field inside List picker when `collections.length > 8`.
|
||
- `<ScannerDisambiguation>` (`components/scanner/ScannerDisambiguation.js`) — **canonical bottom-sheet shell** for checkout cart on mobile: same `fixed inset-0` scrim, `glass-panel-strong` panel, drag handle, `useFocusTrap`, Escape-to-dismiss. Checkout sheet copies this shell; do not invent a second sheet primitive.
|
||
- `<ScannerToast>` (`components/scanner/ScannerToast.js`) — success feedback after identify (`role="status"`, `aria-live="polite"`). Replace emoji glyphs with SVG check per design-direction checklist; message shape: `{Card name} added`.
|
||
- `<ScannerCountPill>` (`components/scanner/ScannerCountPill.js`) — evolve into bottom-bar **Review N** gradient pill (same `onReview` contract, `aria-label={`Review ${count} scanned cards`}`); retire the separate count row above the viewport.
|
||
- `<ReviewCardItem>` (`components/scanner/ReviewCardItem.js`) — cart row body: thumbnail, name/set, qty stepper, condition/foil overflow. Extend with leading checkbox + confidence ring; drop inline destination picker (destinations move to footer CTAs only).
|
||
- `<ScannerCamera>` (`components/scanner/ScannerCamera.js`) — viewfinder, bracket overlays, flash hook wiring, disambiguation mount point. Refactor chrome into top/bottom glass bars; keep `verificationPausedRef` plumbing.
|
||
- `<Layout>` (`components/Layout.js`) — `chrome="immersive"` on `/scanner` for `<md` only; desktop keeps sidebar.
|
||
- `useFocusTrap` / `useFocusTrapContainer` (`lib/use-focus-trap.js`) — every overlay sheet and confirm dialog; restore focus to Review N pill or back button on close.
|
||
|
||
**8 primitives** (+ 2 hooks, 1 page shell). New file `ScannerScanPeek.js` is the only net-new overlay component; it composes `<GlassSurface>` only.
|
||
|
||
### Design direction alignment
|
||
|
||
Locked `design_direction` maps directly to repo tokens — no conflicts deferred:
|
||
|
||
| Design-direction surface | Token / primitive |
|
||
| --- | --- |
|
||
| Top + bottom chrome bars | `--glass-surface-mid`, `--glass-blur-mid`, `--rim-light-*`, safe-area insets |
|
||
| Ember brackets + scan line | `--accent-flame`, `--accent-ember`, `--gradient-primary-*`; scan-line loop disabled under `prefers-reduced-motion` |
|
||
| Review N pill | `var(--gradient-primary-*)` + `--ember-rim-pronounced` on `<Button>` or styled `<GlassSurface as="button">` |
|
||
| Checkout sheet scrim | `--modal-scrim` + `blur(--glass-blur-high)` — same recipe as `<Modal>` backdrop |
|
||
| Confidence rings | green ≥85%, amber 70–84%, orange <70% via `--color-success` / `--accent-ember` tokens (not hex literals) |
|
||
| Footer CTAs | `VOCAB.ADD_TO_MY_COLLECTION`, `VOCAB.ADD_TO_LIST` from `lib/collection-vocabulary.js` |
|
||
| Peek + toast | `--glass-surface-mid` / `--glass-surface-high`; SVG icons only |
|
||
|
||
**UX override of design-direction "Edit toggles bulk-select mode":** reject Edit mode. Checkboxes are **always visible** with all rows selected by default — supermarket checkout does not hide selection behind a mode switch (Nielsen H6).
|
||
|
||
### Existing patterns to follow
|
||
|
||
- **Bottom-sheet overlay stack** — `ScannerDisambiguation.js` (scrim click dismisses only when safe; focus trap; `aria-labelledby`). Checkout sheet sits at `z-40`; disambiguation stays at `z-50` above it.
|
||
- **Cart row editing** — `ReviewCardItem.js` qty stepper + condition select pattern; overflow ⋮ menu mirrors row remove button placement.
|
||
- **Bulk selection semantics** — `useScannerQueue` `selectedCards` Set + `toggleCardSelection` / `handleBulkAction`; do not fork a parallel selection model.
|
||
- **Pause identification** — `CameraScanner.js` + `pages/scanner.js` `verificationPausedRef` pattern: `useCameraScanner` early-returns when `verificationPausedRef.current === true`; `useScannerIdentification` already sets it during disambiguation — extend to checkout sheet + List picker open.
|
||
- **Flash gating** — `useScannerFlash.js` `flashSupported` probe; conditionally render flash button (already in `ScannerCamera.js` L221–242). Flash is **rear camera only** — hide when `facingMode === 'user'`.
|
||
- **Immersive page shell** — follow `components/ProtectedRoute.js` / `pages/scanner.js` auth gate; post-login return to `/scanner` (D2) via `router.query.returnUrl` or equivalent login redirect — do not invent a new auth wrapper.
|
||
- **List display names** — `collectionDisplayName()` for picker rows; never render `'All My Cards'` literal.
|
||
- **Motion** — `docs/MOTION_SYSTEM.md` duration tokens: peek/toast `quick` (150ms), sheet `slow` (400ms); sitewide `prefers-reduced-motion` collapse applies.
|
||
- **Rules** — `.cursor/rules/ui-and-theming.mdc` (Modal primitive mandate, 44px targets, focus rings via `--accent-ember`).
|
||
|
||
### A11y constraints
|
||
|
||
Hand to `role-a11y-auditor`:
|
||
|
||
1. **WCAG 2.5.5 Target Size (AAA goal):** every tappable chrome control (back, gallery, flash, camera swap, Review N, peek tap target, row checkbox, qty ±, overflow ⋮, footer CTAs) ≥ 44×44 CSS px — use invisible padding if icon is smaller.
|
||
2. **WCAG 1.4.3 Contrast (AA):** all text on glass ≥ 4.5:1; if card-art bleeds through a row thumb, use `--bg-secondary` inner panel behind text (design-direction row spec).
|
||
3. **WCAG 2.4.3 Focus Order:** checkout sheet opens → focus moves to sheet panel (first focusable: header close/dismiss or first row checkbox via `useFocusTrap`); on close → restore to Review N pill.
|
||
4. **WCAG 2.1.1 Keyboard:** sheet dismiss via Escape; peek opens sheet on Enter/Space when focused; back button triggers confirm modal before navigation.
|
||
5. **WCAG 4.1.2 Name, Role, Value:** checkout sheet `role="dialog"` `aria-modal="true"` `aria-labelledby` → "{N} cards scanned"; List picker nested dialog gets its own label ("Choose a List").
|
||
6. **`aria-label` required:** back → "Leave scanner"; gallery → "Choose image from gallery"; flash → "Turn on flash" / "Turn off flash" + `aria-pressed`; camera swap → "Switch to front camera" / "Switch to rear camera"; Review N → `Review ${N} scanned cards`; row checkbox → `Select ${card.name}`; qty buttons → `Decrease quantity for ${card.name}` / `Increase…`; overflow → `More actions for ${card.name}`.
|
||
7. **WCAG 1.1.1 Non-text Content:** card thumbs `alt={card.name}`; decorative bracket corners `aria-hidden="true"`.
|
||
8. **WCAG 4.1.3 Status Messages:** identify success toast + post-commit success toast use `role="status"` `aria-live="polite"`; do not steal focus.
|
||
9. **WCAG 2.3.3 Animation from Interactions:** scan-line loop, peek slide, sheet slide respect `prefers-reduced-motion: reduce` (instant show/hide, no sweep).
|
||
10. **Focus trap:** checkout sheet, disambiguation sheet, back-guard confirm, List picker — all trap Tab; disambiguation above checkout maintains trap when both mounted.
|
||
11. **Confidence communicated without color alone:** ring color + text label on row (`aria-label` includes "confidence 62 percent") and header subline "{N} need review" when any row < 70%.
|
||
12. **Gallery file input:** visually hidden `<input type="file" accept="image/*">` triggered by labeled button; `aria-label` on button, not bare input.
|
||
13. **Disabled primary CTA:** when zero rows selected, `Add to My Collection` is `disabled` + `aria-disabled="true"` with visible helper "Select at least one card" (linked via `aria-describedby`).
|
||
14. **Screen reader pause announcement:** when sheet opens, `aria-hidden="true"` on camera video element OR `inert` on viewfinder chrome beneath scrim so SR does not read live camera controls under the sheet.
|
||
|
||
### Interaction patterns
|
||
|
||
| Pattern | Spec | Nielsen |
|
||
| --- | --- | --- |
|
||
| **Cart selection — default all selected** *(required)* | On first sheet open, `selectedCards` = all unprocessed row IDs. New card enqueued while sheet open → auto-add its ID to selection. Deselecting is per-row checkbox. Primary CTA label stays `VOCAB.ADD_TO_MY_COLLECTION`; when subset selected, append count: "Add to My Collection (3)". No "Edit mode" toggle. | H6 Recognition rather than recall |
|
||
| **Low-confidence vs disambiguation** *(required)* | **Pipeline `disambiguation` outcome → always block** via existing `ScannerDisambiguation` sheet; never enqueue until user picks. **Single-match `emit` outcome → enqueue silently** to cart regardless of confidence; surface confidence visually (ring + header subline for any row <70%). Do not re-prompt at scan time for low-confidence singles — checkout is the verification gate. | H5 Error prevention; H3 User control |
|
||
| **Scan peek** *(required)* | Slide-up 3s auto-dismiss. Container `pointer-events-none`; single child button wrapper `pointer-events-auto` with `aria-label="Open cart to review {card name}"`. No condition/foil/Add on peek. Tap opens checkout sheet. | H4 Consistency; H8 Minimalist design |
|
||
| **Checkout sheet open/close** *(required)* | Open via Review N pill or peek tap. Close via scrim tap, swipe-down on handle (nice-to-have), or header ×. On close with items remaining: resume camera (`verificationPausedRef = false`). Empty cart after commit → auto-close sheet (D1). | H1 Visibility of system status |
|
||
| **Pause identification under sheet** *(required)* | `verificationPausedRef.current = true` when checkout sheet **or** List picker **or** disambiguation is open. Camera stream stays mounted; detection/verify loop does not run. Resume on sheet close. | H1 Visibility of system status |
|
||
| **Back with unsaved cart** *(required)* | Header back with any unprocessed cart rows → `<Modal>` confirm: title "Leave scanner?", body "{N} scanned cards haven't been added yet.", primary "Keep scanning" (dismiss), secondary "Leave" (`router.back()` or `/dashboard`). No confirm when cart empty. | H3 User control and freedom |
|
||
| **Partial commit** *(required)* | Commit removes only selected rows from cart; sheet stays open with remainder (D6). Success toast `role="status"`. Primary button shows loading via `<Button loading>`. | H9 Error recovery; H1 Feedback |
|
||
| **sessionStorage cart persist** *(required)* | Serialize `scannedCards` + `selectedCards` to `sessionStorage` on change (D7); hydrate on `/scanner` mount. Tab refresh preserves cart; new tab starts empty. Clear on explicit "Leave" confirm. | H6 Recognition rather than recall |
|
||
| **Flash + camera swap** *(required)* | Flash button rendered only when `flashSupported && facingMode === 'environment'`. Camera-swap toggles `facingMode`; turning to front auto-disables torch. Swap button always visible when streaming. | H2 Match between system and real world |
|
||
| **Gallery import** *(required)* | Top-right gallery button → hidden file input; selected image runs existing identify path; on success same peek + cart enqueue as live scan. Show spinner on gallery button while identifying (`aria-busy`). | H4 Consistency |
|
||
| **Empty states** *(required)* | Cart sheet with 0 rows: should not open (Review N hidden at N=0). If last item removed in sheet → auto-close + resume camera. | H9 Error recovery |
|
||
| **Loading states** *(required)* | Camera starting: existing spinner + "Starting camera…". Commit in flight: footer CTA `loading`, row checkboxes disabled. List picker: skeleton or spinner while `collections` fetch. | H1 Visibility of system status |
|
||
| **Error states** *(required)* | Commit API failure: inline banner in sheet footer (not alert()), rows stay selected, retry enabled. Identify error: existing toast/notice path; do not enqueue. | H9 Error recovery |
|
||
| **Desktop side panel** *(required)* | `md+`: cart is persistent right panel (not bottom sheet); no immersive Layout chrome; same selection + CTA semantics; no `verificationPausedRef` pause required (user can see camera + panel simultaneously) — **optional:** still pause on mobile only. | H4 Consistency |
|
||
| **Optimistic commit** *(nice-to-have)* | Defer — show loading on CTA until API confirms; no optimistic removal (batch failures are painful). | H9 |
|
||
|
||
### Anti-patterns to avoid
|
||
|
||
- **Separate "Edit" mode hiding checkboxes** — violates supermarket-checkout mental model (H6).
|
||
- **Silent enqueue on `disambiguation` / `needsUserSelection`** — must block on picker (H5).
|
||
- **Per-card Add / condition / foil on scan peek** — design-direction anti-pattern; peek is read-only + tap-to-cart (H8).
|
||
- **Unmounting camera when sheet opens** — causes slow re-start and flash re-permission; pause via `verificationPausedRef` only (H1).
|
||
- **Auto-routing to DB on identify** — rebuild D3 reversed; enqueue only (H3).
|
||
- **"binder" / "Save to binder" / "Mark Owned" button copy** — use `VOCAB` constants; CI forbidden-strings (H4).
|
||
- **Hardcoded hex / `rgba(0,0,0,0.55)` in new chrome** — use glass tokens; migrate existing `ScannerCamera` literals in same brief (H4).
|
||
- **Emoji toast glyphs** — SVG only per design-direction checklist (H4).
|
||
- **`backdrop-filter` on cart thumbnail elements** — GPU budget; glass on row container only (H8).
|
||
- **Deck destination in checkout** — D4; no deck picker (H4).
|
||
- **Navigating away on commit** — D1 stay on camera (H3).
|
||
- **Confirm dialog via `window.confirm`** — use `<Modal>` for back-guard; matches focus-trap contract (H7 Flexibility).
|
||
- **Interactive peek controls stealing taps from viewfinder** — peek must not block scanning except its single open-cart hit target (H8).
|
||
- **localStorage for cart** — D7 specifies `sessionStorage`; do not use existing `SCANNER_SESSION_STORAGE_KEY` localStorage for cart rows (H6).
|
||
|
||
### Mobile / responsive notes
|
||
|
||
- **<768px (mobile):** `Layout` `chrome="immersive"` — hide sidebar, `TopSearchBar`, `MobileNavigation`. Camera `100dvh` full-bleed (`min-h-0` flex child). Top bar respects `env(safe-area-inset-top)`; bottom bar `env(safe-area-inset-bottom)`. Checkout = bottom sheet (max-height ~85dvh, scrollable list, sticky footer). Peek sits above bottom bar, below brackets.
|
||
- **≥768px (tablet/desktop):** Layout sidebar visible. Camera in main column; cart as **persistent right panel** (~360px) not sheet. Review N pill still opens/focuses panel if collapsed. No immersive chrome.
|
||
- **375px floor:** verify footer CTAs stack (primary full-width, secondary below) without clipping qty steppers; row thumb stays 40×56.
|
||
- **Landscape phone:** sheet max-height may need `max-h-[70dvh]` so viewfinder remains partially visible — architect to decide in brief; UX prefers partial camera peek above sheet scrim (H1).
|
||
- **Touch:** swipe-down on sheet handle to dismiss (nice-to-have); scrim tap dismisses (required). 8px minimum gap between adjacent 44px targets in bottom bar.
|
||
|
||
## Architecture
|
||
|
||
Locked 2026-08-14. Layout gets `chrome="immersive"` (Brief 1). Four implementer briefs; no new API routes or schema changes.
|
||
|
||
### File plan
|
||
|
||
| File | Action | Purpose |
|
||
| --- | --- | --- |
|
||
| `components/Layout.js` | modified | `chrome="immersive"` prop — hide `MobileNavigation` + `TopSearchBar` below `md` |
|
||
| `test/components/Layout.test.js` | modified | Regression-lock immersive chrome |
|
||
| `lib/use-scanner-queue.js` | modified | Enqueue-only `handleCardScanned`; checkout commit helpers; remove auto-route |
|
||
| `lib/scanner-session.js` | modified | `SCANNER_CART_STORAGE_KEY` + load/save/clear cart in `sessionStorage` (D7) |
|
||
| `test/lib/scanner-session.test.js` | modified | Cart persistence helpers |
|
||
| `test/lib/use-scanner-queue.test.js` | new | No-network enqueue + bulk commit mocks |
|
||
| `lib/use-camera-scanner.js` | modified | `facingMode` state + `switchFacingMode` (restart stream) |
|
||
| `lib/use-scanner-identification.js` | modified | `identifyFromGalleryFile` adapter only (calls existing `tryLayer1TextIdentify`) |
|
||
| `components/scanner/ScannerCamera.js` | modified | Full-bleed glass top/bottom bars; flash/swap/gallery; peek mount point |
|
||
| `components/scanner/ScannerCountPill.js` | modified | Gradient Review {N} pill |
|
||
| `components/scanner/ScannerScanPeek.js` | new | Non-interactive 3s peek; tap opens checkout |
|
||
| `components/scanner/ScannerToast.js` | modified | SVG icons (no Unicode glyphs) |
|
||
| `components/scanner/ScannerCheckoutSheet.js` | new | Mobile bottom-sheet cart (shell from `ScannerDisambiguation`) |
|
||
| `components/scanner/ScannerReview.js` | modified | Desktop `md+` persistent side panel variant |
|
||
| `components/scanner/ReviewCardItem.js` | modified | Checkbox + confidence ring; drop row destination picker |
|
||
| `pages/scanner.js` | modified | Scanning-first orchestration; sheet state; pause ref; immersive Layout |
|
||
| `pages/login.js` | modified | Honor `returnUrl` query after login (D2) |
|
||
| `test/components/ScannerCheckoutSheet.test.js` | new | Sheet a11y + vocab + selection-disabled CTA |
|
||
|
||
**Not touched:** `pages/api/**`, `migrations/**`, `lib/scanner-card-identify.js` (identify pipeline), `components/scanner/ScannerDisambiguation.js` (reuse pattern only), `components/scanner/ScannerSetup.js` (dead path, not deleted).
|
||
|
||
### API surface
|
||
|
||
No new or modified API routes. Commit-time calls (unchanged):
|
||
|
||
| Method | Path | Request | Response | Auth | Rate limit |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| POST | `/api/user-cards` | `buildScannerCardPayload(card)` | 2xx / error JSON | Bearer JWT | none (existing) |
|
||
| POST | `/api/collections/:id/cards` | `buildScannerCardPayload(card)` | 2xx / error JSON | Bearer JWT | none |
|
||
| GET | `/api/collections` | — | collection[] | Bearer JWT | none |
|
||
| POST | `/api/cards/batch-ownership` | `{ cardIds: number[] }` | `{ ownership }` | Bearer JWT | none |
|
||
|
||
Helpers live in `lib/scanner-route-api.js` (`addScannedCardToOwned`, `addScannedCardToCollection`, `fetchScannerCollections`, `fetchBatchOwnership`).
|
||
|
||
### Schema diff
|
||
|
||
None. Cart is client-side `sessionStorage` only (`SCANNER_CART_STORAGE_KEY`).
|
||
|
||
### Test plan
|
||
|
||
| Area | File | What to assert |
|
||
| --- | --- | --- |
|
||
| Cart merge defaults | `test/lib/scanner-session.test.js` (existing) | `processed: false` on new entries — keep green |
|
||
| Cart persistence | `test/lib/scanner-session.test.js` | `saveScannerCart` / `loadScannerCart` round-trip |
|
||
| No auto-route | `test/lib/use-scanner-queue.test.js` (new) | `handleCardScanned` never calls `routeScannedCardToDestination`; mock `fetch` |
|
||
| Bulk commit | `test/lib/use-scanner-queue.test.js` | `handleBulkAction('owned')` invokes `addScannedCardToOwned` per selected id |
|
||
| Layout immersive | `test/components/Layout.test.js` | Mobile nav hidden when `chrome="immersive"` |
|
||
| Checkout sheet | `test/components/ScannerCheckoutSheet.test.js` | Dialog labels, `VOCAB` CTAs, disabled primary when selection empty |
|
||
| Auth/Layout regressions | `test/lib/permission-middleware.test.js`, `test/components/Layout.test.js` | Do not weaken existing assertions |
|
||
|
||
Smoke (manual / post-merge): authenticated `/scanner` on 375px viewport — camera full-bleed, Review N opens sheet, commit to My Collection. Visual-diff may need baseline refresh if Layout chrome changes bleed to homepage (unlikely — scanner-only prop).
|
||
|
||
### Risk list
|
||
|
||
| Risk | Mitigation |
|
||
| --- | --- |
|
||
| `pages/scanner.js` was shared by all three conductor guesses | Serialized: only Brief 4 touches `scanner.js` |
|
||
| Gallery has no existing file-picker hook (boot finding) | Brief 3 adds `identifyFromGalleryFile` calling `tryLayer1TextIdentify` — no API change |
|
||
| `facingMode` swap requires stream restart | Brief 3 `stopCamera` + `startCamera` on toggle; flash forced off on front |
|
||
| `Set` not JSON-serializable | Brief 2 persists `selectedCardIds: number[]` |
|
||
| Login lacks `returnUrl` today | Brief 4 adds query param + login handler; validate path starts with `/` |
|
||
| `handleBulkAction` currently marks `processed: true` | Brief 2 removes committed rows from queue instead |
|
||
| Layout visual-diff sensitivity | Narrow prop; immersive mobile-only; no sidebar restyle |
|
||
| `verificationPausedRef` shared by disambiguation + sheet | Brief 4 `useEffect` ORs sheet/list-picker/disambiguation on mobile |
|
||
| Deck code paths remain in queue hook | UI omits deck (D4); dead `deck` branch in `handleBulkAction` harmless until cleanup convoy |
|
||
| iOS Safari no `torch` | Existing `flashSupported` probe; hide flash button |
|
||
|
||
### Decomposition
|
||
|
||
| Brief # | Title | Files | Depends on | Est. PR size |
|
||
| --- | --- | --- | --- | --- |
|
||
| 1 | Layout immersive chrome | `Layout.js`, `test/components/Layout.test.js` | — | ~80 LOC |
|
||
| 2 | Cart model + sessionStorage | `use-scanner-queue.js`, `scanner-session.js`, tests | — | ~200 LOC |
|
||
| 3 | Camera chrome + facing + peek | `ScannerCamera.js`, `use-camera-scanner.js`, `ScannerCountPill.js`, `ScannerScanPeek.js`, `ScannerToast.js`, `use-scanner-identification.js` | 2 | ~350 LOC |
|
||
| 4 | Page orchestration + checkout sheet | `pages/scanner.js`, `pages/login.js`, `ScannerCheckoutSheet.js`, `ScannerReview.js`, `ReviewCardItem.js`, test | 1, 2, 3 | ~380 LOC |
|
||
|
||
### Slice dependencies (multitask-ready)
|
||
|
||
```yaml
|
||
slice_dependencies:
|
||
- brief: 1
|
||
depends_on: []
|
||
files:
|
||
- components/Layout.js
|
||
- test/components/Layout.test.js
|
||
- brief: 2
|
||
depends_on: []
|
||
files:
|
||
- lib/use-scanner-queue.js
|
||
- lib/scanner-session.js
|
||
- test/lib/scanner-session.test.js
|
||
- test/lib/use-scanner-queue.test.js
|
||
- brief: 3
|
||
depends_on: [2]
|
||
files:
|
||
- components/scanner/ScannerCamera.js
|
||
- components/scanner/ScannerCountPill.js
|
||
- components/scanner/ScannerScanPeek.js
|
||
- components/scanner/ScannerToast.js
|
||
- lib/use-camera-scanner.js
|
||
- lib/use-scanner-identification.js
|
||
- brief: 4
|
||
depends_on: [1, 2, 3]
|
||
files:
|
||
- pages/scanner.js
|
||
- pages/login.js
|
||
- components/scanner/ScannerCheckoutSheet.js
|
||
- components/scanner/ScannerReview.js
|
||
- components/scanner/ReviewCardItem.js
|
||
- test/components/ScannerCheckoutSheet.test.js
|
||
```
|
||
|
||
**Parallelism:** Brief **1** and **2** can run in parallel (`depends_on: []`, disjoint files). Brief **3** starts after **2**. Brief **4** is serial after **1 + 2 + 3**. Do not dispatch Brief 3 until Brief 2 merges (cart API contract). Maximum fan-out: 2 implementers (Brief 1 ∥ Brief 2), then 1, then 1.
|
||
|
||
### Boot-the-brief check (2026-08-14)
|
||
|
||
| Check | Finding | Brief fix |
|
||
| --- | --- | --- |
|
||
| Auto-route still live | `handleCardScanned` L148–168 calls `routeScannedCardToDestination` | Brief 2 deletes block |
|
||
| No `chrome` on Layout | `Layout({ children, user, showSearch })` only | Brief 1 adds prop |
|
||
| `facingMode` hardcoded | `getUserMedia` uses `'environment'` only | Brief 3 adds toggle + restart |
|
||
| No gallery entry point | Grep found zero file-picker wiring | Brief 3 `identifyFromGalleryFile` via `tryLayer1TextIdentify` |
|
||
| Login no `returnUrl` | `login.js` always pushes `/dashboard` | Brief 4 adds safe return path |
|
||
| Cart persistence | IA said in-memory; D7 overrides to `sessionStorage` | Brief 2 separate key from `localStorage` session prefs |
|
||
| `ScannerToast` Unicode | `ICON_MAP` uses `✓` / `✗` | Brief 3 SVG replacement |
|
||
| Cross-brief props | `ScannerCamera` needs sheet orchestration | Declared in Brief 3 ↔ 4 `cross_brief_commitments` |
|
||
|
||
No new npm packages. No peer-dep conflicts. `tryLayer1TextIdentify` and `resolveIdentifyOutcome` confirmed exported from `lib/scanner-card-identify.js`.
|