deckhearth/.convoys/scanner-desktop-layout/brief-6-page-workstation-wiring.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

171 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

---
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 15 ship mountable units; this brief integrates them without blocking parallel implementers.