Files
tfm_ainventory/frontend/components/DebugRotationPanel.tsx
Daniel Bedeleanu 9a87064dbe fix(design): replace all 40+ hardcoded colors with DESIGN.md semantic system
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>
2026-04-25 17:19:53 +03:00

394 lines
15 KiB
TypeScript
Raw 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.
'use client';
import { useState, useEffect, useRef } from 'react';
import { Item } from '@/lib/db';
import { X } from 'lucide-react';
interface DebugRotationPanelProps {
item: Item;
imageUrl: string;
originalPhotoPath?: string;
originalCropBounds?: { x: number; y: number; width: number; height: number };
originalRotation?: number;
imageProcessing?: any;
onClose: () => void;
}
export function DebugRotationPanel({
item,
imageUrl,
originalPhotoPath,
originalCropBounds,
originalRotation = 0,
imageProcessing,
onClose,
}: DebugRotationPanelProps) {
const [rotation, setRotation] = useState(originalRotation);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [log, setLog] = useState('');
const imageRef = useRef<HTMLImageElement | null>(null);
const [imageLoaded, setImageLoaded] = useState(false);
const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 });
const containerRef = useRef<HTMLDivElement>(null);
// Adjustable crop bounds (in original image coordinates)
const [adjustedCropBounds, setAdjustedCropBounds] = useState<{ x: number; y: number; width: number; height: number } | null>(null);
const [draggingState, setDraggingState] = useState<{ type: string; startX: number; startY: number; startBounds: any } | null>(null);
// Use original photo path if available, otherwise fall back to processed image URL
const displayImageUrl = originalPhotoPath ? originalPhotoPath : imageUrl;
const commonAngles = [
{ label: '0°', value: 0 },
{ label: '+22°', value: 22 },
{ label: '-22°', value: -22 },
{ label: '+45°', value: 45 },
{ label: '-45°', value: -45 },
{ label: '+90°', value: 90 },
{ label: '-90°', value: -90 },
{ label: '±180°', value: 180 },
];
const updateLog = (message: string) => {
setLog(message);
};
// Get the crop bounds to display (adjusted if user modified, otherwise original)
const displayCropBounds = adjustedCropBounds || originalCropBounds;
// Handle mouse down on canvas for dragging/resizing
const handleCanvasMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!canvasRef.current || !displayCropBounds) return;
const rect = canvasRef.current.getBoundingClientRect();
const mouseX = (e.clientX - rect.left) / (rect.width / scaledDimensions.width);
const mouseY = (e.clientY - rect.top) / (rect.height / scaledDimensions.height);
// Convert screen coords to original image coords
const scale = scaledDimensions.scale;
const origX = mouseX / scale;
const origY = mouseY / scale;
const cropX = displayCropBounds.x;
const cropY = displayCropBounds.y;
const cropW = displayCropBounds.width;
const cropH = displayCropBounds.height;
const margin = 15; // pixels in original coords for resize handles
let dragType = '';
if (origX >= cropX - margin && origX <= cropX + margin && origY >= cropY - margin && origY <= cropY + margin) {
dragType = 'nw'; // NW corner
} else if (origX >= cropX + cropW - margin && origX <= cropX + cropW + margin && origY >= cropY - margin && origY <= cropY + margin) {
dragType = 'ne'; // NE corner
} else if (origX >= cropX - margin && origX <= cropX + margin && origY >= cropY + cropH - margin && origY <= cropY + cropH + margin) {
dragType = 'sw'; // SW corner
} else if (origX >= cropX + cropW - margin && origX <= cropX + cropW + margin && origY >= cropY + cropH - margin && origY <= cropY + cropH + margin) {
dragType = 'se'; // SE corner
} else if (origX >= cropX && origX <= cropX + cropW && origY >= cropY && origY <= cropY + cropH) {
dragType = 'move'; // Inside the box
}
if (dragType) {
setDraggingState({
type: dragType,
startX: origX,
startY: origY,
startBounds: { ...displayCropBounds },
});
}
};
const handleCanvasMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!draggingState || !canvasRef.current || !displayCropBounds) return;
const rect = canvasRef.current.getBoundingClientRect();
const mouseX = (e.clientX - rect.left) / (rect.width / scaledDimensions.width);
const mouseY = (e.clientY - rect.top) / (rect.height / scaledDimensions.height);
const scale = scaledDimensions.scale;
const origX = mouseX / scale;
const origY = mouseY / scale;
const dx = origX - draggingState.startX;
const dy = origY - draggingState.startY;
const startBounds = draggingState.startBounds;
let newBounds = { ...startBounds };
if (draggingState.type === 'move') {
newBounds.x = Math.max(0, startBounds.x + dx);
newBounds.y = Math.max(0, startBounds.y + dy);
} else if (draggingState.type === 'nw') {
newBounds.x = Math.max(0, startBounds.x + dx);
newBounds.y = Math.max(0, startBounds.y + dy);
newBounds.width = Math.max(50, startBounds.width - dx);
newBounds.height = Math.max(50, startBounds.height - dy);
} else if (draggingState.type === 'ne') {
newBounds.y = Math.max(0, startBounds.y + dy);
newBounds.width = Math.max(50, startBounds.width + dx);
newBounds.height = Math.max(50, startBounds.height - dy);
} else if (draggingState.type === 'sw') {
newBounds.x = Math.max(0, startBounds.x + dx);
newBounds.width = Math.max(50, startBounds.width - dx);
newBounds.height = Math.max(50, startBounds.height + dy);
} else if (draggingState.type === 'se') {
newBounds.width = Math.max(50, startBounds.width + dx);
newBounds.height = Math.max(50, startBounds.height + dy);
}
setAdjustedCropBounds(newBounds);
};
const handleCanvasMouseUp = () => {
setDraggingState(null);
};
useEffect(() => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
imageRef.current = img;
setImageLoaded(true);
// Calculate scaled dimensions to fit available space (much larger)
const maxWidth = 900;
const maxHeight = 600;
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
const scaledW = img.width * scale;
const scaledH = img.height * scale;
setScaledDimensions({ width: scaledW, height: scaledH, scale });
updateLog(`📸 ${img.width}×${img.height}px → scaled ${scaledW.toFixed(0)}×${scaledH.toFixed(0)}px (${scale.toFixed(2)}x)`);
};
img.onerror = () => updateLog('❌ Failed to load image');
img.src = displayImageUrl;
}, [displayImageUrl]);
useEffect(() => {
if (!imageLoaded || !imageRef.current || !canvasRef.current || !displayCropBounds) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const img = imageRef.current;
const { scale } = scaledDimensions;
// Set canvas size to match scaled image
canvas.width = scaledDimensions.width;
canvas.height = scaledDimensions.height;
// Draw scaled image
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Calculate crop box in scaled coordinates
const cropX = displayCropBounds.x * scale;
const cropY = displayCropBounds.y * scale;
const cropW = displayCropBounds.width * scale;
const cropH = displayCropBounds.height * scale;
// Save state
ctx.save();
// Draw orientation indicators at corners
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.font = 'bold 14px monospace';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText('⬆ UP', 10, 5);
ctx.fillText('⬅ LEFT', 5, 20);
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.fillText('RIGHT ➡', canvas.width - 10, 20);
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
ctx.fillText('⬇ DOWN', 10, canvas.height - 5);
// Draw crop bounds rectangle (bright green)
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 4;
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 3;
ctx.strokeRect(cropX, cropY, cropW, cropH);
// Draw edge orientation labels on crop box
ctx.fillStyle = '#00FF00';
ctx.font = 'bold 12px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 2;
// Top edge label
ctx.fillText('T', cropX + cropW / 2, cropY - 8);
// Bottom edge label
ctx.fillText('B', cropX + cropW / 2, cropY + cropH + 8);
// Left edge label
ctx.textAlign = 'right';
ctx.fillText('L', cropX - 8, cropY + cropH / 2);
// Right edge label
ctx.textAlign = 'left';
ctx.fillText('R', cropX + cropW + 8, cropY + cropH / 2);
// Draw resize handles at corners
const handleSize = 8;
ctx.fillStyle = '#FFB800';
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 2;
// NW, NE, SW, SE corners
ctx.fillRect(cropX - handleSize / 2, cropY - handleSize / 2, handleSize, handleSize);
ctx.fillRect(cropX + cropW - handleSize / 2, cropY - handleSize / 2, handleSize, handleSize);
ctx.fillRect(cropX - handleSize / 2, cropY + cropH - handleSize / 2, handleSize, handleSize);
ctx.fillRect(cropX + cropW - handleSize / 2, cropY + cropH - handleSize / 2, handleSize, handleSize);
// Draw rotation indicator (rotated rectangle inside crop area)
if (Math.abs(rotation) > 0.5) {
ctx.strokeStyle = '#FFB800';
ctx.lineWidth = 3;
ctx.setLineDash([5, 5]);
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 2;
// Draw a rotated rectangle showing where text will be after rotation
ctx.save();
ctx.translate(cropX + cropW / 2, cropY + cropH / 2);
ctx.rotate((rotation * Math.PI) / 180);
ctx.strokeRect(-cropW / 2, -cropH / 2, cropW, cropH);
// Add direction markers to rotated box
ctx.setLineDash([]);
ctx.fillStyle = '#FFB800';
ctx.font = 'bold 12px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('↑ TOP', 0, -cropH / 2 - 10);
ctx.fillText('↓ BTM', 0, cropH / 2 + 10);
ctx.restore();
}
ctx.restore();
// Update log with current state
let logText = `Rotation: ${rotation}°`;
if (adjustedCropBounds) {
logText += ` | Adjusted: (${adjustedCropBounds.x}, ${adjustedCropBounds.y}) ${adjustedCropBounds.width}×${adjustedCropBounds.height}px`;
} else {
logText += ` | AI Crop: (${displayCropBounds.x}, ${displayCropBounds.y}) ${displayCropBounds.width}×${displayCropBounds.height}px`;
}
updateLog(logText);
}, [imageLoaded, rotation, displayCropBounds, scaledDimensions, adjustedCropBounds]);
return (
<div className="fixed inset-0 bg-surface-container-lowest/80 flex items-center justify-center z-50 p-4">
<div className="bg-surface-container border border-border rounded-none shadow-none max-w-6xl w-full max-h-[95vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="p-3 border-b border-border flex items-center justify-between bg-surface-bright flex-shrink-0">
<h2 className="text-lg font-normal text-white">Debug Rotation & Crop</h2>
<button
onClick={onClose}
className="p-2 hover:bg-border rounded text-muted hover:text-white transition"
>
<X size={20} />
</button>
</div>
<div className="flex gap-4 p-4 flex-1 overflow-hidden">
{/* Left: Controls */}
<div className="w-48 bg-surface-bright p-3 rounded space-y-4 flex-shrink-0 overflow-y-auto">
{/* Rotation Slider */}
<div>
<label className="block text-sm font-normal text-white mb-2">
Rotation: <span className="text-yellow-400">{rotation}°</span>
</label>
<input
type="range"
min="-180"
max="180"
value={rotation}
onChange={(e) => setRotation(Number(e.target.value))}
className="w-full"
/>
</div>
{/* Quick Presets */}
<div>
<label className="block text-sm font-normal text-white mb-2">Quick Angles</label>
<div className="grid grid-cols-2 gap-1">
{commonAngles.map((angle) => (
<button
key={angle.value}
onClick={() => setRotation(angle.value)}
className={`py-1 px-2 rounded text-xs font-normal transition ${
rotation === angle.value
? 'bg-yellow-600 text-white'
: 'bg-border text-secondary hover:bg-surface-bright'
}`}
>
{angle.label}
</button>
))}
</div>
</div>
{/* Reset Button */}
<button
onClick={() => setAdjustedCropBounds(null)}
disabled={!adjustedCropBounds}
className="w-full py-2 px-3 rounded text-sm font-normal bg-border text-secondary hover:bg-surface-bright disabled:opacity-50 disabled:cursor-not-allowed transition"
>
Reset Crop Box
</button>
{/* Original Values */}
{originalRotation !== undefined && (
<div className="bg-border p-3 rounded text-xs space-y-1">
<h3 className="font-normal text-white">Original AI Values</h3>
<div className="text-secondary font-mono text-xs">
<div>Rotation: {originalRotation}°</div>
{originalCropBounds && (
<>
<div>Crop ({originalCropBounds.x}, {originalCropBounds.y})</div>
<div>Size {originalCropBounds.width}×{originalCropBounds.height}</div>
</>
)}
{imageProcessing?.confidence && (
<div>Confidence: {(imageProcessing.confidence * 100).toFixed(0)}%</div>
)}
</div>
</div>
)}
</div>
{/* Right: Canvas + Log */}
<div className="flex-1 flex flex-col gap-3 min-w-0 overflow-hidden">
{/* Canvas - takes most space */}
<div className="flex-1 flex flex-col bg-surface-bright p-3 rounded border border-border overflow-hidden">
<h3 className="text-xs font-normal text-white mb-2">Preview (Green = Crop, Orange = Rotation)</h3>
<div className="flex-1 flex items-center justify-center bg-surface-container-lowest/70 rounded border border-outline overflow-auto">
<canvas
ref={canvasRef}
className="rounded cursor-move"
onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp}
onMouseLeave={handleCanvasMouseUp}
/>
</div>
</div>
{/* Log - single line */}
<div className="bg-surface-bright p-2 rounded border border-border flex-shrink-0">
<div className="bg-surface-container-lowest text-tertiary p-2 rounded-none font-mono text-xs">
{log}
</div>
</div>
</div>
</div>
</div>
</div>
);
}