panel actions

This commit is contained in:
2025-10-26 01:04:21 +02:00
parent 2a0c96bb4c
commit 0864b39336
3 changed files with 162 additions and 60 deletions
+87 -48
View File
@@ -6,6 +6,9 @@ import {
ArrowLeftIcon,
ArrowRightIcon,
ChevronsRightIcon,
AnalyzeIcon,
WindowMaximizeIcon,
TextScanIcon,
} from '../ui/icons';
import { getTagColorStyle } from '../utils/colors';
import { formatFileSize } from '../utils/format';
@@ -308,6 +311,15 @@ const DetailPanel = ({
const selectedCount = selectedDocuments.length;
const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null;
const singleDownloadHref = useMemo(() => {
if (!singleDoc) return null;
const downloadPath = singleDoc.current_version?.download_path;
if (!downloadPath || !resolveApiPath) {
return null;
}
return resolveApiPath(downloadPath);
}, [singleDoc, resolveApiPath]);
const [titleEditDocId, setTitleEditDocId] = useState(null);
const [titleDraft, setTitleDraft] = useState('');
const [titleSaving, setTitleSaving] = useState(false);
@@ -712,9 +724,6 @@ const DetailPanel = ({
}
const displayName = singleDoc.title || singleDoc.original_name;
const downloadHref = singleDoc.current_version?.download_path
? resolveApiPath?.(singleDoc.current_version.download_path)
: null;
const isEditingTitle = titleEditDocId === singleDoc.id;
const sizeBytes = Number(singleDoc.current_version?.size_bytes) || 0;
const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
@@ -864,44 +873,6 @@ const DetailPanel = ({
{singleDoc.original_name}
</div>
</div>
<div className="detail-actions">
<a
className="button-link with-icon"
href={downloadHref || '#'}
target="_blank"
rel="noopener noreferrer"
aria-disabled={!downloadHref}
onClick={(event) => {
if (!downloadHref) {
event.preventDefault();
}
}}
>
<DownloadIcon className="icon-inline" />
<span>Download</span>
</a>
<button
type="button"
className="secondary"
onClick={() => onOpenPreview(singleDoc.id)}
>
Open preview
</button>
<button
type="button"
className="secondary"
onClick={() => onRegenerateThumbnails(singleDoc.id)}
>
Re-run analysis
</button>
</div>
{hasOcrAsset ? (
<div className="detail-ocr-trigger">
<button type="button" className="secondary" onClick={openOcrModal}>
View OCR text
</button>
</div>
) : null}
<TagSection
title="Tags"
tags={tagsForDoc.map((tag) => ({
@@ -1081,17 +1052,13 @@ const DetailPanel = ({
showCount
className="bulk-correspondents"
/>
<button
type="button"
className="secondary"
onClick={() => onBulkReanalyze?.()}
>
Re-analyze selection
</button>
</>
);
};
const isBulkSelection = selectedCount > 1;
const showOcrAction = Boolean(singleDoc && hasOcrAsset);
return (
<aside className="detail-panel panel">
<div className="panel-header">
@@ -1101,9 +1068,81 @@ const DetailPanel = ({
className="icon-button ghost"
onClick={onClose}
aria-label="Close detail panel"
title="Close detail panel"
>
<ChevronsRightIcon />
</button>
<div className="spacer" />
{isBulkSelection && onBulkReanalyze ? (
<button
type="button"
className="icon-button ghost"
onClick={(event) => {
event.stopPropagation();
onBulkReanalyze();
}}
aria-label="Re-run analysis for selection"
title="Re-run analysis for selection"
>
<AnalyzeIcon />
</button>
) : null}
{singleDoc && singleDownloadHref ? (
<a
className="icon-button"
href={singleDownloadHref}
target="_blank"
rel="noopener noreferrer"
aria-label="Download document"
title="Download document"
onClick={(event) => event.stopPropagation()}
>
<DownloadIcon />
</a>
) : null}
{singleDoc ? (
<button
type="button"
className="icon-button ghost"
onClick={(event) => {
event.stopPropagation();
onOpenPreview(singleDoc.id);
}}
aria-label="Open preview"
title="Open preview"
disabled={!singleHasPreview}
>
<WindowMaximizeIcon />
</button>
) : null}
{showOcrAction ? (
<button
type="button"
className="icon-button ghost"
onClick={(event) => {
event.stopPropagation();
openOcrModal();
}}
aria-label="View OCR text"
title="View OCR text"
>
<TextScanIcon />
</button>
) : null}
{singleDoc ? (
<button
type="button"
className="icon-button ghost"
onClick={(event) => {
event.stopPropagation();
onRegenerateThumbnails(singleDoc.id);
}}
aria-label="Re-run analysis"
title="Re-run analysis"
>
<AnalyzeIcon />
</button>
) : null}
</div>
</div>
<div className="panel-body">
+43 -12
View File
@@ -44,7 +44,6 @@
--danger-border: color-mix(in oklch, var(--danger) 45%, transparent);
--danger-soft: color-mix(in oklch, var(--danger) 12%, transparent);
--danger-subtle: color-mix(in oklch, var(--danger) 8%, transparent);
--detail-panel-width: 30em;
/* --- Explorer states --- */
--row-hover-bg: color-mix(in oklch, var(--accent) 6%, transparent);
@@ -55,6 +54,9 @@
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 15px;
--detail-panel-width: 30em;
--documents-grid-title-size: 0.8rem;
}
html,
@@ -178,6 +180,41 @@ button.danger:hover:not([disabled]) {
background: transparent;
}
.panel-actions .icon-button,
.panel-actions button,
.panel-actions a.icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: var(--muted);
padding: 0.25rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
text-decoration: none;
}
.panel-actions .icon-button:hover:not([disabled]),
.panel-actions button:hover:not([disabled]),
.panel-actions a.icon-button:hover {
background: var(--sidebar-hover-bg);
color: var(--fg);
}
.panel-actions .icon-button.ghost,
.panel-actions 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]) {
color: var(--fg);
background: var(--sidebar-hover-bg);
}
.with-icon {
display: inline-flex;
align-items: center;
@@ -1019,7 +1056,6 @@ button.danger:hover:not([disabled]) {
.sidebar-correspondent-item {
display: block;
width: 100%;
background: transparent;
border-radius: 4px;
padding: 0.35rem 0.5rem;
@@ -1528,8 +1564,12 @@ button.danger:hover:not([disabled]) {
.panel-actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-grow: 1;
}
.panel-actions .spacer {
flex-grow: 1;
}
.detail-panel .panel-body {
@@ -1542,15 +1582,6 @@ button.danger:hover:not([disabled]) {
padding: 1.25rem;
}
.detail-actions {
display: flex;
gap: 0.32rem;
margin: 0.6rem 0;
}
.detail-ocr-trigger {
margin: 0.75rem 0;
}
.detail-ocr__content {
max-height: 60vh;
+32
View File
@@ -10,6 +10,9 @@ import {
IconArrowLeft,
IconArrowRight,
IconChevronsRight,
IconAnalyze,
IconWindowMaximize,
IconTextScan2,
} from '@tabler/icons-react';
import FolderSvg from '../assets/folder.svg';
@@ -130,6 +133,24 @@ export const ChevronsRightIcon = ({ className, size = '1em', stroke = 1.6, ...re
/>
);
export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconAnalyze
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);
export const WindowMaximizeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconWindowMaximize
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);
export default {
ChevronIcon,
TrashIcon,
@@ -141,4 +162,15 @@ export default {
ViewListIcon,
ViewGridIcon,
ChevronsRightIcon,
AnalyzeIcon,
WindowMaximizeIcon,
};
export const TextScanIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconTextScan2
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);