--- convoy: scanner-desktop-layout brief_number: 4 depends_on: [] recommended_model: composer-2.5-fast model_tier: fast files: - components/scanner/ScannerHistoryStrip.js - test/components/ScannerHistoryStrip.test.js cross_brief_commitments: - brief: 6 description: | Brief 6 supplies `focusedCardId`, `onFocusCard`, `batchProgress` (`{ active, current, total, onCancel }`), bulk commit handlers, and `onClearAll`. Row click calls `onFocusCard(card.id)` — no auto-commit. --- # Brief 4: History / queue / duplicates strip ## Goal (1 sentence) Build the bottom `ScannerHistoryStrip` with Recent Scans, Scan Queue, and Duplicates tabs over the existing cart model. ## Files in scope (do not edit anything else) - `components/scanner/ScannerHistoryStrip.js` - `test/components/ScannerHistoryStrip.test.js` ## Conventions to follow - `GlassSurface` tint `mid` for strip container; chips use solid `--bg-secondary` fill — **no per-chip `backdrop-filter`**. - Tab labels: **Recent Scans**, **Scan Queue**, **Duplicates** with badge counts on Queue and Duplicates. - `role="tablist"` / `role="tab"` / `role="tabpanel"`; badges in `aria-label` (e.g. `Scan Queue, 3 unprocessed cards`). - Empty tab copy (UX §5): Recent → *No scans yet this session.* · Queue → *Scan queue is empty — matches appear here before you add them.* · Duplicates → *No duplicates detected.* - **Clear All** text link right-aligned; disabled while `batchProgress?.active`. - **View All Scans** ghost button at strip bottom scrolls chip row to end (v1 — no expanded modal). - Chip row: horizontal `overflow-x-auto`, min height 44px, ember rim on focused chip (`focusedCardId === card.id` → `--ember-rim-pronounced`). - Row click → `onFocusCard(card.id)` only — never auto-commit. - Batch progress UI (when `batchProgress.active`): inline banner in Scan Queue tabpanel — *Scanning {current} of {total}…* determinate bar, ghost **Cancel** calling `batchProgress.onCancel`. - Failed identify rows: `identifyFailed` flag on card (Brief 6 sets) → ember left border + **Identify failed** label + optional reason `text-sm`. ## Duplicate membership helper (export from this file) ```js export function getDuplicateCards(scannedCards, ownershipMap) { const sessionRepeatIds = new Set(); const seen = new Map(); // key: name+set → first id for (const card of scannedCards) { const key = `${card.name}::${card.set}`; if (seen.has(key)) sessionRepeatIds.add(card.id); else seen.set(key, card.id); if ((card.quantity || 1) > 1) sessionRepeatIds.add(card.id); } return scannedCards.filter( (card) => sessionRepeatIds.has(card.id) || (card.databaseId && ownershipMap[card.databaseId]) ); } ``` **Recent Scans** tab: all `scannedCards` session history (processed + unprocessed), newest first. **Scan Queue** tab: `!card.processed` rows; badge = unprocessed count. **Duplicates** tab: `getDuplicateCards(...)`; badge = duplicate set length. ## Props shape ```js export default function ScannerHistoryStrip({ scannedCards, ownershipMap, focusedCardId, onFocusCard, activeTab, onTabChange, batchProgress = null, onClearAll, onCommitSelectedToOwned, onOpenListPicker, isProcessing = false, }) { ``` Bulk commit buttons in Queue tab footer: `VOCAB.ADD_TO_MY_COLLECTION` + `VOCAB.ADD_TO_LIST` calling parent handlers (Brief 6 wires `queue.commitSelectedToOwned`). ## Acceptance criteria - [ ] Three tabs with correct labels and badge counts - [ ] `getDuplicateCards` covers ownershipMap + session name/set repeats + quantity>1 - [ ] Row click invokes `onFocusCard` without commit - [ ] Batch progress banner with Cancel when `batchProgress.active` - [ ] Clear All disabled during active batch - [ ] Chip focus ring matches `focusedCardId` - [ ] tests: tab switching, duplicate helper, empty states, batch banner - [ ] no scope expansion (do not edit files outside `files:` above) ## Rationale (≤3 sentences) The strip is the second commit surface (C5) alongside the inspector. Centralizing duplicate logic in one exported helper keeps `pages/scanner.js` thin. Tab + chip UX is independent of camera device picker, so this brief parallelizes cleanly.