From b6300ffa30e6343ff5e9879a8758dd03ad1094bf Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Sun, 26 Oct 2025 19:14:35 +0100 Subject: [PATCH] panel actions --- frontend/src/documents/DocumentsTable.jsx | 80 ++++---- frontend/src/index.jsx | 215 ++++++++++++++++++-- frontend/src/sidebar/Sidebar.jsx | 233 ++++++++++++---------- frontend/src/skeuomorphic_ws.css | 49 ----- frontend/src/skeuomorphic_ws.jsx | 19 -- frontend/src/styles.css | 134 +++++++++---- frontend/src/ui/icons.js | 55 +++++ 7 files changed, 522 insertions(+), 263 deletions(-) diff --git a/frontend/src/documents/DocumentsTable.jsx b/frontend/src/documents/DocumentsTable.jsx index 954ae4f..9a23c13 100644 --- a/frontend/src/documents/DocumentsTable.jsx +++ b/frontend/src/documents/DocumentsTable.jsx @@ -177,6 +177,7 @@ const DocumentsTable = ({ viewMode = 'list', onViewModeChange, onClearSelection, + showHeader = true, }) => { const showingSearchResults = searchResults !== null; const rows = showingSearchResults ? searchResults : documents; @@ -220,6 +221,11 @@ const DocumentsTable = ({ }, [onViewModeChange], ); + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = 0; + } + }, [viewMode]); const isTagDragEvent = useCallback((event) => { const types = Array.from(event.dataTransfer?.types || []); return TAG_MIME_TYPES.some((type) => types.includes(type)); @@ -338,49 +344,51 @@ const DocumentsTable = ({
-
-
-

{currentFolderName}

- {showingSearchResults && ( -
Search results
- )} -
-
-
+ {showHeader ? ( +
+
+

{currentFolderName}

+ {showingSearchResults && ( +
Search results
+ )} +
+
+
+ + +
- +
- - -
-
+ ) : null} {showDefaultEmptyState && (
Drop files anywhere or onto a folder to upload documents. diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index bbe7ac2..05d6726 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -32,6 +32,16 @@ import CorrespondentsPanel from './correspondents/CorrespondentsPanel'; import { CORRESPONDENT_ROLES } from './constants/correspondents'; import TagManager from './tag_manager'; import Sidebar from './sidebar/Sidebar'; +import { + ChevronsRightIcon, + ChevronsLeftIcon, + ViewListIcon, + ViewGridIcon, + FolderPlusIcon, + RefreshIcon, + ArrowUpIcon, + MinusVerticalIcon, +} from './ui/icons'; import DocumentsTable from './documents/DocumentsTable'; import { AppShellContext, useAppShell } from './appShellContext'; import DocumentViewerRoute from './routes/DocumentViewerRoute'; @@ -311,9 +321,9 @@ const LoginView = ({ -const DocumentsLayout = ({ sidebarProps, children }) => ( -
- +const DocumentsLayout = ({ sidebarProps, children, sidebarCollapsed }) => ( +
+ {!sidebarCollapsed ? : null} {children}
); @@ -337,6 +347,9 @@ const AppLayout = () => { ({ message, variant }) => setStatusMessage(message, variant), [setStatusMessage], ); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const collapseSidebar = useCallback(() => setSidebarCollapsed(true), []); + const expandSidebar = useCallback(() => setSidebarCollapsed(false), []); const reportApiError = useApiError({ onReport: handleApiReport, }); @@ -4488,6 +4501,7 @@ const AppLayout = () => { isFilterActive, onLogout: handleLogout, status, + onCollapse: collapseSidebar, }; const resolveThumbnailUrlForDoc = useCallback( @@ -4847,36 +4861,209 @@ const DocumentsRoute = () => { skeuoWorkspaceProps, openTagsModal, openCorrespondentsModal, + exitSkeuoWorkspace, } = useAppShell(); + const navigate = useNavigate(); + + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const collapseSidebar = useCallback(() => setSidebarCollapsed(true), []); + const expandSidebar = useCallback(() => setSidebarCollapsed(false), []); const sidebarPropsWithActions = useMemo( () => ({ ...sidebarProps, onManageTags: openTagsModal, onManageCorrespondents: openCorrespondentsModal, + onCollapse: collapseSidebar, }), - [sidebarProps, openTagsModal, openCorrespondentsModal], + [sidebarProps, openTagsModal, openCorrespondentsModal, collapseSidebar], ); + const { + currentFolderName, + viewMode, + onViewModeChange, + onRequestCreateFolder, + creatingFolder, + onRefresh, + onShowSkeuoWorkspace, + searchResults, + breadcrumbs, + } = documentsTableProps; + + const isGridView = viewMode === 'grid'; + const showingSearchResults = Array.isArray(searchResults); + const headerTitle = showingSearchResults ? 'Search results' : currentFolderName; + const headerSubtitle = showingSearchResults + ? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}` + : null; + const parentBreadcrumb = breadcrumbs && breadcrumbs.length > 1 ? breadcrumbs[breadcrumbs.length - 2] : null; + const handleNavigateParent = parentBreadcrumb + ? () => { + const target = parentBreadcrumb.id === 'root' + ? '/documents' + : `/documents/folder/${parentBreadcrumb.id}`; + navigate(target); + } + : null; + + const detailHasContent = detailPanelProps.selectedDocuments?.length > 0; + const toggleSidebar = sidebarCollapsed ? expandSidebar : collapseSidebar; + const ToggleIcon = sidebarCollapsed ? ChevronsRightIcon : ChevronsLeftIcon; + const toggleLabel = sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'; + if (workspaceMode === 'skeuo') { return ( - +
- +
+
+ {sidebarCollapsed ? ( + + ) : null} + {parentBreadcrumb ? ( + + ) : null} +

+ {headerTitle} + {headerSubtitle ? ( + {headerSubtitle} + ) : null} +

+
+ + +
+
+
+ +
); } - const detailHasContent = detailPanelProps.selectedDocuments?.length > 0; - return ( - -
- - {detailHasContent ? : null} + +
+
+
+ {sidebarCollapsed ? ( + + ) : null} + {parentBreadcrumb ? ( + + ) : null} +

+ {headerTitle} + {headerSubtitle ? ( + {headerSubtitle} + ) : null} +

+
+
+ + +
+ + + + +
+
+
+ +
+ {detailHasContent ? ( + + ) : null}
); diff --git a/frontend/src/sidebar/Sidebar.jsx b/frontend/src/sidebar/Sidebar.jsx index e5bc356..11851b0 100644 --- a/frontend/src/sidebar/Sidebar.jsx +++ b/frontend/src/sidebar/Sidebar.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo } from 'react'; -import { ChevronIcon, TrashIcon, EditIcon, FolderIcon } from '../ui/icons'; +import { ChevronIcon, TrashIcon, EditIcon, FolderIcon, ChevronsLeftIcon } from '../ui/icons'; import { getTagColorStyle } from '../utils/colors'; @@ -148,6 +148,7 @@ const Sidebar = ({ previewActive, onLogout, status, + onCollapse, }) => { const sortedCorrespondents = useMemo( () => @@ -234,9 +235,30 @@ const Sidebar = ({ return ( ); diff --git a/frontend/src/skeuomorphic_ws.css b/frontend/src/skeuomorphic_ws.css index 00ec775..ff0da1a 100644 --- a/frontend/src/skeuomorphic_ws.css +++ b/frontend/src/skeuomorphic_ws.css @@ -15,55 +15,6 @@ position: relative; } -.skeuo-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - padding: 0.75rem 1rem 0.5rem; -} - -.skeuo-header__meta { - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -.skeuo-header__meta h2 { - margin: 0; - font-size: 1.25rem; - font-weight: 600; -} - -.skeuo-breadcrumbs { - display: flex; - flex-wrap: wrap; - gap: 0.35rem; - font-size: 0.85rem; - color: var(--muted); -} - -.skeuo-crumb { - display: inline-flex; - align-items: center; - gap: 0.35rem; -} - -.skeuo-crumb.is-current { - color: var(--fg); - font-weight: 600; -} - -.skeuo-crumb-separator { - opacity: 0.4; -} - -.skeuo-header__actions { - display: flex; - align-items: center; - gap: 0.5rem; -} - .skeuo-canvas { flex: 1; position: relative; diff --git a/frontend/src/skeuomorphic_ws.jsx b/frontend/src/skeuomorphic_ws.jsx index 1926b6b..39a580a 100644 --- a/frontend/src/skeuomorphic_ws.jsx +++ b/frontend/src/skeuomorphic_ws.jsx @@ -547,10 +547,6 @@ const getContrastingTextColor = (hex) => getReadableTextColor(hex, { light: '#1f const SkeuomorphicWorkspace = ({ documents = [], searchResults = null, - breadcrumbs = [], - currentFolderName = 'Folder', - onExit, - onRefresh, onDocumentOpen, resolveThumbnailUrl, onAssignTagToDocument = null, @@ -560,7 +556,6 @@ const SkeuomorphicWorkspace = ({ activeTagIds = [], }) => { const items = useMemo(() => (searchResults ? searchResults : documents), [documents, searchResults]); - const showingSearchResults = searchResults !== null; const containerRef = useRef(null); @@ -1491,20 +1486,6 @@ const SkeuomorphicWorkspace = ({ return (
-
-
-

{currentFolderName}

- {showingSearchResults && Showing search results} -
-
- - -
-
* { +.main-content__header { + padding: 0.5rem 1.25rem; + justify-content: space-between; + align-items: center; + gap: 0.75rem; +} + +.main-content__body { + position: relative; + flex: 1 1 auto; + display: flex; + min-height: 0; +} + +.main-content__body > * { min-width: 0; min-height: 0; } -.documents-main .main-content--documents > *:not(.detail-panel) { +.main-content__body > *:not(.detail-panel) { flex: 1 1 auto; } -.documents-main .main-content--documents.main-content--has-detail { +.main-content--has-detail { padding-right: var(--detail-panel-width); } +.main-content__title { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--fg); + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.main-content__subtitle { + font-size: 0.9rem; + color: var(--muted); + font-weight: 400; + line-height: 1.2; +} + + +.documents-main--sidebar-collapsed { + position: relative; + grid-template-columns: auto minmax(0, 1fr); +} + +.sidebar .panel-actions .icon, +.main-content__header .panel-actions .icon, +.detail-panel .panel-actions .icon { + width: 1.35rem; + height: 1.35rem; +} + +.main-content__actions { + display: flex; + align-items: center; + gap: 0.6rem; +} + +.main-content__actions-divider { + display: inline-flex; + align-items: center; + color: var(--muted); +} + .panels-main { flex: 1; display: flex; @@ -936,25 +992,26 @@ button.danger:hover:not([disabled]) { } .sidebar { - gap: 0.28rem; + gap: 0; overflow-y: auto; - padding: 1.25rem; color: var(--sidebar-fg); box-shadow: inset -12px 0 24px -12px var(--shadow-faint); display: flex; flex-direction: column; + max-width: 20em; } -.sidebar__meta { +.sidebar__body { + flex: 1; display: flex; flex-direction: column; - gap: 0.2rem; - margin-bottom: 1rem; + padding: 0.5rem 1rem; + gap: 0.75rem; } .sidebar__title { margin: 0; - font-size: 1.2rem; + font-size: 1rem; font-weight: 600; color: var(--fg); } @@ -968,7 +1025,7 @@ button.danger:hover:not([disabled]) { display: flex; align-items: center; gap: 0.4rem; - margin-top: 0.6rem; + margin: 0 0 0.75rem; } .sidebar__search input[type='search'] { @@ -994,10 +1051,6 @@ button.danger:hover:not([disabled]) { background: var(--sidebar-hover-bg); } -.sidebar__status { - margin-top: 0.75rem; -} - .sidebar__footer { margin-top: auto; padding-top: 1rem; @@ -1181,7 +1234,7 @@ button.danger:hover:not([disabled]) { .documents-panel { - padding: 1.25rem 1.25rem 0 1.25rem; + padding: 0 0.75rem 0.75rem 0.75rem; display: flex; flex-direction: column; min-height: 0; @@ -1653,9 +1706,12 @@ button.danger:hover:not([disabled]) { display: flex; align-items: center; justify-content: flex-start; - padding: 0.25rem; gap: 0.5rem; - padding: 0.25rem; + padding: 0.5rem; +} + +.panel-body { + padding: 0.5rem 1rem; } .panel-actions { @@ -1664,6 +1720,11 @@ button.danger:hover:not([disabled]) { flex-grow: 1; } +.panel-actions > h1:first-child, +.panel-actions > h2:first-child { + padding-left: 0.5rem; +} + .panel-actions .spacer { flex-grow: 1; } @@ -2033,7 +2094,6 @@ form.inline { padding: 0.45rem 0.8rem; border-radius: 2px; font-size: 0.85rem; - margin-bottom: 0.8rem; } .status-banner.info { diff --git a/frontend/src/ui/icons.js b/frontend/src/ui/icons.js index 7cc457c..b8533b6 100644 --- a/frontend/src/ui/icons.js +++ b/frontend/src/ui/icons.js @@ -9,10 +9,15 @@ import { IconLayoutGrid, IconArrowLeft, IconArrowRight, + IconArrowUp, IconChevronsRight, + IconChevronsLeft, IconAnalyze, IconWindowMaximize, IconTextScan2, + IconFolderPlus, + IconRefresh, + IconMinusVertical, } from '@tabler/icons-react'; import FolderSvg from '../assets/folder.svg'; @@ -124,6 +129,15 @@ export const ArrowRightIcon = ({ className, size = '1em', stroke = 1.6, ...rest /> ); +export const ChevronsLeftIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( + +); + export const ChevronsRightIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( ); +export const FolderPlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( + +); + +export const RefreshIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( + +); + +export const ArrowUpIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( + +); + +export const MinusVerticalIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( + +); + export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( (