---
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.
