@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.
Files changed (213) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +344 -0
  3. package/dist/browser/codexOverlay.d.ts +109 -0
  4. package/dist/browser/codexOverlay.d.ts.map +1 -0
  5. package/dist/browser/codexOverlay.js +256 -0
  6. package/dist/browser/codexOverlay.js.map +1 -0
  7. package/dist/browser/constants.d.ts +13 -0
  8. package/dist/browser/constants.d.ts.map +1 -0
  9. package/dist/browser/constants.js +13 -0
  10. package/dist/browser/constants.js.map +1 -0
  11. package/dist/browser/index.d.ts +211 -0
  12. package/dist/browser/index.d.ts.map +1 -0
  13. package/dist/browser/index.js +1190 -0
  14. package/dist/browser/index.js.map +1 -0
  15. package/dist/browser/pantone-gold.d.ts +59 -0
  16. package/dist/browser/pantone-gold.d.ts.map +1 -0
  17. package/dist/browser/pantone-gold.js +237 -0
  18. package/dist/browser/pantone-gold.js.map +1 -0
  19. package/dist/components/AnnotationCanvas.d.ts +27 -0
  20. package/dist/components/AnnotationCanvas.d.ts.map +1 -0
  21. package/dist/components/AnnotationCanvas.js +401 -0
  22. package/dist/components/AnnotationCanvas.js.map +1 -0
  23. package/dist/components/AnnotationNotesPanel.d.ts +15 -0
  24. package/dist/components/AnnotationNotesPanel.d.ts.map +1 -0
  25. package/dist/components/AnnotationNotesPanel.js +235 -0
  26. package/dist/components/AnnotationNotesPanel.js.map +1 -0
  27. package/dist/components/AnnotationThread.d.ts +18 -0
  28. package/dist/components/AnnotationThread.d.ts.map +1 -0
  29. package/dist/components/AnnotationThread.js +163 -0
  30. package/dist/components/AnnotationThread.js.map +1 -0
  31. package/dist/components/AnnotationToolbar.d.ts +39 -0
  32. package/dist/components/AnnotationToolbar.d.ts.map +1 -0
  33. package/dist/components/AnnotationToolbar.js +258 -0
  34. package/dist/components/AnnotationToolbar.js.map +1 -0
  35. package/dist/components/BoxOverlay.d.ts +20 -0
  36. package/dist/components/BoxOverlay.d.ts.map +1 -0
  37. package/dist/components/BoxOverlay.js +107 -0
  38. package/dist/components/BoxOverlay.js.map +1 -0
  39. package/dist/components/ColorPickerTool.d.ts +11 -0
  40. package/dist/components/ColorPickerTool.d.ts.map +1 -0
  41. package/dist/components/ColorPickerTool.js +220 -0
  42. package/dist/components/ColorPickerTool.js.map +1 -0
  43. package/dist/components/DensitometerTool.d.ts +25 -0
  44. package/dist/components/DensitometerTool.d.ts.map +1 -0
  45. package/dist/components/DensitometerTool.js +246 -0
  46. package/dist/components/DensitometerTool.js.map +1 -0
  47. package/dist/components/DielineInfoPanel.d.ts +27 -0
  48. package/dist/components/DielineInfoPanel.d.ts.map +1 -0
  49. package/dist/components/DielineInfoPanel.js +23 -0
  50. package/dist/components/DielineInfoPanel.js.map +1 -0
  51. package/dist/components/DielineOverlay.d.ts +10 -0
  52. package/dist/components/DielineOverlay.d.ts.map +1 -0
  53. package/dist/components/DielineOverlay.js +57 -0
  54. package/dist/components/DielineOverlay.js.map +1 -0
  55. package/dist/components/FindingsSidebar.d.ts +50 -0
  56. package/dist/components/FindingsSidebar.d.ts.map +1 -0
  57. package/dist/components/FindingsSidebar.js +78 -0
  58. package/dist/components/FindingsSidebar.js.map +1 -0
  59. package/dist/components/LayerCanvas.d.ts +30 -0
  60. package/dist/components/LayerCanvas.d.ts.map +1 -0
  61. package/dist/components/LayerCanvas.js +84 -0
  62. package/dist/components/LayerCanvas.js.map +1 -0
  63. package/dist/components/LayerPanel.d.ts +9 -0
  64. package/dist/components/LayerPanel.d.ts.map +1 -0
  65. package/dist/components/LayerPanel.js +144 -0
  66. package/dist/components/LayerPanel.js.map +1 -0
  67. package/dist/components/LensPDF.d.ts +61 -0
  68. package/dist/components/LensPDF.d.ts.map +1 -0
  69. package/dist/components/LensPDF.js +49 -0
  70. package/dist/components/LensPDF.js.map +1 -0
  71. package/dist/components/LensPDFDemo.d.ts +160 -0
  72. package/dist/components/LensPDFDemo.d.ts.map +1 -0
  73. package/dist/components/LensPDFDemo.js +1060 -0
  74. package/dist/components/LensPDFDemo.js.map +1 -0
  75. package/dist/components/LensPDFDemo.styles.d.ts +38 -0
  76. package/dist/components/LensPDFDemo.styles.d.ts.map +1 -0
  77. package/dist/components/LensPDFDemo.styles.js +282 -0
  78. package/dist/components/LensPDFDemo.styles.js.map +1 -0
  79. package/dist/components/LensPDFViewer.d.ts +79 -0
  80. package/dist/components/LensPDFViewer.d.ts.map +1 -0
  81. package/dist/components/LensPDFViewer.js +254 -0
  82. package/dist/components/LensPDFViewer.js.map +1 -0
  83. package/dist/components/MeasureTool.d.ts +16 -0
  84. package/dist/components/MeasureTool.d.ts.map +1 -0
  85. package/dist/components/MeasureTool.js +137 -0
  86. package/dist/components/MeasureTool.js.map +1 -0
  87. package/dist/components/MobileBottomSheet.d.ts +12 -0
  88. package/dist/components/MobileBottomSheet.d.ts.map +1 -0
  89. package/dist/components/MobileBottomSheet.js +113 -0
  90. package/dist/components/MobileBottomSheet.js.map +1 -0
  91. package/dist/components/MobileDrawer.d.ts +31 -0
  92. package/dist/components/MobileDrawer.d.ts.map +1 -0
  93. package/dist/components/MobileDrawer.js +67 -0
  94. package/dist/components/MobileDrawer.js.map +1 -0
  95. package/dist/components/PageCanvas.d.ts +33 -0
  96. package/dist/components/PageCanvas.d.ts.map +1 -0
  97. package/dist/components/PageCanvas.js +385 -0
  98. package/dist/components/PageCanvas.js.map +1 -0
  99. package/dist/components/PageNavigator.d.ts +18 -0
  100. package/dist/components/PageNavigator.d.ts.map +1 -0
  101. package/dist/components/PageNavigator.js +44 -0
  102. package/dist/components/PageNavigator.js.map +1 -0
  103. package/dist/components/SeparationCanvas.d.ts +12 -0
  104. package/dist/components/SeparationCanvas.d.ts.map +1 -0
  105. package/dist/components/SeparationCanvas.js +174 -0
  106. package/dist/components/SeparationCanvas.js.map +1 -0
  107. package/dist/components/TACHeatmapOverlay.d.ts +17 -0
  108. package/dist/components/TACHeatmapOverlay.d.ts.map +1 -0
  109. package/dist/components/TACHeatmapOverlay.js +119 -0
  110. package/dist/components/TACHeatmapOverlay.js.map +1 -0
  111. package/dist/components/ZoomControls.d.ts +11 -0
  112. package/dist/components/ZoomControls.d.ts.map +1 -0
  113. package/dist/components/ZoomControls.js +26 -0
  114. package/dist/components/ZoomControls.js.map +1 -0
  115. package/dist/components/defaultShellPlugins.d.ts +3 -0
  116. package/dist/components/defaultShellPlugins.d.ts.map +1 -0
  117. package/dist/components/defaultShellPlugins.js +273 -0
  118. package/dist/components/defaultShellPlugins.js.map +1 -0
  119. package/dist/components/index.d.ts +32 -0
  120. package/dist/components/index.d.ts.map +1 -0
  121. package/dist/components/index.js +32 -0
  122. package/dist/components/index.js.map +1 -0
  123. package/dist/components/presets.d.ts +8 -0
  124. package/dist/components/presets.d.ts.map +1 -0
  125. package/dist/components/presets.js +14 -0
  126. package/dist/components/presets.js.map +1 -0
  127. package/dist/components/shellPlugins.d.ts +105 -0
  128. package/dist/components/shellPlugins.d.ts.map +1 -0
  129. package/dist/components/shellPlugins.js +52 -0
  130. package/dist/components/shellPlugins.js.map +1 -0
  131. package/dist/components/useIsMobile.d.ts +16 -0
  132. package/dist/components/useIsMobile.d.ts.map +1 -0
  133. package/dist/components/useIsMobile.js +30 -0
  134. package/dist/components/useIsMobile.js.map +1 -0
  135. package/dist/fallback-pdfjs/index.d.ts +60 -0
  136. package/dist/fallback-pdfjs/index.d.ts.map +1 -0
  137. package/dist/fallback-pdfjs/index.js +163 -0
  138. package/dist/fallback-pdfjs/index.js.map +1 -0
  139. package/dist/host/LensPDFProvider.d.ts +36 -0
  140. package/dist/host/LensPDFProvider.d.ts.map +1 -0
  141. package/dist/host/LensPDFProvider.js +12 -0
  142. package/dist/host/LensPDFProvider.js.map +1 -0
  143. package/dist/host/index.d.ts +167 -0
  144. package/dist/host/index.d.ts.map +1 -0
  145. package/dist/host/index.js +173 -0
  146. package/dist/host/index.js.map +1 -0
  147. package/dist/host/pdfFallback.d.ts +50 -0
  148. package/dist/host/pdfFallback.d.ts.map +1 -0
  149. package/dist/host/pdfFallback.js +171 -0
  150. package/dist/host/pdfFallback.js.map +1 -0
  151. package/dist/host/pdfValidation.d.ts +45 -0
  152. package/dist/host/pdfValidation.d.ts.map +1 -0
  153. package/dist/host/pdfValidation.js +78 -0
  154. package/dist/host/pdfValidation.js.map +1 -0
  155. package/dist/host/shareLink.d.ts +80 -0
  156. package/dist/host/shareLink.d.ts.map +1 -0
  157. package/dist/host/shareLink.js +114 -0
  158. package/dist/host/shareLink.js.map +1 -0
  159. package/dist/host/useLensPDF.d.ts +73 -0
  160. package/dist/host/useLensPDF.d.ts.map +1 -0
  161. package/dist/host/useLensPDF.js +213 -0
  162. package/dist/host/useLensPDF.js.map +1 -0
  163. package/dist/index.d.ts +68 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +62 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/plugin/context.d.ts +70 -0
  168. package/dist/plugin/context.d.ts.map +1 -0
  169. package/dist/plugin/context.js +16 -0
  170. package/dist/plugin/context.js.map +1 -0
  171. package/dist/plugin/findings-location.d.ts +53 -0
  172. package/dist/plugin/findings-location.d.ts.map +1 -0
  173. package/dist/plugin/findings-location.js +72 -0
  174. package/dist/plugin/findings-location.js.map +1 -0
  175. package/dist/plugin/index.d.ts +19 -0
  176. package/dist/plugin/index.d.ts.map +1 -0
  177. package/dist/plugin/index.js +16 -0
  178. package/dist/plugin/index.js.map +1 -0
  179. package/dist/plugin/registry.d.ts +61 -0
  180. package/dist/plugin/registry.d.ts.map +1 -0
  181. package/dist/plugin/registry.js +102 -0
  182. package/dist/plugin/registry.js.map +1 -0
  183. package/dist/plugin/services.d.ts +380 -0
  184. package/dist/plugin/services.d.ts.map +1 -0
  185. package/dist/plugin/services.js +104 -0
  186. package/dist/plugin/services.js.map +1 -0
  187. package/dist/plugin/types.d.ts +198 -0
  188. package/dist/plugin/types.d.ts.map +1 -0
  189. package/dist/plugin/types.js +24 -0
  190. package/dist/plugin/types.js.map +1 -0
  191. package/dist/types/index.d.ts +191 -0
  192. package/dist/types/index.d.ts.map +1 -0
  193. package/dist/types/index.js +95 -0
  194. package/dist/types/index.js.map +1 -0
  195. package/dist/units/index.d.ts +64 -0
  196. package/dist/units/index.d.ts.map +1 -0
  197. package/dist/units/index.js +98 -0
  198. package/dist/units/index.js.map +1 -0
  199. package/docs/architecture.md +90 -0
  200. package/docs/components.md +569 -0
  201. package/docs/contributing.md +78 -0
  202. package/docs/fallback.md +174 -0
  203. package/docs/lens-pdf-viewer.md +128 -0
  204. package/docs/licensing.md +78 -0
  205. package/docs/measurement-units.md +87 -0
  206. package/docs/plugins.md +256 -0
  207. package/docs/security.md +69 -0
  208. package/docs/server.md +212 -0
  209. package/docs/services.md +210 -0
  210. package/docs/share-links.md +111 -0
  211. package/docs/theming.md +164 -0
  212. package/docs/validation.md +83 -0
  213. 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