diff --git a/frontend/src/desktop/DesktopDocumentCard.tsx b/frontend/src/desktop/DesktopDocumentCard.tsx index 4b3d452..3fc40a2 100644 --- a/frontend/src/desktop/DesktopDocumentCard.tsx +++ b/frontend/src/desktop/DesktopDocumentCard.tsx @@ -16,6 +16,8 @@ interface PendingRemovalTag { tagId?: string; } +import { LayoutCard } from './LayoutSystem'; + interface DesktopDocumentCardProps { doc: Document; style?: React.CSSProperties; @@ -39,7 +41,7 @@ interface DesktopDocumentCardProps { onDocTagDrag?: (event: React.DragEvent) => void; onDocTagDragEnd?: (event: React.DragEvent) => void; pendingRemovalTag?: PendingRemovalTag | null; - registerNode?: (node: HTMLDivElement | null) => void; + layoutCard?: LayoutCard; } const DesktopDocumentCard: React.FC = ({ @@ -65,7 +67,7 @@ const DesktopDocumentCard: React.FC = ({ onDocTagDrag, onDocTagDragEnd, pendingRemovalTag, - registerNode, + layoutCard, }) => { const correspondents = useMemo(() => resolveCorrespondents(doc), [doc]); const tags = Array.isArray(doc?.tags) ? doc.tags : []; @@ -88,7 +90,7 @@ const DesktopDocumentCard: React.FC = ({ data-doc-id={doc.id} data-tag-ids={dataTagIds} aria-hidden={ariaHidden} - ref={registerNode ?? undefined} + ref={(node) => layoutCard?.setRef(node)} {...cardPointerHandlers} onDragEnter={(event) => { if (doc?.id == null) { diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index 1eb2ce4..10ff54f 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -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 = ({ @@ -60,10 +60,9 @@ const DesktopWorkspace: React.FC = ({ getDocumentAsset, onDocumentActivate, onSelectionChange, - layout: initialLayout, + onDocumentTagDrop, }) => { const containerRef = useRef(null); - const itemRefs = useRef>(new Map()); const items = useMemo(() => { return entries @@ -74,7 +73,8 @@ const DesktopWorkspace: React.FC = ({ }, [entries]); // Layout System Initialization - const layoutRef = useRef>(new Map()); + const layoutStore = useMemo(() => new LayoutStore(), []); + const layoutRef = useRef>(new Map()); // Selection Context const { @@ -101,33 +101,46 @@ const DesktopWorkspace: React.FC = ({ 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(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, + }); + // Overlay State const [overlayDisplay, setOverlayDisplay] = useState(null); const closeOverlay = useCallback(() => setOverlayDisplay(null), []); @@ -154,25 +167,10 @@ const DesktopWorkspace: React.FC = ({ 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 ( = ({ }} shouldLoad={true} matchesFilter={true} - tagTargetActive={false} - tagTargetPending={false} + selected={isSelected} docTagTokens="" ensureAssetUrl={ensureAssetUrl} @@ -196,7 +193,18 @@ const DesktopWorkspace: React.FC = ({ 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} /> ); })} diff --git a/frontend/src/desktop/LayoutSystem.ts b/frontend/src/desktop/LayoutSystem.ts index 3d8ee85..16b9f9e 100644 --- a/frontend/src/desktop/LayoutSystem.ts +++ b/frontend/src/desktop/LayoutSystem.ts @@ -1,4 +1,6 @@ -export interface LayoutItem { +import { constrainDimensions } from './utils/layoutUtils'; + +export interface LayoutCardState { id: string; x: number; y: number; @@ -6,79 +8,108 @@ export interface LayoutItem { rotation: number; width: number; height: number; - ref: HTMLElement; +} + +export class LayoutCard implements LayoutCardState { + id: string; + x: number = 0; + y: number = 0; + z: number = 0; + rotation: number = 0; + width: number = 0; + height: number = 0; + ref: HTMLElement | null = null; + + constructor(id: string, initialData: Partial = {}, ref: HTMLElement | null = null) { + this.id = id; + Object.assign(this, initialData); + this.ref = ref; + } + + setRef(ref: HTMLElement | null) { + this.ref = ref; + this.applyTransform(); + } + + update(changes: Partial) { + Object.assign(this, changes); + this.applyTransform(); + } + + private applyTransform() { + if (this.ref) { + this.ref.style.transform = + `translate3d(${this.x}px, ${this.y}px, 0) rotate(${this.rotation}deg)`; + this.ref.style.zIndex = String(this.z); + this.ref.style.width = `${this.width}px`; + this.ref.style.height = `${this.height}px`; + } + } + + toSnapshot(): LayoutCardState { + return { + id: this.id, + x: this.x, + y: this.y, + z: this.z, + rotation: this.rotation, + width: this.width, + height: this.height + }; + } } export class LayoutStore { - items = new Map(); + items = new Map(); zCounter = 100; - register(id: string, ref: HTMLElement, initialData: Partial) { - const existing = this.items.get(id); - this.items.set(id, { - id, - ref, - x: existing?.x ?? 0, - y: existing?.y ?? 0, - z: existing?.z ?? 0, - rotation: existing?.rotation ?? 0, - width: 200, - height: 200, - ...initialData - }); - } - - initialize(id: string, ref: HTMLElement, config: { - x?: number; - y?: number; - rotation?: number; - z: number; + initialize(id: string, ref: HTMLElement | null, config: { width: number; height: number; }) { - if (this.items.has(id)) { - // Update ref if it changed - const item = this.items.get(id)!; - if (item.ref !== ref) { - item.ref = ref; - this.update(id, {}); // Re-apply styles - } - return; + let card = this.items.get(id); + + const { width, height } = constrainDimensions( + config.width, + config.height, + 280 + ); + + if (!card) { + // Apply defaults if not provided + const x = Math.random() * 500; + const y = Math.random() * 500; + const rotation = Math.random() * 10 - 5; + + card = new LayoutCard(id, { + x, + y, + rotation, + width, + height + }, ref); + this.items.set(id, card); + } else { + card.update({ width, height }); } - // Apply defaults if not provided - const x = config.x ?? Math.random() * 500; - const y = config.y ?? Math.random() * 500; - const rotation = config.rotation ?? (Math.random() * 10 - 5); + // Always update ref and ensure transform is applied + if (card.ref !== ref) { + card.setRef(ref); + } - this.register(id, ref, { - ...config, - x, - y, - rotation - }); - - // Apply immediately - this.update(id, {}); + return card; } unregister(id: string) { this.items.delete(id); } - // Fast Update: Updates internal state AND applies CSS transform immediately - update(id: string, updates: Partial) { - const item = this.items.get(id); - if (!item) return; - - Object.assign(item, updates); - if (updates.z) this.zCounter = Math.max(this.zCounter, updates.z); - - // Direct DOM manipulation (The "Engine" part) - if (item.ref) { - item.ref.style.transform = - `translate3d(${item.x}px, ${item.y}px, 0) rotate(${item.rotation}deg)`; - item.ref.style.zIndex = String(item.z); + update(id: string, updates: Partial) { + const card = this.items.get(id); + if (card) { + if (updates.z) this.zCounter = Math.max(this.zCounter, updates.z); + card.update(updates); } } @@ -87,10 +118,6 @@ export class LayoutStore { } getSnapshot() { - // Return serializable data for persistence - return Array.from(this.items.values()).map(({ ref: _ref, ...data }) => data); + return Array.from(this.items.values()).map(card => card.toSnapshot()); } } - -// Singleton or Context-provided instance -export const globalLayout = new LayoutStore(); diff --git a/frontend/src/desktop/hooks/usePreviewMetadata.ts b/frontend/src/desktop/hooks/usePreviewMetadata.ts index b514a74..ab8901e 100644 --- a/frontend/src/desktop/hooks/usePreviewMetadata.ts +++ b/frontend/src/desktop/hooks/usePreviewMetadata.ts @@ -41,7 +41,7 @@ const usePreviewMetadata = ( const docId = String(doc.id); const resolveAsset = (type: string) => getDocumentAsset?.(doc, type) ?? null; - let asset = resolveAsset('preview') || resolveAsset('thumbnail'); + let asset = resolveAsset('thumbnail'); let metadata = (asset?.metadata as { width?: number; height?: number } | null) || null; const hasDimensions = (meta: { width?: number | string; height?: number | string } | null) => diff --git a/frontend/src/desktop/tags/useDeskTagInteractions.js b/frontend/src/desktop/tags/useDeskTagInteractions.js index 619009d..c858c97 100644 --- a/frontend/src/desktop/tags/useDeskTagInteractions.js +++ b/frontend/src/desktop/tags/useDeskTagInteractions.js @@ -15,10 +15,6 @@ const preventAll = (event) => { if (typeof event.stopPropagation === 'function') event.stopPropagation(); }; -// TODO: Restore getPointerPosition -const getPointerPosition = (event) => { - return { x: event.clientX, y: event.clientY }; -}; import { TAG_REMOVE_DISTANCE } from '../../constants/desktop'; const createDragPreview = (node, clientX, clientY) => { @@ -150,7 +146,8 @@ export const useDeskTagInteractions = ({ event.dataTransfer.effectAllowed = 'move'; writeTagTransferData(event.dataTransfer, tag, doc.id); - const { x: pointerX, y: pointerY } = getPointerPosition(event, { fallbackToPage: false }); + const pointerX = event.clientX; + const pointerY = event.clientY; const { clone, offsetX, offsetY } = createDragPreview(event.currentTarget, pointerX, pointerY) || {}; if (clone) { event.dataTransfer.setDragImage(clone, offsetX || 0, offsetY || 0); @@ -181,7 +178,8 @@ export const useDeskTagInteractions = ({ if (!state) { return; } - const { x, y } = getPointerPosition(event); + const x = event.clientX; + const y = event.clientY; const dx = x - (state.initialX || 0); const dy = y - (state.initialY || 0); state.distance = Math.sqrt(dx * dx + dy * dy); diff --git a/frontend/src/desktop/utils/layoutUtils.ts b/frontend/src/desktop/utils/layoutUtils.ts index 0b0c889..cbcde30 100644 --- a/frontend/src/desktop/utils/layoutUtils.ts +++ b/frontend/src/desktop/utils/layoutUtils.ts @@ -33,3 +33,22 @@ export const computeCardBounds = ({ maxY: Math.max(padding + halfH, canvasHeight - padding - halfH), }; }; + +export const constrainDimensions = (width: number, height: number, maxDimension: number) => { + if (width <= maxDimension && height <= maxDimension) { + return { width, height }; + } + + const aspect = width / height; + if (width > height) { + return { + width: maxDimension, + height: maxDimension / aspect + }; + } else { + return { + width: maxDimension * aspect, + height: maxDimension + }; + } +};