MophongLT / frontend /src /components /plugin /FullscreenCircuitViewer.js
gaialive's picture
Upload 170 files
227c43a verified
import React, { useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, ZoomIn, ZoomOut, RotateCcw, Maximize2 } from 'lucide-react';
import PropTypes from 'prop-types';
const FullscreenCircuitViewer = ({ circuitSvg, isOpen, onClose }) => {
const [zoom, setZoom] = useState(1);
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const [scrollPos, setScrollPos] = useState({ x: 0, y: 0 });
const containerRef = useRef(null);
const handleZoomIn = () => setZoom(prev => Math.min(prev + 0.3, 5));
const handleZoomOut = () => setZoom(prev => Math.max(prev - 0.3, 0.3));
const handleResetZoom = () => setZoom(1);
// Keyboard support
React.useEffect(() => {
const handleKeyDown = (e) => {
if (!isOpen) return;
switch (e.key) {
case 'Escape':
onClose();
break;
case '+':
case '=':
e.preventDefault();
handleZoomIn();
break;
case '-':
e.preventDefault();
handleZoomOut();
break;
case '0':
e.preventDefault();
handleResetZoom();
break;
default:
break;
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
const handleMouseDown = (e) => {
if (e.target.closest('.zoom-controls')) return;
setIsDragging(true);
setDragStart({ x: e.clientX, y: e.clientY });
if (containerRef.current) {
setScrollPos({
x: containerRef.current.scrollLeft,
y: containerRef.current.scrollTop
});
}
};
const handleMouseMove = (e) => {
if (!isDragging || !containerRef.current) return;
const deltaX = e.clientX - dragStart.x;
const deltaY = e.clientY - dragStart.y;
containerRef.current.scrollLeft = scrollPos.x - deltaX;
containerRef.current.scrollTop = scrollPos.y - deltaY;
};
const handleMouseUp = () => {
setIsDragging(false);
};
if (!isOpen || !circuitSvg) return null;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 bg-black/90 backdrop-blur-sm flex items-center justify-center"
onClick={onClose}
>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
onClick={(e) => e.stopPropagation()}
className="w-full h-full max-w-7xl max-h-[90vh] bg-white dark:bg-neutral-900 rounded-lg shadow-2xl overflow-hidden m-4"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800">
<div>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Quantum Circuit Diagram
</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Full-screen view with advanced zoom and pan controls
</p>
</div>
{/* Controls */}
<div className="flex items-center gap-2">
<div className="zoom-controls flex items-center gap-1 bg-white dark:bg-neutral-800 rounded-lg p-1 border border-neutral-200 dark:border-neutral-600">
<button
onClick={handleZoomOut}
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded text-neutral-600 dark:text-neutral-400 transition-colors"
title="Zoom Out"
>
<ZoomOut size={16} />
</button>
<span className="text-sm text-neutral-600 dark:text-neutral-400 min-w-[4rem] text-center px-2">
{Math.round(zoom * 100)}%
</span>
<button
onClick={handleZoomIn}
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded text-neutral-600 dark:text-neutral-400 transition-colors"
title="Zoom In"
>
<ZoomIn size={16} />
</button>
<button
onClick={handleResetZoom}
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded text-neutral-600 dark:text-neutral-400 transition-colors"
title="Reset Zoom"
>
<RotateCcw size={16} />
</button>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded text-neutral-600 dark:text-neutral-400 transition-colors"
title="Close Fullscreen"
>
<X size={20} />
</button>
</div>
</div>
{/* Circuit Container */}
<div
ref={containerRef}
className="flex-1 overflow-auto bg-neutral-50 dark:bg-neutral-900 cursor-grab active:cursor-grabbing"
style={{
height: 'calc(100% - 80px)',
scrollbarWidth: 'thin',
scrollbarColor: '#cbd5e1 #f1f5f9'
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<div className="flex items-center justify-center min-h-full p-8">
<div
dangerouslySetInnerHTML={{ __html: circuitSvg }}
className="transition-transform duration-200 bg-white dark:bg-neutral-800 rounded-lg p-6 shadow-lg"
style={{
transform: `scale(${zoom})`,
transformOrigin: 'center center'
}}
/>
</div>
</div>
{/* Footer with instructions */}
<div className="p-3 bg-neutral-50 dark:bg-neutral-800 border-t border-neutral-200 dark:border-neutral-700">
<div className="flex items-center justify-center gap-6 text-xs text-neutral-500 dark:text-neutral-400">
<span className="flex items-center gap-1">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M9 11H1m22 0h-8M9 11l3-3m-3 3l3 3"/>
</svg>
Drag to pan around the circuit
</span>
<span className="flex items-center gap-1">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
Use zoom controls or mouse wheel
</span>
<span className="flex items-center gap-1">
<kbd className="px-1 py-0.5 bg-neutral-200 dark:bg-neutral-700 rounded text-xs">Esc</kbd>
Close fullscreen
</span>
</div>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
};
FullscreenCircuitViewer.propTypes = {
circuitSvg: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
};
export default FullscreenCircuitViewer;