frontend cleanup

This commit is contained in:
2025-10-30 18:17:01 +01:00
parent bb34a47fa8
commit 85f3d90329
7 changed files with 510 additions and 292 deletions
+123 -117
View File
@@ -13,8 +13,9 @@ import { getTagColorStyle } from '../utils/colors';
import { formatFileSize } from '../utils/format';
import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
import { useAssetNavigator } from '../hooks/useAssetNavigator';
import { describeDocumentSummary } from '../documents/documentSummary';
import { createDocumentActionState } from '../documents/documentActions';
import PreviewZoomOverlay from './PreviewZoomOverlay';
import { openOcrTextInNewTab } from '../utils/ocr';
const MAX_PREVIEW_STACK_ITEMS = 15;
@@ -314,14 +315,29 @@ const DetailPanel = ({
[selectedDocuments],
);
const singleDownloadHref = useMemo(() => {
if (!singleDoc) return null;
const downloadPath = singleDoc.current_version?.download_path;
if (!downloadPath || !resolveApiPath) {
return null;
const { downloadHref: singleDownloadHref, hasOcr: singleHasOcr, openOcr } = useMemo(
() =>
createDocumentActionState({
document: singleDoc,
resolveApiPath,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
}),
[singleDoc, resolveApiPath, ensurePreviewData, ensureAssetUrl, getDocumentAsset],
);
const detailSummary = useMemo(() => describeDocumentSummary(singleDoc), [singleDoc]);
const headerTitle = useMemo(() => {
if (selectedCount === 0) {
return 'Document details';
}
return resolveApiPath(downloadPath);
}, [singleDoc, resolveApiPath]);
if (selectedCount === 1) {
return detailSummary.title;
}
return `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
}, [selectedCount, detailSummary]);
const [titleEditDocId, setTitleEditDocId] = useState(null);
const [titleDraft, setTitleDraft] = useState('');
@@ -403,27 +419,6 @@ const DetailPanel = ({
[onPromoteSelection],
);
const hasOcrAsset = useMemo(
() => Boolean(singleDoc && getDocumentAsset(singleDoc, 'ocr-text')),
[singleDoc, getDocumentAsset],
);
const openOcr = useCallback(async () => {
if (!singleDoc) {
return;
}
try {
await openOcrTextInNewTab({
document: singleDoc,
ensurePreviewData,
getDocumentAsset,
ensureAssetUrl,
});
} catch (error) {
/* noop */
}
}, [singleDoc, ensurePreviewData, getDocumentAsset, ensureAssetUrl]);
const singlePreviewNavigator = useAssetNavigator({
document: singleDoc,
assetType: 'preview',
@@ -614,6 +609,90 @@ const DetailPanel = ({
return segments;
}, [singleDoc?.folder_id, resolveFolderPath]);
const folderLabel = detailSummary.folderLabel;
const folderDisplayNode = useMemo(() => {
if (!singleDoc) {
return folderLabel || '—';
}
if (!singleFolderPath?.length) {
return folderLabel || '—';
}
return (
<span className="detail-folder-path">
{singleFolderPath.map((segment, index) => {
const label = segment?.name || '…';
const targetId = segment?.id || null;
const key = `${targetId || label}-${index}`;
const isClickable = Boolean(targetId) && typeof onFolderNavigate === 'function';
const href = !isClickable
? null
: targetId === 'root'
? '/documents'
: `/documents/folder/${targetId}`;
return (
<React.Fragment key={key}>
{index > 0 ? <span className="detail-folder-path__separator">/</span> : null}
{isClickable ? (
<a
href={href}
className="detail-folder-path__link"
onClick={(event) => {
if (
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}
event.preventDefault();
event.stopPropagation();
onFolderNavigate(targetId);
}}
>
{label}
</a>
) : (
<span className="detail-folder-path__segment">{label}</span>
)}
</React.Fragment>
);
})}
</span>
);
}, [singleDoc, singleFolderPath, folderLabel, onFolderNavigate]);
const detailInfoRows = useMemo(() => {
if (!singleDoc) {
return [];
}
const allowedKeys = new Set(['uploaded', 'size', 'type', 'issued', 'pages', 'created', 'updated', 'folder']);
const rows = detailSummary.summaryRows
.filter((row) => {
if (!allowedKeys.has(row.key)) {
return false;
}
if (row.key === 'pages') {
return Number.isFinite(detailSummary.pageCount);
}
if (row.key === 'folder') {
return Boolean(singleFolderPath?.length);
}
return true;
})
.map((row) => (row.key === 'folder' ? { ...row, value: folderDisplayNode } : row));
rows.push({
key: 'original-name',
label: 'Original filename',
value: singleDoc.original_name || '—',
});
return rows;
}, [singleDoc, detailSummary, folderDisplayNode, singleFolderPath]);
const bulkCorrespondents = useMemo(() => {
if (selectedDocuments.length <= 1) {
const doc = selectedDocuments[0];
@@ -890,20 +969,7 @@ const DetailPanel = ({
const displayName = singleDoc.title || singleDoc.original_name;
const isEditingTitle = titleEditDocId === singleDoc.id;
const sizeBytes = Number(singleDoc.current_version?.size_bytes) || 0;
const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
const issuedAt = singleDoc.issued_at
? new Date(singleDoc.issued_at).toLocaleString()
: '—';
const tagsForDoc = Array.isArray(singleDoc.tags) ? singleDoc.tags : [];
const pageCountRaw = singleDoc.current_version?.metadata?.page_count;
const pageCountValue =
typeof pageCountRaw === 'number'
? pageCountRaw
: pageCountRaw != null && pageCountRaw !== ''
? Number.parseInt(pageCountRaw, 10)
: null;
const hasPageCount = Number.isFinite(pageCountValue) && pageCountValue >= 0;
const metadata =
singleDoc.metadata && Object.keys(singleDoc.metadata).length > 0 ? singleDoc.metadata : null;
const effectiveCardinality = singleEffectiveCardinality;
@@ -1014,76 +1080,17 @@ const DetailPanel = ({
</div>
{titleError ? <div className="status-inline error">{titleError}</div> : null}
<div className="meta">
<div>
<strong>Uploaded:</strong>{' '}
{singleDoc.uploaded_at ? new Date(singleDoc.uploaded_at).toLocaleString() : '—'}
</div>
<div>
<strong>Size:</strong>{' '}
{sizeLabel}
</div>
<div>
<strong>Type:</strong> {singleDoc.content_type || 'Unknown'}
</div>
<div>
<strong>Issued:</strong> {issuedAt}
</div>
{hasPageCount ? (
<div>
<strong>Pages:</strong> {pageCountValue}
</div>
) : null}
{singleFolderPath?.length ? (
<div>
<strong>Folder:</strong>{' '}
<span className="detail-folder-path">
{singleFolderPath.map((segment, index) => {
const label = segment?.name || '…';
const targetId = segment?.id || null;
const key = `${targetId || label}-${index}`;
const isClickable = Boolean(targetId) && typeof onFolderNavigate === 'function';
const href = !isClickable
? null
: targetId === 'root'
? '/documents'
: `/documents/folder/${targetId}`;
return (
<React.Fragment key={key}>
{index > 0 ? <span className="detail-folder-path__separator">/</span> : null}
{isClickable ? (
<a
href={href}
className="detail-folder-path__link"
onClick={(event) => {
if (
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}
event.preventDefault();
event.stopPropagation();
onFolderNavigate(targetId);
}}
>
{label}
</a>
) : (
<span className="detail-folder-path__segment">{label}</span>
)}
</React.Fragment>
);
})}
</span>
</div>
) : null}
<div>
<strong>Original filename:</strong>{' '}
{singleDoc.original_name}
</div>
{detailInfoRows.map((row) => {
const rawValue = row.value;
const displayValue =
rawValue === null || rawValue === undefined || rawValue === '' ? '—' : rawValue;
return (
<div key={row.key}>
<strong>{row.label}:</strong>{' '}
{displayValue}
</div>
);
})}
</div>
<TagSection
title="Tags"
@@ -1227,13 +1234,12 @@ const DetailPanel = ({
};
const isBulkSelection = selectedCount > 1;
const showOcrAction = Boolean(singleDoc && hasOcrAsset);
const showOcrAction = Boolean(singleDoc && singleHasOcr);
return (
<>
<aside className="detail-panel panel">
<div className="panel-header">
<div className="panel-actions">
<button
type="button"
className="icon-button"
@@ -1258,7 +1264,8 @@ const DetailPanel = ({
<WindowMaximizeIcon />
</button>
) : null}
<div className="spacer" />
<h3 className="panel-header__title">{headerTitle}</h3>
<div className="panel-actions__spacer" />
{isBulkSelection && onBulkReanalyze ? (
<button
type="button"
@@ -1292,7 +1299,7 @@ const DetailPanel = ({
className="icon-button"
onClick={(event) => {
event.stopPropagation();
openOcr();
openOcr().catch(() => {});
}}
aria-label="View OCR text"
title="View OCR text"
@@ -1315,7 +1322,6 @@ const DetailPanel = ({
</button>
) : null}
</div>
</div>
<div className="panel-body">
{selectedCount <= 1 ? renderSingle() : renderBulk()}
</div>
+75
View File
@@ -0,0 +1,75 @@
import { openOcrTextInNewTab } from '../utils/ocr';
const asyncFalse = async () => false;
const resolveDocumentDownloadHref = (document, resolveApiPath) => {
if (!document || typeof resolveApiPath !== 'function') {
return null;
}
const downloadPath = document.current_version?.download_path;
if (!downloadPath) {
return null;
}
return resolveApiPath(downloadPath);
};
const hasDocumentOcrAsset = (document, getDocumentAsset) => {
if (!document || typeof getDocumentAsset !== 'function') {
return false;
}
return Boolean(getDocumentAsset(document, 'ocr-text'));
};
export const createDocumentActionState = ({
document,
resolveApiPath,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
notifyApiError,
ocrErrorMessage = 'Unable to open OCR text.',
}) => {
if (!document) {
return {
downloadHref: null,
hasOcr: false,
openOcr: asyncFalse,
};
}
const downloadHref = resolveDocumentDownloadHref(document, resolveApiPath);
const hasOcr = hasDocumentOcrAsset(document, getDocumentAsset);
const openOcr = hasOcr
? async () => {
try {
const success = await openOcrTextInNewTab({
document,
ensurePreviewData,
getDocumentAsset,
ensureAssetUrl,
});
if (!success && typeof notifyApiError === 'function') {
notifyApiError(new Error('OCR text URL unavailable.'), ocrErrorMessage);
}
return success;
} catch (error) {
if (typeof notifyApiError === 'function') {
notifyApiError(error, ocrErrorMessage);
}
throw error;
}
}
: asyncFalse;
return {
downloadHref,
hasOcr,
openOcr,
};
};
export const documentActionsTestExports = {
resolveDocumentDownloadHref,
hasDocumentOcrAsset,
};
+140
View File
@@ -0,0 +1,140 @@
import { formatFileSize } from '../utils/format';
const defaultFormatDateTime = (value) => {
if (!value) {
return '—';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '—';
}
return date.toLocaleString();
};
const coercePageCount = (metadata) => {
const raw = metadata?.page_count;
if (typeof raw === 'number') {
return Number.isFinite(raw) && raw >= 0 ? raw : null;
}
if (raw != null && raw !== '') {
const parsed = Number.parseInt(raw, 10);
if (Number.isFinite(parsed) && parsed >= 0) {
return parsed;
}
}
return null;
};
const sanitizeTags = (tags) => {
if (!Array.isArray(tags)) {
return [];
}
return tags
.filter((tag) => tag && (tag.label || tag.id))
.map((tag) => ({
id: tag.id,
label: tag.label || '',
color: tag.color || null,
}));
};
const sanitizeCorrespondents = (entries) => {
if (!Array.isArray(entries)) {
return [];
}
return entries
.filter((entry) => entry && (entry.name || entry.id))
.map((entry) => ({
id: entry.id,
name: entry.name || '',
count: entry.count,
}));
};
export const describeDocumentSummary = (document, options = {}) => {
if (!document) {
return {
title: '',
originalName: '',
mimeTypeLabel: '—',
sizeLabel: '—',
uploadedAtLabel: '—',
issuedAtLabel: '—',
createdAtLabel: '—',
updatedAtLabel: '—',
pageCount: null,
pageCountLabel: '—',
folderLabel: null,
tags: [],
correspondents: [],
tagsSummary: '—',
correspondentsSummary: '—',
summaryRows: [],
};
}
const {
formatDateTime = defaultFormatDateTime,
} = options;
const title = document.title || '';
const originalName = document.original_name || '';
const mimeTypeLabel = document.content_type || 'Unknown';
const sizeBytes = Number(document.current_version?.size_bytes);
const sizeLabel = Number.isFinite(sizeBytes) && sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
const metadata = document.current_version?.metadata || null;
const pageCount = coercePageCount(metadata);
const pageCountLabel = Number.isFinite(pageCount) ? String(pageCount) : '—';
const uploadedAtLabel = formatDateTime(document.uploaded_at);
const issuedAtLabel = formatDateTime(document.issued_at);
const createdAtLabel = formatDateTime(document.created_at);
const updatedAtLabel = formatDateTime(document.updated_at);
const folderLabel = document.folder_path || document.folder_name || null;
const tags = sanitizeTags(document.tags);
const correspondents = sanitizeCorrespondents(document.correspondents);
const tagLabels = tags.map((tag) => tag.label).filter(Boolean);
const correspondentLabels = correspondents.map((entry) => entry.name).filter(Boolean);
const tagsSummary = tagLabels.length ? tagLabels.join(', ') : '—';
const correspondentsSummary = correspondentLabels.length
? correspondentLabels.join(', ')
: '—';
const summaryRows = [
{ key: 'uploaded', label: 'Uploaded', value: uploadedAtLabel },
{ key: 'size', label: 'Size', value: sizeLabel },
{ key: 'type', label: 'Type', value: mimeTypeLabel },
{ key: 'issued', label: 'Issued', value: issuedAtLabel },
{ key: 'pages', label: 'Pages', value: pageCountLabel },
{ key: 'created', label: 'Created', value: createdAtLabel },
{ key: 'updated', label: 'Updated', value: updatedAtLabel },
{ key: 'folder', label: 'Folder', value: folderLabel || '—' },
{ key: 'tags', label: 'Tags', value: tagsSummary },
{ key: 'correspondents', label: 'Correspondents', value: correspondentsSummary },
];
return {
title,
originalName,
mimeTypeLabel,
sizeLabel,
uploadedAtLabel,
issuedAtLabel,
createdAtLabel,
updatedAtLabel,
pageCount,
pageCountLabel,
folderLabel,
tags,
correspondents,
tagsSummary,
correspondentsSummary,
summaryRows,
};
};
+12 -11
View File
@@ -5454,6 +5454,7 @@ const DocumentsRoute = () => {
previewWorkspaceEntry,
closeDocumentPreview,
handleThumbnailRegeneration,
ensurePreviewData,
resolveApiPath,
ensureAssetUrl,
getDocumentAsset,
@@ -5543,6 +5544,7 @@ const DocumentsRoute = () => {
document: previewWorkspaceDocument,
previewEntry: previewWorkspaceEntry,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
@@ -5555,6 +5557,7 @@ const DocumentsRoute = () => {
previewWorkspaceDocument,
previewWorkspaceEntry,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
@@ -5611,17 +5614,15 @@ const DocumentsRoute = () => {
<div className={mainContentClass}>
{header ? (
<div className="panel-header main-content__header">
<div className="panel-actions main-content__actions">
{header.leading}
<h2 className="main-content__title">
{header.title}
{header.subtitle ? (
<span className="main-content__subtitle">{header.subtitle}</span>
) : null}
</h2>
<div className="spacer" />
{header.actions}
</div>
{header.leading}
<h2 className="main-content__title">
{header.title}
{header.subtitle ? (
<span className="main-content__subtitle">{header.subtitle}</span>
) : null}
</h2>
<div className="panel-actions__spacer" />
{header.actions}
</div>
) : null}
<div className={bodyClass}>{surface.content}</div>
+48 -65
View File
@@ -1,7 +1,7 @@
import React from 'react';
import { formatFileSize } from '../utils/format';
import { DownloadIcon, TextScanIcon, AnalyzeIcon, CloseIcon } from '../ui/icons';
import { openOcrTextInNewTab } from '../utils/ocr';
import { describeDocumentSummary } from '../documents/documentSummary';
import { createDocumentActionState } from '../documents/documentActions';
const PreviewWorkspace = ({
document,
@@ -12,53 +12,43 @@ const PreviewWorkspace = ({
}
const title = document.title;
const mime = document.content_type;
const sizeBytes = Number(document.current_version?.size_bytes) || 0;
const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : null;
const summary = describeDocumentSummary(document);
const baseSummaryRows = summary.summaryRows.filter((row) => {
if (row.key === 'pages') {
return Number.isFinite(summary.pageCount);
}
if (row.key === 'folder') {
return Boolean(summary.folderLabel);
}
return true;
});
const summaryRows = [
...baseSummaryRows,
{
key: 'original-name',
label: 'Original filename',
value: document.original_name || '—',
},
];
const metadata =
document.metadata && Object.keys(document.metadata).length > 0 ? document.metadata : null;
const folderName = document.folder_path || document.folder_name || null;
const issuedAt = document.issued_at || null;
const createdAt = document.created_at || null;
const updatedAt = document.updated_at || null;
const tags = Array.isArray(document.tags) ? document.tags : [];
const correspondents = Array.isArray(document.correspondents) ? document.correspondents : [];
const metadataSummary = (() => {
const rows = [];
if (mime) rows.push(['Type', mime]);
if (sizeLabel) rows.push(['Size', sizeLabel]);
if (issuedAt) rows.push(['Issued', new Date(issuedAt).toLocaleString()]);
if (createdAt) rows.push(['Created', new Date(createdAt).toLocaleString()]);
if (updatedAt) rows.push(['Updated', new Date(updatedAt).toLocaleString()]);
if (folderName) rows.push(['Folder', folderName]);
if (tags.length) {
rows.push(['Tags', tags.map((tag) => tag.label).filter(Boolean).join(', ')]);
}
if (correspondents.length) {
rows.push([
'Correspondents',
correspondents
.map((entry) => entry.name)
.filter(Boolean)
.join(', '),
]);
}
return rows;
})();
return (
<section className="preview-workspace">
<aside className="preview-workspace__sidebar">
<div className="preview-workspace__info">
{metadataSummary.length ? (
{summaryRows.length ? (
<dl className="preview-workspace__summary">
{metadataSummary.map(([label, value]) => (
<div className="preview-workspace__summary-row" key={label}>
<dt>{label}</dt>
<dd>{value || '—'}</dd>
</div>
))}
{summaryRows.map(({ key, label, value }) => {
const displayValue =
value === null || value === undefined || value === '' ? '—' : value;
return (
<div className="preview-workspace__summary-row" key={key}>
<dt>{label}</dt>
<dd>{displayValue}</dd>
</div>
);
})}
</dl>
) : null}
</div>
@@ -89,6 +79,7 @@ export default PreviewWorkspace;
export const createPreviewWorkspaceHeaderActions = ({
document,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
@@ -98,27 +89,15 @@ export const createPreviewWorkspaceHeaderActions = ({
return null;
}
const downloadHref = document.current_version?.download_path
? resolveApiPath(document.current_version.download_path)
: null;
const ocrAsset = getDocumentAsset(document, 'ocr-text');
const hasOcr = Boolean(ocrAsset);
const handleOcrClick = async () => {
try {
const success = await openOcrTextInNewTab({
document,
getDocumentAsset,
ensureAssetUrl,
});
if (!success) {
throw new Error('OCR text URL unavailable.');
}
} catch (error) {
notifyApiError(error, 'Unable to open OCR text.');
}
};
const { downloadHref, hasOcr, openOcr } = createDocumentActionState({
document,
resolveApiPath,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
notifyApiError,
ocrErrorMessage: 'Unable to open OCR text.',
});
return (
<>
@@ -138,7 +117,9 @@ export const createPreviewWorkspaceHeaderActions = ({
<button
type="button"
className="icon-button"
onClick={handleOcrClick}
onClick={() => {
openOcr().catch(() => {});
}}
aria-label="View OCR text"
title="View OCR text"
>
@@ -162,6 +143,7 @@ export const createPreviewSurface = ({
document,
previewEntry,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
@@ -174,7 +156,6 @@ export const createPreviewSurface = ({
}
const title = document.title;
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
const closeButton = onClose
? (
<button
@@ -188,7 +169,8 @@ export const createPreviewSurface = ({
</button>
)
: null;
const leading = sidebarToggle || closeButton
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
const leading = closeButton || sidebarToggle
? (
<>
{sidebarToggle}
@@ -203,6 +185,7 @@ export const createPreviewSurface = ({
actions: createPreviewWorkspaceHeaderActions({
document,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
+78 -80
View File
@@ -350,90 +350,88 @@ const Sidebar = ({
return (
<aside className="sidebar">
<div className="panel-header sidebar__header">
<div className="panel-actions">
<button
type="button"
className={`sidebar__title-button${tenantMenuOpen ? ' is-open' : ''}`}
onClick={toggleTenantMenu}
aria-haspopup="true"
aria-expanded={tenantMenuOpen}
ref={tenantButtonRef}
>
<span className="sidebar__title">
Papercrate
{tenantName ? <span className="sidebar__tenant"> / {tenantName}</span> : null}
</span>
<ChevronDownIcon
className={`sidebar__title-chevron${tenantMenuOpen ? ' is-open' : ''}`}
size={16}
/>
</button>
{tenantMenuOpen ? (
<div
className={`menu${showTenantList ? '' : ' menu--simple'}`}
ref={tenantMenuRef}
role="menu"
aria-label="Account menu"
>
{showTenantList ? (
<>
<div className="menu__heading">Switch tenant</div>
<div className="menu__list">
{tenants.map((tenant) => {
const tenantId = tenant?.id || null;
const isActive = tenantId === activeTenantId;
const tenantLabel = tenant?.name;
return (
<button
key={tenantId || tenantLabel}
type="button"
className={`menu__item${isActive ? ' active' : ''}`}
onClick={() => handleTenantSelect(tenant)}
role="menuitem"
>
<span className="menu__check-slot">
{isActive ? <CheckIcon size={16} /> : null}
</span>
<span className="menu__label">{tenantLabel}</span>
</button>
);
})}
</div>
</>
) : null}
<div className="menu__footer">
<button
type="button"
className="menu__settings"
onClick={handleSettingsFromMenu}
>
<SettingsIcon size={16} />
Settings
</button>
<button
type="button"
className="menu__logout"
onClick={handleLogoutFromMenu}
>
<LogoutIcon size={16} />
Log out
</button>
</div>
</div>
) : null}
<div className="panel-actions__spacer" />
{onCollapse ? (
<button
type="button"
className={`sidebar__title-button${tenantMenuOpen ? ' is-open' : ''}`}
onClick={toggleTenantMenu}
aria-haspopup="true"
aria-expanded={tenantMenuOpen}
ref={tenantButtonRef}
className="icon-button"
onClick={onCollapse}
aria-label="Collapse sidebar"
title="Collapse sidebar"
>
<span className="sidebar__title">
Papercrate
{tenantName ? <span className="sidebar__tenant"> / {tenantName}</span> : null}
</span>
<ChevronDownIcon
className={`sidebar__title-chevron${tenantMenuOpen ? ' is-open' : ''}`}
size={16}
/>
<ChevronsLeftIcon />
</button>
{tenantMenuOpen ? (
<div
className={`menu${showTenantList ? '' : ' menu--simple'}`}
ref={tenantMenuRef}
role="menu"
aria-label="Account menu"
>
{showTenantList ? (
<>
<div className="menu__heading">Switch tenant</div>
<div className="menu__list">
{tenants.map((tenant) => {
const tenantId = tenant?.id || null;
const isActive = tenantId === activeTenantId;
const tenantLabel = tenant?.name;
return (
<button
key={tenantId || tenantLabel}
type="button"
className={`menu__item${isActive ? ' active' : ''}`}
onClick={() => handleTenantSelect(tenant)}
role="menuitem"
>
<span className="menu__check-slot">
{isActive ? <CheckIcon size={16} /> : null}
</span>
<span className="menu__label">{tenantLabel}</span>
</button>
);
})}
</div>
</>
) : null}
<div className="menu__footer">
<button
type="button"
className="menu__settings"
onClick={handleSettingsFromMenu}
>
<SettingsIcon size={16} />
Settings
</button>
<button
type="button"
className="menu__logout"
onClick={handleLogoutFromMenu}
>
<LogoutIcon size={16} />
Log out
</button>
</div>
</div>
) : null}
<div className="spacer" />
{onCollapse ? (
<button
type="button"
className="icon-button ghost"
onClick={onCollapse}
aria-label="Collapse sidebar"
title="Collapse sidebar"
>
<ChevronsLeftIcon />
</button>
) : null}
</div>
) : null}
</div>
<div className="panel-body sidebar__body">
{status && (
+34 -19
View File
@@ -313,9 +313,9 @@ button.danger:hover:not([disabled]) {
background: transparent;
}
.panel-actions .icon-button,
.panel-actions button,
.panel-actions a.icon-button {
.panel-header .icon-button,
.panel-header button,
.panel-header a.icon-button {
display: inline-flex;
align-items: flex-start;
justify-content: flex-start;
@@ -330,21 +330,21 @@ button.danger:hover:not([disabled]) {
text-align: left;
}
.panel-actions .icon-button:hover:not([disabled]),
.panel-actions button:hover:not([disabled]),
.panel-actions a.icon-button:hover {
.panel-header .icon-button:hover:not([disabled]),
.panel-header button:hover:not([disabled]),
.panel-header a.icon-button:hover {
background: var(--sidebar-hover-bg);
color: var(--fg);
}
.panel-actions .icon-button.ghost,
.panel-actions button.icon-button.ghost {
.panel-header .icon-button.ghost,
.panel-header button.icon-button.ghost {
color: var(--muted);
padding: 0.25rem;
}
.panel-actions .icon-button.ghost:hover:not([disabled]),
.panel-actions button.icon-button.ghost:hover:not([disabled]) {
.panel-header .icon-button.ghost:hover:not([disabled]),
.panel-header button.icon-button.ghost:hover:not([disabled]) {
color: var(--fg);
background: var(--sidebar-hover-bg);
}
@@ -607,6 +607,8 @@ button.danger:hover:not([disabled]) {
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
flex: 1 1 auto;
min-width: 0;
}
.main-content__subtitle {
@@ -623,9 +625,9 @@ button.danger:hover:not([disabled]) {
grid-template-columns: auto minmax(0, 1fr);
}
.sidebar .panel-actions .icon,
.main-content__header .panel-actions .icon,
.detail-panel .panel-actions .icon {
.sidebar .panel-header .icon,
.main-content__header .icon,
.detail-panel .panel-header .icon {
width: 1.35rem;
height: 1.35rem;
}
@@ -2207,18 +2209,35 @@ button.danger:hover:not([disabled]) {
.panel-header {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.5rem;
justify-content: space-between;
gap: 0.75rem;
padding: 0.5rem;
}
.panel-header__title {
margin: 0;
font-size: 1rem;
font-weight: 600;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 auto;
min-width: 0;
}
.panel-body {
padding: 0.5rem 1rem;
}
.panel-actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1 1 auto;
}
.panel-actions__spacer {
flex-grow: 1;
}
@@ -2227,10 +2246,6 @@ button.danger:hover:not([disabled]) {
padding-left: 0.5rem;
}
.panel-actions .spacer {
flex-grow: 1;
}
.detail-panel .panel-body {
flex: 1;
display: flex;