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