@michaelyagi/shoji 0.1.0-alpha.10

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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/esm/core/EventBus.d.ts +16 -0
  4. package/dist/esm/core/FocusTrap.d.ts +9 -0
  5. package/dist/esm/core/Gallery.d.ts +221 -0
  6. package/dist/esm/core/GestureController.d.ts +57 -0
  7. package/dist/esm/core/LiveRegion.d.ts +6 -0
  8. package/dist/esm/core/SlideManager.d.ts +85 -0
  9. package/dist/esm/core/bodyScrollLock.d.ts +2 -0
  10. package/dist/esm/core/dom.d.ts +27 -0
  11. package/dist/esm/core/icons.d.ts +6 -0
  12. package/dist/esm/core/index.d.ts +5 -0
  13. package/dist/esm/core/index.js +2105 -0
  14. package/dist/esm/core/index.js.map +1 -0
  15. package/dist/esm/core/plugin.d.ts +54 -0
  16. package/dist/esm/core/rotateFlipNormalize.d.ts +19 -0
  17. package/dist/esm/core/scan.d.ts +19 -0
  18. package/dist/esm/core/types.d.ts +290 -0
  19. package/dist/esm/core/zoomTransition.d.ts +33 -0
  20. package/dist/esm/gestures/GestureEngine.d.ts +78 -0
  21. package/dist/esm/index.css +540 -0
  22. package/dist/esm/index.d.ts +32 -0
  23. package/dist/esm/index.js +24 -0
  24. package/dist/esm/index.js.map +1 -0
  25. package/dist/esm/index2.css +22 -0
  26. package/dist/esm/index3.css +170 -0
  27. package/dist/esm/index4.css +27 -0
  28. package/dist/esm/plugins/activeThumbnail/index.d.ts +26 -0
  29. package/dist/esm/plugins/activeThumbnail/index.js +63 -0
  30. package/dist/esm/plugins/activeThumbnail/index.js.map +1 -0
  31. package/dist/esm/plugins/autoplay/icons.d.ts +3 -0
  32. package/dist/esm/plugins/autoplay/index.d.ts +19 -0
  33. package/dist/esm/plugins/autoplay/index.js +207 -0
  34. package/dist/esm/plugins/autoplay/index.js.map +1 -0
  35. package/dist/esm/plugins/fullscreen/icons.d.ts +3 -0
  36. package/dist/esm/plugins/fullscreen/index.d.ts +17 -0
  37. package/dist/esm/plugins/fullscreen/index.js +74 -0
  38. package/dist/esm/plugins/fullscreen/index.js.map +1 -0
  39. package/dist/esm/plugins/layout/index.d.ts +191 -0
  40. package/dist/esm/plugins/layout/index.js +752 -0
  41. package/dist/esm/plugins/layout/index.js.map +1 -0
  42. package/dist/esm/plugins/layout/justified.d.ts +68 -0
  43. package/dist/esm/plugins/layout/masonry.d.ts +92 -0
  44. package/dist/esm/plugins/rotateFlip/icons.d.ts +5 -0
  45. package/dist/esm/plugins/rotateFlip/index.d.ts +17 -0
  46. package/dist/esm/plugins/rotateFlip/index.js +138 -0
  47. package/dist/esm/plugins/rotateFlip/index.js.map +1 -0
  48. package/dist/esm/plugins/video/index.d.ts +13 -0
  49. package/dist/esm/plugins/video/index.js +207 -0
  50. package/dist/esm/plugins/video/index.js.map +1 -0
  51. package/dist/esm/plugins/video/vimeo.d.ts +43 -0
  52. package/dist/esm/plugins/video/youtube.d.ts +61 -0
  53. package/dist/esm/plugins/zoom/icons.d.ts +4 -0
  54. package/dist/esm/plugins/zoom/index.d.ts +30 -0
  55. package/dist/esm/plugins/zoom/index.js +274 -0
  56. package/dist/esm/plugins/zoom/index.js.map +1 -0
  57. package/dist/esm/plugins/zoom/zoomMath.d.ts +24 -0
  58. package/dist/esm/transitions/SlideTransition.d.ts +25 -0
  59. package/dist/esm/transitions/presets.d.ts +21 -0
  60. package/dist/esm/zoomTransition-bbKHpVpA.js +110 -0
  61. package/dist/esm/zoomTransition-bbKHpVpA.js.map +1 -0
  62. package/dist/shoji.css +759 -0
  63. package/dist/shoji.js +3907 -0
  64. package/dist/shoji.js.map +1 -0
  65. package/dist/shoji.min.css +1 -0
  66. package/dist/shoji.min.js +2 -0
  67. package/dist/shoji.min.js.map +1 -0
  68. package/package.json +77 -0
@@ -0,0 +1,290 @@
1
+ import { GestureEngineOptions } from '../gestures/GestureEngine';
2
+ import { ShojiPlugin } from './plugin';
3
+ /** See DESIGN.md §2.1 — the full item model grows plugin-by-plugin (editor, ...). */
4
+ export interface GalleryItem {
5
+ id?: string;
6
+ src: string;
7
+ srcset?: string;
8
+ sizes?: string;
9
+ sources?: MediaSource[];
10
+ thumb?: string;
11
+ poster?: string;
12
+ video?: VideoDescriptor;
13
+ width?: number;
14
+ height?: number;
15
+ alt?: string;
16
+ caption?: string | HTMLElement | DangerousHtmlCaption;
17
+ download?: string | false;
18
+ /** Free-form payload, untouched by core beyond one thing: in selector mode, `scan.ts` populates this from any `data-shoji-*` attribute that isn't already one of the named fields above (e.g. `data-shoji-metadata-id="123"` → `data.{'metadata-id': '123'}`) — dynamic-mode items only ever get whatever the host puts here directly. */
19
+ data?: Record<string, unknown>;
20
+ }
21
+ /**
22
+ * Dynamic-mode-only input shape, widening `video` to also accept `true` —
23
+ * shorthand for "figure it out from `src`," mirroring a bare
24
+ * `data-shoji-video="<url>"` attribute with no explicit provider/id. Always
25
+ * resolved into a real `VideoDescriptor` (or left `undefined`) before an
26
+ * item ever reaches `gallery.items`/rendering — `scan.ts`'s
27
+ * `resolveDynamicVideoItems`, run by `applyOptions()`/`updateSlides()`. Not
28
+ * usable in selector mode: DOM attributes have no boolean shorthand of their
29
+ * own, and `scanContainer()` always returns real `GalleryItem`s already
30
+ * resolved to a concrete descriptor (see `data-shoji-video`/
31
+ * `data-shoji-video-id` in `scan.ts`).
32
+ */
33
+ export type GalleryItemInput = Omit<GalleryItem, 'video'> & {
34
+ video?: VideoDescriptor | true;
35
+ };
36
+ /**
37
+ * Explicit, self-documenting opt-in for raw HTML in a caption — named after
38
+ * React's identical escape hatch on purpose, so it can't be triggered by
39
+ * accident and anyone who's seen that convention knows what it means. Shoji
40
+ * does not sanitize this string; the host must, if it isn't already trusted.
41
+ */
42
+ export interface DangerousHtmlCaption {
43
+ dangerouslySetInnerHTML: string;
44
+ }
45
+ /** Mirrors a `<source src type>` child. */
46
+ export interface MediaSource {
47
+ src: string;
48
+ type: string;
49
+ }
50
+ /**
51
+ * `'youtube'`/`'vimeo'` are detected by core's DOM scanning too (§2.1) —
52
+ * cheap, dependency-free URL parsing, not the provider's SDK. *Rendering*
53
+ * any non-`'html5'` provider is a plugin's job (§4-video), via
54
+ * `ctx.ui.registerVideoProvider()` or `'custom'`'s own `render`.
55
+ *
56
+ * `id` is optional on the youtube/vimeo variant so dynamic-mode hosts can
57
+ * write `video: { provider: 'youtube' }` and let `id` be filled in from
58
+ * `src` (`scan.ts`'s `resolveDynamicVideoItems`) — same reasoning as
59
+ * `video: true` above, just with the provider spelled out explicitly. A
60
+ * rendered item with no `id` (src wasn't a recognized URL either) shows a
61
+ * placeholder instead of a broken embed — see `youtube.ts`/`vimeo.ts`.
62
+ */
63
+ export type VideoDescriptor = {
64
+ provider: 'html5';
65
+ } | {
66
+ provider: 'youtube' | 'vimeo';
67
+ id?: string;
68
+ url?: string;
69
+ } | {
70
+ provider: 'custom';
71
+ /** Same contract as `plugin.ts`'s `VideoProviderRenderer` — see its doc comment. */
72
+ render: (el: HTMLElement, item: GalleryItem, onReady: () => void, signal: AbortSignal) => void;
73
+ };
74
+ export interface GalleryOptions {
75
+ /** DOM selector for gallery items when not using dynamic mode. */
76
+ selector?: string;
77
+ /** Dynamic-mode item list; mutate live via gallery.updateSlides(). */
78
+ items?: GalleryItemInput[];
79
+ /** Index to open on — used both as `open()`'s default argument and, with `openOnInit`, as the index opened automatically at construction. */
80
+ index?: number;
81
+ /**
82
+ * Opens the lightbox immediately at construction (to `index`, default 0),
83
+ * instead of the normal default of staying closed until the host calls
84
+ * `open()` or a scanned/layout-rendered thumbnail is clicked. Default
85
+ * `false`. Useful for hash/deep-link-style integrations that should land
86
+ * straight in the lightbox; everyone else should leave this off — a
87
+ * gallery that pops open on page load without a click is rarely what's
88
+ * wanted.
89
+ */
90
+ openOnInit?: boolean;
91
+ /** Flat locale key map — see CLAUDE.md: no hardcoded English in UI. */
92
+ locale?: Record<string, string>;
93
+ /** Registered plugins; each plugin's own options nest under options[plugin.name]. */
94
+ plugins?: ShojiPlugin[];
95
+ /**
96
+ * DESIGN.md §2.8 — ms of inactivity before all buttons/overlays (toolbar,
97
+ * prev/next, counter, caption) auto-hide; default 5000. `0` is a distinct
98
+ * mode, not "disabled": controls never show at all, regardless of activity.
99
+ */
100
+ autoHideDelay?: number;
101
+ /**
102
+ * DESIGN.md §2.3 — how many slides on each side of the active one are
103
+ * kept mounted *and* proactively decoded ahead of time; default 1.
104
+ * Navigating to any index within that window shows instantly, no loading
105
+ * spinner — its content was already decoded and cached (by item index,
106
+ * surviving the pool's internal slot reshuffling) before you got there.
107
+ * Navigating further than `preload` away (a fast flick through several
108
+ * slides, or a direct `goTo()`/deep link) still shows the spinner while
109
+ * that one decodes, same as ever — this only changes what counts as
110
+ * "already ready."
111
+ */
112
+ preload?: number;
113
+ /** Shows the "N / M" counter badge; default true. Purely visual — the live-region announcement (§2.6) always includes position regardless. */
114
+ counter?: boolean;
115
+ /** DESIGN.md §2.3a — starts a video slide's caption shown instead of hidden; default `false`. No effect on a photo's caption. */
116
+ showVideoCaption?: boolean;
117
+ /**
118
+ * DESIGN.md §2.2 — `next()`/`prev()` (and the arrow-key/toolbar-button paths
119
+ * that call them) wrap past the last/first item instead of stopping; default
120
+ * true. Does not affect `goTo(index)` with an explicit out-of-range index
121
+ * (still clamps — a directed jump, not a step) or `Home`/`End` (still an
122
+ * absolute jump to the first/last item).
123
+ */
124
+ loop?: boolean;
125
+ /**
126
+ * Default `true`. `false` disables every viewer-facing way to close the
127
+ * lightbox: the close button (hidden entirely, not just inert), clicking
128
+ * the backdrop, `Escape`, and vertical swipe-to-close (no live drag
129
+ * feedback either, not just a release that fails to close). For
130
+ * non-closable embeds — a gallery meant to stay open permanently once
131
+ * shown. `gallery.close()` itself is unaffected — this only gates the
132
+ * four built-in viewer-triggered paths, host code can still close it
133
+ * programmatically at any time.
134
+ */
135
+ closable?: boolean;
136
+ /**
137
+ * DESIGN.md §2.4 — overrides the gesture engine's `lockThreshold`/
138
+ * `swipeThreshold`/`swipeVelocity` (defaults: 10px, 50px, 0.3px/ms).
139
+ * Partial: any field left out keeps its default. Momentum easing is not
140
+ * configurable here — it's the CSS custom property
141
+ * `--shoji-momentum-easing`, per CLAUDE.md's "no hardcoded sizes/timings
142
+ * in JS, everything through --shoji-* custom properties."
143
+ */
144
+ gestures?: Partial<GestureEngineOptions>;
145
+ /**
146
+ * DESIGN.md §2.5 — the slide-to-slide transition for programmatic
147
+ * navigation (buttons, keyboard, autoplay, `goTo()`); never affects a
148
+ * gesture-driven swipe, which always uses its own live-drag/settle
149
+ * animation regardless of `mode` (§2.4). Default `'slide'`. One of the
150
+ * built-in preset names (`slide`, `fade`, `zoom`, `deck`, `slideVertical`,
151
+ * `push`, `fadeUp`, `fadeDown`, `fadeLeft`, `fadeRight`, `zoomOut`,
152
+ * `scaleUp`, `scaleDown`, `rotate`, `rotateLeft`, `rotateRight`, `flipX`,
153
+ * `flipY`, `cube`, `coverflow`, `drop`) — or any other string, treated as
154
+ * a custom CSS class pair (`shoji-transition-<mode>-enter`/`-leave`) the
155
+ * host supplies its own `transition`/`@keyframes animation` for; no JS
156
+ * needed on the host's end.
157
+ */
158
+ mode?: string;
159
+ /**
160
+ * DESIGN.md §2.5 — overrides `mode`/hides baseline controls (§2.6a) under
161
+ * a `(pointer: coarse)` device (touch-primary, not a viewport-width
162
+ * breakpoint — a narrow desktop window isn't "mobile" here). Deliberately
163
+ * scoped to just these two fields, matching this section's own example,
164
+ * rather than a general "override any GalleryOptions field under a media
165
+ * query" mechanism — nothing in the roster needs broader coverage yet,
166
+ * and that would be a substantially larger, riskier feature to build
167
+ * speculatively (CLAUDE.md).
168
+ */
169
+ mobileSettings?: {
170
+ mode?: string;
171
+ /** `false` starts controls hidden on a coarse-pointer device (via the existing §2.8 auto-hide, not a separate permanent-hide state) — they still reveal on any activity, same as normal auto-hide; a viewer is never actually stranded without a way to reach Close. */
172
+ controls?: boolean;
173
+ };
174
+ [key: string]: unknown;
175
+ }
176
+ /** See DESIGN.md §2.2 — grows as core lifecycle stages and plugins land. */
177
+ export interface GalleryEvents extends Record<string, unknown> {
178
+ beforeOpen: {
179
+ index: number;
180
+ };
181
+ open: {
182
+ index: number;
183
+ };
184
+ afterOpen: {
185
+ index: number;
186
+ };
187
+ beforeClose: Record<string, never>;
188
+ close: Record<string, never>;
189
+ afterClose: Record<string, never>;
190
+ destroy: Record<string, never>;
191
+ /** DESIGN.md §2.1 — fires after the item list changes via updateSlides()/refresh(). */
192
+ itemsUpdated: {
193
+ items: GalleryItem[];
194
+ };
195
+ /**
196
+ * DESIGN.md §2.2 lists these as cancelable; that isn't implemented yet (no
197
+ * plugin needs it yet) — detail is plain `{from, to}` for now, same as the
198
+ * other lifecycle events, not the `event.preventDefault()` shape §2.2 describes.
199
+ */
200
+ beforeSlide: {
201
+ from: number;
202
+ to: number;
203
+ };
204
+ afterSlide: {
205
+ from: number;
206
+ to: number;
207
+ };
208
+ /** DESIGN.md §2.3 — fires once a slide's media has decoded/settled. */
209
+ slideItemLoad: {
210
+ index: number;
211
+ };
212
+ /** DESIGN.md §2.8 — toolbar auto-hide sync points for plugins with their own overlays. */
213
+ 'controls:hide': Record<string, never>;
214
+ 'controls:show': Record<string, never>;
215
+ /** DESIGN.md §2.2a-autoplay — emitted by the autoplay plugin, not core. */
216
+ autoplayStart: Record<string, never>;
217
+ autoplayStop: Record<string, never>;
218
+ /**
219
+ * DESIGN.md §2.4 — core drives horizontal drag-to-navigate and vertical
220
+ * drag-to-close itself (no event needed for either — they just call
221
+ * next()/prev()/close()); tap/doubleTap/pinch/wheelZoom have no built-in
222
+ * effect in core and exist purely as a relay for a future Zoom plugin
223
+ * (double-tap toggles zoom, pinch/wheel+ctrl zoom, per §4) to consume
224
+ * without core depending on that plugin existing. Coordinates are
225
+ * viewport (client) pixels, same as the underlying PointerEvent/WheelEvent.
226
+ */
227
+ tap: {
228
+ x: number;
229
+ y: number;
230
+ };
231
+ doubleTap: {
232
+ x: number;
233
+ y: number;
234
+ };
235
+ pinchStart: {
236
+ centerX: number;
237
+ centerY: number;
238
+ };
239
+ /** scale is relative to this pinch gesture's own start distance (1 = unchanged), not cumulative across separate pinches. */
240
+ pinchMove: {
241
+ scale: number;
242
+ centerX: number;
243
+ centerY: number;
244
+ };
245
+ pinchEnd: Record<string, never>;
246
+ /** deltaScale is a small per-event increment (positive = zoom in); a consumer accumulates it, it isn't an absolute scale. */
247
+ wheelZoom: {
248
+ deltaScale: number;
249
+ x: number;
250
+ y: number;
251
+ };
252
+ /** DESIGN.md §4-fullscreen — emitted by the fullscreen plugin, not core; fires from the native `fullscreenchange` event, not the toolbar button's own click, so it reflects reality even when fullscreen was entered/exited by other means (browser Escape handling, a request rejected). */
253
+ fullscreenChange: {
254
+ fullscreen: boolean;
255
+ };
256
+ /** DESIGN.md §4-rotate/flip — emitted by the rotateFlip plugin, not core, on every rotate/flip click; the plugin itself never persists this (resets per slide) — a host wanting to keep it stores this event's payload themselves. */
257
+ rotateFlipChange: {
258
+ index: number;
259
+ flipH: boolean;
260
+ flipV: boolean;
261
+ rotation: number;
262
+ };
263
+ /** DESIGN.md §4-zoom — emitted by the zoom plugin, not core, on every scale change (gesture or button-driven); resets per slide, not persisted by the plugin itself, same pattern as rotateFlipChange. */
264
+ zoomChange: {
265
+ index: number;
266
+ scale: number;
267
+ };
268
+ /**
269
+ * DESIGN.md §5 — emitted by the layout plugin, not core, once a render
270
+ * pass has built and appended tile DOM: after `fullRender()` (every tile
271
+ * currently in the container — a full rebuild, e.g. from a non-append
272
+ * items change), or after `appendRender()` (just the newly-created tiles
273
+ * — existing ones are untouched, per the plugin's own incremental-append
274
+ * behavior). `tiles` is exactly the elements built *this pass*, in item
275
+ * order — a host injecting custom content into tiles (a badge, a
276
+ * selection checkbox, anything not itself part of the layout plugin)
277
+ * uses `element` directly and `index` to look the item back up via
278
+ * `gallery.items[index]`; `element.dataset.shojiIndex` (also set on
279
+ * every tile, matching `index` here) is the same lookup from the DOM
280
+ * side, for code that only has the element itself later (e.g. a click
281
+ * handler on injected content) and needs the index back without having
282
+ * kept this event's payload around.
283
+ */
284
+ layoutRender: {
285
+ tiles: {
286
+ index: number;
287
+ element: HTMLElement;
288
+ }[];
289
+ };
290
+ }
@@ -0,0 +1,33 @@
1
+ export interface ZoomTransitionTarget {
2
+ /** The thumbnail element to animate to/from. */
3
+ origin: HTMLElement;
4
+ /** The `.shoji-slide-media` element being animated — not `.shoji-slide` itself, whose transform is already owned by pool-offset positioning. */
5
+ target: HTMLElement;
6
+ /** `item.width / item.height`, when known — `target` is always its full flex-box size, never the (usually smaller, letterboxed) rendered photo inside it; see `effectiveTargetBox`. */
7
+ aspectRatio?: number;
8
+ }
9
+ /**
10
+ * Waits for `target`'s own transform transition to end (with a safety-net
11
+ * timeout in case transitionend never fires — an interrupted/removed
12
+ * element, or a browser quirk) then calls `cb` exactly once. Exported: the
13
+ * gesture-driven drag-to-navigate/drag-to-close settle animations (§2.4,
14
+ * `Gallery.ts`) reuse this same wait-with-fallback logic rather than
15
+ * duplicating it — same instant-jump-then-transition FLIP family of moves
16
+ * as the zoom transition, just on a different element/property pairing.
17
+ */
18
+ export declare function waitForTransitionEnd(target: HTMLElement, cb: () => void): void;
19
+ /**
20
+ * FLIP-style open: `target` starts visually at `origin`'s position/size (an
21
+ * instant, untransitioned jump), then transitions to its natural layout —
22
+ * reads as "growing out of the thumbnail." Fire-and-forget: cleans up its
23
+ * own inline styles once the transition ends, nothing to await.
24
+ */
25
+ export declare function zoomIn({ origin, target, aspectRatio }: ZoomTransitionTarget): void;
26
+ /**
27
+ * Reverse of `zoomIn`: `target` transitions from its natural position down
28
+ * to `origin`'s rect — "shrinking back into the thumbnail." `onComplete`
29
+ * always fires exactly once, synchronously if there's nothing to animate
30
+ * (reduced motion, or no valid rect), otherwise once the transition ends —
31
+ * callers use this to know when it's safe to actually hide/finalize.
32
+ */
33
+ export declare function zoomOut({ origin, target, aspectRatio }: ZoomTransitionTarget, onComplete: () => void): void;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * DESIGN.md §2.4 — one Pointer-Events-based engine feeding all consumers:
3
+ * core drag-to-navigate, core vertical-swipe-to-close, and (via `onPinch`/
4
+ * `onDoubleTap`/`onWheelZoom`) a future zoom plugin that doesn't exist yet.
5
+ * Pointer Events (not separate mouse/touch listeners) give mouse-drag and
6
+ * touch-swipe parity for free — one code path, one set of thresholds.
7
+ *
8
+ * State machine: `idle → pending → dragging(h|v) → idle`. `pending` is the
9
+ * window before `lockThreshold` is crossed, where it's still ambiguous
10
+ * whether this is a tap, a horizontal drag, or a vertical drag; `dragging`
11
+ * is entered once one axis's delta exceeds the threshold first, and the
12
+ * *other* axis is ignored for the rest of that gesture (an axis lock, not
13
+ * a snapped direction — diagonal drags don't fight the engine). There's no
14
+ * literal blocking `settling` state after release: `onDragEnd` reports the
15
+ * outcome synchronously and the caller owns however long its own
16
+ * snap-back/completion animation takes; a new `pointerdown` before that
17
+ * finishes is allowed to interrupt it (matches real touch-carousel UX —
18
+ * grabbing a still-settling slide feels responsive, not broken).
19
+ *
20
+ * Pinch tracking is a separate concern layered on top of the same pointer
21
+ * map: a second simultaneous pointer suspends whatever single-pointer drag
22
+ * state was in progress (real pinches start from a single touch already
23
+ * down) and switches to distance-ratio tracking until back down to one
24
+ * pointer or zero.
25
+ */
26
+ export type GestureDirection = 'horizontal' | 'vertical';
27
+ export interface GestureEngineOptions {
28
+ /** px of movement before a direction locks in. DESIGN.md §2.4 default: 10. */
29
+ lockThreshold: number;
30
+ /** px of movement past which a released drag counts as "swiped" (completes), independent of velocity. DESIGN.md §2.4 default: 50. */
31
+ swipeThreshold: number;
32
+ /** px/ms release velocity past which a drag completes even under swipeThreshold — a fast flick counts. DESIGN.md §2.4 default: 0.3. */
33
+ swipeVelocity: number;
34
+ }
35
+ export interface GestureEngineCallbacks {
36
+ /** Direction has just locked in for this gesture — the first move past lockThreshold. */
37
+ onDragStart?(direction: GestureDirection, event: PointerEvent): void;
38
+ /** delta: signed px moved along the locked axis so far (this move's cumulative total, not a per-frame diff). */
39
+ onDragMove?(direction: GestureDirection, delta: number, event: PointerEvent): void;
40
+ /** Pointer released (or cancelled) after a drag was in progress. velocity is signed px/ms over the whole gesture; completed reflects swipeThreshold/swipeVelocity. cancelled is true for pointercancel (e.g. an OS gesture took over) — always treated as not-completed regardless of delta/velocity. */
41
+ onDragEnd?(direction: GestureDirection, delta: number, velocity: number, completed: boolean, cancelled: boolean): void;
42
+ /** A pointerdown that never crossed lockThreshold before release — not a drag. Used to detect double-tap (the engine tracks tap timing/position itself and calls onDoubleTap instead when two land close enough together) as well as being a simple tap signal on its own. */
43
+ onTap?(x: number, y: number, event: PointerEvent): void;
44
+ onDoubleTap?(x: number, y: number, event: PointerEvent): void;
45
+ /** scale is relative to the pinch's own start distance (1 = unchanged), not a cumulative multiplier across multiple pinch gestures. */
46
+ onPinchStart?(centerX: number, centerY: number): void;
47
+ onPinchMove?(scale: number, centerX: number, centerY: number): void;
48
+ onPinchEnd?(): void;
49
+ /** Ctrl+wheel (trackpad pinch reports this way in every major browser) or a genuine ctrl+scroll-wheel. deltaScale is a small increment (positive = zoom in), not an absolute scale — the caller accumulates it. */
50
+ onWheelZoom?(deltaScale: number, x: number, y: number, event: WheelEvent): void;
51
+ /** Only consulted on pointerdown — returning true means this pointer is ignored for the rest of its lifetime (a click on a real button/control shouldn't also start a drag). */
52
+ ignore?(event: PointerEvent): boolean;
53
+ /** Consulted right before capturing a locked drag's pointer — `false` skips capture for this gesture entirely (not just its drag effect). Default `true` if omitted. See onPointerMove's capture branch for why this needs to be a real skip, not just a suppressed effect. */
54
+ shouldCapture?(): boolean;
55
+ }
56
+ export declare class GestureEngine {
57
+ private readonly target;
58
+ private readonly options;
59
+ private readonly callbacks;
60
+ private readonly pointers;
61
+ private primaryPointerId;
62
+ private direction;
63
+ private dragStartDistance;
64
+ private pinching;
65
+ private pinchStartDistance;
66
+ private lastTapTime;
67
+ private lastTapX;
68
+ private lastTapY;
69
+ constructor(target: HTMLElement, callbacks: GestureEngineCallbacks, options?: Partial<GestureEngineOptions>);
70
+ destroy(): void;
71
+ private readonly onPointerDown;
72
+ private readonly onPointerMove;
73
+ private readonly onPointerUp;
74
+ private readonly onPointerCancel;
75
+ private finishPointer;
76
+ private registerTap;
77
+ private readonly onWheel;
78
+ }