@embedpdf/react 3.0.0-next.10 → 3.0.0-next.11

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 (83) hide show
  1. package/dist/{AnnotationListSnapshot-DDxHpJQU-BG32YXxG.d.ts → AnnotationListSnapshot-C1U3XUxP-6kGwNoxy.d.cts} +223 -24
  2. package/dist/{AnnotationListSnapshot-DDxHpJQU-BG32YXxG.d.cts → AnnotationListSnapshot-C1U3XUxP-6kGwNoxy.d.ts} +223 -24
  3. package/dist/actions.cjs +42 -0
  4. package/dist/actions.cjs.map +1 -0
  5. package/dist/actions.d.cts +18 -0
  6. package/dist/actions.d.ts +18 -0
  7. package/dist/actions.js +34 -0
  8. package/dist/actions.js.map +1 -0
  9. package/dist/annotation-BCED_p9H.d.cts +160 -0
  10. package/dist/annotation-anchors-CetwM1g7.js +15 -0
  11. package/dist/annotation-anchors-CetwM1g7.js.map +1 -0
  12. package/dist/annotation-anchors-DuYRBJLl.cjs +26 -0
  13. package/dist/annotation-anchors-DuYRBJLl.cjs.map +1 -0
  14. package/dist/annotation-hooks-BB5ZhPpf.cjs +16 -0
  15. package/dist/annotation-hooks-BB5ZhPpf.cjs.map +1 -0
  16. package/dist/annotation-hooks-Df8EmzDO.js +11 -0
  17. package/dist/annotation-hooks-Df8EmzDO.js.map +1 -0
  18. package/dist/annotation-k38KDlIZ.d.ts +160 -0
  19. package/dist/annotation-menu.cjs +5 -5
  20. package/dist/annotation-menu.cjs.map +1 -1
  21. package/dist/annotation-menu.js +2 -2
  22. package/dist/annotation-menu.js.map +1 -1
  23. package/dist/annotation.cjs +5 -19
  24. package/dist/annotation.cjs.map +1 -1
  25. package/dist/annotation.d.cts +2 -159
  26. package/dist/annotation.d.ts +2 -159
  27. package/dist/annotation.js +2 -16
  28. package/dist/annotation.js.map +1 -1
  29. package/dist/form-BwrLFpKN.js +805 -0
  30. package/dist/form-BwrLFpKN.js.map +1 -0
  31. package/dist/form-D-ZSfGmB.cjs +836 -0
  32. package/dist/form-D-ZSfGmB.cjs.map +1 -0
  33. package/dist/form.cjs +8 -551
  34. package/dist/form.d.cts +4 -21
  35. package/dist/form.d.ts +4 -21
  36. package/dist/form.js +3 -544
  37. package/dist/index.cjs +15 -11
  38. package/dist/index.d.cts +7 -6
  39. package/dist/index.d.ts +7 -6
  40. package/dist/index.js +8 -5
  41. package/dist/link.cjs +48 -8
  42. package/dist/link.cjs.map +1 -1
  43. package/dist/link.d.cts +2 -2
  44. package/dist/link.d.ts +2 -2
  45. package/dist/link.js +49 -9
  46. package/dist/link.js.map +1 -1
  47. package/dist/{runtime-Cvi0wQ4d.d.cts → runtime-DE76hlnl.d.ts} +13 -5
  48. package/dist/{runtime-DXxr8T4L.d.ts → runtime-umHA_xHt.d.cts} +13 -5
  49. package/dist/runtime.cjs +19 -0
  50. package/dist/runtime.cjs.map +1 -1
  51. package/dist/runtime.d.cts +2 -2
  52. package/dist/runtime.d.ts +2 -2
  53. package/dist/runtime.js +19 -1
  54. package/dist/runtime.js.map +1 -1
  55. package/dist/scrollbar.cjs +3 -3
  56. package/dist/scrollbar.cjs.map +1 -1
  57. package/dist/scrollbar.d.cts +1 -1
  58. package/dist/scrollbar.d.ts +1 -1
  59. package/dist/scrollbar.js +1 -1
  60. package/dist/scrollbar.js.map +1 -1
  61. package/dist/search.d.cts +1 -1
  62. package/dist/search.d.ts +1 -1
  63. package/dist/selection.cjs +2 -2
  64. package/dist/selection.cjs.map +1 -1
  65. package/dist/selection.d.cts +1 -1
  66. package/dist/selection.d.ts +1 -1
  67. package/dist/selection.js +1 -1
  68. package/dist/selection.js.map +1 -1
  69. package/dist/stage.cjs +2 -2
  70. package/dist/stage.cjs.map +1 -1
  71. package/dist/stage.d.cts +1 -1
  72. package/dist/stage.d.ts +1 -1
  73. package/dist/stage.js +1 -1
  74. package/dist/stage.js.map +1 -1
  75. package/dist/toolbar.cjs +4 -4
  76. package/dist/toolbar.cjs.map +1 -1
  77. package/dist/toolbar.d.cts +1 -1
  78. package/dist/toolbar.d.ts +1 -1
  79. package/dist/toolbar.js +1 -1
  80. package/dist/toolbar.js.map +1 -1
  81. package/package.json +29 -23
  82. package/dist/form.cjs.map +0 -1
  83. package/dist/form.js.map +0 -1
@@ -1 +1 @@
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.toPixels (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.toPixels(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
+ {"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.toPixels (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/contract';\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.toPixels(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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"}
@@ -2,7 +2,7 @@ import { AnchoredPlacement as AnchoredPlacement$1 } from "./anchored.cjs";
2
2
  import * as React from "react";
3
3
  import { SelectionClipboardOptions, copySelection } from "@embedpdf/web";
4
4
  import { CapabilityToken } from "@embedpdf/core";
5
- import { StageCapability } from "@embedpdf/plugin-stage";
5
+ import { StageCapability } from "@embedpdf/plugin-stage/contract";
6
6
  export * from "@embedpdf/plugin-selection";
7
7
  //#region src/selection.d.ts
8
8
  interface SelectionLayerProps {
@@ -2,7 +2,7 @@ import { AnchoredPlacement as AnchoredPlacement$1 } from "./anchored.js";
2
2
  import { CapabilityToken } from "@embedpdf/core";
3
3
  import * as React from "react";
4
4
  import { SelectionClipboardOptions, copySelection } from "@embedpdf/web";
5
- import { StageCapability } from "@embedpdf/plugin-stage";
5
+ import { StageCapability } from "@embedpdf/plugin-stage/contract";
6
6
  export * from "@embedpdf/plugin-selection";
7
7
  //#region src/selection.d.ts
8
8
  interface SelectionLayerProps {
package/dist/selection.js CHANGED
@@ -4,7 +4,7 @@ import { Anchored } from "./anchored.js";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { useEffect, useMemo, useRef, useState } from "react";
6
6
  import { attachSelectionHandle, copySelection, wireSelectionClipboard } from "@embedpdf/web";
7
- import { StageToken } from "@embedpdf/plugin-stage";
7
+ import { StageToken } from "@embedpdf/plugin-stage/contract";
8
8
  import { textQuadEquals } from "@embedpdf/core-geometry";
9
9
  import { HANDLE_BAR, HANDLE_HEAD, HANDLE_PAD, SelectionToken, createSelectionHandleDrag, selectionHandleGeom } from "@embedpdf/plugin-selection";
10
10
  import { SelectionToken as SelectionToken$1 } from "@embedpdf/plugin-selection/internal";
@@ -1 +1 @@
1
- {"version":3,"file":"selection.js","names":["SelectionHostToken"],"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.toPixels (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.toPixels(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
+ {"version":3,"file":"selection.js","names":["SelectionHostToken"],"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.toPixels (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/contract';\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.toPixels(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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"}
package/dist/stage.cjs CHANGED
@@ -7,7 +7,7 @@ let react = require("react");
7
7
  react = require_rolldown_runtime.__toESM(react, 1);
8
8
  let _embedpdf_web = require("@embedpdf/web");
9
9
  let _embedpdf_plugin_stage = require("@embedpdf/plugin-stage");
10
- let _embedpdf_plugin_interaction = require("@embedpdf/plugin-interaction");
10
+ let _embedpdf_plugin_interaction_contract = require("@embedpdf/plugin-interaction/contract");
11
11
  //#region src/stage.tsx
12
12
  function PageSurface({ documentId, page, frame, stage, render, chrome }) {
13
13
  const ref = (0, react.useRef)(null);
@@ -82,7 +82,7 @@ function PageSurface({ documentId, page, frame, stage, render, chrome }) {
82
82
  }
83
83
  function Stage({ children, pageChrome, overlay, interaction = true, panFallback = true, zoomGestures = true, token = _embedpdf_plugin_stage.StageToken, className, style }) {
84
84
  const stage = require_runtime.useCapability(token);
85
- const ix = require_runtime.useOptionalCapability(_embedpdf_plugin_interaction.InteractionToken);
85
+ const ix = require_runtime.useOptionalCapability(_embedpdf_plugin_interaction_contract.InteractionToken);
86
86
  const useHub = interaction && !!ix;
87
87
  const hubCursor = require_runtime.useKernelValue(() => ix?.cursor() ?? "default");
88
88
  const ref = (0, react.useRef)(null);
@@ -1 +1 @@
1
- {"version":3,"file":"stage.cjs","names":["makePageContext","PageProvider","StageToken","useCapability","useOptionalCapability","InteractionToken","useKernelValue","useDocumentId","useSelector","ProjectorProvider","settingsEqual"],"sources":["../src/stage.tsx"],"sourcesContent":["/**\n * Stage / RenderLayer + facade hooks.\n *\n * <Stage> virtualizes and positions page surfaces by the camera, and hands each\n * one to YOUR render prop — you bring the layers. (The standalone, Stage-free\n * single-page surface lives in `./page-view` so it never pulls the stage plugin.)\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/plugin-stage';\nimport * as React from 'react';\nimport { useLayoutEffect, useMemo, useRef } from 'react';\nimport { StageToken, createScrollHandler, settingsEqual } from '@embedpdf/plugin-stage';\nimport type { StageCapability, VisiblePage } from '@embedpdf/plugin-stage';\nimport type { CapabilityToken } from '@embedpdf/core';\n\n/** Which stage lens to bind to. Defaults to the main StageToken — pass a custom\n * token to drive an additional lens (e.g. a wrapped thumbnail sidebar). */\nexport type StageTokenProp = CapabilityToken<StageCapability>;\nimport type { PageFrame } from '@embedpdf/core-geometry';\nimport { InteractionToken } from '@embedpdf/plugin-interaction';\nimport { createStageSurface } from '@embedpdf/web';\nimport { ProjectorProvider, type ProjectorBinding, type ViewProjector } from './anchored';\nimport {\n makePageContext,\n PageProvider,\n useCapability,\n useDocumentId,\n useKernelValue,\n useOptionalCapability,\n useSelector,\n} from './runtime';\nimport type { PageContextValue } from './runtime';\n\nfunction PageSurface({\n documentId,\n page,\n frame,\n stage,\n render,\n chrome,\n}: {\n documentId: string;\n page: VisiblePage;\n /** Reserved chrome bands around the page (screen px); the layout reserved the\n * matching space, so the outer box tiles into it. */\n frame: PageFrame;\n /** The stage capability — the demand getter reads visibility LIVE off it. */\n stage: StageCapability;\n render: (page: PageContextValue) => React.ReactNode;\n chrome?: (page: PageContextValue) => React.ReactNode;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const t = page.transform;\n const rotation = page.rotation;\n // All geometry comes from the transform: the DISPLAY footprint (viewWidth/Height,\n // already w↔h-swapped + device-snapped) and the UN-rotated content box\n // (contentWidth/Height). The shell never re-derives `* zoom` / `* dpr` / snapping.\n const outerW = t.viewWidth + frame.left + frame.right;\n const outerH = t.viewHeight + frame.top + frame.bottom;\n // page.screenX/screenY are the device-snapped footprint top-left; the outer box\n // sits one frame further out so the content keeps its scene position.\n const left = page.screenX - frame.left;\n const top = page.screenY - frame.top;\n // Center the (possibly rotated) content box on the display box and rotate about\n // its center — NO translate(), so rotation 0 carries no transform and pixel-snaps\n // like the axis-aligned shadow behind it (no hairline seam).\n const contentLeft = frame.left + (t.viewWidth - t.contentWidth) / 2;\n const contentTop = frame.top + (t.viewHeight - t.contentHeight) / 2;\n // The page-view DEMAND is a PULL: the getter closes over\n // stable references (capability + pon) and reads the stage's live state at\n // call time — visibility is the STAGE's data (`VisiblePage.visibleRect`),\n // not something an adapter re-derives or caches. Absent from the visible\n // set = zero rect (\"want nothing\"), distinct from PageView's undefined\n // getter (\"whole page\").\n const ctx = useMemo(\n () =>\n makePageContext(\n documentId,\n // the hosting lens — per-view raster planning keys tile state by it,\n // so a thumbnail rail and the main view never fight over one plan\n stage.lensId(),\n page.pon,\n page.pageIndex,\n frame,\n t,\n () => ref.current!.getBoundingClientRect(),\n () => {\n const live = stage.visiblePages().find((p) => p.pon === page.pon);\n return live\n ? { desiredDeviceWidth: live.transform.deviceWidth, visibleRect: live.visibleRect }\n : {\n desiredDeviceWidth: t.deviceWidth,\n visibleRect: { x: 0, y: 0, width: 0, height: 0 },\n };\n },\n ),\n [documentId, page.pon, page.pageIndex, frame, t, stage],\n );\n return (\n <div style={{ position: 'absolute', left, top, width: outerW, height: outerH }}>\n <PageProvider value={ctx}>\n {/* drop shadow ONLY — axis-aligned at the content box (inset by the frame),\n transparent fill so it can never peek out behind the bitmap, and it\n stays put under rotation. */}\n <div\n style={{\n position: 'absolute',\n left: frame.left,\n top: frame.top,\n width: t.viewWidth,\n height: t.viewHeight,\n // themeable: override via the CSS variable (app stylesheet), no props\n boxShadow: 'var(--epdf-page-shadow, 0 6px 18px rgba(0,0,0,.18))',\n }}\n />\n {/* the page: white backing + bitmap as ONE rasterized box, so there is no\n seam between them and nothing white larger than the bitmap to leak.\n The ONLY thing rotation turns; markers/annotations ride it in content\n coordinates. Rotation 0 carries no transform → pixel-snaps cleanly. */}\n <div\n ref={ref}\n style={{\n position: 'absolute',\n left: contentLeft,\n top: contentTop,\n width: t.contentWidth,\n height: t.contentHeight,\n background: '#fff',\n transform: rotation ? `rotate(${rotation}deg)` : undefined,\n // We render our own selection highlights — suppress native text/image\n // selection (and the double-click image grab) on the whole page subtree.\n userSelect: 'none',\n WebkitUserSelect: 'none',\n }}\n >\n {render(ctx)}\n </div>\n {/* box-space chrome — labels, selection border, per-page buttons — fills\n the outer box, NEVER rotates. Bands are plain regions: a label is\n `bottom:0; height: frame.bottom`, a button row `top:0; height: frame.top`. */}\n {chrome?.(ctx)}\n </PageProvider>\n </div>\n );\n}\n\nexport interface StageProps {\n /**\n * PAGE-SPACE content for each visible page (RenderLayer, annotations,\n * markers). Rendered inside the page's content frame, so it ROTATES with the\n * page's display rotation — coordinates are plain PDF points.\n */\n children: (page: PageContextValue) => React.ReactNode;\n /**\n * BOX-SPACE chrome for each visible page (page-number label, selection\n * border, a per-page rotate/delete button). Rendered into the OUTER box\n * (content + reserved `pageFrame`), so it does NOT rotate and the reserved\n * bands are plain regions (`bottom:0; height: page.frame.bottom`). The three\n * coordinate spaces: `children` (page content), `pageChrome` (page box +\n * frame), `overlay` (viewport).\n */\n pageChrome?: (page: PageContextValue) => React.ReactNode;\n /** Viewport-space UI (menus, controls) rendered above the pages. */\n overlay?: React.ReactNode;\n /**\n * Route this Stage's pointer events to the interaction hub (page-resolved via\n * `pageAt`) — AND register this lens's tool-gated pan-scroll handler with it\n * (lens-scoped, so multiple stages on one document never pan each other).\n * Pan is then the `pan` tool's job and dragging in `pointer` mode selects\n * text (incl. across pages).\n *\n * Default TRUE: registering `interactionPlugin()` is the one opt-in — tools\n * just work; without the hub this is inert and the stage falls back to\n * built-in drag-to-pan, so a hub-less setup costs nothing. Set `false` on\n * SECONDARY lenses (a thumbnail rail) that should stay click-to-navigate\n * instead of feeding the document's tools.\n */\n interaction?: boolean;\n /**\n * With {@link interaction}: let drags over page GAPS pan regardless of the\n * active tool (and show a grab cursor there) — the gutter always pans; there\n * is nothing to draw/select outside a page. Default true.\n */\n panFallback?: boolean;\n /**\n * Ambient ZOOM gestures on this stage: ctrl/cmd+wheel and trackpad pinch\n * (Safari gesture events included). Default true. Turn OFF for follower\n * lenses with a fixed magnification — a thumbnail rail should scroll under\n * cmd+wheel, not zoom — so a zoom-wheel falls through to ordinary wheel\n * pan, and pinches are still swallowed (they never page-zoom the browser).\n */\n zoomGestures?: boolean;\n /** The stage lens to drive (default: the main StageToken). */\n token?: StageTokenProp;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport function Stage({\n children,\n pageChrome,\n overlay,\n interaction = true,\n panFallback = true,\n zoomGestures = true,\n token = StageToken,\n className,\n style,\n}: StageProps) {\n const stage = useCapability(token);\n const ix = useOptionalCapability(InteractionToken);\n const useHub = interaction && !!ix;\n // The hub's resolved cursor (text/grab/…), applied to the viewport when driving.\n const hubCursor = useKernelValue(() => ix?.cursor() ?? 'default');\n const ref = useRef<HTMLDivElement>(null);\n const docId = useDocumentId();\n // visiblePages already folds in the camera (each page carries its device-snapped\n // screenX/screenY + transform), so panning re-emits the list — no separate\n // camera subscription needed for positioning.\n const pages = useSelector(token, (c) => c.visiblePages()); // memoized -> stable ref\n // Reserved chrome bands (screen px), uniform across pages — the frame the\n // outer box reserves and `pageChrome` paints into.\n const frame = useSelector(\n token,\n (c) => c.pageFrame(),\n (a, b) => a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left,\n );\n\n // The Stage's ViewProjector: anchored UI (menus, popovers) positions through\n // the CAMERA — pure state, no DOM reads, no portal, and NO subscription:\n // `pages` (visiblePages) is the binding's revision, so a camera change\n // re-renders the pages AND every anchored consumer in the SAME React\n // commit — surface and overlay can never paint a frame apart.\n const projector = useMemo<ViewProjector>(\n () => ({\n space: 'overlay',\n toScreen: (pon, rect) => stage.pageRectToScreen(pon, rect),\n toScreenPoint: (pon, at) => {\n const r = stage.pageRectToScreen(pon, { x: at.x, y: at.y, width: 0, height: 0 });\n return r ? { x: r.x, y: r.y } : null;\n },\n viewEnv: (pon) => {\n const t = stage.pageRect(pon)?.transform;\n return t ? { scale: t.viewScale, rotation: t.rotation, zoom: t.zoom } : null;\n },\n }),\n [stage],\n );\n const projectorBinding = useMemo<ProjectorBinding>(\n () => ({ projector, revision: pages }),\n [projector, pages],\n );\n\n useLayoutEffect(() => {\n const el = ref.current!;\n // The WHOLE browser binding — viewport/DPR reporting, sample normalization,\n // gesture controller — is the shared @embedpdf/web surface, so every\n // framework adapter has one feel. This component keeps only React glue.\n const detachSurface = createStageSurface(el, stage, {\n hub: useHub ? ix : null,\n source: stage.lensId(),\n zoomGestures,\n });\n // Interaction opt-in lives WITH the sample source: the same knob that\n // forwards this lens's samples also registers its pan-scroll handler,\n // lens-scoped — two stages on one document can never pan each other.\n const offScroll =\n useHub && ix\n ? ix.registerHandler(createScrollHandler(stage, ix, { panFallback }), {\n source: stage.lensId(),\n })\n : null;\n\n return () => {\n offScroll?.();\n detachSurface();\n };\n }, [stage, ix, useHub, zoomGestures, panFallback]);\n\n return (\n <div\n ref={ref}\n className={className}\n style={{\n position: 'relative',\n overflow: 'hidden',\n touchAction: 'none',\n ...(useHub ? { cursor: hubCursor } : null),\n ...style,\n }}\n >\n {pages.map((p) => (\n <PageSurface\n key={p.pon} // durable page identity — survives move/delete (matches Angular's `track p.pon`)\n documentId={docId ?? ''}\n page={p}\n frame={frame}\n stage={stage}\n render={children}\n chrome={pageChrome}\n />\n ))}\n {/* Anchored UI mounts in the overlay: absolute coords here are the\n projector's overlay space (the stage container). */}\n <ProjectorProvider value={projectorBinding}>{overlay}</ProjectorProvider>\n </div>\n );\n}\n\n// ── Facade hooks — thin sugar over the capability + generic binding ───────────\nexport function useStage(token: StageTokenProp = StageToken) {\n return useCapability(token);\n}\nexport function useZoom(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const zoom = useSelector(token, (c) => c.zoomLevel());\n const mode = useSelector(token, (c) => c.zoomMode());\n return {\n zoom,\n /** Active zoom intent: 'automatic' | 'fit-page' | 'fit-width' | 'fit-all' | 'custom'. */\n mode,\n zoomIn: s.zoomIn,\n zoomOut: s.zoomOut,\n fitWidth: s.fitWidth,\n fitPage: s.fitPage,\n fitAll: s.fitAll,\n automatic: s.automatic,\n zoomTo: s.zoomTo,\n };\n}\nexport function usePages(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const currentPage = useSelector(token, (c) => c.currentPage());\n const pageCount = useSelector(token, (c) => c.pageCount());\n return {\n currentPage,\n pageCount,\n goToPage: s.goToPage,\n next: s.next,\n prev: s.prev,\n reveal: s.reveal,\n };\n}\nexport function useLayout(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const flow = useSelector(token, (c) => c.flow());\n const layout = useSelector(token, (c) => c.layout());\n const spread = useSelector(token, (c) => c.spread());\n const sizing = useSelector(token, (c) => c.sizing());\n const bounded = useSelector(token, (c) => c.bounded());\n return {\n flow,\n layout,\n spread,\n sizing,\n bounded,\n setFlow: s.setFlow,\n setLayout: s.setLayout,\n setSpread: s.setSpread,\n setSizing: s.setSizing,\n setBounded: s.setBounded,\n };\n}\n\n/** The document's page list (with PDF labels) + the current item's pages — the\n * data for page thumbnails / worksheet-style page tabs. */\nexport function usePageList(token: StageTokenProp = StageToken) {\n const pages = useSelector(\n token,\n (c) => c.pages(),\n (a, b) =>\n a.length === b.length && a.every((p, i) => p.pon === b[i].pon && p.label === b[i].label),\n );\n const current = useSelector(\n token,\n (c) => c.currentItemPages(),\n (a, b) => a.length === b.length && a.every((x, i) => x === b[i]),\n );\n return { pages, currentItemPages: current };\n}\n\n/**\n * All Stage settings + the batch `update`. This is the seam for \"presets are a\n * customer concern\": keep your own `Partial<StageSettings>` objects and apply them\n * with `update(preset)` (one anchor-preserving change).\n */\nexport function useStageSettings(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n // settingsEqual derives from the plugin's settings registry — a new setting is\n // covered here automatically, without this package spelling out the shape.\n const settings = useSelector(token, (c) => c.settings(), settingsEqual);\n return { settings, update: s.update, reset: s.resetView };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stage.cjs","names":["makePageContext","PageProvider","StageToken","useCapability","useOptionalCapability","InteractionToken","useKernelValue","useDocumentId","useSelector","ProjectorProvider","settingsEqual"],"sources":["../src/stage.tsx"],"sourcesContent":["/**\n * Stage / RenderLayer + facade hooks.\n *\n * <Stage> virtualizes and positions page surfaces by the camera, and hands each\n * one to YOUR render prop — you bring the layers. (The standalone, Stage-free\n * single-page surface lives in `./page-view` so it never pulls the stage plugin.)\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/plugin-stage';\nimport * as React from 'react';\nimport { useLayoutEffect, useMemo, useRef } from 'react';\nimport { StageToken, createScrollHandler, settingsEqual } from '@embedpdf/plugin-stage';\nimport type { StageCapability, VisiblePage } from '@embedpdf/plugin-stage';\nimport type { CapabilityToken } from '@embedpdf/core';\n\n/** Which stage lens to bind to. Defaults to the main StageToken — pass a custom\n * token to drive an additional lens (e.g. a wrapped thumbnail sidebar). */\nexport type StageTokenProp = CapabilityToken<StageCapability>;\nimport type { PageFrame } from '@embedpdf/core-geometry';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract';\nimport { createStageSurface } from '@embedpdf/web';\nimport { ProjectorProvider, type ProjectorBinding, type ViewProjector } from './anchored';\nimport {\n makePageContext,\n PageProvider,\n useCapability,\n useDocumentId,\n useKernelValue,\n useOptionalCapability,\n useSelector,\n} from './runtime';\nimport type { PageContextValue } from './runtime';\n\nfunction PageSurface({\n documentId,\n page,\n frame,\n stage,\n render,\n chrome,\n}: {\n documentId: string;\n page: VisiblePage;\n /** Reserved chrome bands around the page (screen px); the layout reserved the\n * matching space, so the outer box tiles into it. */\n frame: PageFrame;\n /** The stage capability — the demand getter reads visibility LIVE off it. */\n stage: StageCapability;\n render: (page: PageContextValue) => React.ReactNode;\n chrome?: (page: PageContextValue) => React.ReactNode;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const t = page.transform;\n const rotation = page.rotation;\n // All geometry comes from the transform: the DISPLAY footprint (viewWidth/Height,\n // already w↔h-swapped + device-snapped) and the UN-rotated content box\n // (contentWidth/Height). The shell never re-derives `* zoom` / `* dpr` / snapping.\n const outerW = t.viewWidth + frame.left + frame.right;\n const outerH = t.viewHeight + frame.top + frame.bottom;\n // page.screenX/screenY are the device-snapped footprint top-left; the outer box\n // sits one frame further out so the content keeps its scene position.\n const left = page.screenX - frame.left;\n const top = page.screenY - frame.top;\n // Center the (possibly rotated) content box on the display box and rotate about\n // its center — NO translate(), so rotation 0 carries no transform and pixel-snaps\n // like the axis-aligned shadow behind it (no hairline seam).\n const contentLeft = frame.left + (t.viewWidth - t.contentWidth) / 2;\n const contentTop = frame.top + (t.viewHeight - t.contentHeight) / 2;\n // The page-view DEMAND is a PULL: the getter closes over\n // stable references (capability + pon) and reads the stage's live state at\n // call time — visibility is the STAGE's data (`VisiblePage.visibleRect`),\n // not something an adapter re-derives or caches. Absent from the visible\n // set = zero rect (\"want nothing\"), distinct from PageView's undefined\n // getter (\"whole page\").\n const ctx = useMemo(\n () =>\n makePageContext(\n documentId,\n // the hosting lens — per-view raster planning keys tile state by it,\n // so a thumbnail rail and the main view never fight over one plan\n stage.lensId(),\n page.pon,\n page.pageIndex,\n frame,\n t,\n () => ref.current!.getBoundingClientRect(),\n () => {\n const live = stage.visiblePages().find((p) => p.pon === page.pon);\n return live\n ? { desiredDeviceWidth: live.transform.deviceWidth, visibleRect: live.visibleRect }\n : {\n desiredDeviceWidth: t.deviceWidth,\n visibleRect: { x: 0, y: 0, width: 0, height: 0 },\n };\n },\n ),\n [documentId, page.pon, page.pageIndex, frame, t, stage],\n );\n return (\n <div style={{ position: 'absolute', left, top, width: outerW, height: outerH }}>\n <PageProvider value={ctx}>\n {/* drop shadow ONLY — axis-aligned at the content box (inset by the frame),\n transparent fill so it can never peek out behind the bitmap, and it\n stays put under rotation. */}\n <div\n style={{\n position: 'absolute',\n left: frame.left,\n top: frame.top,\n width: t.viewWidth,\n height: t.viewHeight,\n // themeable: override via the CSS variable (app stylesheet), no props\n boxShadow: 'var(--epdf-page-shadow, 0 6px 18px rgba(0,0,0,.18))',\n }}\n />\n {/* the page: white backing + bitmap as ONE rasterized box, so there is no\n seam between them and nothing white larger than the bitmap to leak.\n The ONLY thing rotation turns; markers/annotations ride it in content\n coordinates. Rotation 0 carries no transform → pixel-snaps cleanly. */}\n <div\n ref={ref}\n style={{\n position: 'absolute',\n left: contentLeft,\n top: contentTop,\n width: t.contentWidth,\n height: t.contentHeight,\n background: '#fff',\n transform: rotation ? `rotate(${rotation}deg)` : undefined,\n // We render our own selection highlights — suppress native text/image\n // selection (and the double-click image grab) on the whole page subtree.\n userSelect: 'none',\n WebkitUserSelect: 'none',\n }}\n >\n {render(ctx)}\n </div>\n {/* box-space chrome — labels, selection border, per-page buttons — fills\n the outer box, NEVER rotates. Bands are plain regions: a label is\n `bottom:0; height: frame.bottom`, a button row `top:0; height: frame.top`. */}\n {chrome?.(ctx)}\n </PageProvider>\n </div>\n );\n}\n\nexport interface StageProps {\n /**\n * PAGE-SPACE content for each visible page (RenderLayer, annotations,\n * markers). Rendered inside the page's content frame, so it ROTATES with the\n * page's display rotation — coordinates are plain PDF points.\n */\n children: (page: PageContextValue) => React.ReactNode;\n /**\n * BOX-SPACE chrome for each visible page (page-number label, selection\n * border, a per-page rotate/delete button). Rendered into the OUTER box\n * (content + reserved `pageFrame`), so it does NOT rotate and the reserved\n * bands are plain regions (`bottom:0; height: page.frame.bottom`). The three\n * coordinate spaces: `children` (page content), `pageChrome` (page box +\n * frame), `overlay` (viewport).\n */\n pageChrome?: (page: PageContextValue) => React.ReactNode;\n /** Viewport-space UI (menus, controls) rendered above the pages. */\n overlay?: React.ReactNode;\n /**\n * Route this Stage's pointer events to the interaction hub (page-resolved via\n * `pageAt`) — AND register this lens's tool-gated pan-scroll handler with it\n * (lens-scoped, so multiple stages on one document never pan each other).\n * Pan is then the `pan` tool's job and dragging in `pointer` mode selects\n * text (incl. across pages).\n *\n * Default TRUE: registering `interactionPlugin()` is the one opt-in — tools\n * just work; without the hub this is inert and the stage falls back to\n * built-in drag-to-pan, so a hub-less setup costs nothing. Set `false` on\n * SECONDARY lenses (a thumbnail rail) that should stay click-to-navigate\n * instead of feeding the document's tools.\n */\n interaction?: boolean;\n /**\n * With {@link interaction}: let drags over page GAPS pan regardless of the\n * active tool (and show a grab cursor there) — the gutter always pans; there\n * is nothing to draw/select outside a page. Default true.\n */\n panFallback?: boolean;\n /**\n * Ambient ZOOM gestures on this stage: ctrl/cmd+wheel and trackpad pinch\n * (Safari gesture events included). Default true. Turn OFF for follower\n * lenses with a fixed magnification — a thumbnail rail should scroll under\n * cmd+wheel, not zoom — so a zoom-wheel falls through to ordinary wheel\n * pan, and pinches are still swallowed (they never page-zoom the browser).\n */\n zoomGestures?: boolean;\n /** The stage lens to drive (default: the main StageToken). */\n token?: StageTokenProp;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport function Stage({\n children,\n pageChrome,\n overlay,\n interaction = true,\n panFallback = true,\n zoomGestures = true,\n token = StageToken,\n className,\n style,\n}: StageProps) {\n const stage = useCapability(token);\n const ix = useOptionalCapability(InteractionToken);\n const useHub = interaction && !!ix;\n // The hub's resolved cursor (text/grab/…), applied to the viewport when driving.\n const hubCursor = useKernelValue(() => ix?.cursor() ?? 'default');\n const ref = useRef<HTMLDivElement>(null);\n const docId = useDocumentId();\n // visiblePages already folds in the camera (each page carries its device-snapped\n // screenX/screenY + transform), so panning re-emits the list — no separate\n // camera subscription needed for positioning.\n const pages = useSelector(token, (c) => c.visiblePages()); // memoized -> stable ref\n // Reserved chrome bands (screen px), uniform across pages — the frame the\n // outer box reserves and `pageChrome` paints into.\n const frame = useSelector(\n token,\n (c) => c.pageFrame(),\n (a, b) => a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left,\n );\n\n // The Stage's ViewProjector: anchored UI (menus, popovers) positions through\n // the CAMERA — pure state, no DOM reads, no portal, and NO subscription:\n // `pages` (visiblePages) is the binding's revision, so a camera change\n // re-renders the pages AND every anchored consumer in the SAME React\n // commit — surface and overlay can never paint a frame apart.\n const projector = useMemo<ViewProjector>(\n () => ({\n space: 'overlay',\n toScreen: (pon, rect) => stage.pageRectToScreen(pon, rect),\n toScreenPoint: (pon, at) => {\n const r = stage.pageRectToScreen(pon, { x: at.x, y: at.y, width: 0, height: 0 });\n return r ? { x: r.x, y: r.y } : null;\n },\n viewEnv: (pon) => {\n const t = stage.pageRect(pon)?.transform;\n return t ? { scale: t.viewScale, rotation: t.rotation, zoom: t.zoom } : null;\n },\n }),\n [stage],\n );\n const projectorBinding = useMemo<ProjectorBinding>(\n () => ({ projector, revision: pages }),\n [projector, pages],\n );\n\n useLayoutEffect(() => {\n const el = ref.current!;\n // The WHOLE browser binding — viewport/DPR reporting, sample normalization,\n // gesture controller — is the shared @embedpdf/web surface, so every\n // framework adapter has one feel. This component keeps only React glue.\n const detachSurface = createStageSurface(el, stage, {\n hub: useHub ? ix : null,\n source: stage.lensId(),\n zoomGestures,\n });\n // Interaction opt-in lives WITH the sample source: the same knob that\n // forwards this lens's samples also registers its pan-scroll handler,\n // lens-scoped — two stages on one document can never pan each other.\n const offScroll =\n useHub && ix\n ? ix.registerHandler(createScrollHandler(stage, ix, { panFallback }), {\n source: stage.lensId(),\n })\n : null;\n\n return () => {\n offScroll?.();\n detachSurface();\n };\n }, [stage, ix, useHub, zoomGestures, panFallback]);\n\n return (\n <div\n ref={ref}\n className={className}\n style={{\n position: 'relative',\n overflow: 'hidden',\n touchAction: 'none',\n ...(useHub ? { cursor: hubCursor } : null),\n ...style,\n }}\n >\n {pages.map((p) => (\n <PageSurface\n key={p.pon} // durable page identity — survives move/delete (matches Angular's `track p.pon`)\n documentId={docId ?? ''}\n page={p}\n frame={frame}\n stage={stage}\n render={children}\n chrome={pageChrome}\n />\n ))}\n {/* Anchored UI mounts in the overlay: absolute coords here are the\n projector's overlay space (the stage container). */}\n <ProjectorProvider value={projectorBinding}>{overlay}</ProjectorProvider>\n </div>\n );\n}\n\n// ── Facade hooks — thin sugar over the capability + generic binding ───────────\nexport function useStage(token: StageTokenProp = StageToken) {\n return useCapability(token);\n}\nexport function useZoom(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const zoom = useSelector(token, (c) => c.zoomLevel());\n const mode = useSelector(token, (c) => c.zoomMode());\n return {\n zoom,\n /** Active zoom intent: 'automatic' | 'fit-page' | 'fit-width' | 'fit-all' | 'custom'. */\n mode,\n zoomIn: s.zoomIn,\n zoomOut: s.zoomOut,\n fitWidth: s.fitWidth,\n fitPage: s.fitPage,\n fitAll: s.fitAll,\n automatic: s.automatic,\n zoomTo: s.zoomTo,\n };\n}\nexport function usePages(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const currentPage = useSelector(token, (c) => c.currentPage());\n const pageCount = useSelector(token, (c) => c.pageCount());\n return {\n currentPage,\n pageCount,\n goToPage: s.goToPage,\n next: s.next,\n prev: s.prev,\n reveal: s.reveal,\n };\n}\nexport function useLayout(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const flow = useSelector(token, (c) => c.flow());\n const layout = useSelector(token, (c) => c.layout());\n const spread = useSelector(token, (c) => c.spread());\n const sizing = useSelector(token, (c) => c.sizing());\n const bounded = useSelector(token, (c) => c.bounded());\n return {\n flow,\n layout,\n spread,\n sizing,\n bounded,\n setFlow: s.setFlow,\n setLayout: s.setLayout,\n setSpread: s.setSpread,\n setSizing: s.setSizing,\n setBounded: s.setBounded,\n };\n}\n\n/** The document's page list (with PDF labels) + the current item's pages — the\n * data for page thumbnails / worksheet-style page tabs. */\nexport function usePageList(token: StageTokenProp = StageToken) {\n const pages = useSelector(\n token,\n (c) => c.pages(),\n (a, b) =>\n a.length === b.length && a.every((p, i) => p.pon === b[i].pon && p.label === b[i].label),\n );\n const current = useSelector(\n token,\n (c) => c.currentItemPages(),\n (a, b) => a.length === b.length && a.every((x, i) => x === b[i]),\n );\n return { pages, currentItemPages: current };\n}\n\n/**\n * All Stage settings + the batch `update`. This is the seam for \"presets are a\n * customer concern\": keep your own `Partial<StageSettings>` objects and apply them\n * with `update(preset)` (one anchor-preserving change).\n */\nexport function useStageSettings(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n // settingsEqual derives from the plugin's settings registry — a new setting is\n // covered here automatically, without this package spelling out the shape.\n const settings = useSelector(token, (c) => c.settings(), settingsEqual);\n return { settings, update: s.update, reset: s.resetView };\n}\n"],"mappings":"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"}
package/dist/stage.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as PageContextValue } from "./runtime-Cvi0wQ4d.cjs";
1
+ import { o as PageContextValue } from "./runtime-umHA_xHt.cjs";
2
2
  import * as React from "react";
3
3
  import { CapabilityToken } from "@embedpdf/core";
4
4
  import { StageCapability } from "@embedpdf/plugin-stage";
package/dist/stage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as PageContextValue } from "./runtime-DXxr8T4L.js";
1
+ import { o as PageContextValue } from "./runtime-DE76hlnl.js";
2
2
  import { CapabilityToken } from "@embedpdf/core";
3
3
  import * as React from "react";
4
4
  import { StageCapability } from "@embedpdf/plugin-stage";
package/dist/stage.js CHANGED
@@ -5,7 +5,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { useLayoutEffect, useMemo, useRef } from "react";
6
6
  import { createStageSurface } from "@embedpdf/web";
7
7
  import { StageToken, createScrollHandler, settingsEqual } from "@embedpdf/plugin-stage";
8
- import { InteractionToken } from "@embedpdf/plugin-interaction";
8
+ import { InteractionToken } from "@embedpdf/plugin-interaction/contract";
9
9
  export * from "@embedpdf/plugin-stage";
10
10
  //#region src/stage.tsx
11
11
  function PageSurface({ documentId, page, frame, stage, render, chrome }) {
package/dist/stage.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"stage.js","names":[],"sources":["../src/stage.tsx"],"sourcesContent":["/**\n * Stage / RenderLayer + facade hooks.\n *\n * <Stage> virtualizes and positions page surfaces by the camera, and hands each\n * one to YOUR render prop — you bring the layers. (The standalone, Stage-free\n * single-page surface lives in `./page-view` so it never pulls the stage plugin.)\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/plugin-stage';\nimport * as React from 'react';\nimport { useLayoutEffect, useMemo, useRef } from 'react';\nimport { StageToken, createScrollHandler, settingsEqual } from '@embedpdf/plugin-stage';\nimport type { StageCapability, VisiblePage } from '@embedpdf/plugin-stage';\nimport type { CapabilityToken } from '@embedpdf/core';\n\n/** Which stage lens to bind to. Defaults to the main StageToken — pass a custom\n * token to drive an additional lens (e.g. a wrapped thumbnail sidebar). */\nexport type StageTokenProp = CapabilityToken<StageCapability>;\nimport type { PageFrame } from '@embedpdf/core-geometry';\nimport { InteractionToken } from '@embedpdf/plugin-interaction';\nimport { createStageSurface } from '@embedpdf/web';\nimport { ProjectorProvider, type ProjectorBinding, type ViewProjector } from './anchored';\nimport {\n makePageContext,\n PageProvider,\n useCapability,\n useDocumentId,\n useKernelValue,\n useOptionalCapability,\n useSelector,\n} from './runtime';\nimport type { PageContextValue } from './runtime';\n\nfunction PageSurface({\n documentId,\n page,\n frame,\n stage,\n render,\n chrome,\n}: {\n documentId: string;\n page: VisiblePage;\n /** Reserved chrome bands around the page (screen px); the layout reserved the\n * matching space, so the outer box tiles into it. */\n frame: PageFrame;\n /** The stage capability — the demand getter reads visibility LIVE off it. */\n stage: StageCapability;\n render: (page: PageContextValue) => React.ReactNode;\n chrome?: (page: PageContextValue) => React.ReactNode;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const t = page.transform;\n const rotation = page.rotation;\n // All geometry comes from the transform: the DISPLAY footprint (viewWidth/Height,\n // already w↔h-swapped + device-snapped) and the UN-rotated content box\n // (contentWidth/Height). The shell never re-derives `* zoom` / `* dpr` / snapping.\n const outerW = t.viewWidth + frame.left + frame.right;\n const outerH = t.viewHeight + frame.top + frame.bottom;\n // page.screenX/screenY are the device-snapped footprint top-left; the outer box\n // sits one frame further out so the content keeps its scene position.\n const left = page.screenX - frame.left;\n const top = page.screenY - frame.top;\n // Center the (possibly rotated) content box on the display box and rotate about\n // its center — NO translate(), so rotation 0 carries no transform and pixel-snaps\n // like the axis-aligned shadow behind it (no hairline seam).\n const contentLeft = frame.left + (t.viewWidth - t.contentWidth) / 2;\n const contentTop = frame.top + (t.viewHeight - t.contentHeight) / 2;\n // The page-view DEMAND is a PULL: the getter closes over\n // stable references (capability + pon) and reads the stage's live state at\n // call time — visibility is the STAGE's data (`VisiblePage.visibleRect`),\n // not something an adapter re-derives or caches. Absent from the visible\n // set = zero rect (\"want nothing\"), distinct from PageView's undefined\n // getter (\"whole page\").\n const ctx = useMemo(\n () =>\n makePageContext(\n documentId,\n // the hosting lens — per-view raster planning keys tile state by it,\n // so a thumbnail rail and the main view never fight over one plan\n stage.lensId(),\n page.pon,\n page.pageIndex,\n frame,\n t,\n () => ref.current!.getBoundingClientRect(),\n () => {\n const live = stage.visiblePages().find((p) => p.pon === page.pon);\n return live\n ? { desiredDeviceWidth: live.transform.deviceWidth, visibleRect: live.visibleRect }\n : {\n desiredDeviceWidth: t.deviceWidth,\n visibleRect: { x: 0, y: 0, width: 0, height: 0 },\n };\n },\n ),\n [documentId, page.pon, page.pageIndex, frame, t, stage],\n );\n return (\n <div style={{ position: 'absolute', left, top, width: outerW, height: outerH }}>\n <PageProvider value={ctx}>\n {/* drop shadow ONLY — axis-aligned at the content box (inset by the frame),\n transparent fill so it can never peek out behind the bitmap, and it\n stays put under rotation. */}\n <div\n style={{\n position: 'absolute',\n left: frame.left,\n top: frame.top,\n width: t.viewWidth,\n height: t.viewHeight,\n // themeable: override via the CSS variable (app stylesheet), no props\n boxShadow: 'var(--epdf-page-shadow, 0 6px 18px rgba(0,0,0,.18))',\n }}\n />\n {/* the page: white backing + bitmap as ONE rasterized box, so there is no\n seam between them and nothing white larger than the bitmap to leak.\n The ONLY thing rotation turns; markers/annotations ride it in content\n coordinates. Rotation 0 carries no transform → pixel-snaps cleanly. */}\n <div\n ref={ref}\n style={{\n position: 'absolute',\n left: contentLeft,\n top: contentTop,\n width: t.contentWidth,\n height: t.contentHeight,\n background: '#fff',\n transform: rotation ? `rotate(${rotation}deg)` : undefined,\n // We render our own selection highlights — suppress native text/image\n // selection (and the double-click image grab) on the whole page subtree.\n userSelect: 'none',\n WebkitUserSelect: 'none',\n }}\n >\n {render(ctx)}\n </div>\n {/* box-space chrome — labels, selection border, per-page buttons — fills\n the outer box, NEVER rotates. Bands are plain regions: a label is\n `bottom:0; height: frame.bottom`, a button row `top:0; height: frame.top`. */}\n {chrome?.(ctx)}\n </PageProvider>\n </div>\n );\n}\n\nexport interface StageProps {\n /**\n * PAGE-SPACE content for each visible page (RenderLayer, annotations,\n * markers). Rendered inside the page's content frame, so it ROTATES with the\n * page's display rotation — coordinates are plain PDF points.\n */\n children: (page: PageContextValue) => React.ReactNode;\n /**\n * BOX-SPACE chrome for each visible page (page-number label, selection\n * border, a per-page rotate/delete button). Rendered into the OUTER box\n * (content + reserved `pageFrame`), so it does NOT rotate and the reserved\n * bands are plain regions (`bottom:0; height: page.frame.bottom`). The three\n * coordinate spaces: `children` (page content), `pageChrome` (page box +\n * frame), `overlay` (viewport).\n */\n pageChrome?: (page: PageContextValue) => React.ReactNode;\n /** Viewport-space UI (menus, controls) rendered above the pages. */\n overlay?: React.ReactNode;\n /**\n * Route this Stage's pointer events to the interaction hub (page-resolved via\n * `pageAt`) — AND register this lens's tool-gated pan-scroll handler with it\n * (lens-scoped, so multiple stages on one document never pan each other).\n * Pan is then the `pan` tool's job and dragging in `pointer` mode selects\n * text (incl. across pages).\n *\n * Default TRUE: registering `interactionPlugin()` is the one opt-in — tools\n * just work; without the hub this is inert and the stage falls back to\n * built-in drag-to-pan, so a hub-less setup costs nothing. Set `false` on\n * SECONDARY lenses (a thumbnail rail) that should stay click-to-navigate\n * instead of feeding the document's tools.\n */\n interaction?: boolean;\n /**\n * With {@link interaction}: let drags over page GAPS pan regardless of the\n * active tool (and show a grab cursor there) — the gutter always pans; there\n * is nothing to draw/select outside a page. Default true.\n */\n panFallback?: boolean;\n /**\n * Ambient ZOOM gestures on this stage: ctrl/cmd+wheel and trackpad pinch\n * (Safari gesture events included). Default true. Turn OFF for follower\n * lenses with a fixed magnification — a thumbnail rail should scroll under\n * cmd+wheel, not zoom — so a zoom-wheel falls through to ordinary wheel\n * pan, and pinches are still swallowed (they never page-zoom the browser).\n */\n zoomGestures?: boolean;\n /** The stage lens to drive (default: the main StageToken). */\n token?: StageTokenProp;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport function Stage({\n children,\n pageChrome,\n overlay,\n interaction = true,\n panFallback = true,\n zoomGestures = true,\n token = StageToken,\n className,\n style,\n}: StageProps) {\n const stage = useCapability(token);\n const ix = useOptionalCapability(InteractionToken);\n const useHub = interaction && !!ix;\n // The hub's resolved cursor (text/grab/…), applied to the viewport when driving.\n const hubCursor = useKernelValue(() => ix?.cursor() ?? 'default');\n const ref = useRef<HTMLDivElement>(null);\n const docId = useDocumentId();\n // visiblePages already folds in the camera (each page carries its device-snapped\n // screenX/screenY + transform), so panning re-emits the list — no separate\n // camera subscription needed for positioning.\n const pages = useSelector(token, (c) => c.visiblePages()); // memoized -> stable ref\n // Reserved chrome bands (screen px), uniform across pages — the frame the\n // outer box reserves and `pageChrome` paints into.\n const frame = useSelector(\n token,\n (c) => c.pageFrame(),\n (a, b) => a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left,\n );\n\n // The Stage's ViewProjector: anchored UI (menus, popovers) positions through\n // the CAMERA — pure state, no DOM reads, no portal, and NO subscription:\n // `pages` (visiblePages) is the binding's revision, so a camera change\n // re-renders the pages AND every anchored consumer in the SAME React\n // commit — surface and overlay can never paint a frame apart.\n const projector = useMemo<ViewProjector>(\n () => ({\n space: 'overlay',\n toScreen: (pon, rect) => stage.pageRectToScreen(pon, rect),\n toScreenPoint: (pon, at) => {\n const r = stage.pageRectToScreen(pon, { x: at.x, y: at.y, width: 0, height: 0 });\n return r ? { x: r.x, y: r.y } : null;\n },\n viewEnv: (pon) => {\n const t = stage.pageRect(pon)?.transform;\n return t ? { scale: t.viewScale, rotation: t.rotation, zoom: t.zoom } : null;\n },\n }),\n [stage],\n );\n const projectorBinding = useMemo<ProjectorBinding>(\n () => ({ projector, revision: pages }),\n [projector, pages],\n );\n\n useLayoutEffect(() => {\n const el = ref.current!;\n // The WHOLE browser binding — viewport/DPR reporting, sample normalization,\n // gesture controller — is the shared @embedpdf/web surface, so every\n // framework adapter has one feel. This component keeps only React glue.\n const detachSurface = createStageSurface(el, stage, {\n hub: useHub ? ix : null,\n source: stage.lensId(),\n zoomGestures,\n });\n // Interaction opt-in lives WITH the sample source: the same knob that\n // forwards this lens's samples also registers its pan-scroll handler,\n // lens-scoped — two stages on one document can never pan each other.\n const offScroll =\n useHub && ix\n ? ix.registerHandler(createScrollHandler(stage, ix, { panFallback }), {\n source: stage.lensId(),\n })\n : null;\n\n return () => {\n offScroll?.();\n detachSurface();\n };\n }, [stage, ix, useHub, zoomGestures, panFallback]);\n\n return (\n <div\n ref={ref}\n className={className}\n style={{\n position: 'relative',\n overflow: 'hidden',\n touchAction: 'none',\n ...(useHub ? { cursor: hubCursor } : null),\n ...style,\n }}\n >\n {pages.map((p) => (\n <PageSurface\n key={p.pon} // durable page identity — survives move/delete (matches Angular's `track p.pon`)\n documentId={docId ?? ''}\n page={p}\n frame={frame}\n stage={stage}\n render={children}\n chrome={pageChrome}\n />\n ))}\n {/* Anchored UI mounts in the overlay: absolute coords here are the\n projector's overlay space (the stage container). */}\n <ProjectorProvider value={projectorBinding}>{overlay}</ProjectorProvider>\n </div>\n );\n}\n\n// ── Facade hooks — thin sugar over the capability + generic binding ───────────\nexport function useStage(token: StageTokenProp = StageToken) {\n return useCapability(token);\n}\nexport function useZoom(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const zoom = useSelector(token, (c) => c.zoomLevel());\n const mode = useSelector(token, (c) => c.zoomMode());\n return {\n zoom,\n /** Active zoom intent: 'automatic' | 'fit-page' | 'fit-width' | 'fit-all' | 'custom'. */\n mode,\n zoomIn: s.zoomIn,\n zoomOut: s.zoomOut,\n fitWidth: s.fitWidth,\n fitPage: s.fitPage,\n fitAll: s.fitAll,\n automatic: s.automatic,\n zoomTo: s.zoomTo,\n };\n}\nexport function usePages(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const currentPage = useSelector(token, (c) => c.currentPage());\n const pageCount = useSelector(token, (c) => c.pageCount());\n return {\n currentPage,\n pageCount,\n goToPage: s.goToPage,\n next: s.next,\n prev: s.prev,\n reveal: s.reveal,\n };\n}\nexport function useLayout(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const flow = useSelector(token, (c) => c.flow());\n const layout = useSelector(token, (c) => c.layout());\n const spread = useSelector(token, (c) => c.spread());\n const sizing = useSelector(token, (c) => c.sizing());\n const bounded = useSelector(token, (c) => c.bounded());\n return {\n flow,\n layout,\n spread,\n sizing,\n bounded,\n setFlow: s.setFlow,\n setLayout: s.setLayout,\n setSpread: s.setSpread,\n setSizing: s.setSizing,\n setBounded: s.setBounded,\n };\n}\n\n/** The document's page list (with PDF labels) + the current item's pages — the\n * data for page thumbnails / worksheet-style page tabs. */\nexport function usePageList(token: StageTokenProp = StageToken) {\n const pages = useSelector(\n token,\n (c) => c.pages(),\n (a, b) =>\n a.length === b.length && a.every((p, i) => p.pon === b[i].pon && p.label === b[i].label),\n );\n const current = useSelector(\n token,\n (c) => c.currentItemPages(),\n (a, b) => a.length === b.length && a.every((x, i) => x === b[i]),\n );\n return { pages, currentItemPages: current };\n}\n\n/**\n * All Stage settings + the batch `update`. This is the seam for \"presets are a\n * customer concern\": keep your own `Partial<StageSettings>` objects and apply them\n * with `update(preset)` (one anchor-preserving change).\n */\nexport function useStageSettings(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n // settingsEqual derives from the plugin's settings registry — a new setting is\n // covered here automatically, without this package spelling out the shape.\n const settings = useSelector(token, (c) => c.settings(), settingsEqual);\n return { settings, update: s.update, reset: s.resetView };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stage.js","names":[],"sources":["../src/stage.tsx"],"sourcesContent":["/**\n * Stage / RenderLayer + facade hooks.\n *\n * <Stage> virtualizes and positions page surfaces by the camera, and hands each\n * one to YOUR render prop — you bring the layers. (The standalone, Stage-free\n * single-page surface lives in `./page-view` so it never pulls the stage plugin.)\n */\n\n// One-line-per-feature: registration travels with the UI.\nexport * from '@embedpdf/plugin-stage';\nimport * as React from 'react';\nimport { useLayoutEffect, useMemo, useRef } from 'react';\nimport { StageToken, createScrollHandler, settingsEqual } from '@embedpdf/plugin-stage';\nimport type { StageCapability, VisiblePage } from '@embedpdf/plugin-stage';\nimport type { CapabilityToken } from '@embedpdf/core';\n\n/** Which stage lens to bind to. Defaults to the main StageToken — pass a custom\n * token to drive an additional lens (e.g. a wrapped thumbnail sidebar). */\nexport type StageTokenProp = CapabilityToken<StageCapability>;\nimport type { PageFrame } from '@embedpdf/core-geometry';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract';\nimport { createStageSurface } from '@embedpdf/web';\nimport { ProjectorProvider, type ProjectorBinding, type ViewProjector } from './anchored';\nimport {\n makePageContext,\n PageProvider,\n useCapability,\n useDocumentId,\n useKernelValue,\n useOptionalCapability,\n useSelector,\n} from './runtime';\nimport type { PageContextValue } from './runtime';\n\nfunction PageSurface({\n documentId,\n page,\n frame,\n stage,\n render,\n chrome,\n}: {\n documentId: string;\n page: VisiblePage;\n /** Reserved chrome bands around the page (screen px); the layout reserved the\n * matching space, so the outer box tiles into it. */\n frame: PageFrame;\n /** The stage capability — the demand getter reads visibility LIVE off it. */\n stage: StageCapability;\n render: (page: PageContextValue) => React.ReactNode;\n chrome?: (page: PageContextValue) => React.ReactNode;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const t = page.transform;\n const rotation = page.rotation;\n // All geometry comes from the transform: the DISPLAY footprint (viewWidth/Height,\n // already w↔h-swapped + device-snapped) and the UN-rotated content box\n // (contentWidth/Height). The shell never re-derives `* zoom` / `* dpr` / snapping.\n const outerW = t.viewWidth + frame.left + frame.right;\n const outerH = t.viewHeight + frame.top + frame.bottom;\n // page.screenX/screenY are the device-snapped footprint top-left; the outer box\n // sits one frame further out so the content keeps its scene position.\n const left = page.screenX - frame.left;\n const top = page.screenY - frame.top;\n // Center the (possibly rotated) content box on the display box and rotate about\n // its center — NO translate(), so rotation 0 carries no transform and pixel-snaps\n // like the axis-aligned shadow behind it (no hairline seam).\n const contentLeft = frame.left + (t.viewWidth - t.contentWidth) / 2;\n const contentTop = frame.top + (t.viewHeight - t.contentHeight) / 2;\n // The page-view DEMAND is a PULL: the getter closes over\n // stable references (capability + pon) and reads the stage's live state at\n // call time — visibility is the STAGE's data (`VisiblePage.visibleRect`),\n // not something an adapter re-derives or caches. Absent from the visible\n // set = zero rect (\"want nothing\"), distinct from PageView's undefined\n // getter (\"whole page\").\n const ctx = useMemo(\n () =>\n makePageContext(\n documentId,\n // the hosting lens — per-view raster planning keys tile state by it,\n // so a thumbnail rail and the main view never fight over one plan\n stage.lensId(),\n page.pon,\n page.pageIndex,\n frame,\n t,\n () => ref.current!.getBoundingClientRect(),\n () => {\n const live = stage.visiblePages().find((p) => p.pon === page.pon);\n return live\n ? { desiredDeviceWidth: live.transform.deviceWidth, visibleRect: live.visibleRect }\n : {\n desiredDeviceWidth: t.deviceWidth,\n visibleRect: { x: 0, y: 0, width: 0, height: 0 },\n };\n },\n ),\n [documentId, page.pon, page.pageIndex, frame, t, stage],\n );\n return (\n <div style={{ position: 'absolute', left, top, width: outerW, height: outerH }}>\n <PageProvider value={ctx}>\n {/* drop shadow ONLY — axis-aligned at the content box (inset by the frame),\n transparent fill so it can never peek out behind the bitmap, and it\n stays put under rotation. */}\n <div\n style={{\n position: 'absolute',\n left: frame.left,\n top: frame.top,\n width: t.viewWidth,\n height: t.viewHeight,\n // themeable: override via the CSS variable (app stylesheet), no props\n boxShadow: 'var(--epdf-page-shadow, 0 6px 18px rgba(0,0,0,.18))',\n }}\n />\n {/* the page: white backing + bitmap as ONE rasterized box, so there is no\n seam between them and nothing white larger than the bitmap to leak.\n The ONLY thing rotation turns; markers/annotations ride it in content\n coordinates. Rotation 0 carries no transform → pixel-snaps cleanly. */}\n <div\n ref={ref}\n style={{\n position: 'absolute',\n left: contentLeft,\n top: contentTop,\n width: t.contentWidth,\n height: t.contentHeight,\n background: '#fff',\n transform: rotation ? `rotate(${rotation}deg)` : undefined,\n // We render our own selection highlights — suppress native text/image\n // selection (and the double-click image grab) on the whole page subtree.\n userSelect: 'none',\n WebkitUserSelect: 'none',\n }}\n >\n {render(ctx)}\n </div>\n {/* box-space chrome — labels, selection border, per-page buttons — fills\n the outer box, NEVER rotates. Bands are plain regions: a label is\n `bottom:0; height: frame.bottom`, a button row `top:0; height: frame.top`. */}\n {chrome?.(ctx)}\n </PageProvider>\n </div>\n );\n}\n\nexport interface StageProps {\n /**\n * PAGE-SPACE content for each visible page (RenderLayer, annotations,\n * markers). Rendered inside the page's content frame, so it ROTATES with the\n * page's display rotation — coordinates are plain PDF points.\n */\n children: (page: PageContextValue) => React.ReactNode;\n /**\n * BOX-SPACE chrome for each visible page (page-number label, selection\n * border, a per-page rotate/delete button). Rendered into the OUTER box\n * (content + reserved `pageFrame`), so it does NOT rotate and the reserved\n * bands are plain regions (`bottom:0; height: page.frame.bottom`). The three\n * coordinate spaces: `children` (page content), `pageChrome` (page box +\n * frame), `overlay` (viewport).\n */\n pageChrome?: (page: PageContextValue) => React.ReactNode;\n /** Viewport-space UI (menus, controls) rendered above the pages. */\n overlay?: React.ReactNode;\n /**\n * Route this Stage's pointer events to the interaction hub (page-resolved via\n * `pageAt`) — AND register this lens's tool-gated pan-scroll handler with it\n * (lens-scoped, so multiple stages on one document never pan each other).\n * Pan is then the `pan` tool's job and dragging in `pointer` mode selects\n * text (incl. across pages).\n *\n * Default TRUE: registering `interactionPlugin()` is the one opt-in — tools\n * just work; without the hub this is inert and the stage falls back to\n * built-in drag-to-pan, so a hub-less setup costs nothing. Set `false` on\n * SECONDARY lenses (a thumbnail rail) that should stay click-to-navigate\n * instead of feeding the document's tools.\n */\n interaction?: boolean;\n /**\n * With {@link interaction}: let drags over page GAPS pan regardless of the\n * active tool (and show a grab cursor there) — the gutter always pans; there\n * is nothing to draw/select outside a page. Default true.\n */\n panFallback?: boolean;\n /**\n * Ambient ZOOM gestures on this stage: ctrl/cmd+wheel and trackpad pinch\n * (Safari gesture events included). Default true. Turn OFF for follower\n * lenses with a fixed magnification — a thumbnail rail should scroll under\n * cmd+wheel, not zoom — so a zoom-wheel falls through to ordinary wheel\n * pan, and pinches are still swallowed (they never page-zoom the browser).\n */\n zoomGestures?: boolean;\n /** The stage lens to drive (default: the main StageToken). */\n token?: StageTokenProp;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport function Stage({\n children,\n pageChrome,\n overlay,\n interaction = true,\n panFallback = true,\n zoomGestures = true,\n token = StageToken,\n className,\n style,\n}: StageProps) {\n const stage = useCapability(token);\n const ix = useOptionalCapability(InteractionToken);\n const useHub = interaction && !!ix;\n // The hub's resolved cursor (text/grab/…), applied to the viewport when driving.\n const hubCursor = useKernelValue(() => ix?.cursor() ?? 'default');\n const ref = useRef<HTMLDivElement>(null);\n const docId = useDocumentId();\n // visiblePages already folds in the camera (each page carries its device-snapped\n // screenX/screenY + transform), so panning re-emits the list — no separate\n // camera subscription needed for positioning.\n const pages = useSelector(token, (c) => c.visiblePages()); // memoized -> stable ref\n // Reserved chrome bands (screen px), uniform across pages — the frame the\n // outer box reserves and `pageChrome` paints into.\n const frame = useSelector(\n token,\n (c) => c.pageFrame(),\n (a, b) => a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left,\n );\n\n // The Stage's ViewProjector: anchored UI (menus, popovers) positions through\n // the CAMERA — pure state, no DOM reads, no portal, and NO subscription:\n // `pages` (visiblePages) is the binding's revision, so a camera change\n // re-renders the pages AND every anchored consumer in the SAME React\n // commit — surface and overlay can never paint a frame apart.\n const projector = useMemo<ViewProjector>(\n () => ({\n space: 'overlay',\n toScreen: (pon, rect) => stage.pageRectToScreen(pon, rect),\n toScreenPoint: (pon, at) => {\n const r = stage.pageRectToScreen(pon, { x: at.x, y: at.y, width: 0, height: 0 });\n return r ? { x: r.x, y: r.y } : null;\n },\n viewEnv: (pon) => {\n const t = stage.pageRect(pon)?.transform;\n return t ? { scale: t.viewScale, rotation: t.rotation, zoom: t.zoom } : null;\n },\n }),\n [stage],\n );\n const projectorBinding = useMemo<ProjectorBinding>(\n () => ({ projector, revision: pages }),\n [projector, pages],\n );\n\n useLayoutEffect(() => {\n const el = ref.current!;\n // The WHOLE browser binding — viewport/DPR reporting, sample normalization,\n // gesture controller — is the shared @embedpdf/web surface, so every\n // framework adapter has one feel. This component keeps only React glue.\n const detachSurface = createStageSurface(el, stage, {\n hub: useHub ? ix : null,\n source: stage.lensId(),\n zoomGestures,\n });\n // Interaction opt-in lives WITH the sample source: the same knob that\n // forwards this lens's samples also registers its pan-scroll handler,\n // lens-scoped — two stages on one document can never pan each other.\n const offScroll =\n useHub && ix\n ? ix.registerHandler(createScrollHandler(stage, ix, { panFallback }), {\n source: stage.lensId(),\n })\n : null;\n\n return () => {\n offScroll?.();\n detachSurface();\n };\n }, [stage, ix, useHub, zoomGestures, panFallback]);\n\n return (\n <div\n ref={ref}\n className={className}\n style={{\n position: 'relative',\n overflow: 'hidden',\n touchAction: 'none',\n ...(useHub ? { cursor: hubCursor } : null),\n ...style,\n }}\n >\n {pages.map((p) => (\n <PageSurface\n key={p.pon} // durable page identity — survives move/delete (matches Angular's `track p.pon`)\n documentId={docId ?? ''}\n page={p}\n frame={frame}\n stage={stage}\n render={children}\n chrome={pageChrome}\n />\n ))}\n {/* Anchored UI mounts in the overlay: absolute coords here are the\n projector's overlay space (the stage container). */}\n <ProjectorProvider value={projectorBinding}>{overlay}</ProjectorProvider>\n </div>\n );\n}\n\n// ── Facade hooks — thin sugar over the capability + generic binding ───────────\nexport function useStage(token: StageTokenProp = StageToken) {\n return useCapability(token);\n}\nexport function useZoom(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const zoom = useSelector(token, (c) => c.zoomLevel());\n const mode = useSelector(token, (c) => c.zoomMode());\n return {\n zoom,\n /** Active zoom intent: 'automatic' | 'fit-page' | 'fit-width' | 'fit-all' | 'custom'. */\n mode,\n zoomIn: s.zoomIn,\n zoomOut: s.zoomOut,\n fitWidth: s.fitWidth,\n fitPage: s.fitPage,\n fitAll: s.fitAll,\n automatic: s.automatic,\n zoomTo: s.zoomTo,\n };\n}\nexport function usePages(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const currentPage = useSelector(token, (c) => c.currentPage());\n const pageCount = useSelector(token, (c) => c.pageCount());\n return {\n currentPage,\n pageCount,\n goToPage: s.goToPage,\n next: s.next,\n prev: s.prev,\n reveal: s.reveal,\n };\n}\nexport function useLayout(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n const flow = useSelector(token, (c) => c.flow());\n const layout = useSelector(token, (c) => c.layout());\n const spread = useSelector(token, (c) => c.spread());\n const sizing = useSelector(token, (c) => c.sizing());\n const bounded = useSelector(token, (c) => c.bounded());\n return {\n flow,\n layout,\n spread,\n sizing,\n bounded,\n setFlow: s.setFlow,\n setLayout: s.setLayout,\n setSpread: s.setSpread,\n setSizing: s.setSizing,\n setBounded: s.setBounded,\n };\n}\n\n/** The document's page list (with PDF labels) + the current item's pages — the\n * data for page thumbnails / worksheet-style page tabs. */\nexport function usePageList(token: StageTokenProp = StageToken) {\n const pages = useSelector(\n token,\n (c) => c.pages(),\n (a, b) =>\n a.length === b.length && a.every((p, i) => p.pon === b[i].pon && p.label === b[i].label),\n );\n const current = useSelector(\n token,\n (c) => c.currentItemPages(),\n (a, b) => a.length === b.length && a.every((x, i) => x === b[i]),\n );\n return { pages, currentItemPages: current };\n}\n\n/**\n * All Stage settings + the batch `update`. This is the seam for \"presets are a\n * customer concern\": keep your own `Partial<StageSettings>` objects and apply them\n * with `update(preset)` (one anchor-preserving change).\n */\nexport function useStageSettings(token: StageTokenProp = StageToken) {\n const s = useCapability(token);\n // settingsEqual derives from the plugin's settings registry — a new setting is\n // covered here automatically, without this package spelling out the shape.\n const settings = useSelector(token, (c) => c.settings(), settingsEqual);\n return { settings, update: s.update, reset: s.resetView };\n}\n"],"mappings":"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"}
package/dist/toolbar.cjs CHANGED
@@ -4,10 +4,10 @@ const require_runtime = require("./runtime.cjs");
4
4
  let react_jsx_runtime = require("react/jsx-runtime");
5
5
  let react = require("react");
6
6
  react = require_rolldown_runtime.__toESM(react, 1);
7
- let _embedpdf_plugin_commands = require("@embedpdf/plugin-commands");
8
7
  let _embedpdf_core_ui = require("@embedpdf/core-ui");
8
+ let _embedpdf_plugin_commands_contract = require("@embedpdf/plugin-commands/contract");
9
9
  //#region src/toolbar.tsx
10
- const stripGroupsEqual = (a, b) => a.length === b.length && a.every((g, i) => g.id === b[i].id && g.commands.length === b[i].commands.length && g.commands.every((c, j) => (0, _embedpdf_plugin_commands.resolvedCommandsEqual)(c, b[i].commands[j])));
10
+ const stripGroupsEqual = (a, b) => a.length === b.length && a.every((g, i) => g.id === b[i].id && g.commands.length === b[i].commands.length && g.commands.every((c, j) => (0, _embedpdf_plugin_commands_contract.resolvedCommandsEqual)(c, b[i].commands[j])));
11
11
  const NO_STRIP_GROUPS = [];
12
12
  /**
13
13
  * The live projection of a bar through the command registry — <Toolbar>'s
@@ -19,7 +19,7 @@ const NO_STRIP_GROUPS = [];
19
19
  * consumers re-render on real change, not on every store action.
20
20
  */
21
21
  function useStripView(bar) {
22
- const commands = require_runtime.useCapability(_embedpdf_plugin_commands.CommandsToken);
22
+ const commands = require_runtime.useCapability(_embedpdf_plugin_commands_contract.CommandsToken);
23
23
  const documentId = require_runtime.useDocumentId();
24
24
  const normalized = (0, react.useMemo)(() => bar ? (0, _embedpdf_core_ui.normalizeBar)(bar) : null, [bar]);
25
25
  const groups = require_runtime.useKernelValue(() => {
@@ -297,7 +297,7 @@ const groupTriggerKey = (id) => `gt:${id}`;
297
297
  const TRIGGER_KEY = "t:";
298
298
  function Toolbar({ bar, gap = 8, separatorWidth = 1, className, style, renderCommand = defaultRenderCommand, renderCustom, renderCollapsed, renderGroupTrigger, renderSeparator = defaultRenderSeparator, renderOverflowTrigger = defaultRenderOverflowTrigger, renderOverflowMenu }) {
299
299
  const kernel = require_runtime.useKernel();
300
- const commands = require_runtime.useCapability(_embedpdf_plugin_commands.CommandsToken);
300
+ const commands = require_runtime.useCapability(_embedpdf_plugin_commands_contract.CommandsToken);
301
301
  const documentId = require_runtime.useDocumentId();
302
302
  const [, force] = (0, react.useReducer)((x) => x + 1, 0);
303
303
  (0, react.useEffect)(() => kernel.subscribe(force), [kernel]);