deckhearth/components/ScanDisambiguationDialog.js

83 lines
3.3 KiB
JavaScript
Raw Normal View History

/* eslint-disable @next/next/no-img-element -- external Scryfall/card CDN URLs */
fix(scanner): import useFocusTrap in ScanDisambiguationDialog (#144) Production runtime ReferenceError on the scan flow: ReferenceError: useFocusTrap is not defined at ScanDisambiguationDialog (...) `components/ScanDisambiguationDialog.js` calls `useFocusTrap(...)` on line 14 but never imports it. The file previously had `import { useRef } from 'react'` (unused since the focus-trap refactor in PR #67 `071a3dc`); the `useFocusTrap` import was never added when the hook was introduced. The bug only manifests at runtime when the scan flow reaches a disambiguation result (i.e. when the OCR/match returns multiple candidate cards), which is why neither smoke nor visual diff caught it — the dialog never renders in the homepage smoke path. Lint also missed it because the project's flat ESLint config does NOT extend `eslint:recommended` / `@eslint/js`'s `no-undef` rule. The `react/jsx-no-undef` rule catches undefined JSX components but not plain JS identifier references. Hardening that gap is the queued `enable-no-undef-eslint-rule` follow-up — out of scope for this hotfix. Replaces the dead `useRef` import with the missing `useFocusTrap` named import from `lib/use-focus-trap.js` (parallel to how `components/ui/Modal.js` imports `useFocusTrapContainer` as the default export — see Modal.js:3 + use-focus-trap.js:21 / :87 for the two-export shape). Test plan: - [x] `npm run lint` — clean (1 pre-existing unrelated warning) - [x] `npm run test:run` — 123 tests pass (24 files) - [ ] CI on this PR - [ ] Post-merge: re-trigger a scan that disambiguates (ambiguous OCR hit) and confirm the modal renders without console errors. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 02:00:47 -04:00
import { useFocusTrap } from '../lib/use-focus-trap';
/**
* Modal for picking among multiple catalog matches after a scan.
*/
export default function ScanDisambiguationDialog({
disambiguation,
submittingReview,
onPick,
onNotInCatalog,
onCancel,
}) {
const dialogRef = useFocusTrap(Boolean(disambiguation));
if (!disambiguation) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60 p-4">
<div
ref={dialogRef}
feat(design-system): sweep authenticated body-content panels to glass (#97) PR #95/#96 shipped the Liquid Glass foundation (tokens, primitives, gates) plus Layout shell, modals, landing, auth pages, and form CTAs — but body- content panels on authenticated pages (admin Card Editor, admin Card Import, admin Submissions, dashboard, my-cards, settings, scanner panels, card detail price cards, popovers) were still rendering as flat var(--bg-secondary) cards. Result: the admin Tools screen and several core pages looked unchanged after the redesign. This sweep adds a `.glass-panel` / `.glass-panel-strong` utility (<GlassSurface tint=mid/high rim=subtle elevation=ambient/pronounced blur=mid/high> in class form) and applies it across 18 surfaces: * Admin Card Editor view, search panel, form (5 sections), preview * Admin Card Import navigation + 3 body cards + sync panel * Admin Card Submissions list items * Dashboard stat cards + empty-state + grid items (5 surfaces) * My-cards empty-state CTA card * Settings panels (3) * Scanner page settings + grid + queue + bulk toolbar + dialog * Scanner destination picker + camera status banner + disambiguation * Card detail price cards (Current / TCGPlayer / CardKingdom) * Permission indicator tooltips * Collections page header card * Card detail view price cards Also migrates the lingering admin Card Editor "Card Editor / Card Import" nav buttons and the "Save Changes" / "Import Cards" / "Run catalog sync" CTAs to the <Button> primitive (consistent loading + disabled states). Page header bands (full-bleed strips with border-bottom on dashboard, my-cards, cards, collections, community/collections, collection/[id]) are intentionally left solid — they're not card-shaped surfaces and stacking glass-on-glass directly below the already-glass topbar would muddy the hierarchy. Tests: lint clean, vitest 104/104, build green. The visual diff baseline will need refresh because the homepage spec is unaffected (it targets the unauthenticated landing page) but the dashboard/ admin/scanner surfaces will diff if/when we add baselines for them. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 22:28:52 -04:00
className="glass-panel-strong max-w-lg w-full rounded-xl p-6 max-h-[80vh] overflow-y-auto"
role="dialog"
aria-modal="true"
aria-labelledby="disambiguation-title"
>
<h3 id="disambiguation-title" className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Which card is this?
</h3>
<p className="text-sm mb-4" style={{ color: 'var(--text-secondary)' }}>
{disambiguation.message || 'Multiple matches found. Select the correct printing.'}
</p>
{disambiguation.visionHint && (
<p className="text-xs mb-3 px-2 py-1 rounded" style={{ color: 'var(--accent-ember)', backgroundColor: 'var(--bg-tertiary)' }}>
Vision detected set: {disambiguation.visionHint}
</p>
)}
<div className="space-y-2">
{disambiguation.candidates.map((candidate) => (
<button
type="button"
key={candidate.id}
onClick={() => onPick(candidate)}
className="w-full flex items-center gap-3 p-3 rounded-lg border text-left hover:opacity-90"
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-tertiary)' }}
aria-label={`Select ${candidate.name}${candidate.set_name ? `, ${candidate.set_name}` : ''}`}
>
{candidate.image_url ? (
<img src={candidate.image_url} alt="" className="w-12 h-16 object-cover rounded" />
) : (
feat(design-system): sweep authenticated body-content panels to glass (#97) PR #95/#96 shipped the Liquid Glass foundation (tokens, primitives, gates) plus Layout shell, modals, landing, auth pages, and form CTAs — but body- content panels on authenticated pages (admin Card Editor, admin Card Import, admin Submissions, dashboard, my-cards, settings, scanner panels, card detail price cards, popovers) were still rendering as flat var(--bg-secondary) cards. Result: the admin Tools screen and several core pages looked unchanged after the redesign. This sweep adds a `.glass-panel` / `.glass-panel-strong` utility (<GlassSurface tint=mid/high rim=subtle elevation=ambient/pronounced blur=mid/high> in class form) and applies it across 18 surfaces: * Admin Card Editor view, search panel, form (5 sections), preview * Admin Card Import navigation + 3 body cards + sync panel * Admin Card Submissions list items * Dashboard stat cards + empty-state + grid items (5 surfaces) * My-cards empty-state CTA card * Settings panels (3) * Scanner page settings + grid + queue + bulk toolbar + dialog * Scanner destination picker + camera status banner + disambiguation * Card detail price cards (Current / TCGPlayer / CardKingdom) * Permission indicator tooltips * Collections page header card * Card detail view price cards Also migrates the lingering admin Card Editor "Card Editor / Card Import" nav buttons and the "Save Changes" / "Import Cards" / "Run catalog sync" CTAs to the <Button> primitive (consistent loading + disabled states). Page header bands (full-bleed strips with border-bottom on dashboard, my-cards, cards, collections, community/collections, collection/[id]) are intentionally left solid — they're not card-shaped surfaces and stacking glass-on-glass directly below the already-glass topbar would muddy the hierarchy. Tests: lint clean, vitest 104/104, build green. The visual diff baseline will need refresh because the homepage spec is unaffected (it targets the unauthenticated landing page) but the dashboard/ admin/scanner surfaces will diff if/when we add baselines for them. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 22:28:52 -04:00
<div className="glass-panel w-12 h-16 rounded flex items-center justify-center text-xs">🃏</div>
)}
<div>
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>{candidate.name}</div>
<div className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{[candidate.set_name, candidate.set_code, candidate.card_number].filter(Boolean).join(' · ')}
</div>
</div>
</button>
))}
</div>
<button
type="button"
onClick={onNotInCatalog}
disabled={submittingReview}
className="mt-4 w-full py-2 rounded-lg text-sm font-medium disabled:opacity-50"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
>
{submittingReview ? 'Submitting…' : "My card isn't listed — send for review"}
</button>
<button
type="button"
onClick={onCancel}
className="mt-2 w-full py-2 rounded-lg border text-sm"
style={{ borderColor: 'var(--border)', color: 'var(--text-secondary)' }}
>
Cancel
</button>
</div>
</div>
);
}