diff --git a/frontend/src/DesktopWorkspace.css b/frontend/src/DesktopWorkspace.css index 4aecad2..835c4f6 100644 --- a/frontend/src/DesktopWorkspace.css +++ b/frontend/src/DesktopWorkspace.css @@ -132,8 +132,7 @@ display: inline-flex; align-items: center; overflow: hidden; - box-shadow: 2px 2px 4px var(--shadow-faint); - background: color-mix(in oklch, var(--surface-subtle) 88%, transparent); + background: color-mix(in oklch, var(--surface-subtle) 90%, transparent); color: var(--muted); } @@ -150,7 +149,6 @@ pointer-events: auto; cursor: grab; transition: transform 0.16s ease, opacity 0.2s ease, box-shadow 0.2s ease; - box-shadow: 2px 2px 4px var(--shadow-medium); } .desk-help-overlay { diff --git a/frontend/src/DesktopWorkspace.jsx b/frontend/src/DesktopWorkspace.jsx index a811c68..db24a3c 100644 --- a/frontend/src/DesktopWorkspace.jsx +++ b/frontend/src/DesktopWorkspace.jsx @@ -9,7 +9,7 @@ import React, { import { createPortal } from 'react-dom'; import { resolveDocumentAssetUrl, createAssetView } from './asset_manager'; import { useAssetNavigator } from './hooks/useAssetNavigator'; -import { ArrowLeftIcon, ArrowRightIcon, CloseIcon, FileIcon, FolderOutlineIcon } from './ui/icons'; +import { ArrowLeftIcon, ArrowRightIcon, CloseIcon } from './ui/icons'; import SelectionFloatingActions from './documents/SelectionFloatingActions'; import { createDocumentsTableHeaderActions } from './documents/DocumentsPanel'; import createWorkspaceSurfaceConfig from './documents/workspaceHeader'; @@ -590,6 +590,7 @@ const DesktopWorkspace = ({ }) => { const items = useMemo(() => (searchResults ? searchResults : documents), [documents, searchResults]); + const allowLayoutPersistence = Boolean(tenantId && viewId && viewId.startsWith('folder:')); const containerRef = useRef(null); const layoutRef = useRef(new Map()); @@ -1120,7 +1121,7 @@ const recalcVisibleDocIds = useCallback(() => { const persistLayoutSnapshot = useCallback( async (snapshot, force = false) => { - if (!tenantId || !viewId) { + if (!allowLayoutPersistence || !tenantId || !viewId) { return; } if (!force && !layoutDirtyRef.current) { @@ -1160,7 +1161,7 @@ const recalcVisibleDocIds = useCallback(() => { await upsertLayoutRecords({ tenantId, viewId, entries: records }); }, - [tenantId, viewId], + [tenantId, viewId, allowLayoutPersistence], ); const syncLayoutSnapshot = useCallback((force = false) => { @@ -1207,6 +1208,11 @@ const recalcVisibleDocIds = useCallback(() => { return () => observer.disconnect(); }, []); useEffect(() => { + if (!allowLayoutPersistence) { + persistedLayoutRef.current = new Map(); + layoutDirtyRef.current = false; + return; + } if (!tenantId || !viewId) { return; } @@ -1245,7 +1251,7 @@ const recalcVisibleDocIds = useCallback(() => { return () => { cancelled = true; }; - }, [tenantId, viewId]); + }, [tenantId, viewId, allowLayoutPersistence]); useLayoutEffect(() => { if (!containerRef.current || !canvasSize.width || !canvasSize.height) { @@ -2496,6 +2502,7 @@ export const createDesktopSurface = ({ selectedDocumentIds, selectedFolderIds, onDeleteSelection, + onClearSelection, tags, correspondents, documentLookup, @@ -2506,6 +2513,7 @@ export const createDesktopSurface = ({ onBulkCorrespondentRemove, onBulkReanalyze, } = workspaceProps; + const title = Array.isArray(searchResults) ? 'Search results' : currentFolderName; const subtitle = Array.isArray(searchResults) ? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}` @@ -2515,46 +2523,6 @@ export const createDesktopSurface = ({ const documentSelectionCount = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.length : 0; const folderSelectionCount = Array.isArray(selectedFolderIds) ? selectedFolderIds.length : 0; const selectionCount = documentSelectionCount + folderSelectionCount; - let selectionLabel = null; - if (selectionCount) { - const tokens = []; - - if (documentSelectionCount) { - tokens.push({ - key: 'documents', - count: documentSelectionCount, - icon: , - }); - } - - if (folderSelectionCount) { - tokens.push({ - key: 'folders', - count: folderSelectionCount, - icon: , - }); - } - - if (tokens.length) { - selectionLabel = ( - - {tokens.map((token, index) => ( - - {index > 0 ? · : null} - - {token.count} - × - {token.icon} - - - ))} - - ); - } else { - selectionLabel = `${selectionCount} item${selectionCount === 1 ? '' : 's'}`; - } - } - const actions = createDocumentsTableHeaderActions({ viewMode: viewMode || 'desk', onViewModeChange, @@ -2562,11 +2530,12 @@ export const createDesktopSurface = ({ onShowDeskHelp: viewMode === 'desk' ? workspaceProps?.onOpenHelp : null, }); - const floatingActions = selectionLabel + const floatingActions = selectionCount > 0 ? ( ) : null; @@ -2593,7 +2563,7 @@ export const createDesktopSurface = ({ onNavigateParent, actions, breadcrumbs: workspaceProps?.breadcrumbs || null, - selectionLabel, + selectionLabel: null, floatingActions, content: , detail, diff --git a/frontend/src/documents/DocumentsPanel.jsx b/frontend/src/documents/DocumentsPanel.jsx index 09a7867..9eaefb7 100644 --- a/frontend/src/documents/DocumentsPanel.jsx +++ b/frontend/src/documents/DocumentsPanel.jsx @@ -6,8 +6,6 @@ import { RefreshIcon, MinusVerticalIcon, InfoIcon, - FileIcon, - FolderOutlineIcon, } from '../ui/icons'; import BreadcrumbTrail from '../ui/BreadcrumbTrail'; import createWorkspaceSurfaceConfig from './workspaceHeader'; @@ -609,6 +607,7 @@ export const createDocumentsSurface = ({ selectedDocumentIds, selectedFolderIds, onDeleteSelection, + onClearSelection, tags, correspondents, documentLookup, @@ -629,45 +628,6 @@ export const createDocumentsSurface = ({ const documentSelectionCount = Array.isArray(selectedDocumentIds) ? selectedDocumentIds.length : 0; const folderSelectionCount = Array.isArray(selectedFolderIds) ? selectedFolderIds.length : 0; const selectionCount = documentSelectionCount + folderSelectionCount; - let selectionLabel = null; - if (selectionCount) { - const tokens = []; - - if (documentSelectionCount) { - tokens.push({ - key: 'documents', - count: documentSelectionCount, - icon: , - }); - } - - if (folderSelectionCount) { - tokens.push({ - key: 'folders', - count: folderSelectionCount, - icon: , - }); - } - - if (tokens.length) { - selectionLabel = ( - - {tokens.map((token, index) => ( - - {index > 0 ? · : null} - - {token.count} - × - {token.icon} - - - ))} - - ); - } else { - selectionLabel = `${selectionCount} item${selectionCount === 1 ? '' : 's'}`; - } - } const actions = createDocumentsTableHeaderActions({ viewMode, @@ -675,11 +635,12 @@ export const createDocumentsSurface = ({ onRefresh, }); - const floatingActions = selectionLabel + const floatingActions = selectionCount > 0 ? ( ) : null; @@ -708,7 +670,7 @@ export const createDocumentsSurface = ({ onNavigateParent, actions, breadcrumbs, - selectionLabel, + selectionLabel: null, floatingActions, content: , detail, diff --git a/frontend/src/documents/SelectionFloatingActions.jsx b/frontend/src/documents/SelectionFloatingActions.jsx index c399eed..3e72cdb 100644 --- a/frontend/src/documents/SelectionFloatingActions.jsx +++ b/frontend/src/documents/SelectionFloatingActions.jsx @@ -1,6 +1,7 @@ import React, { useCallback, useMemo } from 'react'; -import { TrashIcon, AnalyzeIcon } from '../ui/icons'; +import { TrashIcon, AnalyzeIcon, IconX } from '../ui/icons'; import SelectionAssignmentMenu from './SelectionAssignmentMenu'; +import SelectionSummary from './SelectionSummary'; const normalizeDocumentList = (selectedDocumentIds) => Array.isArray(selectedDocumentIds) ? selectedDocumentIds.filter(Boolean) : []; @@ -112,6 +113,7 @@ const buildCorrespondentAssignments = (selectedDocuments, correspondents, total) const SelectionFloatingActions = ({ selectionCount = 0, selectedDocumentIds, + selectedFolderIds = [], documentLookup, tags, tagLookupById, @@ -122,12 +124,24 @@ const SelectionFloatingActions = ({ onBulkCorrespondentRemove, onBulkReanalyze, onDeleteSelection, + onClearSelection = null, }) => { const documentIdList = useMemo( () => normalizeDocumentList(selectedDocumentIds), [selectedDocumentIds], ); + const folderIdList = useMemo( + () => normalizeDocumentList(selectedFolderIds), + [selectedFolderIds], + ); + + const documentCount = documentIdList.length; + const folderCount = folderIdList.length; + const totalCount = typeof selectionCount === 'number' + ? selectionCount + : documentCount + folderCount; + const selectedDocuments = useMemo(() => { if (!documentIdList.length || !(documentLookup instanceof Map)) { return []; @@ -203,52 +217,77 @@ const SelectionFloatingActions = ({ [selectedDocCount, onBulkCorrespondentAdd, documentIdList], ); + const summaryNode = totalCount > 0 ? ( + + ) : null; + return ( -
- - - {typeof onBulkReanalyze === 'function' ? ( - + <> + {summaryNode ? ( + {summaryNode} ) : null} - {typeof onDeleteSelection === 'function' ? ( - - ) : null} -
+
+ + + {typeof onBulkReanalyze === 'function' ? ( + + ) : null} + {typeof onDeleteSelection === 'function' ? ( + + ) : null} + {typeof onClearSelection === 'function' ? ( + + ) : null} +
+ ); }; diff --git a/frontend/src/documents/SelectionSummary.jsx b/frontend/src/documents/SelectionSummary.jsx new file mode 100644 index 0000000..6505bbc --- /dev/null +++ b/frontend/src/documents/SelectionSummary.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { FileIcon, FolderOutlineIcon } from '../ui/icons'; + +const SelectionSummary = ({ documentCount = 0, folderCount = 0, totalCount = 0 }) => { + const docCount = Number(documentCount) || 0; + const folderCountNumber = Number(folderCount) || 0; + const aggregateCount = docCount + folderCountNumber; + const resolvedTotal = Number(totalCount) || aggregateCount; + + if (!docCount && !folderCountNumber && !resolvedTotal) { + return null; + } + + const tokens = []; + + if (docCount) { + tokens.push({ + key: 'documents', + count: docCount, + icon: , + }); + } + + if (folderCountNumber) { + tokens.push({ + key: 'folders', + count: folderCountNumber, + icon: , + }); + } + + if (!tokens.length) { + const count = resolvedTotal; + return ( + + {`${count} item${count === 1 ? '' : 's'}`} + + ); + } + + return ( + + {tokens.map((token, index) => ( + + {index > 0 ? · : null} + + {token.count} + {token.icon} + + + ))} + + ); +}; + +export default SelectionSummary; diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 2dba138..e527fe4 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2076,10 +2076,8 @@ button.danger:hover:not([disabled]) { height: 1rem; } -.selection-summary__multiply { - display: inline-flex; - align-items: center; - opacity: 0.6; +.selection-summary--text { + font-weight: 600; } .selection-summary__separator {