From 321b0d84ae04051fe8244e71d34e20d20a99cbf6 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 31 Oct 2025 13:12:09 +0100 Subject: [PATCH] panelheader --- frontend/src/app/DocumentsRoute.jsx | 30 ++-- frontend/src/detail/DetailPanel.jsx | 201 +++++++++++++--------- frontend/src/preview/PreviewWorkspace.jsx | 14 +- frontend/src/sidebar/Sidebar.jsx | 176 ++++++++++--------- frontend/src/styles.css | 33 ++-- frontend/src/ui/PanelHeader.jsx | 53 ++++++ 6 files changed, 305 insertions(+), 202 deletions(-) create mode 100644 frontend/src/ui/PanelHeader.jsx diff --git a/frontend/src/app/DocumentsRoute.jsx b/frontend/src/app/DocumentsRoute.jsx index 0a66381..52271b2 100644 --- a/frontend/src/app/DocumentsRoute.jsx +++ b/frontend/src/app/DocumentsRoute.jsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { useAppShell } from '../appShellContext'; import DocumentsLayout from './DocumentsLayout'; import { useWorkspaceSurface } from './useWorkspaceSurface'; +import PanelHeader from '../ui/PanelHeader'; const DocumentsRoute = () => { const { @@ -103,6 +104,17 @@ const DocumentsRoute = () => { const header = surface.header || null; + const headerTitle = header + ? header.subtitle + ? ( +

+ {header.title} + {header.subtitle} +

+ ) + : header.title + : null; + return ( { >
{header ? ( -
- {header.leading} -

- {header.title} - {header.subtitle ? ( - {header.subtitle} - ) : null} -

-
- {header.actions} -
+ ) : null}
{surface.content}
{surface.detail || null} diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx index 00c39cd..fe2635f 100644 --- a/frontend/src/detail/DetailPanel.jsx +++ b/frontend/src/detail/DetailPanel.jsx @@ -9,6 +9,7 @@ import { WindowMaximizeIcon, TextScanIcon, } from '../ui/icons'; +import PanelHeader from '../ui/PanelHeader'; import { getTagColorStyle } from '../utils/colors'; import { formatFileSize } from '../utils/format'; import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager'; @@ -1239,95 +1240,125 @@ const DetailPanel = ({ const isBulkSelection = selectedCount > 1; const showOcrAction = Boolean(singleDoc && singleHasOcr); + const headerLeading = [ + ( + + ), + ]; + + if (singleDoc) { + headerLeading.push( + , + ); + } + + const headerActions = []; + + if (isBulkSelection && onBulkReanalyze) { + headerActions.push( + , + ); + } + + if (singleDoc && singleDownloadHref) { + headerActions.push( + event.stopPropagation()} + > + + , + ); + } + + if (showOcrAction) { + headerActions.push( + , + ); + } + + if (singleDoc) { + headerActions.push( + , + ); + } + return ( <> ) : null} {hasOcr ? ( - - {tenantMenuOpen ? ( -
- {showTenantList ? ( - <> -
Switch tenant
-
- {tenants.map((tenant) => { - const tenantId = tenant?.id || null; - const isActive = tenantId === activeTenantId; - const tenantLabel = tenant?.name; - return ( - - ); - })} + + + {tenantMenuOpen ? ( +
+ {showTenantList ? ( + <> +
Switch tenant
+
+ {tenants.map((tenant) => { + const tenantId = tenant?.id || null; + const isActive = tenantId === activeTenantId; + const tenantLabel = tenant?.name; + return ( + + ); + })} +
+ + ) : null} +
+ +
- +
) : null} -
- - -
-
- ) : null} -
- {onCollapse ? ( - - ) : null} -
+ + )} + actions={ + onCollapse + ? [ + ( + + ), + ] + : null + } + />
{status && (
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d67874a..d12131d 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1223,6 +1223,7 @@ button.danger:hover:not([disabled]) { font-size: 1rem; font-weight: 600; color: var(--fg); + padding-left: 0.25rem; } .sidebar__tenant { @@ -2198,11 +2199,17 @@ button.danger:hover:not([disabled]) { .panel-header { display: flex; align-items: center; - justify-content: space-between; gap: 0.75rem; padding: 0.5rem; } +.panel-header__leading, +.panel-header__actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + .panel-header__title { margin: 0; font-size: 1rem; @@ -2215,26 +2222,18 @@ button.danger:hover:not([disabled]) { min-width: 0; } +.panel-header > .panel-header__title:first-child { + padding-left: 0.5rem; +} + +.panel-header__actions { + margin-left: auto; +} + .panel-body { padding: 0.5rem 1rem; } -.panel-actions { - display: flex; - align-items: center; - gap: 0.5rem; - flex: 1 1 auto; -} - -.panel-actions__spacer { - flex-grow: 1; -} - -.panel-actions > h1:first-child, -.panel-actions > h2:first-child { - padding-left: 0.5rem; -} - .detail-panel .panel-body { flex: 1; display: flex; diff --git a/frontend/src/ui/PanelHeader.jsx b/frontend/src/ui/PanelHeader.jsx new file mode 100644 index 0000000..e7bad5f --- /dev/null +++ b/frontend/src/ui/PanelHeader.jsx @@ -0,0 +1,53 @@ +import React from 'react'; + +const composeClassName = (base, extra) => (extra ? `${base} ${extra}` : base); + +const PanelHeader = ({ + className = '', + leading = null, + title = null, + titleTag = 'h2', + titleProps = {}, + actions = null, +}) => { + const headerClassName = composeClassName('panel-header', className); + + const renderTitle = () => { + if (title == null) { + return null; + } + + if (React.isValidElement(title)) { + if (title.type === React.Fragment) { + return ( + + {title} + + ); + } + + const mergedClassName = composeClassName('panel-header__title', title.props.className || ''); + return React.cloneElement(title, { + ...titleProps, + className: mergedClassName, + }); + } + + const HeadingTag = titleTag; + return ( + + {title} + + ); + }; + + return ( +
+ {leading ?
{leading}
: null} + {renderTitle()} + {actions ?
{actions}
: null} +
+ ); +}; + +export default PanelHeader;