frontend: preview
This commit is contained in:
+280
-68
@@ -291,6 +291,7 @@ const DocumentsTable = ({
|
|||||||
onFolderDragLeave,
|
onFolderDragLeave,
|
||||||
onFolderDelete,
|
onFolderDelete,
|
||||||
onDocumentSelect,
|
onDocumentSelect,
|
||||||
|
onDocumentOpen,
|
||||||
selectedDocumentId,
|
selectedDocumentId,
|
||||||
onDocumentDragStart,
|
onDocumentDragStart,
|
||||||
onDocumentDragEnd,
|
onDocumentDragEnd,
|
||||||
@@ -371,6 +372,12 @@ const DocumentsTable = ({
|
|||||||
key={doc.id}
|
key={doc.id}
|
||||||
className={`document${selectedDocumentId === doc.id ? ' selected' : ''}`}
|
className={`document${selectedDocumentId === doc.id ? ' selected' : ''}`}
|
||||||
onClick={() => onDocumentSelect(doc.id)}
|
onClick={() => onDocumentSelect(doc.id)}
|
||||||
|
onDoubleClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (onDocumentOpen) {
|
||||||
|
onDocumentOpen(doc.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(event) => onDocumentDragStart(event, doc.id)}
|
onDragStart={(event) => onDocumentDragStart(event, doc.id)}
|
||||||
onDragEnd={onDocumentDragEnd}
|
onDragEnd={onDocumentDragEnd}
|
||||||
@@ -437,6 +444,8 @@ const DetailPanel = ({
|
|||||||
onTagRemove,
|
onTagRemove,
|
||||||
onRegenerateThumbnails,
|
onRegenerateThumbnails,
|
||||||
loading,
|
loading,
|
||||||
|
previewEntry,
|
||||||
|
onOpenPreview,
|
||||||
}) => (
|
}) => (
|
||||||
<aside className="detail-panel column">
|
<aside className="detail-panel column">
|
||||||
<div className="column-header">
|
<div className="column-header">
|
||||||
@@ -470,6 +479,14 @@ const DetailPanel = ({
|
|||||||
<IconDownload className="icon-inline" />
|
<IconDownload className="icon-inline" />
|
||||||
<span>Download</span>
|
<span>Download</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary"
|
||||||
|
onClick={() => onOpenPreview(document.id)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Open preview
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="secondary"
|
className="secondary"
|
||||||
@@ -480,19 +497,16 @@ const DetailPanel = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Thumbnails</dt>
|
<dt>Preview</dt>
|
||||||
<div className="thumbnail-preview">
|
<div className="preview-pane">
|
||||||
{detail.assets?.length ? (
|
{previewEntry?.url ? (
|
||||||
detail.assets.map((asset) => (
|
<iframe
|
||||||
<img
|
src={previewEntry.url}
|
||||||
key={asset.id}
|
title={`Preview of ${detail.document.original_name}`}
|
||||||
src={asset.url}
|
className="preview-object"
|
||||||
alt={`${asset.asset_type} for ${detail.document.original_name}`}
|
/>
|
||||||
className="thumbnail-image"
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<span className="meta">No thumbnails yet.</span>
|
<span className="meta">Preview loading…</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -544,6 +558,75 @@ const DetailPanel = ({
|
|||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PreviewWorkspace = ({
|
||||||
|
document,
|
||||||
|
detail,
|
||||||
|
previewEntry,
|
||||||
|
loading,
|
||||||
|
onClose,
|
||||||
|
onDownload,
|
||||||
|
onRegenerateThumbnails,
|
||||||
|
}) => {
|
||||||
|
if (!document) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = detail?.document?.original_name || document.original_name;
|
||||||
|
const mime = previewEntry?.contentType || document.content_type || 'application/pdf';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="preview-workspace">
|
||||||
|
<header className="preview-workspace__header">
|
||||||
|
<div className="preview-workspace__meta">
|
||||||
|
<button type="button" className="secondary" onClick={onClose}>
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<span className="meta">
|
||||||
|
{document.content_type || 'Document'}
|
||||||
|
{detail?.current_version?.size_bytes
|
||||||
|
? ` · ${(detail.current_version.size_bytes / 1024 / 1024).toFixed(2)} MB`
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="preview-workspace__actions">
|
||||||
|
<button type="button" className="with-icon" onClick={() => onDownload(document.id)}>
|
||||||
|
<IconDownload className="icon-inline" />
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary"
|
||||||
|
onClick={() => onRegenerateThumbnails(document.id)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Regenerate thumbnails
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="preview-workspace__body">
|
||||||
|
{loading || !previewEntry?.url ? (
|
||||||
|
<div className="preview-workspace__message">Loading preview…</div>
|
||||||
|
) : (
|
||||||
|
<iframe
|
||||||
|
src={previewEntry.url}
|
||||||
|
title={`Preview of ${title}`}
|
||||||
|
className="preview-workspace__object"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{detail?.document?.metadata && Object.keys(detail.document.metadata).length > 0 && (
|
||||||
|
<section className="preview-workspace__metadata">
|
||||||
|
<h3>Metadata</h3>
|
||||||
|
<pre>{JSON.stringify(detail.document.metadata, null, 2)}</pre>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Sidebar = ({
|
const Sidebar = ({
|
||||||
folderNodes,
|
folderNodes,
|
||||||
onToggle,
|
onToggle,
|
||||||
@@ -887,6 +970,10 @@ function App() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const folderPathCacheRef = useRef(new Map());
|
const folderPathCacheRef = useRef(new Map());
|
||||||
|
const previewCacheRef = useRef(new Map());
|
||||||
|
const [previewCacheTick, setPreviewCacheTick] = useState(0);
|
||||||
|
const [previewDocumentId, setPreviewDocumentId] = useState(null);
|
||||||
|
const [previewDocumentLoading, setPreviewDocumentLoading] = useState(false);
|
||||||
|
|
||||||
const ensureFolderPathOnServer = useCallback(
|
const ensureFolderPathOnServer = useCallback(
|
||||||
async (baseFolderId, segments) => {
|
async (baseFolderId, segments) => {
|
||||||
@@ -914,6 +1001,38 @@ function App() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ensurePreviewUrl = useCallback(
|
||||||
|
async (documentId, { force = false } = {}) => {
|
||||||
|
if (!documentId) return null;
|
||||||
|
|
||||||
|
const cached = previewCacheRef.current.get(documentId);
|
||||||
|
const now = Date.now();
|
||||||
|
if (!force && cached && (!cached.expiresAt || cached.expiresAt > now)) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await api.get(`/documents/${documentId}/download`);
|
||||||
|
const ttl = data.expires_in ? Math.max(data.expires_in - 60, 30) * 1000 : 5 * 60 * 1000;
|
||||||
|
const entry = {
|
||||||
|
url: data.url,
|
||||||
|
contentType: data.content_type || null,
|
||||||
|
filename: data.filename,
|
||||||
|
expiresAt: now + ttl,
|
||||||
|
};
|
||||||
|
|
||||||
|
previewCacheRef.current.set(documentId, entry);
|
||||||
|
setPreviewCacheTick((prev) => prev + 1);
|
||||||
|
return entry;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setStatusMessage('Unable to fetch document preview.', 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setStatusMessage],
|
||||||
|
);
|
||||||
|
|
||||||
const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => {
|
const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => {
|
||||||
if (
|
if (
|
||||||
!dataTransfer ||
|
!dataTransfer ||
|
||||||
@@ -987,7 +1106,15 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
folderPathCacheRef.current.clear();
|
folderPathCacheRef.current.clear();
|
||||||
|
|
||||||
const extracted = await extractFilesFromDataTransfer(dataTransfer);
|
let extracted;
|
||||||
|
try {
|
||||||
|
extracted = await extractFilesFromDataTransfer(dataTransfer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setStatusMessage(error.message || 'Failed to process dropped files.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!extracted.length) {
|
if (!extracted.length) {
|
||||||
setStatusMessage('No files to upload.', 'info');
|
setStatusMessage('No files to upload.', 'info');
|
||||||
return;
|
return;
|
||||||
@@ -1101,6 +1228,40 @@ const handleDownload = useCallback(
|
|||||||
[token, ensureDocumentDetail, setStatusMessage],
|
[token, ensureDocumentDetail, setStatusMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openDocumentPreview = useCallback(
|
||||||
|
async (documentId) => {
|
||||||
|
if (!documentId) return;
|
||||||
|
setPreviewDocumentId(documentId);
|
||||||
|
setPreviewDocumentLoading(true);
|
||||||
|
try {
|
||||||
|
await ensureDocumentDetail(documentId, { force: false });
|
||||||
|
await ensurePreviewUrl(documentId, { force: false });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setPreviewDocumentId(null);
|
||||||
|
} finally {
|
||||||
|
setPreviewDocumentLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ensureDocumentDetail, ensurePreviewUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeDocumentPreview = useCallback(() => {
|
||||||
|
setPreviewDocumentId(null);
|
||||||
|
setPreviewDocumentLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!previewDocumentId) return;
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeDocumentPreview();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [previewDocumentId, closeDocumentPreview]);
|
||||||
|
|
||||||
const handleTagRemove = useCallback(
|
const handleTagRemove = useCallback(
|
||||||
async (documentId, tagId) => {
|
async (documentId, tagId) => {
|
||||||
try {
|
try {
|
||||||
@@ -1526,6 +1687,38 @@ const handleDownload = useCallback(
|
|||||||
? documentDetails.get(selectedDocument.id)
|
? documentDetails.get(selectedDocument.id)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedDocument) return;
|
||||||
|
ensurePreviewUrl(selectedDocument.id).catch(() => {});
|
||||||
|
}, [selectedDocument, ensurePreviewUrl]);
|
||||||
|
|
||||||
|
const selectedPreviewEntry = useMemo(() => {
|
||||||
|
if (!selectedDocument) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return previewCacheRef.current.get(selectedDocument.id) || null;
|
||||||
|
}, [selectedDocument, previewCacheTick]);
|
||||||
|
|
||||||
|
const previewWorkspaceEntry = useMemo(() => {
|
||||||
|
if (!previewDocumentId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return previewCacheRef.current.get(previewDocumentId) || null;
|
||||||
|
}, [previewDocumentId, previewCacheTick]);
|
||||||
|
|
||||||
|
const previewWorkspaceDetail = previewDocumentId
|
||||||
|
? documentDetails.get(previewDocumentId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const previewWorkspaceDocument = useMemo(() => {
|
||||||
|
if (!previewDocumentId) return null;
|
||||||
|
if (previewWorkspaceDetail?.document) return previewWorkspaceDetail.document;
|
||||||
|
const pool = searchResults ?? documents;
|
||||||
|
return pool.find((doc) => doc.id === previewDocumentId) || null;
|
||||||
|
}, [previewDocumentId, previewWorkspaceDetail, searchResults, documents]);
|
||||||
|
|
||||||
|
const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
{token ? (
|
{token ? (
|
||||||
@@ -1538,7 +1731,9 @@ const handleDownload = useCallback(
|
|||||||
<div className="app-bar__meta">
|
<div className="app-bar__meta">
|
||||||
<h1>Paperless-NEO</h1>
|
<h1>Paperless-NEO</h1>
|
||||||
<span className="app-bar__hint">
|
<span className="app-bar__hint">
|
||||||
Drag files anywhere to upload into "{currentFolderName}".
|
{previewActive
|
||||||
|
? 'Viewing document preview. Press ← Back to return to the library.'
|
||||||
|
: `Drag files anywhere to upload into "${currentFolderName}".`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="app-bar__actions">
|
<div className="app-bar__actions">
|
||||||
@@ -1560,60 +1755,77 @@ const handleDownload = useCallback(
|
|||||||
<StatusBanner status={status} />
|
<StatusBanner status={status} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<main className="app-main">
|
<main className={previewActive ? 'preview-main' : 'app-main'}>
|
||||||
<Sidebar
|
{previewActive && previewWorkspaceDocument ? (
|
||||||
folderNodes={folderNodes}
|
<PreviewWorkspace
|
||||||
onToggle={folderClickHandlers.onToggle}
|
document={previewWorkspaceDocument}
|
||||||
onSelect={folderClickHandlers.onSelect}
|
detail={previewWorkspaceDetail}
|
||||||
onDrop={folderClickHandlers.onDrop}
|
previewEntry={previewWorkspaceEntry}
|
||||||
onDragOver={folderClickHandlers.onDragOver}
|
loading={previewDocumentLoading}
|
||||||
onDragLeave={folderClickHandlers.onDragLeave}
|
onClose={closeDocumentPreview}
|
||||||
onCreateFolder={handleFolderCreate}
|
onDownload={handleDownload}
|
||||||
onDeleteFolder={handleFolderDelete}
|
onRegenerateThumbnails={handleThumbnailRegeneration}
|
||||||
disableActions={loading}
|
/>
|
||||||
selectedFolder={selectedFolder}
|
) : (
|
||||||
/>
|
<>
|
||||||
<DocumentsTable
|
<Sidebar
|
||||||
currentFolderName={currentFolderName}
|
folderNodes={folderNodes}
|
||||||
loading={loading}
|
onToggle={folderClickHandlers.onToggle}
|
||||||
onRefresh={refreshCurrentFolder}
|
onSelect={folderClickHandlers.onSelect}
|
||||||
subfolders={currentSubfolders}
|
onDrop={folderClickHandlers.onDrop}
|
||||||
documents={documents}
|
onDragOver={folderClickHandlers.onDragOver}
|
||||||
searchResults={searchResults}
|
onDragLeave={folderClickHandlers.onDragLeave}
|
||||||
isFilterActive={isFilterActive}
|
onCreateFolder={handleFolderCreate}
|
||||||
onFolderSelect={selectFolder}
|
onDeleteFolder={handleFolderDelete}
|
||||||
onFolderDrop={folderClickHandlers.onDrop}
|
disableActions={loading}
|
||||||
onFolderDragOver={folderClickHandlers.onDragOver}
|
selectedFolder={selectedFolder}
|
||||||
onFolderDragLeave={folderClickHandlers.onDragLeave}
|
/>
|
||||||
onFolderDelete={handleFolderDelete}
|
<DocumentsTable
|
||||||
onDocumentSelect={(docId) => {
|
currentFolderName={currentFolderName}
|
||||||
setSelectedDocumentId(docId);
|
loading={loading}
|
||||||
ensureDocumentDetail(docId);
|
onRefresh={refreshCurrentFolder}
|
||||||
}}
|
subfolders={currentSubfolders}
|
||||||
selectedDocumentId={selectedDocumentId}
|
documents={documents}
|
||||||
onDocumentDragStart={(event, documentId) => {
|
searchResults={searchResults}
|
||||||
setDraggedDocumentId(documentId);
|
isFilterActive={isFilterActive}
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
onFolderSelect={selectFolder}
|
||||||
event.dataTransfer.setData('application/x-paperless-doc', documentId);
|
onFolderDrop={folderClickHandlers.onDrop}
|
||||||
event.currentTarget.classList.add('dragging');
|
onFolderDragOver={folderClickHandlers.onDragOver}
|
||||||
}}
|
onFolderDragLeave={folderClickHandlers.onDragLeave}
|
||||||
onDocumentDragEnd={(event) => {
|
onFolderDelete={handleFolderDelete}
|
||||||
setDraggedDocumentId(null);
|
onDocumentSelect={(docId) => {
|
||||||
event.currentTarget.classList.remove('dragging');
|
setSelectedDocumentId(docId);
|
||||||
}}
|
ensureDocumentDetail(docId);
|
||||||
onDownload={handleDownload}
|
}}
|
||||||
filterBar={filterBar}
|
onDocumentOpen={openDocumentPreview}
|
||||||
/>
|
selectedDocumentId={selectedDocumentId}
|
||||||
<DetailPanel
|
onDocumentDragStart={(event, documentId) => {
|
||||||
document={selectedDocument}
|
setDraggedDocumentId(documentId);
|
||||||
detail={selectedDetail}
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
tags={tags}
|
event.dataTransfer.setData('application/x-paperless-doc', documentId);
|
||||||
onDownload={handleDownload}
|
event.currentTarget.classList.add('dragging');
|
||||||
onTagAdd={handleTagAdd}
|
}}
|
||||||
onTagRemove={handleTagRemove}
|
onDocumentDragEnd={(event) => {
|
||||||
onRegenerateThumbnails={handleThumbnailRegeneration}
|
setDraggedDocumentId(null);
|
||||||
loading={loading}
|
event.currentTarget.classList.remove('dragging');
|
||||||
/>
|
}}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
filterBar={filterBar}
|
||||||
|
/>
|
||||||
|
<DetailPanel
|
||||||
|
document={selectedDocument}
|
||||||
|
detail={selectedDetail}
|
||||||
|
tags={tags}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
onTagAdd={handleTagAdd}
|
||||||
|
onTagRemove={handleTagRemove}
|
||||||
|
onRegenerateThumbnails={handleThumbnailRegeneration}
|
||||||
|
loading={loading}
|
||||||
|
previewEntry={selectedPreviewEntry}
|
||||||
|
onOpenPreview={openDocumentPreview}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+104
-2
@@ -173,6 +173,89 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.75rem 1.5rem 1.25rem;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__meta h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__meta .meta {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__body {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__object {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__message {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__metadata {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--surface);
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-workspace__metadata h3 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -381,8 +464,8 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
width: 36px;
|
width: 36px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 0;
|
border-radius: 3px;
|
||||||
box-shadow: none;
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb-placeholder {
|
.thumb-placeholder {
|
||||||
@@ -526,6 +609,25 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
margin: 0.6rem 0;
|
margin: 0.6rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-pane {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
min-height: 220px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-object {
|
||||||
|
width: 100%;
|
||||||
|
height: 320px;
|
||||||
|
border: none;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
.thumbnail-preview {
|
.thumbnail-preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user