Phase 10 Implementation: Bulk color system compliance across frontend Applied DESIGN_COLOR_RULES.md to 40 component and page files: - Indigo (3) → primary/secondary (primary actions) - Green (12) → tertiary (#00e639) (success/healthy status) - Red/Rose (20) → error (#ffb4ab) (destructive actions) - Amber/Sky (9) → primary/secondary (warnings/info) - Blue/Slate (various) → primary/design system colors (focus states) - Black → bg-surface-container-lowest (proper design color) Files updated: 40 components + pages Color replacements: 44+ instances Build status: ✓ Zero errors, compiled in 6.3s Test status: Pending (npm run test) All colors now follow DESIGN.md Industrial Precision system: ✅ Primary: #ffb781 (caution orange) ✅ Secondary: #c8c6c5 (gray) ✅ Tertiary: #00e639 (healthy green) ✅ Error: #ffb4ab (destructive red) ✅ Design system enforced across all UI/UX Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
202 lines
6.9 KiB
TypeScript
202 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
X,
|
|
RotateCcw,
|
|
FlipHorizontal,
|
|
Camera,
|
|
Zap,
|
|
Search,
|
|
CheckCircle2,
|
|
ZoomIn,
|
|
ZoomOut
|
|
} from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface CameraViewProps {
|
|
scannerId: string;
|
|
isStarted: boolean;
|
|
paused: boolean;
|
|
onClose: () => void;
|
|
onRotate: () => void;
|
|
onFlip: () => void;
|
|
onFlash: () => void;
|
|
flashOn: boolean;
|
|
onZoomChange: (zoom: number) => void;
|
|
countdown: number | null;
|
|
ocrProcessing: boolean;
|
|
isSelecting: boolean;
|
|
capturedImage: string | null;
|
|
detectedWords: Array<{text: string, x: number, y: number, w: number, h: number}>;
|
|
onScan: () => void;
|
|
onWordSelect: (text: string) => void;
|
|
onCancelSelection: () => void;
|
|
}
|
|
|
|
export default function CameraView({
|
|
scannerId,
|
|
isStarted,
|
|
paused,
|
|
onClose,
|
|
onRotate,
|
|
onFlip,
|
|
onFlash,
|
|
flashOn,
|
|
onZoomChange,
|
|
countdown,
|
|
ocrProcessing,
|
|
isSelecting,
|
|
capturedImage,
|
|
detectedWords,
|
|
onScan,
|
|
onWordSelect,
|
|
onCancelSelection,
|
|
}: CameraViewProps) {
|
|
return (
|
|
<div className="w-full max-w-md mx-auto flex flex-col gap-4">
|
|
{/* Video Viewport Area */}
|
|
<div className="relative w-full aspect-square overflow-hidden level-1 p-6 sm:p-8">
|
|
<div className="absolute inset-6 z-10 pointer-events-none flex items-center justify-center">
|
|
<div className="w-full h-full border border-primary/30 relative">
|
|
<div className="absolute top-0 left-0 w-10 h-10 border-t-4 border-l-4 border-primary" />
|
|
<div className="absolute top-0 right-0 w-10 h-10 border-t-4 border-r-4 border-primary" />
|
|
<div className="absolute bottom-0 left-0 w-10 h-10 border-b-4 border-l-4 border-primary" />
|
|
<div className="absolute bottom-0 right-0 w-10 h-10 border-b-4 border-r-4 border-primary" />
|
|
|
|
{isStarted && !paused && !isSelecting && (
|
|
<div className="absolute top-0 left-0 right-0 h-1 bg-primary animate-scan-fast" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div id={scannerId} data-testid="camera-indicator" className="w-full h-full bg-surface-container-lowest object-cover" />
|
|
|
|
{/* Selection UI */}
|
|
{isSelecting && capturedImage && (
|
|
<div className="absolute inset-0 z-50 bg-surface-container-lowest flex flex-col">
|
|
<div className="relative flex-1 bg-surface-container-lowest flex items-center justify-center overflow-hidden">
|
|
<img
|
|
src={capturedImage || undefined}
|
|
className="max-w-full max-h-full object-contain"
|
|
id="ocr-canvas-preview"
|
|
alt="OCR text detection"
|
|
/>
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<div className="relative" style={{ width: '100%', height: '100%' }}>
|
|
{detectedWords.map((w, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={() => onWordSelect(w.text)}
|
|
className="absolute border border-primary bg-primary/20 hover:bg-primary/40 active:bg-primary/50 cursor-pointer transition-colors"
|
|
style={{
|
|
left: `${w.x}%`,
|
|
top: `${w.y}%`,
|
|
width: `${w.w}%`,
|
|
height: `${w.h}%`
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 bg-surface-container border-t border-border flex justify-between items-center">
|
|
<span className="text-xs text-secondary font-normal">Extract Detected Signatures</span>
|
|
<button
|
|
onClick={onCancelSelection}
|
|
className="px-4 py-2 bg-surface-bright text-white text-xs border border-border"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Countdown */}
|
|
{countdown !== null && (
|
|
<div className="absolute inset-0 z-40 bg-surface-container-lowest/40 flex items-center justify-center">
|
|
<span className="text-8xl font-normal text-white animate-ping">{countdown}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Vision Status */}
|
|
{ocrProcessing && (
|
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 flex flex-col items-center gap-2">
|
|
<div className="w-12 h-12 border-4 border-primary/20 border-t-primary animate-spin" />
|
|
<span className="text-xs text-primary bg-surface-container-lowest/80 px-3 py-1 border border-primary/20">Analyzing Surface Protocols...</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Camera Controls */}
|
|
<div className="grid grid-cols-4 gap-2 no-print">
|
|
<button
|
|
onClick={onFlash}
|
|
className={cn(
|
|
"p-4 bg-surface-container border border-border flex items-center justify-center transition-all",
|
|
flashOn ? "text-primary border-primary/50" : "text-secondary hover:text-white"
|
|
)}
|
|
title="Toggle Flash"
|
|
>
|
|
<Zap size={20} fill={flashOn ? "currentColor" : "none"} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onRotate}
|
|
className="p-4 bg-surface-container border border-border text-secondary hover:text-white flex items-center justify-center transition-all"
|
|
title="Rotate Camera"
|
|
>
|
|
<RotateCcw size={20} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onFlip}
|
|
className="p-4 bg-surface-container border border-border text-secondary hover:text-white flex items-center justify-center transition-all"
|
|
title="Flip Horizontal"
|
|
>
|
|
<FlipHorizontal size={20} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={onClose}
|
|
className="p-4 bg-surface-container border border-border text-secondary hover:text-error flex items-center justify-center transition-all"
|
|
title="Close Camera"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Zoom Controls */}
|
|
<div className="flex items-center gap-4 px-2 no-print">
|
|
<ZoomOut size={16} className="text-secondary" />
|
|
<input
|
|
type="range"
|
|
min="1"
|
|
max="5"
|
|
step="0.1"
|
|
onChange={(e) => onZoomChange(parseFloat(e.target.value))}
|
|
className="flex-1 h-1 bg-border appearance-none cursor-pointer accent-primary"
|
|
/>
|
|
<ZoomIn size={16} className="text-secondary" />
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="grid grid-cols-2 gap-3 no-print">
|
|
<button
|
|
onClick={onScan}
|
|
disabled={paused || ocrProcessing}
|
|
className="btn-primary py-5 text-lg"
|
|
>
|
|
<Camera size={24} /> Capture Asset
|
|
</button>
|
|
<button
|
|
onClick={onScan}
|
|
className="btn-secondary py-5 text-lg"
|
|
>
|
|
<Search size={24} /> Scan Signature
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|