dark mode PDF filter init (#5994)

Co-authored-by: a <a>
This commit is contained in:
Anthony Stirling
2026-03-25 15:38:42 +00:00
committed by GitHub
co-authored by a <a>
parent 28613caf8a
commit bb43e9dcdf
7 changed files with 108 additions and 53 deletions
@@ -140,6 +140,8 @@ const EmbedPdfViewerContent = ({
printActions,
setApplyChanges,
applyChanges: viewerApplyChanges,
pdfRenderMode,
cyclePdfRenderMode,
} = useViewer();
const scrollState = getScrollState();
@@ -449,6 +451,7 @@ const EmbedPdfViewerContent = ({
}
}
return;
case 'z':
case 'Z':
// Ctrl+Z: Undo; Ctrl+Shift+Z: Redo
@@ -503,7 +506,7 @@ const EmbedPdfViewerContent = ({
}, [
isViewerHovered, isSearchInterfaceVisible, zoomActions, searchInterfaceActions,
scrollActions, printActions, exportActions, rotationActions, historyApiRef,
viewerApplyChanges,
viewerApplyChanges, cyclePdfRenderMode,
]);
// Watch the annotation history API to detect when the document becomes "dirty".
@@ -964,6 +967,7 @@ const EmbedPdfViewerContent = ({
}}>
<LocalEmbedPDF
key={currentFileId || 'no-file'}
pdfRenderMode={pdfRenderMode}
file={effectiveFile.file}
url={effectiveFile.url}
fileName={
@@ -88,9 +88,11 @@ interface LocalEmbedPDFProps {
commentsSidebarRightOffset?: string;
/** When true, blocks the general ink/pen annotation tool (sign tool context). */
isSignMode?: boolean;
/** Controls CSS filter applied only to rendered PDF canvas tiles */
pdfRenderMode?: 'normal' | 'dark' | 'sepia';
}
export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false, enableRedaction = false, enableFormFill = false, isManualRedactionMode = false, showBakedAnnotations = true, onSignatureAdded, signatureApiRef, annotationApiRef, historyApiRef, redactionTrackerRef, fileId, isCommentsSidebarVisible = false, commentsSidebarRightOffset = '0rem', isSignMode = false }: LocalEmbedPDFProps) {
export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false, enableRedaction = false, enableFormFill = false, isManualRedactionMode = false, showBakedAnnotations = true, onSignatureAdded, signatureApiRef, annotationApiRef, historyApiRef, redactionTrackerRef, fileId, isCommentsSidebarVisible = false, commentsSidebarRightOffset = '0rem', isSignMode = false, pdfRenderMode = 'normal' }: LocalEmbedPDFProps) {
const { t } = useTranslation();
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: Rect}>>([]);
@@ -784,7 +786,19 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
onDrop={(e) => e.preventDefault()}
onDragOver={(e) => e.preventDefault()}
>
<TilingLayer documentId={documentId} pageIndex={pageIndex} />
<div style={{
position: 'absolute',
inset: 0,
transition: 'filter 0.25s ease',
filter:
pdfRenderMode === 'dark'
? 'invert(1) hue-rotate(180deg)'
: pdfRenderMode === 'sepia'
? 'sepia(0.7) brightness(0.85)'
: undefined,
}}>
<TilingLayer documentId={documentId} pageIndex={pageIndex} />
</div>
<CustomSearchLayer documentId={documentId} pageIndex={pageIndex} />
@@ -9,6 +9,9 @@ import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import LastPageIcon from '@mui/icons-material/LastPage';
import DescriptionIcon from '@mui/icons-material/Description';
import ViewWeekIcon from '@mui/icons-material/ViewWeek';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import WbSunnyIcon from '@mui/icons-material/WbSunny';
import WbTwilightIcon from '@mui/icons-material/WbTwilight';
interface PdfViewerToolbarProps {
// Page navigation props (placeholders for now)
@@ -33,6 +36,8 @@ export function PdfViewerToolbar({
registerImmediateZoomUpdate,
registerImmediateScrollUpdate,
registerImmediateSpreadUpdate,
pdfRenderMode,
cyclePdfRenderMode,
} = useViewer();
const scrollState = getScrollState();
@@ -126,7 +131,7 @@ export function PdfViewerToolbar({
borderBottomRightRadius: 0,
boxShadow: "0 -2px 8px rgba(0,0,0,0.04)",
pointerEvents: "auto",
minWidth: '26.5rem',
minWidth: '30rem',
}}
>
{/* First Page Button */}
@@ -234,6 +239,39 @@ export function PdfViewerToolbar({
</Button>
</Tooltip>
{/* PDF Render Mode Toggle */}
<Tooltip
content={
pdfRenderMode === 'normal'
? t("viewer.enableDarkFilter", "Enable Dark Filter")
: pdfRenderMode === 'dark'
? t("viewer.enableSepiaFilter", "Enable Sepia Filter")
: t("viewer.disableColorFilter", "Disable Color Filter")
}
position="top"
arrow
>
<Button
variant={pdfRenderMode !== 'normal' ? "filled" : "light"}
color="blue"
size="md"
radius="xl"
onClick={cyclePdfRenderMode}
style={{ minWidth: '2.5rem' }}
aria-label={
pdfRenderMode === 'normal'
? t("viewer.enableDarkFilter", "Enable Dark Filter")
: pdfRenderMode === 'dark'
? t("viewer.enableSepiaFilter", "Enable Sepia Filter")
: t("viewer.disableColorFilter", "Disable Color Filter")
}
>
{pdfRenderMode === 'normal' && <DarkModeIcon fontSize="small" />}
{pdfRenderMode === 'dark' && <WbTwilightIcon fontSize="small" />}
{pdfRenderMode === 'sepia' && <WbSunnyIcon fontSize="small" />}
</Button>
</Tooltip>
{/* Zoom Controls */}
<Group gap={4} align="center" style={{ marginLeft: 16 }}>
<Button
@@ -7,6 +7,7 @@ import React, {
useCallback,
} from 'react';
import { useNavigation } from '@app/contexts/NavigationContext';
import { preferencesService, type PdfRenderMode } from '@app/services/preferencesService';
import {
createViewerActions,
ScrollActions,
@@ -170,6 +171,10 @@ export interface ViewerContextType {
// Save changes function - registered by EmbedPdfViewer
applyChanges: (() => Promise<void>) | null;
setApplyChanges: (fn: (() => Promise<void>) | null) => void;
// PDF page color rendering mode (viewer-only, never modifies the PDF)
pdfRenderMode: PdfRenderMode;
cyclePdfRenderMode: () => void;
}
export const ViewerContext = createContext<ViewerContextType | null>(null);
@@ -194,6 +199,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
const [activeFileIndex, setActiveFileIndex] = useState(0);
const [pdfRenderMode, setPdfRenderModeState] = useState<PdfRenderMode>(
() => preferencesService.getPreference('pdfRenderMode')
);
// Get current navigation state to check if we're in sign mode
useNavigation();
@@ -320,6 +328,15 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
setIsAnnotationModeState(enabled);
};
const cyclePdfRenderMode = useCallback(() => {
setPdfRenderModeState(prev => {
const next: PdfRenderMode =
prev === 'normal' ? 'dark' : prev === 'dark' ? 'sepia' : 'normal';
preferencesService.setPreference('pdfRenderMode', next);
return next;
});
}, []);
// State getters - read from bridge refs
const getScrollState = (): ScrollState => {
return bridgeRefs.current.scroll?.state || { currentPage: 1, totalPages: 0 };
@@ -504,6 +521,10 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
// Apply changes
applyChanges,
setApplyChanges,
// PDF page rendering mode
pdfRenderMode,
cyclePdfRenderMode,
};
return (
@@ -3,6 +3,8 @@ import { type ThemeMode, getSystemTheme } from '@app/constants/theme';
export type LogoVariant = 'modern' | 'classic';
export type PdfRenderMode = 'normal' | 'dark' | 'sepia';
export interface UserPreferences {
autoUnzip: boolean;
autoUnzipFileLimit: number;
@@ -17,6 +19,7 @@ export interface UserPreferences {
hideUnavailableTools: boolean;
hideUnavailableConversions: boolean;
logoVariant: LogoVariant | null;
pdfRenderMode: PdfRenderMode;
}
export const DEFAULT_PREFERENCES: UserPreferences = {
@@ -33,6 +36,7 @@ export const DEFAULT_PREFERENCES: UserPreferences = {
hideUnavailableTools: false,
hideUnavailableConversions: false,
logoVariant: null,
pdfRenderMode: 'normal',
};
const STORAGE_KEY = 'stirlingpdf_preferences';