--- convoy: scanner-mobile-checkout brief_number: 3 depends_on: [2] recommended_model: composer-2.5-fast model_tier: fast 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 cross_brief_commitments: - brief: 4 description: | `ScannerCamera` accepts orchestration props wired by Brief 4: `onBack`, `onOpenCheckout`, `onGalleryIdentify`, `latestPeekCard`, `cartCount`, `isCheckoutOpen`, `verificationPausedRef`. Defaults/no-ops keep the component renderable before Brief 4 lands. Brief 4 owns sheet open state and back-guard modal. - brief: 4 description: | `useScannerIdentification` exports `identifyFromGalleryFile(file)` — thin adapter only; does not modify `pages/api/scan/identify.js` or `lib/scanner-card-identify.js` server contract. --- # Brief 3: Camera chrome, facing mode, scan peek, gallery adapter ## Goal (1 sentence) Refactor `ScannerCamera` into full-bleed immersive glass chrome (top/bottom bars, flash, camera swap, Review N pill, scan peek, gallery) and add rear/front `facingMode` support in `useCameraScanner`. ## Files in scope (do not edit anything else) - `components/scanner/ScannerCamera.js` - `components/scanner/ScannerCountPill.js` - `components/scanner/ScannerScanPeek.js` (new) - `components/scanner/ScannerToast.js` - `lib/use-camera-scanner.js` - `lib/use-scanner-identification.js` (gallery adapter + pause contract only) ## Conventions to follow - Glass surfaces: `` per UX brief; migrate hardcoded `rgba(0,0,0,0.55)` in `ScannerCamera` to tokens. - `ScannerToast`: replace `ICON_MAP` Unicode glyphs with inline SVG check/error (design-direction checklist). - `ScannerCountPill`: evolve to gradient **Review {N}** pill; keep `aria-label={`Review ${count} scanned cards`}`. - Flash: `flash.flashSupported && facingMode === 'environment'` (UX spec). Hide on front camera; turning to front must call `toggleFlash` off if torch was on. - Peek: new `ScannerScanPeek.js` — 3s auto-dismiss, `pointer-events-none` wrapper, single child button `pointer-events-auto`, tap calls `onOpenCheckout`. - Copy: toast message `"{card.name} added"` with `role="status"`. - Do **not** mount checkout sheet here — call `onOpenCheckout` only. - Identification pipeline files (`scanner-card-identify.js`, API routes) are out of scope. ## `useCameraScanner` shape (verified) Current `startCamera` hardcodes `facingMode: 'environment'` (line ~119). Extend: ```js export function useCameraScanner({ onError, onVerifyCard, verificationPausedRef, facingMode = 'environment' }) { const [activeFacingMode, setActiveFacingMode] = useState(facingMode); // startCamera uses activeFacingMode in getUserMedia constraints const switchFacingMode = useCallback(() => { setActiveFacingMode((prev) => (prev === 'environment' ? 'user' : 'environment')); }, []); // useEffect: when activeFacingMode changes and stream exists, stopCamera + startCamera return { /* existing */, facingMode: activeFacingMode, switchFacingMode }; } ``` `verificationPausedRef` early-return already exists at line ~91 in the tracking interval — no change needed. ## Gallery adapter (verified — no existing file-picker path) Add to `useScannerIdentification` return object: ```js const identifyFromGalleryFile = async (file) => { if (!file || verificationPausedRef?.current) return; const imageData = await readFileToImageData(file); // canvas draw; local helper in same file const authHeaders = getScanAuthHeaders(); const result = await tryLayer1TextIdentify(imageData, authHeaders); const outcome = resolveIdentifyOutcome(result); const syntheticTracker = { id: `gallery-${Date.now()}`, status: 'verifying' }; await applyIdentifyOutcome(syntheticTracker, imageData, outcome); }; ``` Import `tryLayer1TextIdentify`, `resolveIdentifyOutcome` from `./scanner-card-identify.js` (already exported — verified). ## `ScannerCamera` props contract (for Brief 4) ```js export default function ScannerCamera({ queue, camera, identification, onBack, onOpenCheckout, onGalleryIdentify, // (file) => identification.identifyFromGalleryFile(file) latestPeekCard, // most recent unprocessed queue entry or null cartCount, isCheckoutOpen, verificationPausedRef, }) { /* ... */ } ``` Remove: `deckMode`, `sessionDestination`, `onStopSession`, phase-based stop button. Back button calls `onBack` (Brief 4 shows leave modal). Layout: viewport `min-h-[100dvh] max-md:rounded-none max-md:min-h-[100dvh]`; top bar with back / "Scan Cards" / gallery file input; bottom bar flash | status | Review N. ## Acceptance criteria - [ ] `switchFacingMode` restarts stream with toggled `facingMode`; flash hidden on `user` - [ ] `ScannerScanPeek` auto-dismisses ~3s; tap invokes `onOpenCheckout` - [ ] `ScannerToast` uses SVG icons, not Unicode glyphs - [ ] `identifyFromGalleryFile` calls existing `tryLayer1TextIdentify` — no API route changes - [ ] `ScannerCountPill` hidden when `count <= 0`; shows gradient Review pill when `count > 0` - [ ] No hex literals added in edited `.js` files - [ ] No scope expansion ## Rationale (≤3 sentences) Camera chrome files are disjoint from `pages/scanner.js`, so this brief can land after the cart model without file conflicts. Gallery needs a thin hook adapter because no file-picker path exists today (boot-the-brief finding). Brief 4 wires props and pause ref when the sheet opens.