@surdeddd/bottom-sheet 0.1.0 → 0.3.0

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 (57) hide show
  1. package/README.md +57 -21
  2. package/dist/BottomSheetEngine-CAymrH-P.d.cts +285 -0
  3. package/dist/BottomSheetEngine-DcqcuudN.d.ts +285 -0
  4. package/dist/element.cjs +775 -537
  5. package/dist/element.d.cts +9 -57
  6. package/dist/element.d.ts +9 -57
  7. package/dist/element.iife.global.js +2 -2
  8. package/dist/element.js +775 -537
  9. package/dist/index.cjs +760 -600
  10. package/dist/index.d.cts +18 -85
  11. package/dist/index.d.ts +18 -85
  12. package/dist/index.js +758 -601
  13. package/dist/integrations/formik.d.cts +2 -8
  14. package/dist/integrations/formik.d.ts +2 -8
  15. package/dist/integrations/react-hook-form.d.cts +2 -11
  16. package/dist/integrations/react-hook-form.d.ts +2 -11
  17. package/dist/overlay.cjs +168 -122
  18. package/dist/overlay.d.cts +14 -137
  19. package/dist/overlay.d.ts +14 -137
  20. package/dist/overlay.js +168 -122
  21. package/dist/preact.d.cts +2 -2
  22. package/dist/preact.d.ts +2 -2
  23. package/dist/preact.js +0 -1
  24. package/dist/qwik.cjs +667 -519
  25. package/dist/qwik.d.cts +3 -24
  26. package/dist/qwik.d.ts +3 -24
  27. package/dist/qwik.js +668 -520
  28. package/dist/react.cjs +873 -648
  29. package/dist/react.d.cts +34 -159
  30. package/dist/react.d.ts +34 -159
  31. package/dist/react.js +873 -648
  32. package/dist/solid.cjs +703 -560
  33. package/dist/solid.d.cts +264 -32
  34. package/dist/solid.d.ts +264 -32
  35. package/dist/solid.js +704 -561
  36. package/dist/solid.source.jsx +3215 -0
  37. package/dist/styles.css +37 -112
  38. package/dist/svelte-core.cjs +3149 -0
  39. package/dist/svelte-core.js +3147 -0
  40. package/dist/svelte-src/BottomSheet.svelte +210 -0
  41. package/dist/svelte-src/index.js +2 -0
  42. package/dist/svelte.cjs +718 -547
  43. package/dist/svelte.d.cts +100 -0
  44. package/dist/svelte.d.ts +15 -4
  45. package/dist/svelte.js +719 -548
  46. package/dist/themes/ios.css +0 -3
  47. package/dist/themes/material.css +0 -3
  48. package/dist/themes/vercel.css +0 -3
  49. package/dist/{transform-Brkf-wzo.d.cts → transform-jVfAikE9.d.cts} +0 -1
  50. package/dist/{transform-Brkf-wzo.d.ts → transform-jVfAikE9.d.ts} +0 -1
  51. package/dist/vue.cjs +677 -526
  52. package/dist/vue.d.cts +78 -0
  53. package/dist/vue.d.ts +12 -2
  54. package/dist/vue.js +678 -527
  55. package/package.json +105 -45
  56. package/dist/BottomSheetEngine-CvSVbo6c.d.cts +0 -704
  57. package/dist/BottomSheetEngine-DbO8rW_3.d.ts +0 -704
@@ -1,704 +0,0 @@
1
- import { T as TransformAxis } from './transform-Brkf-wzo.cjs';
2
-
3
- /**
4
- * A snap point can be expressed as:
5
- * - number → pixels
6
- * - `${n}%` → percent of viewport height (or width for side sheets)
7
- * - "fit" → fits to header / measured natural height
8
- * - "full" → 100% of axis
9
- * - any CSS length → resolved via probe element ("50dvh", "min(80dvh, 600px)",
10
- * "clamp(200px, 60%, 800px)", "env(safe-area-inset-bottom)")
11
- * Prefer `dvh` over legacy `vh` — iOS Safari URL bar
12
- * collapse leaves `vh` stale; the engine warns on `vh`.
13
- */
14
- type SnapPoint = number | `${number}%` | "fit" | "full" | (string & {});
15
- type SnapPointDef<Id extends string = string> = {
16
- /** Stable identifier used for `snapTo()` and events. */
17
- id: Id;
18
- /** Target size of the sheet in this state. */
19
- size: SnapPoint;
20
- };
21
- /**
22
- * Extract the union of `id` literals from a `SnapPointDef[]` (or from an
23
- * `EngineOptions`-shaped object). For literal inference the consumer must
24
- * pin the array with `as const`:
25
- *
26
- * const opts = {
27
- * snapPoints: [
28
- * { id: "min", size: 96 },
29
- * { id: "full", size: "85%" },
30
- * ] as const,
31
- * };
32
- * type Id = SnapId<typeof opts.snapPoints>; // "min" | "full"
33
- *
34
- * Without `as const` TS widens `id` to `string` and the helper resolves to
35
- * `string` — no type error, but no narrowing either.
36
- */
37
- type SnapId<T extends readonly SnapPointDef<string>[] | {
38
- snapPoints: readonly SnapPointDef<string>[];
39
- }> = T extends {
40
- snapPoints: readonly SnapPointDef<infer U>[];
41
- } ? U : T extends readonly SnapPointDef<infer U>[] ? U : never;
42
- type SheetMode = TransformAxis;
43
- /**
44
- * Named scrim look-and-feel preset. Each preset bundles a `color`, optional
45
- * `blur`, opacity `range`, and `interactive` default into one value so
46
- * consumers don't need to remember the underlying scrim fields.
47
- *
48
- * - `subtle` — light dim, no blur, late fade-in (good for peeks).
49
- * - `standard` — moderate dim, no blur, full-range fade (default-ish).
50
- * - `monitoring` — bluish dim with light blur (dashboard / panel UX).
51
- * - `cinematic` — heavy dim with strong blur (full-attention modal).
52
- */
53
- type ScrimPreset = "subtle" | "standard" | "monitoring" | "cinematic";
54
- /** Resolved field set for a `ScrimPreset` — used both at construction and by
55
- * `setScrim({ preset })` to layer baseline values under explicit overrides. */
56
- type ScrimPresetConfig = {
57
- color: string;
58
- blur: string | undefined;
59
- range: [number, number];
60
- interactive: boolean;
61
- };
62
- /**
63
- * Frozen so a consumer can't mutate the table at runtime and silently change
64
- * every engine that uses a preset. Each entry mirrors the `ScrimPresetConfig`
65
- * fields exactly.
66
- */
67
- declare const SCRIM_PRESETS: Readonly<Record<ScrimPreset, ScrimPresetConfig>>;
68
- /**
69
- * Batch-update payload for `setScrim`. `null` for `color` / `blur` means
70
- * "clear the inline style" (i.e. revert to whatever CSS provides); `undefined`
71
- * means "leave as-is". `preset` is applied first and any sibling field
72
- * overrides it. A single recompute fires at the end so the per-frame opacity
73
- * stays consistent.
74
- *
75
- * `mode` / `tapToClose` / `enabled` mirror the runtime setters
76
- * `setScrimMode` / `setScrimTapToClose` / `setScrimEnabled` so consumers can
77
- * flip a "monitoring on/off" toggle in one call without four separate
78
- * setters racing each other.
79
- */
80
- type ScrimUpdate = {
81
- color?: string | null;
82
- blur?: string | null;
83
- interactive?: boolean;
84
- range?: [number, number];
85
- preset?: ScrimPreset;
86
- mode?: "full" | "above-sheet" | "off";
87
- tapToClose?: boolean;
88
- enabled?: boolean;
89
- };
90
- /**
91
- * Where in the scrim area a `setScrimOverlay` injection sits. The wrapper is
92
- * `position: absolute` so the consumer's scrim must be a positioning context
93
- * (the engine writes `position: fixed` on `screenComponent` only when
94
- * `scrimMode: "above-sheet"` — for `"full"` you must size + position the
95
- * scrim yourself).
96
- */
97
- type ScrimOverlayPosition = "top-left" | "top-center" | "top-right" | "center-left" | "center" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right";
98
- /**
99
- * Inject a positioned floating element into the scrim area. The classic use
100
- * case is a "monitoring" UX where the rest of the page is dimmed but a small
101
- * cluster of badges / quick-action buttons stays clickable above the sheet
102
- * (the wrapper opts back into `pointer-events: auto` even when the scrim
103
- * itself has `pointer-events: none`).
104
- */
105
- type ScrimOverlayOptions = {
106
- /** Element or fragment to inject as scrim children. */
107
- children: HTMLElement | DocumentFragment;
108
- /** Where in the scrim area to place. Default: "top-right". */
109
- position?: ScrimOverlayPosition;
110
- /**
111
- * Wrapper gets `pointer-events: auto` so clicks land on children even when
112
- * the scrim itself has `pointer-events: none`. Default: true.
113
- */
114
- interactive?: boolean;
115
- /** Padding from the scrim edge in CSS length. Default: "16px". */
116
- inset?: string;
117
- };
118
- type SheetEventMap = {
119
- /**
120
- * Fires before a snap transition starts (programmatic or gesture-driven).
121
- * Listeners may invoke `cancel()` synchronously to abort the transition —
122
- * useful for gating closes (e.g. "form is dirty, ask first"). The check
123
- * runs synchronously during emit; async cancellation is not supported.
124
- */
125
- "before-snap": {
126
- id: string;
127
- size: number;
128
- cancel: () => void;
129
- previousId: string;
130
- };
131
- /** Fires when active snap changes (after animation settles). */
132
- snap: {
133
- id: string;
134
- size: number;
135
- };
136
- /**
137
- * Fires continuously during drag/animation: 0..1 within the allowed range.
138
- *
139
- * **Payload identity is reused across frames.** The engine mutates a single
140
- * `{value, size}` object instead of allocating a fresh one per `applySize`
141
- * (60-120Hz). Mirrors browser `PointerEvent` semantics — consumers must NOT
142
- * retain a reference (e.g. push to an array) without cloning, or every
143
- * stored entry will reflect the latest frame's values. Read fields
144
- * synchronously inside the handler, or copy with `{...payload}` /
145
- * `payload.value`/`payload.size` capture before any async hand-off.
146
- */
147
- progress: {
148
- value: number;
149
- size: number;
150
- };
151
- /** Fires on user-initiated drag start. */
152
- dragstart: {
153
- size: number;
154
- };
155
- /**
156
- * Fires on each pointer move during drag.
157
- *
158
- * **Payload identity is reused across frames.** The engine mutates a single
159
- * `{size, delta}` object instead of allocating a fresh one per `onMove`
160
- * (60-120Hz). Mirrors browser `PointerEvent` semantics — consumers must NOT
161
- * retain a reference (e.g. push to an array) without cloning, or every
162
- * stored entry will reflect the latest frame's values. Read fields
163
- * synchronously inside the handler, or copy with `{...payload}` /
164
- * `payload.size`/`payload.delta` capture before any async hand-off.
165
- */
166
- drag: {
167
- size: number;
168
- delta: number;
169
- };
170
- /** Fires when drag ends, before snap settles. */
171
- dragend: {
172
- size: number;
173
- velocity: number;
174
- };
175
- /** Fires when sheet becomes fully closed (size === 0 / "closed" snap). */
176
- close: void;
177
- /** Fires when sheet opens from a closed state. */
178
- open: {
179
- id: string;
180
- };
181
- };
182
- type EngineOptions = {
183
- /** Element that becomes the sheet container. */
184
- element: HTMLElement;
185
- /** Element used as the drag handle (defaults to `element`). */
186
- handle?: HTMLElement;
187
- /** Element whose scroll position gates pull-to-close swipes. */
188
- scrollContainer?: HTMLElement;
189
- /** Optional backdrop element — opacity is bound to progress. */
190
- backdrop?: HTMLElement;
191
- /** Optional element that fades in behind the sheet driven by progress. */
192
- screenComponent?: HTMLElement;
193
- /** Direction the sheet expands from. Default: "bottom". */
194
- mode?: SheetMode;
195
- /** Ordered list of snap points, smallest → largest. */
196
- snapPoints: SnapPointDef[];
197
- /** Subset of snap-point ids the sheet may settle on right now. */
198
- allowed?: string[];
199
- /** Initial snap id. Falls back to first allowed (or first overall). */
200
- initial?: string;
201
- /** Animation duration in ms. Default: 220. */
202
- duration?: number;
203
- /** Easing function (t in 0..1 → 0..1). Default: critical-damped spring-ish. */
204
- easing?: (t: number) => number;
205
- /** Honour prefers-reduced-motion. Default: true. */
206
- respectReducedMotion?: boolean;
207
- /** Velocity threshold (px/ms) above which a flick promotes to next snap. */
208
- flickVelocity?: number;
209
- /** Distance threshold (px) below which a drag snaps back to origin. */
210
- dragThreshold?: number;
211
- /** Allow dragging beyond max with rubber-banding. Default: true. */
212
- rubberBand?: boolean;
213
- /**
214
- * [start, end] progress range (0..1) over which the backdrop fades from 0
215
- * to 1. Default [0, 1]. Set [0.3, 1] for a "peek" state where the backdrop
216
- * stays invisible until the sheet expands past the peek.
217
- */
218
- backdropRange?: [number, number];
219
- /**
220
- * [start, end] progress range (0..1) over which the screen overlay fades
221
- * from 0 to 1. Mirror of `backdropRange` for `screenComponent`.
222
- * Default [0, 1].
223
- */
224
- screenRange?: [number, number];
225
- /**
226
- * Positioning of `screenComponent`:
227
- * - `"full"` (default): consumer controls layout — no inline positioning
228
- * is applied. Engine only writes opacity and display.
229
- * - `"above-sheet"`: engine pins the screen overlay to the area NOT
230
- * occupied by the sheet (above for `mode:"bottom"`, below for `"top"`,
231
- * etc.) using `position: fixed` + `inset` driven by `--bs-size`. Use
232
- * this for monitoring-style "dim everything except the active panel"
233
- * UX where the sheet stays interactive but the rest of the page dims
234
- * without being clickable.
235
- * - `"off"`: engine forces the scrim invisible (opacity 0 + display none)
236
- * and clears its inline `position` / `inset` / `pointer-events` writes.
237
- * Use this when a parent toggle hides the scrim without re-mounting.
238
- * Switchable at runtime via `setScrimMode`.
239
- */
240
- scrimMode?: "full" | "above-sheet" | "off";
241
- /**
242
- * Quick-win background color for `screenComponent`, written once at
243
- * construction. Skipped entirely when absent so consumers who style the
244
- * scrim via CSS pay no inline-style cost. Any valid CSS `background`
245
- * value (e.g. `"rgba(0,0,0,0.6)"`, `"#000a"`).
246
- *
247
- * **Security**: this string is written to inline `style.background`. The
248
- * value is NOT escaped — pass only trusted CSS, never user input. Any
249
- * `url(...)` token inside the value will trigger a network request from
250
- * the page (potential referer/IP leak via attribute selectors). Reject
251
- * or sanitise consumer input upstream.
252
- */
253
- scrimColor?: string;
254
- /**
255
- * Backdrop blur for `screenComponent`. Engine wraps the supplied length in
256
- * `blur(...)` and writes both `backdropFilter` and the WebKit-prefixed
257
- * variant once at construction. Pass a CSS length like `"8px"`. Skipped
258
- * entirely when absent.
259
- *
260
- * **Security**: same caveat as `scrimColor` — value is written to inline
261
- * `style.backdropFilter` without sanitisation. Pass trusted CSS only.
262
- */
263
- scrimBlur?: string;
264
- /**
265
- * Whether the scrim accepts pointer events. Default `false` — engine
266
- * writes `pointer-events: none` so clicks/taps pass through to underlying
267
- * UI (matches `scrimMode: "above-sheet"`'s existing behaviour). Set
268
- * `true` when the scrim itself must be interactive (e.g. consumer wires
269
- * its own click handler). Applied once at construction.
270
- */
271
- scrimInteractive?: boolean;
272
- /**
273
- * Click on the scrim snaps to the first non-zero allowed snap (same target
274
- * as `autoCollapseAfter`). Default `false`. When enabled together with
275
- * `scrimInteractive: false`, engine auto-promotes pointer-events to `auto`
276
- * — otherwise the listener would never fire. Listener installs in
277
- * `attach()` and tears down in `destroy()`.
278
- */
279
- scrimTapToClose?: boolean;
280
- /**
281
- * Curated scrim look-and-feel preset — applied at construction BEFORE the
282
- * individual `scrimColor` / `scrimBlur` / `screenRange` / `scrimInteractive`
283
- * options, so a preset gives baseline values and any explicit option still
284
- * wins. Use this to opt into a named visual style without remembering each
285
- * underlying field. See `SCRIM_PRESETS` for the exact values per preset.
286
- */
287
- scrimPreset?: ScrimPreset;
288
- /**
289
- * Animation strategy / preset. The two **base kinds** are physics-driven
290
- * `"spring"` (default — carries drag velocity into the settle, feels
291
- * native) and time-driven `"tween"` (fixed-duration easing curve). Four
292
- * named **presets** layer on top, each prefilling `spring` / `duration`
293
- * / `easing` with a curated config — consumer-supplied `spring`,
294
- * `duration`, or `easing` still override the preset's defaults.
295
- *
296
- * Subjective feel:
297
- * - `"spring"` — default critical-damped spring. Native feel.
298
- * - `"tween"` — fixed-duration ease-out. Predictable timing.
299
- * - `"ios-spring"` — quick + slightly underdamped, like iOS modals
300
- * (`{ stiffness: 300, damping: 30, mass: 1 }`). Best for iOS-native
301
- * apps where users expect Apple's exact bounce signature.
302
- * - `"material-bounce"` — slower, more bounce, Material You feel
303
- * (`{ stiffness: 200, damping: 22, mass: 1 }`). Best for Android-
304
- * native apps and playful, brand-led UIs.
305
- * - `"linear"` — `tween` with `easeLinear` (no acceleration). Use when
306
- * the consumer wants explicit reduced-motion-friendly behaviour
307
- * without trading off the deterministic timing of a tween.
308
- * - `"snappy"` — fast tween (`duration: 180`, `easeOutQuint`). Best
309
- * for UI-heavy / dense apps that want IMMEDIATE response — file
310
- * pickers, chat composers, command palettes.
311
- */
312
- animation?: "spring" | "tween" | "ios-spring" | "material-bounce" | "linear" | "snappy";
313
- /** Spring config when `animation: "spring"`. Defaults: critical damping. */
314
- spring?: {
315
- stiffness?: number;
316
- damping?: number;
317
- mass?: number;
318
- };
319
- /**
320
- * Trap Tab focus inside the sheet when expanded. Default: false. When
321
- * true, focus is moved into the sheet on open (or to `initialFocus`),
322
- * Tab cycles within, and original focus is restored on close.
323
- */
324
- focusTrap?: boolean;
325
- /** Selector or HTMLElement to focus when the sheet opens (focusTrap only). */
326
- initialFocus?: string | HTMLElement;
327
- /** Listen for Escape key and close the sheet. Default: true. */
328
- closeOnEscape?: boolean;
329
- /** Lock body scroll when the sheet is non-closed. Default: true. */
330
- lockBodyScroll?: boolean;
331
- /**
332
- * Intercept the Android hardware back button (and browser back) — close the
333
- * sheet instead of navigating away. Pushes a no-op history entry on open.
334
- * Default: false (opt-in to avoid surprising consumers with their own
335
- * routing logic).
336
- */
337
- closeOnBack?: boolean;
338
- /**
339
- * Sync the sheet's open/close state with the URL. When set, opening the
340
- * sheet pushes `routedTo` onto `history` (`history.pushState({__bsRouted:
341
- * routedTo}, "", routedTo)`); closing the sheet calls `history.back()` if
342
- * the current entry was the one we pushed. Browser back (`popstate`) on
343
- * the routed entry programmatically calls `engine.close()`.
344
- *
345
- * Coexists with `closeOnBack` — if both are set, `routedTo` wins (the
346
- * structured version of the same idea). Skipped on SSR (no `window`).
347
- *
348
- * **Security:** the value is forwarded directly to `history.pushState` and
349
- * the URL bar reflects it. Do NOT pass user-controlled input here (e.g.
350
- * `searchParams.get('next')`) without a same-origin allowlist — although
351
- * `pushState` itself does not navigate to `javascript:` URLs, downstream
352
- * code that reads `location.href` (e.g. a "share this URL" button) would
353
- * become an XSS sink at the consumer site. Cross-origin URLs throw
354
- * `SecurityError`, which is silently caught.
355
- *
356
- * Default: undefined (off).
357
- */
358
- routedTo?: string;
359
- /**
360
- * Mark the sheet's siblings as `inert` while open — full-modal a11y. AT
361
- * and pointer events skip the rest of the page. Default: false. Enable
362
- * only when the sheet must hijack the entire surface (e.g. confirmation
363
- * dialog); leave false when the sheet is meant to coexist with chrome
364
- * outside it (search bar, tab controls, etc.).
365
- */
366
- inertSiblings?: boolean;
367
- /**
368
- * Persist active snap id to `localStorage[persistKey]` so the sheet returns
369
- * to its last state after a page reload. Skipped on SSR / private-browsing
370
- * / disabled-storage. The engine never deletes the entry — that's the
371
- * consumer's lifecycle decision (e.g. on logout).
372
- */
373
- persistKey?: string;
374
- /**
375
- * Auto-collapse to the first non-zero allowed snap (typically `minimized`)
376
- * after this many milliseconds of no drag/snap activity. Suppressed mid-
377
- * drag (finger still down) and when already at the target snap. Pass
378
- * `undefined` to disable.
379
- */
380
- autoCollapseAfter?: number;
381
- /**
382
- * Sister sheets that should shrink to their first non-zero allowed snap
383
- * when THIS sheet opens (closed → open transition). Use case: navigation
384
- * rail expands → bottom panel auto-minimizes. Set later via
385
- * `setLinkedSheets` if peers don't exist at construction time.
386
- *
387
- * The reaction is one-directional and fires only on the `open` event —
388
- * the linked sheet's resulting `snap` does not feed back into THIS sheet
389
- * because `open` is gated to closed→open transitions. Wiring sheets in a
390
- * cycle (A links B, B links A) is supported by this gate but only safe
391
- * as long as neither sheet is constructed already-open.
392
- */
393
- linkedSheets?: BottomSheetEngineLike[];
394
- /**
395
- * @experimental
396
- *
397
- * Wrap **programmatic** `snapTo()` calls in `document.startViewTransition`
398
- * so Chrome/Safari can run a smooth shared-element morph between snap
399
- * states. Default: `false`.
400
- *
401
- * Important constraints:
402
- * - **Programmatic snaps only.** Gesture-driven settles (drag → flick)
403
- * are NOT wrapped — view transitions are designed for discrete
404
- * state changes and would conflict with a 60fps spring update loop.
405
- * - **Final settle only.** The engine wraps the single discrete
406
- * "apply target size" call, not each frame of `animateTo`. When
407
- * view transitions are enabled, the spring/tween between frames
408
- * is bypassed and the browser drives the cross-fade instead.
409
- * - **No-op without browser support.** Falls back to the normal
410
- * animation path on browsers that don't ship `startViewTransition`
411
- * (Firefox at the time of writing) — opt-in is safe.
412
- *
413
- * Treat this as a **bleeding-edge** opt-in: visually verify on each
414
- * supported browser before relying on it in production.
415
- */
416
- viewTransitions?: boolean;
417
- };
418
-
419
- /**
420
- * Alias surfaced for `linkedSheets` and `Plugin.install`. Keeping this
421
- * separate from the class lets adapters fake the engine in tests and lets
422
- * future internal-only methods stay private without breaking plugin typing.
423
- */
424
- type BottomSheetEngineLike = BottomSheetEngine;
425
- /**
426
- * Transactional teardown scope passed to `Plugin.install` as the second
427
- * argument. A plugin that registers DOM listeners / timers BEFORE doing
428
- * something that might throw should push their cleanup callbacks into the
429
- * scope — engine drains the scope on install failure so partial side effects
430
- * don't leak. On successful install the scope is merged into the engine's
431
- * destroy-time TeardownStack, so plugins that ONLY use scope.add (and don't
432
- * return a teardown function) still get correct destroy() cleanup.
433
- *
434
- * Optional — plugins that don't accept the second argument keep working
435
- * unchanged. Use when install does multi-step setup that can fail mid-way.
436
- *
437
- * @example
438
- * ```ts
439
- * const myPlugin: Plugin = {
440
- * name: "analytics",
441
- * install: (engine, scope) => {
442
- * // Step 1 — listener that must be cleaned up if step 2 throws.
443
- * const off = engine.on("snap", payload => track("snap", payload));
444
- * scope.add(off);
445
- *
446
- * // Step 2 — risky work that might throw (network probe, feature
447
- * // detection, etc.). If it throws, engine drains `scope` and unsubs
448
- * // the listener registered above.
449
- * const observer = new ResizeObserver(() => {});
450
- * observer.observe(document.body);
451
- * scope.add(() => observer.disconnect());
452
- *
453
- * // Optional: return ONE additional cleanup. Both `scope.add` callbacks
454
- * // AND this returned teardown run on `engine.destroy()` (LIFO).
455
- * return () => track("plugin-destroyed");
456
- * },
457
- * };
458
- * ```
459
- */
460
- type TeardownScope = {
461
- /**
462
- * Register a cleanup callback. Non-functions (`null`/`undefined`/strings/
463
- * numbers/objects) are silently rejected — useful when a plugin's helper
464
- * returns `void | (() => void)` and you want to forward the result without
465
- * a runtime check.
466
- */
467
- add: (fn: (() => void) | null | undefined) => void;
468
- };
469
- /**
470
- * A plugin extends the engine via `engine.use(plugin)`. `install` runs once
471
- * at registration; the optional teardown function (returned from install)
472
- * is captured by the engine and invoked on `destroy()`. Plugins are free
473
- * to subscribe to events, monkey-patch methods, or hold their own state.
474
- *
475
- * **Transactional contract**: `install` should be transactional — if it
476
- * throws midway, any DOM listeners / timers / external state it set up
477
- * BEFORE the throw will leak (engine can't introspect arbitrary side
478
- * effects). Use the optional `scope` parameter to push partial cleanup
479
- * callbacks; the engine will drain `scope` if `install` throws, and merge
480
- * it into destroy-time teardowns on success.
481
- */
482
- type Plugin = {
483
- /** Human-readable identifier — surfaced in warnings and debug logs. */
484
- name: string;
485
- /**
486
- * Called once at `engine.use(plugin)` time. Return a function to run
487
- * during `destroy()` for cleanup (timers, listeners, observers).
488
- *
489
- * The optional `scope` argument lets the plugin register partial cleanups
490
- * before each side effect — engine rolls them back on install failure.
491
- */
492
- install: (engine: BottomSheetEngine, scope: TeardownScope) => void | (() => void);
493
- };
494
- type EngineState = {
495
- size: number;
496
- activeId: string;
497
- isDragging: boolean;
498
- isAnimating: boolean;
499
- progress: number;
500
- };
501
-
502
- type Listener<K extends keyof SheetEventMap> = (payload: SheetEventMap[K]) => void;
503
- /**
504
- * Framework-agnostic bottom-sheet engine.
505
- *
506
- * Owns the size value, snap-point math, gesture handling, and animation.
507
- * Renders nothing on its own — applies size to a host element via the chosen
508
- * `mode` axis. Adapters subscribe to `on(...)` events to mirror the state
509
- * back into their reactive system.
510
- */
511
- declare class BottomSheetEngine {
512
- private id;
513
- private element;
514
- private handle;
515
- private scrollContainer?;
516
- private backdrop?;
517
- private screenComponent?;
518
- private mode;
519
- private flickVelocity;
520
- private dragThreshold;
521
- private rubberBandEnabled;
522
- private scrim;
523
- private aria;
524
- private animation;
525
- private lifecycle;
526
- private closeOnBack;
527
- private routedTo;
528
- private snapPointsRaw;
529
- private snaps;
530
- private activeId;
531
- private size;
532
- private gesture;
533
- private rootEl;
534
- private destroyed;
535
- private currentViewTransition;
536
- private currentAbort;
537
- private sizeWriteSentinel;
538
- private progressWriteSentinel;
539
- private isTopSheet;
540
- private progressPayload;
541
- private dragContextBuf;
542
- private bus;
543
- private persistKey;
544
- private linkedSheets;
545
- private scrollCache;
546
- private teardowns;
547
- private transformTemplate;
548
- constructor(opts: EngineOptions);
549
- get state(): EngineState;
550
- on<K extends keyof SheetEventMap>(event: K, fn: Listener<K>): () => void;
551
- /**
552
- * Register a plugin. `install` runs synchronously; if it returns a function,
553
- * it's captured for teardown during `destroy()`. No-op if already destroyed
554
- * — installing into a torn-down engine would leak listeners.
555
- *
556
- * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
557
- * so a buggy plugin can't take the engine down with it: the engine stays
558
- * usable, sibling plugins still install, and the error surfaces in dev tools
559
- * on the next tick instead of being swallowed. Mirrors the per-fn isolation
560
- * model used for teardowns.
561
- *
562
- * Plugins that accept the second `scope` argument get transactional cleanup:
563
- * push partial-install cleanups via `scope.add(fn)` and the engine drains
564
- * them on throw, OR merges them into destroy-time teardowns on success.
565
- */
566
- use(plugin: Plugin): this;
567
- /**
568
- * Replace the linked-sheets list at runtime. Use when peer engines aren't
569
- * available at construction time (mounted later in the tree). The wiring
570
- * to the `open` event is independent of this list — it reads `linkedSheets`
571
- * lazily on emit, so this setter is a simple field swap.
572
- */
573
- setLinkedSheets(sheets: BottomSheetEngine[]): void;
574
- /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
575
- getAllowedIds(): string[];
576
- /**
577
- * Animate to a snap point by id. Resolves when the animation completes.
578
- *
579
- * Two call shapes:
580
- * ```ts
581
- * engine.snapTo("full"); // default velocity
582
- * engine.snapTo("full", 0.5); // px/ms initial velocity
583
- * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
584
- * ```
585
- *
586
- * When `opts.signal` is provided, calling `controller.abort()` cancels the
587
- * in-flight tween/spring and the returned promise resolves without firing
588
- * `snap`. The captured `AbortSignal` is checked after each await so external
589
- * aborts ride the same cycle invariant as internal ones (resize, destroy,
590
- * concurrent snapTo).
591
- */
592
- snapTo(id: string, velocityOrOpts?: number | {
593
- velocity?: number;
594
- signal?: AbortSignal;
595
- },
596
- /** Internal: skip the before-snap user gate. Used by setAllowed/setSnapPoints
597
- * for self-repair so a listener can't cancel our state-consistency moves. */
598
- _skipBeforeSnap?: boolean): Promise<void>;
599
- /**
600
- * Drive the sheet to an arbitrary px size, bypassing snap matching.
601
- * Useful for preview scrubs, animation chaining, or programmatic gestures.
602
- * Animates with the configured spring/tween. Resolves when the animation
603
- * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
604
- *
605
- * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
606
- * nor `snap` (the sheet isn't settling to a known snap). `progress` events
607
- * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
608
- */
609
- dragTo(targetSize: number, velocityOrOpts?: number | {
610
- velocity?: number;
611
- signal?: AbortSignal;
612
- }): Promise<void>;
613
- open(id?: string): Promise<void>;
614
- close(): Promise<void>;
615
- setAllowed(ids: string[], snap?: string): void;
616
- setBackdropRange(range: [number, number]): void;
617
- setScreenRange(range: [number, number]): void;
618
- setScrimColor(color: string | undefined | null): void;
619
- setScrimBlur(blur: string | undefined | null): void;
620
- setScrimInteractive(interactive: boolean): void;
621
- /**
622
- * Batch-update multiple scrim fields in one call. Apply order is:
623
- * 1. `preset` — fills baseline color/blur/range/interactive.
624
- * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
625
- * 3. `enabled` — flips the opacity contribution on/off.
626
- * 4. `tapToClose` — installs/tears the click listener.
627
- * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
628
- * 6. ONE `applySize` so the per-frame opacity stays consistent.
629
- *
630
- * Performance: collecting changes here (vs calling individual setters)
631
- * avoids repeated `applySize` calls during a multi-field swap. Setters
632
- * called here suppress their own internal `applySize` either by passing
633
- * through fast-paths (mode/enabled no-op when value unchanged) or by
634
- * being visual-only (color/blur/interactive don't influence opacity).
635
- */
636
- setScrim(opts: ScrimUpdate): void;
637
- setScrimMode(mode: "full" | "above-sheet" | "off"): void;
638
- setScrimTapToClose(enabled: boolean): void;
639
- setScrimEnabled(enabled: boolean): void;
640
- setScrimOverlay(opts: ScrimOverlayOptions): () => void;
641
- /**
642
- * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
643
- * Stable public API — internals (e.g. cached opacity sentinels) are NOT
644
- * exposed so consumers can't accidentally couple to ScrimController shape.
645
- */
646
- getScrimState(): {
647
- mode: "full" | "above-sheet" | "off";
648
- enabled: boolean;
649
- };
650
- setSnapPoints(points: EngineOptions["snapPoints"], allowed?: string[]): void;
651
- destroy(): void;
652
- private emit;
653
- /**
654
- * Abort the current cycle's signal and replace the controller. Returns the
655
- * fresh signal so the caller can capture it locally before any await — when
656
- * a future newCycle() / destroy aborts our controller, the captured signal
657
- * flips to aborted and post-await guards short-circuit.
658
- */
659
- private newCycle;
660
- /**
661
- * Fire `before-snap` and report whether a listener cancelled it. The
662
- * `cancel()` callback exposed to listeners is frozen after the synchronous
663
- * emit phase so async callers can't silently miss — they get a console
664
- * warning. Returns `true` when cancelled so callers can early-return /
665
- * spring-back without re-implementing the cancel/frozen pattern.
666
- */
667
- private emitBeforeSnap;
668
- private recomputeSnaps;
669
- /**
670
- * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
671
- * Engine wrapper kept so existing call sites don't all need updating;
672
- * resolver-level memoization survives across method invocations.
673
- */
674
- private getAllowedRange;
675
- /**
676
- * Write ARIA slider value attributes onto the handle so the engine itself
677
- * is the source of truth for screen-reader announcements. Called from the
678
- * constructor and from every code path that mutates `activeId` /
679
- * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
680
- * without it screen readers can't announce drag/snap progress to keyboard
681
- * + AT users (WCAG 4.1.2 Name, Role, Value).
682
- *
683
- * Engine does NOT set `role="slider"` itself — that's the adapter's or
684
- * consumer markup's responsibility. We only decorate with dynamic values.
685
- *
686
- * Empty allowed-list is malformed input (settable via `setAllowed([])`);
687
- * short-circuit to remove ARIA value attributes so screen readers see a
688
- * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
689
- * requires `valuemax > valuemin` for `role="slider"`).
690
- */
691
- private updateAriaSlider;
692
- private registerInStack;
693
- private attach;
694
- private attachFeatures;
695
- private settleAfterDrag;
696
- /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
697
- private haptic;
698
- private applySize;
699
- private computeProgress;
700
- private handleOpen;
701
- private handleClose;
702
- }
703
-
704
- export { BottomSheetEngine as B, type EngineState as E, type Plugin as P, type SnapPointDef as S, type TeardownScope as T, type SheetMode as a, type EngineOptions as b, type SnapPoint as c, type SheetEventMap as d, SCRIM_PRESETS as e, type ScrimOverlayOptions as f, type ScrimOverlayPosition as g, type ScrimPreset as h, type ScrimPresetConfig as i, type ScrimUpdate as j, type SnapId as k };