From e7e7881772780b0740994b6efa38ce0b5cbeab78 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Sun, 2 Nov 2025 04:40:07 +0100 Subject: [PATCH] playful --- frontend/src/DesktopWorkspace.css | 6 +- frontend/src/DesktopWorkspace.jsx | 224 ++++------ frontend/src/app/AppLayout.jsx | 72 ++-- frontend/src/desktop/useDocumentDrag.js | 400 +++++++++++++++++- frontend/src/detail/DetailPanel.jsx | 3 +- .../src/documents/DocumentSummarySection.jsx | 6 +- frontend/src/documents/DocumentsGrid.jsx | 20 +- frontend/src/documents/DocumentsList.jsx | 20 +- frontend/src/documents/DocumentsPanel.jsx | 7 +- frontend/src/documents/correspondents.js | 14 +- frontend/src/documents/documentSummary.js | 11 +- frontend/src/preview/DocumentViewerPanel.jsx | 17 +- frontend/src/sidebar/Sidebar.jsx | 3 +- 13 files changed, 560 insertions(+), 243 deletions(-) diff --git a/frontend/src/DesktopWorkspace.css b/frontend/src/DesktopWorkspace.css index 7c0192e..7870e96 100644 --- a/frontend/src/DesktopWorkspace.css +++ b/frontend/src/DesktopWorkspace.css @@ -69,7 +69,8 @@ } .desk-item.is-tag-pending .desk-item__card { - opacity: 0.6; + outline: 0.25rem solid var(--accent-outline); + outline-offset: 0.25rem; } .desk-item.is-filtered-out { @@ -201,6 +202,9 @@ body.desk-cursor-remove * { height: 100%; object-fit: contain; display: block; + pointer-events: none; + user-select: none; + -webkit-user-drag: none; } .desk-item__card--empty { diff --git a/frontend/src/DesktopWorkspace.jsx b/frontend/src/DesktopWorkspace.jsx index b05e967..8486407 100644 --- a/frontend/src/DesktopWorkspace.jsx +++ b/frontend/src/DesktopWorkspace.jsx @@ -17,13 +17,17 @@ import useDocumentDrag from './desktop/useDocumentDrag'; import { DesktopProvider, useDesktopContext } from './desktop/context'; import PreviewZoomOverlay from './detail/PreviewZoomOverlay'; import { getTagColorStyle } from './utils/colors'; +import { + isTagTransferEvent, + parseTagTransferPayload, + writeTagTransferData, +} from './documents/tagTransfer'; import './DesktopWorkspace.css'; const CANVAS_PADDING = 24; const ROTATION_RANGE = 7; const DEFAULT_CANVAS_WIDTH = 1024; const DEFAULT_CANVAS_HEIGHT = 680; -const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag']; const CARD_MIN = 240; const CARD_MAX = 340; @@ -111,50 +115,6 @@ const polygonCentroid = (polygon) => { }; }; -const readTransferData = (dataTransfer, mimeTypes) => { - if (!dataTransfer) { - return null; - } - for (let index = 0; index < mimeTypes.length; index += 1) { - const type = mimeTypes[index]; - try { - const raw = dataTransfer.getData(type); - if (raw) { - return raw; - } - } catch (error) { - if (DEBUG_DROP) { - console.warn('[desk] readTransferData failed for type', type, error); - } - } - } - return null; -}; - -const parseTagTransferPayload = (event) => { - const raw = readTransferData(event?.dataTransfer, [ - 'application/x-papercrate-tag', - 'text/papercrate-tag', - ]); - if (!raw) { - return null; - } - try { - return JSON.parse(raw); - } catch (error) { - console.warn('[desk] parseTagTransferPayload failed', error); - } - return null; -}; - -const resolveTagKey = (tag) => { - if (!tag) { - return null; - } - const key = tag.id ?? tag.uuid ?? tag.slug ?? tag.label; - return key != null ? String(key) : null; -}; - const DesktopPreviewCard = ({ doc, title, @@ -214,9 +174,21 @@ const DesktopPreviewCard = ({ const showNav = hasPreview && (cardinality > 1 || canGoPrev || canGoNext); return ( -
+
{ + if (event instanceof DragEvent) { + event.preventDefault(); + } + }} + > {hasPreview ? ( - {title} + {title} event.preventDefault()} + /> ) : (
DOC
@@ -817,21 +789,71 @@ const DesktopWorkspace = ({ [updateRemovalCursor], ); - const isTagTransfer = useCallback((event) => { - const types = event.dataTransfer?.types; - if (!types) return false; - return TAG_MIME_TYPES.some((type) => - typeof types.includes === 'function' - ? types.includes(type) - : Array.from(types).includes(type), - ); - }, []); + const isTagTransfer = useCallback((event) => isTagTransferEvent(event), []); const handleTagDragEnd = useCallback(() => { updateRemovalCursor(false); setTagDropTargetId(null); }, [updateRemovalCursor]); + const finalizeTagDrag = useCallback( + (dropEffect = 'none') => { + const state = draggingTagRef.current; + if (!state) { + updateRemovalCursor(false); + return; + } + + draggingTagRef.current = null; + + const node = state.element; + const showNode = () => { + if (node instanceof HTMLElement) { + node.classList.remove('is-drag-hidden'); + } + }; + const scheduleShowNode = () => { + if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') { + window.requestAnimationFrame(showNode); + } else { + setTimeout(showNode, 0); + } + }; + + cleanupPreview(state.previewClone); + + const shouldRemove = + !state.dropHandled && + dropEffect === 'none' && + state.sourceDocId && + typeof onRemoveTagFromDocument === 'function' && + (state.distance || 0) >= TAG_REMOVE_DISTANCE; + + if (!shouldRemove) { + scheduleShowNode(); + updateRemovalCursor(false); + return; + } + + updateRemovalCursor(false); + setPendingRemovalTag({ docId: state.sourceDocId, tagId: state.tagId }); + void (async () => { + try { + await onRemoveTagFromDocument(state.sourceDocId, state.tagId); + if (DEBUG_DROP) { + console.log('[desk] finalizeTagDrag -> removed tag due to fling'); + } + } catch (error) { + console.error('Failed to remove tag after drag', error); + scheduleShowNode(); + } finally { + setPendingRemovalTag(null); + } + })(); + }, + [onRemoveTagFromDocument, updateRemovalCursor, setPendingRemovalTag], + ); + const ensureDocumentSize = useCallback((doc) => { if (!doc?.id) { return null; @@ -897,7 +919,7 @@ const DesktopWorkspace = ({ return null; } const doc = documentLookup.get(overlayDocId); - const alt = snapshot.alt || doc?.title || doc?.original_name || 'Document preview'; + const alt = snapshot.alt || doc?.title; return { url: snapshot.url, alt, @@ -1374,7 +1396,6 @@ const syncLayoutSnapshot = useCallback(() => { if (DEBUG_DROP) { console.log('[desk] handleTagDropOnDoc: missing tag id payload', payload); } - requestCanvasFocus(); return; } @@ -1389,7 +1410,6 @@ const syncLayoutSnapshot = useCallback(() => { if (DEBUG_DROP) { console.log('[desk] handleTagDropOnDoc: drop from same doc ignored', tagId); } - requestCanvasFocus(); return; } @@ -1401,7 +1421,6 @@ const syncLayoutSnapshot = useCallback(() => { if (DEBUG_DROP) { console.log('[desk] handleTagDropOnDoc: tag already assigned', tagId); } - requestCanvasFocus(); return; } @@ -1433,7 +1452,8 @@ const syncLayoutSnapshot = useCallback(() => { if (DEBUG_DROP) { console.log('[desk] handleTagDropOnDoc: finalizing drop for tag', tagId); } - requestCanvasFocus(); + handleTagDragEnd(); + finalizeTagDrag(payload?.sourceDocId ? 'move' : 'copy'); } }, [ @@ -1441,7 +1461,8 @@ const syncLayoutSnapshot = useCallback(() => { markActiveTagDropHandled, onAssignTagToDocument, onRemoveTagFromDocument, - requestCanvasFocus, + handleTagDragEnd, + finalizeTagDrag, ], ); @@ -1556,14 +1577,7 @@ const syncLayoutSnapshot = useCallback(() => { console.warn('[desk] Failed to set drag effect', error); } - const payload = JSON.stringify({ id: tag.id, label: tag.label, sourceDocId: doc.id }); - try { - event.dataTransfer?.setData('application/x-papercrate-tag', payload); - event.dataTransfer?.setData('text/papercrate-tag', payload); - event.dataTransfer?.setData('text/plain', tag.label || 'Tag'); - } catch (error) { - console.warn('[desk] Failed to populate drag data for tag', error); - } + writeTagTransferData(event.dataTransfer, tag, doc.id); const pending = pendingDocTagDragRef.current; const node = event.currentTarget; @@ -1600,7 +1614,7 @@ const syncLayoutSnapshot = useCallback(() => { draggingTagRef.current = { sourceDocId: doc.id, tagId: tag.id, - tagLabel: tag.label || 'Tag', + tagLabel: tag.label, startX: initialX, startY: initialY, distance: 0, @@ -1657,61 +1671,9 @@ const syncLayoutSnapshot = useCallback(() => { const handleDocTagDragEnd = useCallback( (event) => { handleTagDragEnd(); - const state = draggingTagRef.current; - if (!state) { - return; - } - draggingTagRef.current = null; - - const node = state.element; - const showNode = () => { - if (node instanceof HTMLElement) { - node.classList.remove('is-drag-hidden'); - } - }; - cleanupPreview(state.previewClone); - - const scheduleShowNode = () => { - if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') { - window.requestAnimationFrame(showNode); - } else { - setTimeout(showNode, 0); - } - }; - - const dropEffect = event?.dataTransfer?.dropEffect || 'none'; - console.log('[desk] dragEnd dropEffect', dropEffect, 'dropHandled', state.dropHandled); - const shouldRemove = - !state.dropHandled && - dropEffect === 'none' && - state.sourceDocId && - typeof onRemoveTagFromDocument === 'function' && - (state.distance || 0) >= TAG_REMOVE_DISTANCE; - - if (!shouldRemove) { - console.log('[desk] dragEnd -> no removal. distance:', state.distance); - scheduleShowNode(); - requestCanvasFocus(); - updateRemovalCursor(false); - return; - } - - requestCanvasFocus(); - updateRemovalCursor(false); - setPendingRemovalTag({ docId: state.sourceDocId, tagId: state.tagId }); - void (async () => { - try { - await onRemoveTagFromDocument(state.sourceDocId, state.tagId); - console.log('[desk] dragEnd -> removed tag due to fling'); - } catch (error) { - console.error('Failed to remove tag after drag', error); - scheduleShowNode(); - } finally { - setPendingRemovalTag(null); - } - })(); + finalizeTagDrag(event?.dataTransfer?.dropEffect || 'none'); }, - [requestCanvasFocus, handleTagDragEnd, onRemoveTagFromDocument, updateRemovalCursor], + [handleTagDragEnd, finalizeTagDrag], ); const contextValue = useMemo( @@ -1906,11 +1868,10 @@ const DesktopWorkspaceView = () => { }; const docKey = doc?.id != null ? String(doc.id) : null; const shouldLoad = docKey ? visibleDocIds.has(docKey) : false; - const title = doc.title || doc.original_name || 'Document'; const dragging = draggingId === doc.id; const tags = Array.isArray(doc.tags) ? doc.tags : []; const docTagKeys = tags - .map((tag) => resolveTagKey(tag)) + .map((tag) => (tag ? tag.id : null)) .filter(Boolean); const matchesFilter = activeTagSet.size === 0 || docTagKeys.some((key) => activeTagSet.has(key)); @@ -1956,7 +1917,7 @@ const DesktopWorkspaceView = () => {
{ {tags.length > 0 && (