This commit is contained in:
2025-11-11 00:48:07 +01:00
parent 992d7038e5
commit 70b9443e66
11 changed files with 258 additions and 137 deletions
+7 -3
View File
@@ -139,7 +139,9 @@ const DocumentsList = ({
<FolderIcon className="thumb-icon__image" size={32} /> <FolderIcon className="thumb-icon__image" size={32} />
</div> </div>
</td> </td>
<td className="doc-list__name"> <td className="doc-list__name" colSpan={3}>
<div className="folder-row__columns">
<div className="folder-row__primary">
<div className="doc-list__name-content"> <div className="doc-list__name-content">
<span className="doc-name__title"> <span className="doc-name__title">
<span className="doc-name__primary"> <span className="doc-name__primary">
@@ -222,9 +224,11 @@ const DocumentsList = ({
</span> </span>
</span> </span>
</div> </div>
</div>
<div className="folder-row__meta"></div>
<div className="folder-row__meta"></div>
</div>
</td> </td>
<td></td>
<td></td>
</tr> </tr>
); );
} }
@@ -196,7 +196,6 @@ const DocumentsPanel = ({
[handleDocumentPreviewZoom, onInspectDocument], [handleDocumentPreviewZoom, onInspectDocument],
); );
const selectedRowKeySet = useMemo(() => new Set(selectedEntries || []), [selectedEntries]);
const navigableRows = useMemo( const navigableRows = useMemo(
() => entries.map((entry) => ({ key: entry.key, type: entry.type, id: entry.id })), () => entries.map((entry) => ({ key: entry.key, type: entry.type, id: entry.id })),
[entries], [entries],
@@ -234,21 +233,12 @@ const DocumentsPanel = ({
} }
onFocusedRowChange?.(resolvedKey); onFocusedRowChange?.(resolvedKey);
if (!selectedRowKeySet.has(resolvedKey) && typeof onEntrySelection === 'function') {
onEntrySelection(resolvedKey, {
shiftKey: false,
preventDefault: () => {},
});
}
}, [ }, [
focusedRowKey, focusedRowKey,
navigableRowKeys, navigableRowKeys,
navigableRows, navigableRows,
onEntrySelection,
onFocusedRowChange, onFocusedRowChange,
selectedEntries, selectedEntries,
selectedRowKeySet,
]); ]);
const handlePanelKeyDown = useCallback( const handlePanelKeyDown = useCallback(
+19 -22
View File
@@ -14,11 +14,15 @@ export const isPrimaryPointerEvent = (event) => {
return type === 'click' || type === 'pointerdown' || type === 'pointerup'; return type === 'click' || type === 'pointerdown' || type === 'pointerup';
}; };
const EntryType = Object.freeze({
document: 'document',
folder: 'folder',
});
export const useEntryPointer = ({ export const useEntryPointer = ({
resolveDocumentRowKey, resolveDocumentRowKey,
resolveFolderRowKey, resolveFolderRowKey,
onSelectDocument, onSelectEntry,
onSelectFolder,
onInspectDocument, onInspectDocument,
}) => }) =>
useCallback( useCallback(
@@ -28,41 +32,34 @@ export const useEntryPointer = ({
} }
const { type, id } = entry; const { type, id } = entry;
if (type !== 'document' && type !== 'folder') { if (type !== EntryType.document && type !== EntryType.folder) {
return; return;
} }
const rowKey = entry.key const rowKey = entry.key
|| (type === 'document' ? resolveDocumentRowKey?.(id) : resolveFolderRowKey?.(id)); || (type === EntryType.document ? resolveDocumentRowKey?.(id) : resolveFolderRowKey?.(id));
if (!rowKey) { if (!rowKey) {
return; return;
} }
const modifierClick = isPointerModifierEvent(event); const modifierClick = isPointerModifierEvent(event);
const primaryClick = isPrimaryPointerEvent(event); const primaryClick = isPrimaryPointerEvent(event);
const metadata = { modifierClick, primaryClick, rowKey }; const metadata = { modifierClick, primaryClick, rowKey, type, id };
if (type === 'document') { if (typeof onSelectEntry === 'function') {
if (typeof onSelectDocument === 'function') { onSelectEntry(entry, event, metadata);
onSelectDocument(id, event, metadata);
} }
if (!modifierClick && primaryClick && typeof onInspectDocument === 'function') {
if (
type === EntryType.document
&& !modifierClick
&& primaryClick
&& typeof onInspectDocument === 'function'
) {
onInspectDocument(id, metadata); onInspectDocument(id, metadata);
} }
return;
}
if (typeof onSelectFolder === 'function') {
onSelectFolder(id, event, metadata);
}
}, },
[ [resolveDocumentRowKey, resolveFolderRowKey, onSelectEntry, onInspectDocument],
resolveDocumentRowKey,
resolveFolderRowKey,
onSelectDocument,
onSelectFolder,
onInspectDocument,
],
); );
export default useEntryPointer; export default useEntryPointer;
@@ -29,6 +29,10 @@ const useDocumentDragHandlers = ({
({ documents = [], folders = [] } = {}) => { ({ documents = [], folders = [] } = {}) => {
destroyDragPreview(); destroyDragPreview();
if (typeof document === 'undefined') {
return null;
}
const docEntries = (documents || []).filter(Boolean); const docEntries = (documents || []).filter(Boolean);
const folderEntries = (folders || []).filter(Boolean); const folderEntries = (folders || []).filter(Boolean);
const totalCount = docEntries.length + folderEntries.length; const totalCount = docEntries.length + folderEntries.length;
@@ -54,6 +58,8 @@ const useDocumentDragHandlers = ({
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = 'document-drag-preview'; wrapper.className = 'document-drag-preview';
wrapper.style.setProperty('--drag-preview-size', `${canvasSize}px`); wrapper.style.setProperty('--drag-preview-size', `${canvasSize}px`);
wrapper.style.width = `${canvasSize}px`;
wrapper.style.height = `${canvasSize}px`;
visibleItems.forEach((item, index) => { visibleItems.forEach((item, index) => {
const layer = document.createElement('div'); const layer = document.createElement('div');
@@ -101,15 +107,23 @@ const useDocumentDragHandlers = ({
layer.textContent = doc?.title || 'Document'; layer.textContent = doc?.title || 'Document';
} }
} else { } else {
const folderId = item.payload; const payload = item.payload;
const rowEl = folderId ? document.getElementById(`folder-row-${folderId}`) : null; const folderId = typeof payload === 'string' ? payload : payload?.id;
const iconEl = rowEl?.querySelector('.thumb-icon'); 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.width = `${size}px`;
layer.style.height = `${size}px`; layer.style.height = `${size}px`;
layer.classList.add('document-drag-preview__item--folder');
let content = null; let content = null;
if (iconEl instanceof HTMLElement) { 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'); content.classList.add('document-drag-preview__folder-thumb');
const svg = content.querySelector('svg'); const svg = content.querySelector('svg');
if (svg) { if (svg) {
@@ -276,11 +290,19 @@ const useDocumentDragHandlers = ({
console.warn('[documents] Failed to populate folder drag payload', error); console.warn('[documents] Failed to populate folder drag payload', error);
} }
const previewDocs = effectiveDocumentSelection const previewNode = createDragPreview({
documents: effectiveDocumentSelection
.map((id) => documentLookup.get(id) || null) .map((id) => documentLookup.get(id) || null)
.filter(Boolean); .filter(Boolean),
createDragPreview({ documents: previewDocs, folders: uniqueFolders }); folders: uniqueFolders,
});
event.currentTarget.classList.add('dragging'); 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, selectedFolderIds,
@@ -50,6 +50,11 @@ import useDocumentDragHandlers from './useDocumentDragHandlers';
import useDocumentMutations from './useDocumentMutations'; import useDocumentMutations from './useDocumentMutations';
import useDetailWorkspace from '../../detail/useDetailWorkspace'; import useDetailWorkspace from '../../detail/useDetailWorkspace';
const EntryType = Object.freeze({
document: 'document',
folder: 'folder',
});
const noop = () => {}; const noop = () => {};
const useDocumentsWorkspace = ({ const useDocumentsWorkspace = ({
@@ -1185,19 +1190,15 @@ const useDocumentsWorkspace = ({
const handleEntryPointerCore = useEntryPointerCore({ const handleEntryPointerCore = useEntryPointerCore({
resolveDocumentRowKey, resolveDocumentRowKey,
resolveFolderRowKey, resolveFolderRowKey,
onSelectDocument: (documentId, event, { rowKey }) => { onSelectEntry: (entry, event, { rowKey, modifierClick, primaryClick }) => {
const key = rowKey || resolveDocumentRowKey(documentId); const { type, id } = entry;
const key = rowKey
|| (type === EntryType.document ? resolveDocumentRowKey(id) : resolveFolderRowKey(id));
if (key) { if (key) {
handleEntrySelection(key, event); handleEntrySelection(key, event);
} }
}, if (type === EntryType.folder && !modifierClick && primaryClick) {
onSelectFolder: (folderId, event, { modifierClick, primaryClick, rowKey }) => { selectFolder(id);
const key = rowKey || resolveFolderRowKey(folderId);
if (key) {
handleEntrySelection(key, event);
}
if (!modifierClick && primaryClick) {
selectFolder(folderId);
} }
}, },
}); });
@@ -85,9 +85,8 @@ const useFolderTree = ({
})(); })();
setFocusedDocumentId(nextFocus); setFocusedDocumentId(nextFocus);
selectionAnchorRef.current = nextDocKeys.length const nextAnchor = mergedSelection.length ? mergedSelection[mergedSelection.length - 1] : null;
? nextDocKeys[nextDocKeys.length - 1] selectionAnchorRef.current = nextAnchor;
: null;
selectionOrderRef.current = mergedSelection; selectionOrderRef.current = mergedSelection;
setSelectionOrder(mergedSelection); setSelectionOrder(mergedSelection);
}, },
@@ -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));
}
+22 -2
View File
@@ -13,8 +13,28 @@
min-height: 0; 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; outline-offset: 2px;
} }
+6
View File
@@ -63,6 +63,12 @@
pointer-events: none; pointer-events: none;
} }
.document-drag-preview__item--folder {
background: transparent;
box-shadow: none;
border-radius: 0;
}
.document-drag-preview__folder-thumb { .document-drag-preview__folder-thumb {
width: 100%; width: 100%;
height: 100%; height: 100%;
+1
View File
@@ -6,6 +6,7 @@
@import './documents/controls.css'; @import './documents/controls.css';
@import './layout/structure.css'; @import './layout/structure.css';
@import './documents/viewer.css'; @import './documents/viewer.css';
@import './documents/drag-preview.css';
@import './documents/tags-correspondents.css'; @import './documents/tags-correspondents.css';
@import './documents/panel-sections.css'; @import './documents/panel-sections.css';
@import './sidebar/sidebar.css'; @import './sidebar/sidebar.css';
+5
View File
@@ -23,6 +23,11 @@
-webkit-user-select: none; -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 { .folder-row .name-wrap {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;