@surdeddd/bottom-sheet 0.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/dist/BottomSheetEngine-CvSVbo6c.d.cts +704 -0
  4. package/dist/BottomSheetEngine-DbO8rW_3.d.ts +704 -0
  5. package/dist/element.cjs +3442 -0
  6. package/dist/element.d.cts +88 -0
  7. package/dist/element.d.ts +88 -0
  8. package/dist/element.iife.global.js +3 -0
  9. package/dist/element.js +3442 -0
  10. package/dist/index.cjs +3693 -0
  11. package/dist/index.d.cts +184 -0
  12. package/dist/index.d.ts +184 -0
  13. package/dist/index.js +3669 -0
  14. package/dist/integrations/formik.cjs +27 -0
  15. package/dist/integrations/formik.d.cts +27 -0
  16. package/dist/integrations/formik.d.ts +27 -0
  17. package/dist/integrations/formik.js +25 -0
  18. package/dist/integrations/react-hook-form.cjs +28 -0
  19. package/dist/integrations/react-hook-form.d.cts +30 -0
  20. package/dist/integrations/react-hook-form.d.ts +30 -0
  21. package/dist/integrations/react-hook-form.js +26 -0
  22. package/dist/overlay.cjs +1058 -0
  23. package/dist/overlay.d.cts +282 -0
  24. package/dist/overlay.d.ts +282 -0
  25. package/dist/overlay.js +1053 -0
  26. package/dist/preact.cjs +2 -0
  27. package/dist/preact.d.cts +5 -0
  28. package/dist/preact.d.ts +5 -0
  29. package/dist/preact.js +2 -0
  30. package/dist/qwik.cjs +3046 -0
  31. package/dist/qwik.d.cts +55 -0
  32. package/dist/qwik.d.ts +55 -0
  33. package/dist/qwik.js +3044 -0
  34. package/dist/react.cjs +4162 -0
  35. package/dist/react.d.cts +301 -0
  36. package/dist/react.d.ts +301 -0
  37. package/dist/react.js +4156 -0
  38. package/dist/solid.cjs +3063 -0
  39. package/dist/solid.d.cts +66 -0
  40. package/dist/solid.d.ts +66 -0
  41. package/dist/solid.js +3061 -0
  42. package/dist/styles.css +508 -0
  43. package/dist/svelte.cjs +3200 -0
  44. package/dist/svelte.d.ts +89 -0
  45. package/dist/svelte.js +3177 -0
  46. package/dist/themes/ios.css +24 -0
  47. package/dist/themes/material.css +24 -0
  48. package/dist/themes/vercel.css +24 -0
  49. package/dist/transform-Brkf-wzo.d.cts +4 -0
  50. package/dist/transform-Brkf-wzo.d.ts +4 -0
  51. package/dist/vue.cjs +3361 -0
  52. package/dist/vue.d.ts +68 -0
  53. package/dist/vue.js +3358 -0
  54. package/package.json +288 -0
@@ -0,0 +1,1053 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/core/lifecycle/sheetStack.ts
6
+ var BASE_Z = 100;
7
+ var STEP = 10;
8
+ var SheetStack = class {
9
+ constructor() {
10
+ __publicField(this, "entries", []);
11
+ }
12
+ push(entry) {
13
+ this.entries.push(entry);
14
+ this.recompute();
15
+ return () => this.remove(entry.id);
16
+ }
17
+ remove(id) {
18
+ const idx = this.entries.findIndex((e) => e.id === id);
19
+ if (idx === -1) return;
20
+ this.entries.splice(idx, 1);
21
+ this.recompute();
22
+ }
23
+ size() {
24
+ return this.entries.length;
25
+ }
26
+ clear() {
27
+ this.entries = [];
28
+ }
29
+ recompute() {
30
+ this.entries.forEach((entry, i) => {
31
+ entry.setZIndex(BASE_Z + i * STEP);
32
+ entry.setIsTop(i === this.entries.length - 1);
33
+ });
34
+ }
35
+ };
36
+ var sheetStack = new SheetStack();
37
+
38
+ // src/core/primitives/transform.ts
39
+ var CLOSED_TRANSFORM = {
40
+ bottom: "translate3d(0, 100%, 0)",
41
+ top: "translate3d(0, -100%, 0)",
42
+ left: "translate3d(-100%, 0, 0)",
43
+ right: "translate3d(100%, 0, 0)"
44
+ };
45
+
46
+ // src/core/primitives/cssLength.ts
47
+ var probe = null;
48
+ var PROBE_ATTR = "data-bs-probe";
49
+ var ensureProbe = (axis) => {
50
+ if (typeof document === "undefined") return null;
51
+ if (!probe || !probe.isConnected) {
52
+ const existing = document.querySelector(`[${PROBE_ATTR}]`);
53
+ if (existing) {
54
+ probe = existing;
55
+ } else {
56
+ probe = document.createElement("div");
57
+ probe.setAttribute("aria-hidden", "true");
58
+ probe.setAttribute(PROBE_ATTR, "");
59
+ probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;contain:strict;left:0;top:0;width:0;height:0;";
60
+ document.body.appendChild(probe);
61
+ }
62
+ }
63
+ {
64
+ probe.style.width = "0";
65
+ probe.style.height = "";
66
+ }
67
+ return probe;
68
+ };
69
+ var viewportSize = (mode) => {
70
+ if (typeof window === "undefined") return 0;
71
+ return window.innerHeight ;
72
+ };
73
+ var PERCENT = /^(-?\d+(?:\.\d+)?)%$/;
74
+ var resolveSnap = (point, mode, measureFit) => {
75
+ if (typeof point === "number") return Math.max(0, point);
76
+ if (point === "full") return viewportSize();
77
+ if (point === "fit") return Math.max(0, 0);
78
+ const pctMatch = point.match(PERCENT);
79
+ if (pctMatch) {
80
+ const pct = parseFloat(pctMatch[1]) / 100;
81
+ return Math.max(0, viewportSize() * pct);
82
+ }
83
+ const el = ensureProbe();
84
+ if (!el) return 0;
85
+ {
86
+ el.style.height = point;
87
+ return Math.max(0, el.getBoundingClientRect().height);
88
+ }
89
+ };
90
+
91
+ // src/core/primitives/instance-id.ts
92
+ var counters = {};
93
+ function nextInstanceId(prefix) {
94
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
95
+ return `${prefix}-${crypto.randomUUID()}`;
96
+ }
97
+ counters[prefix] = (counters[prefix] ?? 0) + 1;
98
+ return `${prefix}-${counters[prefix]}`;
99
+ }
100
+
101
+ // src/core/primitives/event-bus.ts
102
+ function createEventBus() {
103
+ const listeners = /* @__PURE__ */ new Map();
104
+ return {
105
+ on(event, fn) {
106
+ let set = listeners.get(event);
107
+ if (!set) {
108
+ set = /* @__PURE__ */ new Set();
109
+ listeners.set(event, set);
110
+ }
111
+ set.add(fn);
112
+ return () => {
113
+ set.delete(fn);
114
+ };
115
+ },
116
+ emit(event, payload) {
117
+ const set = listeners.get(event);
118
+ if (!set) return;
119
+ for (const fn of [...set]) {
120
+ fn(payload);
121
+ }
122
+ },
123
+ listenerCount(event) {
124
+ return listeners.get(event)?.size ?? 0;
125
+ },
126
+ clear() {
127
+ listeners.clear();
128
+ }
129
+ };
130
+ }
131
+
132
+ // src/core/lifecycle/focusTrap.ts
133
+ var FOCUSABLE = [
134
+ "a[href]",
135
+ "area[href]",
136
+ "button:not([disabled])",
137
+ "input:not([disabled]):not([type='hidden'])",
138
+ "select:not([disabled])",
139
+ "textarea:not([disabled])",
140
+ "iframe",
141
+ "object",
142
+ "embed",
143
+ "[contenteditable='true']",
144
+ "[tabindex]:not([tabindex='-1'])"
145
+ ].join(",");
146
+ var isVisible = (el) => {
147
+ if (typeof el.checkVisibility === "function") {
148
+ return el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true });
149
+ }
150
+ return el.offsetParent !== null;
151
+ };
152
+ var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
153
+ (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
154
+ );
155
+ var installFocusTrap = (container, options = {}) => {
156
+ if (typeof document === "undefined") return () => {
157
+ };
158
+ const previouslyFocused = document.activeElement;
159
+ const focusInitial = () => {
160
+ const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
161
+ initial?.focus({ preventScroll: true });
162
+ };
163
+ focusInitial();
164
+ const handleKey = (e) => {
165
+ if (e.key === "Escape" && options.onEscape) {
166
+ options.onEscape();
167
+ return;
168
+ }
169
+ if (e.key !== "Tab") return;
170
+ const list = focusables(container);
171
+ if (list.length === 0) {
172
+ e.preventDefault();
173
+ return;
174
+ }
175
+ const first = list[0];
176
+ const last = list[list.length - 1];
177
+ const active = document.activeElement;
178
+ if (e.shiftKey && active === first) {
179
+ e.preventDefault();
180
+ last.focus();
181
+ } else if (!e.shiftKey && active === last) {
182
+ e.preventDefault();
183
+ first.focus();
184
+ }
185
+ };
186
+ const handleFocusIn = (e) => {
187
+ const target = e.target;
188
+ if (target && !container.contains(target)) {
189
+ const list = focusables(container);
190
+ list[0]?.focus({ preventScroll: true });
191
+ }
192
+ };
193
+ document.addEventListener("keydown", handleKey, true);
194
+ document.addEventListener("focusin", handleFocusIn, true);
195
+ let released = false;
196
+ return () => {
197
+ if (released) return;
198
+ released = true;
199
+ document.removeEventListener("keydown", handleKey, true);
200
+ document.removeEventListener("focusin", handleFocusIn, true);
201
+ if (previouslyFocused && document.contains(previouslyFocused)) {
202
+ previouslyFocused.focus({ preventScroll: true });
203
+ } else {
204
+ const fallback = document.querySelector("[data-bs-restore-fallback]");
205
+ fallback?.focus({ preventScroll: true });
206
+ }
207
+ };
208
+ };
209
+
210
+ // src/core/lifecycle/scrollLock.ts
211
+ var lockCount = 0;
212
+ var savedStyles = null;
213
+ var lockBodyScroll = () => {
214
+ if (typeof document === "undefined") return () => {
215
+ };
216
+ if (lockCount === 0) {
217
+ const body = document.body;
218
+ savedStyles = {
219
+ overflow: body.style.overflow,
220
+ position: body.style.position,
221
+ top: body.style.top,
222
+ width: body.style.width,
223
+ scrollY: window.scrollY
224
+ };
225
+ body.style.overflow = "hidden";
226
+ body.style.position = "fixed";
227
+ body.style.top = `-${savedStyles.scrollY}px`;
228
+ body.style.width = "100%";
229
+ }
230
+ lockCount++;
231
+ let released = false;
232
+ return () => {
233
+ if (released) return;
234
+ released = true;
235
+ lockCount = Math.max(0, lockCount - 1);
236
+ if (lockCount === 0 && savedStyles) {
237
+ const body = document.body;
238
+ body.style.overflow = savedStyles.overflow;
239
+ body.style.position = savedStyles.position;
240
+ body.style.top = savedStyles.top;
241
+ body.style.width = savedStyles.width;
242
+ window.scrollTo(0, savedStyles.scrollY);
243
+ savedStyles = null;
244
+ }
245
+ };
246
+ };
247
+
248
+ // src/core/features/inert-siblings.ts
249
+ function createInertSiblings(rootProvider) {
250
+ const tracked = [];
251
+ return {
252
+ apply() {
253
+ if (typeof document === "undefined") return;
254
+ const root = rootProvider();
255
+ const parent = root.parentElement ?? document.body;
256
+ for (const child of Array.from(parent.children)) {
257
+ if (child === root) continue;
258
+ if (child.hasAttribute("inert")) continue;
259
+ child.setAttribute("inert", "");
260
+ tracked.push(child);
261
+ }
262
+ },
263
+ remove() {
264
+ for (const el of tracked) el.removeAttribute("inert");
265
+ tracked.length = 0;
266
+ }
267
+ };
268
+ }
269
+
270
+ // src/core/controllers/lifecycle-controller.ts
271
+ var LifecycleController = class {
272
+ constructor(deps, opts) {
273
+ __publicField(this, "element");
274
+ __publicField(this, "closeFn");
275
+ __publicField(this, "focusTrapEnabled");
276
+ __publicField(this, "initialFocus");
277
+ __publicField(this, "closeOnEscape");
278
+ __publicField(this, "bodyScrollLockEnabled");
279
+ __publicField(this, "inertSiblingsEnabled");
280
+ __publicField(this, "inertSiblings");
281
+ __publicField(this, "shouldApplyInertSiblings");
282
+ __publicField(this, "returnFocus");
283
+ /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
284
+ __publicField(this, "releaseFocusTrap", null);
285
+ __publicField(this, "releaseScrollLock", null);
286
+ /** True between install() and release(). Guards release()/destroy() from
287
+ * re-running side effects (notably `returnFocus.focus()`) on already-
288
+ * released or never-installed instances. */
289
+ __publicField(this, "installed", false);
290
+ __publicField(this, "destroyed", false);
291
+ this.element = deps.element;
292
+ this.closeFn = deps.close;
293
+ this.focusTrapEnabled = opts.focusTrap ?? false;
294
+ this.initialFocus = opts.initialFocus;
295
+ this.closeOnEscape = opts.closeOnEscape ?? true;
296
+ this.bodyScrollLockEnabled = opts.lockBodyScroll ?? true;
297
+ this.inertSiblingsEnabled = opts.inertSiblings ?? false;
298
+ this.shouldApplyInertSiblings = opts.shouldApplyInertSiblings ?? (() => true);
299
+ this.returnFocus = opts.returnFocus;
300
+ this.inertSiblings = createInertSiblings(() => this.element);
301
+ }
302
+ /**
303
+ * Run the open-time install steps. Idempotent: each step checks its
304
+ * existing teardown ref so a re-open during a half-cancelled close
305
+ * doesn't double-install. Sets `installed=true` BEFORE the first side
306
+ * effect so a partial throw (detached / shadow-DOM target) leaves the
307
+ * caller's `release()` rollback path armed to undo whatever did succeed.
308
+ * No-op when destroyed.
309
+ */
310
+ install() {
311
+ if (this.destroyed) return;
312
+ this.installed = true;
313
+ if (this.bodyScrollLockEnabled && !this.releaseScrollLock) {
314
+ this.releaseScrollLock = lockBodyScroll();
315
+ }
316
+ if (this.focusTrapEnabled && !this.releaseFocusTrap) {
317
+ this.releaseFocusTrap = installFocusTrap(this.element, {
318
+ initialFocus: this.initialFocus,
319
+ onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
320
+ });
321
+ }
322
+ if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
323
+ this.inertSiblings.apply();
324
+ }
325
+ }
326
+ /**
327
+ * Update the post-release focus target. Overlay re-binds this when the
328
+ * consumer changes `returnFocus` after construction; the new value applies
329
+ * on the next `release()`. No-op when destroyed.
330
+ */
331
+ setReturnFocus(target) {
332
+ if (this.destroyed) return;
333
+ this.returnFocus = target;
334
+ }
335
+ /**
336
+ * Release all install-time teardowns. Short-circuits when not installed,
337
+ * so `close() → destroy()` (each routing through here) only restores
338
+ * focus once. The `installed` guard lets `inertSiblings.remove()` and
339
+ * the focus-trap/scroll-lock teardown nullings stay simple — they were
340
+ * already idempotent, but `returnFocus.focus()` was firing on every call.
341
+ */
342
+ release() {
343
+ if (!this.installed) return;
344
+ this.installed = false;
345
+ this.releaseFocusTrap?.();
346
+ this.releaseFocusTrap = null;
347
+ this.releaseScrollLock?.();
348
+ this.releaseScrollLock = null;
349
+ this.inertSiblings.remove();
350
+ if (this.returnFocus) {
351
+ const target = typeof this.returnFocus === "function" ? this.returnFocus() : this.returnFocus;
352
+ target?.focus?.();
353
+ }
354
+ }
355
+ /**
356
+ * Final teardown — releases any still-installed listeners and seals the
357
+ * controller. After destroy(), install() is a no-op and setReturnFocus()
358
+ * is rejected, so post-destroy mutations can't queue up a stale focus
359
+ * restore.
360
+ */
361
+ destroy() {
362
+ this.release();
363
+ this.destroyed = true;
364
+ }
365
+ };
366
+
367
+ // src/core/overlay.ts
368
+ var OVERLAY_PRESETS = Object.freeze({
369
+ sheet: Object.freeze({
370
+ enterAnimation: "slide",
371
+ exitAnimation: "slide",
372
+ backdropOpacity: 0.5,
373
+ backdropFilter: void 0,
374
+ swipeToClose: true
375
+ }),
376
+ dialog: Object.freeze({
377
+ enterAnimation: "scale",
378
+ exitAnimation: "scale",
379
+ backdropOpacity: 0.6,
380
+ backdropFilter: "blur(8px)",
381
+ swipeToClose: false
382
+ }),
383
+ sidebar: Object.freeze({
384
+ enterAnimation: "slide",
385
+ exitAnimation: "slide",
386
+ backdropOpacity: 0.4,
387
+ backdropFilter: void 0,
388
+ swipeToClose: true
389
+ }),
390
+ toast: Object.freeze({
391
+ enterAnimation: "fade",
392
+ exitAnimation: "fade",
393
+ backdropOpacity: 0,
394
+ backdropFilter: void 0,
395
+ swipeToClose: false
396
+ })
397
+ });
398
+ var closedTransform = (edge) => CLOSED_TRANSFORM[edge];
399
+ var closedTransformFor = (edge, animation, peekPx) => {
400
+ switch (animation) {
401
+ case "fade":
402
+ return "translate3d(0, 0, 0)";
403
+ case "scale":
404
+ return "translate3d(0, 0, 0) scale(0.94)";
405
+ case "slide":
406
+ default:
407
+ if (peekPx !== void 0 && edge === "bottom") {
408
+ return `translate3d(0, calc(100% - ${peekPx}px), 0)`;
409
+ }
410
+ return closedTransform(edge);
411
+ }
412
+ };
413
+ var openTransformFor = (animation) => animation === "scale" ? "translate3d(0, 0, 0) scale(1)" : "translate3d(0, 0, 0)";
414
+ var OverlayEngine = class {
415
+ constructor(opts) {
416
+ __publicField(this, "id", nextInstanceId("ovl"));
417
+ __publicField(this, "element");
418
+ __publicField(this, "backdrop");
419
+ __publicField(this, "edge");
420
+ __publicField(this, "closeOnEscape");
421
+ __publicField(this, "closeOnBackdropEnabled");
422
+ __publicField(this, "focusTrapEnabled");
423
+ __publicField(this, "initialFocus");
424
+ __publicField(this, "bodyScrollLockEnabled");
425
+ __publicField(this, "inertSiblingsEnabled");
426
+ __publicField(this, "closeOnBackEnabled");
427
+ __publicField(this, "duration");
428
+ __publicField(this, "enterEasing");
429
+ __publicField(this, "exitEasing");
430
+ __publicField(this, "backdropOpacity");
431
+ __publicField(this, "backdropFilter");
432
+ // Definite-assignment: written via assignSwipeFromConfig in the
433
+ // constructor — TS can't follow the helper, mark the assertion explicit.
434
+ __publicField(this, "swipeToCloseEnabled");
435
+ __publicField(this, "swipeThreshold");
436
+ __publicField(this, "swipeVelocityThreshold");
437
+ __publicField(this, "enterAnimation");
438
+ __publicField(this, "exitAnimation");
439
+ __publicField(this, "mountTarget");
440
+ __publicField(this, "originalParent", null);
441
+ __publicField(this, "originalNextSibling", null);
442
+ __publicField(this, "mounted", false);
443
+ __publicField(this, "peekRaw");
444
+ __publicField(this, "peekPx");
445
+ __publicField(this, "closeOnOutsidePointerEnabled");
446
+ __publicField(this, "detachOutsidePointer", null);
447
+ __publicField(this, "detachSwipe", null);
448
+ __publicField(this, "onOpenCb");
449
+ __publicField(this, "onCloseCb");
450
+ __publicField(this, "isOpen_", false);
451
+ __publicField(this, "isAnimating_", false);
452
+ __publicField(this, "destroyed", false);
453
+ // Tracks whether setOverlayChildren ever populated the panel — lets
454
+ // clearOverlayChildren skip the DOM mutation when nothing was injected
455
+ // (so the field also acts as a zero-allocation feature flag).
456
+ __publicField(this, "hasInjectedChildren", false);
457
+ // Bumped on every open/close. awaitTransition captures it and ignores
458
+ // transitionend/timeout fired by older cycles — prevents cross-cycle
459
+ // listener bleed during rapid toggling.
460
+ __publicField(this, "cycleNonce", 0);
461
+ __publicField(this, "bus", createEventBus());
462
+ __publicField(this, "detachEscape", null);
463
+ __publicField(this, "detachBackdrop", null);
464
+ __publicField(this, "detachHardwareBack", null);
465
+ __publicField(this, "releaseStackEntry", null);
466
+ __publicField(this, "lifecycle");
467
+ this.element = opts.element;
468
+ this.backdrop = opts.backdrop;
469
+ this.edge = opts.edge ?? "bottom";
470
+ this.closeOnEscape = opts.closeOnEscape ?? true;
471
+ this.closeOnBackdropEnabled = opts.closeOnBackdrop ?? true;
472
+ this.focusTrapEnabled = opts.focusTrap ?? true;
473
+ this.initialFocus = opts.initialFocus;
474
+ this.bodyScrollLockEnabled = opts.lockBodyScroll ?? true;
475
+ this.inertSiblingsEnabled = opts.inertSiblings ?? false;
476
+ this.closeOnBackEnabled = opts.closeOnBack ?? false;
477
+ this.duration = opts.duration ?? 320;
478
+ this.enterEasing = opts.enterEasing ?? "cubic-bezier(0.29, 1.24, 0.84, 0.99)";
479
+ this.exitEasing = opts.exitEasing ?? "cubic-bezier(0.4, 0, 1, 1)";
480
+ const preset = opts.preset ? OVERLAY_PRESETS[opts.preset] : void 0;
481
+ this.backdropOpacity = opts.backdropOpacity ?? preset?.backdropOpacity ?? 1;
482
+ this.backdropFilter = opts.backdropFilter ?? preset?.backdropFilter ?? void 0;
483
+ const swipeRaw = opts.swipeToClose !== void 0 ? opts.swipeToClose : preset?.swipeToClose;
484
+ this.assignSwipeFromConfig(swipeRaw);
485
+ this.enterAnimation = opts.enterAnimation ?? preset?.enterAnimation ?? "slide";
486
+ this.exitAnimation = opts.exitAnimation ?? preset?.exitAnimation ?? this.enterAnimation;
487
+ this.mountTarget = opts.mountTo;
488
+ this.peekRaw = opts.peek;
489
+ this.closeOnOutsidePointerEnabled = opts.closeOnOutsidePointer ?? false;
490
+ this.onOpenCb = opts.onOpen;
491
+ this.onCloseCb = opts.onClose;
492
+ if (this.peekRaw !== void 0 && this.edge !== "bottom") {
493
+ console.warn(
494
+ "[OverlayEngine] `peek` is only supported on edge: 'bottom' \u2014 ignoring."
495
+ );
496
+ this.peekRaw = void 0;
497
+ }
498
+ if (this.peekRaw !== void 0) {
499
+ this.peekPx = this.resolvePeekPx(this.peekRaw);
500
+ }
501
+ this.lifecycle = new LifecycleController(
502
+ {
503
+ element: this.element,
504
+ close: () => this.close("escape")
505
+ },
506
+ {
507
+ focusTrap: this.focusTrapEnabled,
508
+ initialFocus: this.initialFocus,
509
+ closeOnEscape: this.closeOnEscape,
510
+ lockBodyScroll: this.bodyScrollLockEnabled,
511
+ inertSiblings: this.inertSiblingsEnabled,
512
+ // Overlay-specific guard: only paint body siblings inert when the
513
+ // overlay element is actually a body descendant. Detached / shadow-
514
+ // DOM / portal overlays would otherwise lock unrelated trees.
515
+ shouldApplyInertSiblings: () => {
516
+ const bodyChildren = Array.from(document.body.children);
517
+ return bodyChildren.some(
518
+ (c) => c === this.element || c.contains(this.element)
519
+ );
520
+ },
521
+ returnFocus: opts.returnFocus
522
+ }
523
+ );
524
+ this.applyClosedStyles();
525
+ this.registerInStack();
526
+ if (opts.initialOpen) void this.open();
527
+ }
528
+ get state() {
529
+ return { isOpen: this.isOpen_, isAnimating: this.isAnimating_ };
530
+ }
531
+ on(event, fn) {
532
+ return this.bus.on(event, fn);
533
+ }
534
+ open() {
535
+ if (this.destroyed || this.isOpen_) return Promise.resolve();
536
+ this.maybeMount();
537
+ this.emit("before-open", void 0);
538
+ this.isOpen_ = true;
539
+ this.isAnimating_ = true;
540
+ this.element.removeAttribute("hidden");
541
+ this.element.setAttribute("data-state", "entering");
542
+ if (this.focusTrapEnabled) {
543
+ this.element.setAttribute("aria-modal", "true");
544
+ }
545
+ const transitionStr = `transform ${this.duration}ms ${this.enterEasing}, opacity ${this.duration}ms ease-out`;
546
+ this.element.style.transition = transitionStr;
547
+ if (this.backdrop) {
548
+ this.backdrop.style.transition = `opacity ${this.duration}ms ease-out`;
549
+ }
550
+ const cycle = ++this.cycleNonce;
551
+ return new Promise((resolve) => {
552
+ requestAnimationFrame(() => {
553
+ if (this.destroyed || cycle !== this.cycleNonce) return resolve();
554
+ requestAnimationFrame(() => {
555
+ if (this.destroyed || cycle !== this.cycleNonce) return resolve();
556
+ this.element.style.transform = openTransformFor(this.enterAnimation);
557
+ this.element.style.opacity = "1";
558
+ if (this.backdrop) {
559
+ this.backdrop.style.opacity = String(this.backdropOpacity);
560
+ this.backdrop.style.pointerEvents = "auto";
561
+ if (this.backdropFilter) {
562
+ this.backdrop.style.backdropFilter = this.backdropFilter;
563
+ this.backdrop.style.webkitBackdropFilter = this.backdropFilter;
564
+ }
565
+ }
566
+ try {
567
+ this.installInteractiveListeners();
568
+ } catch (err) {
569
+ this.releaseInteractiveListeners();
570
+ this.isOpen_ = false;
571
+ this.isAnimating_ = false;
572
+ this.element.style.transform = closedTransformFor(
573
+ this.edge,
574
+ this.enterAnimation,
575
+ this.peekPx
576
+ );
577
+ this.element.style.opacity = "0";
578
+ if (this.backdrop) {
579
+ this.backdrop.style.opacity = "0";
580
+ this.backdrop.style.pointerEvents = "none";
581
+ if (this.backdropFilter) {
582
+ this.backdrop.style.backdropFilter = "";
583
+ this.backdrop.style.webkitBackdropFilter = "";
584
+ }
585
+ }
586
+ this.element.setAttribute("data-state", "closed");
587
+ this.element.setAttribute("hidden", "");
588
+ if (this.focusTrapEnabled) {
589
+ this.element.removeAttribute("aria-modal");
590
+ }
591
+ queueMicrotask(() => {
592
+ throw err;
593
+ });
594
+ return resolve();
595
+ }
596
+ this.awaitTransition(cycle).then(() => {
597
+ if (this.destroyed || cycle !== this.cycleNonce) return resolve();
598
+ this.isAnimating_ = false;
599
+ this.element.setAttribute("data-state", "open");
600
+ this.emit("open", void 0);
601
+ this.onOpenCb?.();
602
+ resolve();
603
+ });
604
+ });
605
+ });
606
+ });
607
+ }
608
+ close(reason = "programmatic") {
609
+ if (this.destroyed || !this.isOpen_) return Promise.resolve();
610
+ this.emit("before-close", void 0);
611
+ this.isOpen_ = false;
612
+ this.isAnimating_ = true;
613
+ this.element.setAttribute("data-state", "exiting");
614
+ const transitionStr = `transform ${this.duration}ms ${this.exitEasing}, opacity ${this.duration}ms ease-in`;
615
+ this.element.style.transition = transitionStr;
616
+ if (this.backdrop) {
617
+ this.backdrop.style.transition = `opacity ${this.duration}ms ease-in`;
618
+ }
619
+ const cycle = ++this.cycleNonce;
620
+ return new Promise((resolve) => {
621
+ requestAnimationFrame(() => {
622
+ if (this.destroyed || cycle !== this.cycleNonce) return resolve();
623
+ this.element.style.transform = closedTransformFor(
624
+ this.edge,
625
+ this.exitAnimation,
626
+ this.peekPx
627
+ );
628
+ this.element.style.opacity = "0";
629
+ if (this.backdrop) {
630
+ this.backdrop.style.opacity = "0";
631
+ this.backdrop.style.pointerEvents = "none";
632
+ if (this.backdropFilter) {
633
+ this.backdrop.style.backdropFilter = "";
634
+ this.backdrop.style.webkitBackdropFilter = "";
635
+ }
636
+ }
637
+ this.releaseInteractiveListeners();
638
+ this.awaitTransition(cycle).then(() => {
639
+ if (this.destroyed || cycle !== this.cycleNonce) return resolve();
640
+ this.isAnimating_ = false;
641
+ if (this.peekPx === void 0) {
642
+ this.element.setAttribute("hidden", "");
643
+ }
644
+ this.element.setAttribute("data-state", "closed");
645
+ if (this.focusTrapEnabled) {
646
+ this.element.removeAttribute("aria-modal");
647
+ }
648
+ this.emit("close", { reason });
649
+ this.onCloseCb?.();
650
+ resolve();
651
+ });
652
+ });
653
+ });
654
+ }
655
+ toggle() {
656
+ if (this.destroyed) return Promise.resolve();
657
+ return this.isOpen_ ? this.close() : this.open();
658
+ }
659
+ destroy() {
660
+ this.destroyed = true;
661
+ this.cycleNonce++;
662
+ this.bus.clear();
663
+ this.releaseInteractiveListeners();
664
+ this.releaseStackEntry?.();
665
+ this.releaseStackEntry = null;
666
+ this.restoreMount();
667
+ }
668
+ /**
669
+ * Runtime visual setter — opacity is applied immediately when open so a
670
+ * live-controls panel can preview the change without re-opening. When
671
+ * closed we only stash the field; next open() picks it up so we don't
672
+ * mutate hidden DOM (avoids paint thrash on detached panels).
673
+ */
674
+ setBackdropOpacity(opacity) {
675
+ if (this.destroyed) return;
676
+ this.backdropOpacity = opacity;
677
+ if (this.isOpen_ && this.backdrop) {
678
+ this.backdrop.style.opacity = String(opacity);
679
+ }
680
+ }
681
+ /**
682
+ * Runtime visual setter for `backdrop-filter`. `undefined` clears the
683
+ * filter (mirrors the close() behaviour). Defers to next open() when the
684
+ * overlay is currently closed.
685
+ */
686
+ setBackdropFilter(filter) {
687
+ if (this.destroyed) return;
688
+ this.backdropFilter = filter;
689
+ if (!this.isOpen_ || !this.backdrop) return;
690
+ const style = this.backdrop.style;
691
+ if (filter) {
692
+ this.backdrop.style.backdropFilter = filter;
693
+ style.webkitBackdropFilter = filter;
694
+ } else {
695
+ this.backdrop.style.backdropFilter = "";
696
+ style.webkitBackdropFilter = "";
697
+ }
698
+ }
699
+ /**
700
+ * Toggle / reconfigure swipe-to-close at runtime. Re-resolves the flat
701
+ * threshold fields, then installs or tears down the pointer listener so
702
+ * the change applies to the CURRENT open cycle. Idempotent: safe to call
703
+ * with the same config repeatedly.
704
+ */
705
+ setSwipeToClose(config) {
706
+ if (this.destroyed) return;
707
+ const wasEnabled = this.swipeToCloseEnabled;
708
+ this.assignSwipeFromConfig(config);
709
+ if (!this.isOpen_) return;
710
+ if (this.swipeToCloseEnabled && !this.detachSwipe) {
711
+ this.detachSwipe = this.installSwipeToClose();
712
+ } else if (!this.swipeToCloseEnabled && wasEnabled && this.detachSwipe) {
713
+ this.detachSwipe();
714
+ this.detachSwipe = null;
715
+ }
716
+ }
717
+ /** Animation setters only stage the field — applied on next open/close. */
718
+ setEnterAnimation(animation) {
719
+ if (this.destroyed) return;
720
+ this.enterAnimation = animation;
721
+ }
722
+ setExitAnimation(animation) {
723
+ if (this.destroyed) return;
724
+ this.exitAnimation = animation;
725
+ }
726
+ /** Updates the field used at next close() — no immediate side effect. */
727
+ setReturnFocus(target) {
728
+ if (this.destroyed) return;
729
+ this.lifecycle.setReturnFocus(target);
730
+ }
731
+ /**
732
+ * Batch update — preset first, then individual overrides. Applied as a
733
+ * single DOM-write phase when open so live-controls don't trigger
734
+ * cascading reflows. Safe to call when closed (defers to next open).
735
+ */
736
+ setOverlay(opts) {
737
+ if (this.destroyed) return;
738
+ const preset = opts.preset ? OVERLAY_PRESETS[opts.preset] : void 0;
739
+ const merged = preset ? {
740
+ backdropOpacity: preset.backdropOpacity,
741
+ // Preset's `undefined` filter means "no filter" — surface as null
742
+ // so the merged step clears the field instead of inheriting old.
743
+ backdropFilter: preset.backdropFilter === void 0 ? null : preset.backdropFilter,
744
+ swipeToClose: preset.swipeToClose,
745
+ enterAnimation: preset.enterAnimation,
746
+ exitAnimation: preset.exitAnimation
747
+ } : {};
748
+ if (opts.backdropOpacity !== void 0) {
749
+ merged.backdropOpacity = opts.backdropOpacity;
750
+ }
751
+ if (opts.backdropFilter !== void 0) {
752
+ merged.backdropFilter = opts.backdropFilter;
753
+ }
754
+ if (opts.swipeToClose !== void 0) {
755
+ merged.swipeToClose = opts.swipeToClose;
756
+ }
757
+ if (opts.enterAnimation !== void 0) {
758
+ merged.enterAnimation = opts.enterAnimation;
759
+ }
760
+ if (opts.exitAnimation !== void 0) {
761
+ merged.exitAnimation = opts.exitAnimation;
762
+ }
763
+ if (merged.enterAnimation !== void 0) {
764
+ this.enterAnimation = merged.enterAnimation;
765
+ }
766
+ if (merged.exitAnimation !== void 0) {
767
+ this.exitAnimation = merged.exitAnimation;
768
+ }
769
+ if (merged.swipeToClose !== void 0) {
770
+ this.setSwipeToClose(merged.swipeToClose);
771
+ }
772
+ if (merged.backdropOpacity !== void 0) {
773
+ this.backdropOpacity = merged.backdropOpacity;
774
+ }
775
+ if (merged.backdropFilter !== void 0) {
776
+ this.backdropFilter = merged.backdropFilter === null ? void 0 : merged.backdropFilter;
777
+ }
778
+ if (this.isOpen_ && this.backdrop) {
779
+ if (merged.backdropOpacity !== void 0) {
780
+ this.backdrop.style.opacity = String(this.backdropOpacity);
781
+ }
782
+ if (merged.backdropFilter !== void 0) {
783
+ const style = this.backdrop.style;
784
+ if (this.backdropFilter) {
785
+ this.backdrop.style.backdropFilter = this.backdropFilter;
786
+ style.webkitBackdropFilter = this.backdropFilter;
787
+ } else {
788
+ this.backdrop.style.backdropFilter = "";
789
+ style.webkitBackdropFilter = "";
790
+ }
791
+ }
792
+ }
793
+ if (opts.children !== void 0) {
794
+ if (opts.children === null) {
795
+ this.clearOverlayChildren();
796
+ } else {
797
+ this.setOverlayChildren(opts.children);
798
+ }
799
+ }
800
+ }
801
+ /**
802
+ * Runtime children injection — replace the panel's contents without
803
+ * unmounting the engine. `replaceChildren()` is a single atomic DOM op,
804
+ * so callers can swap content rapidly without intermediate flicker.
805
+ * Function form is resolved exactly ONCE per call (cheap lazy content).
806
+ */
807
+ setOverlayChildren(children) {
808
+ if (this.destroyed) return;
809
+ const node = typeof children === "function" ? children() : children;
810
+ this.element.replaceChildren(node);
811
+ this.hasInjectedChildren = true;
812
+ }
813
+ /**
814
+ * Clear injected children. No-op when nothing was injected so consumers
815
+ * who never used setOverlayChildren can call this defensively without
816
+ * paying for a DOM mutation.
817
+ */
818
+ clearOverlayChildren() {
819
+ if (this.destroyed || !this.hasInjectedChildren) return;
820
+ this.element.replaceChildren();
821
+ this.hasInjectedChildren = false;
822
+ }
823
+ // Shared swipe-config normalizer — used by constructor, setSwipeToClose,
824
+ // and setOverlay so the union/object shape is decoded in exactly one place.
825
+ assignSwipeFromConfig(swipe) {
826
+ if (typeof swipe === "object" && swipe !== null) {
827
+ this.swipeToCloseEnabled = swipe.enabled ?? true;
828
+ this.swipeThreshold = swipe.threshold ?? 0.3;
829
+ this.swipeVelocityThreshold = swipe.velocityThreshold ?? 0.5;
830
+ } else {
831
+ this.swipeToCloseEnabled = swipe ?? false;
832
+ this.swipeThreshold = 0.3;
833
+ this.swipeVelocityThreshold = 0.5;
834
+ }
835
+ }
836
+ emit(event, payload) {
837
+ this.bus.emit(event, payload);
838
+ }
839
+ resolvePeekPx(raw) {
840
+ if (typeof raw === "number") return Math.max(0, raw);
841
+ try {
842
+ return Math.max(0, resolveSnap(raw, "bottom"));
843
+ } catch {
844
+ return 0;
845
+ }
846
+ }
847
+ maybeMount() {
848
+ if (this.mounted || !this.mountTarget || this.mountTarget === "parent") {
849
+ this.mounted = true;
850
+ return;
851
+ }
852
+ const target = this.mountTarget === "body" ? document.body : this.mountTarget;
853
+ if (!target) {
854
+ this.mounted = true;
855
+ return;
856
+ }
857
+ this.originalParent = this.element.parentNode;
858
+ this.originalNextSibling = this.element.nextSibling;
859
+ target.appendChild(this.element);
860
+ this.mounted = true;
861
+ }
862
+ restoreMount() {
863
+ if (!this.originalParent) return;
864
+ try {
865
+ if (this.originalNextSibling && this.originalNextSibling.parentNode === this.originalParent) {
866
+ this.originalParent.insertBefore(this.element, this.originalNextSibling);
867
+ } else {
868
+ this.originalParent.appendChild(this.element);
869
+ }
870
+ } catch {
871
+ }
872
+ this.originalParent = null;
873
+ this.originalNextSibling = null;
874
+ }
875
+ applyClosedStyles() {
876
+ this.element.style.willChange = "transform, opacity";
877
+ this.element.style.transform = closedTransformFor(
878
+ this.edge,
879
+ this.enterAnimation,
880
+ this.peekPx
881
+ );
882
+ this.element.style.opacity = this.peekPx !== void 0 ? "1" : "0";
883
+ if (this.peekPx === void 0) {
884
+ this.element.setAttribute("hidden", "");
885
+ }
886
+ this.element.setAttribute("data-state", "closed");
887
+ if (this.backdrop) {
888
+ this.backdrop.style.opacity = "0";
889
+ this.backdrop.style.pointerEvents = "none";
890
+ this.backdrop.style.willChange = "opacity";
891
+ }
892
+ }
893
+ awaitTransition(cycle) {
894
+ return new Promise((resolve) => {
895
+ let done = false;
896
+ const finish = () => {
897
+ if (done) return;
898
+ done = true;
899
+ this.element.removeEventListener("transitionend", onEnd);
900
+ clearTimeout(timer);
901
+ resolve();
902
+ };
903
+ const onEnd = (e) => {
904
+ if (cycle !== this.cycleNonce) {
905
+ finish();
906
+ return;
907
+ }
908
+ if (e.propertyName === "transform") finish();
909
+ };
910
+ this.element.addEventListener("transitionend", onEnd);
911
+ const timer = window.setTimeout(finish, this.duration + 50);
912
+ });
913
+ }
914
+ installInteractiveListeners() {
915
+ this.lifecycle.install();
916
+ if (this.swipeToCloseEnabled && !this.detachSwipe) {
917
+ this.detachSwipe = this.installSwipeToClose();
918
+ }
919
+ if (this.closeOnEscape && !this.detachEscape) {
920
+ const handler = (e) => {
921
+ if (e.key === "Escape" && this.isOpen_) void this.close("escape");
922
+ };
923
+ document.addEventListener("keydown", handler);
924
+ this.detachEscape = () => document.removeEventListener("keydown", handler);
925
+ }
926
+ if (this.closeOnBackdropEnabled && this.backdrop && !this.detachBackdrop) {
927
+ const handler = () => void this.close("backdrop");
928
+ this.backdrop.addEventListener("click", handler);
929
+ this.detachBackdrop = () => this.backdrop?.removeEventListener("click", handler);
930
+ }
931
+ if (this.closeOnBackEnabled && typeof window !== "undefined" && !this.detachHardwareBack) {
932
+ try {
933
+ history.pushState({ __bsOverlay: this.id }, "");
934
+ } catch {
935
+ }
936
+ const onPop = () => {
937
+ if (this.isOpen_) void this.close("back");
938
+ };
939
+ window.addEventListener("popstate", onPop);
940
+ this.detachHardwareBack = () => window.removeEventListener("popstate", onPop);
941
+ }
942
+ if (this.closeOnOutsidePointerEnabled && !this.detachOutsidePointer) {
943
+ const handler = (e) => {
944
+ if (!this.isOpen_) return;
945
+ const target = e.target;
946
+ if (!target) return;
947
+ if (this.element.contains(target)) return;
948
+ if (this.backdrop && this.backdrop.contains(target)) return;
949
+ void this.close("outside-pointer");
950
+ };
951
+ document.addEventListener("pointerdown", handler, true);
952
+ this.detachOutsidePointer = () => document.removeEventListener("pointerdown", handler, true);
953
+ }
954
+ }
955
+ releaseInteractiveListeners() {
956
+ this.detachEscape?.();
957
+ this.detachEscape = null;
958
+ this.detachBackdrop?.();
959
+ this.detachBackdrop = null;
960
+ this.detachHardwareBack?.();
961
+ this.detachHardwareBack = null;
962
+ this.detachSwipe?.();
963
+ this.detachSwipe = null;
964
+ this.detachOutsidePointer?.();
965
+ this.detachOutsidePointer = null;
966
+ this.lifecycle.release();
967
+ }
968
+ /**
969
+ * Light pointer-driven dismiss. Tracks finger along the edge axis, follows
970
+ * with translate, dismisses if past `swipeThreshold` (fraction of panel
971
+ * size) or release velocity exceeds `swipeVelocityThreshold` (px/ms).
972
+ * Pure CSS-transform during drag — no rAF or physics required since the
973
+ * panel only moves one direction.
974
+ */
975
+ installSwipeToClose() {
976
+ const el = this.element;
977
+ const edge = this.edge;
978
+ const isVertical = edge === "bottom" || edge === "top";
979
+ const sign = edge === "bottom" || edge === "right" ? 1 : -1;
980
+ const threshold = this.swipeThreshold;
981
+ const velocityThreshold = this.swipeVelocityThreshold;
982
+ let startX = 0;
983
+ let startY = 0;
984
+ let startT = 0;
985
+ let dragging = false;
986
+ let pointerId = null;
987
+ const onDown = (e) => {
988
+ if (e.button !== void 0 && e.button !== 0) return;
989
+ pointerId = e.pointerId;
990
+ startX = e.clientX;
991
+ startY = e.clientY;
992
+ startT = performance.now();
993
+ dragging = true;
994
+ el.style.transition = "none";
995
+ try {
996
+ el.setPointerCapture(e.pointerId);
997
+ } catch {
998
+ }
999
+ };
1000
+ const onMove = (e) => {
1001
+ if (!dragging || e.pointerId !== pointerId) return;
1002
+ const delta = isVertical ? e.clientY - startY : e.clientX - startX;
1003
+ const offset = Math.max(0, sign * delta);
1004
+ el.style.transform = isVertical ? `translate3d(0, ${sign * offset}px, 0)` : `translate3d(${sign * offset}px, 0, 0)`;
1005
+ };
1006
+ const finish = (e) => {
1007
+ if (!dragging || e.pointerId !== pointerId) return;
1008
+ dragging = false;
1009
+ pointerId = null;
1010
+ const dt = performance.now() - startT || 1;
1011
+ const delta = isVertical ? e.clientY - startY : e.clientX - startX;
1012
+ const offset = Math.max(0, sign * delta);
1013
+ const velocity = sign * delta / dt;
1014
+ const size = isVertical ? el.offsetHeight : el.offsetWidth;
1015
+ const past = offset > size * threshold || velocity > velocityThreshold;
1016
+ el.style.transition = `transform ${this.duration}ms ${this.exitEasing}`;
1017
+ if (past) {
1018
+ void this.close("swipe");
1019
+ } else {
1020
+ el.style.transform = openTransformFor(this.enterAnimation);
1021
+ }
1022
+ };
1023
+ el.addEventListener("pointerdown", onDown);
1024
+ el.addEventListener("pointermove", onMove);
1025
+ el.addEventListener("pointerup", finish);
1026
+ el.addEventListener("pointercancel", finish);
1027
+ return () => {
1028
+ el.removeEventListener("pointerdown", onDown);
1029
+ el.removeEventListener("pointermove", onMove);
1030
+ el.removeEventListener("pointerup", finish);
1031
+ el.removeEventListener("pointercancel", finish);
1032
+ };
1033
+ }
1034
+ registerInStack() {
1035
+ this.releaseStackEntry = sheetStack.push({
1036
+ id: this.id,
1037
+ setZIndex: (z) => {
1038
+ this.element.style.zIndex = String(z);
1039
+ if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
1040
+ },
1041
+ setIsTop: (isTop) => {
1042
+ if (this.backdrop) {
1043
+ this.backdrop.style.display = isTop ? "" : "none";
1044
+ }
1045
+ }
1046
+ });
1047
+ }
1048
+ };
1049
+ var createOverlay = (opts) => new OverlayEngine(opts);
1050
+
1051
+ export { OVERLAY_PRESETS, OverlayEngine as Overlay, OverlayEngine, createOverlay };
1052
+ //# sourceMappingURL=overlay.js.map
1053
+ //# sourceMappingURL=overlay.js.map