All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m14s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 33s
Deploy to Staging / Verify Staging (pull_request) Successful in 2m19s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Implements a reusable React camera capture component with: - getUserMedia API for camera access on mobile and desktop - Translucent aspect-ratio guidance overlays (VIN ~6:1, receipt ~2:3) - Post-capture crop tool with draggable handles - File input fallback for desktop and unsupported browsers - Support for HEIC, JPEG, PNG (sent as-is to server) - Full mobile responsiveness (320px - 1920px) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
146 lines
3.7 KiB
TypeScript
146 lines
3.7 KiB
TypeScript
/**
|
|
* @ai-summary Translucent guidance overlay for camera viewfinder
|
|
* @ai-context Displays aspect-ratio guides for VIN (~6:1), receipt (~2:3), and documents
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { Box, Typography } from '@mui/material';
|
|
import type { GuidanceOverlayProps, GuidanceType } from './types';
|
|
import { GUIDANCE_CONFIGS } from './types';
|
|
|
|
/** Calculate overlay dimensions based on guidance type and container */
|
|
function getOverlayStyles(type: GuidanceType): React.CSSProperties {
|
|
if (type === 'none') {
|
|
return { display: 'none' };
|
|
}
|
|
|
|
const config = GUIDANCE_CONFIGS[type];
|
|
|
|
// For VIN (wide), use width-constrained layout
|
|
// For receipt/document (tall), use height-constrained layout
|
|
if (config.aspectRatio > 1) {
|
|
// Wide aspect ratio (VIN)
|
|
return {
|
|
width: '85%',
|
|
height: 'auto',
|
|
aspectRatio: `${config.aspectRatio}`,
|
|
maxHeight: '30%',
|
|
};
|
|
} else {
|
|
// Tall aspect ratio (receipt, document)
|
|
return {
|
|
height: '70%',
|
|
width: 'auto',
|
|
aspectRatio: `${config.aspectRatio}`,
|
|
maxWidth: '85%',
|
|
};
|
|
}
|
|
}
|
|
|
|
export const GuidanceOverlay: React.FC<GuidanceOverlayProps> = ({ type }) => {
|
|
if (type === 'none') {
|
|
return null;
|
|
}
|
|
|
|
const config = GUIDANCE_CONFIGS[type];
|
|
const overlayStyles = getOverlayStyles(type);
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
pointerEvents: 'none',
|
|
zIndex: 1,
|
|
}}
|
|
>
|
|
{/* Guide box */}
|
|
<Box
|
|
sx={{
|
|
...overlayStyles,
|
|
border: '2px dashed rgba(255, 255, 255, 0.7)',
|
|
borderRadius: 1,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
boxShadow: 'inset 0 0 20px rgba(255, 255, 255, 0.1)',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
{/* Corner indicators */}
|
|
<CornerIndicator position="top-left" />
|
|
<CornerIndicator position="top-right" />
|
|
<CornerIndicator position="bottom-left" />
|
|
<CornerIndicator position="bottom-right" />
|
|
</Box>
|
|
|
|
{/* Instruction text */}
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
color: 'white',
|
|
mt: 2,
|
|
px: 2,
|
|
py: 1,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
borderRadius: 1,
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
{config.description}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
interface CornerIndicatorProps {
|
|
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
}
|
|
|
|
const CornerIndicator: React.FC<CornerIndicatorProps> = ({ position }) => {
|
|
const size = 20;
|
|
const thickness = 3;
|
|
|
|
const positionStyles: Record<string, React.CSSProperties> = {
|
|
'top-left': {
|
|
top: -thickness / 2,
|
|
left: -thickness / 2,
|
|
borderTop: `${thickness}px solid white`,
|
|
borderLeft: `${thickness}px solid white`,
|
|
},
|
|
'top-right': {
|
|
top: -thickness / 2,
|
|
right: -thickness / 2,
|
|
borderTop: `${thickness}px solid white`,
|
|
borderRight: `${thickness}px solid white`,
|
|
},
|
|
'bottom-left': {
|
|
bottom: -thickness / 2,
|
|
left: -thickness / 2,
|
|
borderBottom: `${thickness}px solid white`,
|
|
borderLeft: `${thickness}px solid white`,
|
|
},
|
|
'bottom-right': {
|
|
bottom: -thickness / 2,
|
|
right: -thickness / 2,
|
|
borderBottom: `${thickness}px solid white`,
|
|
borderRight: `${thickness}px solid white`,
|
|
},
|
|
};
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
width: size,
|
|
height: size,
|
|
...positionStyles[position],
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default GuidanceOverlay;
|