feat: Introduce new LayoutSystem and spatial workspace architecture design document, refactor document card and workspace to use it, and simplify pointer event handling.

This commit is contained in:
2025-11-26 13:39:19 +01:00
parent 27eea08605
commit bbf7d23c96
6 changed files with 167 additions and 113 deletions
+49 -41
View File
@@ -1,15 +1,15 @@
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { globalLayout, LayoutItem } from './LayoutSystem';
import { LayoutStore, LayoutCard } from './LayoutSystem';
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
import DesktopDocumentCard from './DesktopDocumentCard';
import usePreviewMetadata from './hooks/usePreviewMetadata';
import useDeskTagInteractions from './tags/useDeskTagInteractions';
import '../styles/workspace/workspace-layout.css';
import '../styles/workspace/workspace-items.css';
import '../styles/workspace/workspace-cards.css';
@@ -42,7 +42,7 @@ interface DocumentSizeInfo {
// Fallback size computation
const computeFallbackCardSize = (_doc: DeskDocument): DocumentSizeInfo => {
return { width: 200, height: 280, source: 'fallback' };
return { width: 200, height: 200, source: 'fallback' };
};
export interface DesktopWorkspaceProps {
@@ -51,7 +51,7 @@ export interface DesktopWorkspaceProps {
getDocumentAsset?: (...args: any[]) => unknown;
onDocumentActivate?: (doc: DeskDocument, event?: unknown) => void;
onSelectionChange?: (selectedIds: Identifier[]) => void;
layout?: any[];
onDocumentTagDrop?: (docId: Identifier, tag: any) => void;
}
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
@@ -60,10 +60,9 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
getDocumentAsset,
onDocumentActivate,
onSelectionChange,
layout: initialLayout,
onDocumentTagDrop,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<Map<string, HTMLElement>>(new Map());
const items = useMemo(() => {
return entries
@@ -74,7 +73,8 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
}, [entries]);
// Layout System Initialization
const layoutRef = useRef<Map<string, LayoutItem>>(new Map());
const layoutStore = useMemo(() => new LayoutStore(), []);
const layoutRef = useRef<Map<string, LayoutCard>>(new Map());
// Selection Context
const {
@@ -101,33 +101,46 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
const metadataMap = usePreviewMetadata(items, getDocumentAsset, ensureAssetUrl);
const ensureDocumentSize = useCallback((doc: DeskDocument | null): DocumentSizeInfo | null => {
if (!doc) return null;
const ensureDocumentSize = useCallback((doc: DeskDocument): DocumentSizeInfo => {
if (doc.id) {
const meta = metadataMap.get(String(doc.id));
if (meta) return { width: meta.width, height: meta.height, source: 'metadata' };
if (meta && meta.width && meta.height)
return { width: meta.width, height: meta.height, source: 'metadata' };
}
return computeFallbackCardSize(doc);
}, [metadataMap]);
// Initialize Layout System
useLayoutEffect(() => {
if (initialLayout) {
// Seeding logic if needed, but registration happens in render loop
}
}, [initialLayout]);
// Sync LayoutStore to layoutRef
useEffect(() => {
const sync = () => {
layoutRef.current = globalLayout.items;
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<string | null>(null);
const [pendingTagDocId, setPendingTagDocId] = useState<string | null>(null);
const tagEngine = useMemo(() => ({
setPendingRemovalTag,
setTagDropTargetId,
setPendingTagDocId,
}), []);
const tagInteractions = useDeskTagInteractions({
engine: tagEngine,
onAssignTagToDocument: (docId: string, tag: any) => {
onDocumentTagDrop?.(docId, tag);
},
requestCanvasFocus: focusShell,
});
// Overlay State
const [overlayDisplay, setOverlayDisplay] = useState<OverlayDisplay | null>(null);
const closeOverlay = useCallback(() => setOverlayDisplay(null), []);
@@ -154,25 +167,10 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
const isSelected = selectedDocumentIds.includes(docId);
const size = ensureDocumentSize(doc);
// Register with LayoutStore
const registerNode = (node: HTMLDivElement) => {
if (node) {
itemRefs.current.set(docId, node);
const initItem = initialLayout?.find((i: any) => i.id === docId);
globalLayout.initialize(docId, node, {
x: initItem?.x,
y: initItem?.y,
rotation: initItem?.rotation,
z: index,
width: size?.width ?? 200,
height: size?.height ?? 200
});
} else {
itemRefs.current.delete(docId);
}
};
// const cardPointerHandlers = getCardPointerHandlers(doc);
const layoutCard = layoutStore.initialize(docId, null, {
width: size.width,
height: size.height
});
return (
<DesktopDocumentCard
@@ -187,8 +185,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
}}
shouldLoad={true}
matchesFilter={true}
tagTargetActive={false}
tagTargetPending={false}
selected={isSelected}
docTagTokens=""
ensureAssetUrl={ensureAssetUrl}
@@ -196,7 +193,18 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
handleNavigatorSnapshot={() => { }}
cardPointerHandlers={undefined}
onDocumentActivate={onDocumentActivate}
registerNode={registerNode}
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}
/>
);
})}