deckhearth/components/scanner/ScannerToast.js
Randall Stillwell cf9fea0726 feat(scanner): rebuild as mobile-first three-phase flow
Replace the desktop-first, everything-at-once scanner layout with a
phased mobile-optimized experience: Setup → Scanning → Review.

Phase 1 (Setup): destination picker, game filter, deck mode toggle,
scan history (last 5 sessions).

Phase 2 (Scanning): full-screen camera with auto-start, haptic + sound
feedback on card detection, torch/flash toggle, count pill, bottom-sheet
disambiguation (replaces full-screen modal).

Phase 3 (Review): card list with inline condition/foil/qty edits,
batch confirm, 30-second undo, deck progress indicator.

New features:
- Deck mode (progress toward 40/60/99 card target)
- Scan history (persisted to localStorage)
- Sound feedback (Web Audio oscillator, configurable)
- Offline queue (localStorage persistence + auto-retry on reconnect)
- Camera flash/torch toggle
- Batch ownership API (replaces N+1 per-card fetches)
- Visibility pause (detection loop stops when tab is backgrounded)

Convoy: scanner-rebuild
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 08:42:58 -05:00

58 lines
1.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const ICON_MAP = {
success: { glyph: '✓', color: 'var(--color-success, #10B981)' },
error: { glyph: '✗', color: 'var(--color-error, #EF4444)' },
info: { glyph: '', color: 'var(--color-info, #3B82F6)' },
};
/**
* Floating toast overlay for the camera viewport.
*
* Always rendered in the DOM so CSS transitions work on enter and exit.
* The parent controls visibility via the `visible` prop and handles the
* auto-dismiss timer.
*/
export default function ScannerToast({ message, visible, type = 'success' }) {
const { glyph, color } = ICON_MAP[type] || ICON_MAP.success;
return (
<div
role="status"
aria-live="polite"
className="absolute left-1/2 z-20 pointer-events-none"
style={{
bottom: '33%',
transform: `translateX(-50%) translateY(${visible ? '0' : '12px'})`,
opacity: visible ? 1 : 0,
transition: 'opacity 200ms ease, transform 200ms ease',
visibility: visible ? 'visible' : 'hidden',
transitionProperty: 'opacity, transform, visibility',
transitionDelay: visible ? '0ms' : '0ms, 0ms, 200ms',
}}
>
<div
className="flex items-center gap-2 rounded-full px-4 py-3 shadow-lg pointer-events-auto"
style={{
backgroundColor: 'rgba(var(--bg-secondary-rgb), 0.9)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
border: '1px solid var(--border)',
maxWidth: 280,
}}
>
<span
className="text-sm font-bold flex-shrink-0"
style={{ color }}
aria-hidden="true"
>
{glyph}
</span>
<span
className="text-sm font-medium truncate"
style={{ color: 'var(--text-primary)' }}
>
{message}
</span>
</div>
</div>
);
}