musealpha / uiprototype2 /src /components /BrowserPanel.tsx
asdf98's picture
Upload 112 files
3d7d9b5 verified
import { X, ArrowLeft, ArrowRight, RotateCw, Plus, Globe, Search, Maximize2, Minimize2, ShieldCheck, Lock, Camera, Scissors } from 'lucide-react';
import { useAppStore } from '../store';
import { useState, useRef } from 'react';
import { toPng } from 'html-to-image';
const mockWebImages = [
"https://picsum.photos/id/10/800/600",
"https://picsum.photos/id/11/600/800",
"https://picsum.photos/id/12/800/800",
"https://picsum.photos/id/13/400/600",
"https://picsum.photos/id/14/600/400",
"https://picsum.photos/id/15/800/500",
"https://picsum.photos/id/16/500/800",
"https://picsum.photos/id/17/700/700",
];
const bookmarks = [
{ name: "ArtStation", icon: "🎨" },
{ name: "Pinterest", icon: "📌" },
{ name: "Unsplash", icon: "📷" },
{ name: "Sketchfab", icon: "📦" },
{ name: "Google Images", icon: "🔍" },
{ name: "Anatomy360", icon: "🦴" },
{ name: "Line of Action", icon: "🏃" },
{ name: "SculptRef", icon: "🗿" },
{ name: "PolyHaven", icon: "🌍" },
{ name: "reference.pictures", icon: "🖼" },
];
export const BrowserPanel = () => {
const { isBrowserOpen, setIsBrowserOpen, setImages, pan, zoom } = useAppStore();
const [url, setUrl] = useState("https://artstation.com/search");
const [isFullscreen, setIsFullscreen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const browserContentRef = useRef<HTMLDivElement>(null);
const browserVisibleRef = useRef<HTMLDivElement>(null);
const [isCapturing, setIsCapturing] = useState(false);
const [isSnipping, setIsSnipping] = useState(false);
const [snipStart, setSnipStart] = useState<{x: number, y: number} | null>(null);
const [snipCurrent, setSnipCurrent] = useState<{x: number, y: number} | null>(null);
const handlePointerDown = (e: React.PointerEvent) => {
if (!isSnipping) return;
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setSnipStart({ x, y });
setSnipCurrent({ x, y });
};
const handlePointerMove = (e: React.PointerEvent) => {
if (!isSnipping || !snipStart) return;
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setSnipCurrent({ x, y });
};
const handlePointerUp = async (e: React.PointerEvent) => {
if (!isSnipping || !snipStart || !snipCurrent) {
setIsSnipping(false);
setSnipStart(null);
setSnipCurrent(null);
return;
}
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
const endX = e.clientX - rect.left;
const endY = e.clientY - rect.top;
const startX = Math.min(snipStart.x, endX);
const startY = Math.min(snipStart.y, endY);
const width = Math.abs(endX - snipStart.x);
const height = Math.abs(endY - snipStart.y);
setIsSnipping(false);
setSnipStart(null);
setSnipCurrent(null);
if (width < 20 || height < 20) return; // ignore tiny clicks
setIsCapturing(true);
try {
if (!browserVisibleRef.current) return;
const dataUrl = await toPng(browserVisibleRef.current, { cacheBust: true, backgroundColor: '#1C1C1E', pixelRatio: 1 });
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = Math.round(width);
canvas.height = Math.round(height);
const ctx = canvas.getContext('2d');
if (!ctx) return;
// draw specific portion of viewport to canvas
ctx.drawImage(
img,
Math.round(startX), Math.round(startY), Math.round(width), Math.round(height),
0, 0, Math.round(width), Math.round(height)
);
const croppedDataUrl = canvas.toDataURL('image/png');
const ratio = width / height;
const targetWidth = Math.min(width, 800);
const newImg = {
id: Math.random().toString(36).substr(2, 9),
url: croppedDataUrl,
x: (-pan.x + window.innerWidth / 2 - targetWidth / 2) / zoom,
y: (-pan.y + window.innerHeight / 2 - (targetWidth / ratio) / 2) / zoom,
width: Math.round(targetWidth),
height: Math.round(targetWidth / ratio),
aspectRatio: ratio,
};
setImages(prev => [...prev, newImg]);
};
img.src = dataUrl;
} catch (err) {
console.error('Failed to capture web page snip', err);
} finally {
setIsCapturing(false);
}
};
const handleScreenshot = async () => {
if (!browserContentRef.current || isCapturing) return;
setIsCapturing(true);
try {
const dataUrl = await toPng(browserContentRef.current, { cacheBust: true, backgroundColor: '#1C1C1E' });
const img = new Image();
img.src = dataUrl;
img.onload = () => {
const ratio = img.width / img.height;
const targetWidth = Math.min(800, img.width);
const targetHeight = targetWidth / ratio;
const newImg = {
id: Math.random().toString(36).substr(2, 9),
url: dataUrl,
x: (-pan.x + window.innerWidth / 4) / zoom,
y: (-pan.y + window.innerHeight / 4) / zoom,
width: targetWidth,
height: targetHeight,
aspectRatio: ratio,
};
setImages(prev => [...prev, newImg]);
};
} catch (err) {
console.error('Failed to capture web page screenshot', err);
} finally {
setIsCapturing(false);
}
};
const handleCapture = (src: string) => {
const img = new Image();
img.src = src;
img.onload = () => {
const ratio = img.width / img.height;
const targetWidth = Math.min(400, img.width);
const targetHeight = targetWidth / ratio;
const newImg = {
id: Math.random().toString(36).substr(2, 9),
url: src,
x: (-pan.x + window.innerWidth / 4) / zoom,
y: (-pan.y + window.innerHeight / 4) / zoom,
width: targetWidth,
height: targetHeight,
aspectRatio: ratio,
};
setImages(prev => [...prev, newImg]);
};
};
return (
<div className={`absolute right-0 top-0 h-full bg-panel-bg shadow-2xl flex flex-col z-[60] transform transition-all duration-500 ease-[cubic-bezier(0.19,1,0.22,1)] ${isBrowserOpen ? 'translate-x-0' : 'translate-x-full'} ${isFullscreen ? 'w-[100vw] max-w-[100vw]' : 'w-[50vw] max-w-[600px]'}`}>
<div className="flex flex-col bg-panel-bg z-10 px-4 pt-4 pb-2">
<div className="flex items-center gap-2 mb-3">
<div className="flex items-center gap-0.5">
<button className="text-ui-secondary hover:text-ui-primary w-8 h-8 flex items-center justify-center rounded-md hover:bg-white/5 transition-colors"><ArrowLeft size={16} /></button>
<button className="text-ui-secondary hover:text-ui-primary w-8 h-8 flex items-center justify-center rounded-md hover:bg-white/5 transition-colors"><ArrowRight size={16} /></button>
<button className="text-ui-secondary hover:text-ui-primary w-8 h-8 flex items-center justify-center rounded-md hover:bg-white/5 transition-colors"><RotateCw size={14} /></button>
</div>
<div className="flex-1 relative flex items-center group bg-black/40 rounded-lg border border-white/5 focus-within:border-accent-blue/40 focus-within:bg-black/60 transition-all">
<div className="absolute left-3 flex items-center gap-1.5 text-ui-secondary group-focus-within:text-accent-blue transition-colors pointer-events-none">
<ShieldCheck size={13} className="text-emerald-500" />
<Lock size={12} className="opacity-70" />
</div>
<input
ref={inputRef}
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
className="w-full bg-transparent text-ui-primary pl-14 pr-8 py-2 text-[13px] outline-none placeholder:text-ui-secondary"
spellCheck={false}
/>
{url && (
<button onClick={() => { setUrl(''); inputRef.current?.focus(); }} className="absolute right-2.5 text-ui-secondary hover:text-ui-primary"><X size={14} /></button>
)}
</div>
<div className="flex items-center gap-0.5 ml-1">
<button onClick={() => setIsSnipping(!isSnipping)} disabled={isCapturing} className={`text-ui-secondary hover:text-accent-amber w-8 h-8 flex items-center justify-center rounded-md hover:bg-white/5 transition-colors ${isSnipping ? 'bg-accent-amber/20 text-accent-amber' : ''} ${isCapturing ? 'opacity-50 cursor-not-allowed' : ''}`} title="Area Snip">
<Scissors size={16} />
</button>
<button onClick={handleScreenshot} disabled={isCapturing} className={`text-ui-secondary hover:text-accent-blue w-8 h-8 flex items-center justify-center rounded-md hover:bg-white/5 transition-colors ${isCapturing ? 'opacity-50 cursor-not-allowed' : ''}`} title="Full Web Clip">
<Camera size={16} className={isCapturing && !isSnipping ? "animate-pulse" : ""} />
</button>
<button onClick={() => setIsFullscreen(!isFullscreen)} className="text-ui-secondary hover:text-ui-primary w-8 h-8 flex items-center justify-center rounded-md hover:bg-white/5 transition-colors" title={isFullscreen ? "Minimize" : "Maximize"}>
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</button>
<button onClick={() => setIsBrowserOpen(false)} className="text-ui-secondary hover:text-ui-primary w-8 h-8 flex items-center justify-center rounded-md hover:bg-white/5 transition-colors"><X size={18} /></button>
</div>
</div>
<div className="flex items-center gap-2 overflow-x-auto hide-scrollbar pb-2">
{bookmarks.map((bm, i) => (
<button key={i} className="flex items-center gap-1.5 px-3 py-1.5 bg-white/5 hover:bg-white/10 border border-white/5 rounded-md text-[11px] font-medium text-ui-primary whitespace-nowrap transition-colors shadow-sm">
<span>{bm.icon}</span>
<span>{bm.name}</span>
</button>
))}
<button className="flex items-center justify-center w-7 h-7 rounded-md bg-white/5 hover:bg-white/10 text-ui-secondary transition-colors"><Plus size={14}/></button>
</div>
</div>
<div ref={browserVisibleRef} className="flex-1 relative flex flex-col min-h-0 overflow-hidden">
<div ref={browserContentRef} className="flex-1 p-4 bg-[#1C1C1E] overflow-y-auto relative custom-scrollbar">
{/* Mock Web View */}
<div className="columns-2 md:columns-3 xl:columns-4 gap-4 space-y-4">
{mockWebImages.map((src, i) => (
<button key={i} onClick={() => handleCapture(src)} className="block w-full relative group rounded-xl overflow-hidden ring-1 ring-white/5 hover:ring-white/20 transition-all shadow-sm hover:shadow-lg cursor-pointer text-left focus:outline-none focus:ring-2 focus:ring-accent-blue">
<img src={src} className="w-full block transform transition-transform duration-700 ease-out group-hover:scale-105" />
<div className="absolute inset-0 bg-gradient-to-b from-black/0 via-black/0 to-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end justify-center p-4">
<div className="bg-white text-black text-[11px] font-bold px-3 py-1.5 rounded-full flex items-center gap-1 shadow-2xl transform translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
<Plus size={14} strokeWidth={2.5} /> ADD
</div>
</div>
</button>
))}
</div>
</div>
{isSnipping && (
<div
className="absolute inset-0 z-50 cursor-crosshair touchscreen-none"
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
style={{ backgroundColor: 'rgba(0,0,0,0.15)' }}
>
{snipStart && snipCurrent && (
<div
className="absolute border focus:outline-none"
style={{
left: Math.min(snipStart.x, snipCurrent.x),
top: Math.min(snipStart.y, snipCurrent.y),
width: Math.abs(snipCurrent.x - snipStart.x),
height: Math.abs(snipCurrent.y - snipStart.y),
borderStyle: 'dashed',
borderColor: 'rgba(255, 255, 255, 0.8)',
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.4)',
}}
></div>
)}
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-accent-amber text-black text-xs font-bold px-3 py-1.5 rounded-full shadow-lg pointer-events-none">
Drag to capture area
</div>
</div>
)}
</div>
</div>
);
};