mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
pdf layer toggle (#6028)
This commit is contained in:
@@ -4896,6 +4896,7 @@ toggleAttachments = "Toggle Attachments"
|
|||||||
toggleTheme = "Toggle Theme"
|
toggleTheme = "Toggle Theme"
|
||||||
language = "Language"
|
language = "Language"
|
||||||
toggleAnnotations = "Toggle Annotations Visibility"
|
toggleAnnotations = "Toggle Annotations Visibility"
|
||||||
|
toggleLayers = "Toggle Layers"
|
||||||
search = "Search PDF"
|
search = "Search PDF"
|
||||||
panMode = "Pan Mode"
|
panMode = "Pan Mode"
|
||||||
applyRedactionsFirst = "Apply redactions first"
|
applyRedactionsFirst = "Apply redactions first"
|
||||||
|
|||||||
@@ -1,63 +1,4 @@
|
|||||||
/* Attachment Sidebar - Modern styling to match AllTools section */
|
/* Attachment Sidebar - Attachment-specific styles (base styles in SidebarBase.css) */
|
||||||
|
|
||||||
.attachment-sidebar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
color-mix(in srgb, var(--bg-toolbar) 96%, transparent),
|
|
||||||
color-mix(in srgb, var(--bg-background) 90%, transparent)
|
|
||||||
);
|
|
||||||
border-left: 1px solid color-mix(in srgb, var(--border-subtle) 75%, transparent);
|
|
||||||
box-shadow: -2px 0 16px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.35)) 20%, transparent);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header Section */
|
|
||||||
.attachment-sidebar__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.75rem 0.875rem;
|
|
||||||
background: var(--bg-toolbar);
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-sidebar__header-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-sidebar__header-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--mantine-color-blue-6);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search Section */
|
|
||||||
.attachment-sidebar__search {
|
|
||||||
background: var(--tool-panel-search-bg, var(--bg-toolbar));
|
|
||||||
border-bottom: 1px solid var(--tool-panel-search-border-bottom, var(--border-subtle));
|
|
||||||
padding-top: 0.75rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content Section */
|
|
||||||
.attachment-sidebar__content {
|
|
||||||
padding-top: 0.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-sidebar__empty-state,
|
|
||||||
.attachment-sidebar__error {
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-sidebar__loading {
|
|
||||||
padding: 2rem 1rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Attachment List */
|
/* Attachment List */
|
||||||
.attachment-list {
|
.attachment-list {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { PdfAttachmentObject } from '@embedpdf/models';
|
|||||||
import AttachmentIcon from '@mui/icons-material/AttachmentRounded';
|
import AttachmentIcon from '@mui/icons-material/AttachmentRounded';
|
||||||
import DownloadIcon from '@mui/icons-material/DownloadRounded';
|
import DownloadIcon from '@mui/icons-material/DownloadRounded';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import '@app/components/viewer/SidebarBase.css';
|
||||||
import '@app/components/viewer/AttachmentSidebar.css';
|
import '@app/components/viewer/AttachmentSidebar.css';
|
||||||
|
|
||||||
interface AttachmentSidebarProps {
|
interface AttachmentSidebarProps {
|
||||||
@@ -284,7 +285,7 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="attachment-sidebar"
|
className="sidebar-base attachment-sidebar"
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
right: `${(thumbnailVisible ? 15 : 0) + (bookmarkVisible ? 15 : 0)}rem`,
|
right: `${(thumbnailVisible ? 15 : 0) + (bookmarkVisible ? 15 : 0)}rem`,
|
||||||
@@ -294,9 +295,9 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible,
|
|||||||
zIndex: 998,
|
zIndex: 998,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="attachment-sidebar__header">
|
<div className="sidebar-base__header attachment-sidebar__header">
|
||||||
<div className="attachment-sidebar__header-title">
|
<div className="sidebar-base__header-title attachment-sidebar__header-title">
|
||||||
<span className="attachment-sidebar__header-icon">
|
<span className="sidebar-base__header-icon attachment-sidebar__header-icon">
|
||||||
<AttachmentIcon />
|
<AttachmentIcon />
|
||||||
</span>
|
</span>
|
||||||
<Text fw={600} size="sm" tt="uppercase" lts={0.5}>
|
<Text fw={600} size="sm" tt="uppercase" lts={0.5}>
|
||||||
@@ -305,7 +306,7 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Box px="sm" pb="sm" className="attachment-sidebar__search">
|
<Box px="sm" pb="sm" className="sidebar-base__search attachment-sidebar__search">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
placeholder={t('viewer.attachments.searchPlaceholder', 'Search attachments')}
|
placeholder={t('viewer.attachments.searchPlaceholder', 'Search attachments')}
|
||||||
@@ -316,9 +317,9 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible,
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<ScrollArea style={{ flex: 1 }}>
|
<ScrollArea style={{ flex: 1 }}>
|
||||||
<Box p="sm" className="attachment-sidebar__content">
|
<Box p="sm" className="sidebar-base__content attachment-sidebar__content">
|
||||||
{!attachmentSupport && (
|
{!attachmentSupport && (
|
||||||
<div className="attachment-sidebar__empty-state">
|
<div className="sidebar-base__empty-state">
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
{t('viewer.attachments.noSupport', 'Attachment support is unavailable for this viewer.')}
|
{t('viewer.attachments.noSupport', 'Attachment support is unavailable for this viewer.')}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -326,7 +327,7 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{attachmentSupport && showNoDocument && (
|
{attachmentSupport && showNoDocument && (
|
||||||
<div className="attachment-sidebar__empty-state">
|
<div className="sidebar-base__empty-state">
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
{t('viewer.attachments.noDocument', 'Open a PDF to view its attachments.')}
|
{t('viewer.attachments.noDocument', 'Open a PDF to view its attachments.')}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -334,7 +335,7 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{attachmentSupport && documentCacheKey && currentError && (
|
{attachmentSupport && documentCacheKey && currentError && (
|
||||||
<Stack gap="xs" align="center" className="attachment-sidebar__error">
|
<Stack gap="xs" align="center" className="sidebar-base__error">
|
||||||
<Text size="sm" c="red" ta="center">
|
<Text size="sm" c="red" ta="center">
|
||||||
{currentError}
|
{currentError}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -345,7 +346,7 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{attachmentSupport && documentCacheKey && isLocalLoading && (
|
{attachmentSupport && documentCacheKey && isLocalLoading && (
|
||||||
<Stack gap="md" align="center" c="dimmed" py="xl" className="attachment-sidebar__loading">
|
<Stack gap="md" align="center" c="dimmed" py="xl" className="sidebar-base__loading">
|
||||||
<Loader size="md" type="dots" />
|
<Loader size="md" type="dots" />
|
||||||
<Text size="sm" ta="center">
|
<Text size="sm" ta="center">
|
||||||
{t('viewer.attachments.loading', 'Loading attachments...')}
|
{t('viewer.attachments.loading', 'Loading attachments...')}
|
||||||
@@ -354,7 +355,7 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showEmptyState && (
|
{showEmptyState && (
|
||||||
<div className="attachment-sidebar__empty-state">
|
<div className="sidebar-base__empty-state">
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
{t('viewer.attachments.empty', 'No attachments in this document')}
|
{t('viewer.attachments.empty', 'No attachments in this document')}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -368,7 +369,7 @@ export const AttachmentSidebar = ({ visible, thumbnailVisible, bookmarkVisible,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showSearchEmpty && (
|
{showSearchEmpty && (
|
||||||
<div className="attachment-sidebar__empty-state">
|
<div className="sidebar-base__empty-state">
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
{t('viewer.attachments.noMatch', 'No attachments match your search')}
|
{t('viewer.attachments.noMatch', 'No attachments match your search')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,63 +1,4 @@
|
|||||||
/* Bookmark Sidebar - Modern styling to match AllTools section */
|
/* Bookmark Sidebar - Bookmark-specific styles (base styles in SidebarBase.css) */
|
||||||
|
|
||||||
.bookmark-sidebar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
color-mix(in srgb, var(--bg-toolbar) 96%, transparent),
|
|
||||||
color-mix(in srgb, var(--bg-background) 90%, transparent)
|
|
||||||
);
|
|
||||||
border-left: 1px solid color-mix(in srgb, var(--border-subtle) 75%, transparent);
|
|
||||||
box-shadow: -2px 0 16px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.35)) 20%, transparent);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header Section */
|
|
||||||
.bookmark-sidebar__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.75rem 0.875rem;
|
|
||||||
background: var(--bg-toolbar);
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bookmark-sidebar__header-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bookmark-sidebar__header-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--mantine-color-blue-6);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search Section */
|
|
||||||
.bookmark-sidebar__search {
|
|
||||||
background: var(--tool-panel-search-bg, var(--bg-toolbar));
|
|
||||||
border-bottom: 1px solid var(--tool-panel-search-border-bottom, var(--border-subtle));
|
|
||||||
padding-top: 0.75rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content Section */
|
|
||||||
.bookmark-sidebar__content {
|
|
||||||
padding-top: 0.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bookmark-sidebar__empty-state,
|
|
||||||
.bookmark-sidebar__error {
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bookmark-sidebar__loading {
|
|
||||||
padding: 2rem 1rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bookmark List */
|
/* Bookmark List */
|
||||||
.bookmark-list {
|
.bookmark-list {
|
||||||
@@ -193,4 +134,3 @@
|
|||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import LocalIcon from '@app/components/shared/LocalIcon';
|
|||||||
import { useViewer } from '@app/contexts/ViewerContext';
|
import { useViewer } from '@app/contexts/ViewerContext';
|
||||||
import { PdfBookmarkObject, PdfActionType } from '@embedpdf/models';
|
import { PdfBookmarkObject, PdfActionType } from '@embedpdf/models';
|
||||||
import BookmarksIcon from '@mui/icons-material/BookmarksRounded';
|
import BookmarksIcon from '@mui/icons-material/BookmarksRounded';
|
||||||
|
import '@app/components/viewer/SidebarBase.css';
|
||||||
import '@app/components/viewer/BookmarkSidebar.css';
|
import '@app/components/viewer/BookmarkSidebar.css';
|
||||||
|
|
||||||
interface BookmarkSidebarProps {
|
interface BookmarkSidebarProps {
|
||||||
@@ -429,7 +430,7 @@ export const BookmarkSidebar = ({ visible, thumbnailVisible, documentCacheKey, p
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="bookmark-sidebar"
|
className="sidebar-base bookmark-sidebar"
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
right: thumbnailVisible ? SIDEBAR_WIDTH : 0,
|
right: thumbnailVisible ? SIDEBAR_WIDTH : 0,
|
||||||
@@ -439,9 +440,9 @@ export const BookmarkSidebar = ({ visible, thumbnailVisible, documentCacheKey, p
|
|||||||
zIndex: 998,
|
zIndex: 998,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bookmark-sidebar__header">
|
<div className="sidebar-base__header bookmark-sidebar__header">
|
||||||
<div className="bookmark-sidebar__header-title">
|
<div className="sidebar-base__header-title bookmark-sidebar__header-title">
|
||||||
<span className="bookmark-sidebar__header-icon">
|
<span className="sidebar-base__header-icon bookmark-sidebar__header-icon">
|
||||||
<BookmarksIcon />
|
<BookmarksIcon />
|
||||||
</span>
|
</span>
|
||||||
<Text fw={600} size="sm" tt="uppercase" lts={0.5}>
|
<Text fw={600} size="sm" tt="uppercase" lts={0.5}>
|
||||||
@@ -475,7 +476,7 @@ export const BookmarkSidebar = ({ visible, thumbnailVisible, documentCacheKey, p
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Box px="sm" pb="sm" className="bookmark-sidebar__search">
|
<Box px="sm" pb="sm" className="sidebar-base__search bookmark-sidebar__search">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
placeholder="Search bookmarks"
|
placeholder="Search bookmarks"
|
||||||
@@ -486,9 +487,9 @@ export const BookmarkSidebar = ({ visible, thumbnailVisible, documentCacheKey, p
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<ScrollArea style={{ flex: 1 }}>
|
<ScrollArea style={{ flex: 1 }}>
|
||||||
<Box p="sm" className="bookmark-sidebar__content">
|
<Box p="sm" className="sidebar-base__content bookmark-sidebar__content">
|
||||||
{!bookmarkSupport && (
|
{!bookmarkSupport && (
|
||||||
<div className="bookmark-sidebar__empty-state">
|
<div className="sidebar-base__empty-state">
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
Bookmark support is unavailable for this viewer.
|
Bookmark support is unavailable for this viewer.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -496,7 +497,7 @@ export const BookmarkSidebar = ({ visible, thumbnailVisible, documentCacheKey, p
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{bookmarkSupport && showNoDocument && (
|
{bookmarkSupport && showNoDocument && (
|
||||||
<div className="bookmark-sidebar__empty-state">
|
<div className="sidebar-base__empty-state">
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
Open a PDF to view its bookmarks.
|
Open a PDF to view its bookmarks.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -504,7 +505,7 @@ export const BookmarkSidebar = ({ visible, thumbnailVisible, documentCacheKey, p
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{bookmarkSupport && documentCacheKey && currentError && (
|
{bookmarkSupport && documentCacheKey && currentError && (
|
||||||
<Stack gap="xs" align="center" className="bookmark-sidebar__error">
|
<Stack gap="xs" align="center" className="sidebar-base__error">
|
||||||
<Text size="sm" c="red" ta="center">
|
<Text size="sm" c="red" ta="center">
|
||||||
{currentError}
|
{currentError}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -515,7 +516,7 @@ export const BookmarkSidebar = ({ visible, thumbnailVisible, documentCacheKey, p
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{bookmarkSupport && documentCacheKey && isLocalLoading && (
|
{bookmarkSupport && documentCacheKey && isLocalLoading && (
|
||||||
<Stack gap="md" align="center" c="dimmed" py="xl" className="bookmark-sidebar__loading">
|
<Stack gap="md" align="center" c="dimmed" py="xl" className="sidebar-base__loading">
|
||||||
<Loader size="md" type="dots" />
|
<Loader size="md" type="dots" />
|
||||||
<Text size="sm" ta="center">
|
<Text size="sm" ta="center">
|
||||||
Loading bookmarks...
|
Loading bookmarks...
|
||||||
@@ -524,7 +525,7 @@ export const BookmarkSidebar = ({ visible, thumbnailVisible, documentCacheKey, p
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showEmptyState && (
|
{showEmptyState && (
|
||||||
<div className="bookmark-sidebar__empty-state">
|
<div className="sidebar-base__empty-state">
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
No bookmarks in this document
|
No bookmarks in this document
|
||||||
</Text>
|
</Text>
|
||||||
@@ -538,7 +539,7 @@ export const BookmarkSidebar = ({ visible, thumbnailVisible, documentCacheKey, p
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showSearchEmpty && (
|
{showSearchEmpty && (
|
||||||
<div className="bookmark-sidebar__empty-state">
|
<div className="sidebar-base__empty-state">
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
No bookmarks match your search
|
No bookmarks match your search
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { PdfViewerToolbar } from '@app/components/viewer/PdfViewerToolbar';
|
|||||||
import { ThumbnailSidebar } from '@app/components/viewer/ThumbnailSidebar';
|
import { ThumbnailSidebar } from '@app/components/viewer/ThumbnailSidebar';
|
||||||
import { BookmarkSidebar } from '@app/components/viewer/BookmarkSidebar';
|
import { BookmarkSidebar } from '@app/components/viewer/BookmarkSidebar';
|
||||||
import { AttachmentSidebar } from '@app/components/viewer/AttachmentSidebar';
|
import { AttachmentSidebar } from '@app/components/viewer/AttachmentSidebar';
|
||||||
|
import { LayerSidebar } from '@app/components/viewer/LayerSidebar';
|
||||||
import { useNavigationGuard, useNavigationState } from '@app/contexts/NavigationContext';
|
import { useNavigationGuard, useNavigationState } from '@app/contexts/NavigationContext';
|
||||||
import { useSignature } from '@app/contexts/SignatureContext';
|
import { useSignature } from '@app/contexts/SignatureContext';
|
||||||
import { useRedaction } from '@app/contexts/RedactionContext';
|
import { useRedaction } from '@app/contexts/RedactionContext';
|
||||||
@@ -125,6 +126,8 @@ const EmbedPdfViewerContent = ({
|
|||||||
toggleThumbnailSidebar,
|
toggleThumbnailSidebar,
|
||||||
isBookmarkSidebarVisible,
|
isBookmarkSidebarVisible,
|
||||||
isAttachmentSidebarVisible,
|
isAttachmentSidebarVisible,
|
||||||
|
isLayerSidebarVisible,
|
||||||
|
setHasLayers,
|
||||||
isCommentsSidebarVisible,
|
isCommentsSidebarVisible,
|
||||||
isSearchInterfaceVisible,
|
isSearchInterfaceVisible,
|
||||||
searchInterfaceActions,
|
searchInterfaceActions,
|
||||||
@@ -700,6 +703,46 @@ const EmbedPdfViewerContent = ({
|
|||||||
return () => window.removeEventListener('formfill:apply', handler);
|
return () => window.removeEventListener('formfill:apply', handler);
|
||||||
}, [handleFormApply]);
|
}, [handleFormApply]);
|
||||||
|
|
||||||
|
// Apply layer visibility changes - reload the modified PDF into the viewer
|
||||||
|
const layerApplyInProgressRef = useRef(false);
|
||||||
|
const handleLayerApply = useCallback(async (modifiedBlob: Blob) => {
|
||||||
|
if (layerApplyInProgressRef.current) return;
|
||||||
|
if (!currentFile || activeFileIds.length === 0) return;
|
||||||
|
|
||||||
|
layerApplyInProgressRef.current = true;
|
||||||
|
try {
|
||||||
|
const pageToRestore = lastKnownScrollPageRef.current;
|
||||||
|
const currentRotation = rotationState.rotation ?? 0;
|
||||||
|
|
||||||
|
const filename = currentFile.name || 'document.pdf';
|
||||||
|
const file = new File([modifiedBlob], filename, { type: 'application/pdf' });
|
||||||
|
|
||||||
|
const currentFileId = activeFiles[activeFileIndex]?.fileId;
|
||||||
|
if (!currentFileId) throw new Error('Current file ID not found');
|
||||||
|
|
||||||
|
const parentStub = selectors.getStirlingFileStub(currentFileId);
|
||||||
|
if (!parentStub) throw new Error('Parent stub not found');
|
||||||
|
|
||||||
|
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, 'multiTool');
|
||||||
|
|
||||||
|
pendingScrollRestoreRef.current = pageToRestore;
|
||||||
|
scrollRestoreAttemptsRef.current = 0;
|
||||||
|
pendingRotationRestoreRef.current = currentRotation;
|
||||||
|
rotationRestoreAttemptsRef.current = 0;
|
||||||
|
|
||||||
|
const newFileId = stubs[0]?.id;
|
||||||
|
if (newFileId) {
|
||||||
|
pendingFileIdRef.current = newFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await actions.consumeFiles([currentFileId], stirlingFiles, stubs);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Viewer] Apply layer changes failed:', error);
|
||||||
|
} finally {
|
||||||
|
layerApplyInProgressRef.current = false;
|
||||||
|
}
|
||||||
|
}, [currentFile, activeFiles, activeFileIndex, actions, selectors, activeFileIds.length, rotationState.rotation]);
|
||||||
|
|
||||||
// Discard pending redactions but save already-applied ones
|
// Discard pending redactions but save already-applied ones
|
||||||
// This is called when user clicks "Discard & Leave" - we want to:
|
// This is called when user clicks "Discard & Leave" - we want to:
|
||||||
// 1. NOT commit pending redaction marks (they get discarded)
|
// 1. NOT commit pending redaction marks (they get discarded)
|
||||||
@@ -919,6 +962,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
(isThumbnailSidebarVisible ? sidebarWidthRem : 0) +
|
(isThumbnailSidebarVisible ? sidebarWidthRem : 0) +
|
||||||
(isBookmarkSidebarVisible ? sidebarWidthRem : 0) +
|
(isBookmarkSidebarVisible ? sidebarWidthRem : 0) +
|
||||||
(isAttachmentSidebarVisible ? sidebarWidthRem : 0) +
|
(isAttachmentSidebarVisible ? sidebarWidthRem : 0) +
|
||||||
|
(isLayerSidebarVisible ? sidebarWidthRem : 0) +
|
||||||
(isCommentsSidebarVisible ? commentsSidebarWidthRem : 0);
|
(isCommentsSidebarVisible ? commentsSidebarWidthRem : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -987,7 +1031,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
redactionTrackerRef={redactionTrackerRef as React.RefObject<RedactionPendingTrackerAPI>}
|
redactionTrackerRef={redactionTrackerRef as React.RefObject<RedactionPendingTrackerAPI>}
|
||||||
fileId={currentFileId}
|
fileId={currentFileId}
|
||||||
isCommentsSidebarVisible={isCommentsSidebarVisible}
|
isCommentsSidebarVisible={isCommentsSidebarVisible}
|
||||||
commentsSidebarRightOffset={`${(isThumbnailSidebarVisible ? sidebarWidthRem : 0) + (isBookmarkSidebarVisible ? sidebarWidthRem : 0) + (isAttachmentSidebarVisible ? sidebarWidthRem : 0)}rem`}
|
commentsSidebarRightOffset={`${(isThumbnailSidebarVisible ? sidebarWidthRem : 0) + (isBookmarkSidebarVisible ? sidebarWidthRem : 0) + (isAttachmentSidebarVisible ? sidebarWidthRem : 0) + (isLayerSidebarVisible ? sidebarWidthRem : 0)}rem`}
|
||||||
onSignatureAdded={() => {
|
onSignatureAdded={() => {
|
||||||
// Handle signature added - for debugging, enable console logs as needed
|
// Handle signature added - for debugging, enable console logs as needed
|
||||||
// Future: Handle signature completion
|
// Future: Handle signature completion
|
||||||
@@ -1057,6 +1101,18 @@ const EmbedPdfViewerContent = ({
|
|||||||
documentCacheKey={bookmarkCacheKey}
|
documentCacheKey={bookmarkCacheKey}
|
||||||
preloadCacheKeys={allBookmarkCacheKeys}
|
preloadCacheKeys={allBookmarkCacheKeys}
|
||||||
/>
|
/>
|
||||||
|
<LayerSidebar
|
||||||
|
visible={isLayerSidebarVisible}
|
||||||
|
rightOffset={
|
||||||
|
(isThumbnailSidebarVisible ? sidebarWidthRem : 0) +
|
||||||
|
(isBookmarkSidebarVisible ? sidebarWidthRem : 0) +
|
||||||
|
(isAttachmentSidebarVisible ? sidebarWidthRem : 0)
|
||||||
|
}
|
||||||
|
file={effectiveFile?.file ?? null}
|
||||||
|
documentCacheKey={bookmarkCacheKey}
|
||||||
|
onApplyLayers={handleLayerApply}
|
||||||
|
onLayersDetected={setHasLayers}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Navigation Warning Modal */}
|
{/* Navigation Warning Modal */}
|
||||||
{!previewFile && (
|
{!previewFile && (
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/* Layer Sidebar - Layer-specific styles (base styles in SidebarBase.css) */
|
||||||
|
|
||||||
|
/* Header actions (show/hide all buttons) */
|
||||||
|
.layer-sidebar__header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layer List */
|
||||||
|
.layer-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layer Items */
|
||||||
|
.layer-item-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.375rem 0.65rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-item:hover {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 6%, var(--bg-toolbar));
|
||||||
|
border-color: color-mix(in srgb, var(--text-primary) 14%, var(--border-subtle));
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-item:active {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 10%, var(--bg-toolbar));
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-item__expand-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-item__expand-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-item__expand-placeholder {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-item__label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-item--hidden .layer-item__label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-color: color-mix(in srgb, var(--text-muted) 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Children Container */
|
||||||
|
.layer-item__children {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
border-left: 2px solid color-mix(in srgb, var(--border-subtle) 60%, transparent);
|
||||||
|
margin-left: 0.875rem;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer with Apply button */
|
||||||
|
.layer-sidebar__footer {
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
background: var(--bg-toolbar);
|
||||||
|
border-top: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dirty indicator badge */
|
||||||
|
.layer-sidebar__dirty-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--mantine-color-orange-5);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
@keyframes layer-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-item__children {
|
||||||
|
animation: layer-fade-in 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility & Reduced Motion */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.layer-item,
|
||||||
|
.layer-item__children {
|
||||||
|
transition: none;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { Box, ScrollArea, Text, Checkbox, Stack, Loader, ActionIcon, Tooltip } from '@mantine/core';
|
||||||
|
import LayersIcon from '@mui/icons-material/Layers';
|
||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||||
|
import '@app/components/viewer/SidebarBase.css';
|
||||||
|
import '@app/components/viewer/LayerSidebar.css';
|
||||||
|
import { readPdfLayers, applyOCGVisibilityToPdf, collectLeafIds } from '@app/components/viewer/layerUtils';
|
||||||
|
|
||||||
|
import type { LayerInfo } from '@app/components/viewer/layerUtils';
|
||||||
|
export type { LayerInfo };
|
||||||
|
|
||||||
|
interface LayerSidebarProps {
|
||||||
|
visible: boolean;
|
||||||
|
/** Right offset in rem (how far from the right edge). */
|
||||||
|
rightOffset: number;
|
||||||
|
/** The current PDF file to read layers from. */
|
||||||
|
file?: Blob | null;
|
||||||
|
/** Stable key that changes when the document changes (used to avoid re-fetching). */
|
||||||
|
documentCacheKey?: string;
|
||||||
|
/** Called when the user applies layer visibility changes. Receives the modified PDF blob. */
|
||||||
|
onApplyLayers: (modifiedBlob: Blob) => Promise<void>;
|
||||||
|
/** Called when layer detection completes, reporting whether the PDF has layers. */
|
||||||
|
onLayersDetected?: (hasLayers: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIDEBAR_WIDTH = '15rem';
|
||||||
|
|
||||||
|
type LoadStatus = 'idle' | 'loading' | 'ready' | 'no-layers' | 'error';
|
||||||
|
|
||||||
|
export function LayerSidebar({
|
||||||
|
visible,
|
||||||
|
rightOffset,
|
||||||
|
file,
|
||||||
|
documentCacheKey,
|
||||||
|
onApplyLayers,
|
||||||
|
onLayersDetected,
|
||||||
|
}: LayerSidebarProps) {
|
||||||
|
const [layers, setLayers] = useState<LayerInfo[]>([]);
|
||||||
|
const [visibility, setVisibility] = useState<Record<string, boolean>>({});
|
||||||
|
const [status, setStatus] = useState<LoadStatus>('idle');
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
const [isApplying, setIsApplying] = useState(false);
|
||||||
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||||
|
const loadedKeyRef = useRef<string | null>(null);
|
||||||
|
// Track whether visibility was set by user interaction (not initial load)
|
||||||
|
const userChangedRef = useRef(false);
|
||||||
|
|
||||||
|
// Load layers when the document changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!file || !documentCacheKey) {
|
||||||
|
setStatus('idle');
|
||||||
|
setLayers([]);
|
||||||
|
setVisibility({});
|
||||||
|
loadedKeyRef.current = null;
|
||||||
|
userChangedRef.current = false;
|
||||||
|
onLayersDetected?.(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadedKeyRef.current === documentCacheKey) return;
|
||||||
|
|
||||||
|
setStatus('loading');
|
||||||
|
setLoadError(null);
|
||||||
|
userChangedRef.current = false;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
readPdfLayers(file)
|
||||||
|
.then(layerList => {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
if (layerList.length === 0) {
|
||||||
|
setStatus('no-layers');
|
||||||
|
setLayers([]);
|
||||||
|
setVisibility({});
|
||||||
|
loadedKeyRef.current = documentCacheKey;
|
||||||
|
onLayersDetected?.(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build visibility map from the layer defaults (all leaf IDs)
|
||||||
|
const visMap: Record<string, boolean> = {};
|
||||||
|
const populateVisibility = (items: LayerInfo[]) => {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
populateVisibility(item.children);
|
||||||
|
} else {
|
||||||
|
visMap[item.id] = item.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
populateVisibility(layerList);
|
||||||
|
|
||||||
|
setLayers(layerList);
|
||||||
|
setVisibility(visMap);
|
||||||
|
setStatus('ready');
|
||||||
|
loadedKeyRef.current = documentCacheKey;
|
||||||
|
onLayersDetected?.(true);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setStatus('error');
|
||||||
|
setLoadError(err instanceof Error ? err.message : 'Failed to read PDF layers');
|
||||||
|
onLayersDetected?.(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [file, documentCacheKey, onLayersDetected]);
|
||||||
|
|
||||||
|
// Reset when document changes
|
||||||
|
useEffect(() => {
|
||||||
|
setExpanded({});
|
||||||
|
userChangedRef.current = false;
|
||||||
|
}, [documentCacheKey]);
|
||||||
|
|
||||||
|
// Auto-apply: debounce visibility changes from user interaction
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userChangedRef.current || !file || isApplying || layers.length === 0) return;
|
||||||
|
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
setIsApplying(true);
|
||||||
|
try {
|
||||||
|
const nameVisibility: Record<string, boolean> = {};
|
||||||
|
const collectNames = (items: LayerInfo[]) => {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
collectNames(item.children);
|
||||||
|
} else {
|
||||||
|
nameVisibility[item.name] = visibility[item.id] ?? item.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
collectNames(layers);
|
||||||
|
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const modifiedBytes = await applyOCGVisibilityToPdf(arrayBuffer, nameVisibility);
|
||||||
|
const blob = new Blob([new Uint8Array(modifiedBytes)], { type: 'application/pdf' });
|
||||||
|
|
||||||
|
await onApplyLayers(blob);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[LayerSidebar] Failed to apply layer changes:', err);
|
||||||
|
} finally {
|
||||||
|
setIsApplying(false);
|
||||||
|
userChangedRef.current = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [visibility, file, layers, isApplying, onApplyLayers]);
|
||||||
|
|
||||||
|
const toggleLayerVisibility = useCallback((id: string) => {
|
||||||
|
userChangedRef.current = true;
|
||||||
|
setVisibility(prev => ({ ...prev, [id]: !prev[id] }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showAll = useCallback(() => {
|
||||||
|
userChangedRef.current = true;
|
||||||
|
setVisibility(prev => {
|
||||||
|
const updated: Record<string, boolean> = {};
|
||||||
|
for (const id of Object.keys(prev)) {
|
||||||
|
updated[id] = true;
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hideAll = useCallback(() => {
|
||||||
|
userChangedRef.current = true;
|
||||||
|
setVisibility(prev => {
|
||||||
|
const updated: Record<string, boolean> = {};
|
||||||
|
for (const id of Object.keys(prev)) {
|
||||||
|
updated[id] = false;
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleExpanded = useCallback((id: string) => {
|
||||||
|
setExpanded(prev => ({ ...prev, [id]: !(prev[id] ?? true) }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderLayer = (layer: LayerInfo & { depth: number }) => {
|
||||||
|
const hasChildren = Boolean(layer.children && layer.children.length > 0);
|
||||||
|
const isExpanded = expanded[layer.id] !== false; // default expanded
|
||||||
|
const isLeaf = !hasChildren;
|
||||||
|
|
||||||
|
const isVisible = isLeaf
|
||||||
|
? visibility[layer.id] ?? layer.visible
|
||||||
|
: collectLeafIds(layer.children ?? []).every(id => visibility[id] ?? true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={layer.id}
|
||||||
|
className="layer-item-wrapper"
|
||||||
|
style={{ marginLeft: layer.depth > 0 ? `${layer.depth * 0.875}rem` : '0' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`layer-item ${!isVisible ? 'layer-item--hidden' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (hasChildren) {
|
||||||
|
toggleExpanded(layer.id);
|
||||||
|
} else {
|
||||||
|
toggleLayerVisibility(layer.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (hasChildren) {
|
||||||
|
toggleExpanded(layer.id);
|
||||||
|
} else {
|
||||||
|
toggleLayerVisibility(layer.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasChildren ? (
|
||||||
|
<span
|
||||||
|
className="layer-item__expand-btn"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleExpanded(layer.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
||||||
|
<path d="M2 4l4 4 4-4z" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
||||||
|
<path d="M4 2l4 4-4 4z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="layer-item__expand-placeholder" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLeaf && (
|
||||||
|
<Checkbox
|
||||||
|
size="xs"
|
||||||
|
checked={visibility[layer.id] ?? layer.visible}
|
||||||
|
onChange={() => toggleLayerVisibility(layer.id)}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
label={layer.name}
|
||||||
|
position="left"
|
||||||
|
withinPortal
|
||||||
|
disabled={layer.name.length < 20}
|
||||||
|
>
|
||||||
|
<span className="layer-item__label">
|
||||||
|
{layer.name}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasChildren && isExpanded && (
|
||||||
|
<div className="layer-item__children">
|
||||||
|
{(layer.children ?? []).map(child => renderLayer({ ...child, depth: 0 }))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
const leafIds = collectLeafIds(layers);
|
||||||
|
const allVisible = leafIds.every(id => visibility[id] !== false);
|
||||||
|
const allHidden = leafIds.every(id => visibility[id] === false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className="sidebar-base layer-sidebar"
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
right: `${rightOffset}rem`,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: SIDEBAR_WIDTH,
|
||||||
|
zIndex: 998,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sidebar-base__header">
|
||||||
|
<div className="sidebar-base__header-title">
|
||||||
|
<span className="sidebar-base__header-icon">
|
||||||
|
<LayersIcon fontSize="small" />
|
||||||
|
</span>
|
||||||
|
<Text fw={600} size="sm" tt="uppercase" lts={0.5} style={{ flex: 1 }}>
|
||||||
|
Layers
|
||||||
|
</Text>
|
||||||
|
{isApplying && <Loader size="xs" type="dots" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status === 'ready' && leafIds.length > 0 && (
|
||||||
|
<div className="layer-sidebar__header-actions">
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
onClick={showAll}
|
||||||
|
disabled={allVisible || isApplying}
|
||||||
|
aria-label="Show all layers"
|
||||||
|
title="Show all"
|
||||||
|
>
|
||||||
|
<VisibilityIcon sx={{ fontSize: '1rem' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
onClick={hideAll}
|
||||||
|
disabled={allHidden || isApplying}
|
||||||
|
aria-label="Hide all layers"
|
||||||
|
title="Hide all"
|
||||||
|
>
|
||||||
|
<VisibilityOffIcon sx={{ fontSize: '1rem' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<ScrollArea style={{ flex: 1 }}>
|
||||||
|
<Box p="sm" className="sidebar-base__content">
|
||||||
|
{status === 'idle' && (
|
||||||
|
<div className="sidebar-base__empty-state">
|
||||||
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
|
Open a PDF to view its layers.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'loading' && (
|
||||||
|
<Stack gap="md" align="center" c="dimmed" py="xl" className="sidebar-base__loading">
|
||||||
|
<Loader size="md" type="dots" />
|
||||||
|
<Text size="sm" ta="center">
|
||||||
|
Loading layers...
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'error' && (
|
||||||
|
<div className="sidebar-base__error">
|
||||||
|
<Text size="sm" c="red" ta="center">
|
||||||
|
{loadError ?? 'Failed to load layers.'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'no-layers' && (
|
||||||
|
<div className="sidebar-base__empty-state">
|
||||||
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
|
This document has no layers.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'ready' && layers.length > 0 && (
|
||||||
|
<div className="layer-list">
|
||||||
|
{layers.map(layer => renderLayer({ ...layer, depth: 0 }))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/* Shared Sidebar Base - Common styles for all viewer sidebars */
|
||||||
|
|
||||||
|
/* Container */
|
||||||
|
.sidebar-base {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
color-mix(in srgb, var(--bg-toolbar) 96%, transparent),
|
||||||
|
color-mix(in srgb, var(--bg-background) 90%, transparent)
|
||||||
|
);
|
||||||
|
border-left: 1px solid color-mix(in srgb, var(--border-subtle) 75%, transparent);
|
||||||
|
box-shadow: -2px 0 16px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.35)) 20%, transparent);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Section */
|
||||||
|
.sidebar-base__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 0.875rem;
|
||||||
|
background: var(--bg-toolbar);
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-base__header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-base__header-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--mantine-color-blue-6);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Section (used by bookmark & attachment sidebars) */
|
||||||
|
.sidebar-base__search {
|
||||||
|
background: var(--tool-panel-search-bg, var(--bg-toolbar));
|
||||||
|
border-bottom: 1px solid var(--tool-panel-search-border-bottom, var(--border-subtle));
|
||||||
|
padding-top: 0.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Section */
|
||||||
|
.sidebar-base__content {
|
||||||
|
padding-top: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-base__empty-state,
|
||||||
|
.sidebar-base__error {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-base__loading {
|
||||||
|
padding: 2rem 1rem !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
export interface LayerInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
visible: boolean;
|
||||||
|
children?: LayerInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads OCG layer info from a PDF file using pdfjs-dist.
|
||||||
|
* Returns a flat list of all OCG groups with their names and default visibility.
|
||||||
|
*/
|
||||||
|
export async function readPdfLayers(file: Blob): Promise<LayerInfo[]> {
|
||||||
|
const { getDocument, GlobalWorkerOptions } = await import('pdfjs-dist/legacy/build/pdf.mjs');
|
||||||
|
|
||||||
|
if (!GlobalWorkerOptions.workerSrc) {
|
||||||
|
GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const loadingTask = getDocument({ data: arrayBuffer, verbosity: 0 });
|
||||||
|
const pdfDoc = await loadingTask.promise;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ocConfig = await pdfDoc.getOptionalContentConfig();
|
||||||
|
|
||||||
|
if (!ocConfig) return [];
|
||||||
|
|
||||||
|
// pdfjs v5 uses [Symbol.iterator] and getGroup(id), not getGroups()
|
||||||
|
const groups: Record<string, any> = {};
|
||||||
|
for (const [id, group] of ocConfig as any) {
|
||||||
|
groups[id] = group;
|
||||||
|
}
|
||||||
|
if (Object.keys(groups).length === 0) return [];
|
||||||
|
|
||||||
|
// Use getOrder() for hierarchical display
|
||||||
|
let order: any[] | null = null;
|
||||||
|
try {
|
||||||
|
order = ocConfig.getOrder?.() ?? null;
|
||||||
|
} catch {
|
||||||
|
// getOrder not available
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order && Array.isArray(order) && order.length > 0) {
|
||||||
|
return buildLayerTree(order, groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: flat list in enumeration order
|
||||||
|
return Object.entries(groups).map(([id, group]) => ({
|
||||||
|
id,
|
||||||
|
name: (group as any).name ?? id,
|
||||||
|
visible: (group as any).visible ?? true,
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
await pdfDoc.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively builds a LayerInfo tree from pdfjs OCG order array.
|
||||||
|
* The order array can contain:
|
||||||
|
* - string: an OCG id
|
||||||
|
* - { name: string, order: any[] }: a named group with children
|
||||||
|
* - array: a nested group
|
||||||
|
*/
|
||||||
|
function buildLayerTree(
|
||||||
|
order: any[],
|
||||||
|
groups: Record<string, any>,
|
||||||
|
visited = new Set<string>()
|
||||||
|
): LayerInfo[] {
|
||||||
|
const result: LayerInfo[] = [];
|
||||||
|
|
||||||
|
for (const item of order) {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
// It's an OCG id
|
||||||
|
if (visited.has(item)) continue;
|
||||||
|
visited.add(item);
|
||||||
|
const group = groups[item];
|
||||||
|
if (group) {
|
||||||
|
result.push({
|
||||||
|
id: item,
|
||||||
|
name: (group as any).name ?? item,
|
||||||
|
visible: (group as any).visible ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(item)) {
|
||||||
|
// Nested group (unlabeled)
|
||||||
|
const children = buildLayerTree(item, groups, visited);
|
||||||
|
result.push(...children);
|
||||||
|
} else if (item && typeof item === 'object') {
|
||||||
|
// Named group with nested items
|
||||||
|
const { name, order: subOrder } = item as { name?: string; order?: any[] };
|
||||||
|
const children = subOrder ? buildLayerTree(subOrder, groups, visited) : [];
|
||||||
|
if (name && children.length > 0) {
|
||||||
|
// Use the first child's id as a synthetic group id
|
||||||
|
result.push({
|
||||||
|
id: `group-${name}`,
|
||||||
|
name: name,
|
||||||
|
visible: children.every(c => c.visible),
|
||||||
|
children,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.push(...children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies OCG visibility in a PDF using @cantoo/pdf-lib.
|
||||||
|
* Accepts a flat map of { layerName -> visible } and rewrites the /D config.
|
||||||
|
*
|
||||||
|
* Strategy: set /BaseState to /OFF and only list visible layers in /ON.
|
||||||
|
* This is the most unambiguous approach and avoids conflicts between
|
||||||
|
* /BaseState, /ON, and /OFF that can confuse some viewers.
|
||||||
|
* Also removes /AS (auto-state) entries that can override visibility.
|
||||||
|
*/
|
||||||
|
export async function applyOCGVisibilityToPdf(
|
||||||
|
pdfBytes: ArrayBuffer,
|
||||||
|
layerVisibility: Record<string, boolean>
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const { PDFDocument, PDFDict, PDFName, PDFArray, PDFString, PDFHexString } =
|
||||||
|
await import('@cantoo/pdf-lib');
|
||||||
|
|
||||||
|
const doc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
|
||||||
|
const context = doc.context;
|
||||||
|
|
||||||
|
// Access the catalog via the trailer's Root reference
|
||||||
|
const catalogRef = context.trailerInfo.Root;
|
||||||
|
const catalog = context.lookup(catalogRef) as unknown as typeof PDFDict.prototype;
|
||||||
|
|
||||||
|
// Get OCProperties dict (may be a direct dict or an indirect reference)
|
||||||
|
const ocPropsRaw = (catalog as any).lookup(PDFName.of('OCProperties'));
|
||||||
|
if (!ocPropsRaw) {
|
||||||
|
return doc.save();
|
||||||
|
}
|
||||||
|
const ocProps = (ocPropsRaw instanceof PDFDict
|
||||||
|
? ocPropsRaw
|
||||||
|
: context.lookup(ocPropsRaw)) as unknown as typeof PDFDict.prototype;
|
||||||
|
|
||||||
|
// Get the /OCGs array
|
||||||
|
const ocgsRaw = (ocProps as any).lookup(PDFName.of('OCGs'));
|
||||||
|
if (!(ocgsRaw instanceof PDFArray)) {
|
||||||
|
return doc.save();
|
||||||
|
}
|
||||||
|
const ocgsArray = ocgsRaw as unknown as typeof PDFArray.prototype;
|
||||||
|
|
||||||
|
// Get or create the /D (default config) dict
|
||||||
|
const dRaw = (ocProps as any).lookup(PDFName.of('D'));
|
||||||
|
if (!dRaw) {
|
||||||
|
return doc.save();
|
||||||
|
}
|
||||||
|
const dDict = (dRaw instanceof PDFDict
|
||||||
|
? dRaw
|
||||||
|
: context.lookup(dRaw)) as unknown as typeof PDFDict.prototype;
|
||||||
|
|
||||||
|
// Collect OCG refs for ON vs OFF based on user visibility settings
|
||||||
|
const onRefs: any[] = [];
|
||||||
|
const offRefs: any[] = [];
|
||||||
|
|
||||||
|
const size = (ocgsArray as any).size() as number;
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
const ocgRef = (ocgsArray as any).get(i);
|
||||||
|
const ocgDict = context.lookup(ocgRef) as unknown as typeof PDFDict.prototype;
|
||||||
|
if (!ocgDict) continue;
|
||||||
|
|
||||||
|
// Get the OCG name
|
||||||
|
const nameRaw = (ocgDict as any).lookup(PDFName.of('Name'));
|
||||||
|
let ocgName = '';
|
||||||
|
if (nameRaw instanceof PDFString || nameRaw instanceof PDFHexString) {
|
||||||
|
ocgName = (nameRaw as any).decodeText?.() ?? (nameRaw as any).asString?.() ?? '';
|
||||||
|
} else if (nameRaw) {
|
||||||
|
ocgName = String(nameRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up visibility by name
|
||||||
|
const shouldBeVisible = layerVisibility[ocgName] ?? true;
|
||||||
|
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
onRefs.push(ocgRef);
|
||||||
|
} else {
|
||||||
|
offRefs.push(ocgRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set /BaseState to /OFF so all layers start hidden, then /ON lists visible ones.
|
||||||
|
// This is unambiguous and avoids conflicts between /BaseState and /ON//OFF.
|
||||||
|
(dDict as any).set(PDFName.of('BaseState'), PDFName.of('OFF'));
|
||||||
|
|
||||||
|
// Set /ON to only the visible layers
|
||||||
|
if (onRefs.length > 0) {
|
||||||
|
(dDict as any).set(PDFName.of('ON'), context.obj(onRefs));
|
||||||
|
} else {
|
||||||
|
(dDict as any).delete?.(PDFName.of('ON'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set /OFF to only the hidden layers (for viewers that check it)
|
||||||
|
if (offRefs.length > 0) {
|
||||||
|
(dDict as any).set(PDFName.of('OFF'), context.obj(offRefs));
|
||||||
|
} else {
|
||||||
|
(dDict as any).delete?.(PDFName.of('OFF'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove /AS (auto-state) array — it can contain usage-based overrides
|
||||||
|
// (e.g., print vs view) that conflict with our explicit visibility settings.
|
||||||
|
(dDict as any).delete?.(PDFName.of('AS'));
|
||||||
|
|
||||||
|
return doc.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects all leaf-level layer IDs (those that are actual OCGs, not synthetic groups).
|
||||||
|
*/
|
||||||
|
export function collectLeafIds(layers: LayerInfo[]): string[] {
|
||||||
|
const ids: string[] = [];
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (layer.children && layer.children.length > 0) {
|
||||||
|
ids.push(...collectLeafIds(layer.children));
|
||||||
|
} else {
|
||||||
|
ids.push(layer.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import { BASE_PATH, withBasePath } from '@app/constants/app';
|
|||||||
import { useRedaction, useRedactionMode } from '@app/contexts/RedactionContext';
|
import { useRedaction, useRedactionMode } from '@app/contexts/RedactionContext';
|
||||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||||
import StraightenIcon from '@mui/icons-material/Straighten';
|
import StraightenIcon from '@mui/icons-material/Straighten';
|
||||||
|
import LayersIcon from '@mui/icons-material/Layers';
|
||||||
import VolumeUpIcon from '@mui/icons-material/VolumeUp';
|
import VolumeUpIcon from '@mui/icons-material/VolumeUp';
|
||||||
import StopIcon from '@mui/icons-material/Stop';
|
import StopIcon from '@mui/icons-material/Stop';
|
||||||
import { useViewerReadAloud } from '@app/components/viewer/useViewerReadAloud';
|
import { useViewerReadAloud } from '@app/components/viewer/useViewerReadAloud';
|
||||||
@@ -26,7 +27,7 @@ export function useViewerRightRailButtons(
|
|||||||
) {
|
) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const viewer = useViewer();
|
const viewer = useViewer();
|
||||||
const { isThumbnailSidebarVisible, isBookmarkSidebarVisible, isAttachmentSidebarVisible, isCommentsSidebarVisible, toggleCommentsSidebar, isSearchInterfaceVisible, registerImmediatePanUpdate } = viewer;
|
const { isThumbnailSidebarVisible, isBookmarkSidebarVisible, isAttachmentSidebarVisible, isLayerSidebarVisible, hasLayers, isCommentsSidebarVisible, toggleCommentsSidebar, isSearchInterfaceVisible, registerImmediatePanUpdate } = viewer;
|
||||||
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
|
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
|
||||||
const { sidebarRefs } = useSidebarContext();
|
const { sidebarRefs } = useSidebarContext();
|
||||||
const { position: tooltipPosition } = useRightRailTooltipSide(sidebarRefs, 12);
|
const { position: tooltipPosition } = useRightRailTooltipSide(sidebarRefs, 12);
|
||||||
@@ -83,6 +84,7 @@ export function useViewerRightRailButtons(
|
|||||||
const sidebarLabel = t('rightRail.toggleSidebar', 'Toggle Sidebar');
|
const sidebarLabel = t('rightRail.toggleSidebar', 'Toggle Sidebar');
|
||||||
const bookmarkLabel = t('rightRail.toggleBookmarks', 'Toggle Bookmarks');
|
const bookmarkLabel = t('rightRail.toggleBookmarks', 'Toggle Bookmarks');
|
||||||
const attachmentLabel = t('rightRail.toggleAttachments', 'Toggle Attachments');
|
const attachmentLabel = t('rightRail.toggleAttachments', 'Toggle Attachments');
|
||||||
|
const layersLabel = t('rightRail.toggleLayers', 'Toggle Layers');
|
||||||
const commentsLabel = t('rightRail.toggleComments', 'Comments');
|
const commentsLabel = t('rightRail.toggleComments', 'Comments');
|
||||||
const printLabel = t('rightRail.print', 'Print PDF');
|
const printLabel = t('rightRail.print', 'Print PDF');
|
||||||
const annotationsLabel = t('rightRail.annotations', 'Annotations');
|
const annotationsLabel = t('rightRail.annotations', 'Annotations');
|
||||||
@@ -236,6 +238,18 @@ export function useViewerRightRailButtons(
|
|||||||
viewer.toggleAttachmentSidebar();
|
viewer.toggleAttachmentSidebar();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
...(hasLayers ? [{
|
||||||
|
id: 'viewer-toggle-layers',
|
||||||
|
icon: <LayersIcon sx={{ fontSize: '1.5rem' }} />,
|
||||||
|
tooltip: layersLabel,
|
||||||
|
ariaLabel: layersLabel,
|
||||||
|
section: 'top' as const,
|
||||||
|
order: 56.3,
|
||||||
|
active: isLayerSidebarVisible,
|
||||||
|
onClick: () => {
|
||||||
|
viewer.toggleLayerSidebar();
|
||||||
|
}
|
||||||
|
}] : []),
|
||||||
{
|
{
|
||||||
id: 'viewer-toggle-comments',
|
id: 'viewer-toggle-comments',
|
||||||
icon: <LocalIcon icon="comment" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="comment" width="1.5rem" height="1.5rem" />,
|
||||||
@@ -424,6 +438,8 @@ export function useViewerRightRailButtons(
|
|||||||
isThumbnailSidebarVisible,
|
isThumbnailSidebarVisible,
|
||||||
isBookmarkSidebarVisible,
|
isBookmarkSidebarVisible,
|
||||||
isAttachmentSidebarVisible,
|
isAttachmentSidebarVisible,
|
||||||
|
isLayerSidebarVisible,
|
||||||
|
hasLayers,
|
||||||
isSearchInterfaceVisible,
|
isSearchInterfaceVisible,
|
||||||
isPanning,
|
isPanning,
|
||||||
searchLabel,
|
searchLabel,
|
||||||
@@ -434,6 +450,7 @@ export function useViewerRightRailButtons(
|
|||||||
sidebarLabel,
|
sidebarLabel,
|
||||||
bookmarkLabel,
|
bookmarkLabel,
|
||||||
attachmentLabel,
|
attachmentLabel,
|
||||||
|
layersLabel,
|
||||||
printLabel,
|
printLabel,
|
||||||
tooltipPosition,
|
tooltipPosition,
|
||||||
annotationsLabel,
|
annotationsLabel,
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ export interface ViewerContextType {
|
|||||||
toggleBookmarkSidebar: () => void;
|
toggleBookmarkSidebar: () => void;
|
||||||
isAttachmentSidebarVisible: boolean;
|
isAttachmentSidebarVisible: boolean;
|
||||||
toggleAttachmentSidebar: () => void;
|
toggleAttachmentSidebar: () => void;
|
||||||
|
isLayerSidebarVisible: boolean;
|
||||||
|
toggleLayerSidebar: () => void;
|
||||||
|
hasLayers: boolean;
|
||||||
|
setHasLayers: (value: boolean) => void;
|
||||||
isCommentsSidebarVisible: boolean;
|
isCommentsSidebarVisible: boolean;
|
||||||
setCommentsSidebarVisible: (visible: boolean) => void;
|
setCommentsSidebarVisible: (visible: boolean) => void;
|
||||||
toggleCommentsSidebar: () => void;
|
toggleCommentsSidebar: () => void;
|
||||||
@@ -188,6 +192,8 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
||||||
const [isBookmarkSidebarVisible, setIsBookmarkSidebarVisible] = useState(false);
|
const [isBookmarkSidebarVisible, setIsBookmarkSidebarVisible] = useState(false);
|
||||||
const [isAttachmentSidebarVisible, setIsAttachmentSidebarVisible] = useState(false);
|
const [isAttachmentSidebarVisible, setIsAttachmentSidebarVisible] = useState(false);
|
||||||
|
const [isLayerSidebarVisible, setIsLayerSidebarVisible] = useState(false);
|
||||||
|
const [hasLayers, setHasLayers] = useState(false);
|
||||||
const [isCommentsSidebarVisible, setIsCommentsSidebarVisible] = useState(false);
|
const [isCommentsSidebarVisible, setIsCommentsSidebarVisible] = useState(false);
|
||||||
const [highlightCommentRequest, setHighlightCommentRequest] = useState<{
|
const [highlightCommentRequest, setHighlightCommentRequest] = useState<{
|
||||||
documentId: string;
|
documentId: string;
|
||||||
@@ -289,6 +295,10 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
setIsAttachmentSidebarVisible(prev => !prev);
|
setIsAttachmentSidebarVisible(prev => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleLayerSidebar = () => {
|
||||||
|
setIsLayerSidebarVisible(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
const setCommentsSidebarVisible = (visible: boolean) => {
|
const setCommentsSidebarVisible = (visible: boolean) => {
|
||||||
setIsCommentsSidebarVisible(visible);
|
setIsCommentsSidebarVisible(visible);
|
||||||
};
|
};
|
||||||
@@ -454,6 +464,10 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
toggleBookmarkSidebar,
|
toggleBookmarkSidebar,
|
||||||
isAttachmentSidebarVisible,
|
isAttachmentSidebarVisible,
|
||||||
toggleAttachmentSidebar,
|
toggleAttachmentSidebar,
|
||||||
|
isLayerSidebarVisible,
|
||||||
|
toggleLayerSidebar,
|
||||||
|
hasLayers,
|
||||||
|
setHasLayers,
|
||||||
isCommentsSidebarVisible,
|
isCommentsSidebarVisible,
|
||||||
setCommentsSidebarVisible,
|
setCommentsSidebarVisible,
|
||||||
toggleCommentsSidebar,
|
toggleCommentsSidebar,
|
||||||
|
|||||||
Reference in New Issue
Block a user