This commit is contained in:
2025-11-18 13:16:35 +01:00
parent 0b0a1a88fd
commit 0c37ef5578
20 changed files with 434 additions and 417 deletions
-23
View File
@@ -1,23 +0,0 @@
import React, { ReactNode } from 'react';
import Sidebar from '../sidebar/Sidebar';
import { useSidebarContext } from '../sidebar/SidebarContext';
import { usePanelManager } from './PanelManagerContext';
interface DocumentsLayoutProps {
sidebarProps?: Record<string, unknown>;
children?: ReactNode;
}
const DocumentsLayout: React.FC<DocumentsLayoutProps> = ({ sidebarProps = {}, children }) => {
const { collapsed } = useSidebarContext();
const { sidebarSuppressed } = usePanelManager();
const sidebarHidden = collapsed || sidebarSuppressed;
return (
<main className={`documents-main${sidebarHidden ? ' documents-main--sidebar-hidden' : ''}`}>
{!sidebarHidden ? <Sidebar {...sidebarProps} /> : null}
{children}
</main>
);
};
export default DocumentsLayout;
+22 -69
View File
@@ -2,17 +2,14 @@ import React, { useCallback, useEffect, useMemo } from 'react';
import type { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAppShell } from '../appShellContext';
import DocumentsLayout from './DocumentsLayout';
import { useWorkspaceSurface } from './useWorkspaceSurface';
import PanelHeader from '../ui/PanelHeader';
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHeader';
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
import { PanelManagerProvider, usePanelManager } from './PanelManagerContext';
import Sidebar from '../sidebar/Sidebar';
type Identifier = string | number;
type Breadcrumb = { id?: Identifier; name?: string; label?: string; title?: string };
type EnsureAssetUrl = (
docId: Identifier,
asset: unknown,
@@ -25,7 +22,8 @@ type ResolveApiPath = (path: string) => string;
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
interface DocumentsTableProps {
breadcrumbs?: Breadcrumb[] | null;
breadcrumbs?: DocumentsHeaderBreadcrumb[] | null;
onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void;
[key: string]: unknown;
}
@@ -88,7 +86,7 @@ const DocumentsRouteContent: React.FC = () => {
const sidebarHidden = sidebarCollapsed || sidebarSuppressed;
const handleHeaderBreadcrumbClick = useCallback((crumb: Breadcrumb) => {
const handleHeaderBreadcrumbClick = useCallback((crumb: DocumentsHeaderBreadcrumb) => {
if (!crumb || !crumb.id) {
return;
}
@@ -96,10 +94,16 @@ const DocumentsRouteContent: React.FC = () => {
navigate(target);
}, [navigate]);
const documentsTablePropsWithNav = useMemo(() => (
documentsTableProps
? { ...documentsTableProps, onBreadcrumbNavigate: handleHeaderBreadcrumbClick }
: null
), [documentsTableProps, handleHeaderBreadcrumbClick]);
const { surface } = useWorkspaceSurface({
sidebarHidden,
onExpandSidebar: expandSidebar,
documentsTableProps,
documentsTableProps: documentsTablePropsWithNav,
detailPanelProps,
detailPanelOpen,
previewWorkspaceDocument,
@@ -122,75 +126,24 @@ const DocumentsRouteContent: React.FC = () => {
if (!surface) {
return (
<DocumentsLayout sidebarProps={sidebarPropsWithActions}>
<div className="main-content main-content--documents">
<div className="main-content__body main-content__body--documents" />
<main className={`documents-main${sidebarHidden ? ' documents-main--sidebar-hidden' : ''}`}>
{!sidebarHidden ? <Sidebar {...sidebarPropsWithActions} /> : null}
<div className="main-content">
<div className="main-content__body" />
</div>
</DocumentsLayout>
</main>
);
}
const variant = surface.variant || 'documents';
const surfaceDetail = (surface as { detail?: ReactNode }).detail || null;
const hasDetail = Boolean(surfaceDetail);
const mainContentClass = `main-content main-content--${variant}${
hasDetail ? ' main-content--has-detail' : ''
}`;
const bodyClass = `main-content__body main-content__body--${variant}${
hasDetail ? ' main-content__body--has-detail' : ''
}`;
const header = surface.header || null;
let headerTitle: React.ReactNode = null;
if (header) {
const breadcrumbEntries = Array.isArray(header.breadcrumbs) ? header.breadcrumbs.filter(Boolean) : [];
const lastIndex = breadcrumbEntries.length - 1;
const trailEntries = breadcrumbEntries.length
? breadcrumbEntries.map((crumb, index) => ({
id: crumb.id ?? index,
label: crumb.name ?? crumb.label ?? crumb.title ?? '',
onClick: index < lastIndex ? () => handleHeaderBreadcrumbClick(crumb) : null,
}))
: [{ id: 'current-location', label: header.title }];
headerTitle = (
<h2 className="main-content__title">
<BreadcrumbTrail
entries={trailEntries}
className="main-content__breadcrumbs"
separator="/"
/>
{header.subtitle ? (
<span className="main-content__subtitle">{header.subtitle}</span>
) : null}
</h2>
);
}
return (
<DocumentsLayout sidebarProps={sidebarPropsWithActions}>
<div className={mainContentClass}>
{header ? (
<>
<div className="main-content__header-wrapper">
<PanelHeader
className="main-content__header"
leading={header.leading}
title={headerTitle}
titleTag="h2"
actions={header.actions}
/>
</div>
{(header.floatingActions)}
</>
) : null}
<div className={bodyClass}>{surface.content}</div>
<main className={`documents-main${sidebarHidden ? ' documents-main--sidebar-hidden' : ''}`}>
{!sidebarHidden ? <Sidebar {...sidebarPropsWithActions} /> : null}
<div className="main-content">
<div className="main-content__body">{surface.content}</div>
{surfaceDetail}
</div>
</DocumentsLayout>
</main>
);
};
+6 -6
View File
@@ -98,12 +98,13 @@ const persistWidth = (panel: PanelKey, value: number): void => {
window.localStorage.setItem(STORAGE_KEYS[panel], String(Math.round(value)));
};
const applyPanelWidthToRoot = (panel: PanelKey, width: number): void => {
const applyPanelWidthToRoot = (panel: PanelKey, width: number, active: boolean): void => {
if (!Number.isFinite(width)) {
return;
}
const varName = panel === 'sidebar' ? '--sidebar-width' : '--detail-panel-width';
document.documentElement.style.setProperty(varName, `${width}px`);
const resolvedValue = panel === 'detail' && !active ? '0px' : `${width}px`;
document.documentElement.style.setProperty(varName, resolvedValue);
};
interface PanelManagerProviderProps {
@@ -132,13 +133,13 @@ export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ chil
useEffect(() => {
panelWidthsRef.current.sidebar = sidebarWidth;
applyPanelWidthToRoot('sidebar', sidebarWidth);
applyPanelWidthToRoot('sidebar', sidebarWidth, true);
}, [sidebarWidth]);
useEffect(() => {
panelWidthsRef.current.detail = detailWidth;
applyPanelWidthToRoot('detail', detailWidth);
}, [detailWidth]);
applyPanelWidthToRoot('detail', detailWidth, detailPanelOpen);
}, [detailWidth, detailPanelOpen]);
const handlePanelLayoutChange = useCallback((
panel: PanelKey,
@@ -187,7 +188,6 @@ export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ chil
setDetailWidthState((prev) => (prev === clamped ? prev : clamped));
}
panelWidthsRef.current[panel] = clamped;
applyPanelWidthToRoot(panel, clamped);
if (commit) {
preferredPanelWidthsRef.current[panel] = clamped;
persistWidth(panel, clamped);
+68 -82
View File
@@ -9,7 +9,6 @@ import {
WarningIcon,
BottombarCollapseIcon,
BottombarExpandIcon,
ClearAllIcon,
} from '../ui/icons';
import PanelHeader from '../ui/PanelHeader';
@@ -86,11 +85,21 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
const hasActiveUploads = queue.some((item) => item.status === 'uploading' || item.status === 'pending');
const handleClearQueue = () => {
if (!onClearQueue || hasActiveUploads) {
const handleDismissOverlay = () => {
if (!queue.length) {
setDismissed(true);
return;
}
onClearQueue();
if (hasActiveUploads) {
const confirmed = window.confirm('Uploads are still running. Clear the queue and hide the overlay?');
if (!confirmed) {
return;
}
}
onClearQueue?.();
setDismissed(true);
};
if (!queue.length || dismissed) {
@@ -100,11 +109,10 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
return (
<div className={`upload-queue-overlay${collapsed ? ' upload-queue-overlay--collapsed' : ''}`}>
<PanelHeader
className="upload-queue-overlay__header"
title={(
<span className="upload-queue-overlay__title">
<span>
<span>Uploads</span>
<span className="upload-queue-overlay__summary">{summary}</span>
<span className="panel-header__subtitle">{summary}</span>
</span>
)}
actions={(
@@ -117,20 +125,11 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
>
{collapsed ? <BottombarExpandIcon size={16} /> : <BottombarCollapseIcon size={16} />}
</button>
<button
type="button"
className="icon-button ghost"
onClick={handleClearQueue}
disabled={hasActiveUploads || !queue.length}
aria-label="Clear completed uploads"
>
<ClearAllIcon size={16} />
</button>
<button
type="button"
className="icon-button"
onClick={() => setDismissed(true)}
aria-label="Hide upload queue"
onClick={handleDismissOverlay}
aria-label="Clear uploads and hide overlay"
>
<CloseIcon size={16} />
</button>
@@ -138,82 +137,69 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
)}
/>
{!collapsed ? (
<ul className="upload-queue-overlay__list">
{[...queue]
.slice()
.reverse()
.map((item) => {
const meta = STATUS_META[item.status] || STATUS_META.pending;
const fileLabel = item.name;
const duplicateLabel = item.status === 'duplicate' ? item.document?.title || null : null;
const documentId = item.document?.id || item.conflictDocumentId || null;
const hasLink = Boolean(documentId);
const handleNavigate = () => {
if (!documentId) {
return;
}
navigate(`/documents/${documentId}`);
};
return (
<li key={item.id} className={`upload-queue-overlay__item upload-queue-overlay__item--${item.status}`}>
<span className={`upload-queue-overlay__status upload-queue-overlay__status--${meta.tone}`}>
{meta.icon}
</span>
<div className="upload-queue-overlay__details">
{item.status === 'success' && hasLink ? (
<button
type="button"
className="upload-queue-overlay__name-link"
onClick={handleNavigate}
>
{fileLabel}
</button>
) : (
<div className="upload-queue-overlay__name">
{fileLabel}
</div>
)}
<div className="upload-queue-overlay__meta-line">
{item.status === 'duplicate' && duplicateLabel ? (
<span className="upload-queue-overlay__meta-duplicate">
Duplicate of{' '}
<div className="upload-queue-overlay__body">
<ul className="upload-queue-overlay__list">
{[...queue]
.slice()
.reverse()
.map((item) => {
const meta = STATUS_META[item.status] || STATUS_META.pending;
const fileLabel = item.name;
const documentTitle = item.document?.title || null;
const duplicateLabel = item.status === 'duplicate' ? documentTitle : null;
const documentId = item.document?.id || item.conflictDocumentId || null;
const hasLink = Boolean(documentId);
const handleNavigate = () => {
if (!documentId) {
return;
}
navigate(`/documents/${documentId}`);
};
return (
<li key={item.id} className={`upload-queue-overlay__item upload-queue-overlay__item--${item.status}`}>
<span className={`upload-queue-overlay__status upload-queue-overlay__status--${meta.tone}`}>
{meta.icon}
</span>
<div className="upload-queue-overlay__details">
{item.status === 'success' && hasLink ? (
<button
type="button"
className="upload-queue-overlay__meta-link"
className="upload-queue-overlay__name-link"
onClick={handleNavigate}
>
{duplicateLabel}
{fileLabel}
</button>
</span>
) : item.status === 'error' && item.error ? (
<span className="upload-queue-overlay__meta-error" title={item.error}>
{item.error}
</span>
) : (
<>
<span>{meta.label}</span>
{documentId ? (
<span className="upload-queue-overlay__meta-id">
(
) : (
<div className="upload-queue-overlay__name">
{fileLabel}
</div>
)}
<div className="upload-queue-overlay__meta-line">
{item.status === 'duplicate' && duplicateLabel ? (
<span className="upload-queue-overlay__meta-duplicate">
Duplicate of{' '}
<button
type="button"
className="upload-queue-overlay__meta-link"
onClick={handleNavigate}
disabled={!hasLink}
>
{documentId}
{duplicateLabel}
</button>
)
</span>
) : null}
</>
)}
</div>
</div>
</li>
);
})}
</ul>
) : item.status === 'error' && item.error ? (
<span className="upload-queue-overlay__meta-error" title={item.error}>
{item.error}
</span>
) : (
<span>{meta.label}</span>
)}
</div>
</div>
</li>
);
})}
</ul>
</div>
) : null}
</div>
);