import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { LayoutStore, LayoutCard } from './LayoutSystem'; import DesktopDocumentCard from './DesktopDocumentCard'; import usePreviewMetadata from './hooks/usePreviewMetadata'; import useDeskTagInteractions from './tags/useDeskTagInteractions'; import { useCardPointer } from './useCardPointer'; import '../styles/workspace/workspace-layout.css'; import '../styles/workspace/workspace-items.css'; import '../styles/workspace/workspace-cards.css'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; import { createDocumentEntryKey } from '../app/entryKey'; import { PointerTrackingProvider, usePointerTracking } from './PointerTrackingContext'; import type { Identifier } from '../types/identifiers'; import type { DocumentsListEntry, Document } from '../types/documents'; import { usePreviewContext } from '../preview/PreviewContext'; type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null; type OverlaySource = { url: string; alt?: string | null; mimeType?: string | null; }; export interface DeskDocument { id?: Identifier | null; title?: string; tags?: TagLike[] | null; documentLink?: OverlaySource | null; [key: string]: unknown; } interface DocumentSizeInfo { width: number; height: number; source?: 'snapshot' | 'metadata' | 'fallback'; } // Fallback size computation const computeFallbackCardSize = (_doc: DeskDocument): DocumentSizeInfo => { return { width: 200, height: 200, source: 'fallback' }; }; export interface DesktopWorkspaceProps { entries: DocumentsListEntry[]; ensureAssetUrl?: (...args: any[]) => Promise; getDocumentAsset?: (...args: any[]) => unknown; onDocumentActivate?: (doc: DeskDocument, event?: unknown) => void; onSelectionChange?: (selectedIds: Identifier[]) => void; onDocumentTagDrop?: (docId: Identifier, tag: any) => void; tenantId?: Identifier | null; viewId?: string | null; } // Wrapper to handle hooks per card const DesktopDocumentContainer: React.FC & { onSelect: (ids: string[], extend?: boolean) => void; onDeselect: (ids: string[]) => void; onDocumentActivate?: (id: string) => void; selection: string[]; }> = React.memo((props) => { const { layoutCard, selected, onSelect, onDeselect, onDocumentActivate, selection } = props; // We assume layoutCard is always present in this context const cardPointerHandlers = useCardPointer(layoutCard!, !!selected, selection, onSelect, onDeselect, onDocumentActivate); return ( ); }); DesktopDocumentContainer.displayName = 'DesktopDocumentContainer'; const DesktopWorkspaceContent: React.FC = ({ entries, ensureAssetUrl, getDocumentAsset, onDocumentActivate, onSelectionChange, onDocumentTagDrop, tenantId, viewId, }) => { const { openPreview } = usePreviewContext(); const { addPointer, removePointer } = usePointerTracking(); const containerRef = useRef(null); const [isLayoutReady, setIsLayoutReady] = useState(false); const items = useMemo(() => { return entries .filter((entry): entry is { type: 'document'; document: Document } & DocumentsListEntry => entry.type === 'document' && !!entry.document ) .map(entry => entry.document as DeskDocument); }, [entries]); // Layout System Initialization const layoutStore = useMemo(() => new LayoutStore(), []); const layoutRef = useRef>(new Map()); // Update container size in store useEffect(() => { if (!containerRef.current) return; const observer = new ResizeObserver((entries) => { for (const entry of entries) { const { width, height } = entry.contentRect; layoutStore.setContainerSize(width, height); if (width > 0 && height > 0) { setIsLayoutReady(true); } } }); observer.observe(containerRef.current); return () => observer.disconnect(); }, [layoutStore]); useEffect(() => { if (tenantId && viewId) { // Clear store when switching views to prevent stale items layoutStore.clear(); layoutStore.loadLayout(String(tenantId), viewId); } }, [layoutStore, tenantId, viewId]); // Sync LayoutStore items with current entries to remove stale items useEffect(() => { const currentIds = new Set(items.map((doc, index) => doc.id ? String(doc.id) : `temp-${index}`)); // Identify and remove items that are no longer present for (const id of layoutStore.items.keys()) { if (!currentIds.has(id)) { layoutStore.unregister(id); } } }, [items, layoutStore]); // Selection Context const { selectedDocumentIds, setSelectedEntries, clearSelection, } = useWorkspaceSelectionContext(); const handleSelectionChange = useCallback((ids: Identifier[]) => { if (setSelectedEntries) { const keys = ids.map(id => createDocumentEntryKey(id)); setSelectedEntries(keys); } onSelectionChange?.(ids); }, [setSelectedEntries, onSelectionChange]); const onClearSelection = useCallback(() => { clearSelection ? clearSelection() : handleSelectionChange([]); }, [clearSelection, handleSelectionChange]); const metadataMap = usePreviewMetadata(items, getDocumentAsset, ensureAssetUrl); const ensureDocumentSize = useCallback((doc: DeskDocument): DocumentSizeInfo => { if (doc.id) { const meta = metadataMap.get(String(doc.id)); if (meta && meta.width && meta.height) return { width: meta.width, height: meta.height, source: 'metadata' }; } return computeFallbackCardSize(doc); }, [metadataMap]); // Sync LayoutStore to layoutRef useEffect(() => { const sync = () => { layoutRef.current = layoutStore.items; }; sync(); }, [layoutStore.items]); const handleShellKeyDown = useCallback(() => { }, []); const focusShell = useCallback(() => { }, []); // Tag Interactions const [pendingRemovalTag, setPendingRemovalTag] = useState<{ docId?: string; tagId?: string } | null>(null); const [tagDropTargetId, setTagDropTargetId] = useState(null); const [pendingTagDocId, setPendingTagDocId] = useState(null); const tagEngine = useMemo(() => ({ setPendingRemovalTag, setTagDropTargetId, setPendingTagDocId, }), []); const tagInteractions = useDeskTagInteractions({ engine: tagEngine, onAssignTagToDocument: (docId: string, tag: any) => { onDocumentTagDrop?.(docId, tag); }, requestCanvasFocus: focusShell, }); useEffect(() => { const handleWindowKeyDown = (e: KeyboardEvent) => { if (e.code === 'Space' && selectedDocumentIds.length > 0) { // Preview the last selected document const lastId = selectedDocumentIds[selectedDocumentIds.length - 1]; const doc = items.find(i => String(i.id) === lastId); if (doc) { e.preventDefault(); openPreview(doc); } } }; window.addEventListener('keydown', handleWindowKeyDown); return () => window.removeEventListener('keydown', handleWindowKeyDown); }, [selectedDocumentIds, items, openPreview]); return ( <>
{ if (e.target === e.currentTarget) { // Register background pointer addPointer(e.pointerId); (e.target as Element).setPointerCapture(e.pointerId); onClearSelection(); focusShell(); } }} onPointerUp={(e) => { if (e.target === e.currentTarget) { removePointer(e.pointerId); (e.target as Element).releasePointerCapture(e.pointerId); } }} onPointerCancel={(e) => { if (e.target === e.currentTarget) { removePointer(e.pointerId); (e.target as Element).releasePointerCapture(e.pointerId); } }} > {isLayoutReady && items.map((doc, index) => { const docId = doc.id ? String(doc.id) : `temp-${index}`; const isSelected = selectedDocumentIds.includes(docId); const size = ensureDocumentSize(doc); const layoutCard = layoutStore.initialize(docId, null, { width: Number.isFinite(size.width) && size.width > 0 ? size.width : 200, height: Number.isFinite(size.height) && size.height > 0 ? size.height : 200 }); return ( { }} onDocumentActivate={(id) => { onDocumentActivate?.({ id } as DeskDocument) }} layoutCard={layoutCard} onTagDragEnter={tagInteractions.handleTagDragEnterDoc} onTagDragOver={tagInteractions.handleTagDragOverDoc} onTagDragLeave={tagInteractions.handleTagDragLeaveDoc} onTagDrop={tagInteractions.handleTagDropOnDoc} onDocTagPointerDown={tagInteractions.handleDocTagPointerDown} onDocTagDragStart={tagInteractions.handleDocTagDragStart} onDocTagDrag={tagInteractions.handleDocTagDrag} onDocTagDragEnd={tagInteractions.handleDocTagDragEnd} tagTargetActive={tagDropTargetId === docId} tagTargetPending={pendingTagDocId === docId} pendingRemovalTag={pendingRemovalTag} onSelect={(ids, extend = false) => { if (!extend) { handleSelectionChange(ids); } else { const newSelection = new Set(selectedDocumentIds); ids.forEach(id => newSelection.add(id)); handleSelectionChange(Array.from(newSelection)); } }} onDeselect={(ids) => { const newSelection = new Set(selectedDocumentIds); ids.forEach(id => newSelection.delete(id)); handleSelectionChange(Array.from(newSelection)); }} selection={selectedDocumentIds} /> ); })}
); }; const DesktopWorkspace: React.FC = (props) => { return ( ); }; export default DesktopWorkspace;