@embedpdf/react 3.0.0-next.5 → 3.0.0-next.7

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 (43) hide show
  1. package/dist/annotation.d.cts +1 -1
  2. package/dist/annotation.d.ts +1 -1
  3. package/dist/index.cjs +14 -2
  4. package/dist/index.d.cts +5 -5
  5. package/dist/index.d.ts +5 -5
  6. package/dist/index.js +4 -4
  7. package/dist/interaction.cjs +14 -1
  8. package/dist/interaction.cjs.map +1 -1
  9. package/dist/interaction.d.cts +2 -2
  10. package/dist/interaction.d.ts +2 -2
  11. package/dist/interaction.js +4 -3
  12. package/dist/interaction.js.map +1 -1
  13. package/dist/page-view.cjs +5 -3
  14. package/dist/page-view.cjs.map +1 -1
  15. package/dist/page-view.js +6 -4
  16. package/dist/page-view.js.map +1 -1
  17. package/dist/render.cjs +67 -50
  18. package/dist/render.cjs.map +1 -1
  19. package/dist/render.d.cts +6 -19
  20. package/dist/render.d.ts +6 -19
  21. package/dist/render.js +70 -52
  22. package/dist/render.js.map +1 -1
  23. package/dist/{runtime-CMxIuF2O.d.cts → runtime-Bh_9OtWo.d.ts} +12 -3
  24. package/dist/{runtime-DV98q7I0.d.ts → runtime-D4TXEICX.d.cts} +12 -3
  25. package/dist/runtime.cjs +2 -1
  26. package/dist/runtime.cjs.map +1 -1
  27. package/dist/runtime.d.cts +1 -1
  28. package/dist/runtime.d.ts +1 -1
  29. package/dist/runtime.js +2 -1
  30. package/dist/runtime.js.map +1 -1
  31. package/dist/selection.cjs +144 -0
  32. package/dist/selection.cjs.map +1 -1
  33. package/dist/selection.d.cts +26 -1
  34. package/dist/selection.d.ts +26 -1
  35. package/dist/selection.js +149 -6
  36. package/dist/selection.js.map +1 -1
  37. package/dist/stage.cjs +14 -145
  38. package/dist/stage.cjs.map +1 -1
  39. package/dist/stage.d.cts +18 -5
  40. package/dist/stage.d.ts +18 -5
  41. package/dist/stage.js +15 -146
  42. package/dist/stage.js.map +1 -1
  43. package/package.json +23 -23
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.cjs","names":["docInfoListEquals"],"sources":["../src/runtime.tsx"],"sourcesContent":["/**\n * @embedpdf/react — the generic binding.\n *\n * Binds the kernel's one change stream to React (useSyncExternalStore), resolves\n * capabilities (document-scoped ones against the active or `<DocumentScope>`-given\n * document), and provides the page coordinate context. Every plugin and layer rides\n * on this — there is no per-plugin framework code.\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/core';\nimport * as React from 'react';\nimport {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n useSyncExternalStore,\n} from 'react';\nimport { createKernel, docInfoListEquals } from '@embedpdf/core';\nimport type {\n AnyPlugin,\n CapabilityToken,\n Engine,\n EngineFactory,\n InitialDocument,\n Kernel,\n} from '@embedpdf/core';\n// Pure coordinate math from the geometry base — NOT from stage-core. The\n// PageContext seam stays stage-agnostic (it must also serve standalone PageView).\nimport type { PageFrame, PageTransform, Point, Rect } from '@embedpdf/core-geometry';\nimport type { PageViewDemand } from '@embedpdf/plugin-render';\n\nconst KernelCtx = createContext<Kernel | null>(null);\n/** The document a subtree is bound to. null => use the active document. */\nconst DocumentScopeCtx = createContext<string | null>(null);\n\nexport function useKernel(): Kernel {\n const k = useContext(KernelCtx);\n if (!k) throw new Error('useKernel must be used within <Viewer>/<EmbedPDF>');\n return k;\n}\n\nexport const shallowArray = <T,>(a: readonly T[], b: readonly T[]): boolean =>\n a === b || (a.length === b.length && a.every((x, i) => x === b[i]));\n\n/** Read a value derived from the kernel, cached by equality (no tearing loop). */\nexport function useKernelValue<R>(\n select: (k: Kernel) => R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n const next = select(kernel);\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\nexport function useActiveDocumentId(): string | null {\n return useKernelValue((k) => k.documents.activeId());\n}\n\n/** The document id for this subtree: the nearest <DocumentScope>, else the active doc. */\nexport function useDocumentId(): string | null {\n const scoped = useContext(DocumentScopeCtx);\n const active = useActiveDocumentId();\n return scoped ?? active;\n}\n\nexport interface DocumentScopeProps {\n id: string;\n children: React.ReactNode;\n}\n/** Bind a subtree to a specific document (panes, comparison). */\nexport function DocumentScope({ id, children }: DocumentScopeProps) {\n return <DocumentScopeCtx.Provider value={id}>{children}</DocumentScopeCtx.Provider>;\n}\n\nexport interface DocumentGateProps {\n /** Shown while this subtree has NO document (empty workspace, docs still opening). */\n fallback?: React.ReactNode;\n children: React.ReactNode;\n}\n/**\n * Render children only while this subtree has a READY document — the\n * structural way to say \"this UI is defined over a document\". An empty\n * workspace is a legitimate, designable state (the Viewer no longer blocks on\n * documents so chrome can render at t≈0): workspace-scoped UI (toolbars,\n * commands, i18n) lives OUTSIDE the gate; document-scoped UI (Stage, panels,\n * page chrome) lives inside it, or reads through `useOptionalSelector`.\n * A `loading`/`locked`/`error` tab renders `fallback` — so the gate's\n * fallback doubles as the per-tab boot state; richer chrome (a password\n * prompt, an error pane) branches on `useDocumentStatus()` beside the gate.\n * Sibling of <DocumentScope>, which picks WHICH document; this one handles\n * WHETHER.\n */\nexport function DocumentGate({ fallback = null, children }: DocumentGateProps) {\n const docId = useDocumentId();\n const ready = useKernelValue((k) => (docId ? k.documents.get(docId)?.status === 'ready' : false));\n return <>{ready ? children : fallback}</>;\n}\n\n/** Lifecycle status of this subtree's document (loading/locked/ready/error),\n * or null with no document. The password prompt and error panes key off it. */\nexport function useDocumentStatus() {\n const docId = useDocumentId();\n return useKernelValue((k) => (docId ? (k.documents.get(docId)?.status ?? null) : null));\n}\n\n/**\n * Resolve a capability by token, binding document-scoped ones to this\n * subtree's document. Resolution is a REACTIVE read (`tryCapability` through\n * the kernel's one change stream), not a memoized call — under the\n * request-time lifecycle a document can become resolvable while its id stays\n * the same, so any id-keyed cache goes stale; subscribing makes staleness\n * structurally impossible. Fail-fast: while unresolvable, this re-runs the\n * strict resolver so the kernel's truthful reason (`no capability` / `no\n * document` / `document is loading|locked`) is what throws.\n */\nexport function useCapability<T>(token: CapabilityToken<T>): T {\n const kernel = useKernel();\n const scoped = useContext(DocumentScopeCtx);\n const cap = useKernelValue((k) => k.tryCapability(token, scoped ?? undefined));\n return cap ?? kernel.capability(token, scoped ?? undefined);\n}\n\n/** Like `useCapability`, but null while the token can't resolve (no plugin,\n * no document, or a document that isn't ready yet). */\nexport function useOptionalCapability<T>(token: CapabilityToken<T>): T | null {\n const scoped = useContext(DocumentScopeCtx);\n return useKernelValue((k) => k.tryCapability(token, scoped ?? undefined));\n}\n\n/** Subscribe to a selector over a (document-resolved) capability. */\nexport function useSelector<C, R>(\n token: CapabilityToken<C>,\n select: (cap: C) => R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const cap = useCapability(token);\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n const next = select(cap);\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\n/**\n * Null-safe `useSelector`: `fallback` whenever the token can't resolve — no\n * provider, or a document-scoped token with no document. For chrome that stays\n * mounted across the empty-workspace state (a zoom readout, a mode band).\n * `useSelector` stays strict (fail-fast) for code that KNOWS a document exists\n * — e.g. anything inside a <DocumentGate>.\n *\n * The `select` guard also swallows reads through a capability whose document\n * closed between the store notification and this render — that teardown race\n * resolves to `fallback` for one frame, then re-renders against the new state.\n */\nexport function useOptionalSelector<C, R>(\n token: CapabilityToken<C>,\n select: (cap: C) => R,\n fallback: R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const cap = useOptionalCapability(token);\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n let next: R;\n if (cap === null) {\n next = fallback;\n } else {\n try {\n next = select(cap);\n } catch {\n next = fallback;\n }\n }\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\n/** The document registry (open/close/active/list), reactive. */\nexport function useDocuments() {\n const kernel = useKernel();\n const docs = useKernelValue((k) => k.documents.list(), docInfoListEquals);\n const activeId = useActiveDocumentId();\n return {\n docs,\n activeId,\n open: kernel.documents.open,\n unlock: kernel.documents.unlock,\n close: kernel.documents.close,\n setActive: kernel.documents.setActive,\n move: kernel.documents.move,\n swap: kernel.documents.swap,\n download: kernel.documents.download,\n downloadLayer: kernel.documents.downloadLayer,\n };\n}\n\n// `InitialDocument` is the KERNEL's type (re-exported via `export * from\n// '@embedpdf/core'` above) — one shared shape for every adapter.\n\nexport interface ViewerProps {\n /**\n * The engine, as an instance or a thunk. Engines construct synchronously and\n * boot lazily (`localEngine()` allocates nothing until first use), so\n * ownership follows the SHAPE of what you pass:\n *\n * - An **instance** (`engine={engine}`) is BORROWED: the Viewer uses it and\n * never destroys it, because you acquired it and therefore own it. The\n * common path — a module-scope `const engine = localEngine()` shared\n * across viewers and route changes. The Viewer calls `engine.warmup?.()`\n * on mount so the boot overlaps app initialization.\n * - A **thunk** (`engine={() => localEngine()}`) is VIEWER-OWNED: the\n * Viewer calls it on mount and `destroy()`s the result on unmount. Use it\n * for per-mount isolation (StrictMode/HMR-clean teardown, independent\n * multi-viewer engines).\n *\n * Init-only: captured on first render; later identity changes are ignored (dev warns).\n */\n engine: Engine | EngineFactory;\n /** Init-only: captured on first render; later identity changes are ignored (dev warns). */\n plugins: AnyPlugin[];\n /** Documents to open on startup (with optional tab names). Init-only. */\n initialDocuments?: InitialDocument[];\n fallback?: React.ReactNode;\n /** Rendered when kernel construction or `start()` fails. Without it a boot\n * failure renders nothing — but never a silent forever-fallback. */\n renderError?: (error: unknown) => React.ReactNode;\n children: React.ReactNode;\n}\n\ntype BootState =\n | { phase: 'booting'; kernel: Kernel | null }\n | { phase: 'ready'; kernel: Kernel }\n | { phase: 'error'; error: unknown };\n\n/**\n * Owns the kernel as an EFFECT-scoped resource: each effect setup creates and\n * starts exactly one kernel; each cleanup destroys exactly that one. That is\n * the contract StrictMode exercises (two kernels in dev, the first fully\n * destroyed) — the kernel itself is never restarted after destroy.\n *\n * The kernel is published to context the moment it exists — before `start()`\n * resolves — so the `fallback` can use workspace capabilities (i18n copy on a\n * loading screen) exactly as before. The one exception is the very first\n * render, which happens before the effect: it renders nothing. Children mount\n * once `start()` resolves — which never touches the engine, so the shell is\n * alive while WASM compiles or the transport connects. `initialDocuments`\n * open in the BACKGROUND and stream into the registry (`useDocuments()` is\n * reactive); per-document loading UI is the Stage's job, not a root gate.\n *\n * ENGINE OWNERSHIP. When `engine` is a thunk, the Viewer OWNS it: each effect\n * setup constructs one engine (construction is synchronous and inert — boot\n * happens lazily inside the engine) and each cleanup destroys exactly that one\n * — after the kernel, so handles close first. When `engine` is an instance it\n * is BORROWED and never destroyed here; the Viewer only calls `warmup?.()` so\n * the WASM/transport boot overlaps plugin initialization. StrictMode's\n * double-mount therefore constructs two independent thunk engines and tears\n * the first fully down, matching the kernel's own effect-scoped lifecycle.\n * That means dev-only double resource use (two worker spawns, two font\n * fetches, briefly overlapping) — deliberate, because it is exactly the\n * leak-detection contract StrictMode exists to exercise; production mounts\n * once and boots once.\n */\nexport function Viewer({\n engine,\n plugins,\n initialDocuments,\n fallback,\n renderError,\n children,\n}: ViewerProps) {\n // Init-only inputs: the kernel's lifetime is the component's lifetime, so a\n // changed engine/plugins identity cannot mean \"rebuild the workspace\" —\n // that would silently drop every open document. Capture once, warn in dev.\n const initial = useRef({ engine, plugins, initialDocuments });\n const warned = useRef(false);\n if (process.env.NODE_ENV !== 'production' && !warned.current) {\n if (initial.current.engine !== engine || initial.current.plugins !== plugins) {\n warned.current = true;\n console.warn(\n '[embedpdf] <Viewer> engine/plugins are init-only. A changed identity is ignored — ' +\n 'pass stable references (module scope, useState, or useMemo). ' +\n 'An inline `plugins={[...]}` array recreates its identity every render.',\n );\n }\n }\n\n const [boot, setBoot] = useState<BootState>({ phase: 'booting', kernel: null });\n useEffect(() => {\n const captured = initial.current;\n // A thunk is viewer-owned: call it now, destroy on unmount. An instance is\n // borrowed: use as-is, never destroy. Construction is synchronous and inert\n // either way — the engine boots lazily on first use — so kick `warmup()`\n // to overlap the WASM/transport boot with plugin initialization.\n const ownsEngine = typeof captured.engine === 'function';\n const engine: Engine = ownsEngine\n ? (captured.engine as EngineFactory)()\n : (captured.engine as Engine);\n engine.warmup?.();\n let kernel: Kernel;\n try {\n kernel = createKernel({ engine, plugins: captured.plugins });\n } catch (error) {\n setBoot({ phase: 'error', error }); // plan/graph errors surface, not throw mid-render\n if (ownsEngine) void engine.destroy();\n return;\n }\n let alive = true;\n setBoot({ phase: 'booting', kernel }); // context carries the kernel from this frame on\n kernel.start().then(\n () => {\n if (!alive) return; // unmounted mid-boot — don't open anything\n setBoot({ phase: 'ready', kernel });\n // Kernel-owned boot policy: all tabs appear immediately in array\n // order; the `active` entry (else the first) is selected; failures\n // surface as tab status. See DocumentsCapability.openAll.\n kernel.documents.openAll(captured.initialDocuments ?? []);\n },\n (error) => {\n if (alive) setBoot({ phase: 'error', error });\n },\n );\n return () => {\n alive = false;\n // Kernel first (closes every document handle), THEN the engine we own —\n // ownership follows acquisition. `engine.destroy()` joins an in-flight\n // boot (or no-ops if it never started), so an unmount mid-boot is safe.\n void kernel.destroy().then(() => {\n if (ownsEngine) void engine.destroy();\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n if (boot.phase === 'error') {\n return <>{renderError ? renderError(boot.error) : null}</>;\n }\n if (!boot.kernel) return null; // pre-effect first render only\n return (\n <KernelCtx.Provider value={boot.kernel}>\n {boot.phase === 'ready' ? children : (fallback ?? null)}\n </KernelCtx.Provider>\n );\n}\nexport const EmbedPDF = Viewer;\n\n/**\n * PageContext — the seam. A layer depends ONLY on this, never on the Stage. So the\n * same layer works inside a virtualized Stage and in a standalone <PageView>.\n */\nexport interface PageContextValue {\n documentId: string;\n /** Durable page identity (PDF object number) — use for keys / render / annotations. */\n pon: number;\n /** Display index (page N) — use for ordering / human-facing page numbers. */\n pageIndex: number;\n /**\n * Reserved chrome bands around the page (screen px per side). The page-chrome\n * slot renders into the outer box (content + frame); these thicknesses size\n * the bands — a label in the bottom band is `bottom:0; height: frame.bottom`.\n */\n frame: PageFrame;\n /**\n * The single bridge between PDF points, view px, and device px for this page.\n * Layers do ALL coordinate work through it — `pageToContent` to place content-\n * space overlays, `renderScale`/`deviceWidth` to render, `contentWidth` for\n * page-relative sizing. Never re-derive `x * scale` or `* dpr`.\n */\n transform: PageTransform;\n /** Client (screen) point → PDF point — the one platform-bound hit-test. */\n toPagePoint(clientX: number, clientY: number): Point;\n /** PDF/content point → client (screen) px — the exact inverse of `toPagePoint`\n * (rotation applied). Lets viewport-space UI (e.g. a selection menu) anchor to a\n * page point WITHOUT a Stage camera, so it works the same in `<PageView>`. */\n toClientPoint(p: Point): Point;\n /** PDF/content rect → client (screen) px AABB. Rect analog of `toClientPoint`\n * for upright viewport-space UI that frames a selected page region. */\n toClientRect(rect: Rect): Rect;\n /**\n * The page-view DEMAND for raster planning uses dependency inversion:\n * plugin-render defines the shape; the host that CREATED this\n * context fills it — as a PULL. The Stage host's getter closes over the\n * stage capability and reads `VisiblePage.visibleRect` live at call time\n * (visibility is the STAGE's data; adapters never re-derive camera math or\n * cache a copy). Three states, three meanings: a real sub-rect (visible),\n * a ZERO rect (stage host, page currently off-screen — want nothing), and\n * an undefined getter (stage-less `<PageView>` — whole page visible, which\n * a thumbnail-sized demand turns into \"never engages\" by arithmetic).\n */\n getViewDemand?: () => PageViewDemand;\n}\n\nconst PageCtx = createContext<PageContextValue | null>(null);\nexport const PageProvider = PageCtx.Provider;\n\nexport function usePage(): PageContextValue {\n const c = useContext(PageCtx);\n if (!c) throw new Error('usePage must be used inside <PageView> or a <Stage> page');\n return c;\n}\n\nexport function makePageContext(\n documentId: string,\n pon: number,\n pageIndex: number,\n frame: PageFrame,\n transform: PageTransform,\n getRect: () => DOMRect,\n getViewDemand?: () => PageViewDemand,\n): PageContextValue {\n return {\n documentId,\n pon,\n pageIndex,\n frame,\n transform,\n ...(getViewDemand ? { getViewDemand } : {}),\n toPagePoint: (cx, cy) => {\n // `getRect()` is the rotated content wrapper's axis-aligned bounding box =\n // the page's DISPLAY box on screen. Convert client → box-local view px,\n // then invert rotation + scale via the transform (verified once in geometry,\n // not re-derived per framework adapter).\n const r = getRect();\n return transform.viewToPage({ x: cx - r.left, y: cy - r.top });\n },\n toClientPoint: (p) => {\n // Exact inverse of `toPagePoint`: page/content point → display-box view px\n // (rotation applied by the transform), offset by the same live display-box\n // origin. So the two can never drift, in either <Stage> or <PageView>.\n const r = getRect();\n const v = transform.pageToView(p);\n return { x: r.left + v.x, y: r.top + v.y };\n },\n toClientRect: (rect) => {\n const r = getRect();\n const v = transform.pageToViewRect(rect);\n return { x: r.left + v.x, y: r.top + v.y, width: v.width, height: v.height };\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runtime.cjs","names":["docInfoListEquals"],"sources":["../src/runtime.tsx"],"sourcesContent":["/**\n * @embedpdf/react — the generic binding.\n *\n * Binds the kernel's one change stream to React (useSyncExternalStore), resolves\n * capabilities (document-scoped ones against the active or `<DocumentScope>`-given\n * document), and provides the page coordinate context. Every plugin and layer rides\n * on this — there is no per-plugin framework code.\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/core';\nimport * as React from 'react';\nimport {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n useSyncExternalStore,\n} from 'react';\nimport { createKernel, docInfoListEquals } from '@embedpdf/core';\nimport type {\n AnyPlugin,\n CapabilityToken,\n Engine,\n EngineFactory,\n InitialDocument,\n Kernel,\n} from '@embedpdf/core';\n// Pure coordinate math from the geometry base — NOT from stage-core. The\n// PageContext seam stays stage-agnostic (it must also serve standalone PageView).\nimport type { PageFrame, PageTransform, Point, Rect } from '@embedpdf/core-geometry';\nimport type { PageViewDemand } from '@embedpdf/plugin-render';\n\nconst KernelCtx = createContext<Kernel | null>(null);\n/** The document a subtree is bound to. null => use the active document. */\nconst DocumentScopeCtx = createContext<string | null>(null);\n\nexport function useKernel(): Kernel {\n const k = useContext(KernelCtx);\n if (!k) throw new Error('useKernel must be used within <Viewer>/<EmbedPDF>');\n return k;\n}\n\nexport const shallowArray = <T,>(a: readonly T[], b: readonly T[]): boolean =>\n a === b || (a.length === b.length && a.every((x, i) => x === b[i]));\n\n/** Read a value derived from the kernel, cached by equality (no tearing loop). */\nexport function useKernelValue<R>(\n select: (k: Kernel) => R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n const next = select(kernel);\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\nexport function useActiveDocumentId(): string | null {\n return useKernelValue((k) => k.documents.activeId());\n}\n\n/** The document id for this subtree: the nearest <DocumentScope>, else the active doc. */\nexport function useDocumentId(): string | null {\n const scoped = useContext(DocumentScopeCtx);\n const active = useActiveDocumentId();\n return scoped ?? active;\n}\n\nexport interface DocumentScopeProps {\n id: string;\n children: React.ReactNode;\n}\n/** Bind a subtree to a specific document (panes, comparison). */\nexport function DocumentScope({ id, children }: DocumentScopeProps) {\n return <DocumentScopeCtx.Provider value={id}>{children}</DocumentScopeCtx.Provider>;\n}\n\nexport interface DocumentGateProps {\n /** Shown while this subtree has NO document (empty workspace, docs still opening). */\n fallback?: React.ReactNode;\n children: React.ReactNode;\n}\n/**\n * Render children only while this subtree has a READY document — the\n * structural way to say \"this UI is defined over a document\". An empty\n * workspace is a legitimate, designable state (the Viewer no longer blocks on\n * documents so chrome can render at t≈0): workspace-scoped UI (toolbars,\n * commands, i18n) lives OUTSIDE the gate; document-scoped UI (Stage, panels,\n * page chrome) lives inside it, or reads through `useOptionalSelector`.\n * A `loading`/`locked`/`error` tab renders `fallback` — so the gate's\n * fallback doubles as the per-tab boot state; richer chrome (a password\n * prompt, an error pane) branches on `useDocumentStatus()` beside the gate.\n * Sibling of <DocumentScope>, which picks WHICH document; this one handles\n * WHETHER.\n */\nexport function DocumentGate({ fallback = null, children }: DocumentGateProps) {\n const docId = useDocumentId();\n const ready = useKernelValue((k) => (docId ? k.documents.get(docId)?.status === 'ready' : false));\n return <>{ready ? children : fallback}</>;\n}\n\n/** Lifecycle status of this subtree's document (loading/locked/ready/error),\n * or null with no document. The password prompt and error panes key off it. */\nexport function useDocumentStatus() {\n const docId = useDocumentId();\n return useKernelValue((k) => (docId ? (k.documents.get(docId)?.status ?? null) : null));\n}\n\n/**\n * Resolve a capability by token, binding document-scoped ones to this\n * subtree's document. Resolution is a REACTIVE read (`tryCapability` through\n * the kernel's one change stream), not a memoized call — under the\n * request-time lifecycle a document can become resolvable while its id stays\n * the same, so any id-keyed cache goes stale; subscribing makes staleness\n * structurally impossible. Fail-fast: while unresolvable, this re-runs the\n * strict resolver so the kernel's truthful reason (`no capability` / `no\n * document` / `document is loading|locked`) is what throws.\n */\nexport function useCapability<T>(token: CapabilityToken<T>): T {\n const kernel = useKernel();\n const scoped = useContext(DocumentScopeCtx);\n const cap = useKernelValue((k) => k.tryCapability(token, scoped ?? undefined));\n return cap ?? kernel.capability(token, scoped ?? undefined);\n}\n\n/** Like `useCapability`, but null while the token can't resolve (no plugin,\n * no document, or a document that isn't ready yet). */\nexport function useOptionalCapability<T>(token: CapabilityToken<T>): T | null {\n const scoped = useContext(DocumentScopeCtx);\n return useKernelValue((k) => k.tryCapability(token, scoped ?? undefined));\n}\n\n/** Subscribe to a selector over a (document-resolved) capability. */\nexport function useSelector<C, R>(\n token: CapabilityToken<C>,\n select: (cap: C) => R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const cap = useCapability(token);\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n const next = select(cap);\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\n/**\n * Null-safe `useSelector`: `fallback` whenever the token can't resolve — no\n * provider, or a document-scoped token with no document. For chrome that stays\n * mounted across the empty-workspace state (a zoom readout, a mode band).\n * `useSelector` stays strict (fail-fast) for code that KNOWS a document exists\n * — e.g. anything inside a <DocumentGate>.\n *\n * The `select` guard also swallows reads through a capability whose document\n * closed between the store notification and this render — that teardown race\n * resolves to `fallback` for one frame, then re-renders against the new state.\n */\nexport function useOptionalSelector<C, R>(\n token: CapabilityToken<C>,\n select: (cap: C) => R,\n fallback: R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const cap = useOptionalCapability(token);\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n let next: R;\n if (cap === null) {\n next = fallback;\n } else {\n try {\n next = select(cap);\n } catch {\n next = fallback;\n }\n }\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\n/** The document registry (open/close/active/list), reactive. */\nexport function useDocuments() {\n const kernel = useKernel();\n const docs = useKernelValue((k) => k.documents.list(), docInfoListEquals);\n const activeId = useActiveDocumentId();\n return {\n docs,\n activeId,\n open: kernel.documents.open,\n unlock: kernel.documents.unlock,\n close: kernel.documents.close,\n setActive: kernel.documents.setActive,\n move: kernel.documents.move,\n swap: kernel.documents.swap,\n download: kernel.documents.download,\n downloadLayer: kernel.documents.downloadLayer,\n };\n}\n\n// `InitialDocument` is the KERNEL's type (re-exported via `export * from\n// '@embedpdf/core'` above) — one shared shape for every adapter.\n\nexport interface ViewerProps {\n /**\n * The engine, as an instance or a thunk. Engines construct synchronously and\n * boot lazily (`localEngine()` allocates nothing until first use), so\n * ownership follows the SHAPE of what you pass:\n *\n * - An **instance** (`engine={engine}`) is BORROWED: the Viewer uses it and\n * never destroys it, because you acquired it and therefore own it. The\n * common path — a module-scope `const engine = localEngine()` shared\n * across viewers and route changes. The Viewer calls `engine.warmup?.()`\n * on mount so the boot overlaps app initialization.\n * - A **thunk** (`engine={() => localEngine()}`) is VIEWER-OWNED: the\n * Viewer calls it on mount and `destroy()`s the result on unmount. Use it\n * for per-mount isolation (StrictMode/HMR-clean teardown, independent\n * multi-viewer engines).\n *\n * Init-only: captured on first render; later identity changes are ignored (dev warns).\n */\n engine: Engine | EngineFactory;\n /** Init-only: captured on first render; later identity changes are ignored (dev warns). */\n plugins: AnyPlugin[];\n /** Documents to open on startup (with optional tab names). Init-only. */\n initialDocuments?: InitialDocument[];\n fallback?: React.ReactNode;\n /** Rendered when kernel construction or `start()` fails. Without it a boot\n * failure renders nothing — but never a silent forever-fallback. */\n renderError?: (error: unknown) => React.ReactNode;\n children: React.ReactNode;\n}\n\ntype BootState =\n | { phase: 'booting'; kernel: Kernel | null }\n | { phase: 'ready'; kernel: Kernel }\n | { phase: 'error'; error: unknown };\n\n/**\n * Owns the kernel as an EFFECT-scoped resource: each effect setup creates and\n * starts exactly one kernel; each cleanup destroys exactly that one. That is\n * the contract StrictMode exercises (two kernels in dev, the first fully\n * destroyed) — the kernel itself is never restarted after destroy.\n *\n * The kernel is published to context the moment it exists — before `start()`\n * resolves — so the `fallback` can use workspace capabilities (i18n copy on a\n * loading screen) exactly as before. The one exception is the very first\n * render, which happens before the effect: it renders nothing. Children mount\n * once `start()` resolves — which never touches the engine, so the shell is\n * alive while WASM compiles or the transport connects. `initialDocuments`\n * open in the BACKGROUND and stream into the registry (`useDocuments()` is\n * reactive); per-document loading UI is the Stage's job, not a root gate.\n *\n * ENGINE OWNERSHIP. When `engine` is a thunk, the Viewer OWNS it: each effect\n * setup constructs one engine (construction is synchronous and inert — boot\n * happens lazily inside the engine) and each cleanup destroys exactly that one\n * — after the kernel, so handles close first. When `engine` is an instance it\n * is BORROWED and never destroyed here; the Viewer only calls `warmup?.()` so\n * the WASM/transport boot overlaps plugin initialization. StrictMode's\n * double-mount therefore constructs two independent thunk engines and tears\n * the first fully down, matching the kernel's own effect-scoped lifecycle.\n * That means dev-only double resource use (two worker spawns, two font\n * fetches, briefly overlapping) — deliberate, because it is exactly the\n * leak-detection contract StrictMode exists to exercise; production mounts\n * once and boots once.\n */\nexport function Viewer({\n engine,\n plugins,\n initialDocuments,\n fallback,\n renderError,\n children,\n}: ViewerProps) {\n // Init-only inputs: the kernel's lifetime is the component's lifetime, so a\n // changed engine/plugins identity cannot mean \"rebuild the workspace\" —\n // that would silently drop every open document. Capture once, warn in dev.\n const initial = useRef({ engine, plugins, initialDocuments });\n const warned = useRef(false);\n if (process.env.NODE_ENV !== 'production' && !warned.current) {\n if (initial.current.engine !== engine || initial.current.plugins !== plugins) {\n warned.current = true;\n console.warn(\n '[embedpdf] <Viewer> engine/plugins are init-only. A changed identity is ignored — ' +\n 'pass stable references (module scope, useState, or useMemo). ' +\n 'An inline `plugins={[...]}` array recreates its identity every render.',\n );\n }\n }\n\n const [boot, setBoot] = useState<BootState>({ phase: 'booting', kernel: null });\n useEffect(() => {\n const captured = initial.current;\n // A thunk is viewer-owned: call it now, destroy on unmount. An instance is\n // borrowed: use as-is, never destroy. Construction is synchronous and inert\n // either way — the engine boots lazily on first use — so kick `warmup()`\n // to overlap the WASM/transport boot with plugin initialization.\n const ownsEngine = typeof captured.engine === 'function';\n const engine: Engine = ownsEngine\n ? (captured.engine as EngineFactory)()\n : (captured.engine as Engine);\n engine.warmup?.();\n let kernel: Kernel;\n try {\n kernel = createKernel({ engine, plugins: captured.plugins });\n } catch (error) {\n setBoot({ phase: 'error', error }); // plan/graph errors surface, not throw mid-render\n if (ownsEngine) void engine.destroy();\n return;\n }\n let alive = true;\n setBoot({ phase: 'booting', kernel }); // context carries the kernel from this frame on\n kernel.start().then(\n () => {\n if (!alive) return; // unmounted mid-boot — don't open anything\n setBoot({ phase: 'ready', kernel });\n // Kernel-owned boot policy: all tabs appear immediately in array\n // order; the `active` entry (else the first) is selected; failures\n // surface as tab status. See DocumentsCapability.openAll.\n kernel.documents.openAll(captured.initialDocuments ?? []);\n },\n (error) => {\n if (alive) setBoot({ phase: 'error', error });\n },\n );\n return () => {\n alive = false;\n // Kernel first (closes every document handle), THEN the engine we own —\n // ownership follows acquisition. `engine.destroy()` joins an in-flight\n // boot (or no-ops if it never started), so an unmount mid-boot is safe.\n void kernel.destroy().then(() => {\n if (ownsEngine) void engine.destroy();\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n if (boot.phase === 'error') {\n return <>{renderError ? renderError(boot.error) : null}</>;\n }\n if (!boot.kernel) return null; // pre-effect first render only\n return (\n <KernelCtx.Provider value={boot.kernel}>\n {boot.phase === 'ready' ? children : (fallback ?? null)}\n </KernelCtx.Provider>\n );\n}\nexport const EmbedPDF = Viewer;\n\n/**\n * PageContext — the seam. A layer depends ONLY on this, never on the Stage. So the\n * same layer works inside a virtualized Stage and in a standalone <PageView>.\n */\nexport interface PageContextValue {\n documentId: string;\n /** Durable page identity (PDF object number) — use for keys / render / annotations. */\n pon: number;\n /** Display index (page N) — use for ordering / human-facing page numbers. */\n pageIndex: number;\n /**\n * Reserved chrome bands around the page (screen px per side). The page-chrome\n * slot renders into the outer box (content + frame); these thicknesses size\n * the bands — a label in the bottom band is `bottom:0; height: frame.bottom`.\n */\n frame: PageFrame;\n /**\n * The single bridge between PDF points, view px, and device px for this page.\n * Layers do ALL coordinate work through it — `pageToContent` to place content-\n * space overlays, `renderScale`/`deviceWidth` to render, `contentWidth` for\n * page-relative sizing. Never re-derive `x * scale` or `* dpr`.\n */\n transform: PageTransform;\n /** Client (screen) point → PDF point — the one platform-bound hit-test. */\n toPagePoint(clientX: number, clientY: number): Point;\n /** PDF/content point → client (screen) px — the exact inverse of `toPagePoint`\n * (rotation applied). Lets viewport-space UI (e.g. a selection menu) anchor to a\n * page point WITHOUT a Stage camera, so it works the same in `<PageView>`. */\n toClientPoint(p: Point): Point;\n /** PDF/content rect → client (screen) px AABB. Rect analog of `toClientPoint`\n * for upright viewport-space UI that frames a selected page region. */\n toClientRect(rect: Rect): Rect;\n /**\n * The page-view DEMAND for raster planning uses dependency inversion:\n * plugin-render defines the shape; the host that CREATED this\n * context fills it — as a PULL. The Stage host's getter closes over the\n * stage capability and reads `VisiblePage.visibleRect` live at call time\n * (visibility is the STAGE's data; adapters never re-derive camera math or\n * cache a copy). Three states, three meanings: a real sub-rect (visible),\n * a ZERO rect (stage host, page currently off-screen — want nothing), and\n * an undefined getter (stage-less `<PageView>` — whole page visible, which\n * a thumbnail-sized demand turns into \"never engages\" by arithmetic).\n */\n getViewDemand?: () => PageViewDemand;\n /**\n * The hosting VIEW's identity — the stage lens id (`stage.lensId()`) or a\n * per-instance PageView id. IDENTITY, not an option: per-view raster\n * planning (tiles) keys its state by this, so two views showing the SAME\n * page never fight over one plan (a thumbnail rail's never-engaging demand\n * must not disturb the main view's tiles). Every page context host must\n * say which view it is.\n */\n view: string;\n}\n\nconst PageCtx = createContext<PageContextValue | null>(null);\nexport const PageProvider = PageCtx.Provider;\n\nexport function usePage(): PageContextValue {\n const c = useContext(PageCtx);\n if (!c) throw new Error('usePage must be used inside <PageView> or a <Stage> page');\n return c;\n}\n\nexport function makePageContext(\n documentId: string,\n view: string,\n pon: number,\n pageIndex: number,\n frame: PageFrame,\n transform: PageTransform,\n getRect: () => DOMRect,\n getViewDemand?: () => PageViewDemand,\n): PageContextValue {\n return {\n documentId,\n view,\n pon,\n pageIndex,\n frame,\n transform,\n ...(getViewDemand ? { getViewDemand } : {}),\n toPagePoint: (cx, cy) => {\n // `getRect()` is the rotated content wrapper's axis-aligned bounding box =\n // the page's DISPLAY box on screen. Convert client → box-local view px,\n // then invert rotation + scale via the transform (verified once in geometry,\n // not re-derived per framework adapter).\n const r = getRect();\n return transform.viewToPage({ x: cx - r.left, y: cy - r.top });\n },\n toClientPoint: (p) => {\n // Exact inverse of `toPagePoint`: page/content point → display-box view px\n // (rotation applied by the transform), offset by the same live display-box\n // origin. So the two can never drift, in either <Stage> or <PageView>.\n const r = getRect();\n const v = transform.pageToView(p);\n return { x: r.left + v.x, y: r.top + v.y };\n },\n toClientRect: (rect) => {\n const r = getRect();\n const v = transform.pageToViewRect(rect);\n return { x: r.left + v.x, y: r.top + v.y, width: v.width, height: v.height };\n },\n };\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { S as useSelector, _ as useKernel, a as EmbedPDF, b as useOptionalSelector, c as Viewer, d as shallowArray, f as useActiveDocumentId, g as useDocuments, h as useDocumentStatus, i as DocumentScopeProps, l as ViewerProps, m as useDocumentId, n as DocumentGateProps, o as PageContextValue, p as useCapability, r as DocumentScope, s as PageProvider, t as DocumentGate, u as makePageContext, v as useKernelValue, x as usePage, y as useOptionalCapability } from "./runtime-CMxIuF2O.cjs";
1
+ import { S as useSelector, _ as useKernel, a as EmbedPDF, b as useOptionalSelector, c as Viewer, d as shallowArray, f as useActiveDocumentId, g as useDocuments, h as useDocumentStatus, i as DocumentScopeProps, l as ViewerProps, m as useDocumentId, n as DocumentGateProps, o as PageContextValue, p as useCapability, r as DocumentScope, s as PageProvider, t as DocumentGate, u as makePageContext, v as useKernelValue, x as usePage, y as useOptionalCapability } from "./runtime-D4TXEICX.cjs";
2
2
  export * from "@embedpdf/core";
3
3
  export { DocumentGate, DocumentGateProps, DocumentScope, DocumentScopeProps, EmbedPDF, PageContextValue, PageProvider, Viewer, ViewerProps, makePageContext, shallowArray, useActiveDocumentId, useCapability, useDocumentId, useDocumentStatus, useDocuments, useKernel, useKernelValue, useOptionalCapability, useOptionalSelector, usePage, useSelector };
package/dist/runtime.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { S as useSelector, _ as useKernel, a as EmbedPDF, b as useOptionalSelector, c as Viewer, d as shallowArray, f as useActiveDocumentId, g as useDocuments, h as useDocumentStatus, i as DocumentScopeProps, l as ViewerProps, m as useDocumentId, n as DocumentGateProps, o as PageContextValue, p as useCapability, r as DocumentScope, s as PageProvider, t as DocumentGate, u as makePageContext, v as useKernelValue, x as usePage, y as useOptionalCapability } from "./runtime-DV98q7I0.js";
1
+ import { S as useSelector, _ as useKernel, a as EmbedPDF, b as useOptionalSelector, c as Viewer, d as shallowArray, f as useActiveDocumentId, g as useDocuments, h as useDocumentStatus, i as DocumentScopeProps, l as ViewerProps, m as useDocumentId, n as DocumentGateProps, o as PageContextValue, p as useCapability, r as DocumentScope, s as PageProvider, t as DocumentGate, u as makePageContext, v as useKernelValue, x as usePage, y as useOptionalCapability } from "./runtime-Bh_9OtWo.js";
2
2
  export * from "@embedpdf/core";
3
3
  export { DocumentGate, DocumentGateProps, DocumentScope, DocumentScopeProps, EmbedPDF, PageContextValue, PageProvider, Viewer, ViewerProps, makePageContext, shallowArray, useActiveDocumentId, useCapability, useDocumentId, useDocumentStatus, useDocuments, useKernel, useKernelValue, useOptionalCapability, useOptionalSelector, usePage, useSelector };
package/dist/runtime.js CHANGED
@@ -246,9 +246,10 @@ function usePage() {
246
246
  if (!c) throw new Error("usePage must be used inside <PageView> or a <Stage> page");
247
247
  return c;
248
248
  }
249
- function makePageContext(documentId, pon, pageIndex, frame, transform, getRect, getViewDemand) {
249
+ function makePageContext(documentId, view, pon, pageIndex, frame, transform, getRect, getViewDemand) {
250
250
  return {
251
251
  documentId,
252
+ view,
252
253
  pon,
253
254
  pageIndex,
254
255
  frame,
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","names":[],"sources":["../src/runtime.tsx"],"sourcesContent":["/**\n * @embedpdf/react — the generic binding.\n *\n * Binds the kernel's one change stream to React (useSyncExternalStore), resolves\n * capabilities (document-scoped ones against the active or `<DocumentScope>`-given\n * document), and provides the page coordinate context. Every plugin and layer rides\n * on this — there is no per-plugin framework code.\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/core';\nimport * as React from 'react';\nimport {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n useSyncExternalStore,\n} from 'react';\nimport { createKernel, docInfoListEquals } from '@embedpdf/core';\nimport type {\n AnyPlugin,\n CapabilityToken,\n Engine,\n EngineFactory,\n InitialDocument,\n Kernel,\n} from '@embedpdf/core';\n// Pure coordinate math from the geometry base — NOT from stage-core. The\n// PageContext seam stays stage-agnostic (it must also serve standalone PageView).\nimport type { PageFrame, PageTransform, Point, Rect } from '@embedpdf/core-geometry';\nimport type { PageViewDemand } from '@embedpdf/plugin-render';\n\nconst KernelCtx = createContext<Kernel | null>(null);\n/** The document a subtree is bound to. null => use the active document. */\nconst DocumentScopeCtx = createContext<string | null>(null);\n\nexport function useKernel(): Kernel {\n const k = useContext(KernelCtx);\n if (!k) throw new Error('useKernel must be used within <Viewer>/<EmbedPDF>');\n return k;\n}\n\nexport const shallowArray = <T,>(a: readonly T[], b: readonly T[]): boolean =>\n a === b || (a.length === b.length && a.every((x, i) => x === b[i]));\n\n/** Read a value derived from the kernel, cached by equality (no tearing loop). */\nexport function useKernelValue<R>(\n select: (k: Kernel) => R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n const next = select(kernel);\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\nexport function useActiveDocumentId(): string | null {\n return useKernelValue((k) => k.documents.activeId());\n}\n\n/** The document id for this subtree: the nearest <DocumentScope>, else the active doc. */\nexport function useDocumentId(): string | null {\n const scoped = useContext(DocumentScopeCtx);\n const active = useActiveDocumentId();\n return scoped ?? active;\n}\n\nexport interface DocumentScopeProps {\n id: string;\n children: React.ReactNode;\n}\n/** Bind a subtree to a specific document (panes, comparison). */\nexport function DocumentScope({ id, children }: DocumentScopeProps) {\n return <DocumentScopeCtx.Provider value={id}>{children}</DocumentScopeCtx.Provider>;\n}\n\nexport interface DocumentGateProps {\n /** Shown while this subtree has NO document (empty workspace, docs still opening). */\n fallback?: React.ReactNode;\n children: React.ReactNode;\n}\n/**\n * Render children only while this subtree has a READY document — the\n * structural way to say \"this UI is defined over a document\". An empty\n * workspace is a legitimate, designable state (the Viewer no longer blocks on\n * documents so chrome can render at t≈0): workspace-scoped UI (toolbars,\n * commands, i18n) lives OUTSIDE the gate; document-scoped UI (Stage, panels,\n * page chrome) lives inside it, or reads through `useOptionalSelector`.\n * A `loading`/`locked`/`error` tab renders `fallback` — so the gate's\n * fallback doubles as the per-tab boot state; richer chrome (a password\n * prompt, an error pane) branches on `useDocumentStatus()` beside the gate.\n * Sibling of <DocumentScope>, which picks WHICH document; this one handles\n * WHETHER.\n */\nexport function DocumentGate({ fallback = null, children }: DocumentGateProps) {\n const docId = useDocumentId();\n const ready = useKernelValue((k) => (docId ? k.documents.get(docId)?.status === 'ready' : false));\n return <>{ready ? children : fallback}</>;\n}\n\n/** Lifecycle status of this subtree's document (loading/locked/ready/error),\n * or null with no document. The password prompt and error panes key off it. */\nexport function useDocumentStatus() {\n const docId = useDocumentId();\n return useKernelValue((k) => (docId ? (k.documents.get(docId)?.status ?? null) : null));\n}\n\n/**\n * Resolve a capability by token, binding document-scoped ones to this\n * subtree's document. Resolution is a REACTIVE read (`tryCapability` through\n * the kernel's one change stream), not a memoized call — under the\n * request-time lifecycle a document can become resolvable while its id stays\n * the same, so any id-keyed cache goes stale; subscribing makes staleness\n * structurally impossible. Fail-fast: while unresolvable, this re-runs the\n * strict resolver so the kernel's truthful reason (`no capability` / `no\n * document` / `document is loading|locked`) is what throws.\n */\nexport function useCapability<T>(token: CapabilityToken<T>): T {\n const kernel = useKernel();\n const scoped = useContext(DocumentScopeCtx);\n const cap = useKernelValue((k) => k.tryCapability(token, scoped ?? undefined));\n return cap ?? kernel.capability(token, scoped ?? undefined);\n}\n\n/** Like `useCapability`, but null while the token can't resolve (no plugin,\n * no document, or a document that isn't ready yet). */\nexport function useOptionalCapability<T>(token: CapabilityToken<T>): T | null {\n const scoped = useContext(DocumentScopeCtx);\n return useKernelValue((k) => k.tryCapability(token, scoped ?? undefined));\n}\n\n/** Subscribe to a selector over a (document-resolved) capability. */\nexport function useSelector<C, R>(\n token: CapabilityToken<C>,\n select: (cap: C) => R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const cap = useCapability(token);\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n const next = select(cap);\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\n/**\n * Null-safe `useSelector`: `fallback` whenever the token can't resolve — no\n * provider, or a document-scoped token with no document. For chrome that stays\n * mounted across the empty-workspace state (a zoom readout, a mode band).\n * `useSelector` stays strict (fail-fast) for code that KNOWS a document exists\n * — e.g. anything inside a <DocumentGate>.\n *\n * The `select` guard also swallows reads through a capability whose document\n * closed between the store notification and this render — that teardown race\n * resolves to `fallback` for one frame, then re-renders against the new state.\n */\nexport function useOptionalSelector<C, R>(\n token: CapabilityToken<C>,\n select: (cap: C) => R,\n fallback: R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const cap = useOptionalCapability(token);\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n let next: R;\n if (cap === null) {\n next = fallback;\n } else {\n try {\n next = select(cap);\n } catch {\n next = fallback;\n }\n }\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\n/** The document registry (open/close/active/list), reactive. */\nexport function useDocuments() {\n const kernel = useKernel();\n const docs = useKernelValue((k) => k.documents.list(), docInfoListEquals);\n const activeId = useActiveDocumentId();\n return {\n docs,\n activeId,\n open: kernel.documents.open,\n unlock: kernel.documents.unlock,\n close: kernel.documents.close,\n setActive: kernel.documents.setActive,\n move: kernel.documents.move,\n swap: kernel.documents.swap,\n download: kernel.documents.download,\n downloadLayer: kernel.documents.downloadLayer,\n };\n}\n\n// `InitialDocument` is the KERNEL's type (re-exported via `export * from\n// '@embedpdf/core'` above) — one shared shape for every adapter.\n\nexport interface ViewerProps {\n /**\n * The engine, as an instance or a thunk. Engines construct synchronously and\n * boot lazily (`localEngine()` allocates nothing until first use), so\n * ownership follows the SHAPE of what you pass:\n *\n * - An **instance** (`engine={engine}`) is BORROWED: the Viewer uses it and\n * never destroys it, because you acquired it and therefore own it. The\n * common path — a module-scope `const engine = localEngine()` shared\n * across viewers and route changes. The Viewer calls `engine.warmup?.()`\n * on mount so the boot overlaps app initialization.\n * - A **thunk** (`engine={() => localEngine()}`) is VIEWER-OWNED: the\n * Viewer calls it on mount and `destroy()`s the result on unmount. Use it\n * for per-mount isolation (StrictMode/HMR-clean teardown, independent\n * multi-viewer engines).\n *\n * Init-only: captured on first render; later identity changes are ignored (dev warns).\n */\n engine: Engine | EngineFactory;\n /** Init-only: captured on first render; later identity changes are ignored (dev warns). */\n plugins: AnyPlugin[];\n /** Documents to open on startup (with optional tab names). Init-only. */\n initialDocuments?: InitialDocument[];\n fallback?: React.ReactNode;\n /** Rendered when kernel construction or `start()` fails. Without it a boot\n * failure renders nothing — but never a silent forever-fallback. */\n renderError?: (error: unknown) => React.ReactNode;\n children: React.ReactNode;\n}\n\ntype BootState =\n | { phase: 'booting'; kernel: Kernel | null }\n | { phase: 'ready'; kernel: Kernel }\n | { phase: 'error'; error: unknown };\n\n/**\n * Owns the kernel as an EFFECT-scoped resource: each effect setup creates and\n * starts exactly one kernel; each cleanup destroys exactly that one. That is\n * the contract StrictMode exercises (two kernels in dev, the first fully\n * destroyed) — the kernel itself is never restarted after destroy.\n *\n * The kernel is published to context the moment it exists — before `start()`\n * resolves — so the `fallback` can use workspace capabilities (i18n copy on a\n * loading screen) exactly as before. The one exception is the very first\n * render, which happens before the effect: it renders nothing. Children mount\n * once `start()` resolves — which never touches the engine, so the shell is\n * alive while WASM compiles or the transport connects. `initialDocuments`\n * open in the BACKGROUND and stream into the registry (`useDocuments()` is\n * reactive); per-document loading UI is the Stage's job, not a root gate.\n *\n * ENGINE OWNERSHIP. When `engine` is a thunk, the Viewer OWNS it: each effect\n * setup constructs one engine (construction is synchronous and inert — boot\n * happens lazily inside the engine) and each cleanup destroys exactly that one\n * — after the kernel, so handles close first. When `engine` is an instance it\n * is BORROWED and never destroyed here; the Viewer only calls `warmup?.()` so\n * the WASM/transport boot overlaps plugin initialization. StrictMode's\n * double-mount therefore constructs two independent thunk engines and tears\n * the first fully down, matching the kernel's own effect-scoped lifecycle.\n * That means dev-only double resource use (two worker spawns, two font\n * fetches, briefly overlapping) — deliberate, because it is exactly the\n * leak-detection contract StrictMode exists to exercise; production mounts\n * once and boots once.\n */\nexport function Viewer({\n engine,\n plugins,\n initialDocuments,\n fallback,\n renderError,\n children,\n}: ViewerProps) {\n // Init-only inputs: the kernel's lifetime is the component's lifetime, so a\n // changed engine/plugins identity cannot mean \"rebuild the workspace\" —\n // that would silently drop every open document. Capture once, warn in dev.\n const initial = useRef({ engine, plugins, initialDocuments });\n const warned = useRef(false);\n if (process.env.NODE_ENV !== 'production' && !warned.current) {\n if (initial.current.engine !== engine || initial.current.plugins !== plugins) {\n warned.current = true;\n console.warn(\n '[embedpdf] <Viewer> engine/plugins are init-only. A changed identity is ignored — ' +\n 'pass stable references (module scope, useState, or useMemo). ' +\n 'An inline `plugins={[...]}` array recreates its identity every render.',\n );\n }\n }\n\n const [boot, setBoot] = useState<BootState>({ phase: 'booting', kernel: null });\n useEffect(() => {\n const captured = initial.current;\n // A thunk is viewer-owned: call it now, destroy on unmount. An instance is\n // borrowed: use as-is, never destroy. Construction is synchronous and inert\n // either way — the engine boots lazily on first use — so kick `warmup()`\n // to overlap the WASM/transport boot with plugin initialization.\n const ownsEngine = typeof captured.engine === 'function';\n const engine: Engine = ownsEngine\n ? (captured.engine as EngineFactory)()\n : (captured.engine as Engine);\n engine.warmup?.();\n let kernel: Kernel;\n try {\n kernel = createKernel({ engine, plugins: captured.plugins });\n } catch (error) {\n setBoot({ phase: 'error', error }); // plan/graph errors surface, not throw mid-render\n if (ownsEngine) void engine.destroy();\n return;\n }\n let alive = true;\n setBoot({ phase: 'booting', kernel }); // context carries the kernel from this frame on\n kernel.start().then(\n () => {\n if (!alive) return; // unmounted mid-boot — don't open anything\n setBoot({ phase: 'ready', kernel });\n // Kernel-owned boot policy: all tabs appear immediately in array\n // order; the `active` entry (else the first) is selected; failures\n // surface as tab status. See DocumentsCapability.openAll.\n kernel.documents.openAll(captured.initialDocuments ?? []);\n },\n (error) => {\n if (alive) setBoot({ phase: 'error', error });\n },\n );\n return () => {\n alive = false;\n // Kernel first (closes every document handle), THEN the engine we own —\n // ownership follows acquisition. `engine.destroy()` joins an in-flight\n // boot (or no-ops if it never started), so an unmount mid-boot is safe.\n void kernel.destroy().then(() => {\n if (ownsEngine) void engine.destroy();\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n if (boot.phase === 'error') {\n return <>{renderError ? renderError(boot.error) : null}</>;\n }\n if (!boot.kernel) return null; // pre-effect first render only\n return (\n <KernelCtx.Provider value={boot.kernel}>\n {boot.phase === 'ready' ? children : (fallback ?? null)}\n </KernelCtx.Provider>\n );\n}\nexport const EmbedPDF = Viewer;\n\n/**\n * PageContext — the seam. A layer depends ONLY on this, never on the Stage. So the\n * same layer works inside a virtualized Stage and in a standalone <PageView>.\n */\nexport interface PageContextValue {\n documentId: string;\n /** Durable page identity (PDF object number) — use for keys / render / annotations. */\n pon: number;\n /** Display index (page N) — use for ordering / human-facing page numbers. */\n pageIndex: number;\n /**\n * Reserved chrome bands around the page (screen px per side). The page-chrome\n * slot renders into the outer box (content + frame); these thicknesses size\n * the bands — a label in the bottom band is `bottom:0; height: frame.bottom`.\n */\n frame: PageFrame;\n /**\n * The single bridge between PDF points, view px, and device px for this page.\n * Layers do ALL coordinate work through it — `pageToContent` to place content-\n * space overlays, `renderScale`/`deviceWidth` to render, `contentWidth` for\n * page-relative sizing. Never re-derive `x * scale` or `* dpr`.\n */\n transform: PageTransform;\n /** Client (screen) point → PDF point — the one platform-bound hit-test. */\n toPagePoint(clientX: number, clientY: number): Point;\n /** PDF/content point → client (screen) px — the exact inverse of `toPagePoint`\n * (rotation applied). Lets viewport-space UI (e.g. a selection menu) anchor to a\n * page point WITHOUT a Stage camera, so it works the same in `<PageView>`. */\n toClientPoint(p: Point): Point;\n /** PDF/content rect → client (screen) px AABB. Rect analog of `toClientPoint`\n * for upright viewport-space UI that frames a selected page region. */\n toClientRect(rect: Rect): Rect;\n /**\n * The page-view DEMAND for raster planning uses dependency inversion:\n * plugin-render defines the shape; the host that CREATED this\n * context fills it — as a PULL. The Stage host's getter closes over the\n * stage capability and reads `VisiblePage.visibleRect` live at call time\n * (visibility is the STAGE's data; adapters never re-derive camera math or\n * cache a copy). Three states, three meanings: a real sub-rect (visible),\n * a ZERO rect (stage host, page currently off-screen — want nothing), and\n * an undefined getter (stage-less `<PageView>` — whole page visible, which\n * a thumbnail-sized demand turns into \"never engages\" by arithmetic).\n */\n getViewDemand?: () => PageViewDemand;\n}\n\nconst PageCtx = createContext<PageContextValue | null>(null);\nexport const PageProvider = PageCtx.Provider;\n\nexport function usePage(): PageContextValue {\n const c = useContext(PageCtx);\n if (!c) throw new Error('usePage must be used inside <PageView> or a <Stage> page');\n return c;\n}\n\nexport function makePageContext(\n documentId: string,\n pon: number,\n pageIndex: number,\n frame: PageFrame,\n transform: PageTransform,\n getRect: () => DOMRect,\n getViewDemand?: () => PageViewDemand,\n): PageContextValue {\n return {\n documentId,\n pon,\n pageIndex,\n frame,\n transform,\n ...(getViewDemand ? { getViewDemand } : {}),\n toPagePoint: (cx, cy) => {\n // `getRect()` is the rotated content wrapper's axis-aligned bounding box =\n // the page's DISPLAY box on screen. Convert client → box-local view px,\n // then invert rotation + scale via the transform (verified once in geometry,\n // not re-derived per framework adapter).\n const r = getRect();\n return transform.viewToPage({ x: cx - r.left, y: cy - r.top });\n },\n toClientPoint: (p) => {\n // Exact inverse of `toPagePoint`: page/content point → display-box view px\n // (rotation applied by the transform), offset by the same live display-box\n // origin. So the two can never drift, in either <Stage> or <PageView>.\n const r = getRect();\n const v = transform.pageToView(p);\n return { x: r.left + v.x, y: r.top + v.y };\n },\n toClientRect: (rect) => {\n const r = getRect();\n const v = transform.pageToViewRect(rect);\n return { x: r.left + v.x, y: r.top + v.y, width: v.width, height: v.height };\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runtime.js","names":[],"sources":["../src/runtime.tsx"],"sourcesContent":["/**\n * @embedpdf/react — the generic binding.\n *\n * Binds the kernel's one change stream to React (useSyncExternalStore), resolves\n * capabilities (document-scoped ones against the active or `<DocumentScope>`-given\n * document), and provides the page coordinate context. Every plugin and layer rides\n * on this — there is no per-plugin framework code.\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/core';\nimport * as React from 'react';\nimport {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n useSyncExternalStore,\n} from 'react';\nimport { createKernel, docInfoListEquals } from '@embedpdf/core';\nimport type {\n AnyPlugin,\n CapabilityToken,\n Engine,\n EngineFactory,\n InitialDocument,\n Kernel,\n} from '@embedpdf/core';\n// Pure coordinate math from the geometry base — NOT from stage-core. The\n// PageContext seam stays stage-agnostic (it must also serve standalone PageView).\nimport type { PageFrame, PageTransform, Point, Rect } from '@embedpdf/core-geometry';\nimport type { PageViewDemand } from '@embedpdf/plugin-render';\n\nconst KernelCtx = createContext<Kernel | null>(null);\n/** The document a subtree is bound to. null => use the active document. */\nconst DocumentScopeCtx = createContext<string | null>(null);\n\nexport function useKernel(): Kernel {\n const k = useContext(KernelCtx);\n if (!k) throw new Error('useKernel must be used within <Viewer>/<EmbedPDF>');\n return k;\n}\n\nexport const shallowArray = <T,>(a: readonly T[], b: readonly T[]): boolean =>\n a === b || (a.length === b.length && a.every((x, i) => x === b[i]));\n\n/** Read a value derived from the kernel, cached by equality (no tearing loop). */\nexport function useKernelValue<R>(\n select: (k: Kernel) => R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n const next = select(kernel);\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\nexport function useActiveDocumentId(): string | null {\n return useKernelValue((k) => k.documents.activeId());\n}\n\n/** The document id for this subtree: the nearest <DocumentScope>, else the active doc. */\nexport function useDocumentId(): string | null {\n const scoped = useContext(DocumentScopeCtx);\n const active = useActiveDocumentId();\n return scoped ?? active;\n}\n\nexport interface DocumentScopeProps {\n id: string;\n children: React.ReactNode;\n}\n/** Bind a subtree to a specific document (panes, comparison). */\nexport function DocumentScope({ id, children }: DocumentScopeProps) {\n return <DocumentScopeCtx.Provider value={id}>{children}</DocumentScopeCtx.Provider>;\n}\n\nexport interface DocumentGateProps {\n /** Shown while this subtree has NO document (empty workspace, docs still opening). */\n fallback?: React.ReactNode;\n children: React.ReactNode;\n}\n/**\n * Render children only while this subtree has a READY document — the\n * structural way to say \"this UI is defined over a document\". An empty\n * workspace is a legitimate, designable state (the Viewer no longer blocks on\n * documents so chrome can render at t≈0): workspace-scoped UI (toolbars,\n * commands, i18n) lives OUTSIDE the gate; document-scoped UI (Stage, panels,\n * page chrome) lives inside it, or reads through `useOptionalSelector`.\n * A `loading`/`locked`/`error` tab renders `fallback` — so the gate's\n * fallback doubles as the per-tab boot state; richer chrome (a password\n * prompt, an error pane) branches on `useDocumentStatus()` beside the gate.\n * Sibling of <DocumentScope>, which picks WHICH document; this one handles\n * WHETHER.\n */\nexport function DocumentGate({ fallback = null, children }: DocumentGateProps) {\n const docId = useDocumentId();\n const ready = useKernelValue((k) => (docId ? k.documents.get(docId)?.status === 'ready' : false));\n return <>{ready ? children : fallback}</>;\n}\n\n/** Lifecycle status of this subtree's document (loading/locked/ready/error),\n * or null with no document. The password prompt and error panes key off it. */\nexport function useDocumentStatus() {\n const docId = useDocumentId();\n return useKernelValue((k) => (docId ? (k.documents.get(docId)?.status ?? null) : null));\n}\n\n/**\n * Resolve a capability by token, binding document-scoped ones to this\n * subtree's document. Resolution is a REACTIVE read (`tryCapability` through\n * the kernel's one change stream), not a memoized call — under the\n * request-time lifecycle a document can become resolvable while its id stays\n * the same, so any id-keyed cache goes stale; subscribing makes staleness\n * structurally impossible. Fail-fast: while unresolvable, this re-runs the\n * strict resolver so the kernel's truthful reason (`no capability` / `no\n * document` / `document is loading|locked`) is what throws.\n */\nexport function useCapability<T>(token: CapabilityToken<T>): T {\n const kernel = useKernel();\n const scoped = useContext(DocumentScopeCtx);\n const cap = useKernelValue((k) => k.tryCapability(token, scoped ?? undefined));\n return cap ?? kernel.capability(token, scoped ?? undefined);\n}\n\n/** Like `useCapability`, but null while the token can't resolve (no plugin,\n * no document, or a document that isn't ready yet). */\nexport function useOptionalCapability<T>(token: CapabilityToken<T>): T | null {\n const scoped = useContext(DocumentScopeCtx);\n return useKernelValue((k) => k.tryCapability(token, scoped ?? undefined));\n}\n\n/** Subscribe to a selector over a (document-resolved) capability. */\nexport function useSelector<C, R>(\n token: CapabilityToken<C>,\n select: (cap: C) => R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const cap = useCapability(token);\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n const next = select(cap);\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\n/**\n * Null-safe `useSelector`: `fallback` whenever the token can't resolve — no\n * provider, or a document-scoped token with no document. For chrome that stays\n * mounted across the empty-workspace state (a zoom readout, a mode band).\n * `useSelector` stays strict (fail-fast) for code that KNOWS a document exists\n * — e.g. anything inside a <DocumentGate>.\n *\n * The `select` guard also swallows reads through a capability whose document\n * closed between the store notification and this render — that teardown race\n * resolves to `fallback` for one frame, then re-renders against the new state.\n */\nexport function useOptionalSelector<C, R>(\n token: CapabilityToken<C>,\n select: (cap: C) => R,\n fallback: R,\n isEqual: (a: R, b: R) => boolean = Object.is,\n): R {\n const kernel = useKernel();\n const cap = useOptionalCapability(token);\n const last = useRef<{ v: R } | null>(null);\n const get = () => {\n let next: R;\n if (cap === null) {\n next = fallback;\n } else {\n try {\n next = select(cap);\n } catch {\n next = fallback;\n }\n }\n if (last.current && isEqual(last.current.v, next)) return last.current.v;\n last.current = { v: next };\n return next;\n };\n return useSyncExternalStore(kernel.subscribe, get, get);\n}\n\n/** The document registry (open/close/active/list), reactive. */\nexport function useDocuments() {\n const kernel = useKernel();\n const docs = useKernelValue((k) => k.documents.list(), docInfoListEquals);\n const activeId = useActiveDocumentId();\n return {\n docs,\n activeId,\n open: kernel.documents.open,\n unlock: kernel.documents.unlock,\n close: kernel.documents.close,\n setActive: kernel.documents.setActive,\n move: kernel.documents.move,\n swap: kernel.documents.swap,\n download: kernel.documents.download,\n downloadLayer: kernel.documents.downloadLayer,\n };\n}\n\n// `InitialDocument` is the KERNEL's type (re-exported via `export * from\n// '@embedpdf/core'` above) — one shared shape for every adapter.\n\nexport interface ViewerProps {\n /**\n * The engine, as an instance or a thunk. Engines construct synchronously and\n * boot lazily (`localEngine()` allocates nothing until first use), so\n * ownership follows the SHAPE of what you pass:\n *\n * - An **instance** (`engine={engine}`) is BORROWED: the Viewer uses it and\n * never destroys it, because you acquired it and therefore own it. The\n * common path — a module-scope `const engine = localEngine()` shared\n * across viewers and route changes. The Viewer calls `engine.warmup?.()`\n * on mount so the boot overlaps app initialization.\n * - A **thunk** (`engine={() => localEngine()}`) is VIEWER-OWNED: the\n * Viewer calls it on mount and `destroy()`s the result on unmount. Use it\n * for per-mount isolation (StrictMode/HMR-clean teardown, independent\n * multi-viewer engines).\n *\n * Init-only: captured on first render; later identity changes are ignored (dev warns).\n */\n engine: Engine | EngineFactory;\n /** Init-only: captured on first render; later identity changes are ignored (dev warns). */\n plugins: AnyPlugin[];\n /** Documents to open on startup (with optional tab names). Init-only. */\n initialDocuments?: InitialDocument[];\n fallback?: React.ReactNode;\n /** Rendered when kernel construction or `start()` fails. Without it a boot\n * failure renders nothing — but never a silent forever-fallback. */\n renderError?: (error: unknown) => React.ReactNode;\n children: React.ReactNode;\n}\n\ntype BootState =\n | { phase: 'booting'; kernel: Kernel | null }\n | { phase: 'ready'; kernel: Kernel }\n | { phase: 'error'; error: unknown };\n\n/**\n * Owns the kernel as an EFFECT-scoped resource: each effect setup creates and\n * starts exactly one kernel; each cleanup destroys exactly that one. That is\n * the contract StrictMode exercises (two kernels in dev, the first fully\n * destroyed) — the kernel itself is never restarted after destroy.\n *\n * The kernel is published to context the moment it exists — before `start()`\n * resolves — so the `fallback` can use workspace capabilities (i18n copy on a\n * loading screen) exactly as before. The one exception is the very first\n * render, which happens before the effect: it renders nothing. Children mount\n * once `start()` resolves — which never touches the engine, so the shell is\n * alive while WASM compiles or the transport connects. `initialDocuments`\n * open in the BACKGROUND and stream into the registry (`useDocuments()` is\n * reactive); per-document loading UI is the Stage's job, not a root gate.\n *\n * ENGINE OWNERSHIP. When `engine` is a thunk, the Viewer OWNS it: each effect\n * setup constructs one engine (construction is synchronous and inert — boot\n * happens lazily inside the engine) and each cleanup destroys exactly that one\n * — after the kernel, so handles close first. When `engine` is an instance it\n * is BORROWED and never destroyed here; the Viewer only calls `warmup?.()` so\n * the WASM/transport boot overlaps plugin initialization. StrictMode's\n * double-mount therefore constructs two independent thunk engines and tears\n * the first fully down, matching the kernel's own effect-scoped lifecycle.\n * That means dev-only double resource use (two worker spawns, two font\n * fetches, briefly overlapping) — deliberate, because it is exactly the\n * leak-detection contract StrictMode exists to exercise; production mounts\n * once and boots once.\n */\nexport function Viewer({\n engine,\n plugins,\n initialDocuments,\n fallback,\n renderError,\n children,\n}: ViewerProps) {\n // Init-only inputs: the kernel's lifetime is the component's lifetime, so a\n // changed engine/plugins identity cannot mean \"rebuild the workspace\" —\n // that would silently drop every open document. Capture once, warn in dev.\n const initial = useRef({ engine, plugins, initialDocuments });\n const warned = useRef(false);\n if (process.env.NODE_ENV !== 'production' && !warned.current) {\n if (initial.current.engine !== engine || initial.current.plugins !== plugins) {\n warned.current = true;\n console.warn(\n '[embedpdf] <Viewer> engine/plugins are init-only. A changed identity is ignored — ' +\n 'pass stable references (module scope, useState, or useMemo). ' +\n 'An inline `plugins={[...]}` array recreates its identity every render.',\n );\n }\n }\n\n const [boot, setBoot] = useState<BootState>({ phase: 'booting', kernel: null });\n useEffect(() => {\n const captured = initial.current;\n // A thunk is viewer-owned: call it now, destroy on unmount. An instance is\n // borrowed: use as-is, never destroy. Construction is synchronous and inert\n // either way — the engine boots lazily on first use — so kick `warmup()`\n // to overlap the WASM/transport boot with plugin initialization.\n const ownsEngine = typeof captured.engine === 'function';\n const engine: Engine = ownsEngine\n ? (captured.engine as EngineFactory)()\n : (captured.engine as Engine);\n engine.warmup?.();\n let kernel: Kernel;\n try {\n kernel = createKernel({ engine, plugins: captured.plugins });\n } catch (error) {\n setBoot({ phase: 'error', error }); // plan/graph errors surface, not throw mid-render\n if (ownsEngine) void engine.destroy();\n return;\n }\n let alive = true;\n setBoot({ phase: 'booting', kernel }); // context carries the kernel from this frame on\n kernel.start().then(\n () => {\n if (!alive) return; // unmounted mid-boot — don't open anything\n setBoot({ phase: 'ready', kernel });\n // Kernel-owned boot policy: all tabs appear immediately in array\n // order; the `active` entry (else the first) is selected; failures\n // surface as tab status. See DocumentsCapability.openAll.\n kernel.documents.openAll(captured.initialDocuments ?? []);\n },\n (error) => {\n if (alive) setBoot({ phase: 'error', error });\n },\n );\n return () => {\n alive = false;\n // Kernel first (closes every document handle), THEN the engine we own —\n // ownership follows acquisition. `engine.destroy()` joins an in-flight\n // boot (or no-ops if it never started), so an unmount mid-boot is safe.\n void kernel.destroy().then(() => {\n if (ownsEngine) void engine.destroy();\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n if (boot.phase === 'error') {\n return <>{renderError ? renderError(boot.error) : null}</>;\n }\n if (!boot.kernel) return null; // pre-effect first render only\n return (\n <KernelCtx.Provider value={boot.kernel}>\n {boot.phase === 'ready' ? children : (fallback ?? null)}\n </KernelCtx.Provider>\n );\n}\nexport const EmbedPDF = Viewer;\n\n/**\n * PageContext — the seam. A layer depends ONLY on this, never on the Stage. So the\n * same layer works inside a virtualized Stage and in a standalone <PageView>.\n */\nexport interface PageContextValue {\n documentId: string;\n /** Durable page identity (PDF object number) — use for keys / render / annotations. */\n pon: number;\n /** Display index (page N) — use for ordering / human-facing page numbers. */\n pageIndex: number;\n /**\n * Reserved chrome bands around the page (screen px per side). The page-chrome\n * slot renders into the outer box (content + frame); these thicknesses size\n * the bands — a label in the bottom band is `bottom:0; height: frame.bottom`.\n */\n frame: PageFrame;\n /**\n * The single bridge between PDF points, view px, and device px for this page.\n * Layers do ALL coordinate work through it — `pageToContent` to place content-\n * space overlays, `renderScale`/`deviceWidth` to render, `contentWidth` for\n * page-relative sizing. Never re-derive `x * scale` or `* dpr`.\n */\n transform: PageTransform;\n /** Client (screen) point → PDF point — the one platform-bound hit-test. */\n toPagePoint(clientX: number, clientY: number): Point;\n /** PDF/content point → client (screen) px — the exact inverse of `toPagePoint`\n * (rotation applied). Lets viewport-space UI (e.g. a selection menu) anchor to a\n * page point WITHOUT a Stage camera, so it works the same in `<PageView>`. */\n toClientPoint(p: Point): Point;\n /** PDF/content rect → client (screen) px AABB. Rect analog of `toClientPoint`\n * for upright viewport-space UI that frames a selected page region. */\n toClientRect(rect: Rect): Rect;\n /**\n * The page-view DEMAND for raster planning uses dependency inversion:\n * plugin-render defines the shape; the host that CREATED this\n * context fills it — as a PULL. The Stage host's getter closes over the\n * stage capability and reads `VisiblePage.visibleRect` live at call time\n * (visibility is the STAGE's data; adapters never re-derive camera math or\n * cache a copy). Three states, three meanings: a real sub-rect (visible),\n * a ZERO rect (stage host, page currently off-screen — want nothing), and\n * an undefined getter (stage-less `<PageView>` — whole page visible, which\n * a thumbnail-sized demand turns into \"never engages\" by arithmetic).\n */\n getViewDemand?: () => PageViewDemand;\n /**\n * The hosting VIEW's identity — the stage lens id (`stage.lensId()`) or a\n * per-instance PageView id. IDENTITY, not an option: per-view raster\n * planning (tiles) keys its state by this, so two views showing the SAME\n * page never fight over one plan (a thumbnail rail's never-engaging demand\n * must not disturb the main view's tiles). Every page context host must\n * say which view it is.\n */\n view: string;\n}\n\nconst PageCtx = createContext<PageContextValue | null>(null);\nexport const PageProvider = PageCtx.Provider;\n\nexport function usePage(): PageContextValue {\n const c = useContext(PageCtx);\n if (!c) throw new Error('usePage must be used inside <PageView> or a <Stage> page');\n return c;\n}\n\nexport function makePageContext(\n documentId: string,\n view: string,\n pon: number,\n pageIndex: number,\n frame: PageFrame,\n transform: PageTransform,\n getRect: () => DOMRect,\n getViewDemand?: () => PageViewDemand,\n): PageContextValue {\n return {\n documentId,\n view,\n pon,\n pageIndex,\n frame,\n transform,\n ...(getViewDemand ? { getViewDemand } : {}),\n toPagePoint: (cx, cy) => {\n // `getRect()` is the rotated content wrapper's axis-aligned bounding box =\n // the page's DISPLAY box on screen. Convert client → box-local view px,\n // then invert rotation + scale via the transform (verified once in geometry,\n // not re-derived per framework adapter).\n const r = getRect();\n return transform.viewToPage({ x: cx - r.left, y: cy - r.top });\n },\n toClientPoint: (p) => {\n // Exact inverse of `toPagePoint`: page/content point → display-box view px\n // (rotation applied by the transform), offset by the same live display-box\n // origin. So the two can never drift, in either <Stage> or <PageView>.\n const r = getRect();\n const v = transform.pageToView(p);\n return { x: r.left + v.x, y: r.top + v.y };\n },\n toClientRect: (rect) => {\n const r = getRect();\n const v = transform.pageToViewRect(rect);\n return { x: r.left + v.x, y: r.top + v.y, width: v.width, height: v.height };\n },\n };\n}\n"],"mappings":"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"}
@@ -6,6 +6,8 @@ let react_jsx_runtime = require("react/jsx-runtime");
6
6
  let react = require("react");
7
7
  react = require_rolldown_runtime.__toESM(react, 1);
8
8
  let _embedpdf_web = require("@embedpdf/web");
9
+ let _embedpdf_plugin_stage = require("@embedpdf/plugin-stage");
10
+ let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
9
11
  let _embedpdf_plugin_selection = require("@embedpdf/plugin-selection");
10
12
  let _embedpdf_plugin_selection_internal = require("@embedpdf/plugin-selection/internal");
11
13
  //#region src/selection.tsx
@@ -76,6 +78,147 @@ function SelectionMenu({ children, gap = 8, placement = "top" }) {
76
78
  children
77
79
  });
78
80
  }
81
+ const sameEndpoints = (a, b) => {
82
+ if (a === b) return true;
83
+ if (!a || !b) return false;
84
+ return a.start.pon === b.start.pon && a.end.pon === b.end.pon && a.start.advance === b.start.advance && a.end.advance === b.end.advance && (0, _embedpdf_core_geometry.textQuadEquals)(a.start.glyphQuad, b.start.glyphQuad) && (0, _embedpdf_core_geometry.textQuadEquals)(a.end.glyphQuad, b.end.glyphQuad);
85
+ };
86
+ /** The plugin's structural view over this Stage: point-exact projection in,
87
+ * page resolution out. (`pageRectToScreen` is the AABB projector — upright
88
+ * overlays only — and would collapse exactly the orientation handles need.) */
89
+ const handleView = (stage) => ({
90
+ toOverlay: (pon, pt) => {
91
+ const world = stage.pageToWorld(pon, pt);
92
+ return world ? stage.toScreen(world) : null;
93
+ },
94
+ pageAt: (overlay) => stage.pageAt(overlay),
95
+ pointOnPage: (pon, overlay) => stage.pointOnPage(pon, overlay)
96
+ });
97
+ /**
98
+ * iOS-style draggable selection handles. Each is drawn the way the platform
99
+ * draws them: a thin caret BAR that forms the selection's own start/end edge
100
+ * — the boundary glyph's oriented edge, so rotated text and rotated pages
101
+ * carry the handle with them — capped by a screen-constant circle, above the
102
+ * first line at the start, below the last line at the end.
103
+ *
104
+ * On touch, where a caret drag isn't available, the handles ARE the way to
105
+ * grow or shrink a selection: a long-press selects a word, then each handle
106
+ * extends from the OPPOSITE endpoint, snapping to glyphs and crossing pages
107
+ * exactly like a pointer drag — it rides the same `beginAt`/`extendTo`
108
+ * gesture path, so highlights, menus, and commit signals all behave
109
+ * identically. Mount in the `<Stage>` overlay slot next to `<SelectionMenu>`;
110
+ * outside a Stage it renders nothing (a `PageView` has no camera to project
111
+ * through). Pointer-isolated, so grabbing a handle never pans the stage.
112
+ */
113
+ function SelectionHandles({ color = "#2196f3", token = _embedpdf_plugin_stage.StageToken }) {
114
+ const host = require_runtime.useCapability(_embedpdf_plugin_selection_internal.SelectionToken);
115
+ const stage = require_runtime.useOptionalCapability(token);
116
+ const selecting = require_runtime.useSelector(_embedpdf_plugin_selection.SelectionToken, (c) => c.isSelecting());
117
+ const visible = require_runtime.useSelector(_embedpdf_plugin_selection_internal.SelectionToken, (c) => c.highlightVisible());
118
+ const endpoints = require_runtime.useSelector(_embedpdf_plugin_selection_internal.SelectionToken, (c) => {
119
+ const s = c.snapshot();
120
+ if (!s.start || !s.end) return null;
121
+ return {
122
+ start: {
123
+ pon: s.start.pon,
124
+ glyphQuad: s.start.glyphQuad,
125
+ advance: s.start.advance
126
+ },
127
+ end: {
128
+ pon: s.end.pon,
129
+ glyphQuad: s.end.glyphQuad,
130
+ advance: s.end.advance
131
+ }
132
+ };
133
+ }, sameEndpoints);
134
+ require_runtime.useKernelValue(() => stage?.visiblePages() ?? null);
135
+ const [dragging, setDragging] = (0, react.useState)(null);
136
+ const armSource = (0, react.useRef)(null);
137
+ armSource.current = stage && endpoints ? {
138
+ stage,
139
+ endpoints
140
+ } : null;
141
+ const bindHandle = (0, react.useMemo)(() => {
142
+ const detach = {};
143
+ const make = (role) => (el) => {
144
+ detach[role]?.();
145
+ delete detach[role];
146
+ if (!el) return;
147
+ detach[role] = (0, _embedpdf_web.attachSelectionHandle)(el, { arm: () => {
148
+ const src = armSource.current;
149
+ if (!src) return null;
150
+ const view = handleView(src.stage);
151
+ const geom = (0, _embedpdf_plugin_selection.selectionHandleGeom)(view, src.endpoints[role], role);
152
+ if (!geom) return null;
153
+ const opposite = src.endpoints[role === "start" ? "end" : "start"];
154
+ const drag = (0, _embedpdf_plugin_selection.createSelectionHandleDrag)(host, view, opposite, src.endpoints[role].pon);
155
+ setDragging(role);
156
+ return {
157
+ base: {
158
+ x: (geom.bar.from.x + geom.bar.to.x) / 2,
159
+ y: (geom.bar.from.y + geom.bar.to.y) / 2
160
+ },
161
+ session: {
162
+ move: drag.move,
163
+ end: () => {
164
+ drag.end();
165
+ setDragging(null);
166
+ }
167
+ }
168
+ };
169
+ } });
170
+ };
171
+ return {
172
+ start: make("start"),
173
+ end: make("end")
174
+ };
175
+ }, [host]);
176
+ if (!stage || !endpoints || !visible) return null;
177
+ if (selecting && !dragging) return null;
178
+ const view = handleView(stage);
179
+ const renderHandle = (role) => {
180
+ const geom = (0, _embedpdf_plugin_selection.selectionHandleGeom)(view, endpoints[role], role);
181
+ if (!geom) return null;
182
+ const barTop = _embedpdf_plugin_selection.HANDLE_PAD + (role === "start" ? _embedpdf_plugin_selection.HANDLE_HEAD : 0);
183
+ const pivotX = _embedpdf_plugin_selection.HANDLE_PAD + _embedpdf_plugin_selection.HANDLE_BAR / 2;
184
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
185
+ ref: bindHandle[role],
186
+ style: {
187
+ position: "absolute",
188
+ left: geom.bar.from.x - pivotX,
189
+ top: geom.bar.from.y - barTop,
190
+ width: _embedpdf_plugin_selection.HANDLE_BAR + 2 * _embedpdf_plugin_selection.HANDLE_PAD,
191
+ height: geom.length + _embedpdf_plugin_selection.HANDLE_HEAD + 2 * _embedpdf_plugin_selection.HANDLE_PAD,
192
+ ...geom.upright ? null : {
193
+ transform: `rotate(${geom.rotation}deg)`,
194
+ transformOrigin: `${pivotX}px ${barTop}px`
195
+ },
196
+ touchAction: "none",
197
+ cursor: "grab",
198
+ pointerEvents: "auto"
199
+ },
200
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
201
+ position: "absolute",
202
+ left: _embedpdf_plugin_selection.HANDLE_PAD,
203
+ top: barTop,
204
+ width: _embedpdf_plugin_selection.HANDLE_BAR,
205
+ height: geom.length,
206
+ background: color,
207
+ borderRadius: _embedpdf_plugin_selection.HANDLE_BAR / 2
208
+ } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
209
+ position: "absolute",
210
+ left: pivotX - _embedpdf_plugin_selection.HANDLE_HEAD / 2,
211
+ top: role === "start" ? _embedpdf_plugin_selection.HANDLE_PAD : _embedpdf_plugin_selection.HANDLE_PAD + geom.length,
212
+ width: _embedpdf_plugin_selection.HANDLE_HEAD,
213
+ height: _embedpdf_plugin_selection.HANDLE_HEAD,
214
+ borderRadius: "50%",
215
+ background: color,
216
+ boxShadow: "0 1px 4px rgba(0, 0, 0, 0.35)"
217
+ } })]
218
+ }, role);
219
+ };
220
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [renderHandle("start"), renderHandle("end")] });
221
+ }
79
222
  /**
80
223
  * Mount ONCE per viewer to wire clipboard copy: prefetches the selected text
81
224
  * when the selection settles, answers the native `copy` event synchronously,
@@ -90,6 +233,7 @@ function SelectionClipboard({ prefetch } = {}) {
90
233
  }
91
234
  //#endregion
92
235
  exports.SelectionClipboard = SelectionClipboard;
236
+ exports.SelectionHandles = SelectionHandles;
93
237
  exports.SelectionLayer = SelectionLayer;
94
238
  exports.SelectionMenu = SelectionMenu;
95
239
  Object.defineProperty(exports, "copySelection", {
@@ -1 +1 @@
1
- {"version":3,"file":"selection.cjs","names":["usePage","useCapability","SelectionHostToken","useSelector","shallowArray","SelectionToken","Anchored"],"sources":["../src/selection.tsx"],"sourcesContent":["/**\n * The React surface for @embedpdf/plugin-selection.\n *\n * <SelectionLayer> is a dumb renderer: it warms the page's geometry on mount,\n * reads the content-space highlight rects from the capability, and paints them —\n * mapping each rect through PageContext.pageToContent (the same path markers use).\n * Zero pointer handling here; that's the PagePointerSource + the hub.\n *\n * The layer resolves the HOST lens (`/internal`: geometry warming, the\n * highlight handshake) — the adapter is exactly what that entry exists for.\n * `useSelection()` hands app code the PUBLIC lens only.\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/plugin-selection';\n// The clipboard side effect lives in @embedpdf/web (the plugin is DOM-free);\n// re-exported here so app code has one import for the whole feature.\nexport { copySelection } from '@embedpdf/web';\nimport * as React from 'react';\nimport { useEffect } from 'react';\nimport { SelectionToken, type SelectionMenuAnchor } from '@embedpdf/plugin-selection';\nimport { SelectionToken as SelectionHostToken } from '@embedpdf/plugin-selection/internal';\nimport { wireSelectionClipboard, type SelectionClipboardOptions } from '@embedpdf/web';\nimport { Anchored, type AnchoredPlacement } from './anchored';\nimport { shallowArray, useCapability, usePage, useSelector } from './runtime';\n\nexport interface SelectionLayerProps {\n /** Highlight colour (default: translucent blue). */\n color?: string;\n}\n\nexport function SelectionLayer({ color = 'rgba(33, 150, 243, 0.35)' }: SelectionLayerProps) {\n const page = usePage();\n const selection = useCapability(SelectionHostToken);\n const segments = useSelector(\n SelectionHostToken,\n (c) => c.segmentsForPage(page.pon),\n shallowArray,\n );\n // A consumer (e.g. a markup tool drawing its own preview) can take over the\n // selection visual; when it does, we render nothing so the two never overlap.\n const visible = useSelector(SelectionHostToken, (c) => c.highlightVisible());\n\n // Warm this page's text geometry as soon as it's on screen, so the first\n // pointer-down can hit-test without waiting on the engine round-trip.\n // (A no-op without doc.text.select — nothing warms, nothing renders.)\n useEffect(() => {\n selection.ensurePage(page.pon);\n }, [selection, page.pon]);\n\n if (!visible) return null;\n\n return (\n <svg\n style={{\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n overflow: 'visible',\n pointerEvents: 'none',\n }}\n >\n {segments.map((s, i) => {\n // content space → un-rotated content view px (rides the page's CSS\n // rotation). An affine map, so mapping the four corners is exact —\n // upright segments render pixel-identical to the old div-per-rect.\n const ring = [s.quad.upperStart, s.quad.upperEnd, s.quad.lowerEnd, s.quad.lowerStart].map(\n (p) => page.transform.pageToContent(p),\n );\n return (\n <polygon key={i} points={ring.map((p) => `${p.x},${p.y}`).join(' ')} fill={color} />\n );\n })}\n </svg>\n );\n}\n\n/** The PUBLIC selection capability (select(), readText(), canCopy(), …) for\n * app chrome — toolbars, context menus, automation. */\nexport function useSelection() {\n return useCapability(SelectionToken);\n}\n\n/** Structural equality for the selection's menu anchor — keeps the menu from\n * re-rendering on unrelated dispatches (the capability returns a fresh\n * object each call). */\nconst sameMenuAnchor = (\n a: SelectionMenuAnchor | null,\n b: SelectionMenuAnchor | null,\n): boolean => {\n if (a === b) return true;\n if (!a || !b) return false;\n return (\n a.pon === b.pon &&\n a.bounds.x === b.bounds.x &&\n a.bounds.y === b.bounds.y &&\n a.bounds.width === b.bounds.width &&\n a.bounds.height === b.bounds.height\n );\n};\n\nexport interface SelectionMenuProps {\n children: React.ReactNode;\n /** Gap in screen px between the selection and the menu (default 8). */\n gap?: number;\n /** Where to place the menu relative to the selection. Default 'top'. */\n placement?: AnchoredPlacement;\n}\n\n/**\n * Floats over the current TEXT selection (one anchor regardless of\n * cross-page selection; it rides the gesture's end page) — and only once the\n * selection SETTLES: hidden while `isSelecting()` (mid-drag), it appears at\n * pointer-up; programmatic selections show immediately (born settled). Works\n * under `<Stage>` (mount in the overlay slot) and `<PageView>` alike — the\n * surface provides the projection. Compose the contents from hooks:\n * `useSelection()` for copy/clear, `useAnnotation()` for\n * `markupFromSelection('highlight')`, `copySelection` for the clipboard.\n * For live-follow UI during the drag, compose `<Anchored>` with\n * `menuAnchor()` yourself — the primitive carries no policy.\n */\nexport function SelectionMenu({ children, gap = 8, placement = 'top' }: SelectionMenuProps) {\n const selecting = useSelector(SelectionToken, (c) => c.isSelecting());\n const anchor = useSelector(SelectionToken, (c) => c.menuAnchor(), sameMenuAnchor);\n if (selecting || !anchor) return null;\n return (\n <Anchored anchor={anchor} placement={placement} gap={gap}>\n {children}\n </Anchored>\n );\n}\n\nexport type SelectionClipboardProps = Pick<SelectionClipboardOptions, 'prefetch'>;\n\n/**\n * Mount ONCE per viewer to wire clipboard copy: prefetches the selected text\n * when the selection settles, answers the native `copy` event synchronously,\n * and falls back to the async Clipboard API for ctrl/cmd+C when the page has\n * no DOM selection. Renders nothing. For a toolbar Copy button, call\n * `copySelection(useSelection())` from its click handler instead.\n */\nexport function SelectionClipboard({ prefetch }: SelectionClipboardProps = {}) {\n const selection = useCapability(SelectionToken);\n useEffect(\n () => wireSelectionClipboard(selection, prefetch === undefined ? {} : { prefetch }),\n [selection, prefetch],\n );\n return null;\n}\n"],"mappings":";;;;;;;;;;;AA+BA,SAAgB,eAAe,EAAE,QAAQ,8BAAmD;CAC1F,MAAM,OAAOA,gBAAAA,QAAQ;CACrB,MAAM,YAAYC,gBAAAA,cAAcC,oCAAAA,cAAkB;CAClD,MAAM,WAAWC,gBAAAA,YACfD,oCAAAA,iBACC,MAAM,EAAE,gBAAgB,KAAK,GAAG,GACjCE,gBAAAA,YACF;CAGA,MAAM,UAAUD,gBAAAA,YAAYD,oCAAAA,iBAAqB,MAAM,EAAE,iBAAiB,CAAC;CAK3E,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,UAAU,WAAW,KAAK,GAAG;CAC/B,GAAG,CAAC,WAAW,KAAK,GAAG,CAAC;CAExB,IAAI,CAAC,SAAS,OAAO;CAErB,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,OAAO;GACP,OAAO;GACP,QAAQ;GACR,UAAU;GACV,eAAe;EACjB;EAEC,UAAA,SAAS,KAAK,GAAG,MAAM;GAOtB,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;IAAiB,QAJN;KAAC,EAAE,KAAK;KAAY,EAAE,KAAK;KAAU,EAAE,KAAK;KAAU,EAAE,KAAK;IAAU,CAAC,CAAC,KACnF,MAAM,KAAK,UAAU,cAAc,CAAC,CAGT,CAAC,CAAC,KAAK,MAAM,GAAG,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,GAAG;IAAG,MAAM;GAAQ,GAArE,CAAqE;EAEvF,CAAC;CACE,CAAA;AAET;;;AAIA,SAAgB,eAAe;CAC7B,OAAOD,gBAAAA,cAAcI,2BAAAA,cAAc;AACrC;;;;AAKA,MAAM,kBACJ,GACA,MACY;CACZ,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,CAAC,KAAK,CAAC,GAAG,OAAO;CACrB,OACE,EAAE,QAAQ,EAAE,OACZ,EAAE,OAAO,MAAM,EAAE,OAAO,KACxB,EAAE,OAAO,MAAM,EAAE,OAAO,KACxB,EAAE,OAAO,UAAU,EAAE,OAAO,SAC5B,EAAE,OAAO,WAAW,EAAE,OAAO;AAEjC;;;;;;;;;;;;;AAsBA,SAAgB,cAAc,EAAE,UAAU,MAAM,GAAG,YAAY,SAA6B;CAC1F,MAAM,YAAYF,gBAAAA,YAAYE,2BAAAA,iBAAiB,MAAM,EAAE,YAAY,CAAC;CACpE,MAAM,SAASF,gBAAAA,YAAYE,2BAAAA,iBAAiB,MAAM,EAAE,WAAW,GAAG,cAAc;CAChF,IAAI,aAAa,CAAC,QAAQ,OAAO;CACjC,OACE,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,UAAD;EAAkB;EAAmB;EAAgB;EAClD;CACO,CAAA;AAEd;;;;;;;;AAWA,SAAgB,mBAAmB,EAAE,aAAsC,CAAC,GAAG;CAC7E,MAAM,YAAYL,gBAAAA,cAAcI,2BAAAA,cAAc;CAC9C,CAAA,GAAA,MAAA,UAAA,QAAA,GAAA,cAAA,uBAAA,CAC+B,WAAW,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,CAAC,GAClF,CAAC,WAAW,QAAQ,CACtB;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"selection.cjs","names":["usePage","useCapability","SelectionHostToken","useSelector","shallowArray","SelectionToken","Anchored","StageToken","useOptionalCapability","HANDLE_PAD","HANDLE_HEAD","HANDLE_BAR"],"sources":["../src/selection.tsx"],"sourcesContent":["/**\n * The React surface for @embedpdf/plugin-selection.\n *\n * <SelectionLayer> is a dumb renderer: it warms the page's geometry on mount,\n * reads the content-space highlight rects from the capability, and paints them —\n * mapping each rect through PageContext.pageToContent (the same path markers use).\n * Zero pointer handling here; that's the PagePointerSource + the hub.\n *\n * The layer resolves the HOST lens (`/internal`: geometry warming, the\n * highlight handshake) — the adapter is exactly what that entry exists for.\n * `useSelection()` hands app code the PUBLIC lens only.\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/plugin-selection';\n// The clipboard side effect lives in @embedpdf/web (the plugin is DOM-free);\n// re-exported here so app code has one import for the whole feature.\nexport { copySelection } from '@embedpdf/web';\nimport * as React from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { textQuadEquals } from '@embedpdf/core-geometry';\nimport type { CapabilityToken } from '@embedpdf/core';\nimport {\n HANDLE_BAR,\n HANDLE_HEAD,\n HANDLE_PAD,\n SelectionToken,\n createSelectionHandleDrag,\n selectionHandleGeom,\n type SelectionHandleEndpoint,\n type SelectionHandleView,\n type SelectionMenuAnchor,\n} from '@embedpdf/plugin-selection';\nimport { SelectionToken as SelectionHostToken } from '@embedpdf/plugin-selection/internal';\nimport { StageToken, type StageCapability } from '@embedpdf/plugin-stage';\nimport {\n attachSelectionHandle,\n wireSelectionClipboard,\n type SelectionClipboardOptions,\n} from '@embedpdf/web';\nimport { Anchored, type AnchoredPlacement } from './anchored';\nimport {\n shallowArray,\n useCapability,\n useKernelValue,\n useOptionalCapability,\n usePage,\n useSelector,\n} from './runtime';\n\nexport interface SelectionLayerProps {\n /** Highlight colour (default: translucent blue). */\n color?: string;\n}\n\nexport function SelectionLayer({ color = 'rgba(33, 150, 243, 0.35)' }: SelectionLayerProps) {\n const page = usePage();\n const selection = useCapability(SelectionHostToken);\n const segments = useSelector(\n SelectionHostToken,\n (c) => c.segmentsForPage(page.pon),\n shallowArray,\n );\n // A consumer (e.g. a markup tool drawing its own preview) can take over the\n // selection visual; when it does, we render nothing so the two never overlap.\n const visible = useSelector(SelectionHostToken, (c) => c.highlightVisible());\n\n // Warm this page's text geometry as soon as it's on screen, so the first\n // pointer-down can hit-test without waiting on the engine round-trip.\n // (A no-op without doc.text.select — nothing warms, nothing renders.)\n useEffect(() => {\n selection.ensurePage(page.pon);\n }, [selection, page.pon]);\n\n if (!visible) return null;\n\n return (\n <svg\n style={{\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n overflow: 'visible',\n pointerEvents: 'none',\n }}\n >\n {segments.map((s, i) => {\n // content space → un-rotated content view px (rides the page's CSS\n // rotation). An affine map, so mapping the four corners is exact —\n // upright segments render pixel-identical to the old div-per-rect.\n const ring = [s.quad.upperStart, s.quad.upperEnd, s.quad.lowerEnd, s.quad.lowerStart].map(\n (p) => page.transform.pageToContent(p),\n );\n return (\n <polygon key={i} points={ring.map((p) => `${p.x},${p.y}`).join(' ')} fill={color} />\n );\n })}\n </svg>\n );\n}\n\n/** The PUBLIC selection capability (select(), readText(), canCopy(), …) for\n * app chrome — toolbars, context menus, automation. */\nexport function useSelection() {\n return useCapability(SelectionToken);\n}\n\n/** Structural equality for the selection's menu anchor — keeps the menu from\n * re-rendering on unrelated dispatches (the capability returns a fresh\n * object each call). */\nconst sameMenuAnchor = (\n a: SelectionMenuAnchor | null,\n b: SelectionMenuAnchor | null,\n): boolean => {\n if (a === b) return true;\n if (!a || !b) return false;\n return (\n a.pon === b.pon &&\n a.bounds.x === b.bounds.x &&\n a.bounds.y === b.bounds.y &&\n a.bounds.width === b.bounds.width &&\n a.bounds.height === b.bounds.height\n );\n};\n\nexport interface SelectionMenuProps {\n children: React.ReactNode;\n /** Gap in screen px between the selection and the menu (default 8). */\n gap?: number;\n /** Where to place the menu relative to the selection. Default 'top'. */\n placement?: AnchoredPlacement;\n}\n\n/**\n * Floats over the current TEXT selection (one anchor regardless of\n * cross-page selection; it rides the gesture's end page) — and only once the\n * selection SETTLES: hidden while `isSelecting()` (mid-drag), it appears at\n * pointer-up; programmatic selections show immediately (born settled). Works\n * under `<Stage>` (mount in the overlay slot) and `<PageView>` alike — the\n * surface provides the projection. Compose the contents from hooks:\n * `useSelection()` for copy/clear, `useAnnotation()` for\n * `markupFromSelection('highlight')`, `copySelection` for the clipboard.\n * For live-follow UI during the drag, compose `<Anchored>` with\n * `menuAnchor()` yourself — the primitive carries no policy.\n */\nexport function SelectionMenu({ children, gap = 8, placement = 'top' }: SelectionMenuProps) {\n const selecting = useSelector(SelectionToken, (c) => c.isSelecting());\n const anchor = useSelector(SelectionToken, (c) => c.menuAnchor(), sameMenuAnchor);\n if (selecting || !anchor) return null;\n return (\n <Anchored anchor={anchor} placement={placement} gap={gap}>\n {children}\n </Anchored>\n );\n}\n\n// ── selection handles (the touch affordance) ────────────────────────────────\n//\n// Policy lives out of this file, per the layering razor: the geometry and the\n// drag session are `@embedpdf/plugin-selection`'s pure `handles` module (one\n// source for every adapter), the native listener mechanics are\n// `@embedpdf/web`'s `attachSelectionHandle` (the down-shield timing subtlety\n// lives ONCE), and this component keeps what only React can do —\n// subscriptions, markup, theming.\n\ninterface Endpoints {\n start: SelectionHandleEndpoint;\n end: SelectionHandleEndpoint;\n}\nconst sameEndpoints = (a: Endpoints | null, b: Endpoints | null): boolean => {\n if (a === b) return true;\n if (!a || !b) return false;\n return (\n a.start.pon === b.start.pon &&\n a.end.pon === b.end.pon &&\n a.start.advance === b.start.advance &&\n a.end.advance === b.end.advance &&\n // corner-wise, so a boundary that ROTATES without moving its bounding box\n // still re-renders (an AABB comparison would call that \"unchanged\")\n textQuadEquals(a.start.glyphQuad, b.start.glyphQuad) &&\n textQuadEquals(a.end.glyphQuad, b.end.glyphQuad)\n );\n};\n\n/** The plugin's structural view over this Stage: point-exact projection in,\n * page resolution out. (`pageRectToScreen` is the AABB projector — upright\n * overlays only — and would collapse exactly the orientation handles need.) */\nconst handleView = (stage: StageCapability): SelectionHandleView => ({\n toOverlay: (pon, pt) => {\n const world = stage.pageToWorld(pon, pt);\n return world ? stage.toScreen(world) : null;\n },\n pageAt: (overlay) => stage.pageAt(overlay),\n pointOnPage: (pon, overlay) => stage.pointOnPage(pon, overlay),\n});\n\nexport interface SelectionHandlesProps {\n /** Handle colour (default: the selection blue). */\n color?: string;\n /** The stage lens hosting this overlay (default: the main StageToken). */\n token?: CapabilityToken<StageCapability>;\n}\n\n/**\n * iOS-style draggable selection handles. Each is drawn the way the platform\n * draws them: a thin caret BAR that forms the selection's own start/end edge\n * — the boundary glyph's oriented edge, so rotated text and rotated pages\n * carry the handle with them — capped by a screen-constant circle, above the\n * first line at the start, below the last line at the end.\n *\n * On touch, where a caret drag isn't available, the handles ARE the way to\n * grow or shrink a selection: a long-press selects a word, then each handle\n * extends from the OPPOSITE endpoint, snapping to glyphs and crossing pages\n * exactly like a pointer drag — it rides the same `beginAt`/`extendTo`\n * gesture path, so highlights, menus, and commit signals all behave\n * identically. Mount in the `<Stage>` overlay slot next to `<SelectionMenu>`;\n * outside a Stage it renders nothing (a `PageView` has no camera to project\n * through). Pointer-isolated, so grabbing a handle never pans the stage.\n */\nexport function SelectionHandles({ color = '#2196f3', token = StageToken }: SelectionHandlesProps) {\n const host = useCapability(SelectionHostToken);\n const stage = useOptionalCapability(token);\n const selecting = useSelector(SelectionToken, (c) => c.isSelecting());\n const visible = useSelector(SelectionHostToken, (c) => c.highlightVisible());\n const endpoints = useSelector(\n SelectionHostToken,\n (c): Endpoints | null => {\n const s = c.snapshot();\n if (!s.start || !s.end) return null;\n return {\n start: { pon: s.start.pon, glyphQuad: s.start.glyphQuad, advance: s.start.advance },\n end: { pon: s.end.pon, glyphQuad: s.end.glyphQuad, advance: s.end.advance },\n };\n },\n sameEndpoints,\n );\n // The handles are positioned by PROJECTING the endpoint corners through the\n // camera, so they must re-render whenever the camera moves — visiblePages is\n // the stage's reference-stable revision for exactly that (the same value the\n // page surfaces re-render on, so handle and highlight move in one commit).\n useKernelValue(() => stage?.visiblePages() ?? null);\n const [dragging, setDragging] = useState<'start' | 'end' | null>(null);\n // The web binder's `arm` must read the CURRENT endpoints/stage at pointer\n // down, not the ones captured when the listener attached — a stable ref\n // callback with a live arm-source is the standard escape from that.\n const armSource = useRef<{ stage: StageCapability; endpoints: Endpoints } | null>(null);\n armSource.current = stage && endpoints ? { stage, endpoints } : null;\n const bindHandle = useMemo(() => {\n const detach: Partial<Record<'start' | 'end', () => void>> = {};\n const make = (role: 'start' | 'end') => (el: HTMLDivElement | null) => {\n detach[role]?.();\n delete detach[role];\n if (!el) return;\n detach[role] = attachSelectionHandle(el, {\n arm: () => {\n const src = armSource.current;\n if (!src) return null;\n const view = handleView(src.stage);\n const geom = selectionHandleGeom(view, src.endpoints[role], role);\n if (!geom) return null;\n const opposite = src.endpoints[role === 'start' ? 'end' : 'start'];\n const drag = createSelectionHandleDrag(\n host,\n view,\n opposite,\n src.endpoints[role].pon,\n );\n setDragging(role);\n return {\n // the point the user grabbed: the bar's midpoint\n base: {\n x: (geom.bar.from.x + geom.bar.to.x) / 2,\n y: (geom.bar.from.y + geom.bar.to.y) / 2,\n },\n session: {\n move: drag.move,\n end: () => {\n drag.end(); // settle → menu reappears, onCommit fires\n setDragging(null);\n },\n },\n };\n },\n });\n };\n return { start: make('start'), end: make('end') };\n }, [host]);\n\n if (!stage || !endpoints || !visible) return null;\n // Hidden while a pointer drag-select is in flight (like the menu) — but a\n // HANDLE drag is itself a selection gesture, so it keeps its handles.\n if (selecting && !dragging) return null;\n const view = handleView(stage);\n\n const renderHandle = (role: 'start' | 'end') => {\n const geom = selectionHandleGeom(view, endpoints[role], role);\n if (!geom) return null; // endpoint page not laid out right now\n // The shell is laid out UPRIGHT in its own frame — bar of the edge's\n // length, head stacked above (start) or below (end) — then rotated onto\n // the projected edge. The pivot is the bar's ascent-side tip, the one\n // point that must land exactly on the glyph corner.\n const barTop = HANDLE_PAD + (role === 'start' ? HANDLE_HEAD : 0);\n const pivotX = HANDLE_PAD + HANDLE_BAR / 2;\n return (\n <div\n key={role}\n ref={bindHandle[role]}\n style={{\n position: 'absolute',\n left: geom.bar.from.x - pivotX,\n top: geom.bar.from.y - barTop,\n width: HANDLE_BAR + 2 * HANDLE_PAD,\n height: geom.length + HANDLE_HEAD + 2 * HANDLE_PAD,\n // Upright text carries NO transform — pixel-identical to an\n // axis-aligned box (the geometry's float-noise guard decides).\n ...(geom.upright\n ? null\n : {\n transform: `rotate(${geom.rotation}deg)`,\n transformOrigin: `${pivotX}px ${barTop}px`,\n }),\n touchAction: 'none',\n cursor: 'grab',\n pointerEvents: 'auto',\n }}\n >\n {/* the caret bar — the selection's own edge, spanning the glyph's ink\n height (which is why it stays the text's height at any tilt) */}\n <div\n style={{\n position: 'absolute',\n left: HANDLE_PAD,\n top: barTop,\n width: HANDLE_BAR,\n height: geom.length,\n background: color,\n borderRadius: HANDLE_BAR / 2,\n }}\n />\n {/* the head — flush against the bar, beyond the ascent at the start and\n past the baseline at the end, in the TEXT's frame */}\n <div\n style={{\n position: 'absolute',\n left: pivotX - HANDLE_HEAD / 2,\n top: role === 'start' ? HANDLE_PAD : HANDLE_PAD + geom.length,\n width: HANDLE_HEAD,\n height: HANDLE_HEAD,\n borderRadius: '50%',\n background: color,\n boxShadow: '0 1px 4px rgba(0, 0, 0, 0.35)',\n }}\n />\n </div>\n );\n };\n\n return (\n <>\n {renderHandle('start')}\n {renderHandle('end')}\n </>\n );\n}\n\n\nexport type SelectionClipboardProps = Pick<SelectionClipboardOptions, 'prefetch'>;\n\n/**\n * Mount ONCE per viewer to wire clipboard copy: prefetches the selected text\n * when the selection settles, answers the native `copy` event synchronously,\n * and falls back to the async Clipboard API for ctrl/cmd+C when the page has\n * no DOM selection. Renders nothing. For a toolbar Copy button, call\n * `copySelection(useSelection())` from its click handler instead.\n */\nexport function SelectionClipboard({ prefetch }: SelectionClipboardProps = {}) {\n const selection = useCapability(SelectionToken);\n useEffect(\n () => wireSelectionClipboard(selection, prefetch === undefined ? {} : { prefetch }),\n [selection, prefetch],\n );\n return null;\n}\n"],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  import { AnchoredPlacement as AnchoredPlacement$1 } from "./anchored.cjs";
2
2
  import * as React from "react";
3
3
  import { SelectionClipboardOptions, copySelection } from "@embedpdf/web";
4
+ import { CapabilityToken } from "@embedpdf/core";
5
+ import { StageCapability } from "@embedpdf/plugin-stage";
4
6
  export * from "@embedpdf/plugin-selection";
5
7
  //#region src/selection.d.ts
6
8
  interface SelectionLayerProps {
@@ -31,6 +33,29 @@ interface SelectionMenuProps {
31
33
  * `menuAnchor()` yourself — the primitive carries no policy.
32
34
  */
33
35
  declare function SelectionMenu({ children, gap, placement }: SelectionMenuProps): import("react/jsx-runtime").JSX.Element | null;
36
+ interface SelectionHandlesProps {
37
+ /** Handle colour (default: the selection blue). */
38
+ color?: string;
39
+ /** The stage lens hosting this overlay (default: the main StageToken). */
40
+ token?: CapabilityToken<StageCapability>;
41
+ }
42
+ /**
43
+ * iOS-style draggable selection handles. Each is drawn the way the platform
44
+ * draws them: a thin caret BAR that forms the selection's own start/end edge
45
+ * — the boundary glyph's oriented edge, so rotated text and rotated pages
46
+ * carry the handle with them — capped by a screen-constant circle, above the
47
+ * first line at the start, below the last line at the end.
48
+ *
49
+ * On touch, where a caret drag isn't available, the handles ARE the way to
50
+ * grow or shrink a selection: a long-press selects a word, then each handle
51
+ * extends from the OPPOSITE endpoint, snapping to glyphs and crossing pages
52
+ * exactly like a pointer drag — it rides the same `beginAt`/`extendTo`
53
+ * gesture path, so highlights, menus, and commit signals all behave
54
+ * identically. Mount in the `<Stage>` overlay slot next to `<SelectionMenu>`;
55
+ * outside a Stage it renders nothing (a `PageView` has no camera to project
56
+ * through). Pointer-isolated, so grabbing a handle never pans the stage.
57
+ */
58
+ declare function SelectionHandles({ color, token }: SelectionHandlesProps): import("react/jsx-runtime").JSX.Element | null;
34
59
  type SelectionClipboardProps = Pick<SelectionClipboardOptions, 'prefetch'>;
35
60
  /**
36
61
  * Mount ONCE per viewer to wire clipboard copy: prefetches the selected text
@@ -41,5 +66,5 @@ type SelectionClipboardProps = Pick<SelectionClipboardOptions, 'prefetch'>;
41
66
  */
42
67
  declare function SelectionClipboard({ prefetch }?: SelectionClipboardProps): null;
43
68
  //#endregion
44
- export { SelectionClipboard, SelectionClipboardProps, SelectionLayer, SelectionLayerProps, SelectionMenu, SelectionMenuProps, copySelection, useSelection };
69
+ export { SelectionClipboard, SelectionClipboardProps, SelectionHandles, SelectionHandlesProps, SelectionLayer, SelectionLayerProps, SelectionMenu, SelectionMenuProps, copySelection, useSelection };
45
70
  //# sourceMappingURL=selection.d.cts.map
@@ -1,6 +1,8 @@
1
1
  import { AnchoredPlacement as AnchoredPlacement$1 } from "./anchored.js";
2
+ import { CapabilityToken } from "@embedpdf/core";
2
3
  import * as React from "react";
3
4
  import { SelectionClipboardOptions, copySelection } from "@embedpdf/web";
5
+ import { StageCapability } from "@embedpdf/plugin-stage";
4
6
  export * from "@embedpdf/plugin-selection";
5
7
  //#region src/selection.d.ts
6
8
  interface SelectionLayerProps {
@@ -31,6 +33,29 @@ interface SelectionMenuProps {
31
33
  * `menuAnchor()` yourself — the primitive carries no policy.
32
34
  */
33
35
  declare function SelectionMenu({ children, gap, placement }: SelectionMenuProps): import("react/jsx-runtime").JSX.Element | null;
36
+ interface SelectionHandlesProps {
37
+ /** Handle colour (default: the selection blue). */
38
+ color?: string;
39
+ /** The stage lens hosting this overlay (default: the main StageToken). */
40
+ token?: CapabilityToken<StageCapability>;
41
+ }
42
+ /**
43
+ * iOS-style draggable selection handles. Each is drawn the way the platform
44
+ * draws them: a thin caret BAR that forms the selection's own start/end edge
45
+ * — the boundary glyph's oriented edge, so rotated text and rotated pages
46
+ * carry the handle with them — capped by a screen-constant circle, above the
47
+ * first line at the start, below the last line at the end.
48
+ *
49
+ * On touch, where a caret drag isn't available, the handles ARE the way to
50
+ * grow or shrink a selection: a long-press selects a word, then each handle
51
+ * extends from the OPPOSITE endpoint, snapping to glyphs and crossing pages
52
+ * exactly like a pointer drag — it rides the same `beginAt`/`extendTo`
53
+ * gesture path, so highlights, menus, and commit signals all behave
54
+ * identically. Mount in the `<Stage>` overlay slot next to `<SelectionMenu>`;
55
+ * outside a Stage it renders nothing (a `PageView` has no camera to project
56
+ * through). Pointer-isolated, so grabbing a handle never pans the stage.
57
+ */
58
+ declare function SelectionHandles({ color, token }: SelectionHandlesProps): import("react/jsx-runtime").JSX.Element | null;
34
59
  type SelectionClipboardProps = Pick<SelectionClipboardOptions, 'prefetch'>;
35
60
  /**
36
61
  * Mount ONCE per viewer to wire clipboard copy: prefetches the selected text
@@ -41,5 +66,5 @@ type SelectionClipboardProps = Pick<SelectionClipboardOptions, 'prefetch'>;
41
66
  */
42
67
  declare function SelectionClipboard({ prefetch }?: SelectionClipboardProps): null;
43
68
  //#endregion
44
- export { SelectionClipboard, SelectionClipboardProps, SelectionLayer, SelectionLayerProps, SelectionMenu, SelectionMenuProps, copySelection, useSelection };
69
+ export { SelectionClipboard, SelectionClipboardProps, SelectionHandles, SelectionHandlesProps, SelectionLayer, SelectionLayerProps, SelectionMenu, SelectionMenuProps, copySelection, useSelection };
45
70
  //# sourceMappingURL=selection.d.ts.map