panel actions
This commit is contained in:
@@ -6,6 +6,9 @@ import {
|
|||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
ChevronsRightIcon,
|
ChevronsRightIcon,
|
||||||
|
AnalyzeIcon,
|
||||||
|
WindowMaximizeIcon,
|
||||||
|
TextScanIcon,
|
||||||
} from '../ui/icons';
|
} from '../ui/icons';
|
||||||
import { getTagColorStyle } from '../utils/colors';
|
import { getTagColorStyle } from '../utils/colors';
|
||||||
import { formatFileSize } from '../utils/format';
|
import { formatFileSize } from '../utils/format';
|
||||||
@@ -308,6 +311,15 @@ const DetailPanel = ({
|
|||||||
const selectedCount = selectedDocuments.length;
|
const selectedCount = selectedDocuments.length;
|
||||||
const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null;
|
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 [titleEditDocId, setTitleEditDocId] = useState(null);
|
||||||
const [titleDraft, setTitleDraft] = useState('');
|
const [titleDraft, setTitleDraft] = useState('');
|
||||||
const [titleSaving, setTitleSaving] = useState(false);
|
const [titleSaving, setTitleSaving] = useState(false);
|
||||||
@@ -712,9 +724,6 @@ const DetailPanel = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displayName = singleDoc.title || singleDoc.original_name;
|
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 isEditingTitle = titleEditDocId === singleDoc.id;
|
||||||
const sizeBytes = Number(singleDoc.current_version?.size_bytes) || 0;
|
const sizeBytes = Number(singleDoc.current_version?.size_bytes) || 0;
|
||||||
const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
|
const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
|
||||||
@@ -864,44 +873,6 @@ const DetailPanel = ({
|
|||||||
{singleDoc.original_name}
|
{singleDoc.original_name}
|
||||||
</div>
|
</div>
|
||||||
</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
|
<TagSection
|
||||||
title="Tags"
|
title="Tags"
|
||||||
tags={tagsForDoc.map((tag) => ({
|
tags={tagsForDoc.map((tag) => ({
|
||||||
@@ -1081,17 +1052,13 @@ const DetailPanel = ({
|
|||||||
showCount
|
showCount
|
||||||
className="bulk-correspondents"
|
className="bulk-correspondents"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="secondary"
|
|
||||||
onClick={() => onBulkReanalyze?.()}
|
|
||||||
>
|
|
||||||
Re-analyze selection
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isBulkSelection = selectedCount > 1;
|
||||||
|
const showOcrAction = Boolean(singleDoc && hasOcrAsset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="detail-panel panel">
|
<aside className="detail-panel panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
@@ -1101,9 +1068,81 @@ const DetailPanel = ({
|
|||||||
className="icon-button ghost"
|
className="icon-button ghost"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close detail panel"
|
aria-label="Close detail panel"
|
||||||
|
title="Close detail panel"
|
||||||
>
|
>
|
||||||
<ChevronsRightIcon />
|
<ChevronsRightIcon />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<div className="panel-body">
|
<div className="panel-body">
|
||||||
|
|||||||
+43
-11
@@ -44,7 +44,6 @@
|
|||||||
--danger-border: color-mix(in oklch, var(--danger) 45%, transparent);
|
--danger-border: color-mix(in oklch, var(--danger) 45%, transparent);
|
||||||
--danger-soft: color-mix(in oklch, var(--danger) 12%, transparent);
|
--danger-soft: color-mix(in oklch, var(--danger) 12%, transparent);
|
||||||
--danger-subtle: color-mix(in oklch, var(--danger) 8%, transparent);
|
--danger-subtle: color-mix(in oklch, var(--danger) 8%, transparent);
|
||||||
--detail-panel-width: 30em;
|
|
||||||
|
|
||||||
/* --- Explorer states --- */
|
/* --- Explorer states --- */
|
||||||
--row-hover-bg: color-mix(in oklch, var(--accent) 6%, transparent);
|
--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-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
|
--detail-panel-width: 30em;
|
||||||
|
--documents-grid-title-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -178,6 +180,41 @@ button.danger:hover:not([disabled]) {
|
|||||||
background: transparent;
|
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 {
|
.with-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1528,8 +1565,12 @@ button.danger:hover:not([disabled]) {
|
|||||||
|
|
||||||
.panel-actions {
|
.panel-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-actions .spacer {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-panel .panel-body {
|
.detail-panel .panel-body {
|
||||||
@@ -1542,15 +1583,6 @@ button.danger:hover:not([disabled]) {
|
|||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.32rem;
|
|
||||||
margin: 0.6rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-ocr-trigger {
|
|
||||||
margin: 0.75rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-ocr__content {
|
.detail-ocr__content {
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
IconArrowLeft,
|
IconArrowLeft,
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
IconChevronsRight,
|
IconChevronsRight,
|
||||||
|
IconAnalyze,
|
||||||
|
IconWindowMaximize,
|
||||||
|
IconTextScan2,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import FolderSvg from '../assets/folder.svg';
|
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 {
|
export default {
|
||||||
ChevronIcon,
|
ChevronIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
@@ -141,4 +162,15 @@ export default {
|
|||||||
ViewListIcon,
|
ViewListIcon,
|
||||||
ViewGridIcon,
|
ViewGridIcon,
|
||||||
ChevronsRightIcon,
|
ChevronsRightIcon,
|
||||||
|
AnalyzeIcon,
|
||||||
|
WindowMaximizeIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TextScanIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||||
|
<IconTextScan2
|
||||||
|
className={composeClassName('icon', className)}
|
||||||
|
size={size}
|
||||||
|
stroke={stroke}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user