cleanup
This commit is contained in:
@@ -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;
|
|
||||||
@@ -2,17 +2,14 @@ import React, { useCallback, useEffect, useMemo } from 'react';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAppShell } from '../appShellContext';
|
import { useAppShell } from '../appShellContext';
|
||||||
import DocumentsLayout from './DocumentsLayout';
|
|
||||||
import { useWorkspaceSurface } from './useWorkspaceSurface';
|
import { useWorkspaceSurface } from './useWorkspaceSurface';
|
||||||
import PanelHeader from '../ui/PanelHeader';
|
import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHeader';
|
||||||
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
|
||||||
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
|
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
|
||||||
import { PanelManagerProvider, usePanelManager } from './PanelManagerContext';
|
import { PanelManagerProvider, usePanelManager } from './PanelManagerContext';
|
||||||
|
import Sidebar from '../sidebar/Sidebar';
|
||||||
|
|
||||||
type Identifier = string | number;
|
type Identifier = string | number;
|
||||||
|
|
||||||
type Breadcrumb = { id?: Identifier; name?: string; label?: string; title?: string };
|
|
||||||
|
|
||||||
type EnsureAssetUrl = (
|
type EnsureAssetUrl = (
|
||||||
docId: Identifier,
|
docId: Identifier,
|
||||||
asset: unknown,
|
asset: unknown,
|
||||||
@@ -25,7 +22,8 @@ type ResolveApiPath = (path: string) => string;
|
|||||||
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
|
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
|
||||||
|
|
||||||
interface DocumentsTableProps {
|
interface DocumentsTableProps {
|
||||||
breadcrumbs?: Breadcrumb[] | null;
|
breadcrumbs?: DocumentsHeaderBreadcrumb[] | null;
|
||||||
|
onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +86,7 @@ const DocumentsRouteContent: React.FC = () => {
|
|||||||
|
|
||||||
const sidebarHidden = sidebarCollapsed || sidebarSuppressed;
|
const sidebarHidden = sidebarCollapsed || sidebarSuppressed;
|
||||||
|
|
||||||
const handleHeaderBreadcrumbClick = useCallback((crumb: Breadcrumb) => {
|
const handleHeaderBreadcrumbClick = useCallback((crumb: DocumentsHeaderBreadcrumb) => {
|
||||||
if (!crumb || !crumb.id) {
|
if (!crumb || !crumb.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -96,10 +94,16 @@ const DocumentsRouteContent: React.FC = () => {
|
|||||||
navigate(target);
|
navigate(target);
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
|
const documentsTablePropsWithNav = useMemo(() => (
|
||||||
|
documentsTableProps
|
||||||
|
? { ...documentsTableProps, onBreadcrumbNavigate: handleHeaderBreadcrumbClick }
|
||||||
|
: null
|
||||||
|
), [documentsTableProps, handleHeaderBreadcrumbClick]);
|
||||||
|
|
||||||
const { surface } = useWorkspaceSurface({
|
const { surface } = useWorkspaceSurface({
|
||||||
sidebarHidden,
|
sidebarHidden,
|
||||||
onExpandSidebar: expandSidebar,
|
onExpandSidebar: expandSidebar,
|
||||||
documentsTableProps,
|
documentsTableProps: documentsTablePropsWithNav,
|
||||||
detailPanelProps,
|
detailPanelProps,
|
||||||
detailPanelOpen,
|
detailPanelOpen,
|
||||||
previewWorkspaceDocument,
|
previewWorkspaceDocument,
|
||||||
@@ -122,75 +126,24 @@ const DocumentsRouteContent: React.FC = () => {
|
|||||||
|
|
||||||
if (!surface) {
|
if (!surface) {
|
||||||
return (
|
return (
|
||||||
<DocumentsLayout sidebarProps={sidebarPropsWithActions}>
|
<main className={`documents-main${sidebarHidden ? ' documents-main--sidebar-hidden' : ''}`}>
|
||||||
<div className="main-content main-content--documents">
|
{!sidebarHidden ? <Sidebar {...sidebarPropsWithActions} /> : null}
|
||||||
<div className="main-content__body main-content__body--documents" />
|
<div className="main-content">
|
||||||
|
<div className="main-content__body" />
|
||||||
</div>
|
</div>
|
||||||
</DocumentsLayout>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const variant = surface.variant || 'documents';
|
|
||||||
|
|
||||||
const surfaceDetail = (surface as { detail?: ReactNode }).detail || null;
|
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 (
|
return (
|
||||||
<DocumentsLayout sidebarProps={sidebarPropsWithActions}>
|
<main className={`documents-main${sidebarHidden ? ' documents-main--sidebar-hidden' : ''}`}>
|
||||||
<div className={mainContentClass}>
|
{!sidebarHidden ? <Sidebar {...sidebarPropsWithActions} /> : null}
|
||||||
{header ? (
|
<div className="main-content">
|
||||||
<>
|
<div className="main-content__body">{surface.content}</div>
|
||||||
<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>
|
|
||||||
{surfaceDetail}
|
{surfaceDetail}
|
||||||
</div>
|
</div>
|
||||||
</DocumentsLayout>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -98,12 +98,13 @@ const persistWidth = (panel: PanelKey, value: number): void => {
|
|||||||
window.localStorage.setItem(STORAGE_KEYS[panel], String(Math.round(value)));
|
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)) {
|
if (!Number.isFinite(width)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const varName = panel === 'sidebar' ? '--sidebar-width' : '--detail-panel-width';
|
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 {
|
interface PanelManagerProviderProps {
|
||||||
@@ -132,13 +133,13 @@ export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ chil
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
panelWidthsRef.current.sidebar = sidebarWidth;
|
panelWidthsRef.current.sidebar = sidebarWidth;
|
||||||
applyPanelWidthToRoot('sidebar', sidebarWidth);
|
applyPanelWidthToRoot('sidebar', sidebarWidth, true);
|
||||||
}, [sidebarWidth]);
|
}, [sidebarWidth]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
panelWidthsRef.current.detail = detailWidth;
|
panelWidthsRef.current.detail = detailWidth;
|
||||||
applyPanelWidthToRoot('detail', detailWidth);
|
applyPanelWidthToRoot('detail', detailWidth, detailPanelOpen);
|
||||||
}, [detailWidth]);
|
}, [detailWidth, detailPanelOpen]);
|
||||||
|
|
||||||
const handlePanelLayoutChange = useCallback((
|
const handlePanelLayoutChange = useCallback((
|
||||||
panel: PanelKey,
|
panel: PanelKey,
|
||||||
@@ -187,7 +188,6 @@ export const PanelManagerProvider: React.FC<PanelManagerProviderProps> = ({ chil
|
|||||||
setDetailWidthState((prev) => (prev === clamped ? prev : clamped));
|
setDetailWidthState((prev) => (prev === clamped ? prev : clamped));
|
||||||
}
|
}
|
||||||
panelWidthsRef.current[panel] = clamped;
|
panelWidthsRef.current[panel] = clamped;
|
||||||
applyPanelWidthToRoot(panel, clamped);
|
|
||||||
if (commit) {
|
if (commit) {
|
||||||
preferredPanelWidthsRef.current[panel] = clamped;
|
preferredPanelWidthsRef.current[panel] = clamped;
|
||||||
persistWidth(panel, clamped);
|
persistWidth(panel, clamped);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
WarningIcon,
|
WarningIcon,
|
||||||
BottombarCollapseIcon,
|
BottombarCollapseIcon,
|
||||||
BottombarExpandIcon,
|
BottombarExpandIcon,
|
||||||
ClearAllIcon,
|
|
||||||
} from '../ui/icons';
|
} from '../ui/icons';
|
||||||
import PanelHeader from '../ui/PanelHeader';
|
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 hasActiveUploads = queue.some((item) => item.status === 'uploading' || item.status === 'pending');
|
||||||
|
|
||||||
const handleClearQueue = () => {
|
const handleDismissOverlay = () => {
|
||||||
if (!onClearQueue || hasActiveUploads) {
|
if (!queue.length) {
|
||||||
|
setDismissed(true);
|
||||||
return;
|
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) {
|
if (!queue.length || dismissed) {
|
||||||
@@ -100,11 +109,10 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
|
|||||||
return (
|
return (
|
||||||
<div className={`upload-queue-overlay${collapsed ? ' upload-queue-overlay--collapsed' : ''}`}>
|
<div className={`upload-queue-overlay${collapsed ? ' upload-queue-overlay--collapsed' : ''}`}>
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
className="upload-queue-overlay__header"
|
|
||||||
title={(
|
title={(
|
||||||
<span className="upload-queue-overlay__title">
|
<span>
|
||||||
<span>Uploads</span>
|
<span>Uploads</span>
|
||||||
<span className="upload-queue-overlay__summary">{summary}</span>
|
<span className="panel-header__subtitle">{summary}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
actions={(
|
actions={(
|
||||||
@@ -117,20 +125,11 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
|
|||||||
>
|
>
|
||||||
{collapsed ? <BottombarExpandIcon size={16} /> : <BottombarCollapseIcon size={16} />}
|
{collapsed ? <BottombarExpandIcon size={16} /> : <BottombarCollapseIcon size={16} />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-button ghost"
|
|
||||||
onClick={handleClearQueue}
|
|
||||||
disabled={hasActiveUploads || !queue.length}
|
|
||||||
aria-label="Clear completed uploads"
|
|
||||||
>
|
|
||||||
<ClearAllIcon size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-button"
|
className="icon-button"
|
||||||
onClick={() => setDismissed(true)}
|
onClick={handleDismissOverlay}
|
||||||
aria-label="Hide upload queue"
|
aria-label="Clear uploads and hide overlay"
|
||||||
>
|
>
|
||||||
<CloseIcon size={16} />
|
<CloseIcon size={16} />
|
||||||
</button>
|
</button>
|
||||||
@@ -138,6 +137,7 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!collapsed ? (
|
{!collapsed ? (
|
||||||
|
<div className="upload-queue-overlay__body">
|
||||||
<ul className="upload-queue-overlay__list">
|
<ul className="upload-queue-overlay__list">
|
||||||
{[...queue]
|
{[...queue]
|
||||||
.slice()
|
.slice()
|
||||||
@@ -145,7 +145,8 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
|
|||||||
.map((item) => {
|
.map((item) => {
|
||||||
const meta = STATUS_META[item.status] || STATUS_META.pending;
|
const meta = STATUS_META[item.status] || STATUS_META.pending;
|
||||||
const fileLabel = item.name;
|
const fileLabel = item.name;
|
||||||
const duplicateLabel = item.status === 'duplicate' ? item.document?.title || null : null;
|
const documentTitle = item.document?.title || null;
|
||||||
|
const duplicateLabel = item.status === 'duplicate' ? documentTitle : null;
|
||||||
const documentId = item.document?.id || item.conflictDocumentId || null;
|
const documentId = item.document?.id || item.conflictDocumentId || null;
|
||||||
const hasLink = Boolean(documentId);
|
const hasLink = Boolean(documentId);
|
||||||
const handleNavigate = () => {
|
const handleNavigate = () => {
|
||||||
@@ -190,23 +191,7 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
|
|||||||
{item.error}
|
{item.error}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
|
||||||
<span>{meta.label}</span>
|
<span>{meta.label}</span>
|
||||||
{documentId ? (
|
|
||||||
<span className="upload-queue-overlay__meta-id">
|
|
||||||
(
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="upload-queue-overlay__meta-link"
|
|
||||||
onClick={handleNavigate}
|
|
||||||
disabled={!hasLink}
|
|
||||||
>
|
|
||||||
{documentId}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,6 +199,7 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
import DocumentsGrid from '../DocumentsGrid';
|
import DocumentsGrid from '../DocumentsGrid';
|
||||||
import DocumentsList from '../DocumentsList';
|
import DocumentsList from '../DocumentsList';
|
||||||
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
|
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
|
||||||
@@ -6,6 +7,10 @@ import { isTagTransferEvent } from '../tagTransfer';
|
|||||||
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
|
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
|
||||||
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
|
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
|
||||||
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
|
import { useWorkspaceSelectionContext } from '../../app/WorkspaceSelectionContext';
|
||||||
|
import DocumentsPanelHeader, {
|
||||||
|
DocumentsPanelHeaderConfig,
|
||||||
|
DocumentsHeaderBreadcrumb,
|
||||||
|
} from './DocumentsPanelHeader';
|
||||||
|
|
||||||
const DEFAULT_GRID_ICON_SIZE = 144;
|
const DEFAULT_GRID_ICON_SIZE = 144;
|
||||||
|
|
||||||
@@ -15,6 +20,8 @@ const EntryType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface DocumentsPanelProps {
|
interface DocumentsPanelProps {
|
||||||
|
headerConfig?: DocumentsPanelHeaderConfig;
|
||||||
|
onBreadcrumbNavigate?: (crumb: DocumentsHeaderBreadcrumb) => void;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +30,8 @@ const defaultGetDocumentAsset = (_doc?: unknown, _type?: string) => null;
|
|||||||
export type PreviewEntryLike = { url?: string | null; contentType?: string | null };
|
export type PreviewEntryLike = { url?: string | null; contentType?: string | null };
|
||||||
|
|
||||||
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
||||||
|
headerConfig,
|
||||||
|
onBreadcrumbNavigate,
|
||||||
currentFolderName: _currentFolderName,
|
currentFolderName: _currentFolderName,
|
||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
subfolders,
|
subfolders,
|
||||||
@@ -56,7 +65,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
previewEntries,
|
previewEntries,
|
||||||
ensureDownloadUrl,
|
ensureDownloadUrl,
|
||||||
deskWorkspaceProps = null,
|
deskWorkspaceProps = null,
|
||||||
}) => {
|
}): ReactNode => {
|
||||||
const {
|
const {
|
||||||
selectedEntries,
|
selectedEntries,
|
||||||
focusedRowKey,
|
focusedRowKey,
|
||||||
@@ -123,20 +132,8 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
() => new Set(activeCorrespondentIds || []),
|
() => new Set(activeCorrespondentIds || []),
|
||||||
[activeCorrespondentIds],
|
[activeCorrespondentIds],
|
||||||
);
|
);
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef<HTMLElement | null>(null);
|
||||||
const suppressDocumentClickRef = useRef(false);
|
const suppressDocumentClickRef = useRef(false);
|
||||||
const [, forceVisibilityTick] = useState(0);
|
|
||||||
const lastScrollNodeRef = useRef(null);
|
|
||||||
const assignScrollRef = useCallback((node) => {
|
|
||||||
if (lastScrollNodeRef.current === node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastScrollNodeRef.current = node;
|
|
||||||
scrollRef.current = node;
|
|
||||||
if (node) {
|
|
||||||
forceVisibilityTick((value) => value + 1);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
const isGridView = viewMode === 'grid';
|
const isGridView = viewMode === 'grid';
|
||||||
const isDeskView = viewMode === 'desk';
|
const isDeskView = viewMode === 'desk';
|
||||||
|
|
||||||
@@ -577,44 +574,34 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
const showGridSearchEmptyState = isGridView && showingSearchResults && !hasDocumentEntries && !isSearchLoading;
|
const showGridSearchEmptyState = isGridView && showingSearchResults && !hasDocumentEntries && !isSearchLoading;
|
||||||
const renderBody = () => {
|
const renderBody = () => {
|
||||||
if (isDeskView) {
|
if (isDeskView) {
|
||||||
return (
|
return deskWorkspaceProps ? (
|
||||||
<div className="panel-section__body panel-section__body--desk">
|
|
||||||
{deskWorkspaceProps ? (
|
|
||||||
<DesktopWorkspace {...deskWorkspaceProps} />
|
<DesktopWorkspace {...deskWorkspaceProps} />
|
||||||
) : (
|
) : (
|
||||||
<div className="empty-state empty-state--global">
|
<div className="empty-state empty-state--global">
|
||||||
Desk view is unavailable.
|
Desk view is unavailable.
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDefaultEmptyState) {
|
if (showDefaultEmptyState) {
|
||||||
return (
|
return (
|
||||||
<div className="panel-section__body">
|
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
Drop files anywhere or onto a folder to upload documents.
|
Drop files anywhere or onto a folder to upload documents.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showGridSearchEmptyState) {
|
if (showGridSearchEmptyState) {
|
||||||
return (
|
return (
|
||||||
<div className="panel-section__body">
|
|
||||||
<div className="empty-state empty-state--global">
|
<div className="empty-state empty-state--global">
|
||||||
No documents match the current filters.
|
No documents match the current filters.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showListSearchEmptyState) {
|
if (showListSearchEmptyState) {
|
||||||
return (
|
return (
|
||||||
<div className="panel-section__body">
|
|
||||||
<div className="empty-state">No documents match the current filters.</div>
|
<div className="empty-state">No documents match the current filters.</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,31 +609,8 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isGridView) {
|
||||||
return (
|
return (
|
||||||
<div className="panel-section__body">
|
|
||||||
<div
|
|
||||||
ref={assignScrollRef}
|
|
||||||
className="documents-scroll"
|
|
||||||
tabIndex={0}
|
|
||||||
onFocus={(event) => {
|
|
||||||
if (event.target === scrollRef.current) {
|
|
||||||
handlePanelFocus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.target !== scrollRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePanelKeyDown(event);
|
|
||||||
}}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (event.target === event.currentTarget) {
|
|
||||||
clearSelection();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-activedescendant={isGridView ? undefined : activeDescendantId}
|
|
||||||
>
|
|
||||||
{isGridView ? (
|
|
||||||
<DocumentsGrid
|
<DocumentsGrid
|
||||||
entries={entries}
|
entries={entries}
|
||||||
draggingDocumentIdsSet={draggingSet}
|
draggingDocumentIdsSet={draggingSet}
|
||||||
@@ -664,7 +628,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
onDocumentDragEnd={handleDocumentDragEndLocal}
|
onDocumentDragEnd={handleDocumentDragEndLocal}
|
||||||
onDocumentTagDragOver={handleDocumentTagDragOver}
|
onDocumentTagDragOver={handleDocumentTagDragOver}
|
||||||
onDocumentTagDragLeave={handleDocumentTagDragLeave}
|
onDocumentTagDragLeave={handleDocumentTagDragLeave}
|
||||||
onDocumentTagDrop={handleDocumentTagDrop}
|
onDocumentTagDrop={onDocumentTagDrop}
|
||||||
ensureAssetUrl={ensureAssetUrl}
|
ensureAssetUrl={ensureAssetUrl}
|
||||||
getDocumentAsset={getDocumentAsset}
|
getDocumentAsset={getDocumentAsset}
|
||||||
gridIconSize={gridIconSize}
|
gridIconSize={gridIconSize}
|
||||||
@@ -676,7 +640,10 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
onDocumentRename={onDocumentRename}
|
onDocumentRename={onDocumentRename}
|
||||||
onFolderRename={onFolderRename}
|
onFolderRename={onFolderRename}
|
||||||
/>
|
/>
|
||||||
) : (
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<DocumentsList
|
<DocumentsList
|
||||||
entries={entries}
|
entries={entries}
|
||||||
focusedRowKey={focusedRowKey}
|
focusedRowKey={focusedRowKey}
|
||||||
@@ -706,18 +673,49 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
|
|||||||
activeCorrespondentIdSet={activeCorrespondentIdSet}
|
activeCorrespondentIdSet={activeCorrespondentIdSet}
|
||||||
scrollRef={scrollRef}
|
scrollRef={scrollRef}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const panelVariant = isDeskView ? 'desk' : isGridView ? 'grid' : 'list';
|
const panelVariant = isDeskView ? 'desk' : isGridView ? 'grid' : 'list';
|
||||||
|
const shouldHandlePanelInteractions = !isDeskView && showTableRows;
|
||||||
|
|
||||||
|
const handleSectionFocus = useCallback((event: React.FocusEvent<HTMLElement>) => {
|
||||||
|
if (!shouldHandlePanelInteractions || event.target !== event.currentTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePanelFocus();
|
||||||
|
}, [shouldHandlePanelInteractions, handlePanelFocus]);
|
||||||
|
|
||||||
|
const handleSectionKeyDown = useCallback((event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (!shouldHandlePanelInteractions || event.target !== event.currentTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePanelKeyDown(event);
|
||||||
|
}, [shouldHandlePanelInteractions, handlePanelKeyDown]);
|
||||||
|
|
||||||
|
const handleSectionClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
if (!shouldHandlePanelInteractions || event.target !== event.currentTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearSelection();
|
||||||
|
}, [shouldHandlePanelInteractions, clearSelection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{headerConfig ? (
|
||||||
|
<DocumentsPanelHeader
|
||||||
|
header={headerConfig}
|
||||||
|
onBreadcrumbClick={onBreadcrumbNavigate}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<section
|
<section
|
||||||
|
ref={scrollRef}
|
||||||
className={`documents-panel documents-panel--view-${panelVariant}`}
|
className={`documents-panel documents-panel--view-${panelVariant}`}
|
||||||
|
tabIndex={shouldHandlePanelInteractions ? 0 : undefined}
|
||||||
|
onFocus={handleSectionFocus}
|
||||||
|
onKeyDown={handleSectionKeyDown}
|
||||||
|
onClick={handleSectionClick}
|
||||||
|
aria-activedescendant={shouldHandlePanelInteractions && !isGridView ? activeDescendantId : undefined}
|
||||||
>
|
>
|
||||||
{renderBody()}
|
{renderBody()}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import PanelHeader from '../../ui/PanelHeader';
|
||||||
|
import BreadcrumbTrail from '../../ui/BreadcrumbTrail';
|
||||||
|
|
||||||
|
type Identifier = string | number;
|
||||||
|
|
||||||
|
export interface DocumentsHeaderBreadcrumb {
|
||||||
|
id?: Identifier;
|
||||||
|
name?: string;
|
||||||
|
label?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentsPanelHeaderConfig {
|
||||||
|
title?: ReactNode;
|
||||||
|
subtitle?: ReactNode;
|
||||||
|
leading?: ReactNode;
|
||||||
|
actions?: ReactNode;
|
||||||
|
breadcrumbs?: DocumentsHeaderBreadcrumb[] | null;
|
||||||
|
floatingActions?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocumentsPanelHeaderProps {
|
||||||
|
header?: DocumentsPanelHeaderConfig | null;
|
||||||
|
onBreadcrumbClick?: (crumb: DocumentsHeaderBreadcrumb) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentsPanelHeader: React.FC<DocumentsPanelHeaderProps> = ({
|
||||||
|
header,
|
||||||
|
onBreadcrumbClick,
|
||||||
|
}) => {
|
||||||
|
if (!header) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && onBreadcrumbClick
|
||||||
|
? () => onBreadcrumbClick(crumb)
|
||||||
|
: null,
|
||||||
|
}))
|
||||||
|
: [{ id: 'current-location', label: header.title }];
|
||||||
|
|
||||||
|
const headerTitle = (
|
||||||
|
<h2>
|
||||||
|
<BreadcrumbTrail entries={trailEntries} separator="/" />
|
||||||
|
{header.subtitle ? (
|
||||||
|
<span className="panel-header__subtitle">{header.subtitle}</span>
|
||||||
|
) : null}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PanelHeader
|
||||||
|
leading={header.leading}
|
||||||
|
title={headerTitle}
|
||||||
|
titleTag="h2"
|
||||||
|
actions={header.actions}
|
||||||
|
/>
|
||||||
|
{header.floatingActions}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentsPanelHeader;
|
||||||
@@ -111,7 +111,7 @@ export const createDocumentsTableHeaderActions = ({
|
|||||||
<div className="view-toggle" role="group" aria-label="Change view">
|
<div className="view-toggle" role="group" aria-label="Change view">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`view-toggle__button${isListView ? ' active' : ''}`}
|
className={`toggle-button${isListView ? ' active' : ''}`}
|
||||||
onClick={() => onViewModeChange?.('list')}
|
onClick={() => onViewModeChange?.('list')}
|
||||||
aria-pressed={isListView}
|
aria-pressed={isListView}
|
||||||
title="List view"
|
title="List view"
|
||||||
@@ -120,7 +120,7 @@ export const createDocumentsTableHeaderActions = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`view-toggle__button${isGridView ? ' active' : ''}`}
|
className={`toggle-button${isGridView ? ' active' : ''}`}
|
||||||
onClick={() => onViewModeChange?.('grid')}
|
onClick={() => onViewModeChange?.('grid')}
|
||||||
aria-pressed={isGridView}
|
aria-pressed={isGridView}
|
||||||
title="Icons view"
|
title="Icons view"
|
||||||
@@ -129,7 +129,7 @@ export const createDocumentsTableHeaderActions = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`view-toggle__button${isDeskView ? ' active' : ''}`}
|
className={`toggle-button${isDeskView ? ' active' : ''}`}
|
||||||
onClick={() => onViewModeChange?.('desk')}
|
onClick={() => onViewModeChange?.('desk')}
|
||||||
aria-pressed={isDeskView}
|
aria-pressed={isDeskView}
|
||||||
title="Desk view"
|
title="Desk view"
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const SortFieldQuickMenu: React.FC<SortFieldQuickMenuProps> = ({ sortField, onCh
|
|||||||
className="documents-sort__quickmenu"
|
className="documents-sort__quickmenu"
|
||||||
options={options}
|
options={options}
|
||||||
onSelectOption={handleSelect}
|
onSelectOption={handleSelect}
|
||||||
triggerClassName="view-toggle__button documents-sort__trigger quick-add__trigger"
|
triggerClassName="toggle-button documents-sort__trigger quick-add__trigger"
|
||||||
triggerContent={(
|
triggerContent={(
|
||||||
<span className="documents-sort__trigger-content">
|
<span className="documents-sort__trigger-content">
|
||||||
<span className="documents-sort__label">{label}</span>
|
<span className="documents-sort__label">{label}</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
|
|||||||
import DocumentViewerPanel from '../../preview/DocumentViewerPanel';
|
import DocumentViewerPanel from '../../preview/DocumentViewerPanel';
|
||||||
import createWorkspaceSurfaceConfig from '../workspaceHeader';
|
import createWorkspaceSurfaceConfig from '../workspaceHeader';
|
||||||
import DocumentsPanel from './DocumentsPanel';
|
import DocumentsPanel from './DocumentsPanel';
|
||||||
|
import type { DocumentsPanelHeaderConfig } from './DocumentsPanelHeader';
|
||||||
import { SelectionFloatingPanel } from '../SelectionFloatingActions';
|
import { SelectionFloatingPanel } from '../SelectionFloatingActions';
|
||||||
import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
|
import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
|
||||||
|
|
||||||
@@ -100,6 +101,15 @@ const createDocumentsSurface = ({
|
|||||||
})()
|
})()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const headerConfig: DocumentsPanelHeaderConfig = {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
leading: sidebarToggle,
|
||||||
|
actions,
|
||||||
|
breadcrumbs,
|
||||||
|
floatingActions,
|
||||||
|
};
|
||||||
|
|
||||||
return createWorkspaceSurfaceConfig({
|
return createWorkspaceSurfaceConfig({
|
||||||
key: isDeskView ? 'workspace' : 'documents',
|
key: isDeskView ? 'workspace' : 'documents',
|
||||||
variant: isDeskView ? 'workspace' : 'documents',
|
variant: isDeskView ? 'workspace' : 'documents',
|
||||||
@@ -113,6 +123,7 @@ const createDocumentsSurface = ({
|
|||||||
content: (
|
content: (
|
||||||
<DocumentsPanel
|
<DocumentsPanel
|
||||||
{...tableProps}
|
{...tableProps}
|
||||||
|
headerConfig={headerConfig}
|
||||||
onInspectDocument={onInspectDocument}
|
onInspectDocument={onInspectDocument}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -48,6 +48,37 @@ interface DocumentViewerLayoutProps {
|
|||||||
layoutMode?: LayoutMode;
|
layoutMode?: LayoutMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AUDIO_EXTENSIONS = new Set([
|
||||||
|
'aac',
|
||||||
|
'aiff',
|
||||||
|
'flac',
|
||||||
|
'm4a',
|
||||||
|
'mp3',
|
||||||
|
'ogg',
|
||||||
|
'oga',
|
||||||
|
'opus',
|
||||||
|
'wav',
|
||||||
|
'weba',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const VIDEO_EXTENSIONS = new Set([
|
||||||
|
'avi',
|
||||||
|
'mkv',
|
||||||
|
'mov',
|
||||||
|
'mp4',
|
||||||
|
'm4v',
|
||||||
|
'webm',
|
||||||
|
'wmv',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getFileExtension = (filename?: string | null) => {
|
||||||
|
if (!filename) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const match = filename.toLowerCase().match(/\.([a-z0-9]+)$/);
|
||||||
|
return match ? match[1] : '';
|
||||||
|
};
|
||||||
|
|
||||||
const DocumentViewerLayout = ({
|
const DocumentViewerLayout = ({
|
||||||
document,
|
document,
|
||||||
previewEntry,
|
previewEntry,
|
||||||
@@ -73,9 +104,21 @@ const DocumentViewerLayout = ({
|
|||||||
|| document.content_type
|
|| document.content_type
|
||||||
|| '')
|
|| '')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
const normalizedFilename = previewEntry.filename
|
||||||
|
|| document.filename
|
||||||
|
|| document.original_name
|
||||||
|
|| '';
|
||||||
|
const fileExtension = getFileExtension(normalizedFilename);
|
||||||
const isImage = normalizedContentType.startsWith('image/');
|
const isImage = normalizedContentType.startsWith('image/');
|
||||||
const isPdf = normalizedContentType === 'application/pdf'
|
const isPdf = normalizedContentType === 'application/pdf'
|
||||||
|| normalizedContentType === 'application/x-pdf';
|
|| normalizedContentType === 'application/x-pdf';
|
||||||
|
const isAudio = normalizedContentType.startsWith('audio/')
|
||||||
|
|| AUDIO_EXTENSIONS.has(fileExtension);
|
||||||
|
const isVideo = normalizedContentType.startsWith('video/')
|
||||||
|
|| VIDEO_EXTENSIONS.has(fileExtension);
|
||||||
|
const mediaLabel = document.title
|
||||||
|
|| normalizedFilename
|
||||||
|
|| 'Document preview';
|
||||||
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
return (
|
return (
|
||||||
@@ -101,6 +144,32 @@ const DocumentViewerLayout = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAudio) {
|
||||||
|
return (
|
||||||
|
<audio
|
||||||
|
className="document-viewer__object document-viewer__object--audio"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
src={previewEntry.url}
|
||||||
|
aria-label={`Audio preview of ${mediaLabel}`}
|
||||||
|
>
|
||||||
|
</audio>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVideo) {
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
className="document-viewer__object document-viewer__object--video"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
src={previewEntry.url}
|
||||||
|
aria-label={`Video preview of ${mediaLabel}`}
|
||||||
|
>
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const displayContentType = document.content_type || previewEntry.contentType || 'this file type';
|
const displayContentType = document.content_type || previewEntry.contentType || 'this file type';
|
||||||
const displayFilename = previewEntry.filename
|
const displayFilename = previewEntry.filename
|
||||||
|| document.filename
|
|| document.filename
|
||||||
|
|||||||
@@ -814,7 +814,6 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
onChange={handleUploadInputChange}
|
onChange={handleUploadInputChange}
|
||||||
/>
|
/>
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
className="sidebar__header"
|
|
||||||
leading={(
|
leading={(
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -63,6 +63,12 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header .icon {
|
||||||
|
width: 1.35rem;
|
||||||
|
height: 1.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header__leading,
|
.panel-header__leading,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toggle__button {
|
.toggle-button {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -14,24 +14,19 @@
|
|||||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toggle__button:hover,
|
.toggle-button:hover,
|
||||||
.view-toggle__button:focus-visible {
|
.toggle-button:focus-visible {
|
||||||
background: var(--surface-subtle);
|
background: var(--surface-subtle);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toggle__button.active {
|
.toggle-button.active {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--on-accent);
|
color: var(--on-accent);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toggle__icon {
|
|
||||||
width: 1.4rem;
|
|
||||||
height: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.documents-actions__sort-group {
|
.documents-actions__sort-group {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -2,10 +2,18 @@
|
|||||||
padding: 0 0.75rem 0.75rem 0.75rem;
|
padding: 0 0.75rem 0.75rem 0.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
--documents-list-row-height: calc(48px + 0.2rem);
|
--documents-list-row-height: calc(48px + 0.2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.documents-panel:focus-visible {
|
||||||
|
outline: 2px solid var(--selection-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.tags-panel,
|
.tags-panel,
|
||||||
.correspondents-panel {
|
.correspondents-panel {
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
@@ -53,12 +61,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.documents-panel .panel-section__body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-floating-region {
|
.panel-floating-region {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
@@ -221,25 +223,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.documents-panel .documents-scroll {
|
|
||||||
overflow-y: auto;
|
|
||||||
background: transparent;
|
|
||||||
min-height: 0;
|
|
||||||
padding-right: 0.3rem;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.documents-panel .documents-scroll:focus-visible {
|
|
||||||
outline: 2px solid var(--selection-ring);
|
|
||||||
outline-offset: 2px;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.documents-panel table {
|
.documents-panel table {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
margin-top: 0.4rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.documents-panel thead th {
|
.documents-panel thead th {
|
||||||
@@ -258,7 +245,7 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.documents-panel--view-grid .documents-scroll {
|
.documents-panel--view-grid {
|
||||||
padding: 0.35rem 0.5rem 1rem;
|
padding: 0.35rem 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,12 +51,13 @@
|
|||||||
.breadcrumb-trail {
|
.breadcrumb-trail {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: 0.05rem;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin-left: -0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-trail--measure {
|
.breadcrumb-trail--measure {
|
||||||
@@ -91,7 +92,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.breadcrumb-trail__link,
|
button.breadcrumb-trail__link,
|
||||||
.breadcrumb-trail__ellipsis-button {
|
.breadcrumb-trail__ellipsis-button,
|
||||||
|
.breadcrumb-trail__link.is-current {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
background: none;
|
background: none;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|||||||
@@ -19,8 +19,3 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header .icon-button svg {
|
|
||||||
width: 1.41rem;
|
|
||||||
height: 1.41rem;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -396,6 +396,25 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-viewer__object--audio,
|
||||||
|
.document-viewer__object--video {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
background: var(--surface-overlay, #000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-viewer__object--audio {
|
||||||
|
height: auto;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-viewer__object--video {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.document-viewer__object:not(.document-viewer__object--image) {
|
.document-viewer__object:not(.document-viewer__object--image) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
.app-shell {
|
.app-shell {
|
||||||
height: 100%;
|
height: 100dvh;
|
||||||
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
@@ -7,10 +8,9 @@
|
|||||||
|
|
||||||
|
|
||||||
.documents-main {
|
.documents-main {
|
||||||
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
|
||||||
min-height: 100dvh;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
.main-content {
|
.main-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -35,20 +36,10 @@
|
|||||||
border-left: 1px solid var(--border);
|
border-left: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content__header {
|
|
||||||
padding: 0.5rem 1.25rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content__header-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content__body {
|
.main-content__body {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
@@ -58,38 +49,19 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content__body > *:not(.detail-panel) {
|
.main-content__body--workspace.main-content__body > .desk-shell {
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content__body--workspace.main-content__body--has-detail > .desk-shell {
|
|
||||||
margin-right: calc(-1 * var(--detail-panel-width));
|
margin-right: calc(-1 * var(--detail-panel-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content--has-detail {
|
.main-content {
|
||||||
padding-right: var(--detail-panel-width);
|
padding-right: var(--detail-panel-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.documents-main--sidebar-hidden {
|
.documents-main--sidebar-hidden {
|
||||||
position: relative;
|
position: relative;
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .panel-header .icon,
|
|
||||||
.main-content__header .icon,
|
|
||||||
.detail-panel .panel-header .icon {
|
|
||||||
width: 1.35rem;
|
|
||||||
height: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content__actions {
|
.main-content__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -102,25 +74,12 @@
|
|||||||
color: var(--muted-subtle);
|
color: var(--muted-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--fg);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content__breadcrumbs {
|
.main-content__breadcrumbs {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content__subtitle {
|
.panel-header__subtitle {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|||||||
@@ -134,6 +134,7 @@
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1000000;
|
z-index: 1000000;
|
||||||
|
container-type: inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar--resizing {
|
.sidebar--resizing {
|
||||||
@@ -210,15 +211,17 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container (max-width: 20rem) {
|
||||||
|
.sidebar__title-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar__tenant {
|
.sidebar__tenant {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__header {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar__title-button {
|
.sidebar__title-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.upload-queue-overlay {
|
.upload-queue-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
right: 24px;
|
right: calc(24px + var(--detail-panel-width, 0px));
|
||||||
width: min(360px, calc(100vw - 32px));
|
width: min(360px, calc(100vw - 32px));
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -10,30 +10,15 @@
|
|||||||
z-index: 400;
|
z-index: 400;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.9rem;
|
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.upload-queue-overlay__header {
|
.upload-queue-overlay__body {
|
||||||
padding: 0;
|
padding: 0.6rem 0.9rem 0.9rem;
|
||||||
border: none;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
.upload-queue-overlay__title {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-queue-overlay__summary {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-queue-overlay__controls {
|
.upload-queue-overlay__controls {
|
||||||
|
|||||||
Reference in New Issue
Block a user