improvements

This commit is contained in:
Reece Browne 2025-09-19 10:48:29 +01:00
parent dc71b3007b
commit b574cef54a
7 changed files with 192 additions and 86 deletions

View File

@ -18,8 +18,7 @@ import { ViewerContext } from '../../contexts/ViewerContext';
export default function RightRail() { export default function RightRail() {
const { t } = useTranslation(); const { t } = useTranslation();
const [isPanning, setIsPanning] = useState(false); const [isPanning, setIsPanning] = useState(false);
const [_currentRotation, setCurrentRotation] = useState(0);
// Viewer context for PDF controls - safely handle when not available // Viewer context for PDF controls - safely handle when not available
const viewerContext = React.useContext(ViewerContext); const viewerContext = React.useContext(ViewerContext);
const { toggleTheme } = useRainbowThemeContext(); const { toggleTheme } = useRainbowThemeContext();
@ -35,14 +34,6 @@ export default function RightRail() {
// Navigation view // Navigation view
const { workbench: currentView } = useNavigationState(); const { workbench: currentView } = useNavigationState();
// Update rotation display when switching to viewer mode
useEffect(() => {
if (currentView === 'viewer' && viewerContext) {
const rotationState = viewerContext.getRotationState();
setCurrentRotation((rotationState?.rotation ?? 0) * 90);
}
}, [currentView, viewerContext]);
// File state and selection // File state and selection
const { state, selectors } = useFileState(); const { state, selectors } = useFileState();
const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection(); const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection();
@ -244,9 +235,9 @@ export default function RightRail() {
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
<div style={{ minWidth: '20rem' }}> <div style={{ minWidth: '20rem' }}>
<SearchInterface <SearchInterface
visible={true} visible={true}
onClose={() => {}} onClose={() => {}}
/> />
</div> </div>
</Popover.Dropdown> </Popover.Dropdown>

View File

@ -26,8 +26,8 @@ interface SearchResultState {
export function CustomSearchLayer({ export function CustomSearchLayer({
pageIndex, pageIndex,
scale, scale,
highlightColor = '#FFFF00', highlightColor = 'var(--search-highlight-bg)',
activeHighlightColor = '#FFBF00', activeHighlightColor = 'var(--search-highlight-active-bg)',
opacity = 0.6, opacity = 0.6,
padding = 2, padding = 2,
borderRadius = 4 borderRadius = 4
@ -107,7 +107,7 @@ export function CustomSearchLayer({
transition: 'opacity 0.3s ease-in-out, background-color 0.2s ease-in-out', transition: 'opacity 0.3s ease-in-out, background-color 0.2s ease-in-out',
pointerEvents: 'none', pointerEvents: 'none',
boxShadow: originalIndex === searchResultState?.activeResultIndex boxShadow: originalIndex === searchResultState?.activeResultIndex
? '0 0 0 1px rgba(255, 191, 0, 0.8)' ? '0 0 0 1px var(--search-highlight-active-border)'
: 'none' : 'none'
}} }}
/> />

View File

@ -28,7 +28,7 @@ const EmbedPdfViewerContent = ({
const viewerRef = React.useRef<HTMLDivElement>(null); const viewerRef = React.useRef<HTMLDivElement>(null);
const [isViewerHovered, setIsViewerHovered] = React.useState(false); const [isViewerHovered, setIsViewerHovered] = React.useState(false);
const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState } = useViewer(); const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState } = useViewer();
const scrollState = getScrollState(); const scrollState = getScrollState();
const zoomState = getZoomState(); const zoomState = getZoomState();
const spreadState = getSpreadState(); const spreadState = getSpreadState();
@ -64,21 +64,27 @@ const EmbedPdfViewerContent = ({
} }
}, [previewFile, fileWithUrl]); }, [previewFile, fileWithUrl]);
// Handle scroll wheel zoom // Handle scroll wheel zoom with accumulator for smooth trackpad pinch
React.useEffect(() => { React.useEffect(() => {
let accumulator = 0;
const handleWheel = (event: WheelEvent) => { const handleWheel = (event: WheelEvent) => {
// Check if Ctrl (Windows/Linux) or Cmd (Mac) is pressed // Check if Ctrl (Windows/Linux) or Cmd (Mac) is pressed
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (event.deltaY < 0) { accumulator += event.deltaY;
// Scroll up - zoom in const threshold = 10;
zoomActions.zoomIn();
} else {
// Scroll down - zoom out
zoomActions.zoomOut();
if (accumulator <= -threshold) {
// Accumulated scroll up - zoom in
zoomActions.zoomIn();
accumulator = 0;
} else if (accumulator >= threshold) {
// Accumulated scroll down - zoom out
zoomActions.zoomOut();
accumulator = 0;
} }
} }
}; };
@ -90,7 +96,7 @@ const EmbedPdfViewerContent = ({
viewerElement.removeEventListener('wheel', handleWheel); viewerElement.removeEventListener('wheel', handleWheel);
}; };
} }
}, []); }, [zoomActions]);
// Handle keyboard zoom shortcuts // Handle keyboard zoom shortcuts
React.useEffect(() => { React.useEffect(() => {

View File

@ -2,6 +2,14 @@ import { useEffect, useState } from 'react';
import { useSearch } from '@embedpdf/plugin-search/react'; import { useSearch } from '@embedpdf/plugin-search/react';
import { useViewer } from '../../contexts/ViewerContext'; import { useViewer } from '../../contexts/ViewerContext';
interface SearchResult {
pageIndex: number;
rects: Array<{
origin: { x: number; y: number };
size: { width: number; height: number };
}>;
}
/** /**
* SearchAPIBridge manages search state and provides search functionality. * SearchAPIBridge manages search state and provides search functionality.
* Listens for search result changes from EmbedPDF and maintains local state. * Listens for search result changes from EmbedPDF and maintains local state.
@ -11,7 +19,7 @@ export function SearchAPIBridge() {
const { registerBridge } = useViewer(); const { registerBridge } = useViewer();
const [localState, setLocalState] = useState({ const [localState, setLocalState] = useState({
results: null as any[] | null, results: null as SearchResult[] | null,
activeIndex: 0 activeIndex: 0
}); });

View File

@ -15,12 +15,18 @@ export function ThumbnailSidebar({ visible, onToggle: _onToggle }: ThumbnailSide
const scrollState = getScrollState(); const scrollState = getScrollState();
const thumbnailAPI = getThumbnailAPI(); const thumbnailAPI = getThumbnailAPI();
// Clear thumbnails when sidebar closes // Clear thumbnails when sidebar closes and revoke blob URLs to prevent memory leaks
useEffect(() => { useEffect(() => {
if (!visible) { if (!visible) {
Object.values(thumbnails).forEach((thumbUrl) => {
// Only revoke if it's a blob URL (not 'error')
if (typeof thumbUrl === 'string' && thumbUrl.startsWith('blob:')) {
URL.revokeObjectURL(thumbUrl);
}
});
setThumbnails({}); setThumbnails({});
} }
}, [visible]); }, [visible, thumbnails]);
// Generate thumbnails when sidebar becomes visible // Generate thumbnails when sidebar becomes visible
useEffect(() => { useEffect(() => {
@ -32,7 +38,7 @@ export function ThumbnailSidebar({ visible, onToggle: _onToggle }: ThumbnailSide
if (thumbnails[pageIndex]) continue; // Skip if already generated if (thumbnails[pageIndex]) continue; // Skip if already generated
try { try {
const thumbTask = (thumbnailAPI as any).renderThumb(pageIndex, 1.0); const thumbTask = thumbnailAPI.renderThumb(pageIndex, 1.0);
// Convert Task to Promise and handle properly // Convert Task to Promise and handle properly
thumbTask.toPromise().then((thumbBlob: Blob) => { thumbTask.toPromise().then((thumbBlob: Blob) => {

View File

@ -1,6 +1,57 @@
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react'; import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
import { SpreadMode } from '@embedpdf/plugin-spread/react'; import { SpreadMode } from '@embedpdf/plugin-spread/react';
// Bridge API interfaces - these match what the bridges provide
interface ScrollAPIWrapper {
scrollToPage: (params: { pageNumber: number }) => void;
scrollToPreviousPage: () => void;
scrollToNextPage: () => void;
}
interface ZoomAPIWrapper {
zoomIn: () => void;
zoomOut: () => void;
toggleMarqueeZoom: () => void;
requestZoom: (level: number) => void;
}
interface PanAPIWrapper {
enable: () => void;
disable: () => void;
toggle: () => void;
}
interface SelectionAPIWrapper {
copyToClipboard: () => void;
getSelectedText: () => string | any;
getFormattedSelection: () => any;
}
interface SpreadAPIWrapper {
setSpreadMode: (mode: SpreadMode) => void;
getSpreadMode: () => SpreadMode | null;
toggleSpreadMode: () => void;
}
interface RotationAPIWrapper {
rotateForward: () => void;
rotateBackward: () => void;
setRotation: (rotation: number) => void;
getRotation: () => number;
}
interface SearchAPIWrapper {
search: (query: string) => Promise<any>;
clear: () => void;
next: () => void;
previous: () => void;
}
interface ThumbnailAPIWrapper {
renderThumb: (pageIndex: number, scale: number) => { toPromise: () => Promise<Blob> };
}
// State interfaces - represent the shape of data from each bridge // State interfaces - represent the shape of data from each bridge
interface ScrollState { interface ScrollState {
currentPage: number; currentPage: number;
@ -29,8 +80,16 @@ interface RotationState {
rotation: number; rotation: number;
} }
interface SearchResult {
pageIndex: number;
rects: Array<{
origin: { x: number; y: number };
size: { width: number; height: number };
}>;
}
interface SearchState { interface SearchState {
results: any[] | null; results: SearchResult[] | null;
activeIndex: number; activeIndex: number;
} }
@ -42,7 +101,7 @@ interface BridgeRef<TState = unknown, TApi = unknown> {
/** /**
* ViewerContext provides a unified interface to EmbedPDF functionality. * ViewerContext provides a unified interface to EmbedPDF functionality.
* *
* Architecture: * Architecture:
* - Bridges store their own state locally and register with this context * - Bridges store their own state locally and register with this context
* - Context provides read-only access to bridge state via getter functions * - Context provides read-only access to bridge state via getter functions
@ -53,7 +112,7 @@ interface ViewerContextType {
// UI state managed by this context // UI state managed by this context
isThumbnailSidebarVisible: boolean; isThumbnailSidebarVisible: boolean;
toggleThumbnailSidebar: () => void; toggleThumbnailSidebar: () => void;
// State getters - read current state from bridges // State getters - read current state from bridges
getScrollState: () => ScrollState; getScrollState: () => ScrollState;
getZoomState: () => ZoomState; getZoomState: () => ZoomState;
@ -62,16 +121,16 @@ interface ViewerContextType {
getSpreadState: () => SpreadState; getSpreadState: () => SpreadState;
getRotationState: () => RotationState; getRotationState: () => RotationState;
getSearchState: () => SearchState; getSearchState: () => SearchState;
getThumbnailAPI: () => unknown; getThumbnailAPI: () => ThumbnailAPIWrapper | null;
// Immediate update callbacks // Immediate update callbacks
registerImmediateZoomUpdate: (callback: (percent: number) => void) => void; registerImmediateZoomUpdate: (callback: (percent: number) => void) => void;
registerImmediateScrollUpdate: (callback: (currentPage: number, totalPages: number) => void) => void; registerImmediateScrollUpdate: (callback: (currentPage: number, totalPages: number) => void) => void;
// Internal - for bridges to trigger immediate updates // Internal - for bridges to trigger immediate updates
triggerImmediateScrollUpdate: (currentPage: number, totalPages: number) => void; triggerImmediateScrollUpdate: (currentPage: number, totalPages: number) => void;
triggerImmediateZoomUpdate: (zoomPercent: number) => void; triggerImmediateZoomUpdate: (zoomPercent: number) => void;
// Action handlers - call EmbedPDF APIs directly // Action handlers - call EmbedPDF APIs directly
scrollActions: { scrollActions: {
scrollToPage: (page: number) => void; scrollToPage: (page: number) => void;
@ -80,39 +139,39 @@ interface ViewerContextType {
scrollToNextPage: () => void; scrollToNextPage: () => void;
scrollToLastPage: () => void; scrollToLastPage: () => void;
}; };
zoomActions: { zoomActions: {
zoomIn: () => void; zoomIn: () => void;
zoomOut: () => void; zoomOut: () => void;
toggleMarqueeZoom: () => void; toggleMarqueeZoom: () => void;
requestZoom: (level: number) => void; requestZoom: (level: number) => void;
}; };
panActions: { panActions: {
enablePan: () => void; enablePan: () => void;
disablePan: () => void; disablePan: () => void;
togglePan: () => void; togglePan: () => void;
}; };
selectionActions: { selectionActions: {
copyToClipboard: () => void; copyToClipboard: () => void;
getSelectedText: () => string; getSelectedText: () => string;
getFormattedSelection: () => unknown; getFormattedSelection: () => unknown;
}; };
spreadActions: { spreadActions: {
setSpreadMode: (mode: SpreadMode) => void; setSpreadMode: (mode: SpreadMode) => void;
getSpreadMode: () => SpreadMode; getSpreadMode: () => SpreadMode | null;
toggleSpreadMode: () => void; toggleSpreadMode: () => void;
}; };
rotationActions: { rotationActions: {
rotateForward: () => void; rotateForward: () => void;
rotateBackward: () => void; rotateBackward: () => void;
setRotation: (rotation: number) => void; setRotation: (rotation: number) => void;
getRotation: () => number; getRotation: () => number;
}; };
searchActions: { searchActions: {
search: (query: string) => Promise<void>; search: (query: string) => Promise<void>;
next: () => void; next: () => void;
@ -120,7 +179,7 @@ interface ViewerContextType {
clear: () => void; clear: () => void;
}; };
// Bridge registration - internal use by bridges // Bridge registration - internal use by bridges
registerBridge: (type: string, ref: BridgeRef) => void; registerBridge: (type: string, ref: BridgeRef) => void;
} }
@ -133,27 +192,53 @@ interface ViewerProviderProps {
export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => { export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
// UI state - only state directly managed by this context // UI state - only state directly managed by this context
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false); const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
// Bridge registry - bridges register their state and APIs here // Bridge registry - bridges register their state and APIs here
const bridgeRefs = useRef({ const bridgeRefs = useRef({
scroll: null as BridgeRef<ScrollState> | null, scroll: null as BridgeRef<ScrollState, ScrollAPIWrapper> | null,
zoom: null as BridgeRef<ZoomState> | null, zoom: null as BridgeRef<ZoomState, ZoomAPIWrapper> | null,
pan: null as BridgeRef<PanState> | null, pan: null as BridgeRef<PanState, PanAPIWrapper> | null,
selection: null as BridgeRef<SelectionState> | null, selection: null as BridgeRef<SelectionState, SelectionAPIWrapper> | null,
search: null as BridgeRef<SearchState> | null, search: null as BridgeRef<SearchState, SearchAPIWrapper> | null,
spread: null as BridgeRef<SpreadState> | null, spread: null as BridgeRef<SpreadState, SpreadAPIWrapper> | null,
rotation: null as BridgeRef<RotationState> | null, rotation: null as BridgeRef<RotationState, RotationAPIWrapper> | null,
thumbnail: null as BridgeRef<unknown> | null, thumbnail: null as BridgeRef<unknown, ThumbnailAPIWrapper> | null,
}); });
// Immediate zoom callback for responsive display updates // Immediate zoom callback for responsive display updates
const immediateZoomUpdateCallback = useRef<((percent: number) => void) | null>(null); const immediateZoomUpdateCallback = useRef<((percent: number) => void) | null>(null);
// Immediate scroll callback for responsive display updates // Immediate scroll callback for responsive display updates
const immediateScrollUpdateCallback = useRef<((currentPage: number, totalPages: number) => void) | null>(null); const immediateScrollUpdateCallback = useRef<((currentPage: number, totalPages: number) => void) | null>(null);
const registerBridge = (type: string, ref: BridgeRef) => { const registerBridge = (type: string, ref: BridgeRef) => {
(bridgeRefs.current as any)[type] = ref; // Type-safe assignment - we know the bridges will provide correct types
switch (type) {
case 'scroll':
bridgeRefs.current.scroll = ref as BridgeRef<ScrollState, ScrollAPIWrapper>;
break;
case 'zoom':
bridgeRefs.current.zoom = ref as BridgeRef<ZoomState, ZoomAPIWrapper>;
break;
case 'pan':
bridgeRefs.current.pan = ref as BridgeRef<PanState, PanAPIWrapper>;
break;
case 'selection':
bridgeRefs.current.selection = ref as BridgeRef<SelectionState, SelectionAPIWrapper>;
break;
case 'search':
bridgeRefs.current.search = ref as BridgeRef<SearchState, SearchAPIWrapper>;
break;
case 'spread':
bridgeRefs.current.spread = ref as BridgeRef<SpreadState, SpreadAPIWrapper>;
break;
case 'rotation':
bridgeRefs.current.rotation = ref as BridgeRef<RotationState, RotationAPIWrapper>;
break;
case 'thumbnail':
bridgeRefs.current.thumbnail = ref as BridgeRef<unknown, ThumbnailAPIWrapper>;
break;
}
}; };
const toggleThumbnailSidebar = () => { const toggleThumbnailSidebar = () => {
@ -196,32 +281,32 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
// Action handlers - call APIs directly // Action handlers - call APIs directly
const scrollActions = { const scrollActions = {
scrollToPage: (page: number) => { scrollToPage: (page: number) => {
const api = bridgeRefs.current.scroll?.api as any; const api = bridgeRefs.current.scroll?.api;
if (api?.scrollToPage) { if (api?.scrollToPage) {
api.scrollToPage({ pageNumber: page }); api.scrollToPage({ pageNumber: page });
} }
}, },
scrollToFirstPage: () => { scrollToFirstPage: () => {
const api = bridgeRefs.current.scroll?.api as any; const api = bridgeRefs.current.scroll?.api;
if (api?.scrollToPage) { if (api?.scrollToPage) {
api.scrollToPage({ pageNumber: 1 }); api.scrollToPage({ pageNumber: 1 });
} }
}, },
scrollToPreviousPage: () => { scrollToPreviousPage: () => {
const api = bridgeRefs.current.scroll?.api as any; const api = bridgeRefs.current.scroll?.api;
if (api?.scrollToPreviousPage) { if (api?.scrollToPreviousPage) {
api.scrollToPreviousPage(); api.scrollToPreviousPage();
} }
}, },
scrollToNextPage: () => { scrollToNextPage: () => {
const api = bridgeRefs.current.scroll?.api as any; const api = bridgeRefs.current.scroll?.api;
if (api?.scrollToNextPage) { if (api?.scrollToNextPage) {
api.scrollToNextPage(); api.scrollToNextPage();
} }
}, },
scrollToLastPage: () => { scrollToLastPage: () => {
const scrollState = getScrollState(); const scrollState = getScrollState();
const api = bridgeRefs.current.scroll?.api as any; const api = bridgeRefs.current.scroll?.api;
if (api?.scrollToPage && scrollState.totalPages > 0) { if (api?.scrollToPage && scrollState.totalPages > 0) {
api.scrollToPage({ pageNumber: scrollState.totalPages }); api.scrollToPage({ pageNumber: scrollState.totalPages });
} }
@ -230,7 +315,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const zoomActions = { const zoomActions = {
zoomIn: () => { zoomIn: () => {
const api = bridgeRefs.current.zoom?.api as any; const api = bridgeRefs.current.zoom?.api;
if (api?.zoomIn) { if (api?.zoomIn) {
// Update display immediately if callback is registered // Update display immediately if callback is registered
if (immediateZoomUpdateCallback.current) { if (immediateZoomUpdateCallback.current) {
@ -242,7 +327,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
} }
}, },
zoomOut: () => { zoomOut: () => {
const api = bridgeRefs.current.zoom?.api as any; const api = bridgeRefs.current.zoom?.api;
if (api?.zoomOut) { if (api?.zoomOut) {
// Update display immediately if callback is registered // Update display immediately if callback is registered
if (immediateZoomUpdateCallback.current) { if (immediateZoomUpdateCallback.current) {
@ -254,13 +339,13 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
} }
}, },
toggleMarqueeZoom: () => { toggleMarqueeZoom: () => {
const api = bridgeRefs.current.zoom?.api as any; const api = bridgeRefs.current.zoom?.api;
if (api?.toggleMarqueeZoom) { if (api?.toggleMarqueeZoom) {
api.toggleMarqueeZoom(); api.toggleMarqueeZoom();
} }
}, },
requestZoom: (level: number) => { requestZoom: (level: number) => {
const api = bridgeRefs.current.zoom?.api as any; const api = bridgeRefs.current.zoom?.api;
if (api?.requestZoom) { if (api?.requestZoom) {
api.requestZoom(level); api.requestZoom(level);
} }
@ -269,19 +354,19 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const panActions = { const panActions = {
enablePan: () => { enablePan: () => {
const api = bridgeRefs.current.pan?.api as any; const api = bridgeRefs.current.pan?.api;
if (api?.enable) { if (api?.enable) {
api.enable(); api.enable();
} }
}, },
disablePan: () => { disablePan: () => {
const api = bridgeRefs.current.pan?.api as any; const api = bridgeRefs.current.pan?.api;
if (api?.disable) { if (api?.disable) {
api.disable(); api.disable();
} }
}, },
togglePan: () => { togglePan: () => {
const api = bridgeRefs.current.pan?.api as any; const api = bridgeRefs.current.pan?.api;
if (api?.toggle) { if (api?.toggle) {
api.toggle(); api.toggle();
} }
@ -290,20 +375,20 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const selectionActions = { const selectionActions = {
copyToClipboard: () => { copyToClipboard: () => {
const api = bridgeRefs.current.selection?.api as any; const api = bridgeRefs.current.selection?.api;
if (api?.copyToClipboard) { if (api?.copyToClipboard) {
api.copyToClipboard(); api.copyToClipboard();
} }
}, },
getSelectedText: () => { getSelectedText: () => {
const api = bridgeRefs.current.selection?.api as any; const api = bridgeRefs.current.selection?.api;
if (api?.getSelectedText) { if (api?.getSelectedText) {
return api.getSelectedText(); return api.getSelectedText();
} }
return ''; return '';
}, },
getFormattedSelection: () => { getFormattedSelection: () => {
const api = bridgeRefs.current.selection?.api as any; const api = bridgeRefs.current.selection?.api;
if (api?.getFormattedSelection) { if (api?.getFormattedSelection) {
return api.getFormattedSelection(); return api.getFormattedSelection();
} }
@ -313,20 +398,20 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const spreadActions = { const spreadActions = {
setSpreadMode: (mode: SpreadMode) => { setSpreadMode: (mode: SpreadMode) => {
const api = bridgeRefs.current.spread?.api as any; const api = bridgeRefs.current.spread?.api;
if (api?.setSpreadMode) { if (api?.setSpreadMode) {
api.setSpreadMode(mode); api.setSpreadMode(mode);
} }
}, },
getSpreadMode: () => { getSpreadMode: () => {
const api = bridgeRefs.current.spread?.api as any; const api = bridgeRefs.current.spread?.api;
if (api?.getSpreadMode) { if (api?.getSpreadMode) {
return api.getSpreadMode(); return api.getSpreadMode();
} }
return null; return null;
}, },
toggleSpreadMode: () => { toggleSpreadMode: () => {
const api = bridgeRefs.current.spread?.api as any; const api = bridgeRefs.current.spread?.api;
if (api?.toggleSpreadMode) { if (api?.toggleSpreadMode) {
api.toggleSpreadMode(); api.toggleSpreadMode();
} }
@ -335,25 +420,25 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const rotationActions = { const rotationActions = {
rotateForward: () => { rotateForward: () => {
const api = bridgeRefs.current.rotation?.api as any; const api = bridgeRefs.current.rotation?.api;
if (api?.rotateForward) { if (api?.rotateForward) {
api.rotateForward(); api.rotateForward();
} }
}, },
rotateBackward: () => { rotateBackward: () => {
const api = bridgeRefs.current.rotation?.api as any; const api = bridgeRefs.current.rotation?.api;
if (api?.rotateBackward) { if (api?.rotateBackward) {
api.rotateBackward(); api.rotateBackward();
} }
}, },
setRotation: (rotation: number) => { setRotation: (rotation: number) => {
const api = bridgeRefs.current.rotation?.api as any; const api = bridgeRefs.current.rotation?.api;
if (api?.setRotation) { if (api?.setRotation) {
api.setRotation(rotation); api.setRotation(rotation);
} }
}, },
getRotation: () => { getRotation: () => {
const api = bridgeRefs.current.rotation?.api as any; const api = bridgeRefs.current.rotation?.api;
if (api?.getRotation) { if (api?.getRotation) {
return api.getRotation(); return api.getRotation();
} }
@ -363,25 +448,25 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const searchActions = { const searchActions = {
search: async (query: string) => { search: async (query: string) => {
const api = bridgeRefs.current.search?.api as any; const api = bridgeRefs.current.search?.api;
if (api?.search) { if (api?.search) {
return api.search(query); return api.search(query);
} }
}, },
next: () => { next: () => {
const api = bridgeRefs.current.search?.api as any; const api = bridgeRefs.current.search?.api;
if (api?.next) { if (api?.next) {
api.next(); api.next();
} }
}, },
previous: () => { previous: () => {
const api = bridgeRefs.current.search?.api as any; const api = bridgeRefs.current.search?.api;
if (api?.previous) { if (api?.previous) {
api.previous(); api.previous();
} }
}, },
clear: () => { clear: () => {
const api = bridgeRefs.current.search?.api as any; const api = bridgeRefs.current.search?.api;
if (api?.clear) { if (api?.clear) {
api.clear(); api.clear();
} }
@ -412,7 +497,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
// UI state // UI state
isThumbnailSidebarVisible, isThumbnailSidebarVisible,
toggleThumbnailSidebar, toggleThumbnailSidebar,
// State getters // State getters
getScrollState, getScrollState,
getZoomState, getZoomState,
@ -422,13 +507,13 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
getRotationState, getRotationState,
getSearchState, getSearchState,
getThumbnailAPI, getThumbnailAPI,
// Immediate updates // Immediate updates
registerImmediateZoomUpdate, registerImmediateZoomUpdate,
registerImmediateScrollUpdate, registerImmediateScrollUpdate,
triggerImmediateScrollUpdate, triggerImmediateScrollUpdate,
triggerImmediateZoomUpdate, triggerImmediateZoomUpdate,
// Actions // Actions
scrollActions, scrollActions,
zoomActions, zoomActions,
@ -437,7 +522,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
spreadActions, spreadActions,
rotationActions, rotationActions,
searchActions, searchActions,
// Bridge registration // Bridge registration
registerBridge, registerBridge,
}; };
@ -455,4 +540,4 @@ export const useViewer = (): ViewerContextType => {
throw new Error('useViewer must be used within a ViewerProvider'); throw new Error('useViewer must be used within a ViewerProvider');
} }
return context; return context;
}; };

View File

@ -152,6 +152,11 @@
--text-brand: var(--color-gray-700); --text-brand: var(--color-gray-700);
--text-brand-accent: #DC2626; --text-brand-accent: #DC2626;
/* Search highlight colors */
--search-highlight-bg: #FFFF00;
--search-highlight-active-bg: #FFBF00;
--search-highlight-active-border: rgba(255, 191, 0, 0.8);
/* Placeholder text colors */ /* Placeholder text colors */
--search-text-and-icon-color: #6B7382; --search-text-and-icon-color: #6B7382;
@ -337,6 +342,11 @@
--tool-subcategory-text-color: #9CA3AF; /* lighter text in dark mode as well */ --tool-subcategory-text-color: #9CA3AF; /* lighter text in dark mode as well */
--tool-subcategory-rule-color: #3A4047; /* doubly lighter (relative) line in dark */ --tool-subcategory-rule-color: #3A4047; /* doubly lighter (relative) line in dark */
/* Search highlight colors (dark mode) */
--search-highlight-bg: #FFF700;
--search-highlight-active-bg: #FFD700;
--search-highlight-active-border: rgba(255, 215, 0, 0.8);
/* Placeholder text colors (dark mode) */ /* Placeholder text colors (dark mode) */
--search-text-and-icon-color: #FFFFFF !important; --search-text-and-icon-color: #FFFFFF !important;