mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-20 02:29:28 +00:00
improvements
This commit is contained in:
parent
dc71b3007b
commit
b574cef54a
@ -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>
|
||||||
|
@ -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'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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) => {
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user