panel actions
This commit is contained in:
@@ -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 = ({
|
||||
<section
|
||||
className={`documents-panel column documents-panel--view-${isGridView ? 'grid' : 'list'}`}
|
||||
>
|
||||
<div className="column-header">
|
||||
<div className="column-header__titles">
|
||||
<h2>{currentFolderName}</h2>
|
||||
{showingSearchResults && (
|
||||
<div className="column-subtitle">Search results</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<div className="view-toggle" role="group" aria-label="Change view">
|
||||
{showHeader ? (
|
||||
<div className="column-header">
|
||||
<div className="column-header__titles">
|
||||
<h2>{currentFolderName}</h2>
|
||||
{showingSearchResults && (
|
||||
<div className="column-subtitle">Search results</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<div className="view-toggle" role="group" aria-label="Change view">
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? '' : ' active'}`}
|
||||
onClick={() => handleSetViewMode('list')}
|
||||
aria-pressed={!isGridView}
|
||||
title="List view"
|
||||
>
|
||||
<ViewListIcon className="view-toggle__icon" size={18} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? ' active' : ''}`}
|
||||
onClick={() => handleSetViewMode('grid')}
|
||||
aria-pressed={isGridView}
|
||||
title="Icons view"
|
||||
>
|
||||
<ViewGridIcon className="view-toggle__icon" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? '' : ' active'}`}
|
||||
onClick={() => handleSetViewMode('list')}
|
||||
aria-pressed={!isGridView}
|
||||
title="List view"
|
||||
onClick={onRequestCreateFolder}
|
||||
disabled={creatingFolder}
|
||||
>
|
||||
<ViewListIcon className="view-toggle__icon" size={18} />
|
||||
{creatingFolder ? 'Creating…' : 'New folder'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? ' active' : ''}`}
|
||||
onClick={() => handleSetViewMode('grid')}
|
||||
aria-pressed={isGridView}
|
||||
title="Icons view"
|
||||
>
|
||||
<ViewGridIcon className="view-toggle__icon" size={18} />
|
||||
<button className="secondary" onClick={onRefresh}>
|
||||
Refresh
|
||||
</button>
|
||||
<button className="secondary" type="button" onClick={onShowSkeuoWorkspace}>
|
||||
Desk View
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRequestCreateFolder}
|
||||
disabled={creatingFolder}
|
||||
>
|
||||
{creatingFolder ? 'Creating…' : 'New folder'}
|
||||
</button>
|
||||
<button className="secondary" onClick={onRefresh}>
|
||||
Refresh
|
||||
</button>
|
||||
<button className="secondary" type="button" onClick={onShowSkeuoWorkspace}>
|
||||
Desk View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{showDefaultEmptyState && (
|
||||
<div className="empty-state">
|
||||
Drop files anywhere or onto a folder to upload documents.
|
||||
|
||||
+201
-14
@@ -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 }) => (
|
||||
<main className="documents-main">
|
||||
<Sidebar {...sidebarProps} />
|
||||
const DocumentsLayout = ({ sidebarProps, children, sidebarCollapsed }) => (
|
||||
<main className={`documents-main${sidebarCollapsed ? ' documents-main--sidebar-collapsed' : ''}`}>
|
||||
{!sidebarCollapsed ? <Sidebar {...sidebarProps} /> : null}
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
@@ -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 (
|
||||
<DocumentsLayout sidebarProps={sidebarPropsWithActions}>
|
||||
<DocumentsLayout
|
||||
sidebarProps={sidebarPropsWithActions}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
>
|
||||
<div className="main-content">
|
||||
<SkeuomorphicWorkspace {...skeuoWorkspaceProps} />
|
||||
<div className="panel-header main-content__header">
|
||||
<div className="panel-actions main-content__actions">
|
||||
{sidebarCollapsed ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={toggleSidebar}
|
||||
aria-label={toggleLabel}
|
||||
title={toggleLabel}
|
||||
>
|
||||
<ToggleIcon />
|
||||
</button>
|
||||
) : null}
|
||||
{parentBreadcrumb ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={handleNavigateParent}
|
||||
aria-label="Go to parent folder"
|
||||
title="Go to parent folder"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
</button>
|
||||
) : null}
|
||||
<h2 className="main-content__title">
|
||||
{headerTitle}
|
||||
{headerSubtitle ? (
|
||||
<span className="main-content__subtitle">{headerSubtitle}</span>
|
||||
) : null}
|
||||
</h2>
|
||||
<div className="spacer" />
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={skeuoWorkspaceProps.onRefresh}
|
||||
aria-label="Refresh"
|
||||
title="Refresh"
|
||||
>
|
||||
<RefreshIcon />
|
||||
</button>
|
||||
<button type="button" className="secondary" onClick={exitSkeuoWorkspace}>
|
||||
Back to List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="main-content__body">
|
||||
<SkeuomorphicWorkspace {...skeuoWorkspaceProps} />
|
||||
</div>
|
||||
</div>
|
||||
</DocumentsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const detailHasContent = detailPanelProps.selectedDocuments?.length > 0;
|
||||
|
||||
return (
|
||||
<DocumentsLayout sidebarProps={sidebarPropsWithActions}>
|
||||
<div
|
||||
className={`main-content main-content--documents${detailHasContent ? ' main-content--has-detail' : ''}`}
|
||||
>
|
||||
<DocumentsTable {...documentsTableProps} />
|
||||
{detailHasContent ? <DetailPanel {...detailPanelProps} /> : null}
|
||||
<DocumentsLayout
|
||||
sidebarProps={sidebarPropsWithActions}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
>
|
||||
<div className={`main-content main-content--documents${detailHasContent ? ' main-content--has-detail' : ''}`}>
|
||||
<div className="panel-header main-content__header">
|
||||
<div className="panel-actions main-content__actions">
|
||||
{sidebarCollapsed ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={toggleSidebar}
|
||||
aria-label={toggleLabel}
|
||||
title={toggleLabel}
|
||||
>
|
||||
<ToggleIcon />
|
||||
</button>
|
||||
) : null}
|
||||
{parentBreadcrumb ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={handleNavigateParent}
|
||||
aria-label="Go to parent folder"
|
||||
title="Go to parent folder"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
</button>
|
||||
) : null}
|
||||
<h2 className="main-content__title">
|
||||
{headerTitle}
|
||||
{headerSubtitle ? (
|
||||
<span className="main-content__subtitle">{headerSubtitle}</span>
|
||||
) : null}
|
||||
</h2>
|
||||
<div className="spacer" />
|
||||
<div className="view-toggle" role="group" aria-label="Change view">
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? '' : ' active'}`}
|
||||
onClick={() => onViewModeChange?.('list')}
|
||||
aria-pressed={!isGridView}
|
||||
title="List view"
|
||||
>
|
||||
<ViewListIcon className="view-toggle__icon" size={18} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? ' active' : ''}`}
|
||||
onClick={() => onViewModeChange?.('grid')}
|
||||
aria-pressed={isGridView}
|
||||
title="Icons view"
|
||||
>
|
||||
<ViewGridIcon className="view-toggle__icon" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="main-content__actions-divider" aria-hidden="true">
|
||||
<MinusVerticalIcon />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={onRequestCreateFolder}
|
||||
disabled={creatingFolder}
|
||||
aria-label={creatingFolder ? 'Creating folder…' : 'Create folder'}
|
||||
title={creatingFolder ? 'Creating folder…' : 'Create folder'}
|
||||
>
|
||||
<FolderPlusIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={onRefresh}
|
||||
aria-label="Refresh"
|
||||
title="Refresh"
|
||||
>
|
||||
<RefreshIcon />
|
||||
</button>
|
||||
<button className="secondary" type="button" onClick={onShowSkeuoWorkspace}>
|
||||
Desk View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`main-content__body main-content__body--documents${detailHasContent ? ' main-content__body--has-detail' : ''}`}
|
||||
>
|
||||
<DocumentsTable {...documentsTableProps} showHeader={false} />
|
||||
</div>
|
||||
{detailHasContent ? (
|
||||
<DetailPanel {...detailPanelProps} />
|
||||
) : null}
|
||||
</div>
|
||||
</DocumentsLayout>
|
||||
);
|
||||
|
||||
+125
-108
@@ -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 (
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar__meta">
|
||||
<h1 className="sidebar__title">Papercrate</h1>
|
||||
<div className="panel-header sidebar__header">
|
||||
<div className="panel-actions">
|
||||
<h1 className="sidebar__title">Papercrate</h1>
|
||||
<div className="spacer" />
|
||||
{onCollapse ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={onCollapse}
|
||||
aria-label="Collapse sidebar"
|
||||
title="Collapse sidebar"
|
||||
>
|
||||
<ChevronsLeftIcon />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="panel-body sidebar__body">
|
||||
<span className="sidebar__hint">{hintText}</span>
|
||||
{status && (
|
||||
<div className="sidebar__status">
|
||||
<div className={`status-banner ${status.variant}`}>{status.message}</div>
|
||||
</div>
|
||||
)}
|
||||
{onSearchChange && (
|
||||
<form className="sidebar__search" onSubmit={handleSearchFormSubmit}>
|
||||
<input
|
||||
@@ -253,117 +275,112 @@ const Sidebar = ({
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
{status && (
|
||||
<div className="sidebar__status">
|
||||
<div className={`status-banner ${status.variant}`}>{status.message}</div>
|
||||
<div className="sidebar-section sidebar-section--folders">
|
||||
<div className="sidebar-section__header">
|
||||
<h3>Folders</h3>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="sidebar-section sidebar-section--folders">
|
||||
<div className="sidebar-section__header">
|
||||
<h3>Folders</h3>
|
||||
<ul className="folder-tree">
|
||||
{rootNode && renderNodes([rootNode.id], 0)}
|
||||
</ul>
|
||||
</div>
|
||||
<ul className="folder-tree">
|
||||
{rootNode && renderNodes([rootNode.id], 0)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-section__header">
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-section__title"
|
||||
onClick={handleManageTags}
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-section__header">
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-section__title"
|
||||
onClick={handleManageTags}
|
||||
>
|
||||
<h3>Tags</h3>
|
||||
<span className="meta">{tags.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={`sidebar-tag-cloud${
|
||||
activeTagSet.size ? ' sidebar-tag-cloud--has-active' : ''
|
||||
}`}
|
||||
role="list"
|
||||
>
|
||||
<h3>Tags</h3>
|
||||
<span className="meta">{tags.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={`sidebar-tag-cloud${
|
||||
activeTagSet.size ? ' sidebar-tag-cloud--has-active' : ''
|
||||
}`}
|
||||
role="list"
|
||||
>
|
||||
{tags.map((tag) => {
|
||||
const isActive = activeTagSet.has(tag.id);
|
||||
const style = getTagColorStyle(tag.color);
|
||||
const className = `sidebar-tag-pill${isActive ? ' active' : ''}`;
|
||||
return (
|
||||
<button
|
||||
key={tag.id}
|
||||
type="button"
|
||||
role="listitem"
|
||||
className={className}
|
||||
style={style || undefined}
|
||||
onClick={() => handleToggleTag(tag.id)}
|
||||
aria-pressed={isActive}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
try {
|
||||
const payload = JSON.stringify({
|
||||
id: tag.id,
|
||||
label: tag.label,
|
||||
color: tag.color || null,
|
||||
});
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('application/x-papercrate-tag', payload);
|
||||
event.dataTransfer.setData('text/papercrate-tag', payload);
|
||||
} catch (
|
||||
// eslint-disable-next-line no-empty
|
||||
error
|
||||
) {}
|
||||
}}
|
||||
>
|
||||
{tag.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-section__header">
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-section__title"
|
||||
onClick={handleManageCorrespondents}
|
||||
>
|
||||
<h3>Correspondents</h3>
|
||||
<span className="meta">{correspondents.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul className="sidebar-correspondent-list">
|
||||
{sortedCorrespondents.map((correspondent) => {
|
||||
const isActive = activeCorrespondentSet.has(correspondent.id);
|
||||
const className = `sidebar-correspondent-item${isActive ? ' active' : ''}`;
|
||||
const label = correspondent.name || 'Unnamed';
|
||||
const handleSelect = () => {
|
||||
const nextId = isActive ? null : correspondent.id;
|
||||
onToggleCorrespondentFilter?.(nextId);
|
||||
};
|
||||
return (
|
||||
<li key={correspondent.id}>
|
||||
<span
|
||||
{tags.map((tag) => {
|
||||
const isActive = activeTagSet.has(tag.id);
|
||||
const style = getTagColorStyle(tag.color);
|
||||
const className = `sidebar-tag-pill${isActive ? ' active' : ''}`;
|
||||
return (
|
||||
<button
|
||||
key={tag.id}
|
||||
type="button"
|
||||
role="listitem"
|
||||
className={className}
|
||||
role="button"
|
||||
onClick={handleSelect}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleSelect();
|
||||
}
|
||||
style={style || undefined}
|
||||
onClick={() => handleToggleTag(tag.id)}
|
||||
aria-pressed={isActive}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
try {
|
||||
const payload = JSON.stringify({
|
||||
id: tag.id,
|
||||
label: tag.label,
|
||||
color: tag.color || null,
|
||||
});
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('application/x-papercrate-tag', payload);
|
||||
event.dataTransfer.setData('text/papercrate-tag', payload);
|
||||
} catch (
|
||||
// eslint-disable-next-line no-empty
|
||||
error
|
||||
) {}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="sidebar__footer">
|
||||
<button className="secondary" type="button" onClick={handleLogoutClick}>
|
||||
Log out
|
||||
</button>
|
||||
{tag.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-section__header">
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-section__title"
|
||||
onClick={handleManageCorrespondents}
|
||||
>
|
||||
<h3>Correspondents</h3>
|
||||
<span className="meta">{correspondents.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul className="sidebar-correspondent-list">
|
||||
{sortedCorrespondents.map((correspondent) => {
|
||||
const isActive = activeCorrespondentSet.has(correspondent.id);
|
||||
const className = `sidebar-correspondent-item${isActive ? ' active' : ''}`;
|
||||
const label = correspondent.name || 'Unnamed';
|
||||
const handleSelect = () => {
|
||||
const nextId = isActive ? null : correspondent.id;
|
||||
onToggleCorrespondentFilter?.(nextId);
|
||||
};
|
||||
return (
|
||||
<li key={correspondent.id}>
|
||||
<span
|
||||
className={className}
|
||||
role="button"
|
||||
onClick={handleSelect}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleSelect();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="sidebar__footer">
|
||||
<button className="secondary" type="button" onClick={handleLogoutClick}>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<div className="skeuo-shell">
|
||||
<header className="skeuo-header">
|
||||
<div className="skeuo-header__meta">
|
||||
<h2>{currentFolderName}</h2>
|
||||
{showingSearchResults && <span className="meta">Showing search results</span>}
|
||||
</div>
|
||||
<div className="skeuo-header__actions">
|
||||
<button type="button" className="secondary" onClick={onRefresh}>
|
||||
Refresh
|
||||
</button>
|
||||
<button type="button" onClick={onExit}>
|
||||
Back to List
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="skeuo-canvas"
|
||||
ref={containerRef}
|
||||
|
||||
+97
-37
@@ -352,39 +352,34 @@ button.danger:hover:not([disabled]) {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
|
||||
.view-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-subtle);
|
||||
margin-right: 0.5rem;
|
||||
overflow: hidden;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.view-toggle__button {
|
||||
width: 2.5rem;
|
||||
height: 2.2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
padding: 0;
|
||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.view-toggle__button:hover,
|
||||
.view-toggle__button:focus-visible {
|
||||
background: transparent;
|
||||
background: var(--surface-subtle);
|
||||
color: var(--fg);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.view-toggle__button.active {
|
||||
background: var(--accent-soft, rgba(63, 106, 216, 0.16));
|
||||
color: var(--accent);
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.view-toggle__icon {
|
||||
@@ -403,35 +398,96 @@ button.danger:hover:not([disabled]) {
|
||||
|
||||
.documents-main {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
display: flex;
|
||||
grid-template-columns: 20rem minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.documents-main--sidebar-collapsed {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
background: var(--surface);
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.documents-main .main-content--documents > * {
|
||||
.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 {
|
||||
|
||||
@@ -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 }) => (
|
||||
<IconChevronsLeft
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ChevronsRightIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconChevronsRight
|
||||
className={composeClassName('icon', className)}
|
||||
@@ -133,6 +147,42 @@ export const ChevronsRightIcon = ({ className, size = '1em', stroke = 1.6, ...re
|
||||
/>
|
||||
);
|
||||
|
||||
export const FolderPlusIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconFolderPlus
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RefreshIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconRefresh
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ArrowUpIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconArrowUp
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const MinusVerticalIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconMinusVertical
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconAnalyze
|
||||
className={composeClassName('icon', className)}
|
||||
@@ -161,9 +211,14 @@ export default {
|
||||
DownloadIcon,
|
||||
ViewListIcon,
|
||||
ViewGridIcon,
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon,
|
||||
AnalyzeIcon,
|
||||
WindowMaximizeIcon,
|
||||
FolderPlusIcon,
|
||||
RefreshIcon,
|
||||
ArrowUpIcon,
|
||||
MinusVerticalIcon,
|
||||
};
|
||||
|
||||
export const TextScanIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
|
||||
Reference in New Issue
Block a user