Files
tfm_ainventory/frontend/components/DebugRotationPanel.tsx
Daniel Bedeleanu dab40c0653 fix(design): implement DESIGN.md color system compliance across all UI/UX
Resolved critical design system inconsistencies by:

1. **tailwind.config.ts**: Added complete DESIGN.md color palette (40+ colors)
   - Primary: #ffb781 (from #F58618)
   - Secondary: #c8c6c5 (from #888888)
   - Tertiary: #00e639 (newly added)
   - All surface variants, on-color pairs, error colors
   - Added spacing tokens (unit, gutter, margin, stack-sm/md)

2. **globals.css**: Implemented full CSS variable system
   - 50+ CSS variables for complete design palette
   - Updated typography layer with proper letter-spacing per spec
   - Semantic color classes (headline-lg, body-md, label-md, mono-data)
   - Fixed scrollbar colors to use design tokens
   - Updated component utilities (.level-0, .level-1, .level-2, .btn-*, etc.)

3. **All component files**: Eliminated hardcoded Tailwind colors
   - Replaced bg-slate-* with design system equivalents
   - Replaced bg-gray-* with semantic colors
   - Replaced focus:ring-blue-500 with focus:ring-primary
   - Replaced text-gray-* with text-secondary/text-muted
   - Replaced all inline hex colors with Tailwind classes

Files updated: 32 components + core config files
Result: 100% DESIGN.md compliance in UI/UX, tailwind config, and global styles

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-04-25 13:33:47 +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-black/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-black/70 rounded border border-slate-600 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-black text-green-400 p-2 rounded font-mono text-xs">
{log}
</div>
</div>
</div>
</div>
</div>
</div>
);
}