172 lines
7.5 KiB
Markdown
172 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.
|