@printwithsynergy/lens-pdf 0.3.0-beta.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +344 -0
- package/dist/browser/codexOverlay.d.ts +109 -0
- package/dist/browser/codexOverlay.d.ts.map +1 -0
- package/dist/browser/codexOverlay.js +256 -0
- package/dist/browser/codexOverlay.js.map +1 -0
- package/dist/browser/constants.d.ts +13 -0
- package/dist/browser/constants.d.ts.map +1 -0
- package/dist/browser/constants.js +13 -0
- package/dist/browser/constants.js.map +1 -0
- package/dist/browser/index.d.ts +211 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +1190 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/pantone-gold.d.ts +59 -0
- package/dist/browser/pantone-gold.d.ts.map +1 -0
- package/dist/browser/pantone-gold.js +237 -0
- package/dist/browser/pantone-gold.js.map +1 -0
- package/dist/components/AnnotationCanvas.d.ts +27 -0
- package/dist/components/AnnotationCanvas.d.ts.map +1 -0
- package/dist/components/AnnotationCanvas.js +401 -0
- package/dist/components/AnnotationCanvas.js.map +1 -0
- package/dist/components/AnnotationNotesPanel.d.ts +15 -0
- package/dist/components/AnnotationNotesPanel.d.ts.map +1 -0
- package/dist/components/AnnotationNotesPanel.js +235 -0
- package/dist/components/AnnotationNotesPanel.js.map +1 -0
- package/dist/components/AnnotationThread.d.ts +18 -0
- package/dist/components/AnnotationThread.d.ts.map +1 -0
- package/dist/components/AnnotationThread.js +163 -0
- package/dist/components/AnnotationThread.js.map +1 -0
- package/dist/components/AnnotationToolbar.d.ts +39 -0
- package/dist/components/AnnotationToolbar.d.ts.map +1 -0
- package/dist/components/AnnotationToolbar.js +258 -0
- package/dist/components/AnnotationToolbar.js.map +1 -0
- package/dist/components/BoxOverlay.d.ts +20 -0
- package/dist/components/BoxOverlay.d.ts.map +1 -0
- package/dist/components/BoxOverlay.js +107 -0
- package/dist/components/BoxOverlay.js.map +1 -0
- package/dist/components/ColorPickerTool.d.ts +11 -0
- package/dist/components/ColorPickerTool.d.ts.map +1 -0
- package/dist/components/ColorPickerTool.js +220 -0
- package/dist/components/ColorPickerTool.js.map +1 -0
- package/dist/components/DensitometerTool.d.ts +25 -0
- package/dist/components/DensitometerTool.d.ts.map +1 -0
- package/dist/components/DensitometerTool.js +246 -0
- package/dist/components/DensitometerTool.js.map +1 -0
- package/dist/components/DielineInfoPanel.d.ts +27 -0
- package/dist/components/DielineInfoPanel.d.ts.map +1 -0
- package/dist/components/DielineInfoPanel.js +23 -0
- package/dist/components/DielineInfoPanel.js.map +1 -0
- package/dist/components/DielineOverlay.d.ts +10 -0
- package/dist/components/DielineOverlay.d.ts.map +1 -0
- package/dist/components/DielineOverlay.js +57 -0
- package/dist/components/DielineOverlay.js.map +1 -0
- package/dist/components/FindingsSidebar.d.ts +50 -0
- package/dist/components/FindingsSidebar.d.ts.map +1 -0
- package/dist/components/FindingsSidebar.js +78 -0
- package/dist/components/FindingsSidebar.js.map +1 -0
- package/dist/components/LayerCanvas.d.ts +30 -0
- package/dist/components/LayerCanvas.d.ts.map +1 -0
- package/dist/components/LayerCanvas.js +84 -0
- package/dist/components/LayerCanvas.js.map +1 -0
- package/dist/components/LayerPanel.d.ts +9 -0
- package/dist/components/LayerPanel.d.ts.map +1 -0
- package/dist/components/LayerPanel.js +144 -0
- package/dist/components/LayerPanel.js.map +1 -0
- package/dist/components/LensPDF.d.ts +61 -0
- package/dist/components/LensPDF.d.ts.map +1 -0
- package/dist/components/LensPDF.js +49 -0
- package/dist/components/LensPDF.js.map +1 -0
- package/dist/components/LensPDFDemo.d.ts +160 -0
- package/dist/components/LensPDFDemo.d.ts.map +1 -0
- package/dist/components/LensPDFDemo.js +1060 -0
- package/dist/components/LensPDFDemo.js.map +1 -0
- package/dist/components/LensPDFDemo.styles.d.ts +38 -0
- package/dist/components/LensPDFDemo.styles.d.ts.map +1 -0
- package/dist/components/LensPDFDemo.styles.js +282 -0
- package/dist/components/LensPDFDemo.styles.js.map +1 -0
- package/dist/components/LensPDFViewer.d.ts +79 -0
- package/dist/components/LensPDFViewer.d.ts.map +1 -0
- package/dist/components/LensPDFViewer.js +254 -0
- package/dist/components/LensPDFViewer.js.map +1 -0
- package/dist/components/MeasureTool.d.ts +16 -0
- package/dist/components/MeasureTool.d.ts.map +1 -0
- package/dist/components/MeasureTool.js +137 -0
- package/dist/components/MeasureTool.js.map +1 -0
- package/dist/components/MobileBottomSheet.d.ts +12 -0
- package/dist/components/MobileBottomSheet.d.ts.map +1 -0
- package/dist/components/MobileBottomSheet.js +113 -0
- package/dist/components/MobileBottomSheet.js.map +1 -0
- package/dist/components/MobileDrawer.d.ts +31 -0
- package/dist/components/MobileDrawer.d.ts.map +1 -0
- package/dist/components/MobileDrawer.js +67 -0
- package/dist/components/MobileDrawer.js.map +1 -0
- package/dist/components/PageCanvas.d.ts +33 -0
- package/dist/components/PageCanvas.d.ts.map +1 -0
- package/dist/components/PageCanvas.js +385 -0
- package/dist/components/PageCanvas.js.map +1 -0
- package/dist/components/PageNavigator.d.ts +18 -0
- package/dist/components/PageNavigator.d.ts.map +1 -0
- package/dist/components/PageNavigator.js +44 -0
- package/dist/components/PageNavigator.js.map +1 -0
- package/dist/components/SeparationCanvas.d.ts +12 -0
- package/dist/components/SeparationCanvas.d.ts.map +1 -0
- package/dist/components/SeparationCanvas.js +174 -0
- package/dist/components/SeparationCanvas.js.map +1 -0
- package/dist/components/TACHeatmapOverlay.d.ts +17 -0
- package/dist/components/TACHeatmapOverlay.d.ts.map +1 -0
- package/dist/components/TACHeatmapOverlay.js +119 -0
- package/dist/components/TACHeatmapOverlay.js.map +1 -0
- package/dist/components/ZoomControls.d.ts +11 -0
- package/dist/components/ZoomControls.d.ts.map +1 -0
- package/dist/components/ZoomControls.js +26 -0
- package/dist/components/ZoomControls.js.map +1 -0
- package/dist/components/defaultShellPlugins.d.ts +3 -0
- package/dist/components/defaultShellPlugins.d.ts.map +1 -0
- package/dist/components/defaultShellPlugins.js +273 -0
- package/dist/components/defaultShellPlugins.js.map +1 -0
- package/dist/components/index.d.ts +32 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +32 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/presets.d.ts +8 -0
- package/dist/components/presets.d.ts.map +1 -0
- package/dist/components/presets.js +14 -0
- package/dist/components/presets.js.map +1 -0
- package/dist/components/shellPlugins.d.ts +105 -0
- package/dist/components/shellPlugins.d.ts.map +1 -0
- package/dist/components/shellPlugins.js +52 -0
- package/dist/components/shellPlugins.js.map +1 -0
- package/dist/components/useIsMobile.d.ts +16 -0
- package/dist/components/useIsMobile.d.ts.map +1 -0
- package/dist/components/useIsMobile.js +30 -0
- package/dist/components/useIsMobile.js.map +1 -0
- package/dist/fallback-pdfjs/index.d.ts +60 -0
- package/dist/fallback-pdfjs/index.d.ts.map +1 -0
- package/dist/fallback-pdfjs/index.js +163 -0
- package/dist/fallback-pdfjs/index.js.map +1 -0
- package/dist/host/LensPDFProvider.d.ts +36 -0
- package/dist/host/LensPDFProvider.d.ts.map +1 -0
- package/dist/host/LensPDFProvider.js +12 -0
- package/dist/host/LensPDFProvider.js.map +1 -0
- package/dist/host/index.d.ts +167 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +173 -0
- package/dist/host/index.js.map +1 -0
- package/dist/host/pdfFallback.d.ts +50 -0
- package/dist/host/pdfFallback.d.ts.map +1 -0
- package/dist/host/pdfFallback.js +171 -0
- package/dist/host/pdfFallback.js.map +1 -0
- package/dist/host/pdfValidation.d.ts +45 -0
- package/dist/host/pdfValidation.d.ts.map +1 -0
- package/dist/host/pdfValidation.js +78 -0
- package/dist/host/pdfValidation.js.map +1 -0
- package/dist/host/shareLink.d.ts +80 -0
- package/dist/host/shareLink.d.ts.map +1 -0
- package/dist/host/shareLink.js +114 -0
- package/dist/host/shareLink.js.map +1 -0
- package/dist/host/useLensPDF.d.ts +73 -0
- package/dist/host/useLensPDF.d.ts.map +1 -0
- package/dist/host/useLensPDF.js +213 -0
- package/dist/host/useLensPDF.js.map +1 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/context.d.ts +70 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +16 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/findings-location.d.ts +53 -0
- package/dist/plugin/findings-location.d.ts.map +1 -0
- package/dist/plugin/findings-location.js +72 -0
- package/dist/plugin/findings-location.js.map +1 -0
- package/dist/plugin/index.d.ts +19 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +16 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/registry.d.ts +61 -0
- package/dist/plugin/registry.d.ts.map +1 -0
- package/dist/plugin/registry.js +102 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/services.d.ts +380 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +104 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/types.d.ts +198 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/plugin/types.js +24 -0
- package/dist/plugin/types.js.map +1 -0
- package/dist/types/index.d.ts +191 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +95 -0
- package/dist/types/index.js.map +1 -0
- package/dist/units/index.d.ts +64 -0
- package/dist/units/index.d.ts.map +1 -0
- package/dist/units/index.js +98 -0
- package/dist/units/index.js.map +1 -0
- package/docs/architecture.md +90 -0
- package/docs/components.md +569 -0
- package/docs/contributing.md +78 -0
- package/docs/fallback.md +174 -0
- package/docs/lens-pdf-viewer.md +128 -0
- package/docs/licensing.md +78 -0
- package/docs/measurement-units.md +87 -0
- package/docs/plugins.md +256 -0
- package/docs/security.md +69 -0
- package/docs/server.md +212 -0
- package/docs/services.md +210 -0
- package/docs/share-links.md +111 -0
- package/docs/theming.md +164 -0
- package/docs/validation.md +83 -0
- package/package.json +139 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* `<LensPDFDemo>` — kitchen-sink interactive demo component.
|
|
5
|
+
*
|
|
6
|
+
* **Most consumers should not import this directly.** Use
|
|
7
|
+
* {@link LensPDF} instead — it's a one-liner production drop-in:
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <LensPDF pdfUrl="/proofs/abc.pdf" workerSrc={pdfWorkerSrc} />
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* `<LensPDFDemo>` is the same renderer with the marketing chrome
|
|
14
|
+
* (URL bar, drag-and-drop upload, file picker, empty state) turned
|
|
15
|
+
* on; it powers the public showcase at lenspdf.com so reviewers
|
|
16
|
+
* can drop arbitrary PDFs into the page without a host.
|
|
17
|
+
*
|
|
18
|
+
* One mount, full feature surface. Backed by
|
|
19
|
+
* `createBrowserViewerServices`, every viewer-only feature LensPDF
|
|
20
|
+
* ships works on any PDF the browser can fetch:
|
|
21
|
+
*
|
|
22
|
+
* - PageCanvas + multi-page navigation + multi-DPI tile cache
|
|
23
|
+
* - Color picker (RGB + CMYK + every detected spot ink + TAC)
|
|
24
|
+
* - Densitometer (CMYK + every detected spot ink + TAC limit)
|
|
25
|
+
* - Measure tool (mm / in / pt)
|
|
26
|
+
* - TAC heatmap overlay (CMYK + spots)
|
|
27
|
+
* - Per-ink CMYK + spot separations preview (inks default ON,
|
|
28
|
+
* untick to hide that plate — same UX as Acrobat's Output
|
|
29
|
+
* Preview)
|
|
30
|
+
* - PDF layers (per-OCG isolated rendering, default all on)
|
|
31
|
+
* - Annotation canvas + toolbar + thread (in-memory)
|
|
32
|
+
*
|
|
33
|
+
* Three mutually-exclusive primary canvases — Page (default),
|
|
34
|
+
* Separation preview, Layer preview — match the lint-pdf reference
|
|
35
|
+
* viewer's UX so the same muscle memory carries over.
|
|
36
|
+
*
|
|
37
|
+
* Server-only features (true ICC separations, preflight findings,
|
|
38
|
+
* server-persisted annotations, PDF report exports) self-hide because
|
|
39
|
+
* their dedicated services are intentionally `markUnwired`. Hosts
|
|
40
|
+
* that have a backend pass `services` to override.
|
|
41
|
+
*
|
|
42
|
+
* Internal organisation:
|
|
43
|
+
*
|
|
44
|
+
* - Inline CSS-in-JS lives in `LensPDFDemo.styles.ts` (so this
|
|
45
|
+
* file focuses on the React tree, not 270 lines of styling).
|
|
46
|
+
* - Smaller building blocks (`PageCanvas`, `SeparationCanvas`,
|
|
47
|
+
* `LayerCanvas`, `AnnotationCanvas`, `AnnotationToolbar`,
|
|
48
|
+
* `AnnotationThread`, `LayerPanel`, `BoxOverlay`,
|
|
49
|
+
* `DielineOverlay`, `TACHeatmapOverlay`, `ColorPickerTool`,
|
|
50
|
+
* `DensitometerTool`, `MeasureTool`) each ship as their own
|
|
51
|
+
* component file and are composed here.
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
56
|
+
import { createBrowserViewerServices, useBrowserViewerServicesVersion, createCodexOverlayServices, extractInksFromColorWorld, extractLayersFromOcgs, PROCESS_CHANNELS, } from "../browser/index.js";
|
|
57
|
+
import { darkThemeTokens } from "../plugin/services.js";
|
|
58
|
+
import { DEFAULT_DPI, pageInfoFromDimensions } from "../types/index.js";
|
|
59
|
+
import { isUnwired, ViewerHostContext, ViewerServicesContext } from "../host/index.js";
|
|
60
|
+
import { validatePdfFile, validatePdfUrl } from "../host/pdfValidation.js";
|
|
61
|
+
import { brandStyle, btnStyle, dropOverlayStyle, emptyStateStyle, errorStyle, exitFsStyle, footerStyle, ghostBtnStyle, headingStyle, layoutStyle, pageNavBtnStyle, pageNavStyle, preparingOverlayStyle, shellStyle, sidebarStyle, stageInnerStyle, stageStyle, topbarStyle, urlBarStyle, urlInputStyle, } from "./LensPDFDemo.styles.js";
|
|
62
|
+
import { AnnotationCanvas } from "./AnnotationCanvas.js";
|
|
63
|
+
import { useIsMobile } from "./useIsMobile.js";
|
|
64
|
+
import { BoxOverlay } from "./BoxOverlay.js";
|
|
65
|
+
import { ColorPickerTool } from "./ColorPickerTool.js";
|
|
66
|
+
import { DensitometerTool } from "./DensitometerTool.js";
|
|
67
|
+
import { DielineOverlay } from "./DielineOverlay.js";
|
|
68
|
+
import { LayerCanvas } from "./LayerCanvas.js";
|
|
69
|
+
import { MeasureTool } from "./MeasureTool.js";
|
|
70
|
+
import { PageCanvas } from "./PageCanvas.js";
|
|
71
|
+
import { SeparationCanvas } from "./SeparationCanvas.js";
|
|
72
|
+
import { TACHeatmapOverlay } from "./TACHeatmapOverlay.js";
|
|
73
|
+
import { pluginsForPreset } from "./presets.js";
|
|
74
|
+
import { computeFeatureAvailability, pluginsForSlot, resolveShellPlugins, } from "./shellPlugins.js";
|
|
75
|
+
const DEFAULT_TOOLS = [
|
|
76
|
+
"color-picker",
|
|
77
|
+
"densitometer",
|
|
78
|
+
"measure",
|
|
79
|
+
"annotate",
|
|
80
|
+
"tac-heatmap",
|
|
81
|
+
"separations",
|
|
82
|
+
"layers",
|
|
83
|
+
];
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Constants
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
88
|
+
const FLATTENED_LAYER_INDEX = -1;
|
|
89
|
+
// PTS_TO_PX must match PageCanvas's internal pts-to-pixel conversion
|
|
90
|
+
// (which is `DEFAULT_DPI / 72`). Using a different ratio here makes
|
|
91
|
+
// the canvas-area parent div size disagree with PageCanvas's rendered
|
|
92
|
+
// page, so absolute-positioned overlays (TAC heatmap, separations,
|
|
93
|
+
// layers, annotations, dieline) shift relative to the page content.
|
|
94
|
+
const PTS_TO_PX = DEFAULT_DPI / 72;
|
|
95
|
+
const DEFAULT_PAGE = pageInfoFromDimensions(1, 612, 792);
|
|
96
|
+
function formatMaxSize(bytes) {
|
|
97
|
+
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Main component
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Complete interactive LensPDF demo — upload, URL paste, drag-drop,
|
|
104
|
+
* validation, sidebar controls, theming, and optional fullscreen mode.
|
|
105
|
+
* All viewer-only features (color picker, densitometer, measure,
|
|
106
|
+
* separations, TAC heatmap, layers, annotations) are wired out of the
|
|
107
|
+
* box.
|
|
108
|
+
*
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
export function LensPDFDemo({ tokens: tokenOverrides, maxFileSize = DEFAULT_MAX_BYTES, brand, brandLogoUrl, className, tools = DEFAULT_TOOLS, initialZoom = 80, tacLimit = 300, workerSrc, services: serviceOverrides, footer, fullscreen: initialFullscreen = false, initialPdfUrl, initialPage = 1, embedded = false, items, forceInspectionPanel, spotPalette, selectedItem, onItemSelect, dieline, showBoxOverlays = false, cropToTrim = false, onPageChange: onPageChangeProp, onZoomChange: onZoomChangeProp, onError: onErrorProp, preset = "demo", plugins: customPlugins = [], codex, }) {
|
|
112
|
+
const overlayItems = useMemo(() => items ?? [], [items]);
|
|
113
|
+
// Selection: controlled when onItemSelect is supplied, uncontrolled otherwise.
|
|
114
|
+
const [internalSelected, setInternalSelected] = useState(null);
|
|
115
|
+
const effectiveSelected = onItemSelect !== undefined ? (selectedItem ?? null) : internalSelected;
|
|
116
|
+
const handleItemClick = useCallback((item) => {
|
|
117
|
+
if (onItemSelect)
|
|
118
|
+
onItemSelect(item);
|
|
119
|
+
else
|
|
120
|
+
setInternalSelected(item);
|
|
121
|
+
}, [onItemSelect]);
|
|
122
|
+
// -----------------------------------------------------------------------
|
|
123
|
+
// Tokens
|
|
124
|
+
// -----------------------------------------------------------------------
|
|
125
|
+
const tokens = useMemo(() => ({ ...darkThemeTokens, ...tokenOverrides }),
|
|
126
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
127
|
+
[JSON.stringify(tokenOverrides)]);
|
|
128
|
+
// Brand resolution: explicit prop > tokens.logo* > built-in default.
|
|
129
|
+
// Lets a host bundle its identity (colors + logo + label) into one
|
|
130
|
+
// tokens object without dropping the existing prop API.
|
|
131
|
+
const effectiveBrand = brand ?? tokens.logoText ?? "LensPDF";
|
|
132
|
+
const effectiveLogoUrl = brandLogoUrl ?? tokens.logoUrl;
|
|
133
|
+
const effectiveLogoMaxHeight = tokens.logoMaxHeight ?? 24;
|
|
134
|
+
const effectiveLogoAlt = tokens.logoAlt;
|
|
135
|
+
// -----------------------------------------------------------------------
|
|
136
|
+
// Responsive layout
|
|
137
|
+
// -----------------------------------------------------------------------
|
|
138
|
+
// On mobile the tools sidebar collapses into a slide-in drawer
|
|
139
|
+
// anchored to the left edge; the densitometer / color-picker
|
|
140
|
+
// readouts switch to bottom sheets via `useIsMobile()` inside those
|
|
141
|
+
// components. Desktop keeps the persistent sidebar.
|
|
142
|
+
const isMobile = useIsMobile();
|
|
143
|
+
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
144
|
+
// Open when no PDF pre-loaded so the user sees how to load one; auto-closes
|
|
145
|
+
// after a PDF is loaded so the canvas gets the space back.
|
|
146
|
+
const [mobileUrlBarOpen, setMobileUrlBarOpen] = useState(!initialPdfUrl);
|
|
147
|
+
/** Height of the marketing top bar (URL row). Drawer + dimmer start below it so they never cover the chrome or collide with the tools toggle. */
|
|
148
|
+
const headerBarRef = useRef(null);
|
|
149
|
+
const [headerChromePx, setHeaderChromePx] = useState(0);
|
|
150
|
+
useLayoutEffect(() => {
|
|
151
|
+
if (embedded) {
|
|
152
|
+
setHeaderChromePx(0);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const el = headerBarRef.current;
|
|
156
|
+
if (!el || typeof ResizeObserver === "undefined") {
|
|
157
|
+
setHeaderChromePx(el?.offsetHeight ?? 0);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const sync = () => setHeaderChromePx(Math.ceil(el.getBoundingClientRect().height));
|
|
161
|
+
sync();
|
|
162
|
+
const ro = new ResizeObserver(sync);
|
|
163
|
+
ro.observe(el);
|
|
164
|
+
return () => ro.disconnect();
|
|
165
|
+
}, [embedded, isMobile]);
|
|
166
|
+
// -----------------------------------------------------------------------
|
|
167
|
+
// PDF state
|
|
168
|
+
// -----------------------------------------------------------------------
|
|
169
|
+
const [pdfUrl, setPdfUrl] = useState(initialPdfUrl ?? "");
|
|
170
|
+
const [draftUrl, setDraftUrl] = useState(initialPdfUrl ?? "");
|
|
171
|
+
const [error, setError] = useState(null);
|
|
172
|
+
const [dragging, setDragging] = useState(false);
|
|
173
|
+
const [fullscreen, setFullscreen] = useState(initialFullscreen);
|
|
174
|
+
const fileInputRef = useRef(null);
|
|
175
|
+
// Embedded mode treats `initialPdfUrl` as a controlled prop — when
|
|
176
|
+
// it changes, swap the loaded PDF and reset to page 1. Demo mode
|
|
177
|
+
// ignores subsequent changes (the user drives the URL via the
|
|
178
|
+
// upload bar) so behaviour stays unsurprising.
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!embedded)
|
|
181
|
+
return;
|
|
182
|
+
const next = initialPdfUrl ?? "";
|
|
183
|
+
setPdfUrl((prev) => (prev === next ? prev : next));
|
|
184
|
+
setDraftUrl(next);
|
|
185
|
+
if (next)
|
|
186
|
+
setCurrentPage(initialPage);
|
|
187
|
+
// initialPage intentionally read once via closure; the page-reset
|
|
188
|
+
// belongs to URL change only.
|
|
189
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
190
|
+
}, [embedded, initialPdfUrl]);
|
|
191
|
+
// -----------------------------------------------------------------------
|
|
192
|
+
// Page / zoom state
|
|
193
|
+
// -----------------------------------------------------------------------
|
|
194
|
+
const [zoom, setZoom] = useState(initialZoom);
|
|
195
|
+
const [page, setPage] = useState(initialPage !== 1
|
|
196
|
+
? { ...DEFAULT_PAGE, page_num: initialPage }
|
|
197
|
+
: DEFAULT_PAGE);
|
|
198
|
+
const [pageCount, setPageCount] = useState(1);
|
|
199
|
+
const [currentPage, setCurrentPage] = useState(initialPage);
|
|
200
|
+
// Lifecycle callbacks: fire host listeners whenever core state moves.
|
|
201
|
+
// Wrapped in effects rather than threading through every setter
|
|
202
|
+
// callsite (zoom slider / arrow keys / wheel pinch / page nav buttons /
|
|
203
|
+
// "jump to page" from annotation thread, etc.) so behaviour stays in
|
|
204
|
+
// one place.
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
onPageChangeProp?.(currentPage);
|
|
207
|
+
}, [currentPage, onPageChangeProp]);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
onZoomChangeProp?.(zoom);
|
|
210
|
+
}, [zoom, onZoomChangeProp]);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (error)
|
|
213
|
+
onErrorProp?.(error);
|
|
214
|
+
}, [error, onErrorProp]);
|
|
215
|
+
// -----------------------------------------------------------------------
|
|
216
|
+
// Viewer mode (mutually exclusive primary canvas) + tool overlay state
|
|
217
|
+
// -----------------------------------------------------------------------
|
|
218
|
+
const [viewerMode, setViewerMode] = useState("page");
|
|
219
|
+
const [activeTool, setActiveTool] = useState("none");
|
|
220
|
+
const [showHeatmap, setShowHeatmap] = useState(false);
|
|
221
|
+
const [allLayerIndices, setAllLayerIndices] = useState([]);
|
|
222
|
+
const [enabledLayers, setEnabledLayers] = useState(new Set());
|
|
223
|
+
const [enabledChannels, setEnabledChannels] = useState(new Set(PROCESS_CHANNELS));
|
|
224
|
+
const [detectedInks, setDetectedInks] = useState([]);
|
|
225
|
+
// -----------------------------------------------------------------------
|
|
226
|
+
// Annotation state
|
|
227
|
+
// -----------------------------------------------------------------------
|
|
228
|
+
// Pen first — draws immediately. Select (second in toolbar) only
|
|
229
|
+
// grabs existing annotations; defaulting to pointer felt "broken" on
|
|
230
|
+
// an empty page.
|
|
231
|
+
const [annotationTool, setAnnotationTool] = useState("pen");
|
|
232
|
+
const [strokeColor, setStrokeColor] = useState(tokens.accent);
|
|
233
|
+
const [savingAnnotation, setSavingAnnotation] = useState(false);
|
|
234
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
235
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
236
|
+
const [indexedAnnotations, setIndexedAnnotations] = useState([]);
|
|
237
|
+
const [selectedAnnotationId, setSelectedAnnotationId] = useState(null);
|
|
238
|
+
// -----------------------------------------------------------------------
|
|
239
|
+
// Services
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
const [browserServices, setBrowserServices] = useState(null);
|
|
242
|
+
const [codexOverlay, setCodexOverlay] = useState(null);
|
|
243
|
+
const [preparing, setPreparing] = useState(false);
|
|
244
|
+
const [toolsLoading, setToolsLoading] = useState(false);
|
|
245
|
+
// Reactive: re-render every time the services notify a new tile / channel
|
|
246
|
+
// / heatmap / annotation has landed. PageCanvas / SeparationCanvas /
|
|
247
|
+
// TACHeatmapOverlay re-read the synchronous URL builders and pick up the
|
|
248
|
+
// fresh blob URL. AnnotationThread reads it as `refreshKey` so the
|
|
249
|
+
// sidebar list re-fetches after AnnotationCanvas persists a drawing.
|
|
250
|
+
const servicesVersion = useBrowserViewerServicesVersion(browserServices);
|
|
251
|
+
// Subscribe to codex overlay notifications (blob URLs for Ghostscript renders).
|
|
252
|
+
const [codexVersion, setCodexVersion] = useState(0);
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (!codexOverlay)
|
|
255
|
+
return;
|
|
256
|
+
return codexOverlay.subscribe(() => setCodexVersion((v) => v + 1));
|
|
257
|
+
}, [codexOverlay]);
|
|
258
|
+
// Build / dispose services whenever the PDF URL changes.
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (!pdfUrl) {
|
|
261
|
+
setBrowserServices(null);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const next = createBrowserViewerServices({
|
|
265
|
+
pdfUrl,
|
|
266
|
+
workerSrc,
|
|
267
|
+
tokens,
|
|
268
|
+
tacLimit,
|
|
269
|
+
});
|
|
270
|
+
setBrowserServices(next);
|
|
271
|
+
return () => next.dispose();
|
|
272
|
+
}, [pdfUrl, workerSrc, tacLimit, tokens, serviceOverrides]);
|
|
273
|
+
// Codex background extraction — fires extractStream in parallel with pdfjs.
|
|
274
|
+
// As SSE events arrive the viewer silently upgrades ink list + renders.
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
if (!pdfUrl || !codex) {
|
|
277
|
+
setCodexOverlay(null);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
let cancelled = false;
|
|
281
|
+
let overlay = null;
|
|
282
|
+
let layerData = [];
|
|
283
|
+
(async () => {
|
|
284
|
+
const res = await fetch(pdfUrl);
|
|
285
|
+
if (!res.ok || cancelled)
|
|
286
|
+
return;
|
|
287
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
288
|
+
if (cancelled)
|
|
289
|
+
return;
|
|
290
|
+
await codex.extractStream(bytes, {
|
|
291
|
+
granular: true,
|
|
292
|
+
onColorWorld: (data) => {
|
|
293
|
+
if (cancelled)
|
|
294
|
+
return;
|
|
295
|
+
const inks = extractInksFromColorWorld(data);
|
|
296
|
+
setDetectedInks(inks);
|
|
297
|
+
setEnabledChannels(new Set(inks.map((i) => i.name)));
|
|
298
|
+
},
|
|
299
|
+
onOcgs: (data) => {
|
|
300
|
+
if (cancelled)
|
|
301
|
+
return;
|
|
302
|
+
layerData = extractLayersFromOcgs(data);
|
|
303
|
+
},
|
|
304
|
+
onPhase2: (doc) => {
|
|
305
|
+
if (cancelled)
|
|
306
|
+
return;
|
|
307
|
+
overlay = createCodexOverlayServices(codex, doc.pdf_sha256, tacLimit, layerData);
|
|
308
|
+
setCodexOverlay(overlay);
|
|
309
|
+
// Update layer indices from codex's accurate OCG list.
|
|
310
|
+
if (layerData.length > 0) {
|
|
311
|
+
const indices = layerData.map((l) => l.ocg_index);
|
|
312
|
+
setAllLayerIndices(indices);
|
|
313
|
+
setEnabledLayers(new Set(indices));
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
})().catch((err) => {
|
|
318
|
+
// eslint-disable-next-line no-console
|
|
319
|
+
console.warn("[lens-pdf] codex overlay extraction failed", err);
|
|
320
|
+
});
|
|
321
|
+
return () => {
|
|
322
|
+
cancelled = true;
|
|
323
|
+
if (overlay) {
|
|
324
|
+
overlay.dispose();
|
|
325
|
+
overlay = null;
|
|
326
|
+
}
|
|
327
|
+
setCodexOverlay(null);
|
|
328
|
+
};
|
|
329
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
330
|
+
}, [pdfUrl, codex, tacLimit]);
|
|
331
|
+
// Resolve page count + initial layer list when services come online.
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
const svc = browserServices;
|
|
334
|
+
if (!svc) {
|
|
335
|
+
setPageCount(1);
|
|
336
|
+
setToolsLoading(false);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
let cancelled = false;
|
|
340
|
+
setToolsLoading(true);
|
|
341
|
+
(async () => {
|
|
342
|
+
try {
|
|
343
|
+
const total = await svc.getPageCount();
|
|
344
|
+
if (cancelled)
|
|
345
|
+
return;
|
|
346
|
+
setPageCount(total);
|
|
347
|
+
const next = Math.min(total, Math.max(1, currentPage));
|
|
348
|
+
if (next !== currentPage)
|
|
349
|
+
setCurrentPage(next);
|
|
350
|
+
const dims = await svc.getPageDimensions(next);
|
|
351
|
+
if (cancelled)
|
|
352
|
+
return;
|
|
353
|
+
setPage(pageInfoFromDimensions(next, dims.widthPts, dims.heightPts));
|
|
354
|
+
const layers = await svc.layers.listLayers();
|
|
355
|
+
if (cancelled)
|
|
356
|
+
return;
|
|
357
|
+
const indices = layers.length > 0
|
|
358
|
+
? layers.map((l) => l.ocg_index)
|
|
359
|
+
: [FLATTENED_LAYER_INDEX];
|
|
360
|
+
setAllLayerIndices(indices);
|
|
361
|
+
// Default all detected layers ON, matching the lint-pdf
|
|
362
|
+
// viewer's "Layers mode" default.
|
|
363
|
+
setEnabledLayers(new Set(indices));
|
|
364
|
+
// Surface every ink the PDF declares so the Inks panel can
|
|
365
|
+
// toggle CMYK + spots, and the densitometer / color picker
|
|
366
|
+
// report on every plate the document carries.
|
|
367
|
+
const inks = await svc.getInks();
|
|
368
|
+
if (cancelled)
|
|
369
|
+
return;
|
|
370
|
+
setDetectedInks(inks);
|
|
371
|
+
setEnabledChannels(new Set(inks.map((i) => i.name)));
|
|
372
|
+
setError(null);
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
if (!cancelled) {
|
|
376
|
+
setError(err instanceof Error ? err.message : "Failed to load PDF.");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
if (!cancelled)
|
|
381
|
+
setToolsLoading(false);
|
|
382
|
+
}
|
|
383
|
+
})();
|
|
384
|
+
return () => {
|
|
385
|
+
cancelled = true;
|
|
386
|
+
};
|
|
387
|
+
// currentPage intentionally omitted — handled below.
|
|
388
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
389
|
+
}, [browserServices]);
|
|
390
|
+
// Re-read page dimensions on page navigation.
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
const svc = browserServices;
|
|
393
|
+
if (!svc)
|
|
394
|
+
return;
|
|
395
|
+
let cancelled = false;
|
|
396
|
+
(async () => {
|
|
397
|
+
try {
|
|
398
|
+
const dims = await svc.getPageDimensions(currentPage);
|
|
399
|
+
if (cancelled)
|
|
400
|
+
return;
|
|
401
|
+
setPage(pageInfoFromDimensions(currentPage, dims.widthPts, dims.heightPts));
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// Ignore — error already surfaced by the initial load effect.
|
|
405
|
+
}
|
|
406
|
+
})();
|
|
407
|
+
return () => {
|
|
408
|
+
cancelled = true;
|
|
409
|
+
};
|
|
410
|
+
}, [browserServices, currentPage]);
|
|
411
|
+
// Pre-warm separations / heatmap / layer rasters whenever we enter a
|
|
412
|
+
// mode that needs them. Without this, <SeparationCanvas> /
|
|
413
|
+
// <LayerCanvas> latch onto the empty URL the lazy builder returns
|
|
414
|
+
// before the analysis raster lands and never retry.
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
const svc = browserServices;
|
|
417
|
+
if (!svc)
|
|
418
|
+
return;
|
|
419
|
+
if (viewerMode === "page" && !showHeatmap)
|
|
420
|
+
return;
|
|
421
|
+
let cancelled = false;
|
|
422
|
+
setPreparing(true);
|
|
423
|
+
(async () => {
|
|
424
|
+
try {
|
|
425
|
+
await svc.prepare(currentPage, { tacLimit });
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
if (!cancelled) {
|
|
429
|
+
setError(err instanceof Error ? err.message : "Failed to prepare page.");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
if (!cancelled)
|
|
434
|
+
setPreparing(false);
|
|
435
|
+
}
|
|
436
|
+
})();
|
|
437
|
+
return () => {
|
|
438
|
+
cancelled = true;
|
|
439
|
+
};
|
|
440
|
+
}, [browserServices, currentPage, viewerMode, showHeatmap, tacLimit]);
|
|
441
|
+
const services = useMemo(() => {
|
|
442
|
+
if (!browserServices)
|
|
443
|
+
return serviceOverrides ?? null;
|
|
444
|
+
// Base: pdfjs services with optional host overrides on top.
|
|
445
|
+
const base = serviceOverrides
|
|
446
|
+
? {
|
|
447
|
+
pageImages: isUnwired(serviceOverrides.pageImages)
|
|
448
|
+
? browserServices.pageImages
|
|
449
|
+
: serviceOverrides.pageImages,
|
|
450
|
+
layers: isUnwired(serviceOverrides.layers)
|
|
451
|
+
? browserServices.layers
|
|
452
|
+
: serviceOverrides.layers,
|
|
453
|
+
separations: isUnwired(serviceOverrides.separations)
|
|
454
|
+
? browserServices.separations
|
|
455
|
+
: serviceOverrides.separations,
|
|
456
|
+
tacHeatmap: isUnwired(serviceOverrides.tacHeatmap)
|
|
457
|
+
? browserServices.tacHeatmap
|
|
458
|
+
: serviceOverrides.tacHeatmap,
|
|
459
|
+
colorSample: isUnwired(serviceOverrides.colorSample)
|
|
460
|
+
? browserServices.colorSample
|
|
461
|
+
: serviceOverrides.colorSample,
|
|
462
|
+
densitometer: isUnwired(serviceOverrides.densitometer)
|
|
463
|
+
? browserServices.densitometer
|
|
464
|
+
: serviceOverrides.densitometer,
|
|
465
|
+
annotations: isUnwired(serviceOverrides.annotations)
|
|
466
|
+
? browserServices.annotations
|
|
467
|
+
: serviceOverrides.annotations,
|
|
468
|
+
reports: isUnwired(serviceOverrides.reports)
|
|
469
|
+
? browserServices.reports
|
|
470
|
+
: serviceOverrides.reports,
|
|
471
|
+
telemetry: isUnwired(serviceOverrides.telemetry)
|
|
472
|
+
? browserServices.telemetry
|
|
473
|
+
: serviceOverrides.telemetry,
|
|
474
|
+
i18n: isUnwired(serviceOverrides.i18n)
|
|
475
|
+
? browserServices.i18n
|
|
476
|
+
: serviceOverrides.i18n,
|
|
477
|
+
tokens: serviceOverrides.tokens ?? browserServices.tokens,
|
|
478
|
+
}
|
|
479
|
+
: browserServices;
|
|
480
|
+
// Codex overlay: swap in Ghostscript-accurate renders once available.
|
|
481
|
+
// pageImages, colorSample, densitometer, annotations stay on pdfjs.
|
|
482
|
+
if (codexOverlay) {
|
|
483
|
+
return {
|
|
484
|
+
...base,
|
|
485
|
+
separations: codexOverlay.separations,
|
|
486
|
+
tacHeatmap: codexOverlay.tacHeatmap,
|
|
487
|
+
layers: codexOverlay.layers,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return base;
|
|
491
|
+
// codexVersion + servicesVersion are intentionally in deps to force a
|
|
492
|
+
// re-render when lazy blob URLs land inside the overlay or pdfjs caches.
|
|
493
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
494
|
+
}, [serviceOverrides, browserServices, codexOverlay, codexVersion, servicesVersion]);
|
|
495
|
+
// -----------------------------------------------------------------------
|
|
496
|
+
// Blob URL lifecycle (uploads only)
|
|
497
|
+
// -----------------------------------------------------------------------
|
|
498
|
+
const blobUrlRef = useRef(null);
|
|
499
|
+
const revokePreviousBlob = useCallback(() => {
|
|
500
|
+
if (blobUrlRef.current) {
|
|
501
|
+
URL.revokeObjectURL(blobUrlRef.current);
|
|
502
|
+
blobUrlRef.current = null;
|
|
503
|
+
}
|
|
504
|
+
}, []);
|
|
505
|
+
useEffect(() => revokePreviousBlob, [revokePreviousBlob]);
|
|
506
|
+
// -----------------------------------------------------------------------
|
|
507
|
+
// Derived
|
|
508
|
+
// -----------------------------------------------------------------------
|
|
509
|
+
const scale = zoom / 100;
|
|
510
|
+
const canvasW = Math.round(page.width_pts * PTS_TO_PX * scale);
|
|
511
|
+
const canvasH = Math.round(page.height_pts * PTS_TO_PX * scale);
|
|
512
|
+
const hostValue = useMemo(() => ({
|
|
513
|
+
apiBase: "",
|
|
514
|
+
jobApiBase: "",
|
|
515
|
+
// readOnly = false so AnnotationCanvas persists drawings to the
|
|
516
|
+
// in-memory annotation service.
|
|
517
|
+
readOnly: false,
|
|
518
|
+
debug: false,
|
|
519
|
+
pdfUrl: pdfUrl || undefined,
|
|
520
|
+
}), [pdfUrl]);
|
|
521
|
+
// -----------------------------------------------------------------------
|
|
522
|
+
// Input handlers
|
|
523
|
+
// -----------------------------------------------------------------------
|
|
524
|
+
const loadUrl = useCallback((e) => {
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
const result = validatePdfUrl(draftUrl);
|
|
527
|
+
if (!result.valid) {
|
|
528
|
+
setError(result.error ?? "Invalid URL.");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
setError(null);
|
|
532
|
+
revokePreviousBlob();
|
|
533
|
+
setCurrentPage(1);
|
|
534
|
+
setViewerMode("page");
|
|
535
|
+
setPdfUrl(draftUrl.trim());
|
|
536
|
+
}, [draftUrl, revokePreviousBlob]);
|
|
537
|
+
const loadFile = useCallback(async (file) => {
|
|
538
|
+
const result = await validatePdfFile(file, maxFileSize);
|
|
539
|
+
if (!result.valid) {
|
|
540
|
+
setError(result.error ?? "Invalid file.");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
setError(null);
|
|
544
|
+
revokePreviousBlob();
|
|
545
|
+
const blobUrl = URL.createObjectURL(file);
|
|
546
|
+
blobUrlRef.current = blobUrl;
|
|
547
|
+
setDraftUrl(file.name);
|
|
548
|
+
setCurrentPage(1);
|
|
549
|
+
setViewerMode("page");
|
|
550
|
+
setPdfUrl(blobUrl);
|
|
551
|
+
}, [revokePreviousBlob, maxFileSize]);
|
|
552
|
+
const onFileChange = useCallback((e) => {
|
|
553
|
+
const file = e.target.files?.[0];
|
|
554
|
+
if (file)
|
|
555
|
+
loadFile(file);
|
|
556
|
+
e.target.value = "";
|
|
557
|
+
}, [loadFile]);
|
|
558
|
+
const onDragOver = useCallback((e) => {
|
|
559
|
+
e.preventDefault();
|
|
560
|
+
setDragging(true);
|
|
561
|
+
}, []);
|
|
562
|
+
const onDragLeave = useCallback((e) => {
|
|
563
|
+
e.preventDefault();
|
|
564
|
+
setDragging(false);
|
|
565
|
+
}, []);
|
|
566
|
+
const onDrop = useCallback((e) => {
|
|
567
|
+
e.preventDefault();
|
|
568
|
+
setDragging(false);
|
|
569
|
+
const file = e.dataTransfer.files?.[0];
|
|
570
|
+
if (file)
|
|
571
|
+
loadFile(file);
|
|
572
|
+
}, [loadFile]);
|
|
573
|
+
// -----------------------------------------------------------------------
|
|
574
|
+
// Annotation undo/redo plumbing
|
|
575
|
+
// -----------------------------------------------------------------------
|
|
576
|
+
const annotationCanvasRef = useRef(null);
|
|
577
|
+
const annotationWrapRef = useRef(null);
|
|
578
|
+
useEffect(() => {
|
|
579
|
+
if (activeTool !== "annotate")
|
|
580
|
+
return;
|
|
581
|
+
const wrap = annotationWrapRef.current;
|
|
582
|
+
if (!wrap)
|
|
583
|
+
return;
|
|
584
|
+
annotationCanvasRef.current =
|
|
585
|
+
wrap.querySelector("canvas") ?? null;
|
|
586
|
+
}, [activeTool, currentPage, canvasW, canvasH]);
|
|
587
|
+
useEffect(() => {
|
|
588
|
+
setIndexedAnnotations([]);
|
|
589
|
+
setSelectedAnnotationId(null);
|
|
590
|
+
}, [currentPage]);
|
|
591
|
+
const triggerUndo = useCallback(() => {
|
|
592
|
+
const fn = annotationCanvasRef.current?.__annotationUndo;
|
|
593
|
+
fn?.();
|
|
594
|
+
}, []);
|
|
595
|
+
const triggerRedo = useCallback(() => {
|
|
596
|
+
const fn = annotationCanvasRef.current?.__annotationRedo;
|
|
597
|
+
fn?.();
|
|
598
|
+
}, []);
|
|
599
|
+
const handleAnnotationHistoryChange = useCallback((canU, canR) => {
|
|
600
|
+
setCanUndo(canU);
|
|
601
|
+
setCanRedo(canR);
|
|
602
|
+
}, []);
|
|
603
|
+
// -----------------------------------------------------------------------
|
|
604
|
+
// Plugin availability + slot resolution
|
|
605
|
+
// -----------------------------------------------------------------------
|
|
606
|
+
const availability = useMemo(() => computeFeatureAvailability({
|
|
607
|
+
tools,
|
|
608
|
+
services,
|
|
609
|
+
detectedInkCount: detectedInks.length,
|
|
610
|
+
layerCount: allLayerIndices.length,
|
|
611
|
+
isUnwired,
|
|
612
|
+
}), [tools, services, detectedInks.length, allLayerIndices.length]);
|
|
613
|
+
const shellPluginContext = useMemo(() => ({
|
|
614
|
+
tokens,
|
|
615
|
+
isMobile,
|
|
616
|
+
pdfUrl,
|
|
617
|
+
servicesVersion,
|
|
618
|
+
currentPage,
|
|
619
|
+
setCurrentPage,
|
|
620
|
+
viewerMode,
|
|
621
|
+
setViewerMode,
|
|
622
|
+
activeTool,
|
|
623
|
+
setActiveTool,
|
|
624
|
+
showHeatmap,
|
|
625
|
+
setShowHeatmap,
|
|
626
|
+
enabledChannels,
|
|
627
|
+
setEnabledChannels,
|
|
628
|
+
detectedInks: detectedInks.map((ink) => ({
|
|
629
|
+
name: ink.name,
|
|
630
|
+
type: ink.type,
|
|
631
|
+
altRgb: ink.altRgb,
|
|
632
|
+
})),
|
|
633
|
+
spotPalette,
|
|
634
|
+
items,
|
|
635
|
+
forceInspectionPanel,
|
|
636
|
+
selectedItem,
|
|
637
|
+
onItemSelect,
|
|
638
|
+
enabledLayers,
|
|
639
|
+
setEnabledLayers,
|
|
640
|
+
allLayerIndices,
|
|
641
|
+
annotationTool,
|
|
642
|
+
setAnnotationTool,
|
|
643
|
+
strokeColor,
|
|
644
|
+
setStrokeColor,
|
|
645
|
+
savingAnnotation,
|
|
646
|
+
canUndo,
|
|
647
|
+
canRedo,
|
|
648
|
+
triggerUndo,
|
|
649
|
+
triggerRedo,
|
|
650
|
+
indexedAnnotations,
|
|
651
|
+
selectedAnnotationId,
|
|
652
|
+
setSelectedAnnotationId,
|
|
653
|
+
availability,
|
|
654
|
+
}), [
|
|
655
|
+
tokens,
|
|
656
|
+
isMobile,
|
|
657
|
+
pdfUrl,
|
|
658
|
+
servicesVersion,
|
|
659
|
+
currentPage,
|
|
660
|
+
viewerMode,
|
|
661
|
+
activeTool,
|
|
662
|
+
showHeatmap,
|
|
663
|
+
enabledChannels,
|
|
664
|
+
detectedInks,
|
|
665
|
+
enabledLayers,
|
|
666
|
+
allLayerIndices,
|
|
667
|
+
annotationTool,
|
|
668
|
+
strokeColor,
|
|
669
|
+
savingAnnotation,
|
|
670
|
+
canUndo,
|
|
671
|
+
canRedo,
|
|
672
|
+
triggerUndo,
|
|
673
|
+
triggerRedo,
|
|
674
|
+
indexedAnnotations,
|
|
675
|
+
selectedAnnotationId,
|
|
676
|
+
availability,
|
|
677
|
+
]);
|
|
678
|
+
const resolvedPlugins = useMemo(() => resolveShellPlugins([...pluginsForPreset(preset), ...customPlugins]), [preset, customPlugins]);
|
|
679
|
+
const leftPanelPlugins = useMemo(() => pluginsForSlot(resolvedPlugins, "panel.left", shellPluginContext), [resolvedPlugins, shellPluginContext]);
|
|
680
|
+
const toolbarOverlayPlugins = useMemo(() => pluginsForSlot(resolvedPlugins, "overlay.toolbar", shellPluginContext), [resolvedPlugins, shellPluginContext]);
|
|
681
|
+
const showColorPicker = availability.colorPicker;
|
|
682
|
+
const showDensitometer = availability.densitometer;
|
|
683
|
+
const showMeasure = availability.measure;
|
|
684
|
+
const showAnnotate = availability.annotate;
|
|
685
|
+
const showSeparations = availability.separations;
|
|
686
|
+
const showLayersControl = availability.layers;
|
|
687
|
+
const hasAnyTool = leftPanelPlugins.length > 0;
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
if (viewerMode === "separation" && !availability.separations)
|
|
690
|
+
setViewerMode("page");
|
|
691
|
+
if (viewerMode === "layer" && !availability.layers)
|
|
692
|
+
setViewerMode("page");
|
|
693
|
+
}, [viewerMode, availability.separations, availability.layers]);
|
|
694
|
+
useEffect(() => {
|
|
695
|
+
if (activeTool === "color-picker" && !availability.colorPicker)
|
|
696
|
+
setActiveTool("none");
|
|
697
|
+
if (activeTool === "densitometer" && !availability.densitometer)
|
|
698
|
+
setActiveTool("none");
|
|
699
|
+
if (activeTool === "measure" && !availability.measure)
|
|
700
|
+
setActiveTool("none");
|
|
701
|
+
if (activeTool === "annotate" && !availability.annotate)
|
|
702
|
+
setActiveTool("none");
|
|
703
|
+
}, [
|
|
704
|
+
activeTool,
|
|
705
|
+
availability.colorPicker,
|
|
706
|
+
availability.densitometer,
|
|
707
|
+
availability.measure,
|
|
708
|
+
availability.annotate,
|
|
709
|
+
]);
|
|
710
|
+
useEffect(() => {
|
|
711
|
+
if (!availability.tacHeatmap && showHeatmap)
|
|
712
|
+
setShowHeatmap(false);
|
|
713
|
+
}, [availability.tacHeatmap, showHeatmap]);
|
|
714
|
+
// On mobile, dismiss the tools drawer automatically when the user
|
|
715
|
+
// activates an interactive tool so the canvas is immediately visible.
|
|
716
|
+
useEffect(() => {
|
|
717
|
+
if (isMobile && activeTool !== "none")
|
|
718
|
+
setMobileSidebarOpen(false);
|
|
719
|
+
}, [activeTool, isMobile]);
|
|
720
|
+
// Collapse the URL bar accordion when a PDF finishes loading on mobile.
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
if (isMobile && pdfUrl)
|
|
723
|
+
setMobileUrlBarOpen(false);
|
|
724
|
+
}, [pdfUrl, isMobile]);
|
|
725
|
+
// Match the document background to the viewer's dark bg so overscroll
|
|
726
|
+
// bounce (iOS rubber-band, macOS elastic scroll) shows the same colour
|
|
727
|
+
// as the viewer chrome instead of the host page's white body background.
|
|
728
|
+
// Only applies in standalone (non-embedded) mode; embedded consumers own
|
|
729
|
+
// their own page background.
|
|
730
|
+
useEffect(() => {
|
|
731
|
+
if (embedded || typeof document === "undefined")
|
|
732
|
+
return;
|
|
733
|
+
const html = document.documentElement;
|
|
734
|
+
const body = document.body;
|
|
735
|
+
const prevHtmlBg = html.style.backgroundColor;
|
|
736
|
+
const prevBodyBg = body.style.backgroundColor;
|
|
737
|
+
const prevHtmlOverscroll = html.style.overscrollBehavior;
|
|
738
|
+
const prevBodyOverscroll = body.style.overscrollBehavior;
|
|
739
|
+
html.style.backgroundColor = tokens.bg;
|
|
740
|
+
body.style.backgroundColor = tokens.bg;
|
|
741
|
+
html.style.overscrollBehavior = "none";
|
|
742
|
+
body.style.overscrollBehavior = "none";
|
|
743
|
+
return () => {
|
|
744
|
+
html.style.backgroundColor = prevHtmlBg;
|
|
745
|
+
body.style.backgroundColor = prevBodyBg;
|
|
746
|
+
html.style.overscrollBehavior = prevHtmlOverscroll;
|
|
747
|
+
body.style.overscrollBehavior = prevBodyOverscroll;
|
|
748
|
+
};
|
|
749
|
+
}, [embedded, tokens.bg]);
|
|
750
|
+
// -----------------------------------------------------------------------
|
|
751
|
+
// Render
|
|
752
|
+
// -----------------------------------------------------------------------
|
|
753
|
+
const placeholderServices = services;
|
|
754
|
+
return (_jsx(ViewerHostContext.Provider, { value: hostValue, children: placeholderServices ? (_jsx(ViewerServicesContext.Provider, { value: placeholderServices, children: renderShell() })) : (renderShell()) }));
|
|
755
|
+
function renderShell() {
|
|
756
|
+
return (_jsxs("div", { className: className, style: shellStyle(tokens, fullscreen), onDragOver: embedded ? undefined : onDragOver, onDragLeave: embedded ? undefined : onDragLeave, onDrop: embedded ? undefined : onDrop, children: [fullscreen && (_jsx("button", { type: "button", style: exitFsStyle, onClick: () => setFullscreen(false), children: "Exit fullscreen" })), !embedded && dragging && (_jsx("div", { style: dropOverlayStyle, children: "Drop your PDF here" })), !embedded && (_jsxs("header", { ref: headerBarRef, style: {
|
|
757
|
+
...topbarStyle,
|
|
758
|
+
position: "relative",
|
|
759
|
+
zIndex: 100,
|
|
760
|
+
background: tokens.bg,
|
|
761
|
+
borderBottom: `1px solid ${tokens.border}`,
|
|
762
|
+
...(isMobile
|
|
763
|
+
? {
|
|
764
|
+
flexDirection: "column",
|
|
765
|
+
alignItems: "stretch",
|
|
766
|
+
gap: 12,
|
|
767
|
+
padding: "12px 14px",
|
|
768
|
+
}
|
|
769
|
+
: {}),
|
|
770
|
+
}, children: [isMobile ? (_jsxs(_Fragment, { children: [_jsxs("div", { style: {
|
|
771
|
+
display: "flex",
|
|
772
|
+
alignItems: "center",
|
|
773
|
+
gap: 10,
|
|
774
|
+
width: "100%",
|
|
775
|
+
minWidth: 0,
|
|
776
|
+
}, children: [hasAnyTool && (_jsx("button", { type: "button", "aria-label": "Open tools panel", "aria-expanded": mobileSidebarOpen, onClick: () => setMobileSidebarOpen((v) => !v), style: {
|
|
777
|
+
flexShrink: 0,
|
|
778
|
+
width: 44,
|
|
779
|
+
height: 44,
|
|
780
|
+
borderRadius: 8,
|
|
781
|
+
border: `1px solid ${tokens.border}`,
|
|
782
|
+
background: tokens.bg,
|
|
783
|
+
color: tokens.fg,
|
|
784
|
+
cursor: "pointer",
|
|
785
|
+
fontSize: 22,
|
|
786
|
+
lineHeight: 1,
|
|
787
|
+
display: "flex",
|
|
788
|
+
alignItems: "center",
|
|
789
|
+
justifyContent: "center",
|
|
790
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.25)",
|
|
791
|
+
}, children: "\u2630" })), _jsxs("div", { style: {
|
|
792
|
+
...brandStyle,
|
|
793
|
+
flex: 1,
|
|
794
|
+
minWidth: 0,
|
|
795
|
+
overflow: "hidden",
|
|
796
|
+
}, children: [effectiveLogoUrl && (_jsx("img", { src: effectiveLogoUrl, alt: effectiveLogoAlt ?? "", "aria-hidden": effectiveLogoAlt ? undefined : "true", style: {
|
|
797
|
+
height: effectiveLogoMaxHeight,
|
|
798
|
+
width: "auto",
|
|
799
|
+
maxHeight: effectiveLogoMaxHeight,
|
|
800
|
+
flexShrink: 0,
|
|
801
|
+
} })), _jsx("span", { style: { overflow: "hidden", textOverflow: "ellipsis" }, children: effectiveBrand }), _jsx("span", { style: { opacity: 0.4 }, children: "\u00B7" }), _jsx("span", { style: {
|
|
802
|
+
opacity: 0.6,
|
|
803
|
+
fontWeight: 400,
|
|
804
|
+
fontSize: 13,
|
|
805
|
+
flexShrink: 0,
|
|
806
|
+
}, children: "demo" })] }), _jsx("button", { type: "button", "aria-label": mobileUrlBarOpen ? "Close file controls" : pdfUrl ? "Change file" : "Open a PDF", "aria-expanded": mobileUrlBarOpen, onClick: () => setMobileUrlBarOpen((v) => !v), style: {
|
|
807
|
+
flexShrink: 0,
|
|
808
|
+
width: 36,
|
|
809
|
+
height: 36,
|
|
810
|
+
borderRadius: 8,
|
|
811
|
+
border: `1px solid ${(pdfUrl || preparing) ? tokens.accent : tokens.border}`,
|
|
812
|
+
background: (pdfUrl || preparing) ? `${tokens.accent}22` : "transparent",
|
|
813
|
+
color: (pdfUrl || preparing) ? tokens.accent : tokens.fg,
|
|
814
|
+
cursor: "pointer",
|
|
815
|
+
display: "flex",
|
|
816
|
+
alignItems: "center",
|
|
817
|
+
justifyContent: "center",
|
|
818
|
+
opacity: mobileUrlBarOpen ? 0.6 : 1,
|
|
819
|
+
transition: "opacity 0.15s",
|
|
820
|
+
}, children: preparing ? (
|
|
821
|
+
// Spinner
|
|
822
|
+
_jsxs("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", "aria-hidden": true, children: [_jsx("circle", { cx: 8, cy: 8, r: 6, stroke: "currentColor", strokeWidth: 2, strokeOpacity: 0.25 }), _jsx("path", { d: "M14 8a6 6 0 0 0-6-6", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", style: { transformOrigin: "8px 8px", animation: "lens-pdf-tools-spin 0.7s linear infinite" } })] })) : pdfUrl ? (
|
|
823
|
+
// Filled document — file is loaded
|
|
824
|
+
_jsx("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true, children: _jsx("path", { d: "M4 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6.414A1 1 0 0 0 12.707 6L9 2.293A1 1 0 0 0 8.586 2H4zm4 .5V6a1 1 0 0 0 1 1h3.5L8 2.5z" }) })) : (
|
|
825
|
+
// Outline folder — nothing loaded yet
|
|
826
|
+
_jsx("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": true, children: _jsx("path", { d: "M2 5a1 1 0 0 1 1-1h3.586a1 1 0 0 1 .707.293L8.414 5.4A1 1 0 0 0 9.121 5.7H13a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5z", strokeLinejoin: "round" }) })) })] }), _jsx("div", { style: {
|
|
827
|
+
overflow: "hidden",
|
|
828
|
+
maxHeight: mobileUrlBarOpen ? 300 : 0,
|
|
829
|
+
transition: "max-height 0.22s ease",
|
|
830
|
+
}, children: _jsxs("form", { onSubmit: loadUrl, style: {
|
|
831
|
+
display: "flex",
|
|
832
|
+
flexDirection: "column",
|
|
833
|
+
gap: 10,
|
|
834
|
+
width: "100%",
|
|
835
|
+
paddingTop: 2,
|
|
836
|
+
paddingBottom: 2,
|
|
837
|
+
}, children: [_jsx("input", { type: "text", inputMode: "url", autoCapitalize: "off", autoCorrect: "off", spellCheck: false, enterKeyHint: "go", placeholder: "Paste PDF URL (https://\u2026)", value: draftUrl, onChange: (e) => setDraftUrl(e.target.value), style: {
|
|
838
|
+
...urlInputStyle(tokens),
|
|
839
|
+
width: "100%",
|
|
840
|
+
boxSizing: "border-box",
|
|
841
|
+
minHeight: 44,
|
|
842
|
+
fontSize: 16,
|
|
843
|
+
} }), _jsxs("div", { style: {
|
|
844
|
+
display: "flex",
|
|
845
|
+
gap: 10,
|
|
846
|
+
width: "100%",
|
|
847
|
+
}, children: [_jsx("button", { type: "submit", disabled: !/^https?:\/\//i.test(draftUrl.trim()), style: {
|
|
848
|
+
...btnStyle(tokens, !/^https?:\/\//i.test(draftUrl.trim())),
|
|
849
|
+
flex: 1,
|
|
850
|
+
minHeight: 44,
|
|
851
|
+
padding: "12px 14px",
|
|
852
|
+
fontSize: 15,
|
|
853
|
+
}, children: "Load" }), _jsx("button", { type: "button", style: {
|
|
854
|
+
...ghostBtnStyle(tokens),
|
|
855
|
+
flex: 1,
|
|
856
|
+
minHeight: 44,
|
|
857
|
+
padding: "12px 14px",
|
|
858
|
+
fontSize: 15,
|
|
859
|
+
}, onClick: () => fileInputRef.current?.click(), children: "Upload PDF" })] })] }) })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: brandStyle, children: [effectiveLogoUrl && (_jsx("img", { src: effectiveLogoUrl, alt: effectiveLogoAlt ?? "", "aria-hidden": effectiveLogoAlt ? undefined : "true", style: {
|
|
860
|
+
height: effectiveLogoMaxHeight,
|
|
861
|
+
width: "auto",
|
|
862
|
+
maxHeight: effectiveLogoMaxHeight,
|
|
863
|
+
} })), _jsx("span", { children: effectiveBrand }), _jsx("span", { style: { opacity: 0.4 }, children: "\u00B7" }), _jsx("span", { style: { opacity: 0.6, fontWeight: 400, fontSize: 13 }, children: "demo" })] }), _jsxs("form", { style: urlBarStyle, onSubmit: loadUrl, children: [_jsx("input", { type: "text", placeholder: "Paste any PDF URL the browser can fetch\u2026", value: draftUrl, onChange: (e) => setDraftUrl(e.target.value), style: urlInputStyle(tokens) }), _jsx("button", { type: "submit", disabled: !/^https?:\/\//i.test(draftUrl.trim()), style: btnStyle(tokens, !/^https?:\/\//i.test(draftUrl.trim())), children: "Load" })] }), _jsx("button", { type: "button", style: ghostBtnStyle(tokens), onClick: () => fileInputRef.current?.click(), children: "Upload PDF" })] })), _jsx("input", { ref: fileInputRef, type: "file", accept: "application/pdf,.pdf", style: { display: "none" }, onChange: onFileChange })] })), error && (_jsxs("div", { style: errorStyle(), children: [_jsx("span", { children: error }), _jsx("button", { type: "button", onClick: () => setError(null), "aria-label": "Dismiss", style: {
|
|
864
|
+
background: "transparent",
|
|
865
|
+
border: "none",
|
|
866
|
+
color: "inherit",
|
|
867
|
+
cursor: "pointer",
|
|
868
|
+
fontSize: 18,
|
|
869
|
+
}, children: "\u00D7" })] })), _jsxs("div", { style: { ...layoutStyle, position: "relative" }, children: [embedded && hasAnyTool && isMobile && !mobileSidebarOpen && (_jsx("button", { type: "button", "aria-label": "Open tools panel", "aria-expanded": mobileSidebarOpen, onClick: () => setMobileSidebarOpen((v) => !v), style: {
|
|
870
|
+
position: "absolute",
|
|
871
|
+
top: 12,
|
|
872
|
+
right: 12,
|
|
873
|
+
left: "auto",
|
|
874
|
+
zIndex: 60,
|
|
875
|
+
width: 44,
|
|
876
|
+
height: 44,
|
|
877
|
+
borderRadius: 8,
|
|
878
|
+
border: `1px solid ${tokens.border}`,
|
|
879
|
+
background: tokens.bg,
|
|
880
|
+
color: tokens.fg,
|
|
881
|
+
cursor: "pointer",
|
|
882
|
+
fontSize: 22,
|
|
883
|
+
lineHeight: 1,
|
|
884
|
+
display: "flex",
|
|
885
|
+
alignItems: "center",
|
|
886
|
+
justifyContent: "center",
|
|
887
|
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.35)",
|
|
888
|
+
}, children: "\u2630" })), hasAnyTool && isMobile && mobileSidebarOpen && (_jsx("div", { onClick: () => setMobileSidebarOpen(false), style: {
|
|
889
|
+
position: "fixed",
|
|
890
|
+
left: 0,
|
|
891
|
+
right: 0,
|
|
892
|
+
bottom: 0,
|
|
893
|
+
top: 0,
|
|
894
|
+
zIndex: 140,
|
|
895
|
+
background: "rgba(0, 0, 0, 0.72)",
|
|
896
|
+
} })), hasAnyTool && (_jsxs("aside", { style: isMobile
|
|
897
|
+
? {
|
|
898
|
+
...sidebarStyle(tokens),
|
|
899
|
+
position: "fixed",
|
|
900
|
+
top: 0,
|
|
901
|
+
left: 0,
|
|
902
|
+
bottom: 0,
|
|
903
|
+
width: "min(85vw, 320px)",
|
|
904
|
+
maxWidth: "100%",
|
|
905
|
+
zIndex: 141,
|
|
906
|
+
transform: mobileSidebarOpen
|
|
907
|
+
? "translateX(0)"
|
|
908
|
+
: "translateX(-100%)",
|
|
909
|
+
transition: "transform 0.22s ease-out",
|
|
910
|
+
borderRight: `1px solid ${tokens.border}`,
|
|
911
|
+
boxShadow: mobileSidebarOpen
|
|
912
|
+
? "8px 0 24px rgba(0, 0, 0, 0.45)"
|
|
913
|
+
: "none",
|
|
914
|
+
WebkitOverflowScrolling: "touch",
|
|
915
|
+
overscrollBehavior: "contain",
|
|
916
|
+
paddingTop: "max(12px, env(safe-area-inset-top))",
|
|
917
|
+
paddingBottom: "max(16px, env(safe-area-inset-bottom))",
|
|
918
|
+
}
|
|
919
|
+
: sidebarStyle(tokens), children: [isMobile && (_jsxs("div", { style: {
|
|
920
|
+
display: "flex",
|
|
921
|
+
alignItems: "center",
|
|
922
|
+
justifyContent: "space-between",
|
|
923
|
+
gap: 8,
|
|
924
|
+
marginBottom: 8,
|
|
925
|
+
position: "sticky",
|
|
926
|
+
top: 0,
|
|
927
|
+
zIndex: 2,
|
|
928
|
+
paddingTop: 2,
|
|
929
|
+
paddingBottom: 8,
|
|
930
|
+
background: tokens.bg,
|
|
931
|
+
borderBottom: `1px solid ${tokens.border}`,
|
|
932
|
+
}, children: [_jsx("h2", { style: { ...headingStyle, margin: 0 }, children: "Tools" }), _jsx("button", { type: "button", onClick: () => setMobileSidebarOpen(false), "aria-label": "Close tools panel", style: {
|
|
933
|
+
width: 36,
|
|
934
|
+
height: 36,
|
|
935
|
+
borderRadius: 8,
|
|
936
|
+
border: `1px solid ${tokens.border}`,
|
|
937
|
+
background: tokens.bg,
|
|
938
|
+
color: tokens.fg,
|
|
939
|
+
cursor: "pointer",
|
|
940
|
+
fontSize: 20,
|
|
941
|
+
lineHeight: 1,
|
|
942
|
+
display: "flex",
|
|
943
|
+
alignItems: "center",
|
|
944
|
+
justifyContent: "center",
|
|
945
|
+
flexShrink: 0,
|
|
946
|
+
}, children: "\u00D7" })] })), _jsxs("div", { style: pageNavStyle, children: [_jsx("span", { style: { width: 44 }, children: "Zoom" }), _jsx("input", { type: "range", min: "25", max: "400", step: "5", value: zoom, onChange: (e) => setZoom(Number(e.target.value)), style: { flex: 1 } }), _jsxs("span", { style: {
|
|
947
|
+
minWidth: 44,
|
|
948
|
+
textAlign: "right",
|
|
949
|
+
fontVariantNumeric: "tabular-nums",
|
|
950
|
+
}, children: [zoom, "%"] })] }), pageCount > 1 && (_jsxs("div", { style: pageNavStyle, children: [_jsx("button", { type: "button", style: pageNavBtnStyle(tokens, currentPage <= 1), onClick: () => setCurrentPage((p) => Math.max(1, p - 1)), disabled: currentPage <= 1, "aria-label": "Previous page", children: "\u2039" }), _jsxs("span", { style: {
|
|
951
|
+
flex: 1,
|
|
952
|
+
textAlign: "center",
|
|
953
|
+
fontSize: 12,
|
|
954
|
+
fontVariantNumeric: "tabular-nums",
|
|
955
|
+
opacity: 0.8,
|
|
956
|
+
}, children: ["Page ", currentPage, " / ", pageCount] }), _jsx("button", { type: "button", style: pageNavBtnStyle(tokens, currentPage >= pageCount), onClick: () => setCurrentPage((p) => Math.min(pageCount, p + 1)), disabled: currentPage >= pageCount, "aria-label": "Next page", children: "\u203A" })] })), toolsLoading ? (_jsxs("div", { style: {
|
|
957
|
+
display: "flex",
|
|
958
|
+
alignItems: "center",
|
|
959
|
+
gap: 8,
|
|
960
|
+
padding: "8px 0",
|
|
961
|
+
opacity: 0.8,
|
|
962
|
+
fontSize: 12,
|
|
963
|
+
}, children: [_jsx("span", { "aria-hidden": true, style: {
|
|
964
|
+
width: 14,
|
|
965
|
+
height: 14,
|
|
966
|
+
borderRadius: "50%",
|
|
967
|
+
border: "2px solid rgba(255,255,255,0.2)",
|
|
968
|
+
borderTopColor: "rgba(255,255,255,0.75)",
|
|
969
|
+
animation: "lens-pdf-tools-spin 0.85s linear infinite",
|
|
970
|
+
} }), _jsx("span", { children: "Loading tools\u2026" }), _jsx("style", { children: `@keyframes lens-pdf-tools-spin { to { transform: rotate(360deg); } }` })] })) : (leftPanelPlugins.map((plugin) => (_jsx("div", { children: plugin.render(shellPluginContext) }, plugin.id))))] })), _jsx("section", { style: {
|
|
971
|
+
...stageStyle,
|
|
972
|
+
...(isMobile
|
|
973
|
+
? {
|
|
974
|
+
padding: "12px 8px",
|
|
975
|
+
paddingBottom: "max(12px, env(safe-area-inset-bottom))",
|
|
976
|
+
gap: 8,
|
|
977
|
+
}
|
|
978
|
+
: {}),
|
|
979
|
+
}, children: !pdfUrl && embedded ? (_jsx("div", { style: emptyStateStyle, children: _jsx("p", { style: { margin: 0, opacity: 0.6 }, children: "Loading\u2026" }) })) : !pdfUrl ? (_jsxs("div", { style: emptyStateStyle, children: [effectiveLogoUrl && (_jsx("img", { src: effectiveLogoUrl, alt: effectiveLogoAlt ?? "", "aria-hidden": effectiveLogoAlt ? undefined : "true", style: { height: 64, width: "auto", maxHeight: 64, opacity: 0.85 } })), _jsxs("h2", { style: { margin: 0 }, children: [effectiveBrand, " demo viewer"] }), _jsx("p", { style: { margin: 0, maxWidth: 380 }, children: "Paste a PDF URL above or drag-and-drop a file anywhere on this page to start inspecting." }), _jsx("button", { type: "button", style: {
|
|
980
|
+
...btnStyle(tokens),
|
|
981
|
+
padding: "10px 24px",
|
|
982
|
+
fontSize: 15,
|
|
983
|
+
}, onClick: () => fileInputRef.current?.click(), children: "Choose a file" }), _jsxs("p", { style: {
|
|
984
|
+
fontSize: 11,
|
|
985
|
+
opacity: 0.55,
|
|
986
|
+
maxWidth: 460,
|
|
987
|
+
lineHeight: 1.55,
|
|
988
|
+
margin: 0,
|
|
989
|
+
}, children: ["LensPDF supports ", _jsx("strong", { children: "full CMYK + spot inks" }), " ", "with no approximation when a backend (Ghostscript / MuPDF + ICC profiles) is wired through the", " ", _jsx("code", { children: "services" }), " prop \u2014 the densitometer, TAC heatmap, and color picker read true plate values straight from the host. The RGB-derived path is only used as the fallback when no backend data is supplied, which is the mode this demo runs in. Annotations live in this tab only and are discarded on reload. Max upload ", formatMaxSize(maxFileSize), "."] })] })) : (_jsxs("div", { style: stageInnerStyle, children: [toolbarOverlayPlugins.length > 0 && (
|
|
990
|
+
// Sticky at the top of the stage scroll container on both
|
|
991
|
+
// mobile and desktop — the toolbar stays visible while the
|
|
992
|
+
// canvas scrolls, but never escapes upward into the host
|
|
993
|
+
// page's chrome (the `fixed` variant covered marketing-site
|
|
994
|
+
// nav when the viewer was mounted in `embedded` mode).
|
|
995
|
+
_jsx("div", { style: {
|
|
996
|
+
position: "sticky",
|
|
997
|
+
top: 0,
|
|
998
|
+
zIndex: 30,
|
|
999
|
+
alignSelf: "center",
|
|
1000
|
+
...(isMobile
|
|
1001
|
+
? {
|
|
1002
|
+
paddingTop: 8,
|
|
1003
|
+
maxWidth: "100%",
|
|
1004
|
+
}
|
|
1005
|
+
: {}),
|
|
1006
|
+
}, children: toolbarOverlayPlugins.map((plugin) => (_jsx("div", { children: plugin.render(shellPluginContext) }, plugin.id))) })), _jsxs("div", { style: {
|
|
1007
|
+
width: canvasW,
|
|
1008
|
+
height: canvasH,
|
|
1009
|
+
position: "relative",
|
|
1010
|
+
background: "#fff",
|
|
1011
|
+
boxShadow: "0 24px 60px rgba(0,0,0,0.55), 0 6px 18px rgba(0,0,0,0.3)",
|
|
1012
|
+
borderRadius: 4,
|
|
1013
|
+
}, children: [viewerMode === "separation" && services ? (_jsx(SeparationCanvas, { jobId: "lens-pdf-demo", pageNum: page.page_num, enabledChannels: enabledChannels, allChannels: detectedInks.length > 0
|
|
1014
|
+
? detectedInks.map((i) => i.name)
|
|
1015
|
+
: [...PROCESS_CHANNELS], width: canvasW, height: canvasH })) : viewerMode === "layer" &&
|
|
1016
|
+
services &&
|
|
1017
|
+
allLayerIndices.length > 0 &&
|
|
1018
|
+
allLayerIndices.every((layerIndex) => layerIndex !== FLATTENED_LAYER_INDEX) ? (_jsx(LayerCanvas, { jobId: "lens-pdf-demo", pageNum: page.page_num, enabledLayers: enabledLayers, allLayers: allLayerIndices, width: canvasW, height: canvasH })) : (_jsx(PageCanvas, { jobId: "lens-pdf-demo", page: page, zoom: zoom, items: overlayItems, selectedItem: effectiveSelected, onItemClick: handleItemClick, cropToTrim: cropToTrim })), viewerMode === "page" && showBoxOverlays && (_jsx(BoxOverlay, { page: page, canvasWidth: canvasW, canvasHeight: canvasH, dieline: dieline ?? null })), viewerMode === "page" && dieline && !showBoxOverlays && (_jsx(DielineOverlay, { page: page, canvasWidth: canvasW, canvasHeight: canvasH, dieline: dieline })), services && showHeatmap && (_jsx(TACHeatmapOverlay, { jobId: "lens-pdf-demo", pageNum: page.page_num, width: canvasW, height: canvasH, pageWidthPts: page.width_pts, pageHeightPts: page.height_pts, tacLimit: tacLimit })), services && showAnnotate && (_jsx("div", { ref: annotationWrapRef, style: {
|
|
1019
|
+
position: "absolute",
|
|
1020
|
+
inset: 0,
|
|
1021
|
+
pointerEvents: activeTool === "annotate" ? "auto" : "none",
|
|
1022
|
+
}, children: _jsx(AnnotationCanvas, { jobId: "lens-pdf-demo", pageNum: page.page_num, width: canvasW, height: canvasH, activeTool: annotationTool, strokeColor: strokeColor, onSavingChange: setSavingAnnotation, onHistoryChange: handleAnnotationHistoryChange, onIndexedAnnotationsChange: setIndexedAnnotations, selectedAnnotationNumber: selectedAnnotationId?.startsWith("obj-")
|
|
1023
|
+
? Number(selectedAnnotationId.slice(4))
|
|
1024
|
+
: null, onSelectedAnnotationNumberChange: (annotationNumber) => {
|
|
1025
|
+
setSelectedAnnotationId(annotationNumber != null ? `obj-${annotationNumber}` : null);
|
|
1026
|
+
} }) })), showAnnotate &&
|
|
1027
|
+
indexedAnnotations.map((row) => {
|
|
1028
|
+
const id = `obj-${row.number}`;
|
|
1029
|
+
const selected = selectedAnnotationId === id;
|
|
1030
|
+
return (_jsx("button", { type: "button", onClick: () => {
|
|
1031
|
+
setSelectedAnnotationId(id);
|
|
1032
|
+
setActiveTool("annotate");
|
|
1033
|
+
}, title: `Annotation #${row.number}`, style: {
|
|
1034
|
+
position: "absolute",
|
|
1035
|
+
left: Math.max(10, row.centerX - 12),
|
|
1036
|
+
top: Math.max(10, row.centerY - 12),
|
|
1037
|
+
width: 24,
|
|
1038
|
+
height: 24,
|
|
1039
|
+
borderRadius: "50%",
|
|
1040
|
+
border: selected
|
|
1041
|
+
? "2px solid rgba(251,191,36,0.98)"
|
|
1042
|
+
: "1px solid rgba(255,255,255,0.82)",
|
|
1043
|
+
background: selected
|
|
1044
|
+
? "rgba(251,191,36,0.95)"
|
|
1045
|
+
: "rgba(15,23,42,0.9)",
|
|
1046
|
+
color: selected ? "#111827" : "#f8fafc",
|
|
1047
|
+
fontSize: 11,
|
|
1048
|
+
fontWeight: 700,
|
|
1049
|
+
lineHeight: "24px",
|
|
1050
|
+
textAlign: "center",
|
|
1051
|
+
cursor: "pointer",
|
|
1052
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.45)",
|
|
1053
|
+
zIndex: 26,
|
|
1054
|
+
padding: 0,
|
|
1055
|
+
}, children: row.number }, id));
|
|
1056
|
+
}), activeTool === "color-picker" && (_jsx(ColorPickerTool, { jobId: "lens-pdf-demo", pageNum: page.page_num, pageWidthPts: page.width_pts, pageHeightPts: page.height_pts, canvasWidth: canvasW, canvasHeight: canvasH })), activeTool === "densitometer" && (_jsx(DensitometerTool, { jobId: "lens-pdf-demo", pageNum: page.page_num, pageWidthPts: page.width_pts, pageHeightPts: page.height_pts, canvasWidth: canvasW, canvasHeight: canvasH, tacLimit: tacLimit })), activeTool === "measure" && (_jsx(MeasureTool, { pageWidthPts: page.width_pts, pageHeightPts: page.height_pts, canvasWidth: canvasW, canvasHeight: canvasH })), preparing &&
|
|
1057
|
+
(viewerMode !== "page" || showHeatmap) && (_jsx("div", { style: preparingOverlayStyle, children: "Rasterising page & computing CMYK\u2026" }))] })] })) })] }), _jsxs("footer", { style: footerStyle(tokens), children: [_jsxs("span", { children: [effectiveBrand, " \u00B7 AGPL-3.0"] }), footer] })] }));
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
//# sourceMappingURL=LensPDFDemo.js.map
|