@embedpdf/plugin-stage 3.0.0-next.6 → 3.0.0-next.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CapabilityToken, PageLayout, PageObjectNumber, PdfDestination } from "@embedpdf/core";
2
2
  import { PageRotation, PageTransform, Rect } from "@embedpdf/core-geometry";
3
3
  import { Align, AlignValue, Alignment, Alignment as Alignment$1, AlignmentValue, Anchor, Camera, Direction, Direction as Direction$1, PageBox, PageFrame, PageFrame as PageFrame$1, Point, ScrollMetrics, ScrollMetrics as ScrollMetrics$1, Size, SizingMode, SizingMode as SizingMode$1, SpreadMode, SpreadMode as SpreadMode$1, ZoomMode, ZoomModeValue, ZoomModeValue as ZoomModeValue$1, ZoomSpec, ZoomSpec as ZoomSpec$1 } from "@embedpdf/core-stage";
4
+ import { InteractionCapability, InteractionHandler } from "@embedpdf/plugin-interaction";
4
5
  //#region src/types.d.ts
5
6
  type LayoutKind = 'vertical' | 'horizontal' | 'grid';
6
7
  /** Navigation (goToPage) either tweens the camera or jumps instantly. */
@@ -30,6 +31,53 @@ type GridColumns = 'square' | 'auto' | number;
30
31
  type Gap = number | {
31
32
  px: number;
32
33
  };
34
+ /**
35
+ * The one environmental fact a headless stage has: the box it was told about
36
+ * (`setViewport`). Responsive rules can query nothing else — no user agent, no
37
+ * pointer type (modality is per-event, on `PointerSample`), no window. Space,
38
+ * not device: a narrow pane on a desktop is compact too, and each stage
39
+ * instance resolves against ITS OWN box.
40
+ */
41
+ interface StageBox {
42
+ width: number;
43
+ height: number;
44
+ /** Of the CONTAINER, not the device. A square box is 'portrait' (the CSS rule). */
45
+ orientation: 'portrait' | 'landscape';
46
+ }
47
+ /** Declarative box query — all bounds inclusive, all fields optional (AND-ed). */
48
+ interface BoxQuery {
49
+ minWidth?: number;
50
+ maxWidth?: number;
51
+ minHeight?: number;
52
+ maxHeight?: number;
53
+ orientation?: 'portrait' | 'landscape';
54
+ }
55
+ /**
56
+ * One `@container` block for the settings bag: when the box matches, assert
57
+ * this settings patch. Rules evaluate in source order and ALL matching rules
58
+ * apply, later winning per key (each key replaced whole — no deep merges).
59
+ * Effective settings = base (config + runtime setters) ⊕ matching patches.
60
+ *
61
+ * Semantics, stated honestly:
62
+ * • Runtime setters write the BASE; a matching rule wins over it. Apps
63
+ * needing situational absolute control edit the rules (`setResponsive`).
64
+ * • Rules assert at TRANSITIONS (box/base/rules changes), not continuously —
65
+ * between crossings, interaction owns the state. A rule containing `zoom`
66
+ * re-fits when the box crosses it (the rotate-an-iPad behavior) and then
67
+ * leaves the pinch alone.
68
+ *
69
+ * A rule with a `name` is a queryable fact (`matches(name)`), reactive in
70
+ * every framework; a named rule with NO settings is a pure shared breakpoint
71
+ * the app chrome can key its own presentation off — one definition serving
72
+ * both the layout math and the UI.
73
+ */
74
+ interface ResponsiveRule {
75
+ name?: string;
76
+ /** Declarative box query, or a predicate for anything the box can answer. */
77
+ when: BoxQuery | ((box: StageBox) => boolean);
78
+ /** The settings this situation asserts. Omit for a pure named query. */
79
+ settings?: Partial<StageSettings>;
80
+ }
33
81
  /**
34
82
  * One axis of the ARRIVAL policy — stage-core's AlignValue ('start' |
35
83
  * 'center' | 'end' | viewport fraction 0–1) plus one navigation-only word:
@@ -186,6 +234,19 @@ interface VisiblePage extends PageBox {
186
234
  }
187
235
  interface StageState extends StageSettings {
188
236
  camera: Camera;
237
+ /**
238
+ * One-way render-commit latch. False while the initial viewport-driven
239
+ * placement is unresolved; true only after its camera/settings writes have
240
+ * completed. Page/scroll screen-space selectors refuse to answer while
241
+ * false, so an adapter can never paint the placeholder camera at the origin.
242
+ */
243
+ placed: boolean;
244
+ /**
245
+ * Names of the responsive rules currently matching the box, in source order.
246
+ * State (not derived) so `matches()` is reactive through the ordinary
247
+ * selector machinery in every framework.
248
+ */
249
+ activeRules: readonly string[];
189
250
  /**
190
251
  * False while the ZOOM is in motion, true once it has rested (~150ms of
191
252
  * frames without a zoom write). Device-snapping of page origins is gated
@@ -217,6 +278,8 @@ type StageAction = {
217
278
  } | {
218
279
  type: 'CAMERA_REST';
219
280
  resting: boolean;
281
+ } | {
282
+ type: 'PLACED';
220
283
  } | {
221
284
  type: 'VP';
222
285
  vp: Size;
@@ -229,6 +292,9 @@ type StageAction = {
229
292
  } | {
230
293
  type: 'PATCH';
231
294
  patch: Partial<StageSettings>;
295
+ } | {
296
+ type: 'RESPONSIVE';
297
+ active: readonly string[];
232
298
  };
233
299
  /**
234
300
  * A page-relative view memento: "what I'm looking at and how zoomed". The durable
@@ -437,6 +503,45 @@ interface StageCapability {
437
503
  /** `Element.scrollBy`: relative offsets — sugar over `scrollTo`. */
438
504
  scrollBy(opts: StageScrollToOptions): void;
439
505
  zoomAround(screenPt: Point, factor: number): void;
506
+ /**
507
+ * Bracket a continuous direct-manipulation gesture (touch pan / pinch).
508
+ * While a gesture is open the camera writes stay cheap and visually calm:
509
+ * `zoomAround` defers its zoom-intent PATCH (one per gesture instead of one
510
+ * per event), and the camera-rest detector holds un-rested — device
511
+ * snapping and settle-gated rendering wait for the END of the gesture, not
512
+ * for a 150 ms hesitation inside it. Re-entrant (nesting counts). Opening a
513
+ * gesture cancels any running tween or fling — that is how a finger
514
+ * "catches" a moving page.
515
+ *
516
+ * `elastic` opts the gesture into RUBBER-BAND overscroll: pans past the
517
+ * clamp stretch on the iOS resistance curve instead of stopping dead, and
518
+ * ending the gesture (or a fling reaching an edge) springs the camera home
519
+ * on a critically-damped curve. The clamp itself stays the untouched law of
520
+ * REST — elasticity is a transient the gesture is allowed to hold, never a
521
+ * state the camera can settle in. Touch contacts pass it; mouse drags and
522
+ * wheel pans stay rigid (the desktop convention). Default false.
523
+ */
524
+ beginGesture(options?: {
525
+ elastic?: boolean;
526
+ }): void;
527
+ endGesture(): void;
528
+ /**
529
+ * Momentum scroll: keep panning from a release velocity (screen px/s, the
530
+ * same sign convention as `panBy` deltas), decelerating on UIScrollView's
531
+ * curve. Every other camera verb — including `beginGesture` from the next
532
+ * touch — cancels it. No-op without host frames (SSR/tests without a
533
+ * scheduler jump nowhere: momentum is presentation, not state).
534
+ */
535
+ fling(velocityX: number, velocityY: number): void;
536
+ /** True while the camera is animating (navigation tween or fling) — the
537
+ * input layer reads it to tell a "catch" from a tap. */
538
+ cameraInMotion(): boolean;
539
+ /**
540
+ * The touch double-tap toggle: zoomed out → animate to ~2.5× the automatic
541
+ * fit around `screenPt`; already zoomed past that → animate back to the fit
542
+ * level, same focal point. Lands as a fixed zoom level either way.
543
+ */
544
+ doubleTapZoom(screenPt: Point): void;
440
545
  zoomIn(): void;
441
546
  zoomOut(): void;
442
547
  zoomTo(spec: ZoomSpec$1): void;
@@ -472,9 +577,26 @@ interface StageCapability {
472
577
  next(opts?: GoToOptions): void;
473
578
  /** Step backward by the navigation unit. */
474
579
  prev(opts?: GoToOptions): void;
580
+ /**
581
+ * This lens's identity — the stage plugin id it was registered under
582
+ * ('stage' for the main lens; a custom id per additional lens). The surface
583
+ * binding stamps it on every pointer sample (`PointerSample.source`) and
584
+ * lens-scoped interaction handlers register under it, so two stages on one
585
+ * document can never capture each other's input.
586
+ */
587
+ lensId(): string;
475
588
  /** Set any subset of settings at once — ONE anchor-preserving update. The way to
476
- * apply a customer preset: `update(myPreset)`. */
589
+ * apply a customer preset: `update(myPreset)`. Writes the responsive BASE:
590
+ * a matching rule's key wins until its rule stops matching. */
477
591
  update(patch: Partial<StageSettings>): void;
592
+ /** Replace the responsive rules (see {@link ResponsiveRule}); re-resolves
593
+ * immediately with the usual anchor-preserving reactions. */
594
+ setResponsive(rules: readonly ResponsiveRule[]): void;
595
+ /** Is the named responsive rule currently matching? Reactive: recomputed
596
+ * whenever the box crosses a rule boundary. */
597
+ matches(name: string): boolean;
598
+ /** Names of all currently-matching rules, in source order. */
599
+ activeRules(): readonly string[];
478
600
  setFlow(flow: FlowMode): void;
479
601
  setLayout(layout: LayoutKind): void;
480
602
  setSpread(spread: SpreadMode$1): void;
@@ -508,6 +630,12 @@ interface StageCapability {
508
630
  interface StageConfig extends Partial<StageSettings> {
509
631
  /** Override the host timing seam (tests/SSR). Defaults to browser rAF. */
510
632
  scheduler?: Scheduler;
633
+ /**
634
+ * Container queries for the settings bag (see {@link ResponsiveRule}).
635
+ * Defaults to `DEFAULT_RESPONSIVE` (compact containers get the thin phone
636
+ * gutter); pass `[]` to opt out entirely.
637
+ */
638
+ responsive?: readonly ResponsiveRule[];
511
639
  }
512
640
  declare const StageToken: import("@embedpdf/core").CapabilityToken<StageCapability>;
513
641
  //#endregion
@@ -531,20 +659,6 @@ declare const StageToken: import("@embedpdf/core").CapabilityToken<StageCapabili
531
659
  interface StagePluginOptions extends StageConfig {
532
660
  id?: string;
533
661
  token?: CapabilityToken<StageCapability>;
534
- /**
535
- * Opt this lens into the interaction hub: register a tool-gated `scroll`
536
- * handler (so dragging pans only in `pan` mode). Pair with `<Stage interaction>`
537
- * on the React side, which forwards pointer events to the hub. Default false —
538
- * secondary lenses (thumbnails) stay click-to-navigate.
539
- */
540
- interaction?: boolean;
541
- /**
542
- * When {@link interaction} is on, let drags over page GAPS pan regardless of the
543
- * active tool (and show a grab cursor there) — the gutter always pans, matching
544
- * v2 and the intuition that there's nothing to draw/select outside a page.
545
- * On-page behaviour is untouched. Default true; ignored without `interaction`.
546
- */
547
- panFallback?: boolean;
548
662
  }
549
663
  /**
550
664
  * Wires the parts into a kernel plugin. This file is the "manifest": it says what
@@ -567,37 +681,28 @@ interface DestinationReveal {
567
681
  */
568
682
  declare function destinationToReveal(dest: PdfDestination, layout: PageLayout, bbox?: Rect): DestinationReveal;
569
683
  //#endregion
570
- //#region src/wheel.d.ts
684
+ //#region src/scroll-handler.d.ts
685
+ interface ScrollHandlerOptions {
686
+ /**
687
+ * Let a drag over a page GAP pan regardless of the active tool — and show a grab
688
+ * cursor there — so the gutter always pans (v2 parity, and the natural feel:
689
+ * outside a page there's nothing to draw/select on). On-page behaviour is
690
+ * untouched; the tool's own gesture still owns page space. Default true.
691
+ */
692
+ panFallback?: boolean;
693
+ }
571
694
  /**
572
- * Wheel-input classification for ambient zoom — pure, so every framework
573
- * adapter shares one feel and the mapping is unit-testable.
574
- *
575
- * A ctrl/cmd wheel event is one of three physically different inputs that
576
- * happen to share an event type, each with its own unit system:
577
- *
578
- * • trackpad PINCH — browsers (Chrome/Edge/Firefox) synthesize ctrl+wheel
579
- * px-deltas scaled to the de-facto convention that `exp(-Δ/100)` tracks
580
- * the physical finger scale 1:1. Anything weaker feels dead (the content
581
- * must track the fingers — the Figma/maps feel).
582
- * • mouse NOTCHES — coarse steps: ±100/±120 px in Chrome, lines/pages mode
583
- * in Firefox. The pinch mapping would triple per click; a notch instead
584
- * steps by 1.2, the same ratio as the zoom buttons.
585
- * • cmd + continuous SCROLL — real scroll px, big and streaming (with
586
- * momentum): a gentle scrub coefficient. A cmd+mouse notch (±120 px)
587
- * lands within 0.3% of the 1.2 button step, so it needs no special case.
695
+ * Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
696
+ * (the `scroll` tag) and — with `panFallback` — under every tool, where it then
697
+ * captures ONLY page-gap drags (on-page downs belong to the active tool, which
698
+ * claims them first at higher priority). It uses the sample's viewport delta, so
699
+ * it pans over the whole viewport (pages and gaps).
588
700
  *
589
- * (Safari never synthesizes ctrl+wheel for pinch — it fires proprietary
590
- * gesture events with an absolute scale; that wiring lives in the adapters.)
701
+ * Cursor feel: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
702
+ * hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
703
+ * from its `Tool.cursor`, so hovering it needs no claim here.
591
704
  */
592
- interface WheelSample {
593
- deltaY: number;
594
- /** WheelEvent.deltaMode: 0 = pixels, 1 = lines, 2 = pages. */
595
- deltaMode: number;
596
- ctrlKey: boolean;
597
- metaKey: boolean;
598
- }
599
- /** Zoom factor for a ctrl/cmd wheel event (the caller decides zoom vs pan). */
600
- declare function wheelZoomFactor(e: WheelSample): number;
705
+ declare function createScrollHandler(stage: StageCapability, interaction: InteractionCapability, options?: ScrollHandlerOptions): InteractionHandler;
601
706
  //#endregion
602
707
  //#region src/settings.d.ts
603
708
  /**
@@ -608,9 +713,41 @@ declare function wheelZoomFactor(e: WheelSample): number;
608
713
  * `update()`, so that taxonomy stays a customer concern.
609
714
  */
610
715
  declare const DEFAULT_SETTINGS: StageSettings;
716
+ /**
717
+ * Out-of-the-box responsive rules — the platform feel with zero configuration:
718
+ * a compact container gets the thin gutter phones use (space, not device — an
719
+ * embedded 500px pane on a desktop is compact too). Override the whole list
720
+ * with `stagePlugin({ responsive: [...] })`; `responsive: []` opts out. The
721
+ * 'compact' name is queryable app-side via `stage.matches('compact')`.
722
+ */
723
+ declare const DEFAULT_RESPONSIVE: readonly ResponsiveRule[];
611
724
  /** Field-by-field settings equality, derived from the registry — a new setting is
612
725
  * covered automatically. (The React `useStageSettings` selector equality.) */
613
726
  declare const settingsEqual: (a: StageSettings, b: StageSettings) => boolean;
614
727
  //#endregion
615
- export { type Align, type Alignment, DEFAULT_SETTINGS, type DestinationReveal, type Direction, type FlowMode, type Gap, type GoToOptions, type GridColumns, type LayoutKind, type PageFrame, type RevealAnchor, type RevealAnchorValue, type RevealOptions, type RevealZoom, type Scheduler, type ScrollBehaviorKind, type ScrollMetrics, type SizingMode, type SpreadMode, type StageAction, type StageCapability, type StageConfig, type StagePluginOptions, type StageScrollToOptions, type StageSettings, type StageState, StageToken, type StageViewState, type Viewpoint, type VisiblePage, type WheelSample, ZoomMode, type ZoomModeValue, type ZoomSpec, destinationToReveal, settingsEqual, stagePlugin, wheelZoomFactor };
728
+ //#region src/responsive.d.ts
729
+ /**
730
+ * Container queries for the settings bag — the pure half. The capability owns
731
+ * the driver (when to re-resolve, how to react); the LAWS live here: what a
732
+ * box is, when a query matches, and what the effective settings are. No state,
733
+ * no dispatch — property-testable in isolation (the `wheel.ts` convention).
734
+ *
735
+ * The query vocabulary is deliberately just the box: a headless, DOM-free
736
+ * stage knows exactly one environmental fact — the viewport it was told.
737
+ * Anything else (pointer coarseness, platform, app state) is either per-event
738
+ * (`PointerSample.pointerType`) or the app's own business via `update()`.
739
+ */
740
+ /** The reported viewport as a queryable box. A square box is 'portrait' (CSS). */
741
+ declare const boxOf: (vp: Size) => StageBox;
742
+ /** All bounds inclusive, all fields optional, conditions AND-ed (CSS ranges). */
743
+ declare const matchesQuery: (q: BoxQuery, box: StageBox) => boolean;
744
+ interface ResolvedResponsive {
745
+ /** base ⊕ every matching rule's patch, in source order (later wins per key). */
746
+ effective: StageSettings;
747
+ /** Names of the matching rules, in source order. */
748
+ active: string[];
749
+ }
750
+ declare const resolveResponsive: (base: StageSettings, rules: readonly ResponsiveRule[], box: StageBox) => ResolvedResponsive;
751
+ //#endregion
752
+ export { type Align, type Alignment, type BoxQuery, DEFAULT_RESPONSIVE, DEFAULT_SETTINGS, type DestinationReveal, type Direction, type FlowMode, type Gap, type GoToOptions, type GridColumns, type LayoutKind, type PageFrame, type ResponsiveRule, type RevealAnchor, type RevealAnchorValue, type RevealOptions, type RevealZoom, type Scheduler, type ScrollBehaviorKind, type ScrollHandlerOptions, type ScrollMetrics, type SizingMode, type SpreadMode, type StageAction, type StageBox, type StageCapability, type StageConfig, type StagePluginOptions, type StageScrollToOptions, type StageSettings, type StageState, StageToken, type StageViewState, type Viewpoint, type VisiblePage, ZoomMode, type ZoomModeValue, type ZoomSpec, boxOf, createScrollHandler, destinationToReveal, matchesQuery, resolveResponsive, settingsEqual, stagePlugin };
616
753
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CapabilityToken, PageLayout, PageObjectNumber, PdfDestination } from "@embedpdf/core";
2
2
  import { Align, AlignValue, Alignment, Alignment as Alignment$1, AlignmentValue, Anchor, Camera, Direction, Direction as Direction$1, PageBox, PageFrame, PageFrame as PageFrame$1, Point, ScrollMetrics, ScrollMetrics as ScrollMetrics$1, Size, SizingMode, SizingMode as SizingMode$1, SpreadMode, SpreadMode as SpreadMode$1, ZoomMode, ZoomModeValue, ZoomModeValue as ZoomModeValue$1, ZoomSpec, ZoomSpec as ZoomSpec$1 } from "@embedpdf/core-stage";
3
3
  import { PageRotation, PageTransform, Rect } from "@embedpdf/core-geometry";
4
+ import { InteractionCapability, InteractionHandler } from "@embedpdf/plugin-interaction";
4
5
  //#region src/types.d.ts
5
6
  type LayoutKind = 'vertical' | 'horizontal' | 'grid';
6
7
  /** Navigation (goToPage) either tweens the camera or jumps instantly. */
@@ -30,6 +31,53 @@ type GridColumns = 'square' | 'auto' | number;
30
31
  type Gap = number | {
31
32
  px: number;
32
33
  };
34
+ /**
35
+ * The one environmental fact a headless stage has: the box it was told about
36
+ * (`setViewport`). Responsive rules can query nothing else — no user agent, no
37
+ * pointer type (modality is per-event, on `PointerSample`), no window. Space,
38
+ * not device: a narrow pane on a desktop is compact too, and each stage
39
+ * instance resolves against ITS OWN box.
40
+ */
41
+ interface StageBox {
42
+ width: number;
43
+ height: number;
44
+ /** Of the CONTAINER, not the device. A square box is 'portrait' (the CSS rule). */
45
+ orientation: 'portrait' | 'landscape';
46
+ }
47
+ /** Declarative box query — all bounds inclusive, all fields optional (AND-ed). */
48
+ interface BoxQuery {
49
+ minWidth?: number;
50
+ maxWidth?: number;
51
+ minHeight?: number;
52
+ maxHeight?: number;
53
+ orientation?: 'portrait' | 'landscape';
54
+ }
55
+ /**
56
+ * One `@container` block for the settings bag: when the box matches, assert
57
+ * this settings patch. Rules evaluate in source order and ALL matching rules
58
+ * apply, later winning per key (each key replaced whole — no deep merges).
59
+ * Effective settings = base (config + runtime setters) ⊕ matching patches.
60
+ *
61
+ * Semantics, stated honestly:
62
+ * • Runtime setters write the BASE; a matching rule wins over it. Apps
63
+ * needing situational absolute control edit the rules (`setResponsive`).
64
+ * • Rules assert at TRANSITIONS (box/base/rules changes), not continuously —
65
+ * between crossings, interaction owns the state. A rule containing `zoom`
66
+ * re-fits when the box crosses it (the rotate-an-iPad behavior) and then
67
+ * leaves the pinch alone.
68
+ *
69
+ * A rule with a `name` is a queryable fact (`matches(name)`), reactive in
70
+ * every framework; a named rule with NO settings is a pure shared breakpoint
71
+ * the app chrome can key its own presentation off — one definition serving
72
+ * both the layout math and the UI.
73
+ */
74
+ interface ResponsiveRule {
75
+ name?: string;
76
+ /** Declarative box query, or a predicate for anything the box can answer. */
77
+ when: BoxQuery | ((box: StageBox) => boolean);
78
+ /** The settings this situation asserts. Omit for a pure named query. */
79
+ settings?: Partial<StageSettings>;
80
+ }
33
81
  /**
34
82
  * One axis of the ARRIVAL policy — stage-core's AlignValue ('start' |
35
83
  * 'center' | 'end' | viewport fraction 0–1) plus one navigation-only word:
@@ -186,6 +234,19 @@ interface VisiblePage extends PageBox {
186
234
  }
187
235
  interface StageState extends StageSettings {
188
236
  camera: Camera;
237
+ /**
238
+ * One-way render-commit latch. False while the initial viewport-driven
239
+ * placement is unresolved; true only after its camera/settings writes have
240
+ * completed. Page/scroll screen-space selectors refuse to answer while
241
+ * false, so an adapter can never paint the placeholder camera at the origin.
242
+ */
243
+ placed: boolean;
244
+ /**
245
+ * Names of the responsive rules currently matching the box, in source order.
246
+ * State (not derived) so `matches()` is reactive through the ordinary
247
+ * selector machinery in every framework.
248
+ */
249
+ activeRules: readonly string[];
189
250
  /**
190
251
  * False while the ZOOM is in motion, true once it has rested (~150ms of
191
252
  * frames without a zoom write). Device-snapping of page origins is gated
@@ -217,6 +278,8 @@ type StageAction = {
217
278
  } | {
218
279
  type: 'CAMERA_REST';
219
280
  resting: boolean;
281
+ } | {
282
+ type: 'PLACED';
220
283
  } | {
221
284
  type: 'VP';
222
285
  vp: Size;
@@ -229,6 +292,9 @@ type StageAction = {
229
292
  } | {
230
293
  type: 'PATCH';
231
294
  patch: Partial<StageSettings>;
295
+ } | {
296
+ type: 'RESPONSIVE';
297
+ active: readonly string[];
232
298
  };
233
299
  /**
234
300
  * A page-relative view memento: "what I'm looking at and how zoomed". The durable
@@ -437,6 +503,45 @@ interface StageCapability {
437
503
  /** `Element.scrollBy`: relative offsets — sugar over `scrollTo`. */
438
504
  scrollBy(opts: StageScrollToOptions): void;
439
505
  zoomAround(screenPt: Point, factor: number): void;
506
+ /**
507
+ * Bracket a continuous direct-manipulation gesture (touch pan / pinch).
508
+ * While a gesture is open the camera writes stay cheap and visually calm:
509
+ * `zoomAround` defers its zoom-intent PATCH (one per gesture instead of one
510
+ * per event), and the camera-rest detector holds un-rested — device
511
+ * snapping and settle-gated rendering wait for the END of the gesture, not
512
+ * for a 150 ms hesitation inside it. Re-entrant (nesting counts). Opening a
513
+ * gesture cancels any running tween or fling — that is how a finger
514
+ * "catches" a moving page.
515
+ *
516
+ * `elastic` opts the gesture into RUBBER-BAND overscroll: pans past the
517
+ * clamp stretch on the iOS resistance curve instead of stopping dead, and
518
+ * ending the gesture (or a fling reaching an edge) springs the camera home
519
+ * on a critically-damped curve. The clamp itself stays the untouched law of
520
+ * REST — elasticity is a transient the gesture is allowed to hold, never a
521
+ * state the camera can settle in. Touch contacts pass it; mouse drags and
522
+ * wheel pans stay rigid (the desktop convention). Default false.
523
+ */
524
+ beginGesture(options?: {
525
+ elastic?: boolean;
526
+ }): void;
527
+ endGesture(): void;
528
+ /**
529
+ * Momentum scroll: keep panning from a release velocity (screen px/s, the
530
+ * same sign convention as `panBy` deltas), decelerating on UIScrollView's
531
+ * curve. Every other camera verb — including `beginGesture` from the next
532
+ * touch — cancels it. No-op without host frames (SSR/tests without a
533
+ * scheduler jump nowhere: momentum is presentation, not state).
534
+ */
535
+ fling(velocityX: number, velocityY: number): void;
536
+ /** True while the camera is animating (navigation tween or fling) — the
537
+ * input layer reads it to tell a "catch" from a tap. */
538
+ cameraInMotion(): boolean;
539
+ /**
540
+ * The touch double-tap toggle: zoomed out → animate to ~2.5× the automatic
541
+ * fit around `screenPt`; already zoomed past that → animate back to the fit
542
+ * level, same focal point. Lands as a fixed zoom level either way.
543
+ */
544
+ doubleTapZoom(screenPt: Point): void;
440
545
  zoomIn(): void;
441
546
  zoomOut(): void;
442
547
  zoomTo(spec: ZoomSpec$1): void;
@@ -472,9 +577,26 @@ interface StageCapability {
472
577
  next(opts?: GoToOptions): void;
473
578
  /** Step backward by the navigation unit. */
474
579
  prev(opts?: GoToOptions): void;
580
+ /**
581
+ * This lens's identity — the stage plugin id it was registered under
582
+ * ('stage' for the main lens; a custom id per additional lens). The surface
583
+ * binding stamps it on every pointer sample (`PointerSample.source`) and
584
+ * lens-scoped interaction handlers register under it, so two stages on one
585
+ * document can never capture each other's input.
586
+ */
587
+ lensId(): string;
475
588
  /** Set any subset of settings at once — ONE anchor-preserving update. The way to
476
- * apply a customer preset: `update(myPreset)`. */
589
+ * apply a customer preset: `update(myPreset)`. Writes the responsive BASE:
590
+ * a matching rule's key wins until its rule stops matching. */
477
591
  update(patch: Partial<StageSettings>): void;
592
+ /** Replace the responsive rules (see {@link ResponsiveRule}); re-resolves
593
+ * immediately with the usual anchor-preserving reactions. */
594
+ setResponsive(rules: readonly ResponsiveRule[]): void;
595
+ /** Is the named responsive rule currently matching? Reactive: recomputed
596
+ * whenever the box crosses a rule boundary. */
597
+ matches(name: string): boolean;
598
+ /** Names of all currently-matching rules, in source order. */
599
+ activeRules(): readonly string[];
478
600
  setFlow(flow: FlowMode): void;
479
601
  setLayout(layout: LayoutKind): void;
480
602
  setSpread(spread: SpreadMode$1): void;
@@ -508,6 +630,12 @@ interface StageCapability {
508
630
  interface StageConfig extends Partial<StageSettings> {
509
631
  /** Override the host timing seam (tests/SSR). Defaults to browser rAF. */
510
632
  scheduler?: Scheduler;
633
+ /**
634
+ * Container queries for the settings bag (see {@link ResponsiveRule}).
635
+ * Defaults to `DEFAULT_RESPONSIVE` (compact containers get the thin phone
636
+ * gutter); pass `[]` to opt out entirely.
637
+ */
638
+ responsive?: readonly ResponsiveRule[];
511
639
  }
512
640
  declare const StageToken: import("@embedpdf/core").CapabilityToken<StageCapability>;
513
641
  //#endregion
@@ -531,20 +659,6 @@ declare const StageToken: import("@embedpdf/core").CapabilityToken<StageCapabili
531
659
  interface StagePluginOptions extends StageConfig {
532
660
  id?: string;
533
661
  token?: CapabilityToken<StageCapability>;
534
- /**
535
- * Opt this lens into the interaction hub: register a tool-gated `scroll`
536
- * handler (so dragging pans only in `pan` mode). Pair with `<Stage interaction>`
537
- * on the React side, which forwards pointer events to the hub. Default false —
538
- * secondary lenses (thumbnails) stay click-to-navigate.
539
- */
540
- interaction?: boolean;
541
- /**
542
- * When {@link interaction} is on, let drags over page GAPS pan regardless of the
543
- * active tool (and show a grab cursor there) — the gutter always pans, matching
544
- * v2 and the intuition that there's nothing to draw/select outside a page.
545
- * On-page behaviour is untouched. Default true; ignored without `interaction`.
546
- */
547
- panFallback?: boolean;
548
662
  }
549
663
  /**
550
664
  * Wires the parts into a kernel plugin. This file is the "manifest": it says what
@@ -567,37 +681,28 @@ interface DestinationReveal {
567
681
  */
568
682
  declare function destinationToReveal(dest: PdfDestination, layout: PageLayout, bbox?: Rect): DestinationReveal;
569
683
  //#endregion
570
- //#region src/wheel.d.ts
684
+ //#region src/scroll-handler.d.ts
685
+ interface ScrollHandlerOptions {
686
+ /**
687
+ * Let a drag over a page GAP pan regardless of the active tool — and show a grab
688
+ * cursor there — so the gutter always pans (v2 parity, and the natural feel:
689
+ * outside a page there's nothing to draw/select on). On-page behaviour is
690
+ * untouched; the tool's own gesture still owns page space. Default true.
691
+ */
692
+ panFallback?: boolean;
693
+ }
571
694
  /**
572
- * Wheel-input classification for ambient zoom — pure, so every framework
573
- * adapter shares one feel and the mapping is unit-testable.
574
- *
575
- * A ctrl/cmd wheel event is one of three physically different inputs that
576
- * happen to share an event type, each with its own unit system:
577
- *
578
- * • trackpad PINCH — browsers (Chrome/Edge/Firefox) synthesize ctrl+wheel
579
- * px-deltas scaled to the de-facto convention that `exp(-Δ/100)` tracks
580
- * the physical finger scale 1:1. Anything weaker feels dead (the content
581
- * must track the fingers — the Figma/maps feel).
582
- * • mouse NOTCHES — coarse steps: ±100/±120 px in Chrome, lines/pages mode
583
- * in Firefox. The pinch mapping would triple per click; a notch instead
584
- * steps by 1.2, the same ratio as the zoom buttons.
585
- * • cmd + continuous SCROLL — real scroll px, big and streaming (with
586
- * momentum): a gentle scrub coefficient. A cmd+mouse notch (±120 px)
587
- * lands within 0.3% of the 1.2 button step, so it needs no special case.
695
+ * Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
696
+ * (the `scroll` tag) and — with `panFallback` — under every tool, where it then
697
+ * captures ONLY page-gap drags (on-page downs belong to the active tool, which
698
+ * claims them first at higher priority). It uses the sample's viewport delta, so
699
+ * it pans over the whole viewport (pages and gaps).
588
700
  *
589
- * (Safari never synthesizes ctrl+wheel for pinch — it fires proprietary
590
- * gesture events with an absolute scale; that wiring lives in the adapters.)
701
+ * Cursor feel: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
702
+ * hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
703
+ * from its `Tool.cursor`, so hovering it needs no claim here.
591
704
  */
592
- interface WheelSample {
593
- deltaY: number;
594
- /** WheelEvent.deltaMode: 0 = pixels, 1 = lines, 2 = pages. */
595
- deltaMode: number;
596
- ctrlKey: boolean;
597
- metaKey: boolean;
598
- }
599
- /** Zoom factor for a ctrl/cmd wheel event (the caller decides zoom vs pan). */
600
- declare function wheelZoomFactor(e: WheelSample): number;
705
+ declare function createScrollHandler(stage: StageCapability, interaction: InteractionCapability, options?: ScrollHandlerOptions): InteractionHandler;
601
706
  //#endregion
602
707
  //#region src/settings.d.ts
603
708
  /**
@@ -608,9 +713,41 @@ declare function wheelZoomFactor(e: WheelSample): number;
608
713
  * `update()`, so that taxonomy stays a customer concern.
609
714
  */
610
715
  declare const DEFAULT_SETTINGS: StageSettings;
716
+ /**
717
+ * Out-of-the-box responsive rules — the platform feel with zero configuration:
718
+ * a compact container gets the thin gutter phones use (space, not device — an
719
+ * embedded 500px pane on a desktop is compact too). Override the whole list
720
+ * with `stagePlugin({ responsive: [...] })`; `responsive: []` opts out. The
721
+ * 'compact' name is queryable app-side via `stage.matches('compact')`.
722
+ */
723
+ declare const DEFAULT_RESPONSIVE: readonly ResponsiveRule[];
611
724
  /** Field-by-field settings equality, derived from the registry — a new setting is
612
725
  * covered automatically. (The React `useStageSettings` selector equality.) */
613
726
  declare const settingsEqual: (a: StageSettings, b: StageSettings) => boolean;
614
727
  //#endregion
615
- export { type Align, type Alignment, DEFAULT_SETTINGS, type DestinationReveal, type Direction, type FlowMode, type Gap, type GoToOptions, type GridColumns, type LayoutKind, type PageFrame, type RevealAnchor, type RevealAnchorValue, type RevealOptions, type RevealZoom, type Scheduler, type ScrollBehaviorKind, type ScrollMetrics, type SizingMode, type SpreadMode, type StageAction, type StageCapability, type StageConfig, type StagePluginOptions, type StageScrollToOptions, type StageSettings, type StageState, StageToken, type StageViewState, type Viewpoint, type VisiblePage, type WheelSample, ZoomMode, type ZoomModeValue, type ZoomSpec, destinationToReveal, settingsEqual, stagePlugin, wheelZoomFactor };
728
+ //#region src/responsive.d.ts
729
+ /**
730
+ * Container queries for the settings bag — the pure half. The capability owns
731
+ * the driver (when to re-resolve, how to react); the LAWS live here: what a
732
+ * box is, when a query matches, and what the effective settings are. No state,
733
+ * no dispatch — property-testable in isolation (the `wheel.ts` convention).
734
+ *
735
+ * The query vocabulary is deliberately just the box: a headless, DOM-free
736
+ * stage knows exactly one environmental fact — the viewport it was told.
737
+ * Anything else (pointer coarseness, platform, app state) is either per-event
738
+ * (`PointerSample.pointerType`) or the app's own business via `update()`.
739
+ */
740
+ /** The reported viewport as a queryable box. A square box is 'portrait' (CSS). */
741
+ declare const boxOf: (vp: Size) => StageBox;
742
+ /** All bounds inclusive, all fields optional, conditions AND-ed (CSS ranges). */
743
+ declare const matchesQuery: (q: BoxQuery, box: StageBox) => boolean;
744
+ interface ResolvedResponsive {
745
+ /** base ⊕ every matching rule's patch, in source order (later wins per key). */
746
+ effective: StageSettings;
747
+ /** Names of the matching rules, in source order. */
748
+ active: string[];
749
+ }
750
+ declare const resolveResponsive: (base: StageSettings, rules: readonly ResponsiveRule[], box: StageBox) => ResolvedResponsive;
751
+ //#endregion
752
+ export { type Align, type Alignment, type BoxQuery, DEFAULT_RESPONSIVE, DEFAULT_SETTINGS, type DestinationReveal, type Direction, type FlowMode, type Gap, type GoToOptions, type GridColumns, type LayoutKind, type PageFrame, type ResponsiveRule, type RevealAnchor, type RevealAnchorValue, type RevealOptions, type RevealZoom, type Scheduler, type ScrollBehaviorKind, type ScrollHandlerOptions, type ScrollMetrics, type SizingMode, type SpreadMode, type StageAction, type StageBox, type StageCapability, type StageConfig, type StagePluginOptions, type StageScrollToOptions, type StageSettings, type StageState, StageToken, type StageViewState, type Viewpoint, type VisiblePage, ZoomMode, type ZoomModeValue, type ZoomSpec, boxOf, createScrollHandler, destinationToReveal, matchesQuery, resolveResponsive, settingsEqual, stagePlugin };
616
753
  //# sourceMappingURL=index.d.ts.map