@rightkit/document-core 0.1.2 → 0.2.0
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/CHANGELOG.md +23 -0
- package/README.md +31 -7
- package/dist/app/chrome.js +1 -1
- package/dist/app/documentPrinting.d.ts +0 -6
- package/dist/app/documentPrinting.js +0 -18
- package/dist/app/shell/nativeMenu.d.ts +3 -2
- package/dist/app/shell/nativeMenu.js +8 -5
- package/dist/documents/perfEventCounters.d.ts +0 -8
- package/dist/documents/perfEventCounters.js +2 -1
- package/dist/documents/preview/previewFrame.d.ts +2 -1
- package/dist/documents/preview/previewFrame.js +9 -7
- package/dist/documents/searchUtils.js +1 -1
- package/dist/host.d.ts +36 -0
- package/dist/host.js +54 -0
- package/dist/index.d.ts +1 -46
- package/dist/index.js +1 -46
- package/dist/settings/themes.js +1 -1
- package/dist/state/bridgeEvents.d.ts +1 -1
- package/dist/state/bridgeEvents.js +7 -4
- package/dist/state/recentUrls.js +4 -3
- package/dist/state/tauriBridge.d.ts +3 -47
- package/dist/state/tauriBridge.js +6 -162
- package/dist/styles.css +2 -2
- package/package.json +16 -87
- package/dist/app/shortcutChords.d.ts +0 -6
- package/dist/app/shortcutChords.js +0 -14
- package/dist/app/useCloseFindOnSurfaceChange.d.ts +0 -14
- package/dist/app/useCloseFindOnSurfaceChange.js +0 -23
- package/dist/app/useEditText.d.ts +0 -6
- package/dist/app/useEditText.js +0 -10
- package/dist/appName.d.ts +0 -2
- package/dist/appName.js +0 -8
- package/dist/documents/FullscreenIcon.d.ts +0 -4
- package/dist/documents/FullscreenIcon.js +0 -7
- package/dist/documents/mdx/mdxRuntimeScript.d.ts +0 -2
- package/dist/documents/mdx/mdxRuntimeScript.js +0 -3
- package/dist/documents/mdx/mdxRuntimeScript.txt +0 -584
- package/dist/documents/pdf/MacPdfReadPane.d.ts +0 -23
- package/dist/documents/pdf/MacPdfReadPane.js +0 -708
- package/dist/documents/pdf/NativePdfDocument.d.ts +0 -9
- package/dist/documents/pdf/NativePdfDocument.js +0 -41
- package/dist/documents/pdf/PdfInlineTextEditor.d.ts +0 -53
- package/dist/documents/pdf/PdfInlineTextEditor.js +0 -225
- package/dist/documents/pdf/PdfOcrLayer.d.ts +0 -4
- package/dist/documents/pdf/PdfOcrLayer.js +0 -30
- package/dist/documents/pdf/PdfOutline.d.ts +0 -6
- package/dist/documents/pdf/PdfOutline.js +0 -12
- package/dist/documents/pdf/PdfPasswordPrompt.d.ts +0 -5
- package/dist/documents/pdf/PdfPasswordPrompt.js +0 -24
- package/dist/documents/pdf/PdfThumbnails.d.ts +0 -8
- package/dist/documents/pdf/PdfThumbnails.js +0 -201
- package/dist/documents/pdf/PdfToolbar.d.ts +0 -47
- package/dist/documents/pdf/PdfToolbar.js +0 -76
- package/dist/documents/pdf/PdfViewer.d.ts +0 -36
- package/dist/documents/pdf/PdfViewer.js +0 -1522
- package/dist/documents/pdf/PdfViewerBody.d.ts +0 -22
- package/dist/documents/pdf/PdfViewerBody.js +0 -42
- package/dist/documents/pdf/macPdfNativeBridge.d.ts +0 -118
- package/dist/documents/pdf/macPdfNativeBridge.js +0 -134
- package/dist/documents/pdf/nearestVisiblePdfPage.d.ts +0 -2
- package/dist/documents/pdf/nearestVisiblePdfPage.js +0 -22
- package/dist/documents/pdf/pdfBench.d.ts +0 -88
- package/dist/documents/pdf/pdfBench.js +0 -535
- package/dist/documents/pdf/pdfBenchAutopilot.d.ts +0 -48
- package/dist/documents/pdf/pdfBenchAutopilot.js +0 -154
- package/dist/documents/pdf/pdfBenchTransport.d.ts +0 -21
- package/dist/documents/pdf/pdfBenchTransport.js +0 -233
- package/dist/documents/pdf/pdfEditModel.d.ts +0 -294
- package/dist/documents/pdf/pdfEditModel.js +0 -105
- package/dist/documents/pdf/pdfEditor.d.ts +0 -100
- package/dist/documents/pdf/pdfEditor.js +0 -68
- package/dist/documents/pdf/pdfOcr.d.ts +0 -36
- package/dist/documents/pdf/pdfOcr.js +0 -88
- package/dist/documents/pdf/pdfOcrEdit.d.ts +0 -32
- package/dist/documents/pdf/pdfOcrEdit.js +0 -138
- package/dist/documents/pdf/pdfPrinting.d.ts +0 -28
- package/dist/documents/pdf/pdfPrinting.js +0 -110
- package/dist/documents/pdf/pdfSave.d.ts +0 -87
- package/dist/documents/pdf/pdfSave.js +0 -250
- package/dist/documents/pdf/pdfShared.d.ts +0 -16
- package/dist/documents/pdf/pdfShared.js +0 -20
- package/dist/documents/pdf/pdfToMarkdown.d.ts +0 -11
- package/dist/documents/pdf/pdfToMarkdown.js +0 -67
- package/dist/documents/pdf/pdfjsLibGlobal.d.ts +0 -1
- package/dist/documents/pdf/pdfjsLibGlobal.js +0 -12
- package/dist/documents/pdf/toolbarIcons.d.ts +0 -18
- package/dist/documents/pdf/toolbarIcons.js +0 -31
- package/dist/documents/pdf/usePdfDocumentLoader.d.ts +0 -40
- package/dist/documents/pdf/usePdfDocumentLoader.js +0 -243
- package/dist/documents/pdf/usePdfEditSession.d.ts +0 -88
- package/dist/documents/pdf/usePdfEditSession.js +0 -341
- package/dist/documents/pdf/usePdfEditor.d.ts +0 -59
- package/dist/documents/pdf/usePdfEditor.js +0 -317
- package/dist/documents/pdf/usePdfFit.d.ts +0 -19
- package/dist/documents/pdf/usePdfFit.js +0 -55
- package/dist/documents/pdf/usePdfOcr.d.ts +0 -38
- package/dist/documents/pdf/usePdfOcr.js +0 -337
- package/dist/documents/pdf/usePdfPreviewRecompose.d.ts +0 -28
- package/dist/documents/pdf/usePdfPreviewRecompose.js +0 -256
- package/dist/documents/pdf/usePdfSavePlan.d.ts +0 -61
- package/dist/documents/pdf/usePdfSavePlan.js +0 -287
- package/dist/documents/pdf/usePdfSearch.d.ts +0 -31
- package/dist/documents/pdf/usePdfSearch.js +0 -367
- package/dist/documents/pdf/usePdfViewControls.d.ts +0 -72
- package/dist/documents/pdf/usePdfViewControls.js +0 -152
- package/dist/documents/scopedSelectAll.d.ts +0 -5
- package/dist/documents/scopedSelectAll.js +0 -77
- package/dist/documents/txt/cmScroll.d.ts +0 -11
- package/dist/documents/txt/cmScroll.js +0 -52
- package/dist/documents/useMirroredEditText.d.ts +0 -15
- package/dist/documents/useMirroredEditText.js +0 -29
- package/dist/documents/useSplitResize.d.ts +0 -7
- package/dist/documents/useSplitResize.js +0 -37
- package/dist/settings/rk-settings.css +0 -38
- package/dist/settings/sections/EditorSection.d.ts +0 -1
- package/dist/settings/sections/EditorSection.js +0 -18
- package/dist/settings/sections/FileAssociationsSection.d.ts +0 -1
- package/dist/settings/sections/FileAssociationsSection.js +0 -5
- package/dist/settings/sections/MacFileAssociationsBlock.d.ts +0 -33
- package/dist/settings/sections/MacFileAssociationsBlock.js +0 -151
- package/dist/settings/sections/ThemeSection.d.ts +0 -1
- package/dist/settings/sections/ThemeSection.js +0 -31
- package/dist/settings/sections/TypographySection.d.ts +0 -1
- package/dist/settings/sections/TypographySection.js +0 -19
- package/dist/settings/sections/onOff.d.ts +0 -11
- package/dist/settings/sections/onOff.js +0 -8
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type ComponentProps } from "react";
|
|
2
|
-
import type { PdfViewer as LegacyPdfViewer } from "./PdfViewer.js";
|
|
3
|
-
export declare function supportsNativePdfReading(): boolean;
|
|
4
|
-
type Props = ComponentProps<typeof LegacyPdfViewer>;
|
|
5
|
-
/** Native reading & PDF.js editing have separate document lifetimes. Once editing starts, keep
|
|
6
|
-
* its renderer mounted so annotations, source commands & undo history retain their existing owner.
|
|
7
|
-
*/
|
|
8
|
-
export declare function NativePdfDocument(props: Props): import("react").JSX.Element;
|
|
9
|
-
export {};
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { lazy, Suspense, useCallback, useEffect, useRef, useState, } from "react";
|
|
3
|
-
import { isMac } from "../../platform.js";
|
|
4
|
-
import { logError } from "../../state/diagnostics.js";
|
|
5
|
-
const MacPdfReadPane = lazy(() => import("./MacPdfReadPane.js").then((module) => ({ default: module.MacPdfReadPane })));
|
|
6
|
-
const PdfViewer = lazy(() => import("./PdfViewer.js").then((module) => ({ default: module.PdfViewer })));
|
|
7
|
-
export function supportsNativePdfReading() {
|
|
8
|
-
return isMac() && typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
|
9
|
-
}
|
|
10
|
-
/** Native reading & PDF.js editing have separate document lifetimes. Once editing starts, keep
|
|
11
|
-
* its renderer mounted so annotations, source commands & undo history retain their existing owner.
|
|
12
|
-
*/
|
|
13
|
-
export function NativePdfDocument(props) {
|
|
14
|
-
const readOnly = props.readOnly ?? true;
|
|
15
|
-
const [legacy, setLegacy] = useState(() => !supportsNativePdfReading());
|
|
16
|
-
const [editingAction, setEditingAction] = useState();
|
|
17
|
-
// Wave 2: Markup hands off to the PDF.js editor permanently for this document — the editor's
|
|
18
|
-
// document lifetime rule (annotations, source commands & undo keep their existing owner) makes
|
|
19
|
-
// a return to the native reader destructive, so Read after Markup renders the same editor in
|
|
20
|
-
// its read-only mode instead of remounting PDFKit.
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (!readOnly)
|
|
23
|
-
setLegacy(true);
|
|
24
|
-
}, [readOnly]);
|
|
25
|
-
const showNative = !legacy && readOnly;
|
|
26
|
-
const viewRef = useRef(props.initialView);
|
|
27
|
-
const onViewState = useCallback((view) => {
|
|
28
|
-
viewRef.current = view;
|
|
29
|
-
props.onViewState?.(view);
|
|
30
|
-
}, [props.onViewState]);
|
|
31
|
-
const requestEditing = useCallback((action, view) => {
|
|
32
|
-
viewRef.current = view;
|
|
33
|
-
setEditingAction(action);
|
|
34
|
-
setLegacy(true);
|
|
35
|
-
}, []);
|
|
36
|
-
const fallback = useCallback(() => {
|
|
37
|
-
logError("pdf:native-reading-failed");
|
|
38
|
-
setLegacy(true);
|
|
39
|
-
}, []);
|
|
40
|
-
return (_jsx(Suspense, { fallback: _jsx("div", { className: "flex h-full items-center justify-center text-sm text-muted", children: "Loading PDF\u2026" }), children: showNative ? (_jsx(MacPdfReadPane, { documentPath: props.documentPath, url: props.url, active: props.active, initialView: viewRef.current, onViewState: onViewState, registerPrint: props.registerPrint, registerSearchProvider: props.registerSearchProvider, findBar: props.findBar, findMode: props.findMode, onRequestFind: props.onRequestFind, onRequestReplace: props.onRequestReplace, onRequestEditing: requestEditing, onRequestFallback: fallback, readOnly: true }, props.url ?? props.documentPath)) : (_jsx(PdfViewer, { ...props, readOnly: readOnly, initialView: viewRef.current, initialEditingAction: editingAction, onViewState: onViewState })) }));
|
|
41
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
-
import type { PdfColor, PdfRect, PdfTextDraft } from "./pdfEditModel.js";
|
|
3
|
-
/** Default CSS px per PDF user-space unit when the caller does not supply the live viewport scale:
|
|
4
|
-
* 96dpi CSS px over 72dpi PDF points, i.e. a viewer at 100% with pdf.js's PDF_TO_CSS_UNITS. */
|
|
5
|
-
export declare const DEFAULT_CSS_PER_PDF_UNIT: number;
|
|
6
|
-
export interface PdfInlineTextEditorProps {
|
|
7
|
-
/** The in-progress draft being edited (existing-island `sourceEdit` or fresh `addText`). */
|
|
8
|
-
draft: PdfTextDraft;
|
|
9
|
-
/** Live CSS px per PDF user-space unit for the draft's page — `PageViewport.scale` from the same
|
|
10
|
-
* viewport `rect` was converted with. Without it the editor's glyphs render at a fixed 100% size
|
|
11
|
-
* while the page underneath is zoomed, which is exactly the size mismatch users saw. */
|
|
12
|
-
cssPerPdfUnit?: number;
|
|
13
|
-
/** Background painted behind the editable so the ORIGINAL glyphs (still on the canvas underneath
|
|
14
|
-
* for a `sourceEdit` draft) don't show through the text being typed over them. The caller
|
|
15
|
-
* samples this from the page canvas (see `sampleCanvasRectBackground`); `null`/undefined leaves
|
|
16
|
-
* the editor transparent (right for `addText` over empty page space). */
|
|
17
|
-
coverColor?: PdfColor | null;
|
|
18
|
-
/** Screen-space (CSS pixel) rect for this draft, already converted from PDF-space via
|
|
19
|
-
* `pdfRectToViewport` by the caller — see judgment call 1 above. Reuses the frozen `PdfRect`
|
|
20
|
-
* field shape; unit is CSS px here, not PDF user-space units. */
|
|
21
|
-
rect: PdfRect;
|
|
22
|
-
/** Fired on every keystroke with the editable's current plain-text content, so the caller can
|
|
23
|
-
* keep `session.draft.text` in sync (`usePdfEditSession.ts`'s `updateDraftText`). */
|
|
24
|
-
onTextChange: (text: string) => void;
|
|
25
|
-
/** §3.2: Enter, click-away (blur), or an external "commit the active draft" call (Done, page
|
|
26
|
-
* switch, another selection) should all resolve here. Zero-arg to match §6.1's
|
|
27
|
-
* `onCommit={commitDraft}` usage exactly. */
|
|
28
|
-
onCommit: () => void;
|
|
29
|
-
/** §3.2 Escape: "discards active draft without a history command." Zero-arg to match §6.1's
|
|
30
|
-
* `onCancel={cancelDraft}` usage exactly. */
|
|
31
|
-
onCancel: () => void;
|
|
32
|
-
/** §3.3: fires whenever the measured overflow state changes, so the caller can mirror it onto
|
|
33
|
-
* `session.draft.overflow` via `setDraftOverflow`. Optional because a caller doing its own
|
|
34
|
-
* layout measurement elsewhere is not required to wire this. */
|
|
35
|
-
onOverflowChange?: (overflow: boolean) => void;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Maps a PDF font resource name to CSS font properties. PDF names are not CSS families: embedded
|
|
39
|
-
* subsets arrive as `ABCDEF+SourceSerif4-Bold`, base-14 fonts as `Helvetica-BoldOblique`, and
|
|
40
|
-
* TrueType names as `Arial,BoldItalic`. Handing those to `font-family` verbatim falls back to the
|
|
41
|
-
* browser default (a serif), which is the wrong-face rendering users saw over sans-serif source
|
|
42
|
-
* text. The style suffix is peeled into weight/style, and the family gets a generic fallback
|
|
43
|
-
* chosen from the base name so an unknown embedded face at least lands in the right class.
|
|
44
|
-
*/
|
|
45
|
-
export declare function pdfFontToCss(family: string): Pick<CSSProperties, "fontFamily" | "fontWeight" | "fontStyle">;
|
|
46
|
-
/**
|
|
47
|
-
* Page-slot inline editor for one text draft. Uses a `contenteditable` div (§12's
|
|
48
|
-
* "contenteditable/input") rather than an `<input>`/`<textarea>` because the draft's appearance
|
|
49
|
-
* (arbitrary embedded font, PDF point size) must render exactly as it will in the composed
|
|
50
|
-
* candidate, and a plain form control cannot host that plus multi-run styling if it is extended
|
|
51
|
-
* later; a single style span is sufficient for the current single-appearance-per-draft contract.
|
|
52
|
-
*/
|
|
53
|
-
export declare function PdfInlineTextEditor(props: PdfInlineTextEditorProps): ReactNode;
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Page-slot inline text editor (docs/PDF-EDITOR-NOW.md §6.1, §12
|
|
3
|
-
// "src/documents/pdf/PdfInlineTextEditor.tsx | Page-slot contenteditable/input, click-away,
|
|
4
|
-
// Escape, composition, overflow").
|
|
5
|
-
//
|
|
6
|
-
// Renders inside the same absolute page slot as the canvas/text/annotation layers, positioned at
|
|
7
|
-
// a caller-supplied screen-space rect, editing one `PdfTextDraft` (either an existing-island
|
|
8
|
-
// `sourceEdit` draft or a fresh `addText` draft; see `pdfEditModel.ts`, frozen).
|
|
9
|
-
//
|
|
10
|
-
// Prop-contract judgment calls (pdfEditModel.ts is frozen and defines neither a screen-rect type
|
|
11
|
-
// nor this component's props, so these are resolved here, not invented there):
|
|
12
|
-
//
|
|
13
|
-
// 1. Screen-space rect: `PdfTextIsland`/`PdfTextDraft` carry only PDF-space geometry (`PdfRect`,
|
|
14
|
-
// §3.2's `pdfRectToViewport(session.draft.rect, viewport)`). The caller (PdfViewer) already
|
|
15
|
-
// owns the viewport and must do that conversion; this component receives the already-converted
|
|
16
|
-
// rect as plain CSS pixels. Reusing the frozen `PdfRect` shape for it (rather than inventing a
|
|
17
|
-
// `PdfViewportRect`) keeps the field names (`x`, `y`, `width`, `height`) consistent with the
|
|
18
|
-
// rest of the contract — only the unit (CSS px vs PDF user-space units) differs, and that unit
|
|
19
|
-
// is documented on the prop itself.
|
|
20
|
-
// 2. `onCommit`/`onCancel` are zero-argument, matching the exact §6.1 usage snippet
|
|
21
|
-
// (`onCommit={commitDraft}`, `onCancel={cancelDraft}`, both zero-arg methods on
|
|
22
|
-
// `PdfEditSessionController`). Live text edits are not part of that snippet's shape, so they are
|
|
23
|
-
// threaded back through a separate `onTextChange(text)` prop (wired by the caller to
|
|
24
|
-
// `session.updateDraftText`) rather than overloading `onCommit` with an argument that would
|
|
25
|
-
// break the documented call site.
|
|
26
|
-
// 3. Overflow: `pdfEditModel.ts`'s `PdfTextDraft.overflow` is the authoritative flag (read by Save
|
|
27
|
-
// to disable that command, §3.3), and `usePdfEditSession.ts` exposes a matching zero-arg-return
|
|
28
|
-
// setter `setDraftOverflow(overflow: boolean)`. This component does not call that setter
|
|
29
|
-
// directly (it has no session reference) — it reports the same boolean shape via an
|
|
30
|
-
// `onOverflowChange(overflow: boolean)` prop so the caller can wire
|
|
31
|
-
// `onOverflowChange={session.setDraftOverflow}` verbatim. Overflow is measured against the
|
|
32
|
-
// caller-supplied screen rect (the on-screen box `availableBounds` maps to), comparing the
|
|
33
|
-
// editable element's scrollWidth/scrollHeight to its clientWidth/clientHeight — the same
|
|
34
|
-
// "does the content option outgrow its box" signal `validate_layout` (§8) checks in PDF space.
|
|
35
|
-
//
|
|
36
|
-
// Appearance -> CSS conversion: `PdfTextAppearance.sizePt` is in PDF points (1/72in); CSS px
|
|
37
|
-
// assumes 96dpi, so `px = pt * 96/72`. `color` fields are 0-1 floats (`{r,g,b,alpha}`); CSS wants
|
|
38
|
-
// 0-255 integers for r/g/b and 0-1 for alpha, so only r/g/b are scaled.
|
|
39
|
-
import { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react";
|
|
40
|
-
/** Default CSS px per PDF user-space unit when the caller does not supply the live viewport scale:
|
|
41
|
-
* 96dpi CSS px over 72dpi PDF points, i.e. a viewer at 100% with pdf.js's PDF_TO_CSS_UNITS. */
|
|
42
|
-
export const DEFAULT_CSS_PER_PDF_UNIT = 96 / 72;
|
|
43
|
-
/**
|
|
44
|
-
* Maps a PDF font resource name to CSS font properties. PDF names are not CSS families: embedded
|
|
45
|
-
* subsets arrive as `ABCDEF+SourceSerif4-Bold`, base-14 fonts as `Helvetica-BoldOblique`, and
|
|
46
|
-
* TrueType names as `Arial,BoldItalic`. Handing those to `font-family` verbatim falls back to the
|
|
47
|
-
* browser default (a serif), which is the wrong-face rendering users saw over sans-serif source
|
|
48
|
-
* text. The style suffix is peeled into weight/style, and the family gets a generic fallback
|
|
49
|
-
* chosen from the base name so an unknown embedded face at least lands in the right class.
|
|
50
|
-
*/
|
|
51
|
-
export function pdfFontToCss(family) {
|
|
52
|
-
const trimmed = family.trim().replace(/^[A-Z]{6}\+/, "");
|
|
53
|
-
const [rawBase, ...suffixParts] = trimmed.split(/[-,]/);
|
|
54
|
-
const styleText = `${suffixParts.join(" ")} ${rawBase}`.toLowerCase();
|
|
55
|
-
const bold = /bold|black|heavy|semibold|demibold/.test(styleText);
|
|
56
|
-
const italic = /italic|oblique/.test(styleText);
|
|
57
|
-
// Split CamelCase / digits so "SourceSerif4" becomes a name a CSS lookup can match ("Source
|
|
58
|
-
// Serif 4") when that font is installed or bundled.
|
|
59
|
-
const base = rawBase
|
|
60
|
-
.replace(/([a-z])([A-Z0-9])/g, "$1 $2")
|
|
61
|
-
.replace(/([A-Za-z])(\d)/g, "$1 $2")
|
|
62
|
-
.replace(/\s+/g, " ")
|
|
63
|
-
.trim();
|
|
64
|
-
const lower = base.toLowerCase();
|
|
65
|
-
let generic = "sans-serif";
|
|
66
|
-
if (/courier|mono|consol|menlo|monaco/.test(lower))
|
|
67
|
-
generic = "monospace";
|
|
68
|
-
else if (/times|serif|georgia|garamond|book|roman|cambria|palatino|minion|baskerville|caslon/.test(lower) &&
|
|
69
|
-
!/sans/.test(lower)) {
|
|
70
|
-
generic = "serif";
|
|
71
|
-
}
|
|
72
|
-
const fontFamily = base ? `"${base}", ${generic}` : generic;
|
|
73
|
-
return {
|
|
74
|
-
fontFamily,
|
|
75
|
-
fontWeight: bold ? 700 : 400,
|
|
76
|
-
fontStyle: italic ? "italic" : "normal",
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function appearanceToStyle(appearance, rect, cssPerPdfUnit, coverColor) {
|
|
80
|
-
const { r, g, b, alpha } = appearance.color;
|
|
81
|
-
const cover = coverColor
|
|
82
|
-
? `rgba(${Math.round(coverColor.r * 255)}, ${Math.round(coverColor.g * 255)}, ${Math.round(coverColor.b * 255)}, ${coverColor.alpha})`
|
|
83
|
-
: "transparent";
|
|
84
|
-
return {
|
|
85
|
-
position: "absolute",
|
|
86
|
-
left: rect.x,
|
|
87
|
-
top: rect.y,
|
|
88
|
-
width: rect.width,
|
|
89
|
-
height: rect.height,
|
|
90
|
-
...pdfFontToCss(appearance.family),
|
|
91
|
-
fontSize: `${appearance.sizePt * cssPerPdfUnit}px`,
|
|
92
|
-
color: `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`,
|
|
93
|
-
// The rect is the island's glyph box (ascender to descender), so centering one line in it
|
|
94
|
-
// lands the glyphs where the source run sits far better than lineHeight:1 at the box top did.
|
|
95
|
-
lineHeight: `${rect.height}px`,
|
|
96
|
-
whiteSpace: "pre",
|
|
97
|
-
overflow: "visible",
|
|
98
|
-
outline: "none",
|
|
99
|
-
background: cover,
|
|
100
|
-
border: "none",
|
|
101
|
-
padding: 0,
|
|
102
|
-
margin: 0,
|
|
103
|
-
cursor: "text",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Page-slot inline editor for one text draft. Uses a `contenteditable` div (§12's
|
|
108
|
-
* "contenteditable/input") rather than an `<input>`/`<textarea>` because the draft's appearance
|
|
109
|
-
* (arbitrary embedded font, PDF point size) must render exactly as it will in the composed
|
|
110
|
-
* candidate, and a plain form control cannot host that plus multi-run styling if it is extended
|
|
111
|
-
* later; a single style span is sufficient for the current single-appearance-per-draft contract.
|
|
112
|
-
*/
|
|
113
|
-
export function PdfInlineTextEditor(props) {
|
|
114
|
-
const { draft, rect, onTextChange, onCommit, onCancel, onOverflowChange } = props;
|
|
115
|
-
const editableRef = useRef(null);
|
|
116
|
-
const composingRef = useRef(false);
|
|
117
|
-
const overflowRef = useRef(false);
|
|
118
|
-
const [overflowed, setOverflowed] = useState(draft.overflow);
|
|
119
|
-
// Reading the latest callbacks from refs (rather than depending on them in effects/handlers)
|
|
120
|
-
// avoids re-binding DOM listeners or re-running layout measurement solely because the caller
|
|
121
|
-
// passed a new function identity, and, for onCommit/onCancel specifically, guarantees the
|
|
122
|
-
// handler that actually runs at blur/keydown time is not a stale closure over an earlier draft.
|
|
123
|
-
const onTextChangeRef = useRef(onTextChange);
|
|
124
|
-
onTextChangeRef.current = onTextChange;
|
|
125
|
-
const onCommitRef = useRef(onCommit);
|
|
126
|
-
onCommitRef.current = onCommit;
|
|
127
|
-
const onCancelRef = useRef(onCancel);
|
|
128
|
-
onCancelRef.current = onCancel;
|
|
129
|
-
const onOverflowChangeRef = useRef(onOverflowChange);
|
|
130
|
-
onOverflowChangeRef.current = onOverflowChange;
|
|
131
|
-
// Seed / re-seed DOM content when the draft identity changes (new draft started), not on every
|
|
132
|
-
// keystroke — otherwise typing would fight the controlled `draft.text` echoed back a render
|
|
133
|
-
// later and the caret would jump to the end on every character.
|
|
134
|
-
const draftIdRef = useRef(null);
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
const el = editableRef.current;
|
|
137
|
-
if (!el)
|
|
138
|
-
return;
|
|
139
|
-
if (draftIdRef.current === draft.id)
|
|
140
|
-
return;
|
|
141
|
-
draftIdRef.current = draft.id;
|
|
142
|
-
overflowRef.current = draft.overflow;
|
|
143
|
-
setOverflowed(draft.overflow);
|
|
144
|
-
el.textContent = draft.text;
|
|
145
|
-
el.focus();
|
|
146
|
-
// Place caret at end of content.
|
|
147
|
-
const selection = window.getSelection();
|
|
148
|
-
if (selection) {
|
|
149
|
-
const range = document.createRange();
|
|
150
|
-
range.selectNodeContents(el);
|
|
151
|
-
range.collapse(false);
|
|
152
|
-
selection.removeAllRanges();
|
|
153
|
-
selection.addRange(range);
|
|
154
|
-
}
|
|
155
|
-
}, [draft.id, draft.text, draft.overflow]);
|
|
156
|
-
const draftRef = useRef(draft);
|
|
157
|
-
draftRef.current = draft;
|
|
158
|
-
const measureOverflow = useCallback(() => {
|
|
159
|
-
const el = editableRef.current;
|
|
160
|
-
if (!el)
|
|
161
|
-
return;
|
|
162
|
-
const current = draftRef.current;
|
|
163
|
-
// Unchanged text can't overflow the run it came from: the CSS fallback face is never a
|
|
164
|
-
// metric-exact match for the embedded PDF font, so measuring the untouched original against
|
|
165
|
-
// the source glyph box produced a false "Text exceeds available space" the moment the editor
|
|
166
|
-
// opened. Only text the user actually changed is measured, with a 1px rounding allowance.
|
|
167
|
-
const unchanged = current.originalText !== null && current.text === current.originalText;
|
|
168
|
-
const overflow = !unchanged && (el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1);
|
|
169
|
-
if (overflow !== overflowRef.current) {
|
|
170
|
-
overflowRef.current = overflow;
|
|
171
|
-
setOverflowed(overflow);
|
|
172
|
-
onOverflowChangeRef.current?.(overflow);
|
|
173
|
-
}
|
|
174
|
-
}, []);
|
|
175
|
-
// Re-measure whenever text, appearance, or the available rect changes — all three affect
|
|
176
|
-
// whether content fits (§3.3: overflow is checked against `availableBounds`, and appearance
|
|
177
|
-
// changes come from the toolbar while this element stays mounted). measureOverflow itself reads
|
|
178
|
-
// editableRef's live DOM measurements, not these values directly, but they must stay listed so
|
|
179
|
-
// the effect actually re-runs when any of them changes.
|
|
180
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: see comment above.
|
|
181
|
-
useLayoutEffect(() => {
|
|
182
|
-
measureOverflow();
|
|
183
|
-
}, [draft.text, draft.appearance, rect.width, rect.height, measureOverflow]);
|
|
184
|
-
const handleInput = useCallback(() => {
|
|
185
|
-
const el = editableRef.current;
|
|
186
|
-
if (!el)
|
|
187
|
-
return;
|
|
188
|
-
onTextChangeRef.current(el.textContent ?? "");
|
|
189
|
-
}, []);
|
|
190
|
-
const handleCompositionStart = useCallback((_event) => {
|
|
191
|
-
composingRef.current = true;
|
|
192
|
-
}, []);
|
|
193
|
-
const handleCompositionEnd = useCallback((event) => {
|
|
194
|
-
composingRef.current = false;
|
|
195
|
-
onTextChangeRef.current(event.currentTarget.textContent ?? "");
|
|
196
|
-
}, []);
|
|
197
|
-
const handleKeyDown = useCallback((event) => {
|
|
198
|
-
// While an IME composition is active, Enter/Escape belong to the IME (candidate selection),
|
|
199
|
-
// not to this editor's commit/cancel — committing mid-composition would persist an
|
|
200
|
-
// intermediate, not-yet-confirmed string. `event.nativeEvent.isComposing` covers the case
|
|
201
|
-
// some browsers still report `keyCode === 229` for; jsdom user-event does not set that, so
|
|
202
|
-
// the `composingRef` flag driven by our own composition handlers is the reliable signal here.
|
|
203
|
-
if (composingRef.current || event.nativeEvent.isComposing)
|
|
204
|
-
return;
|
|
205
|
-
if (event.key === "Enter") {
|
|
206
|
-
event.preventDefault();
|
|
207
|
-
onCommitRef.current();
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
if (event.key === "Escape") {
|
|
211
|
-
event.preventDefault();
|
|
212
|
-
onCancelRef.current();
|
|
213
|
-
}
|
|
214
|
-
}, []);
|
|
215
|
-
// §3.2: "Clicking outside active text commits valid draft." Blur is the DOM signal for
|
|
216
|
-
// click-away (or focus moving to the toolbar, which per §3.2 stays "inside edit interaction
|
|
217
|
-
// scope" — that exemption is the caller's responsibility via focus management / event ordering
|
|
218
|
-
// in PdfViewer.tsx, not something this leaf component can see).
|
|
219
|
-
const handleBlur = useCallback(() => {
|
|
220
|
-
if (composingRef.current)
|
|
221
|
-
return;
|
|
222
|
-
onCommitRef.current();
|
|
223
|
-
}, []);
|
|
224
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: editableRef, role: "textbox", "aria-label": `Edit PDF text on page ${draft.page}`, "aria-multiline": "false", "aria-invalid": overflowed || undefined, "aria-describedby": overflowed ? `pdf-inline-text-editor-feedback-${draft.id}` : undefined, tabIndex: 0, contentEditable: true, suppressContentEditableWarning: true, "data-pdf-inline-text-editor": "", "data-draft-kind": draft.kind, "data-draft-id": draft.id, style: appearanceToStyle(draft.appearance, rect, props.cssPerPdfUnit ?? DEFAULT_CSS_PER_PDF_UNIT, props.coverColor), onInput: handleInput, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd, onKeyDown: handleKeyDown, onBlur: handleBlur }), overflowed && (_jsx("span", { id: `pdf-inline-text-editor-feedback-${draft.id}`, className: "sr-only", children: "Text exceeds available space." }))] }));
|
|
225
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
export function PdfOcrLayer({ lines }) {
|
|
3
|
-
return (_jsx("div", { role: "group", "aria-label": "Recognized text", className: "pdfOcrLayer", style: {
|
|
4
|
-
position: "absolute",
|
|
5
|
-
inset: 0,
|
|
6
|
-
overflow: "hidden",
|
|
7
|
-
// Above the page canvas. PdfViewer appends the canvas AFTER this layer and styles it
|
|
8
|
-
// `absolute`, so at an equal z-index the opaque scan would paint over this transparent
|
|
9
|
-
// text and swallow the pointer events that make it selectable — i.e. the recognized text
|
|
10
|
-
// would exist in the DOM but be unusable, which is the entire point of the layer.
|
|
11
|
-
zIndex: 2,
|
|
12
|
-
pointerEvents: "none",
|
|
13
|
-
lineHeight: 1,
|
|
14
|
-
}, children: lines.map((line, index) => {
|
|
15
|
-
const style = {
|
|
16
|
-
position: "absolute",
|
|
17
|
-
left: `${line.rect.x * 100}%`,
|
|
18
|
-
top: `${line.rect.y * 100}%`,
|
|
19
|
-
width: `${line.rect.width * 100}%`,
|
|
20
|
-
height: `${line.rect.height * 100}%`,
|
|
21
|
-
color: "transparent",
|
|
22
|
-
whiteSpace: "pre",
|
|
23
|
-
userSelect: "text",
|
|
24
|
-
pointerEvents: "auto",
|
|
25
|
-
cursor: "text",
|
|
26
|
-
overflow: "hidden",
|
|
27
|
-
};
|
|
28
|
-
return (_jsx("span", { style: style, children: line.text }, `${index}-${line.text}`));
|
|
29
|
-
}) }));
|
|
30
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
// Recursive outline (bookmarks) tree. Each node with a destination is a clickable row; nodes with
|
|
4
|
-
// children get an expand/collapse chevron (expanded by default).
|
|
5
|
-
export function PdfOutline({ nodes, onSelect, depth = 0, }) {
|
|
6
|
-
return (_jsx("ul", { children: nodes.map((node, i) => (_jsx(OutlineRow, { node: node, onSelect: onSelect, depth: depth }, i))) }));
|
|
7
|
-
}
|
|
8
|
-
function OutlineRow({ node, onSelect, depth, }) {
|
|
9
|
-
const [open, setOpen] = useState(true);
|
|
10
|
-
const hasKids = node.items && node.items.length > 0;
|
|
11
|
-
return (_jsxs("li", { children: [_jsxs("div", { className: "flex items-center text-muted hover:text-paper", style: { paddingLeft: depth * 12 + 4 }, children: [hasKids ? (_jsx("button", { type: "button", onClick: () => setOpen((o) => !o), className: "vr-press vr-focus w-4 shrink-0 rounded text-muted-2 hover:text-paper", title: open ? "Collapse outline section" : "Expand outline section", "aria-label": open ? "Collapse outline section" : "Expand outline section", "aria-expanded": open, children: open ? "▾" : "▸" })) : (_jsx("span", { className: "w-4 shrink-0" })), _jsx("button", { type: "button", onClick: () => onSelect(node.dest), disabled: !node.dest, className: `flex-1 truncate py-1 pr-2 text-left ${node.bold ? "font-semibold" : ""} ${node.italic ? "italic" : ""} disabled:cursor-default disabled:opacity-60`, title: node.title, children: node.title || "Untitled" })] }), hasKids && open && _jsx(PdfOutline, { nodes: node.items, onSelect: onSelect, depth: depth + 1 })] }));
|
|
12
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState } from "react";
|
|
3
|
-
import { ActionDialog } from "@rightkit/react-shell/react";
|
|
4
|
-
import { DIALOG_SLOTS, cancelAction, primaryAction } from "../../app/chrome.js";
|
|
5
|
-
// App-native PDF password modal (replaces window.prompt). Keyboard-accessible: autofocus, Enter to
|
|
6
|
-
// submit, Esc to cancel; the shared modal shell provides role="dialog", focus trap, and Escape.
|
|
7
|
-
export function PdfPasswordPrompt({ retry, onSubmit, onCancel, }) {
|
|
8
|
-
const [pw, setPw] = useState("");
|
|
9
|
-
const inputRef = useRef(null);
|
|
10
|
-
return (_jsx(ActionDialog, { title: "Password required", ariaLabel: "Password required", description: retry ? "Incorrect password. Try again." : "This PDF is password-protected.", onClose: onCancel, width: 400, slots: { ...DIALOG_SLOTS, body: "mt-3" }, actions: [
|
|
11
|
-
cancelAction(onCancel),
|
|
12
|
-
{ ...primaryAction("Unlock", () => pw && onSubmit(pw)), disabled: !pw },
|
|
13
|
-
], children: _jsx("input", { ref: inputRef, type: "password", "aria-label": "PDF password", value: pw, onChange: (e) => setPw(e.target.value), onKeyDown: (e) => {
|
|
14
|
-
if (e.key === "Enter") {
|
|
15
|
-
e.preventDefault();
|
|
16
|
-
if (pw)
|
|
17
|
-
onSubmit(pw);
|
|
18
|
-
}
|
|
19
|
-
else if (e.key === "Escape") {
|
|
20
|
-
e.preventDefault();
|
|
21
|
-
onCancel();
|
|
22
|
-
}
|
|
23
|
-
}, placeholder: "Password", className: "mb-5 w-full rounded border border-line bg-ink px-3 py-1.5 text-sm text-paper outline-none focus:border-copper" }) }));
|
|
24
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { PDFDocumentProxy } from "pdfjs-dist";
|
|
2
|
-
export declare function PdfThumbnails({ pdf, numPages, current, rotation, onSelect, }: {
|
|
3
|
-
pdf: PDFDocumentProxy;
|
|
4
|
-
numPages: number;
|
|
5
|
-
current: number;
|
|
6
|
-
rotation: number;
|
|
7
|
-
onSelect: (n: number) => void;
|
|
8
|
-
}): import("react").JSX.Element;
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { boundedCanvasDpr, THUMB_W, viewportRotation } from "./pdfShared.js";
|
|
4
|
-
import { thumbnailWindow } from "@rightkit/pdf-ui";
|
|
5
|
-
const THUMB_ITEM_HEIGHT = Math.round(THUMB_W * 1.3) + 30;
|
|
6
|
-
const THUMB_OVERSCAN = 2;
|
|
7
|
-
// Page-thumbnail rail. Lazily renders each page to a small canvas as it scrolls into the rail
|
|
8
|
-
// (IntersectionObserver), caches the result, and re-renders when rotation changes. Clicking a
|
|
9
|
-
// thumbnail navigates the main view; the current page is highlighted and scrolled into view.
|
|
10
|
-
export function PdfThumbnails({ pdf, numPages, current, rotation, onSelect, }) {
|
|
11
|
-
const rootRef = useRef(null);
|
|
12
|
-
const observerRef = useRef(null);
|
|
13
|
-
const slotRefs = useRef(new Map());
|
|
14
|
-
const renderedRef = useRef(new Set());
|
|
15
|
-
const visibleRef = useRef(new Set());
|
|
16
|
-
const generationRef = useRef(0);
|
|
17
|
-
const renderTasksRef = useRef(new Map());
|
|
18
|
-
const renderAttemptsRef = useRef(new Map());
|
|
19
|
-
const [viewportHeight, setViewportHeight] = useState(600);
|
|
20
|
-
const [scrollTop, setScrollTop] = useState(0);
|
|
21
|
-
const windowed = thumbnailWindow({
|
|
22
|
-
numPages,
|
|
23
|
-
scrollTop,
|
|
24
|
-
viewportHeight,
|
|
25
|
-
itemHeight: THUMB_ITEM_HEIGHT,
|
|
26
|
-
overscan: THUMB_OVERSCAN,
|
|
27
|
-
});
|
|
28
|
-
const pages = useMemo(() => Array.from({ length: Math.max(0, windowed.end - windowed.start + 1) }, (_, index) => windowed.start + index), [windowed.end, windowed.start]);
|
|
29
|
-
const cancelThumb = useCallback((n) => {
|
|
30
|
-
const task = renderTasksRef.current.get(n);
|
|
31
|
-
renderTasksRef.current.delete(n);
|
|
32
|
-
renderAttemptsRef.current.delete(n);
|
|
33
|
-
try {
|
|
34
|
-
task?.cancel?.();
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// pdf.js cancellation is best-effort during slot reuse and unmount.
|
|
38
|
-
}
|
|
39
|
-
renderedRef.current.delete(n);
|
|
40
|
-
}, []);
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
const root = rootRef.current;
|
|
43
|
-
if (!root)
|
|
44
|
-
return;
|
|
45
|
-
const updateHeight = () => setViewportHeight(Math.max(1, root.clientHeight));
|
|
46
|
-
updateHeight();
|
|
47
|
-
const observer = new ResizeObserver(updateHeight);
|
|
48
|
-
observer.observe(root);
|
|
49
|
-
return () => observer.disconnect();
|
|
50
|
-
}, []);
|
|
51
|
-
const renderThumb = useCallback(async (n) => {
|
|
52
|
-
if (renderedRef.current.has(n))
|
|
53
|
-
return;
|
|
54
|
-
const generation = generationRef.current;
|
|
55
|
-
const attempt = {};
|
|
56
|
-
renderAttemptsRef.current.set(n, attempt);
|
|
57
|
-
renderedRef.current.add(n);
|
|
58
|
-
const slot = slotRefs.current.get(n);
|
|
59
|
-
if (!slot) {
|
|
60
|
-
renderAttemptsRef.current.delete(n);
|
|
61
|
-
renderedRef.current.delete(n);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
const page = await pdf.getPage(n);
|
|
66
|
-
if (renderAttemptsRef.current.get(n) !== attempt ||
|
|
67
|
-
generationRef.current !== generation ||
|
|
68
|
-
slotRefs.current.get(n) !== slot) {
|
|
69
|
-
if (renderAttemptsRef.current.get(n) === attempt) {
|
|
70
|
-
renderAttemptsRef.current.delete(n);
|
|
71
|
-
renderedRef.current.delete(n);
|
|
72
|
-
}
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const pageRotation = viewportRotation(page.rotate, rotation);
|
|
76
|
-
const unit = page.getViewport({ scale: 1, rotation: pageRotation });
|
|
77
|
-
const scale = THUMB_W / unit.width;
|
|
78
|
-
const vp = page.getViewport({ scale, rotation: pageRotation });
|
|
79
|
-
const dpr = boundedCanvasDpr(vp, window.devicePixelRatio || 1);
|
|
80
|
-
const canvas = document.createElement("canvas");
|
|
81
|
-
canvas.width = Math.floor(vp.width * dpr);
|
|
82
|
-
canvas.height = Math.floor(vp.height * dpr);
|
|
83
|
-
canvas.style.width = `${vp.width}px`;
|
|
84
|
-
canvas.style.height = `${vp.height}px`;
|
|
85
|
-
const ctx = canvas.getContext("2d");
|
|
86
|
-
if (!ctx) {
|
|
87
|
-
renderAttemptsRef.current.delete(n);
|
|
88
|
-
renderedRef.current.delete(n);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
// intent: "print" is NOT about printing — it's the cheapest way to give this render its own
|
|
92
|
-
// pdf.js-internal render slot. PdfViewer's main pane calls page.render() on the SAME cached
|
|
93
|
-
// PDFPageProxy (pdf.js caches proxies per document) for the same page, under the default
|
|
94
|
-
// "display" intent. pdf.js keys concurrent render/operator-list state by rendering intent
|
|
95
|
-
// (_intentStates, keyed by a cacheKey derived from the intent flags — see pdf.mjs
|
|
96
|
-
// getRenderingIntent/render), so two "display" renders on one page proxy collide and one
|
|
97
|
-
// silently produces a blank canvas. A distinct intent gets its own intentState entry instead
|
|
98
|
-
// of a second parsed document, which the print path uses for the same underlying collision
|
|
99
|
-
// (documentPrinting.ts printPdfBytes).
|
|
100
|
-
const renderTask = page.render({
|
|
101
|
-
canvas,
|
|
102
|
-
canvasContext: ctx,
|
|
103
|
-
viewport: vp,
|
|
104
|
-
intent: "print",
|
|
105
|
-
transform: dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined,
|
|
106
|
-
});
|
|
107
|
-
renderTasksRef.current.set(n, renderTask);
|
|
108
|
-
try {
|
|
109
|
-
await renderTask.promise;
|
|
110
|
-
}
|
|
111
|
-
finally {
|
|
112
|
-
if (renderTasksRef.current.get(n) === renderTask)
|
|
113
|
-
renderTasksRef.current.delete(n);
|
|
114
|
-
}
|
|
115
|
-
// Only discard on a genuine slot swap (this numbered DOM slot got reused for a different
|
|
116
|
-
// page — real virtualization reuse), NOT on `!visibleRef.current.has(n)`. That extra check
|
|
117
|
-
// used to race: `rotation` settles one commit after `pdf` (usePdfDocumentLoader sets them in
|
|
118
|
-
// two separate setState calls either side of an `await`), so the observer-setup effect below
|
|
119
|
-
// (deps: [renderThumb], which depends on `rotation`) can tear down and rebuild while THIS
|
|
120
|
-
// render is still in flight — its cleanup clears visibleRef out from under a render that was
|
|
121
|
-
// legitimately visible when it started and still is when it finishes, so the completed,
|
|
122
|
-
// correctly-rasterized canvas got thrown away every time. If the page truly scrolled out of
|
|
123
|
-
// view in the meantime, the intersection observer's own "not intersecting" branch already
|
|
124
|
-
// removes the canvas — that's the correct place for that decision, not a second check here.
|
|
125
|
-
if (renderAttemptsRef.current.get(n) !== attempt ||
|
|
126
|
-
generationRef.current !== generation ||
|
|
127
|
-
slotRefs.current.get(n) !== slot) {
|
|
128
|
-
if (renderAttemptsRef.current.get(n) === attempt) {
|
|
129
|
-
renderAttemptsRef.current.delete(n);
|
|
130
|
-
renderedRef.current.delete(n);
|
|
131
|
-
}
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
slot.querySelector("canvas")?.remove();
|
|
135
|
-
slot.appendChild(canvas);
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
if (renderAttemptsRef.current.get(n) === attempt) {
|
|
139
|
-
renderAttemptsRef.current.delete(n);
|
|
140
|
-
renderedRef.current.delete(n); // allow a retry on the next intersection
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}, [pdf, rotation]);
|
|
144
|
-
// (Re)render visible thumbnails. Re-runs when rotation changes: clear the cache + existing
|
|
145
|
-
// canvases so each page re-rasterizes at the new orientation.
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
generationRef.current += 1;
|
|
148
|
-
renderTasksRef.current.forEach((_, n) => cancelThumb(n));
|
|
149
|
-
renderedRef.current.clear();
|
|
150
|
-
visibleRef.current.clear();
|
|
151
|
-
slotRefs.current.forEach((el) => el.querySelector("canvas")?.remove());
|
|
152
|
-
const root = rootRef.current;
|
|
153
|
-
if (!root)
|
|
154
|
-
return;
|
|
155
|
-
const obs = new IntersectionObserver((entries) => {
|
|
156
|
-
for (const e of entries) {
|
|
157
|
-
const n = Number(e.target.dataset.page);
|
|
158
|
-
if (e.isIntersecting) {
|
|
159
|
-
visibleRef.current.add(n);
|
|
160
|
-
void renderThumb(n);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
visibleRef.current.delete(n);
|
|
164
|
-
cancelThumb(n);
|
|
165
|
-
e.target.querySelector("canvas")?.remove();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}, { root, rootMargin: "200px 0px" });
|
|
169
|
-
slotRefs.current.forEach((el) => obs.observe(el));
|
|
170
|
-
observerRef.current = obs;
|
|
171
|
-
return () => {
|
|
172
|
-
visibleRef.current.clear();
|
|
173
|
-
renderTasksRef.current.forEach((_, n) => cancelThumb(n));
|
|
174
|
-
observerRef.current = null;
|
|
175
|
-
obs.disconnect();
|
|
176
|
-
};
|
|
177
|
-
}, [cancelThumb, renderThumb]);
|
|
178
|
-
// Keep the current page's thumbnail visible as the main view scrolls.
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
const root = rootRef.current;
|
|
181
|
-
if (!root)
|
|
182
|
-
return;
|
|
183
|
-
const top = (current - 1) * THUMB_ITEM_HEIGHT;
|
|
184
|
-
const bottom = top + THUMB_ITEM_HEIGHT;
|
|
185
|
-
if (top < root.scrollTop)
|
|
186
|
-
root.scrollTop = top;
|
|
187
|
-
else if (bottom > root.scrollTop + root.clientHeight)
|
|
188
|
-
root.scrollTop = bottom - root.clientHeight;
|
|
189
|
-
}, [current]);
|
|
190
|
-
return (_jsxs("div", { ref: rootRef, className: "h-full overflow-auto py-2 text-chrome-xs", onScroll: (event) => setScrollTop(event.currentTarget.scrollTop), children: [windowed.before > 0 && _jsx("div", { "aria-hidden": "true", style: { height: windowed.before } }), pages.map((n) => (_jsxs("button", { type: "button", onClick: () => onSelect(n), className: "flex w-full flex-col items-center gap-1 py-1.5", style: { height: THUMB_ITEM_HEIGHT }, title: `Page ${n}`, children: [_jsx("div", { "data-page": n, ref: (el) => {
|
|
191
|
-
if (el) {
|
|
192
|
-
slotRefs.current.set(n, el);
|
|
193
|
-
observerRef.current?.observe(el);
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
cancelThumb(n);
|
|
197
|
-
slotRefs.current.delete(n);
|
|
198
|
-
visibleRef.current.delete(n);
|
|
199
|
-
}
|
|
200
|
-
}, className: `flex items-center justify-center overflow-hidden border bg-white ${n === current ? "border-copper" : "border-line"}`, style: { width: THUMB_W, minHeight: Math.round(THUMB_W * 1.3) } }), _jsx("span", { className: `font-mono ${n === current ? "text-copper-hi" : "text-muted-2"}`, children: n })] }, n))), windowed.after > 0 && _jsx("div", { "aria-hidden": "true", style: { height: windowed.after } })] }));
|
|
201
|
-
}
|