deckhearth/.convoys/scanner-mobile-checkout/brief-3-camera-chrome-facing-peek.md

125 lines
5.5 KiB
Markdown
Raw Permalink Normal View History

---
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: `<GlassSurface tint="mid">` 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.