document viewer
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import DocumentSummarySection from './DocumentSummarySection';
|
||||
import { buildDocumentMetadataItems, extractDocumentMetadataPayload } from './documentMetadata';
|
||||
|
||||
@@ -16,6 +16,13 @@ const DocumentInfoPanel = ({
|
||||
resetKey = null,
|
||||
classNamePrefix = 'document-info',
|
||||
hideTabNavWhenSingle = true,
|
||||
summaryPlacement = 'inline',
|
||||
summaryTabLabel = 'Summary',
|
||||
summaryTabId = 'summary',
|
||||
leadingTabs = [],
|
||||
trailingTabs = [],
|
||||
tabsPlacement = 'top',
|
||||
summaryLayout = 'default',
|
||||
}) => {
|
||||
const base = classNamePrefix;
|
||||
|
||||
@@ -92,29 +99,90 @@ const DocumentInfoPanel = ({
|
||||
};
|
||||
}, [contentConfig, contentEnabled, showContentTab, document?.id, resetKey]);
|
||||
|
||||
const renderSummarySection = useCallback(() => (
|
||||
<DocumentSummarySection
|
||||
document={document}
|
||||
detailItems={metadataItems}
|
||||
layout={summaryLayout}
|
||||
{...summaryProps}
|
||||
/>
|
||||
), [document, summaryLayout, summaryProps, metadataItems]);
|
||||
|
||||
const renderDetailsSection = useCallback(() => (
|
||||
<section className={`${base}__section`}>
|
||||
{metadataItems.length ? (
|
||||
<dl className={`${base}__section-list`}>
|
||||
{metadataItems.map(({ label, value }) => (
|
||||
<div className={`${base}__section-item`} key={label}>
|
||||
<dt>{label}</dt>
|
||||
<dd>{value || '—'}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : (
|
||||
<p className={`${base}__section-placeholder`}>No details available.</p>
|
||||
)}
|
||||
</section>
|
||||
), [base, metadataItems]);
|
||||
|
||||
const summaryInline = summaryPlacement !== 'tabs';
|
||||
|
||||
const summaryTab = useMemo(() => {
|
||||
if (summaryPlacement !== 'tabs') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: summaryTabId,
|
||||
label: summaryTabLabel,
|
||||
render: () => (
|
||||
<div className={`${base}__summary-tab-content`}>
|
||||
{renderSummarySection()}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}, [summaryPlacement, summaryTabId, summaryTabLabel, base, renderSummarySection]);
|
||||
|
||||
const normalizedLeadingTabs = useMemo(
|
||||
() => (Array.isArray(leadingTabs)
|
||||
? leadingTabs.filter((tab) => tab && tab.id && tab.label)
|
||||
: []),
|
||||
[leadingTabs],
|
||||
);
|
||||
|
||||
const normalizedTrailingTabs = useMemo(
|
||||
() => (Array.isArray(trailingTabs)
|
||||
? trailingTabs.filter((tab) => tab && tab.id && tab.label)
|
||||
: []),
|
||||
[trailingTabs],
|
||||
);
|
||||
|
||||
const summaryNode = summaryInline
|
||||
? (
|
||||
<>
|
||||
{renderSummarySection()}
|
||||
{renderDetailsSection()}
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
|
||||
const visibleTabs = useMemo(() => {
|
||||
const tabsList = [];
|
||||
|
||||
tabsList.push({
|
||||
id: 'details',
|
||||
label: detailsTabLabel,
|
||||
render: () => (
|
||||
<section className={`${base}__section`}>
|
||||
{metadataItems.length ? (
|
||||
<dl className={`${base}__section-list`}>
|
||||
{metadataItems.map(({ label, value }) => (
|
||||
<div className={`${base}__section-item`} key={label}>
|
||||
<dt>{label}</dt>
|
||||
<dd>{value || '—'}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : (
|
||||
<p className={`${base}__section-placeholder`}>No details available.</p>
|
||||
)}
|
||||
</section>
|
||||
),
|
||||
});
|
||||
if (normalizedLeadingTabs.length) {
|
||||
tabsList.push(...normalizedLeadingTabs);
|
||||
}
|
||||
|
||||
if (summaryTab) {
|
||||
tabsList.push(summaryTab);
|
||||
}
|
||||
|
||||
if (summaryPlacement !== 'tabs') {
|
||||
tabsList.push({
|
||||
id: 'details',
|
||||
label: detailsTabLabel,
|
||||
render: () => renderDetailsSection(),
|
||||
});
|
||||
}
|
||||
|
||||
if (showContentTab && contentConfig) {
|
||||
tabsList.push({
|
||||
@@ -183,30 +251,38 @@ const DocumentInfoPanel = ({
|
||||
}
|
||||
|
||||
if (metadataPayload) {
|
||||
tabsList.push({
|
||||
id: 'metadata',
|
||||
label: metadataTabLabel,
|
||||
render: () => (
|
||||
<section className={`${base}__section ${base}__section--metadata-json`}>
|
||||
<pre className={`${base}__metadata-json`}>
|
||||
{JSON.stringify(metadataPayload, null, 2)}
|
||||
</pre>
|
||||
</section>
|
||||
),
|
||||
});
|
||||
tabsList.push({
|
||||
id: 'metadata',
|
||||
label: metadataTabLabel,
|
||||
render: () => (
|
||||
<section className={`${base}__section ${base}__section--metadata-json`}>
|
||||
<pre className={`${base}__metadata-json`}>
|
||||
{JSON.stringify(metadataPayload, null, 2)}
|
||||
</pre>
|
||||
</section>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (normalizedTrailingTabs.length) {
|
||||
tabsList.push(...normalizedTrailingTabs);
|
||||
}
|
||||
|
||||
return tabsList;
|
||||
}, [
|
||||
base,
|
||||
detailsTabLabel,
|
||||
metadataItems,
|
||||
contentConfig,
|
||||
contentEnabled,
|
||||
contentState,
|
||||
metadataPayload,
|
||||
metadataTabLabel,
|
||||
showContentTab,
|
||||
summaryTab,
|
||||
normalizedLeadingTabs,
|
||||
normalizedTrailingTabs,
|
||||
summaryPlacement,
|
||||
renderDetailsSection,
|
||||
]);
|
||||
|
||||
const fallbackTabId = useMemo(() => {
|
||||
@@ -242,7 +318,7 @@ const DocumentInfoPanel = ({
|
||||
if (!isControlled) {
|
||||
setUncontrolledTab(fallbackTabId);
|
||||
}
|
||||
}, [fallbackTabId, resetKey, isControlled]);
|
||||
}, [fallbackTabId, isControlled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled && controlledActiveTab && !visibleTabs.some((tab) => tab.id === controlledActiveTab)) {
|
||||
@@ -270,12 +346,40 @@ const DocumentInfoPanel = ({
|
||||
const singleTab = visibleTabs.length === 1 ? visibleTabs[0] : null;
|
||||
const shouldHideNav = hideTabNavWhenSingle && singleTab;
|
||||
|
||||
const tabNav = (
|
||||
<div className={`${base}__tabs`} role="tablist" aria-label="Document details">
|
||||
{visibleTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab.id === activeTabId}
|
||||
className={`${base}__tab${tab.id === activeTabId ? ' is-active' : ''}`}
|
||||
onClick={() => handleTabSelect(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const tabPanels = (
|
||||
<div className={`${base}__tabpanes`}>
|
||||
{visibleTabs.map((tab) => (
|
||||
tab.id === activeTabId ? (
|
||||
<div key={tab.id} role="tabpanel" className={`${base}__tabpanel`}>
|
||||
{renderTabContent(tab, { document })}
|
||||
</div>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const tabsWrapperClass = `${base}__tabs-wrapper${tabsPlacement === 'bottom' ? ` ${base}__tabs-wrapper--bottom` : ''}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentSummarySection
|
||||
document={document}
|
||||
{...summaryProps}
|
||||
/>
|
||||
{summaryNode}
|
||||
{shouldHideNav ? (
|
||||
<div className={`${base}__tabpanes ${base}__tabpanes--single`}>
|
||||
<div className={`${base}__tabpanel`}>
|
||||
@@ -283,30 +387,11 @@ const DocumentInfoPanel = ({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${base}__tabs-wrapper`}>
|
||||
<div className={`${base}__tabs`} role="tablist" aria-label="Document details">
|
||||
{visibleTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab.id === activeTabId}
|
||||
className={`${base}__tab${tab.id === activeTabId ? ' is-active' : ''}`}
|
||||
onClick={() => handleTabSelect(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${base}__tabpanes`}>
|
||||
{visibleTabs.map((tab) => (
|
||||
tab.id === activeTabId ? (
|
||||
<div key={tab.id} role="tabpanel" className={`${base}__tabpanel`}>
|
||||
{renderTabContent(tab, { document })}
|
||||
</div>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
<div className={tabsWrapperClass}>
|
||||
{tabsPlacement !== 'bottom' ? tabNav : null}
|
||||
{tabsPlacement === 'bottom' ? tabPanels : null}
|
||||
{tabsPlacement === 'bottom' ? tabNav : null}
|
||||
{tabsPlacement !== 'bottom' ? tabPanels : null}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -348,8 +348,11 @@ const DocumentSummarySection = ({
|
||||
onCorrespondentAdd,
|
||||
onCorrespondentRemove,
|
||||
onUpdateTitle,
|
||||
onUpdateIssued
|
||||
onUpdateIssued,
|
||||
layout = 'default',
|
||||
detailItems = [],
|
||||
}) => {
|
||||
const isCompactLayout = layout === 'compact';
|
||||
const summary = useMemo(() => {
|
||||
if (!document) {
|
||||
return {
|
||||
@@ -507,6 +510,204 @@ const DocumentSummarySection = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const TitleSection = () => (
|
||||
editableTitle && isTitleEditing ? (
|
||||
<form className="doc-title-edit" onSubmit={submitTitleEdit}>
|
||||
<input
|
||||
value={titleDraft}
|
||||
onChange={(event) => {
|
||||
setTitleDraft(event.target.value);
|
||||
if (titleError) {
|
||||
setTitleError(null);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelTitleEdit();
|
||||
}
|
||||
}}
|
||||
aria-label="Document title"
|
||||
autoFocus
|
||||
disabled={titleSaving}
|
||||
/>
|
||||
<button type="submit" disabled={titleSaving}>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={cancelTitleEdit}
|
||||
disabled={titleSaving}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="doc-title-row__title">{summary.title}</h3>
|
||||
{editableTitle ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={startTitleEdit}
|
||||
aria-label="Edit title"
|
||||
title="Edit title"
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
const issuedDisplay = editableIssued && isIssuedEditing ? (
|
||||
<form className="doc-issued-edit" onSubmit={submitIssuedEdit}>
|
||||
<input
|
||||
type="date"
|
||||
value={issuedDraft}
|
||||
onChange={(event) => {
|
||||
setIssuedDraft(event.target.value);
|
||||
if (issuedError) {
|
||||
setIssuedError(null);
|
||||
}
|
||||
}}
|
||||
aria-label="Issued on"
|
||||
disabled={issuedSaving}
|
||||
/>
|
||||
<button type="submit" disabled={issuedSaving}>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={cancelIssuedEdit}
|
||||
disabled={issuedSaving}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<span className="detail-meta__value">{issuedDateLabel || 'Not set'}</span>
|
||||
{editableIssued ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={startIssuedEdit}
|
||||
aria-label={issuedDateLabel ? 'Edit issued date' : 'Set issued date'}
|
||||
title={issuedDateLabel ? 'Edit issued date' : 'Set issued date'}
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const metaItems = [
|
||||
{
|
||||
key: 'issued',
|
||||
label: 'Issued',
|
||||
valueContent: issuedDisplay,
|
||||
error: issuedError,
|
||||
},
|
||||
...metaRows.map((row) => ({
|
||||
key: row.key,
|
||||
label: row.label,
|
||||
fallbackValue: row.value,
|
||||
})),
|
||||
];
|
||||
|
||||
const detailRows = Array.isArray(detailItems)
|
||||
? detailItems.map((item, index) => ({
|
||||
key: `detail-${item?.label || index}`,
|
||||
label: item?.label || '—',
|
||||
fallbackValue: item?.value,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const compactRows = [...metaItems, ...detailRows];
|
||||
|
||||
const renderTags = () => (
|
||||
<section className="document-summary__section document-summary__section--tags">
|
||||
<TagSection
|
||||
tags={resolvedTags}
|
||||
onRemove={
|
||||
onTagRemove
|
||||
? (tag) => onTagRemove(document.id, tag.id)
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
onTagAdd
|
||||
? ({ value, option }) => onTagAdd(document, value, { option })
|
||||
: undefined
|
||||
}
|
||||
datalistOptions={tagOptions}
|
||||
className="document-summary__tags"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
const renderCorrespondents = () => (
|
||||
<section className="document-summary__section document-summary__section--correspondents">
|
||||
<CorrespondentSection
|
||||
entries={resolvedCorrespondents}
|
||||
onRemove={
|
||||
onCorrespondentRemove
|
||||
? (entry) =>
|
||||
onCorrespondentRemove({
|
||||
documentId: document.id,
|
||||
correspondentId: entry.id,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
onCorrespondentAdd
|
||||
? ({ name, option }) =>
|
||||
onCorrespondentAdd({
|
||||
document,
|
||||
name,
|
||||
option,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
showCount
|
||||
datalistOptions={correspondentOptions}
|
||||
className="document-summary__correspondents"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
if (isCompactLayout) {
|
||||
return (
|
||||
<div className="document-summary document-summary--compact">
|
||||
<section className="document-summary__section document-summary__title-row">
|
||||
<TitleSection />
|
||||
</section>
|
||||
{titleError ? <div className="status-inline error">{titleError}</div> : null}
|
||||
{renderTags()}
|
||||
{renderCorrespondents()}
|
||||
{compactRows.length ? (
|
||||
<section className="document-summary__section document-summary__meta document-summary__meta--compact">
|
||||
<dl className="document-summary__details-list document-summary__details-list--meta">
|
||||
{compactRows.map((item) => (
|
||||
<div key={item.key} className="document-summary__details-row">
|
||||
<dt>{item.label}</dt>
|
||||
<dd>
|
||||
{item.valueContent != null && item.valueContent !== ''
|
||||
? item.valueContent
|
||||
: item.fallbackValue || '—'}
|
||||
</dd>
|
||||
{item.error ? <div className="status-inline error">{item.error}</div> : null}
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="document-summary">
|
||||
<div className="doc-title-row">
|
||||
@@ -552,62 +753,21 @@ const DocumentSummarySection = ({
|
||||
className="icon-button"
|
||||
onClick={startTitleEdit}
|
||||
aria-label="Edit title"
|
||||
title="Edit title"
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
title="Edit title"
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{titleError ? <div className="status-inline error">{titleError}</div> : null}
|
||||
|
||||
<div className="detail-meta">
|
||||
<div className="detail-meta__row">
|
||||
<span className="detail-meta__label">Issued:</span>
|
||||
{editableIssued && isIssuedEditing ? (
|
||||
<form className="doc-issued-edit" onSubmit={submitIssuedEdit}>
|
||||
<input
|
||||
type="date"
|
||||
value={issuedDraft}
|
||||
onChange={(event) => {
|
||||
setIssuedDraft(event.target.value);
|
||||
if (issuedError) {
|
||||
setIssuedError(null);
|
||||
}
|
||||
}}
|
||||
aria-label="Issued on"
|
||||
disabled={issuedSaving}
|
||||
/>
|
||||
<button type="submit" disabled={issuedSaving}>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={cancelIssuedEdit}
|
||||
disabled={issuedSaving}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<span className="detail-meta__value">{issuedDateLabel || 'Not set'}</span>
|
||||
{editableIssued ? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={startIssuedEdit}
|
||||
aria-label={issuedDateLabel ? 'Edit issued date' : 'Set issued date'}
|
||||
title={issuedDateLabel ? 'Edit issued date' : 'Set issued date'}
|
||||
>
|
||||
<EditIcon className="icon-inline" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{issuedDisplay}
|
||||
</div>
|
||||
{issuedError ? <div className="status-inline error">{issuedError}</div> : null}
|
||||
|
||||
@@ -619,45 +779,9 @@ const DocumentSummarySection = ({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<TagSection
|
||||
tags={resolvedTags}
|
||||
onRemove={
|
||||
onTagRemove
|
||||
? (tag) => onTagRemove(document.id, tag.id)
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
onTagAdd
|
||||
? ({ value, option }) => onTagAdd(document, value, { option })
|
||||
: undefined
|
||||
}
|
||||
datalistOptions={tagOptions}
|
||||
/>
|
||||
{renderTags()}
|
||||
|
||||
<CorrespondentSection
|
||||
entries={resolvedCorrespondents}
|
||||
onRemove={
|
||||
onCorrespondentRemove
|
||||
? (entry) =>
|
||||
onCorrespondentRemove({
|
||||
documentId: document.id,
|
||||
correspondentId: entry.id,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onAdd={
|
||||
onCorrespondentAdd
|
||||
? ({ name, option }) =>
|
||||
onCorrespondentAdd({
|
||||
document,
|
||||
name,
|
||||
option,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
showCount
|
||||
datalistOptions={correspondentOptions}
|
||||
/>
|
||||
{renderCorrespondents()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
||||
import { DownloadIcon } from '../ui/icons';
|
||||
|
||||
@@ -13,7 +13,10 @@ const DocumentViewerLayout = ({
|
||||
defaultTabId = 'details',
|
||||
infoPanelProps = {},
|
||||
previewLoadingMessage = 'Preparing preview…',
|
||||
layoutMode = 'split',
|
||||
}) => {
|
||||
const isStacked = layoutMode === 'stacked';
|
||||
|
||||
const previewContent = useMemo(() => {
|
||||
if (!document || !previewEntry?.url) {
|
||||
return null;
|
||||
@@ -74,30 +77,65 @@ const DocumentViewerLayout = ({
|
||||
);
|
||||
}, [previewEntry, document]);
|
||||
|
||||
const renderViewportPane = useCallback(() => (
|
||||
<div className="document-viewer__viewport">
|
||||
{!previewEntry?.url ? (
|
||||
<div className="document-viewer__message">{previewLoadingMessage}</div>
|
||||
) : (
|
||||
previewContent
|
||||
)}
|
||||
</div>
|
||||
), [previewEntry?.url, previewLoadingMessage, previewContent]);
|
||||
|
||||
const viewportPane = renderViewportPane();
|
||||
|
||||
const stackedLeadingTabs = useMemo(() => (
|
||||
isStacked
|
||||
? [
|
||||
{
|
||||
id: 'preview',
|
||||
label: 'Preview',
|
||||
render: () => renderViewportPane(),
|
||||
},
|
||||
]
|
||||
: []
|
||||
), [isStacked, renderViewportPane]);
|
||||
|
||||
const resolvedDefaultTabId = isStacked ? 'preview' : defaultTabId;
|
||||
const summaryPlacement = 'tabs';
|
||||
const tabsPlacement = isStacked ? 'bottom' : 'top';
|
||||
const summaryLayout = 'compact';
|
||||
|
||||
const detailsPane = (
|
||||
<div className="document-viewer__details-pane">
|
||||
<div className="document-viewer__details">
|
||||
<DocumentInfoPanel
|
||||
document={document}
|
||||
summaryProps={summaryProps}
|
||||
metadataPayload={metadataPayload}
|
||||
contentConfig={contentTabConfig}
|
||||
defaultTabId={resolvedDefaultTabId}
|
||||
classNamePrefix={classNamePrefix}
|
||||
hideTabNavWhenSingle={false}
|
||||
resetKey={resetKey || document?.id}
|
||||
summaryPlacement={summaryPlacement}
|
||||
summaryLayout={summaryLayout}
|
||||
leadingTabs={stackedLeadingTabs}
|
||||
tabsPlacement={tabsPlacement}
|
||||
{...infoPanelProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isStacked) {
|
||||
return detailsPane;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="document-viewer__details-pane">
|
||||
<div className="document-viewer__details">
|
||||
<DocumentInfoPanel
|
||||
document={document}
|
||||
summaryProps={summaryProps}
|
||||
metadataPayload={metadataPayload}
|
||||
contentConfig={contentTabConfig}
|
||||
defaultTabId={defaultTabId}
|
||||
classNamePrefix={classNamePrefix}
|
||||
hideTabNavWhenSingle={false}
|
||||
resetKey={resetKey || document?.id}
|
||||
{...infoPanelProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="document-viewer__viewport">
|
||||
{!previewEntry?.url ? (
|
||||
<div className="document-viewer__message">{previewLoadingMessage}</div>
|
||||
) : (
|
||||
previewContent
|
||||
)}
|
||||
</div>
|
||||
{detailsPane}
|
||||
{viewportPane}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -432,6 +432,7 @@ const DocumentViewerPanel = ({
|
||||
metadataPayload={metadataPayload}
|
||||
contentTabConfig={contentTabConfig}
|
||||
previewLoadingMessage="Loading preview…"
|
||||
layoutMode={isStackedLayout ? 'stacked' : 'split'}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
font-size: 15px;
|
||||
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
|
||||
--detail-panel-width: 30em;
|
||||
--detail-panel-width: calc(100vw / 3);
|
||||
--documents-grid-title-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -489,6 +489,88 @@
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
.document-summary--compact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.document-summary__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.document-summary__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.document-summary__meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.document-summary__meta-label {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.document-summary__meta-value {
|
||||
color: var(--fg);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.document-summary__meta-value .detail-meta__value {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.document-summary__section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tag-list.document-summary__tags,
|
||||
.correspondent-list.document-summary__correspondents {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.document-summary__details {
|
||||
}
|
||||
|
||||
.document-summary__details-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.document-summary__details-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.document-summary__details-row dt {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.document-summary__details-row dd {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.detail-panel .doc-title-edit,
|
||||
.document-summary .doc-title-edit {
|
||||
display: flex;
|
||||
@@ -773,7 +855,7 @@
|
||||
|
||||
.detail-panel dt {
|
||||
font-weight: 600;
|
||||
margin-top: 0.8rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.detail-panel dd {
|
||||
@@ -786,7 +868,6 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem 1rem;
|
||||
padding: 0.5rem 1rem 0.5rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-height: calc(var(--document-viewer-portrait-height-ratio) * 100vh);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
order: -1;
|
||||
}
|
||||
|
||||
@@ -189,6 +190,15 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.document-viewer__tabs-wrapper--bottom .document-viewer__tabpanes {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.document-viewer__tabs-wrapper--bottom .document-viewer__tabs {
|
||||
order: 2;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.document-viewer__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -268,28 +278,37 @@
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.35;
|
||||
overflow: auto;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.document-viewer__tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid var(--outline-subtle);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-overlay, var(--surface));
|
||||
border: 1px solid var(--outline-subtle);
|
||||
box-shadow: var(--panel-shadow-soft, none);
|
||||
}
|
||||
|
||||
.document-viewer__tab {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0.4rem 0.75rem;
|
||||
padding: 0.35rem 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition:
|
||||
color 120ms ease,
|
||||
border-color 120ms ease;
|
||||
background-color 120ms ease;
|
||||
border-radius: 999px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.document-viewer__tab:hover,
|
||||
@@ -298,8 +317,8 @@
|
||||
}
|
||||
|
||||
.document-viewer__tab.is-active {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent-strong, var(--accent));
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.document-viewer__tabpanes {
|
||||
@@ -308,6 +327,10 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.document-viewer--stacked .document-viewer__tabpanes {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.document-viewer__tabpanes--single {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -345,7 +368,7 @@
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
max-height: 100%;
|
||||
grid-area: viewport;
|
||||
@@ -369,12 +392,11 @@
|
||||
}
|
||||
|
||||
.document-viewer--stacked .document-viewer__object:not(.document-viewer__object--image) {
|
||||
height: calc(var(--document-viewer-portrait-height-ratio) * 100vh);
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.document-viewer--stacked .document-viewer__object--image {
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.document-viewer__unsupported {
|
||||
@@ -408,3 +430,6 @@
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
.document-viewer__summary-tab-content {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user