@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +31 -7
  3. package/dist/app/chrome.js +1 -1
  4. package/dist/app/documentPrinting.d.ts +0 -6
  5. package/dist/app/documentPrinting.js +0 -18
  6. package/dist/app/shell/nativeMenu.d.ts +3 -2
  7. package/dist/app/shell/nativeMenu.js +8 -5
  8. package/dist/documents/perfEventCounters.d.ts +0 -8
  9. package/dist/documents/perfEventCounters.js +2 -1
  10. package/dist/documents/preview/previewFrame.d.ts +2 -1
  11. package/dist/documents/preview/previewFrame.js +9 -7
  12. package/dist/documents/searchUtils.js +1 -1
  13. package/dist/host.d.ts +36 -0
  14. package/dist/host.js +54 -0
  15. package/dist/index.d.ts +1 -46
  16. package/dist/index.js +1 -46
  17. package/dist/settings/themes.js +1 -1
  18. package/dist/state/bridgeEvents.d.ts +1 -1
  19. package/dist/state/bridgeEvents.js +7 -4
  20. package/dist/state/recentUrls.js +4 -3
  21. package/dist/state/tauriBridge.d.ts +3 -47
  22. package/dist/state/tauriBridge.js +6 -162
  23. package/dist/styles.css +2 -2
  24. package/package.json +16 -87
  25. package/dist/app/shortcutChords.d.ts +0 -6
  26. package/dist/app/shortcutChords.js +0 -14
  27. package/dist/app/useCloseFindOnSurfaceChange.d.ts +0 -14
  28. package/dist/app/useCloseFindOnSurfaceChange.js +0 -23
  29. package/dist/app/useEditText.d.ts +0 -6
  30. package/dist/app/useEditText.js +0 -10
  31. package/dist/appName.d.ts +0 -2
  32. package/dist/appName.js +0 -8
  33. package/dist/documents/FullscreenIcon.d.ts +0 -4
  34. package/dist/documents/FullscreenIcon.js +0 -7
  35. package/dist/documents/mdx/mdxRuntimeScript.d.ts +0 -2
  36. package/dist/documents/mdx/mdxRuntimeScript.js +0 -3
  37. package/dist/documents/mdx/mdxRuntimeScript.txt +0 -584
  38. package/dist/documents/pdf/MacPdfReadPane.d.ts +0 -23
  39. package/dist/documents/pdf/MacPdfReadPane.js +0 -708
  40. package/dist/documents/pdf/NativePdfDocument.d.ts +0 -9
  41. package/dist/documents/pdf/NativePdfDocument.js +0 -41
  42. package/dist/documents/pdf/PdfInlineTextEditor.d.ts +0 -53
  43. package/dist/documents/pdf/PdfInlineTextEditor.js +0 -225
  44. package/dist/documents/pdf/PdfOcrLayer.d.ts +0 -4
  45. package/dist/documents/pdf/PdfOcrLayer.js +0 -30
  46. package/dist/documents/pdf/PdfOutline.d.ts +0 -6
  47. package/dist/documents/pdf/PdfOutline.js +0 -12
  48. package/dist/documents/pdf/PdfPasswordPrompt.d.ts +0 -5
  49. package/dist/documents/pdf/PdfPasswordPrompt.js +0 -24
  50. package/dist/documents/pdf/PdfThumbnails.d.ts +0 -8
  51. package/dist/documents/pdf/PdfThumbnails.js +0 -201
  52. package/dist/documents/pdf/PdfToolbar.d.ts +0 -47
  53. package/dist/documents/pdf/PdfToolbar.js +0 -76
  54. package/dist/documents/pdf/PdfViewer.d.ts +0 -36
  55. package/dist/documents/pdf/PdfViewer.js +0 -1522
  56. package/dist/documents/pdf/PdfViewerBody.d.ts +0 -22
  57. package/dist/documents/pdf/PdfViewerBody.js +0 -42
  58. package/dist/documents/pdf/macPdfNativeBridge.d.ts +0 -118
  59. package/dist/documents/pdf/macPdfNativeBridge.js +0 -134
  60. package/dist/documents/pdf/nearestVisiblePdfPage.d.ts +0 -2
  61. package/dist/documents/pdf/nearestVisiblePdfPage.js +0 -22
  62. package/dist/documents/pdf/pdfBench.d.ts +0 -88
  63. package/dist/documents/pdf/pdfBench.js +0 -535
  64. package/dist/documents/pdf/pdfBenchAutopilot.d.ts +0 -48
  65. package/dist/documents/pdf/pdfBenchAutopilot.js +0 -154
  66. package/dist/documents/pdf/pdfBenchTransport.d.ts +0 -21
  67. package/dist/documents/pdf/pdfBenchTransport.js +0 -233
  68. package/dist/documents/pdf/pdfEditModel.d.ts +0 -294
  69. package/dist/documents/pdf/pdfEditModel.js +0 -105
  70. package/dist/documents/pdf/pdfEditor.d.ts +0 -100
  71. package/dist/documents/pdf/pdfEditor.js +0 -68
  72. package/dist/documents/pdf/pdfOcr.d.ts +0 -36
  73. package/dist/documents/pdf/pdfOcr.js +0 -88
  74. package/dist/documents/pdf/pdfOcrEdit.d.ts +0 -32
  75. package/dist/documents/pdf/pdfOcrEdit.js +0 -138
  76. package/dist/documents/pdf/pdfPrinting.d.ts +0 -28
  77. package/dist/documents/pdf/pdfPrinting.js +0 -110
  78. package/dist/documents/pdf/pdfSave.d.ts +0 -87
  79. package/dist/documents/pdf/pdfSave.js +0 -250
  80. package/dist/documents/pdf/pdfShared.d.ts +0 -16
  81. package/dist/documents/pdf/pdfShared.js +0 -20
  82. package/dist/documents/pdf/pdfToMarkdown.d.ts +0 -11
  83. package/dist/documents/pdf/pdfToMarkdown.js +0 -67
  84. package/dist/documents/pdf/pdfjsLibGlobal.d.ts +0 -1
  85. package/dist/documents/pdf/pdfjsLibGlobal.js +0 -12
  86. package/dist/documents/pdf/toolbarIcons.d.ts +0 -18
  87. package/dist/documents/pdf/toolbarIcons.js +0 -31
  88. package/dist/documents/pdf/usePdfDocumentLoader.d.ts +0 -40
  89. package/dist/documents/pdf/usePdfDocumentLoader.js +0 -243
  90. package/dist/documents/pdf/usePdfEditSession.d.ts +0 -88
  91. package/dist/documents/pdf/usePdfEditSession.js +0 -341
  92. package/dist/documents/pdf/usePdfEditor.d.ts +0 -59
  93. package/dist/documents/pdf/usePdfEditor.js +0 -317
  94. package/dist/documents/pdf/usePdfFit.d.ts +0 -19
  95. package/dist/documents/pdf/usePdfFit.js +0 -55
  96. package/dist/documents/pdf/usePdfOcr.d.ts +0 -38
  97. package/dist/documents/pdf/usePdfOcr.js +0 -337
  98. package/dist/documents/pdf/usePdfPreviewRecompose.d.ts +0 -28
  99. package/dist/documents/pdf/usePdfPreviewRecompose.js +0 -256
  100. package/dist/documents/pdf/usePdfSavePlan.d.ts +0 -61
  101. package/dist/documents/pdf/usePdfSavePlan.js +0 -287
  102. package/dist/documents/pdf/usePdfSearch.d.ts +0 -31
  103. package/dist/documents/pdf/usePdfSearch.js +0 -367
  104. package/dist/documents/pdf/usePdfViewControls.d.ts +0 -72
  105. package/dist/documents/pdf/usePdfViewControls.js +0 -152
  106. package/dist/documents/scopedSelectAll.d.ts +0 -5
  107. package/dist/documents/scopedSelectAll.js +0 -77
  108. package/dist/documents/txt/cmScroll.d.ts +0 -11
  109. package/dist/documents/txt/cmScroll.js +0 -52
  110. package/dist/documents/useMirroredEditText.d.ts +0 -15
  111. package/dist/documents/useMirroredEditText.js +0 -29
  112. package/dist/documents/useSplitResize.d.ts +0 -7
  113. package/dist/documents/useSplitResize.js +0 -37
  114. package/dist/settings/rk-settings.css +0 -38
  115. package/dist/settings/sections/EditorSection.d.ts +0 -1
  116. package/dist/settings/sections/EditorSection.js +0 -18
  117. package/dist/settings/sections/FileAssociationsSection.d.ts +0 -1
  118. package/dist/settings/sections/FileAssociationsSection.js +0 -5
  119. package/dist/settings/sections/MacFileAssociationsBlock.d.ts +0 -33
  120. package/dist/settings/sections/MacFileAssociationsBlock.js +0 -151
  121. package/dist/settings/sections/ThemeSection.d.ts +0 -1
  122. package/dist/settings/sections/ThemeSection.js +0 -31
  123. package/dist/settings/sections/TypographySection.d.ts +0 -1
  124. package/dist/settings/sections/TypographySection.js +0 -19
  125. package/dist/settings/sections/onOff.d.ts +0 -11
  126. 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,4 +0,0 @@
1
- import type { OcrLine } from "./pdfOcr.js";
2
- export declare function PdfOcrLayer({ lines }: {
3
- lines: OcrLine[];
4
- }): import("react").JSX.Element;
@@ -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,6 +0,0 @@
1
- import type { OutlineNode } from "./pdfShared.js";
2
- export declare function PdfOutline({ nodes, onSelect, depth, }: {
3
- nodes: OutlineNode[];
4
- onSelect: (dest: string | unknown[] | null) => void;
5
- depth?: number;
6
- }): import("react").JSX.Element;
@@ -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,5 +0,0 @@
1
- export declare function PdfPasswordPrompt({ retry, onSubmit, onCancel, }: {
2
- retry: boolean;
3
- onSubmit: (pw: string) => void;
4
- onCancel: () => void;
5
- }): import("react").JSX.Element;
@@ -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
- }