172 lines
5.4 KiB
TypeScript
172 lines
5.4 KiB
TypeScript
import React, { useRef, useState, useCallback } from 'react';
|
|
import clsx from 'clsx';
|
|
import { Sparkles, Maximize2, Minimize2, Minus, X } from 'lucide-react';
|
|
import { useWindowStore, type WindowState } from '@/store/windowStore';
|
|
import { AiChatPanel } from './AiChatPanel';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface WindowProps {
|
|
window: WindowState;
|
|
}
|
|
|
|
export function Window({ window: win }: WindowProps) {
|
|
const { t } = useTranslation();
|
|
const {
|
|
closeWindow,
|
|
minimizeWindow,
|
|
toggleFullscreen,
|
|
toggleAiChat,
|
|
setActiveWindow,
|
|
updateWindowPosition,
|
|
} = useWindowStore();
|
|
|
|
const headerRef = useRef<HTMLDivElement>(null);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const dragStart = useRef({ x: 0, y: 0, posX: 0, posY: 0 });
|
|
|
|
const ContentComponent = win.component;
|
|
|
|
const handleMouseDown = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
if (win.state === 'fullscreen') return;
|
|
// Only drag from header, not from buttons
|
|
if ((e.target as HTMLElement).closest('button')) return;
|
|
setIsDragging(true);
|
|
dragStart.current = {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
posX: win.position.x,
|
|
posY: win.position.y,
|
|
};
|
|
setActiveWindow(win.id);
|
|
},
|
|
[win.id, win.state, win.position, setActiveWindow]
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
if (!isDragging) return;
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const dx = e.clientX - dragStart.current.x;
|
|
const dy = e.clientY - dragStart.current.y;
|
|
const newX = dragStart.current.posX + dx;
|
|
const newY = Math.max(0, dragStart.current.posY + dy);
|
|
updateWindowPosition(win.id, { x: newX, y: newY });
|
|
};
|
|
const handleMouseUp = () => setIsDragging(false);
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
return () => {
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
}, [isDragging, win.id, updateWindowPosition]);
|
|
|
|
if (win.state === 'minimized') return null;
|
|
|
|
const isFullscreen = win.state === 'fullscreen';
|
|
|
|
const containerClass = clsx(
|
|
'fixed flex flex-col bg-white shadow-2xl rounded-lg overflow-hidden',
|
|
isFullscreen
|
|
? 'inset-2 rounded-lg'
|
|
: 'rounded-lg',
|
|
isDragging && 'cursor-grabbing'
|
|
);
|
|
|
|
const containerStyle: React.CSSProperties = isFullscreen
|
|
? { zIndex: win.zIndex }
|
|
: {
|
|
left: win.position.x,
|
|
top: win.position.y,
|
|
width: win.size.width,
|
|
height: win.size.height,
|
|
zIndex: win.zIndex,
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={containerClass}
|
|
style={containerStyle}
|
|
onMouseDown={() => setActiveWindow(win.id)}
|
|
role="dialog"
|
|
aria-label={win.title}
|
|
>
|
|
{/* Header */}
|
|
<div
|
|
ref={headerRef}
|
|
onMouseDown={handleMouseDown}
|
|
className="bg-secondary-50 border-b border-secondary-200 px-4 py-2 flex items-center justify-between cursor-grab select-none flex-shrink-0"
|
|
>
|
|
<span className="text-sm font-semibold text-secondary-700 truncate">
|
|
{win.title}
|
|
</span>
|
|
<div className="flex items-center gap-1">
|
|
{/* AI Chat Toggle */}
|
|
<button
|
|
onClick={() => toggleAiChat(win.id)}
|
|
className={clsx(
|
|
'p-1.5 rounded hover:bg-secondary-200',
|
|
win.aiChatVisible && 'bg-primary-100 text-primary-600'
|
|
)}
|
|
aria-label={t('window.kichateinaus')}
|
|
title={t('window.kichat')}
|
|
>
|
|
<Sparkles className="w-4 h-4" />
|
|
</button>
|
|
{/* Fullscreen Toggle */}
|
|
<button
|
|
onClick={() => toggleFullscreen(win.id)}
|
|
className="p-1.5 rounded hover:bg-secondary-200"
|
|
aria-label={isFullscreen ? 'Vollbild verlassen' : 'Vollbild'}
|
|
title={isFullscreen ? 'Vollbild verlassen' : 'Vollbild'}
|
|
>
|
|
{isFullscreen ? (
|
|
<Minimize2 className="w-4 h-4" />
|
|
) : (
|
|
<Maximize2 className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
{/* Minimize */}
|
|
<button
|
|
onClick={() => minimizeWindow(win.id)}
|
|
className="p-1.5 rounded hover:bg-secondary-200"
|
|
aria-label={t('window.minimieren')}
|
|
title={t('window.minimieren')}
|
|
>
|
|
<Minus className="w-4 h-4" />
|
|
</button>
|
|
{/* Close */}
|
|
<button
|
|
onClick={() => closeWindow(win.id)}
|
|
className="p-1.5 rounded hover:bg-danger-100 hover:text-danger-600"
|
|
aria-label={t('window.schließen')}
|
|
title={t('window.schließen')}
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content Area */}
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{/* Main Content */}
|
|
<div
|
|
className={clsx(
|
|
'flex-1 overflow-y-auto',
|
|
win.aiChatVisible && 'border-r border-secondary-200'
|
|
)}
|
|
>
|
|
<ContentComponent {...win.componentProps} windowId={win.id} />
|
|
</div>
|
|
|
|
{/* AI Chat Panel */}
|
|
{win.aiChatVisible && (
|
|
<div className="w-1/2 flex flex-col">
|
|
<AiChatPanel windowTitle={win.title} windowType={win.type} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|