diff --git a/frontend/src/documents/DocumentsList.jsx b/frontend/src/documents/DocumentsList.jsx
index 714b2d8..d17fdce 100644
--- a/frontend/src/documents/DocumentsList.jsx
+++ b/frontend/src/documents/DocumentsList.jsx
@@ -139,92 +139,96 @@ const DocumentsList = ({
-
-
-
-
- {isFolderEditing ? (
-
- setFolderDraft(event.target.value)}
- onClick={(event) => event.stopPropagation()}
- onKeyDown={(event) => {
- if (event.key === 'Enter') {
+
+
+
+
+
+
+ {isFolderEditing ? (
+
+ setFolderDraft(event.target.value)}
+ onClick={(event) => event.stopPropagation()}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ submitFolderEditing(folder);
+ } else if (event.key === 'Escape') {
+ event.preventDefault();
+ cancelFolderEditing(event);
+ }
+ }}
+ onBlur={(event) => {
+ const nextFocus = event.relatedTarget;
+ if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
+ cancelFolderEditing();
+ }
+ }}
+ />
+
+
+
+ ) : (
+ {
+ if (!allowInlineFolderEdit) {
+ return;
+ }
event.preventDefault();
- submitFolderEditing(folder);
- } else if (event.key === 'Escape') {
- event.preventDefault();
- cancelFolderEditing(event);
- }
- }}
- onBlur={(event) => {
- const nextFocus = event.relatedTarget;
- if (!nextFocus || !event.currentTarget.parentElement?.contains(nextFocus)) {
- cancelFolderEditing();
- }
- }}
- />
-
-
+ event.stopPropagation();
+ beginFolderEditing(folder);
+ }}
+ onKeyDown={(event) => {
+ if (!allowInlineFolderEdit) {
+ return;
+ }
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ event.stopPropagation();
+ beginFolderEditing(folder);
+ }
+ }}
+ >
+ {folder.name}
+
+ )}
- ) : (
- {
- if (!allowInlineFolderEdit) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- beginFolderEditing(folder);
- }}
- onKeyDown={(event) => {
- if (!allowInlineFolderEdit) {
- return;
- }
- if (event.key === 'Enter') {
- event.preventDefault();
- event.stopPropagation();
- beginFolderEditing(folder);
- }
- }}
- >
- {folder.name}
-
- )}
-
-
+
+
+
+ —
+ —
|
- — |
- — |
);
}
diff --git a/frontend/src/documents/panel/DocumentsPanel.jsx b/frontend/src/documents/panel/DocumentsPanel.jsx
index a5d307b..5692736 100644
--- a/frontend/src/documents/panel/DocumentsPanel.jsx
+++ b/frontend/src/documents/panel/DocumentsPanel.jsx
@@ -196,7 +196,6 @@ const DocumentsPanel = ({
[handleDocumentPreviewZoom, onInspectDocument],
);
- const selectedRowKeySet = useMemo(() => new Set(selectedEntries || []), [selectedEntries]);
const navigableRows = useMemo(
() => entries.map((entry) => ({ key: entry.key, type: entry.type, id: entry.id })),
[entries],
@@ -234,21 +233,12 @@ const DocumentsPanel = ({
}
onFocusedRowChange?.(resolvedKey);
-
- if (!selectedRowKeySet.has(resolvedKey) && typeof onEntrySelection === 'function') {
- onEntrySelection(resolvedKey, {
- shiftKey: false,
- preventDefault: () => {},
- });
- }
}, [
focusedRowKey,
navigableRowKeys,
navigableRows,
- onEntrySelection,
onFocusedRowChange,
selectedEntries,
- selectedRowKeySet,
]);
const handlePanelKeyDown = useCallback(
diff --git a/frontend/src/documents/useEntryPointer.js b/frontend/src/documents/useEntryPointer.js
index 826645f..4d5cb13 100644
--- a/frontend/src/documents/useEntryPointer.js
+++ b/frontend/src/documents/useEntryPointer.js
@@ -14,11 +14,15 @@ export const isPrimaryPointerEvent = (event) => {
return type === 'click' || type === 'pointerdown' || type === 'pointerup';
};
+const EntryType = Object.freeze({
+ document: 'document',
+ folder: 'folder',
+});
+
export const useEntryPointer = ({
resolveDocumentRowKey,
resolveFolderRowKey,
- onSelectDocument,
- onSelectFolder,
+ onSelectEntry,
onInspectDocument,
}) =>
useCallback(
@@ -28,41 +32,34 @@ export const useEntryPointer = ({
}
const { type, id } = entry;
- if (type !== 'document' && type !== 'folder') {
+ if (type !== EntryType.document && type !== EntryType.folder) {
return;
}
const rowKey = entry.key
- || (type === 'document' ? resolveDocumentRowKey?.(id) : resolveFolderRowKey?.(id));
+ || (type === EntryType.document ? resolveDocumentRowKey?.(id) : resolveFolderRowKey?.(id));
if (!rowKey) {
return;
}
const modifierClick = isPointerModifierEvent(event);
const primaryClick = isPrimaryPointerEvent(event);
- const metadata = { modifierClick, primaryClick, rowKey };
+ const metadata = { modifierClick, primaryClick, rowKey, type, id };
- if (type === 'document') {
- if (typeof onSelectDocument === 'function') {
- onSelectDocument(id, event, metadata);
- }
- if (!modifierClick && primaryClick && typeof onInspectDocument === 'function') {
- onInspectDocument(id, metadata);
- }
- return;
+ if (typeof onSelectEntry === 'function') {
+ onSelectEntry(entry, event, metadata);
}
- if (typeof onSelectFolder === 'function') {
- onSelectFolder(id, event, metadata);
+ if (
+ type === EntryType.document
+ && !modifierClick
+ && primaryClick
+ && typeof onInspectDocument === 'function'
+ ) {
+ onInspectDocument(id, metadata);
}
},
- [
- resolveDocumentRowKey,
- resolveFolderRowKey,
- onSelectDocument,
- onSelectFolder,
- onInspectDocument,
- ],
+ [resolveDocumentRowKey, resolveFolderRowKey, onSelectEntry, onInspectDocument],
);
export default useEntryPointer;
diff --git a/frontend/src/hooks/documents/useDocumentDragHandlers.js b/frontend/src/hooks/documents/useDocumentDragHandlers.js
index 9662e89..eaec276 100644
--- a/frontend/src/hooks/documents/useDocumentDragHandlers.js
+++ b/frontend/src/hooks/documents/useDocumentDragHandlers.js
@@ -29,6 +29,10 @@ const useDocumentDragHandlers = ({
({ documents = [], folders = [] } = {}) => {
destroyDragPreview();
+ if (typeof document === 'undefined') {
+ return null;
+ }
+
const docEntries = (documents || []).filter(Boolean);
const folderEntries = (folders || []).filter(Boolean);
const totalCount = docEntries.length + folderEntries.length;
@@ -54,6 +58,8 @@ const useDocumentDragHandlers = ({
const wrapper = document.createElement('div');
wrapper.className = 'document-drag-preview';
wrapper.style.setProperty('--drag-preview-size', `${canvasSize}px`);
+ wrapper.style.width = `${canvasSize}px`;
+ wrapper.style.height = `${canvasSize}px`;
visibleItems.forEach((item, index) => {
const layer = document.createElement('div');
@@ -101,15 +107,23 @@ const useDocumentDragHandlers = ({
layer.textContent = doc?.title || 'Document';
}
} else {
- const folderId = item.payload;
- const rowEl = folderId ? document.getElementById(`folder-row-${folderId}`) : null;
- const iconEl = rowEl?.querySelector('.thumb-icon');
+ const payload = item.payload;
+ const folderId = typeof payload === 'string' ? payload : payload?.id;
+ const rowEl = folderId
+ ? document.getElementById(`folder-row-${folderId}`)
+ || document.getElementById(`folder-card-${folderId}`)
+ : null;
+ const iconEl = rowEl?.querySelector('.thumb-icon, .folder-card__icon');
layer.style.width = `${size}px`;
layer.style.height = `${size}px`;
+ layer.classList.add('document-drag-preview__item--folder');
let content = null;
if (iconEl instanceof HTMLElement) {
- content = iconEl.cloneNode(true);
+ const cloneSource = iconEl.classList.contains('folder-card__icon')
+ ? iconEl.querySelector('svg') || iconEl
+ : iconEl;
+ content = cloneSource.cloneNode(true);
content.classList.add('document-drag-preview__folder-thumb');
const svg = content.querySelector('svg');
if (svg) {
@@ -276,11 +290,19 @@ const useDocumentDragHandlers = ({
console.warn('[documents] Failed to populate folder drag payload', error);
}
- const previewDocs = effectiveDocumentSelection
- .map((id) => documentLookup.get(id) || null)
- .filter(Boolean);
- createDragPreview({ documents: previewDocs, folders: uniqueFolders });
+ const previewNode = createDragPreview({
+ documents: effectiveDocumentSelection
+ .map((id) => documentLookup.get(id) || null)
+ .filter(Boolean),
+ folders: uniqueFolders,
+ });
event.currentTarget.classList.add('dragging');
+
+ if (previewNode) {
+ const width = previewNode.offsetWidth || 96;
+ const height = previewNode.offsetHeight || 96;
+ event.dataTransfer.setDragImage(previewNode, width / 2, height / 2);
+ }
},
[
selectedFolderIds,
diff --git a/frontend/src/hooks/documents/useDocumentsWorkspace.js b/frontend/src/hooks/documents/useDocumentsWorkspace.js
index f79dc09..f17a52c 100644
--- a/frontend/src/hooks/documents/useDocumentsWorkspace.js
+++ b/frontend/src/hooks/documents/useDocumentsWorkspace.js
@@ -50,6 +50,11 @@ import useDocumentDragHandlers from './useDocumentDragHandlers';
import useDocumentMutations from './useDocumentMutations';
import useDetailWorkspace from '../../detail/useDetailWorkspace';
+const EntryType = Object.freeze({
+ document: 'document',
+ folder: 'folder',
+});
+
const noop = () => {};
const useDocumentsWorkspace = ({
@@ -1185,19 +1190,15 @@ const useDocumentsWorkspace = ({
const handleEntryPointerCore = useEntryPointerCore({
resolveDocumentRowKey,
resolveFolderRowKey,
- onSelectDocument: (documentId, event, { rowKey }) => {
- const key = rowKey || resolveDocumentRowKey(documentId);
+ onSelectEntry: (entry, event, { rowKey, modifierClick, primaryClick }) => {
+ const { type, id } = entry;
+ const key = rowKey
+ || (type === EntryType.document ? resolveDocumentRowKey(id) : resolveFolderRowKey(id));
if (key) {
handleEntrySelection(key, event);
}
- },
- onSelectFolder: (folderId, event, { modifierClick, primaryClick, rowKey }) => {
- const key = rowKey || resolveFolderRowKey(folderId);
- if (key) {
- handleEntrySelection(key, event);
- }
- if (!modifierClick && primaryClick) {
- selectFolder(folderId);
+ if (type === EntryType.folder && !modifierClick && primaryClick) {
+ selectFolder(id);
}
},
});
diff --git a/frontend/src/hooks/documents/useFolderTree.js b/frontend/src/hooks/documents/useFolderTree.js
index ce5f369..b0f7675 100644
--- a/frontend/src/hooks/documents/useFolderTree.js
+++ b/frontend/src/hooks/documents/useFolderTree.js
@@ -85,9 +85,8 @@ const useFolderTree = ({
})();
setFocusedDocumentId(nextFocus);
- selectionAnchorRef.current = nextDocKeys.length
- ? nextDocKeys[nextDocKeys.length - 1]
- : null;
+ const nextAnchor = mergedSelection.length ? mergedSelection[mergedSelection.length - 1] : null;
+ selectionAnchorRef.current = nextAnchor;
selectionOrderRef.current = mergedSelection;
setSelectionOrder(mergedSelection);
},
diff --git a/frontend/src/styles/documents/drag-preview.css b/frontend/src/styles/documents/drag-preview.css
new file mode 100644
index 0000000..ada971f
--- /dev/null
+++ b/frontend/src/styles/documents/drag-preview.css
@@ -0,0 +1,76 @@
+.document-drag-preview {
+ position: fixed;
+ pointer-events: none;
+ top: -9999px;
+ left: -9999px;
+ width: var(--drag-preview-size, 96px);
+ height: var(--drag-preview-size, 96px);
+ z-index: 9999;
+}
+
+.document-drag-preview__item {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 64px;
+ height: 64px;
+ border-radius: 6px;
+ box-shadow: 0 6px 12px var(--shadow-pop);
+ overflow: hidden;
+ background-color: var(--overlay-dim);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-weight: 600;
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ transform: translate(-50%, -50%) rotate(var(--rotation-deg, 0deg));
+ transform-origin: center;
+}
+
+.document-drag-preview__item--folder {
+ background: transparent;
+ box-shadow: none;
+ border-radius: 0;
+}
+
+.document-drag-preview__item--image {
+ background-color: #000;
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+}
+
+.document-drag-preview__item .document-thumbnail,
+.document-drag-preview__item img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ pointer-events: none;
+}
+
+.document-drag-preview__item .thumb-placeholder,
+.document-drag-preview__item .thumb-placeholder * {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+}
+
+.document-drag-preview__folder-thumb {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+}
+
+.document-drag-preview__folder-thumb svg {
+ width: 48px;
+ height: 48px;
+ color: var(--accent-strong, var(--accent));
+}
diff --git a/frontend/src/styles/documents/listing.css b/frontend/src/styles/documents/listing.css
index 751336e..9741eb5 100644
--- a/frontend/src/styles/documents/listing.css
+++ b/frontend/src/styles/documents/listing.css
@@ -13,8 +13,28 @@
min-height: 0;
}
-.folder-row.is-drop-target {
- outline: 2px dashed var(--accent);
+
+
+.documents-panel tr.folder.is-drop-target {
+ outline: 2px dashed var(--accent-strong, var(--accent));
+ outline-offset: -2px;
+}
+
+.folder-row__columns {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 7rem 7rem;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+.folder-row__meta {
+ font-variant-numeric: tabular-nums;
+ color: var(--muted);
+ text-align: left;
+}
+
+.documents-grid .folder-card.is-drop-target {
+ outline: 2px dashed var(--accent-strong, var(--accent));
outline-offset: 2px;
}
diff --git a/frontend/src/styles/documents/viewer.css b/frontend/src/styles/documents/viewer.css
index c73de23..17e2e21 100644
--- a/frontend/src/styles/documents/viewer.css
+++ b/frontend/src/styles/documents/viewer.css
@@ -63,6 +63,12 @@
pointer-events: none;
}
+.document-drag-preview__item--folder {
+ background: transparent;
+ box-shadow: none;
+ border-radius: 0;
+}
+
.document-drag-preview__folder-thumb {
width: 100%;
height: 100%;
diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css
index 884e2fd..e313188 100644
--- a/frontend/src/styles/index.css
+++ b/frontend/src/styles/index.css
@@ -6,6 +6,7 @@
@import './documents/controls.css';
@import './layout/structure.css';
@import './documents/viewer.css';
+@import './documents/drag-preview.css';
@import './documents/tags-correspondents.css';
@import './documents/panel-sections.css';
@import './sidebar/sidebar.css';
diff --git a/frontend/src/styles/sidebar/sidebar.css b/frontend/src/styles/sidebar/sidebar.css
index fdb64a4..a1a8479 100644
--- a/frontend/src/styles/sidebar/sidebar.css
+++ b/frontend/src/styles/sidebar/sidebar.css
@@ -23,6 +23,11 @@
-webkit-user-select: none;
}
+.sidebar .folder-row.is-drop-target {
+ outline: 2px dashed var(--accent-strong, var(--accent));
+ outline-offset: 2px;
+}
+
.folder-row .name-wrap {
display: inline-flex;
align-items: center;
|