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>
171 lines
7.5 KiB
Markdown
171 lines
7.5 KiB
Markdown
---
|
||
convoy: scanner-desktop-layout
|
||
brief_number: 6
|
||
depends_on: [1, 2, 3, 4, 5]
|
||
recommended_model: composer-2.5-fast
|
||
model_tier: fast
|
||
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
|
||
cross_brief_commitments:
|
||
- brief: 1
|
||
description: |
|
||
Wires `camera.videoDevices`, `selectedDeviceId`, `setSelectedDeviceId`,
|
||
and `devicePickerStatus` into the desk Camera `<select>` (md+ only).
|
||
- brief: 2
|
||
description: |
|
||
Mounts `<ScannerTips />` in desktop page header (`hidden md:flex` row).
|
||
- brief: 3
|
||
description: |
|
||
Mounts `<ScannerResultPanel />` with focus/advance/toast orchestration
|
||
after `addSingleCardToOwned`.
|
||
- brief: 4
|
||
description: |
|
||
Mounts `<ScannerHistoryStrip />` with `focusedCardId`, batch progress,
|
||
and bulk commit handlers.
|
||
- brief: 5
|
||
description: |
|
||
Batch Scan multi-file input calls `runSequentialGalleryIdentify` with
|
||
`identification.identifyFromGalleryFile`.
|
||
---
|
||
|
||
# Brief 6: Desktop workstation page wiring
|
||
|
||
## Goal (1 sentence)
|
||
|
||
Wire `pages/scanner.js` into the md+ workstation grid (inspector + strip + desk controls) without regressing mobile immersive checkout.
|
||
|
||
## Files in scope (do not edit anything else)
|
||
|
||
- `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`
|
||
|
||
## Conventions to follow
|
||
|
||
- **Layout chrome (boot finding):** `chrome="immersive"` already shows sidebar +
|
||
TopSearchBar at `md+` per `Layout.js` (`max-md:hidden` only on mobile nav).
|
||
Use explicit viewport split for semantics:
|
||
|
||
```js
|
||
const [isDesktop, setIsDesktop] = useState(false);
|
||
useEffect(() => {
|
||
const mq = window.matchMedia('(min-width: 768px)');
|
||
const sync = () => setIsDesktop(mq.matches);
|
||
sync();
|
||
mq.addEventListener('change', sync);
|
||
return () => mq.removeEventListener('change', sync);
|
||
}, []);
|
||
// <Layout chrome={isDesktop ? 'default' : 'immersive'} />
|
||
```
|
||
|
||
Smallest change — no `Layout.js` edits.
|
||
|
||
- **Mobile zero regression:** keep `ScannerCheckoutSheet`, overlay camera chrome,
|
||
`ScannerCountPill`, scan peek — all `md:hidden` or `max-md:` as today.
|
||
- Remove desktop `ScannerReview` side panel (`hidden md:flex` block) — replaced by
|
||
inspector + strip.
|
||
- **Pause coordination (boot finding):** `useScannerIdentification` currently has:
|
||
|
||
```js
|
||
useEffect(() => {
|
||
if (verificationPausedRef) {
|
||
verificationPausedRef.current = Boolean(disambiguation);
|
||
}
|
||
}, [disambiguation, verificationPausedRef]);
|
||
```
|
||
|
||
**Delete this effect** — page owns composite pause:
|
||
|
||
```js
|
||
verificationPausedRef.current =
|
||
mobileCheckoutPauses ||
|
||
isListPickerOpen ||
|
||
isAutoDetectPaused ||
|
||
Boolean(identification.disambiguation);
|
||
```
|
||
|
||
Desktop Auto-detect toggle sets `isAutoDetectPaused` (OFF = paused).
|
||
|
||
- **Focused card state:** `focusedCardId` + derived `focusedCard`. On new scan,
|
||
auto-focus latest unprocessed (`scannedCards.find(c => !c.processed)`).
|
||
- **Inspector add success:** wrap `addSingleCardToOwned` — on success show
|
||
`ScannerToast` (*Added to My Collection*), advance to next unprocessed or empty.
|
||
Boot finding: current `addSingleCardToOwned` does not return boolean — check
|
||
`markCardAsProcessed` by awaiting and verifying card.processed or catch errors;
|
||
optionally extend queue method to `return true/false` in a follow-up if needed
|
||
inside this brief only if `use-scanner-queue.js` is NOT in scope — use try/catch
|
||
around API and inspect state after await.
|
||
|
||
**Note:** To return success reliably, add minimal change to
|
||
`lib/use-scanner-queue.js` `addSingleCardToOwned` → `return true` on success,
|
||
`return false` on error — **only if needed**; prefer not to expand files list.
|
||
Alternative: duplicate add call in page using `addScannedCardToOwned` from
|
||
`scanner-route-api.js` — **rejected**; instead add `return true/false` to
|
||
`use-scanner-queue.js` is out of files — use `addingCardIds` clearing + no error
|
||
log as success heuristic OR add `use-scanner-queue.js` to this brief's files.
|
||
|
||
**Architect lock:** add `lib/use-scanner-queue.js` to this brief for
|
||
`addSingleCardToOwned` returning `boolean`.
|
||
|
||
- **List picker desktop:** `handleListPick` uses **focused card id** single selection
|
||
(`queue.addSingleCardToCollection(focusedCard, collectionId)`), not bulk selected set.
|
||
- **Leave modal:** keep existing; trigger on back + unprocessed queue.
|
||
- **Batch Scan:** hidden `<input type="file" multiple accept="image/*" />` on desk bar;
|
||
`runSequentialGalleryIdentify` with `cancelRef`; on `onFileError` enqueue synthetic
|
||
failed row via `queue.handleCardScanned` with `identifyFailed: true` OR dedicated
|
||
`enqueueFailedIdentify(file, error)` inline in page.
|
||
- **Rescan:** clear identify fields on focused card (`updateCardMetadata`), set
|
||
`processed: false`; if `scanImageUrl` present fetch blob and
|
||
`identifyFromGalleryFile`; else toast *Rescan from camera*.
|
||
- **ScannerCamera workstation:** add `variant="default" | "workstation"` (or `layoutMode`).
|
||
|
||
At `md+` with `variant="workstation"`:
|
||
- Container: framed panel not `min-h-[100dvh]` full-bleed — use `md:rounded-xl`,
|
||
`md:aspect-video`, `md:min-h-0`, outer `GlassSurface` + ember rim.
|
||
- Hide overlay top bar + bottom bar (`max-md:` keep current).
|
||
- Hide `ScannerScanPeek`, mobile LIVE badge, `ScannerCountPill` at `md+`.
|
||
- Show **Auto-detect badge** top-left in frame (ON/OFF from `!verificationPausedRef`
|
||
composite for auto-detect slice only — pass `autoDetectOn` prop).
|
||
- Badge dot pulse when ON; `prefers-reduced-motion: reduce` → static dot.
|
||
- Accept optional `deskControls` slot rendered **below** frame by page OR
|
||
`deskControlBar` prop — page renders Upload/Batch/Auto-detect/Camera select below.
|
||
|
||
- **Desk control bar (page, md+):** Camera `<select>` + Upload Image + Batch Scan +
|
||
Auto-detect switch per Design direction. Upload uses existing single-file input path.
|
||
- **Tests:** `test/pages/scanner.test.js` — mock hooks; assert mobile checkout sheet
|
||
present at narrow width; workstation elements (`ScannerResultPanel`, strip) at `md+`
|
||
(use `window.matchMedia` mock). `test/components/ScannerCamera.test.js` — overlay
|
||
chrome hidden at workstation variant.
|
||
- **Visual-diff:** `/scanner` is **not** in `tests/visual/` (only `home.png`) — no
|
||
baseline update required.
|
||
|
||
## `use-scanner-queue.js` changes
|
||
|
||
- `addSingleCardToOwned` returns `true` on success, `false` on error (after `try/catch`).
|
||
- `addSingleCardToCollection` same boolean return for desktop list picker.
|
||
|
||
## Acceptance criteria
|
||
|
||
- [ ] `md+` workstation grid: header + camera column + inspector + bottom strip
|
||
- [ ] `max-md` immersive checkout sheet unchanged (regression test)
|
||
- [ ] `chrome` default on desktop, immersive on mobile
|
||
- [ ] Device picker + Batch Scan + Auto-detect on desk bar only at `md+`
|
||
- [ ] `ScannerTips` in desktop header
|
||
- [ ] Inspector advance + toast on successful add
|
||
- [ ] Batch sequential with cancel + per-file failure flags
|
||
- [ ] `verificationPausedRef` composite pause without identification hook overwrite
|
||
- [ ] No forbidden copy strings
|
||
- [ ] tests added; no visual-diff baseline change
|
||
- [ ] no scope expansion beyond listed files
|
||
|
||
## Rationale (≤3 sentences)
|
||
|
||
Only one brief can own `pages/scanner.js` composition. Camera workstation variant and pause fix are tightly coupled to page wiring. Briefs 1–5 ship mountable units; this brief integrates them without blocking parallel implementers.
|