panelheader

This commit is contained in:
2025-10-31 13:12:09 +01:00
parent 6b4a4af5f1
commit 321b0d84ae
6 changed files with 305 additions and 202 deletions
+19 -11
View File
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useAppShell } from '../appShellContext'; import { useAppShell } from '../appShellContext';
import DocumentsLayout from './DocumentsLayout'; import DocumentsLayout from './DocumentsLayout';
import { useWorkspaceSurface } from './useWorkspaceSurface'; import { useWorkspaceSurface } from './useWorkspaceSurface';
import PanelHeader from '../ui/PanelHeader';
const DocumentsRoute = () => { const DocumentsRoute = () => {
const { const {
@@ -103,6 +104,17 @@ const DocumentsRoute = () => {
const header = surface.header || null; const header = surface.header || null;
const headerTitle = header
? header.subtitle
? (
<h2 className="main-content__title">
{header.title}
<span className="main-content__subtitle">{header.subtitle}</span>
</h2>
)
: header.title
: null;
return ( return (
<DocumentsLayout <DocumentsLayout
sidebarProps={sidebarPropsWithActions} sidebarProps={sidebarPropsWithActions}
@@ -110,17 +122,13 @@ const DocumentsRoute = () => {
> >
<div className={mainContentClass}> <div className={mainContentClass}>
{header ? ( {header ? (
<div className="panel-header main-content__header"> <PanelHeader
{header.leading} className="main-content__header"
<h2 className="main-content__title"> leading={header.leading}
{header.title} title={headerTitle}
{header.subtitle ? ( titleTag="h2"
<span className="main-content__subtitle">{header.subtitle}</span> actions={header.actions}
) : null} />
</h2>
<div className="panel-actions__spacer" />
{header.actions}
</div>
) : null} ) : null}
<div className={bodyClass}>{surface.content}</div> <div className={bodyClass}>{surface.content}</div>
{surface.detail || null} {surface.detail || null}
+53 -22
View File
@@ -9,6 +9,7 @@ import {
WindowMaximizeIcon, WindowMaximizeIcon,
TextScanIcon, TextScanIcon,
} from '../ui/icons'; } from '../ui/icons';
import PanelHeader from '../ui/PanelHeader';
import { getTagColorStyle } from '../utils/colors'; import { getTagColorStyle } from '../utils/colors';
import { formatFileSize } from '../utils/format'; import { formatFileSize } from '../utils/format';
import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager'; import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
@@ -1239,11 +1240,10 @@ const DetailPanel = ({
const isBulkSelection = selectedCount > 1; const isBulkSelection = selectedCount > 1;
const showOcrAction = Boolean(singleDoc && singleHasOcr); const showOcrAction = Boolean(singleDoc && singleHasOcr);
return ( const headerLeading = [
<> (
<aside className="detail-panel panel">
<div className="panel-header">
<button <button
key="close"
type="button" type="button"
className="icon-button" className="icon-button"
onClick={onClose} onClick={onClose}
@@ -1252,8 +1252,13 @@ const DetailPanel = ({
> >
<ChevronsRightIcon /> <ChevronsRightIcon />
</button> </button>
{singleDoc ? ( ),
];
if (singleDoc) {
headerLeading.push(
<button <button
key="preview"
type="button" type="button"
className="icon-button" className="icon-button"
onClick={(event) => { onClick={(event) => {
@@ -1265,12 +1270,16 @@ const DetailPanel = ({
disabled={!singleHasPreview} disabled={!singleHasPreview}
> >
<WindowMaximizeIcon /> <WindowMaximizeIcon />
</button> </button>,
) : null} );
<h3 className="panel-header__title">{headerTitle}</h3> }
<div className="panel-actions__spacer" />
{isBulkSelection && onBulkReanalyze ? ( const headerActions = [];
if (isBulkSelection && onBulkReanalyze) {
headerActions.push(
<button <button
key="bulk-reanalyze"
type="button" type="button"
className="icon-button" className="icon-button"
onClick={(event) => { onClick={(event) => {
@@ -1281,10 +1290,14 @@ const DetailPanel = ({
title="Re-run analysis for selection" title="Re-run analysis for selection"
> >
<AnalyzeIcon /> <AnalyzeIcon />
</button> </button>,
) : null} );
{singleDoc && singleDownloadHref ? ( }
if (singleDoc && singleDownloadHref) {
headerActions.push(
<a <a
key="download"
className="icon-button" className="icon-button"
href={singleDownloadHref} href={singleDownloadHref}
target="_blank" target="_blank"
@@ -1294,10 +1307,14 @@ const DetailPanel = ({
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
> >
<DownloadIcon /> <DownloadIcon />
</a> </a>,
) : null} );
{showOcrAction ? ( }
if (showOcrAction) {
headerActions.push(
<button <button
key="ocr"
type="button" type="button"
className="icon-button" className="icon-button"
onClick={(event) => { onClick={(event) => {
@@ -1308,10 +1325,14 @@ const DetailPanel = ({
title="View OCR text" title="View OCR text"
> >
<TextScanIcon /> <TextScanIcon />
</button> </button>,
) : null} );
{singleDoc ? ( }
if (singleDoc) {
headerActions.push(
<button <button
key="reanalyze"
type="button" type="button"
className="icon-button" className="icon-button"
onClick={(event) => { onClick={(event) => {
@@ -1322,9 +1343,19 @@ const DetailPanel = ({
title="Re-run analysis" title="Re-run analysis"
> >
<AnalyzeIcon /> <AnalyzeIcon />
</button> </button>,
) : null} );
</div> }
return (
<>
<aside className="detail-panel panel">
<PanelHeader
leading={headerLeading}
title={headerTitle}
titleTag="h3"
actions={headerActions.length ? headerActions : null}
/>
<div className="panel-body"> <div className="panel-body">
{selectedCount <= 1 ? renderSingle() : renderBulk()} {selectedCount <= 1 ? renderSingle() : renderBulk()}
</div> </div>
+17 -5
View File
@@ -13,6 +13,7 @@ import {
FolderPlusIcon, FolderPlusIcon,
RestoreIcon, RestoreIcon,
} from '../ui/icons'; } from '../ui/icons';
import PanelHeader from '../ui/PanelHeader';
import { getTagColorStyle } from '../utils/colors'; import { getTagColorStyle } from '../utils/colors';
@@ -351,7 +352,10 @@ const Sidebar = ({
const rootNode = folderNodes.get('root'); const rootNode = folderNodes.get('root');
return ( return (
<aside className="sidebar"> <aside className="sidebar">
<div className="panel-header sidebar__header"> <PanelHeader
className="sidebar__header"
leading={(
<>
<button <button
type="button" type="button"
className={`sidebar__title-button${tenantMenuOpen ? ' is-open' : ''}`} className={`sidebar__title-button${tenantMenuOpen ? ' is-open' : ''}`}
@@ -422,9 +426,14 @@ const Sidebar = ({
</div> </div>
</div> </div>
) : null} ) : null}
<div className="panel-actions__spacer" /> </>
{onCollapse ? ( )}
actions={
onCollapse
? [
(
<button <button
key="collapse"
type="button" type="button"
className="icon-button" className="icon-button"
onClick={onCollapse} onClick={onCollapse}
@@ -433,8 +442,11 @@ const Sidebar = ({
> >
<ChevronsLeftIcon /> <ChevronsLeftIcon />
</button> </button>
) : null} ),
</div> ]
: null
}
/>
<div className="panel-body sidebar__body"> <div className="panel-body sidebar__body">
{status && ( {status && (
<div className="sidebar__status"> <div className="sidebar__status">
+16 -17
View File
@@ -1223,6 +1223,7 @@ button.danger:hover:not([disabled]) {
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
color: var(--fg); color: var(--fg);
padding-left: 0.25rem;
} }
.sidebar__tenant { .sidebar__tenant {
@@ -2198,11 +2199,17 @@ button.danger:hover:not([disabled]) {
.panel-header { .panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 0.75rem; gap: 0.75rem;
padding: 0.5rem; padding: 0.5rem;
} }
.panel-header__leading,
.panel-header__actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.panel-header__title { .panel-header__title {
margin: 0; margin: 0;
font-size: 1rem; font-size: 1rem;
@@ -2215,26 +2222,18 @@ button.danger:hover:not([disabled]) {
min-width: 0; min-width: 0;
} }
.panel-header > .panel-header__title:first-child {
padding-left: 0.5rem;
}
.panel-header__actions {
margin-left: auto;
}
.panel-body { .panel-body {
padding: 0.5rem 1rem; 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 { .detail-panel .panel-body {
flex: 1; flex: 1;
display: flex; display: flex;
+53
View File
@@ -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 (
<span className="panel-header__title" {...titleProps}>
{title}
</span>
);
}
const mergedClassName = composeClassName('panel-header__title', title.props.className || '');
return React.cloneElement(title, {
...titleProps,
className: mergedClassName,
});
}
const HeadingTag = titleTag;
return (
<HeadingTag className="panel-header__title" {...titleProps}>
{title}
</HeadingTag>
);
};
return (
<div className={headerClassName}>
{leading ? <div className="panel-header__leading">{leading}</div> : null}
{renderTitle()}
{actions ? <div className="panel-header__actions">{actions}</div> : null}
</div>
);
};
export default PanelHeader;