@michaelyagi/shoji 0.1.0-alpha.6
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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/esm/core/EventBus.d.ts +16 -0
- package/dist/esm/core/FocusTrap.d.ts +9 -0
- package/dist/esm/core/Gallery.d.ts +221 -0
- package/dist/esm/core/GestureController.d.ts +50 -0
- package/dist/esm/core/LiveRegion.d.ts +6 -0
- package/dist/esm/core/SlideManager.d.ts +83 -0
- package/dist/esm/core/bodyScrollLock.d.ts +2 -0
- package/dist/esm/core/dom.d.ts +27 -0
- package/dist/esm/core/icons.d.ts +6 -0
- package/dist/esm/core/index.d.ts +5 -0
- package/dist/esm/core/index.js +1966 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/plugin.d.ts +54 -0
- package/dist/esm/core/rotateFlipNormalize.d.ts +19 -0
- package/dist/esm/core/scan.d.ts +8 -0
- package/dist/esm/core/types.d.ts +268 -0
- package/dist/esm/core/zoomTransition.d.ts +33 -0
- package/dist/esm/gestures/GestureEngine.d.ts +78 -0
- package/dist/esm/index.css +475 -0
- package/dist/esm/index.d.ts +32 -0
- package/dist/esm/index.js +24 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index2.css +22 -0
- package/dist/esm/index3.css +166 -0
- package/dist/esm/index4.css +27 -0
- package/dist/esm/plugins/activeThumbnail/index.d.ts +26 -0
- package/dist/esm/plugins/activeThumbnail/index.js +48 -0
- package/dist/esm/plugins/activeThumbnail/index.js.map +1 -0
- package/dist/esm/plugins/autoplay/icons.d.ts +3 -0
- package/dist/esm/plugins/autoplay/index.d.ts +17 -0
- package/dist/esm/plugins/autoplay/index.js +177 -0
- package/dist/esm/plugins/autoplay/index.js.map +1 -0
- package/dist/esm/plugins/fullscreen/icons.d.ts +3 -0
- package/dist/esm/plugins/fullscreen/index.d.ts +17 -0
- package/dist/esm/plugins/fullscreen/index.js +74 -0
- package/dist/esm/plugins/fullscreen/index.js.map +1 -0
- package/dist/esm/plugins/layout/index.d.ts +191 -0
- package/dist/esm/plugins/layout/index.js +746 -0
- package/dist/esm/plugins/layout/index.js.map +1 -0
- package/dist/esm/plugins/layout/justified.d.ts +68 -0
- package/dist/esm/plugins/layout/masonry.d.ts +92 -0
- package/dist/esm/plugins/rotateFlip/icons.d.ts +5 -0
- package/dist/esm/plugins/rotateFlip/index.d.ts +17 -0
- package/dist/esm/plugins/rotateFlip/index.js +138 -0
- package/dist/esm/plugins/rotateFlip/index.js.map +1 -0
- package/dist/esm/plugins/video/index.d.ts +13 -0
- package/dist/esm/plugins/video/index.js +95 -0
- package/dist/esm/plugins/video/index.js.map +1 -0
- package/dist/esm/plugins/video/youtube.d.ts +61 -0
- package/dist/esm/plugins/zoom/icons.d.ts +4 -0
- package/dist/esm/plugins/zoom/index.d.ts +30 -0
- package/dist/esm/plugins/zoom/index.js +274 -0
- package/dist/esm/plugins/zoom/index.js.map +1 -0
- package/dist/esm/plugins/zoom/zoomMath.d.ts +24 -0
- package/dist/esm/transitions/SlideTransition.d.ts +25 -0
- package/dist/esm/transitions/presets.d.ts +21 -0
- package/dist/esm/zoomTransition-bbKHpVpA.js +110 -0
- package/dist/esm/zoomTransition-bbKHpVpA.js.map +1 -0
- package/dist/shoji.css +690 -0
- package/dist/shoji.js +3605 -0
- package/dist/shoji.js.map +1 -0
- package/dist/shoji.min.css +1 -0
- package/dist/shoji.min.js +2 -0
- package/dist/shoji.min.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,268 @@
|
|
|
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
|
+
* Explicit, self-documenting opt-in for raw HTML in a caption — named after
|
|
23
|
+
* React's identical escape hatch on purpose, so it can't be triggered by
|
|
24
|
+
* accident and anyone who's seen that convention knows what it means. Shoji
|
|
25
|
+
* does not sanitize this string; the host must, if it isn't already trusted.
|
|
26
|
+
*/
|
|
27
|
+
export interface DangerousHtmlCaption {
|
|
28
|
+
dangerouslySetInnerHTML: string;
|
|
29
|
+
}
|
|
30
|
+
/** Mirrors a `<source src type>` child. */
|
|
31
|
+
export interface MediaSource {
|
|
32
|
+
src: string;
|
|
33
|
+
type: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `'youtube'` is detected by core's DOM scanning too (§2.1) — cheap,
|
|
37
|
+
* dependency-free URL parsing, not the provider's SDK. *Rendering* any
|
|
38
|
+
* non-`'html5'` provider is a plugin's job (§4-video), via
|
|
39
|
+
* `ctx.ui.registerVideoProvider()` or `'custom'`'s own `render`.
|
|
40
|
+
*/
|
|
41
|
+
export type VideoDescriptor = {
|
|
42
|
+
provider: 'html5';
|
|
43
|
+
} | {
|
|
44
|
+
provider: 'youtube' | 'vimeo' | 'wistia';
|
|
45
|
+
id: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
} | {
|
|
48
|
+
provider: 'custom';
|
|
49
|
+
/** Same contract as `plugin.ts`'s `VideoProviderRenderer` — see its doc comment. */
|
|
50
|
+
render: (el: HTMLElement, item: GalleryItem, onReady: () => void, signal: AbortSignal) => void;
|
|
51
|
+
};
|
|
52
|
+
export interface GalleryOptions {
|
|
53
|
+
/** DOM selector for gallery items when not using dynamic mode. */
|
|
54
|
+
selector?: string;
|
|
55
|
+
/** Dynamic-mode item list; mutate live via gallery.updateSlides(). */
|
|
56
|
+
items?: GalleryItem[];
|
|
57
|
+
/** Index to open on — used both as `open()`'s default argument and, with `openOnInit`, as the index opened automatically at construction. */
|
|
58
|
+
index?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Opens the lightbox immediately at construction (to `index`, default 0),
|
|
61
|
+
* instead of the normal default of staying closed until the host calls
|
|
62
|
+
* `open()` or a scanned/layout-rendered thumbnail is clicked. Default
|
|
63
|
+
* `false`. Useful for hash/deep-link-style integrations that should land
|
|
64
|
+
* straight in the lightbox; everyone else should leave this off — a
|
|
65
|
+
* gallery that pops open on page load without a click is rarely what's
|
|
66
|
+
* wanted.
|
|
67
|
+
*/
|
|
68
|
+
openOnInit?: boolean;
|
|
69
|
+
/** Flat locale key map — see CLAUDE.md: no hardcoded English in UI. */
|
|
70
|
+
locale?: Record<string, string>;
|
|
71
|
+
/** Registered plugins; each plugin's own options nest under options[plugin.name]. */
|
|
72
|
+
plugins?: ShojiPlugin[];
|
|
73
|
+
/**
|
|
74
|
+
* DESIGN.md §2.8 — ms of inactivity before all buttons/overlays (toolbar,
|
|
75
|
+
* prev/next, counter, caption) auto-hide; default 5000. `0` is a distinct
|
|
76
|
+
* mode, not "disabled": controls never show at all, regardless of activity.
|
|
77
|
+
*/
|
|
78
|
+
autoHideDelay?: number;
|
|
79
|
+
/**
|
|
80
|
+
* DESIGN.md §2.3 — how many slides on each side of the active one are
|
|
81
|
+
* kept mounted *and* proactively decoded ahead of time; default 1.
|
|
82
|
+
* Navigating to any index within that window shows instantly, no loading
|
|
83
|
+
* spinner — its content was already decoded and cached (by item index,
|
|
84
|
+
* surviving the pool's internal slot reshuffling) before you got there.
|
|
85
|
+
* Navigating further than `preload` away (a fast flick through several
|
|
86
|
+
* slides, or a direct `goTo()`/deep link) still shows the spinner while
|
|
87
|
+
* that one decodes, same as ever — this only changes what counts as
|
|
88
|
+
* "already ready."
|
|
89
|
+
*/
|
|
90
|
+
preload?: number;
|
|
91
|
+
/** Shows the "N / M" counter badge; default true. Purely visual — the live-region announcement (§2.6) always includes position regardless. */
|
|
92
|
+
counter?: boolean;
|
|
93
|
+
/** DESIGN.md §2.3a — starts a video slide's caption shown instead of hidden; default `false`. No effect on a photo's caption. */
|
|
94
|
+
showVideoCaption?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* DESIGN.md §2.2 — `next()`/`prev()` (and the arrow-key/toolbar-button paths
|
|
97
|
+
* that call them) wrap past the last/first item instead of stopping; default
|
|
98
|
+
* true. Does not affect `goTo(index)` with an explicit out-of-range index
|
|
99
|
+
* (still clamps — a directed jump, not a step) or `Home`/`End` (still an
|
|
100
|
+
* absolute jump to the first/last item).
|
|
101
|
+
*/
|
|
102
|
+
loop?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Default `true`. `false` disables every viewer-facing way to close the
|
|
105
|
+
* lightbox: the close button (hidden entirely, not just inert), clicking
|
|
106
|
+
* the backdrop, `Escape`, and vertical swipe-to-close (no live drag
|
|
107
|
+
* feedback either, not just a release that fails to close). For
|
|
108
|
+
* non-closable embeds — a gallery meant to stay open permanently once
|
|
109
|
+
* shown. `gallery.close()` itself is unaffected — this only gates the
|
|
110
|
+
* four built-in viewer-triggered paths, host code can still close it
|
|
111
|
+
* programmatically at any time.
|
|
112
|
+
*/
|
|
113
|
+
closable?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* DESIGN.md §2.4 — overrides the gesture engine's `lockThreshold`/
|
|
116
|
+
* `swipeThreshold`/`swipeVelocity` (defaults: 10px, 50px, 0.3px/ms).
|
|
117
|
+
* Partial: any field left out keeps its default. Momentum easing is not
|
|
118
|
+
* configurable here — it's the CSS custom property
|
|
119
|
+
* `--shoji-momentum-easing`, per CLAUDE.md's "no hardcoded sizes/timings
|
|
120
|
+
* in JS, everything through --shoji-* custom properties."
|
|
121
|
+
*/
|
|
122
|
+
gestures?: Partial<GestureEngineOptions>;
|
|
123
|
+
/**
|
|
124
|
+
* DESIGN.md §2.5 — the slide-to-slide transition for programmatic
|
|
125
|
+
* navigation (buttons, keyboard, autoplay, `goTo()`); never affects a
|
|
126
|
+
* gesture-driven swipe, which always uses its own live-drag/settle
|
|
127
|
+
* animation regardless of `mode` (§2.4). Default `'slide'`. One of the
|
|
128
|
+
* built-in preset names (`slide`, `fade`, `zoom`, `deck`, `slideVertical`,
|
|
129
|
+
* `push`, `fadeUp`, `fadeDown`, `fadeLeft`, `fadeRight`, `zoomOut`,
|
|
130
|
+
* `scaleUp`, `scaleDown`, `rotate`, `rotateLeft`, `rotateRight`, `flipX`,
|
|
131
|
+
* `flipY`, `cube`, `coverflow`, `drop`) — or any other string, treated as
|
|
132
|
+
* a custom CSS class pair (`shoji-transition-<mode>-enter`/`-leave`) the
|
|
133
|
+
* host supplies its own `transition`/`@keyframes animation` for; no JS
|
|
134
|
+
* needed on the host's end.
|
|
135
|
+
*/
|
|
136
|
+
mode?: string;
|
|
137
|
+
/**
|
|
138
|
+
* DESIGN.md §2.5 — overrides `mode`/hides baseline controls (§2.6a) under
|
|
139
|
+
* a `(pointer: coarse)` device (touch-primary, not a viewport-width
|
|
140
|
+
* breakpoint — a narrow desktop window isn't "mobile" here). Deliberately
|
|
141
|
+
* scoped to just these two fields, matching this section's own example,
|
|
142
|
+
* rather than a general "override any GalleryOptions field under a media
|
|
143
|
+
* query" mechanism — nothing in the roster needs broader coverage yet,
|
|
144
|
+
* and that would be a substantially larger, riskier feature to build
|
|
145
|
+
* speculatively (CLAUDE.md).
|
|
146
|
+
*/
|
|
147
|
+
mobileSettings?: {
|
|
148
|
+
mode?: string;
|
|
149
|
+
/** `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. */
|
|
150
|
+
controls?: boolean;
|
|
151
|
+
};
|
|
152
|
+
[key: string]: unknown;
|
|
153
|
+
}
|
|
154
|
+
/** See DESIGN.md §2.2 — grows as core lifecycle stages and plugins land. */
|
|
155
|
+
export interface GalleryEvents extends Record<string, unknown> {
|
|
156
|
+
beforeOpen: {
|
|
157
|
+
index: number;
|
|
158
|
+
};
|
|
159
|
+
open: {
|
|
160
|
+
index: number;
|
|
161
|
+
};
|
|
162
|
+
afterOpen: {
|
|
163
|
+
index: number;
|
|
164
|
+
};
|
|
165
|
+
beforeClose: Record<string, never>;
|
|
166
|
+
close: Record<string, never>;
|
|
167
|
+
afterClose: Record<string, never>;
|
|
168
|
+
destroy: Record<string, never>;
|
|
169
|
+
/** DESIGN.md §2.1 — fires after the item list changes via updateSlides()/refresh(). */
|
|
170
|
+
itemsUpdated: {
|
|
171
|
+
items: GalleryItem[];
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* DESIGN.md §2.2 lists these as cancelable; that isn't implemented yet (no
|
|
175
|
+
* plugin needs it yet) — detail is plain `{from, to}` for now, same as the
|
|
176
|
+
* other lifecycle events, not the `event.preventDefault()` shape §2.2 describes.
|
|
177
|
+
*/
|
|
178
|
+
beforeSlide: {
|
|
179
|
+
from: number;
|
|
180
|
+
to: number;
|
|
181
|
+
};
|
|
182
|
+
afterSlide: {
|
|
183
|
+
from: number;
|
|
184
|
+
to: number;
|
|
185
|
+
};
|
|
186
|
+
/** DESIGN.md §2.3 — fires once a slide's media has decoded/settled. */
|
|
187
|
+
slideItemLoad: {
|
|
188
|
+
index: number;
|
|
189
|
+
};
|
|
190
|
+
/** DESIGN.md §2.8 — toolbar auto-hide sync points for plugins with their own overlays. */
|
|
191
|
+
'controls:hide': Record<string, never>;
|
|
192
|
+
'controls:show': Record<string, never>;
|
|
193
|
+
/** DESIGN.md §2.2a-autoplay — emitted by the autoplay plugin, not core. */
|
|
194
|
+
autoplayStart: Record<string, never>;
|
|
195
|
+
autoplayStop: Record<string, never>;
|
|
196
|
+
/**
|
|
197
|
+
* DESIGN.md §2.4 — core drives horizontal drag-to-navigate and vertical
|
|
198
|
+
* drag-to-close itself (no event needed for either — they just call
|
|
199
|
+
* next()/prev()/close()); tap/doubleTap/pinch/wheelZoom have no built-in
|
|
200
|
+
* effect in core and exist purely as a relay for a future Zoom plugin
|
|
201
|
+
* (double-tap toggles zoom, pinch/wheel+ctrl zoom, per §4) to consume
|
|
202
|
+
* without core depending on that plugin existing. Coordinates are
|
|
203
|
+
* viewport (client) pixels, same as the underlying PointerEvent/WheelEvent.
|
|
204
|
+
*/
|
|
205
|
+
tap: {
|
|
206
|
+
x: number;
|
|
207
|
+
y: number;
|
|
208
|
+
};
|
|
209
|
+
doubleTap: {
|
|
210
|
+
x: number;
|
|
211
|
+
y: number;
|
|
212
|
+
};
|
|
213
|
+
pinchStart: {
|
|
214
|
+
centerX: number;
|
|
215
|
+
centerY: number;
|
|
216
|
+
};
|
|
217
|
+
/** scale is relative to this pinch gesture's own start distance (1 = unchanged), not cumulative across separate pinches. */
|
|
218
|
+
pinchMove: {
|
|
219
|
+
scale: number;
|
|
220
|
+
centerX: number;
|
|
221
|
+
centerY: number;
|
|
222
|
+
};
|
|
223
|
+
pinchEnd: Record<string, never>;
|
|
224
|
+
/** deltaScale is a small per-event increment (positive = zoom in); a consumer accumulates it, it isn't an absolute scale. */
|
|
225
|
+
wheelZoom: {
|
|
226
|
+
deltaScale: number;
|
|
227
|
+
x: number;
|
|
228
|
+
y: number;
|
|
229
|
+
};
|
|
230
|
+
/** 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). */
|
|
231
|
+
fullscreenChange: {
|
|
232
|
+
fullscreen: boolean;
|
|
233
|
+
};
|
|
234
|
+
/** 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. */
|
|
235
|
+
rotateFlipChange: {
|
|
236
|
+
index: number;
|
|
237
|
+
flipH: boolean;
|
|
238
|
+
flipV: boolean;
|
|
239
|
+
rotation: number;
|
|
240
|
+
};
|
|
241
|
+
/** 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. */
|
|
242
|
+
zoomChange: {
|
|
243
|
+
index: number;
|
|
244
|
+
scale: number;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* DESIGN.md §5 — emitted by the layout plugin, not core, once a render
|
|
248
|
+
* pass has built and appended tile DOM: after `fullRender()` (every tile
|
|
249
|
+
* currently in the container — a full rebuild, e.g. from a non-append
|
|
250
|
+
* items change), or after `appendRender()` (just the newly-created tiles
|
|
251
|
+
* — existing ones are untouched, per the plugin's own incremental-append
|
|
252
|
+
* behavior). `tiles` is exactly the elements built *this pass*, in item
|
|
253
|
+
* order — a host injecting custom content into tiles (a badge, a
|
|
254
|
+
* selection checkbox, anything not itself part of the layout plugin)
|
|
255
|
+
* uses `element` directly and `index` to look the item back up via
|
|
256
|
+
* `gallery.items[index]`; `element.dataset.shojiIndex` (also set on
|
|
257
|
+
* every tile, matching `index` here) is the same lookup from the DOM
|
|
258
|
+
* side, for code that only has the element itself later (e.g. a click
|
|
259
|
+
* handler on injected content) and needs the index back without having
|
|
260
|
+
* kept this event's payload around.
|
|
261
|
+
*/
|
|
262
|
+
layoutRender: {
|
|
263
|
+
tiles: {
|
|
264
|
+
index: number;
|
|
265
|
+
element: HTMLElement;
|
|
266
|
+
}[];
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -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
|
+
}
|