deckhearth/components/scanner/ScannerToast.js
varutasu 73424aae59
Mobile scanner checkout: scan first, commit later (#157)
* Start scanner-mobile-checkout convoy for the cart-then-commit phone flow.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Ship a cart-then-commit mobile scanner so phone sessions stay on the camera.

Scan matches enqueue locally instead of auto-writing ownership, checkout happens in a sheet, and audit fixes cover stale commit detection, returnUrl open redirects, nested Escape, and ember detection chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 20:20:43 -05:00

116 lines
2.9 KiB
JavaScript

function ToastIcon({ type }) {
if (type === 'error') {
return (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
);
}
if (type === 'info') {
return (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
);
}
return (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M20 6L9 17l-5-5" />
</svg>
);
}
const ICON_COLORS = {
success: 'var(--color-success)',
error: 'var(--color-error)',
info: 'var(--color-info)',
};
/**
* 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 color = ICON_COLORS[type] || ICON_COLORS.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="flex-shrink-0"
style={{ color }}
aria-hidden="true"
>
<ToastIcon type={type} />
</span>
<span
className="text-sm font-medium truncate"
style={{ color: 'var(--text-primary)' }}
>
{message}
</span>
</div>
</div>
);
}