@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,3442 @@
1
+ 'use strict';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
14
+
15
+ // src/core/lifecycle/sheetStack.ts
16
+ var BASE_Z, STEP, SheetStack, sheetStack;
17
+ var init_sheetStack = __esm({
18
+ "src/core/lifecycle/sheetStack.ts"() {
19
+ BASE_Z = 100;
20
+ STEP = 10;
21
+ SheetStack = class {
22
+ constructor() {
23
+ __publicField(this, "entries", []);
24
+ }
25
+ push(entry) {
26
+ this.entries.push(entry);
27
+ this.recompute();
28
+ return () => this.remove(entry.id);
29
+ }
30
+ remove(id) {
31
+ const idx = this.entries.findIndex((e) => e.id === id);
32
+ if (idx === -1) return;
33
+ this.entries.splice(idx, 1);
34
+ this.recompute();
35
+ }
36
+ size() {
37
+ return this.entries.length;
38
+ }
39
+ clear() {
40
+ this.entries = [];
41
+ }
42
+ recompute() {
43
+ this.entries.forEach((entry, i) => {
44
+ entry.setZIndex(BASE_Z + i * STEP);
45
+ entry.setIsTop(i === this.entries.length - 1);
46
+ });
47
+ }
48
+ };
49
+ sheetStack = new SheetStack();
50
+ }
51
+ });
52
+
53
+ // src/core/primitives/transform.ts
54
+ function buildTransformTemplate(axis) {
55
+ switch (axis) {
56
+ case "bottom":
57
+ return (offset) => `translate3d(0, ${offset}px, 0)`;
58
+ case "top":
59
+ return (offset) => `translate3d(0, ${-offset}px, 0)`;
60
+ case "left":
61
+ return (offset) => `translate3d(${-offset}px, 0, 0)`;
62
+ case "right":
63
+ return (offset) => `translate3d(${offset}px, 0, 0)`;
64
+ }
65
+ }
66
+ var layoutAxis;
67
+ var init_transform = __esm({
68
+ "src/core/primitives/transform.ts"() {
69
+ layoutAxis = (axis) => axis === "left" || axis === "right" ? "width" : "height";
70
+ }
71
+ });
72
+
73
+ // src/core/primitives/instance-id.ts
74
+ function nextInstanceId(prefix) {
75
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
76
+ return `${prefix}-${crypto.randomUUID()}`;
77
+ }
78
+ counters[prefix] = (counters[prefix] ?? 0) + 1;
79
+ return `${prefix}-${counters[prefix]}`;
80
+ }
81
+ var counters;
82
+ var init_instance_id = __esm({
83
+ "src/core/primitives/instance-id.ts"() {
84
+ counters = {};
85
+ }
86
+ });
87
+
88
+ // src/core/primitives/event-bus.ts
89
+ function createEventBus() {
90
+ const listeners = /* @__PURE__ */ new Map();
91
+ return {
92
+ on(event, fn) {
93
+ let set = listeners.get(event);
94
+ if (!set) {
95
+ set = /* @__PURE__ */ new Set();
96
+ listeners.set(event, set);
97
+ }
98
+ set.add(fn);
99
+ return () => {
100
+ set.delete(fn);
101
+ };
102
+ },
103
+ emit(event, payload) {
104
+ const set = listeners.get(event);
105
+ if (!set) return;
106
+ for (const fn of [...set]) {
107
+ fn(payload);
108
+ }
109
+ },
110
+ listenerCount(event) {
111
+ return listeners.get(event)?.size ?? 0;
112
+ },
113
+ clear() {
114
+ listeners.clear();
115
+ }
116
+ };
117
+ }
118
+ var init_event_bus = __esm({
119
+ "src/core/primitives/event-bus.ts"() {
120
+ }
121
+ });
122
+
123
+ // src/core/primitives/cssLength.ts
124
+ var probe, PROBE_ATTR, ensureProbe, isViewportAxisVertical, viewportSize, PERCENT, resolveSnap;
125
+ var init_cssLength = __esm({
126
+ "src/core/primitives/cssLength.ts"() {
127
+ probe = null;
128
+ PROBE_ATTR = "data-bs-probe";
129
+ ensureProbe = (axis) => {
130
+ if (typeof document === "undefined") return null;
131
+ if (!probe || !probe.isConnected) {
132
+ const existing = document.querySelector(`[${PROBE_ATTR}]`);
133
+ if (existing) {
134
+ probe = existing;
135
+ } else {
136
+ probe = document.createElement("div");
137
+ probe.setAttribute("aria-hidden", "true");
138
+ probe.setAttribute(PROBE_ATTR, "");
139
+ probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;contain:strict;left:0;top:0;width:0;height:0;";
140
+ document.body.appendChild(probe);
141
+ }
142
+ }
143
+ if (axis === "vertical") {
144
+ probe.style.width = "0";
145
+ probe.style.height = "";
146
+ } else {
147
+ probe.style.height = "0";
148
+ probe.style.width = "";
149
+ }
150
+ return probe;
151
+ };
152
+ isViewportAxisVertical = (mode) => mode === "bottom" || mode === "top";
153
+ viewportSize = (mode) => {
154
+ if (typeof window === "undefined") return 0;
155
+ return isViewportAxisVertical(mode) ? window.innerHeight : window.innerWidth;
156
+ };
157
+ PERCENT = /^(-?\d+(?:\.\d+)?)%$/;
158
+ resolveSnap = (point, mode, measureFit) => {
159
+ if (typeof point === "number") return Math.max(0, point);
160
+ if (point === "full") return viewportSize(mode);
161
+ if (point === "fit") return Math.max(0, measureFit?.() ?? 0);
162
+ const pctMatch = point.match(PERCENT);
163
+ if (pctMatch) {
164
+ const pct = parseFloat(pctMatch[1]) / 100;
165
+ return Math.max(0, viewportSize(mode) * pct);
166
+ }
167
+ const axis = isViewportAxisVertical(mode) ? "vertical" : "horizontal";
168
+ const el = ensureProbe(axis);
169
+ if (!el) return 0;
170
+ if (axis === "vertical") {
171
+ el.style.height = point;
172
+ return Math.max(0, el.getBoundingClientRect().height);
173
+ }
174
+ el.style.width = point;
175
+ return Math.max(0, el.getBoundingClientRect().width);
176
+ };
177
+ }
178
+ });
179
+
180
+ // src/core/primitives/snap-points.ts
181
+ var resolveSnapList, VH_NOT_DVH, auditVhUsage, findNearest, allowedRange, findById, MOUSE_FLICK_VELOCITY, MOUSE_DRAG_THRESHOLD, DIRECTIONAL_INTENT_VELOCITY, findDragSettleTarget;
182
+ var init_snap_points = __esm({
183
+ "src/core/primitives/snap-points.ts"() {
184
+ init_cssLength();
185
+ resolveSnapList = (points, mode, measureFit) => points.map((p) => {
186
+ const raw = resolveSnap(p.size, mode, measureFit);
187
+ if (!Number.isFinite(raw) || raw < 0) {
188
+ console.warn(
189
+ `[BottomSheet] snap "${p.id}" resolved to invalid size (${String(p.size)} \u2192 ${raw}); clamped to 0.`
190
+ );
191
+ return { id: p.id, size: 0 };
192
+ }
193
+ return { id: p.id, size: raw };
194
+ }).sort((a, b) => a.size - b.size);
195
+ VH_NOT_DVH = /(^|[^a-z])vh\b/i;
196
+ auditVhUsage = (points) => {
197
+ for (const p of points) {
198
+ if (typeof p.size !== "string") continue;
199
+ if (!VH_NOT_DVH.test(p.size)) continue;
200
+ console.warn(
201
+ `[BottomSheet] snap "${p.id}" uses "vh" \u2014 prefer "dvh" on mobile (iOS Safari URL bar makes vh unstable). See https://web.dev/blog/viewport-units`
202
+ );
203
+ }
204
+ };
205
+ findNearest = (size, resolved, allowed, direction = 0, bias = 0) => {
206
+ const pool = resolved.filter((p) => allowed.includes(p.id));
207
+ if (pool.length === 0) return null;
208
+ const target = size + direction * bias;
209
+ let best = pool[0];
210
+ let bestDist = Math.abs(target - best.size);
211
+ for (let i = 1; i < pool.length; i++) {
212
+ const cand = pool[i];
213
+ const dist = Math.abs(target - cand.size);
214
+ if (dist < bestDist) {
215
+ best = cand;
216
+ bestDist = dist;
217
+ }
218
+ }
219
+ return best;
220
+ };
221
+ allowedRange = (resolved, allowed) => {
222
+ const pool = resolved.filter((p) => allowed.includes(p.id));
223
+ if (pool.length === 0) return { min: 0, max: 0 };
224
+ return {
225
+ min: pool[0].size,
226
+ max: pool[pool.length - 1].size
227
+ };
228
+ };
229
+ findById = (id, resolved) => resolved.find((p) => p.id === id) ?? null;
230
+ MOUSE_FLICK_VELOCITY = 0.4;
231
+ MOUSE_DRAG_THRESHOLD = 12;
232
+ DIRECTIONAL_INTENT_VELOCITY = 0.15;
233
+ findDragSettleTarget = (input) => {
234
+ const speed = Math.abs(input.velocity);
235
+ const flickThreshold = input.pointerKind === "mouse" ? MOUSE_FLICK_VELOCITY : input.flickVelocity;
236
+ const dragThresh = input.pointerKind === "mouse" ? MOUSE_DRAG_THRESHOLD : input.dragThreshold;
237
+ let direction = 0;
238
+ if (speed > flickThreshold || speed > DIRECTIONAL_INTENT_VELOCITY) {
239
+ direction = input.velocity > 0 ? 1 : -1;
240
+ }
241
+ if (Math.abs(input.delta) < dragThresh && direction === 0) {
242
+ return findById(input.activeId, input.resolved);
243
+ }
244
+ const speedBias = speed > flickThreshold ? Math.min(speed * 180, input.maxAxisSize) : 0;
245
+ return findNearest(
246
+ input.size,
247
+ input.resolved,
248
+ input.allowed,
249
+ direction,
250
+ speedBias
251
+ );
252
+ };
253
+ }
254
+ });
255
+
256
+ // src/core/primitives/snap-resolver.ts
257
+ var SnapResolver;
258
+ var init_snap_resolver = __esm({
259
+ "src/core/primitives/snap-resolver.ts"() {
260
+ init_snap_points();
261
+ SnapResolver = class {
262
+ constructor(raw, allowed, mode, measureFit, onMaxAxisSizeChange) {
263
+ __publicField(this, "mode", mode);
264
+ __publicField(this, "measureFit", measureFit);
265
+ __publicField(this, "onMaxAxisSizeChange", onMaxAxisSizeChange);
266
+ __publicField(this, "raw");
267
+ __publicField(this, "allowedIds");
268
+ __publicField(this, "resolved", []);
269
+ __publicField(this, "maxAxisSize", 0);
270
+ __publicField(this, "rangeCache", null);
271
+ this.raw = raw;
272
+ this.allowedIds = allowed ?? raw.map((p) => p.id);
273
+ this.recompute();
274
+ }
275
+ /**
276
+ * Re-resolve raw → resolved against the current viewport / fit-measure,
277
+ * recompute maxAxisSize, invalidate range cache, notify engine. Called on
278
+ * resize, orientationchange, visualViewport changes, and snap-list swaps.
279
+ */
280
+ recompute() {
281
+ this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
282
+ this.maxAxisSize = this.resolved.reduce(
283
+ (m, s) => s.size > m ? s.size : m,
284
+ 0
285
+ );
286
+ this.rangeCache = null;
287
+ this.onMaxAxisSizeChange?.(this.maxAxisSize);
288
+ }
289
+ /** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
290
+ setRaw(raw) {
291
+ this.raw = raw;
292
+ this.recompute();
293
+ }
294
+ /**
295
+ * Replace the allow-list. Range cache depends on `(resolvedSnaps ×
296
+ * allowedIds)` so flipping ids alone invalidates without a full recompute.
297
+ */
298
+ setAllowedIds(ids) {
299
+ this.allowedIds = ids;
300
+ this.rangeCache = null;
301
+ }
302
+ findById(id) {
303
+ return findById(id, this.resolved);
304
+ }
305
+ /**
306
+ * Pure drag-settle target resolution. Engine passes the gesture-specific
307
+ * fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
308
+ * `size`/`activeId` since those are still engine-owned.
309
+ */
310
+ findDragSettleTarget(args) {
311
+ return findDragSettleTarget({
312
+ ...args,
313
+ resolved: this.resolved,
314
+ allowed: this.allowedIds,
315
+ maxAxisSize: this.maxAxisSize
316
+ });
317
+ }
318
+ /**
319
+ * Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
320
+ * applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
321
+ * to one recompute per geometry/allow-list change.
322
+ */
323
+ getAllowedRange() {
324
+ return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
325
+ }
326
+ /**
327
+ * Read-only view of resolved snaps. Returned as `readonly` to discourage
328
+ * accidental mutation by feature install sites that close over this list.
329
+ */
330
+ getResolvedSnaps() {
331
+ return this.resolved;
332
+ }
333
+ getAllowedIds() {
334
+ return this.allowedIds;
335
+ }
336
+ getMaxAxisSize() {
337
+ return this.maxAxisSize;
338
+ }
339
+ /**
340
+ * External clamp override. The resize-observer feature compares
341
+ * `getMaxAxisSize()` against the live viewport and shrinks if the largest
342
+ * resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
343
+ * window resize). The next `recompute()` overwrites this clamp from the
344
+ * resolved-snap reduction.
345
+ *
346
+ * Does NOT invalidate the range cache — viewport clamping doesn't change
347
+ * `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
348
+ * so the resolver owns the layout-axis style write end-to-end (symmetric
349
+ * with `recompute()` — feature call sites no longer duplicate the write).
350
+ */
351
+ setMaxAxisSize(n) {
352
+ this.maxAxisSize = n;
353
+ this.onMaxAxisSizeChange?.(n);
354
+ }
355
+ };
356
+ }
357
+ });
358
+
359
+ // src/core/features/persist.ts
360
+ function readPersistedId(key) {
361
+ if (!key) return null;
362
+ if (typeof window === "undefined") return null;
363
+ if (typeof localStorage === "undefined") return null;
364
+ try {
365
+ return localStorage.getItem(key);
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+ function installPersist(engine, key) {
371
+ const unsubscribe = engine.on("snap", ({ id }) => {
372
+ if (typeof window === "undefined") return;
373
+ if (typeof localStorage === "undefined") return;
374
+ try {
375
+ localStorage.setItem(key, id);
376
+ } catch {
377
+ }
378
+ });
379
+ return unsubscribe;
380
+ }
381
+ var init_persist = __esm({
382
+ "src/core/features/persist.ts"() {
383
+ }
384
+ });
385
+
386
+ // src/core/features/auto-collapse.ts
387
+ function installAutoCollapse(deps) {
388
+ const { ms } = deps;
389
+ if (ms === void 0 || ms <= 0) {
390
+ return () => {
391
+ };
392
+ }
393
+ let timer = null;
394
+ const clearTimer = () => {
395
+ if (timer !== null) {
396
+ clearTimeout(timer);
397
+ timer = null;
398
+ }
399
+ };
400
+ const fire = () => {
401
+ if (deps.isDestroyed()) return;
402
+ if (deps.isDragging()) return;
403
+ const allowed = deps.getAllowedIds();
404
+ const target = allowed.find((id) => {
405
+ const snap = deps.resolveSnap(id);
406
+ return snap !== null && snap.size > 0;
407
+ });
408
+ if (!target) return;
409
+ if (target === deps.getActiveId()) return;
410
+ deps.snapTo(target);
411
+ };
412
+ const reset = () => {
413
+ clearTimer();
414
+ if (deps.isDestroyed()) return;
415
+ timer = setTimeout(() => {
416
+ timer = null;
417
+ fire();
418
+ }, ms);
419
+ };
420
+ const unsubscribe = deps.on("snap", () => reset());
421
+ return () => {
422
+ clearTimer();
423
+ unsubscribe();
424
+ };
425
+ }
426
+ var init_auto_collapse = __esm({
427
+ "src/core/features/auto-collapse.ts"() {
428
+ }
429
+ });
430
+
431
+ // src/core/features/linked-sheets.ts
432
+ function notifyLinkedSheets(sheets, self) {
433
+ if (sheets.length === 0) return;
434
+ for (const linked of sheets) {
435
+ if (linked === self) continue;
436
+ try {
437
+ const ids = linked.getAllowedIds();
438
+ const target = ids.find((id) => id !== "closed");
439
+ if (target && target !== linked.state.activeId) {
440
+ void linked.snapTo(target);
441
+ }
442
+ } catch (err) {
443
+ queueMicrotask(() => {
444
+ throw err;
445
+ });
446
+ }
447
+ }
448
+ }
449
+ var init_linked_sheets = __esm({
450
+ "src/core/features/linked-sheets.ts"() {
451
+ }
452
+ });
453
+
454
+ // src/core/features/route.ts
455
+ function installRoute(deps) {
456
+ if (typeof window === "undefined") {
457
+ return () => {
458
+ };
459
+ }
460
+ if (!deps.routedTo && !deps.closeOnBack) {
461
+ return () => {
462
+ };
463
+ }
464
+ let routePushed = false;
465
+ let closeOnBackPushed = false;
466
+ let detachPopstate = null;
467
+ const popOurMarker = (key, expected) => {
468
+ const state = history.state;
469
+ if (state && state[key] === expected) {
470
+ try {
471
+ history.back();
472
+ } catch {
473
+ }
474
+ }
475
+ };
476
+ const onOpen = () => {
477
+ if (deps.routedTo && !detachPopstate) {
478
+ const target = deps.routedTo;
479
+ try {
480
+ history.pushState({ __bsRouted: target }, "", target);
481
+ routePushed = true;
482
+ } catch {
483
+ }
484
+ const onPop = (event) => {
485
+ if (deps.isDestroyed()) return;
486
+ const state = event.state;
487
+ if (state && state.__bsRouted === target && deps.getSize() > 0) {
488
+ routePushed = false;
489
+ void deps.close();
490
+ }
491
+ };
492
+ window.addEventListener("popstate", onPop);
493
+ detachPopstate = () => window.removeEventListener("popstate", onPop);
494
+ } else if (deps.closeOnBack && !detachPopstate) {
495
+ try {
496
+ history.pushState({ __bsSheet: deps.sheetId }, "");
497
+ closeOnBackPushed = true;
498
+ } catch {
499
+ }
500
+ const onPop = () => {
501
+ if (deps.isDestroyed()) return;
502
+ if (deps.getSize() > 0 && deps.isTopSheet()) {
503
+ const state = history.state;
504
+ if (state?.__bsSheet !== deps.sheetId) closeOnBackPushed = false;
505
+ void deps.close();
506
+ }
507
+ };
508
+ window.addEventListener("popstate", onPop);
509
+ detachPopstate = () => window.removeEventListener("popstate", onPop);
510
+ }
511
+ };
512
+ const onClose = () => {
513
+ detachPopstate?.();
514
+ detachPopstate = null;
515
+ if (routePushed) {
516
+ routePushed = false;
517
+ if (deps.routedTo !== void 0) popOurMarker("__bsRouted", deps.routedTo);
518
+ }
519
+ if (closeOnBackPushed) {
520
+ closeOnBackPushed = false;
521
+ popOurMarker("__bsSheet", deps.sheetId);
522
+ }
523
+ };
524
+ const unsubscribeOpen = deps.on("open", onOpen);
525
+ const unsubscribeClose = deps.on("close", onClose);
526
+ if (deps.getSize() > 0) onOpen();
527
+ return () => {
528
+ unsubscribeOpen();
529
+ unsubscribeClose();
530
+ detachPopstate?.();
531
+ detachPopstate = null;
532
+ if (routePushed) {
533
+ routePushed = false;
534
+ if (deps.routedTo !== void 0) popOurMarker("__bsRouted", deps.routedTo);
535
+ }
536
+ if (closeOnBackPushed) {
537
+ closeOnBackPushed = false;
538
+ popOurMarker("__bsSheet", deps.sheetId);
539
+ }
540
+ };
541
+ }
542
+ var init_route = __esm({
543
+ "src/core/features/route.ts"() {
544
+ }
545
+ });
546
+
547
+ // src/core/features/content-swipe.ts
548
+ function installContentSwipe(deps) {
549
+ const { container } = deps;
550
+ let startY = 0;
551
+ let moved = false;
552
+ const onTouchStart = (e) => {
553
+ const t = e.touches[0];
554
+ if (!t) return;
555
+ startY = t.clientY;
556
+ moved = false;
557
+ };
558
+ const onTouchMove = (e) => {
559
+ const t = e.touches[0];
560
+ if (!t) return;
561
+ if (Math.abs(t.clientY - startY) > CONTENT_PULL_THRESHOLD) moved = true;
562
+ };
563
+ const onTouchEnd = (e) => {
564
+ if (!moved) return;
565
+ if (deps.isDragging() || deps.isAnimating()) return;
566
+ const t = e.changedTouches[0];
567
+ if (!t) return;
568
+ const deltaY = t.clientY - startY;
569
+ const atTop = container.scrollTop <= 0;
570
+ const allowed = deps.getAllowedIds();
571
+ const idx = allowed.indexOf(deps.getActiveId());
572
+ if (idx === -1) return;
573
+ if (atTop && deltaY > CONTENT_PULL_THRESHOLD && idx > 0) {
574
+ deps.snapTo(allowed[idx - 1]);
575
+ } else if (deltaY < -CONTENT_PULL_THRESHOLD && idx < allowed.length - 1) {
576
+ deps.snapTo(allowed[idx + 1]);
577
+ }
578
+ };
579
+ container.addEventListener("touchstart", onTouchStart, { passive: true });
580
+ container.addEventListener("touchmove", onTouchMove, { passive: true });
581
+ container.addEventListener("touchend", onTouchEnd, { passive: true });
582
+ return () => {
583
+ container.removeEventListener("touchstart", onTouchStart);
584
+ container.removeEventListener("touchmove", onTouchMove);
585
+ container.removeEventListener("touchend", onTouchEnd);
586
+ };
587
+ }
588
+ var CONTENT_PULL_THRESHOLD;
589
+ var init_content_swipe = __esm({
590
+ "src/core/features/content-swipe.ts"() {
591
+ CONTENT_PULL_THRESHOLD = 60;
592
+ }
593
+ });
594
+
595
+ // src/core/features/visual-viewport.ts
596
+ function installVisualViewport(deps) {
597
+ if (typeof window === "undefined" || !window.visualViewport) {
598
+ return () => {
599
+ };
600
+ }
601
+ const vv = window.visualViewport;
602
+ let rafId = null;
603
+ const onResize = () => {
604
+ if (rafId !== null) return;
605
+ rafId = requestAnimationFrame(() => {
606
+ rafId = null;
607
+ if (deps.isDestroyed()) return;
608
+ deps.recomputeSnaps();
609
+ const isVertical = deps.isVerticalAxis();
610
+ const visible = isVertical ? vv.height : vv.width;
611
+ const windowSize = isVertical ? window.innerHeight : window.innerWidth;
612
+ const clamp = Math.max(0, Math.min(windowSize, visible) - SAFETY_PAD);
613
+ if (clamp < deps.getMaxAxisSize()) {
614
+ deps.setMaxAxisSize(clamp);
615
+ }
616
+ const max = deps.getMaxAxisSize();
617
+ deps.setSize(Math.min(deps.getSize(), max));
618
+ const current = deps.resolveActiveSnap();
619
+ if (current && !deps.isDragging()) {
620
+ deps.applySize(Math.min(current.size, max));
621
+ } else {
622
+ deps.applySize(deps.getSize());
623
+ }
624
+ });
625
+ };
626
+ vv.addEventListener("resize", onResize);
627
+ return () => {
628
+ vv.removeEventListener("resize", onResize);
629
+ if (rafId !== null) cancelAnimationFrame(rafId);
630
+ };
631
+ }
632
+ var SAFETY_PAD;
633
+ var init_visual_viewport = __esm({
634
+ "src/core/features/visual-viewport.ts"() {
635
+ SAFETY_PAD = 8;
636
+ }
637
+ });
638
+
639
+ // src/core/features/resize-observer.ts
640
+ function installResizeObserver(deps) {
641
+ if (typeof window === "undefined") {
642
+ return () => {
643
+ };
644
+ }
645
+ const onResize = () => {
646
+ if (deps.isDestroyed()) return;
647
+ deps.recomputeSnaps();
648
+ const mode = deps.getMode();
649
+ const isVerticalAxis = mode === "bottom" || mode === "top";
650
+ const viewportSize2 = isVerticalAxis ? window.innerHeight : window.innerWidth;
651
+ const max = deps.getMaxAxisSize();
652
+ if (viewportSize2 > 0 && viewportSize2 < max) {
653
+ deps.setMaxAxisSize(viewportSize2);
654
+ }
655
+ deps.cancelInFlight();
656
+ deps.newCycle();
657
+ const current = deps.resolveActiveSnap();
658
+ const newMax = deps.getMaxAxisSize();
659
+ const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
660
+ deps.setSize(targetSize);
661
+ if (current && !deps.isDragging()) deps.applySize(targetSize);
662
+ };
663
+ let resizeObserver = null;
664
+ if (typeof ResizeObserver !== "undefined") {
665
+ resizeObserver = new ResizeObserver(onResize);
666
+ resizeObserver.observe(document.documentElement);
667
+ } else {
668
+ window.addEventListener("resize", onResize);
669
+ }
670
+ window.addEventListener("orientationchange", onResize);
671
+ const onVisibility = () => {
672
+ if (deps.isDestroyed()) return;
673
+ if (document.hidden) deps.cancelInFlight();
674
+ };
675
+ document.addEventListener("visibilitychange", onVisibility);
676
+ return () => {
677
+ resizeObserver?.disconnect();
678
+ window.removeEventListener("resize", onResize);
679
+ window.removeEventListener("orientationchange", onResize);
680
+ document.removeEventListener("visibilitychange", onVisibility);
681
+ };
682
+ }
683
+ var init_resize_observer = __esm({
684
+ "src/core/features/resize-observer.ts"() {
685
+ }
686
+ });
687
+
688
+ // src/core/features/slider-keyboard.ts
689
+ function installSliderKeyboard(deps) {
690
+ const isVerticalAxis = deps.mode === "bottom" || deps.mode === "top";
691
+ const stepUp = isVerticalAxis ? "ArrowUp" : "ArrowRight";
692
+ const stepDown = isVerticalAxis ? "ArrowDown" : "ArrowLeft";
693
+ const sheetExpandsKey = deps.mode === "top" ? stepDown : stepUp;
694
+ const sheetCollapsesKey = deps.mode === "top" ? stepUp : stepDown;
695
+ const onHandleKey = (e) => {
696
+ if (deps.isDestroyed()) return;
697
+ const allowed = deps.getAllowedIds();
698
+ const idx = allowed.indexOf(deps.getActiveId());
699
+ if (idx === -1) return;
700
+ if (e.key === sheetExpandsKey && idx < allowed.length - 1) {
701
+ e.preventDefault();
702
+ deps.snapTo(allowed[idx + 1]);
703
+ } else if (e.key === sheetCollapsesKey && idx > 0) {
704
+ e.preventDefault();
705
+ deps.snapTo(allowed[idx - 1]);
706
+ } else if (e.key === "Home") {
707
+ e.preventDefault();
708
+ deps.snapTo(allowed[0]);
709
+ } else if (e.key === "End") {
710
+ e.preventDefault();
711
+ deps.snapTo(allowed[allowed.length - 1]);
712
+ }
713
+ };
714
+ deps.handle.addEventListener("keydown", onHandleKey);
715
+ return () => deps.handle.removeEventListener("keydown", onHandleKey);
716
+ }
717
+ var init_slider_keyboard = __esm({
718
+ "src/core/features/slider-keyboard.ts"() {
719
+ }
720
+ });
721
+
722
+ // src/core/types.ts
723
+ var SCRIM_PRESETS;
724
+ var init_types = __esm({
725
+ "src/core/types.ts"() {
726
+ SCRIM_PRESETS = Object.freeze({
727
+ subtle: {
728
+ color: "rgba(0,0,0,0.2)",
729
+ blur: void 0,
730
+ range: [0.3, 1],
731
+ interactive: false
732
+ },
733
+ standard: {
734
+ color: "rgba(0,0,0,0.4)",
735
+ blur: void 0,
736
+ range: [0, 1],
737
+ interactive: false
738
+ },
739
+ monitoring: {
740
+ color: "rgba(15,15,20,0.55)",
741
+ blur: "4px",
742
+ range: [0, 1],
743
+ interactive: false
744
+ },
745
+ cinematic: {
746
+ color: "rgba(0,0,0,0.7)",
747
+ blur: "12px",
748
+ range: [0, 1],
749
+ interactive: false
750
+ }
751
+ });
752
+ }
753
+ });
754
+
755
+ // src/core/primitives/hot-path-thresholds.ts
756
+ var SIZE_WRITE_EPSILON, OPACITY_WRITE_EPSILON, POINTER_EVENTS_OPACITY_THRESHOLD, RANGE_DIVISION_EPSILON;
757
+ var init_hot_path_thresholds = __esm({
758
+ "src/core/primitives/hot-path-thresholds.ts"() {
759
+ SIZE_WRITE_EPSILON = 0.5;
760
+ OPACITY_WRITE_EPSILON = 5e-3;
761
+ POINTER_EVENTS_OPACITY_THRESHOLD = 0.05;
762
+ RANGE_DIVISION_EPSILON = 1e-4;
763
+ }
764
+ });
765
+
766
+ // src/core/primitives/opacity-dedup.ts
767
+ var WriteSentinel;
768
+ var init_opacity_dedup = __esm({
769
+ "src/core/primitives/opacity-dedup.ts"() {
770
+ WriteSentinel = class {
771
+ constructor() {
772
+ __publicField(this, "last", -1);
773
+ }
774
+ /**
775
+ * Returns `true` (and updates the cached value) if `next` differs from the
776
+ * cached `last` by more than `epsilon`. Returns `false` (and leaves the
777
+ * cache untouched) when the delta is within epsilon — caller skips the
778
+ * CSSOM write.
779
+ */
780
+ shouldWrite(next, epsilon) {
781
+ if (Math.abs(next - this.last) <= epsilon) return false;
782
+ this.last = next;
783
+ return true;
784
+ }
785
+ /**
786
+ * Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
787
+ * non-negative `next` exceeds the epsilon comparison. Called from setters
788
+ * that mutate state under the cache (range/mode/enabled flips that change
789
+ * the progress→opacity mapping but don't move `progress` itself).
790
+ */
791
+ invalidate() {
792
+ this.last = -1;
793
+ }
794
+ /**
795
+ * Record `value` as the last-written without performing a write. Used
796
+ * when a setter writes a literal value through a different path (e.g.
797
+ * `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
798
+ * `shouldWrite` calls to short-circuit against that known cached state.
799
+ */
800
+ setLastWritten(value) {
801
+ this.last = value;
802
+ }
803
+ /** Read-only accessor for diagnostic / dedup-aware peek. */
804
+ get value() {
805
+ return this.last;
806
+ }
807
+ };
808
+ }
809
+ });
810
+
811
+ // src/core/controllers/scrim-controller.ts
812
+ var ScrimController;
813
+ var init_scrim_controller = __esm({
814
+ "src/core/controllers/scrim-controller.ts"() {
815
+ init_types();
816
+ init_snap_points();
817
+ init_hot_path_thresholds();
818
+ init_opacity_dedup();
819
+ ScrimController = class {
820
+ constructor(deps, opts) {
821
+ // ── DOM refs (mirror engine's; cached for hot path) ─────────────────
822
+ __publicField(this, "screenComponent");
823
+ __publicField(this, "backdrop");
824
+ __publicField(this, "mode");
825
+ // ── Engine callbacks ────────────────────────────────────────────────
826
+ __publicField(this, "isDestroyed");
827
+ __publicField(this, "isTopSheet");
828
+ __publicField(this, "getAllowedIds");
829
+ __publicField(this, "getResolvedSnaps");
830
+ __publicField(this, "snapToFn");
831
+ // ── Mode + state ────────────────────────────────────────────────────
832
+ __publicField(this, "scrimMode");
833
+ __publicField(this, "backdropRange");
834
+ __publicField(this, "screenRange");
835
+ // setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
836
+ // ranges so re-enable can restore prior values without a remount. Public
837
+ // so tests / introspection can read it without an `as any` cast.
838
+ __publicField(this, "scrimEnabled", true);
839
+ __publicField(this, "savedScrimRanges", null);
840
+ // ── Tap-to-close ────────────────────────────────────────────────────
841
+ __publicField(this, "scrimTapToCloseEnabled", false);
842
+ __publicField(this, "detachScrimTap", null);
843
+ // ── Scrim overlay slot (positioned floating element) ────────────────
844
+ __publicField(this, "scrimOverlayTeardown", null);
845
+ // ── Hot-path opacity dedup caches (per-frame writes) ────────────────
846
+ // Numeric sentinels migrated to WriteSentinel primitive — single
847
+ // invalidation surface, shared with the engine's size/progress sentinels.
848
+ // The two string-typed sentinels (`Pointer`, `Display`) keep their inline
849
+ // pattern: equality on string union, no epsilon, different reset semantics.
850
+ __publicField(this, "backdropOpacitySentinel", new WriteSentinel());
851
+ __publicField(this, "screenOpacitySentinel", new WriteSentinel());
852
+ __publicField(this, "lastBackdropPointer", "");
853
+ __publicField(this, "lastScreenDisplay", "");
854
+ this.screenComponent = deps.screenComponent;
855
+ this.backdrop = deps.backdrop;
856
+ this.mode = deps.mode;
857
+ this.isDestroyed = deps.isDestroyed;
858
+ this.isTopSheet = deps.isTopSheet;
859
+ this.getAllowedIds = deps.getAllowedIds;
860
+ this.getResolvedSnaps = deps.getResolvedSnaps;
861
+ this.snapToFn = deps.snapTo;
862
+ this.scrimMode = opts.scrimMode ?? "full";
863
+ const preset = opts.scrimPreset ? SCRIM_PRESETS[opts.scrimPreset] : null;
864
+ this.backdropRange = opts.backdropRange ?? preset?.range ?? [0, 1];
865
+ this.screenRange = opts.screenRange ?? preset?.range ?? [0, 1];
866
+ this.scrimTapToCloseEnabled = opts.scrimTapToClose ?? false;
867
+ if (this.screenComponent) {
868
+ this.applyScrimStyles({
869
+ color: opts.scrimColor ?? preset?.color,
870
+ blur: opts.scrimBlur ?? preset?.blur,
871
+ interactive: opts.scrimInteractive ?? preset?.interactive ?? false
872
+ });
873
+ if (this.scrimMode === "above-sheet") {
874
+ this.applyAboveSheetInset();
875
+ } else if (this.scrimMode === "off") {
876
+ const s = this.screenComponent.style;
877
+ s.opacity = "0";
878
+ s.display = "none";
879
+ this.screenOpacitySentinel.setLastWritten(0);
880
+ this.lastScreenDisplay = "none";
881
+ }
882
+ }
883
+ }
884
+ /**
885
+ * Invalidate the opacity dedup caches. Called from any setter that mutates
886
+ * range/mode/enabled — opacity is a pure function of (progress, range,
887
+ * enabled), so a config change makes the cached values stale even if
888
+ * `progress` itself didn't move. Next `applyOpacity(progress, false)` call
889
+ * sees the -1 sentinel and re-runs the math instead of short-circuiting.
890
+ *
891
+ * Public for the engine: `setSnapPoints` mutates resolved geometry which
892
+ * shifts `progress→opacity` mapping under the cache; engine calls this to
893
+ * keep the next render frame correct on a static sheet.
894
+ * @internal
895
+ */
896
+ invalidateOpacityCache() {
897
+ this.backdropOpacitySentinel.invalidate();
898
+ this.screenOpacitySentinel.invalidate();
899
+ }
900
+ /** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
901
+ attach() {
902
+ if (this.scrimTapToCloseEnabled) {
903
+ this.setScrimTapToClose(true);
904
+ }
905
+ }
906
+ // ── Hot-path: opacity computation called per-frame from engine.applySize ──
907
+ /**
908
+ * Per-frame opacity writer. Computes backdrop + screen opacity from the
909
+ * sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
910
+ * caches so steady-state frames don't trigger style recalc.
911
+ *
912
+ * `progressChanged` is the engine's already-computed dedup gate — when
913
+ * `false`, opacity is by construction a pure function of `progress` so
914
+ * cached values are still valid; we short-circuit the Math.min/max +
915
+ * division work entirely. Setters that mutate ranges/mode/enabled
916
+ * invalidate the cache by writing -1 sentinels so the next call re-runs
917
+ * the math.
918
+ */
919
+ applyOpacity(progress, progressChanged) {
920
+ if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
921
+ return;
922
+ }
923
+ if (this.backdrop && this.isTopSheet()) {
924
+ const [s, e] = this.backdropRange;
925
+ const range = Math.max(e - s, RANGE_DIVISION_EPSILON);
926
+ const backdropOpacity = Math.min(
927
+ Math.max((progress - s) / range, 0),
928
+ 1
929
+ );
930
+ if (this.backdropOpacitySentinel.shouldWrite(
931
+ backdropOpacity,
932
+ OPACITY_WRITE_EPSILON
933
+ )) {
934
+ this.backdrop.style.opacity = String(backdropOpacity);
935
+ }
936
+ const nextPointer = backdropOpacity > POINTER_EVENTS_OPACITY_THRESHOLD ? "auto" : "none";
937
+ if (nextPointer !== this.lastBackdropPointer) {
938
+ this.backdrop.style.pointerEvents = nextPointer;
939
+ this.lastBackdropPointer = nextPointer;
940
+ }
941
+ }
942
+ if (this.screenComponent && this.scrimMode !== "off") {
943
+ const [ss, se] = this.screenRange;
944
+ const screenOpacity = Math.min(
945
+ 1,
946
+ Math.max(
947
+ 0,
948
+ (progress - ss) / Math.max(RANGE_DIVISION_EPSILON, se - ss)
949
+ )
950
+ );
951
+ if (this.screenOpacitySentinel.shouldWrite(
952
+ screenOpacity,
953
+ OPACITY_WRITE_EPSILON
954
+ )) {
955
+ this.screenComponent.style.opacity = String(screenOpacity);
956
+ }
957
+ const nextDisplay = screenOpacity > 0 ? "" : "none";
958
+ if (nextDisplay !== this.lastScreenDisplay) {
959
+ this.screenComponent.style.display = nextDisplay;
960
+ this.lastScreenDisplay = nextDisplay;
961
+ }
962
+ }
963
+ }
964
+ // ── Public setters (engine forwards these 1:1) ─────────────────────
965
+ /**
966
+ * Replace the [start, end] progress window in which the backdrop fades 0→1.
967
+ * @param range - Tuple of two progress values in 0..1 (start <= end).
968
+ * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
969
+ * @example
970
+ * // Backdrop only starts darkening after the sheet crosses 30% open.
971
+ * engine.setBackdropRange([0.3, 1]);
972
+ */
973
+ setBackdropRange(range, applySize) {
974
+ if (this.isDestroyed()) return;
975
+ this.backdropRange = range;
976
+ this.invalidateOpacityCache();
977
+ applySize();
978
+ }
979
+ /**
980
+ * Replace the [start, end] progress window in which the screen scrim fades 0→1.
981
+ * @param range - Tuple of two progress values in 0..1 (start <= end).
982
+ * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
983
+ * @example
984
+ * // Scrim is fully opaque only when the sheet is past 50% — softer feel.
985
+ * engine.setScreenRange([0, 0.5]);
986
+ */
987
+ setScreenRange(range, applySize) {
988
+ if (this.isDestroyed()) return;
989
+ this.screenRange = range;
990
+ this.invalidateOpacityCache();
991
+ applySize();
992
+ }
993
+ /**
994
+ * Set the scrim background color. Pass `null`/`undefined` to clear the
995
+ * inline write and let consumer CSS take over.
996
+ * @param color - Any valid CSS color string, or `null`/`undefined` to clear.
997
+ * @example
998
+ * engine.setScrimColor("rgba(0, 0, 0, 0.6)");
999
+ * engine.setScrimColor(null); // hand back to consumer CSS
1000
+ */
1001
+ setScrimColor(color) {
1002
+ if (this.isDestroyed()) return;
1003
+ this.applyScrimStyles({ color: color === void 0 ? null : color });
1004
+ }
1005
+ /**
1006
+ * Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
1007
+ * @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
1008
+ * `null`/`undefined` to remove the filter.
1009
+ * @example
1010
+ * engine.setScrimBlur("12px"); // frosted-glass effect
1011
+ * engine.setScrimBlur(null); // disable blur
1012
+ */
1013
+ setScrimBlur(blur) {
1014
+ if (this.isDestroyed()) return;
1015
+ this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
1016
+ }
1017
+ /**
1018
+ * Toggle whether the scrim layer captures pointer events.
1019
+ * @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
1020
+ * @example
1021
+ * engine.setScrimInteractive(true); // scrim swallows clicks
1022
+ * engine.setScrimInteractive(false); // clicks pass through to content below
1023
+ */
1024
+ setScrimInteractive(interactive) {
1025
+ if (this.isDestroyed()) return;
1026
+ this.applyScrimStyles({ interactive });
1027
+ }
1028
+ /**
1029
+ * Switch scrim positioning at runtime.
1030
+ * @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
1031
+ * so only the area NOT covered by the sheet is dimmed), or
1032
+ * `"off"` (display:none + opacity:0 — no remount needed).
1033
+ * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1034
+ * @example
1035
+ * engine.setScrimMode("above-sheet"); // dim only the area above the sheet
1036
+ * engine.setScrimMode("off"); // disable scrim layer
1037
+ */
1038
+ setScrimMode(mode, applySize) {
1039
+ if (this.isDestroyed()) return;
1040
+ if (mode === this.scrimMode) return;
1041
+ this.invalidateOpacityCache();
1042
+ const previous = this.scrimMode;
1043
+ this.scrimMode = mode;
1044
+ if (this.screenComponent) {
1045
+ const s = this.screenComponent.style;
1046
+ if (previous === "above-sheet" && mode !== "above-sheet") {
1047
+ s.position = "";
1048
+ s.inset = "";
1049
+ s.pointerEvents = "";
1050
+ }
1051
+ if (mode === "above-sheet") {
1052
+ this.applyAboveSheetInset();
1053
+ } else if (mode === "off") {
1054
+ s.opacity = "0";
1055
+ s.display = "none";
1056
+ this.screenOpacitySentinel.setLastWritten(0);
1057
+ this.lastScreenDisplay = "none";
1058
+ } else {
1059
+ if (this.lastScreenDisplay === "none") {
1060
+ s.display = "";
1061
+ this.lastScreenDisplay = "";
1062
+ }
1063
+ }
1064
+ }
1065
+ applySize();
1066
+ }
1067
+ /**
1068
+ * Enable or disable click-on-scrim → snap to first non-zero allowed snap.
1069
+ * Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
1070
+ * click listener actually fires.
1071
+ * @param enabled - `true` installs the click listener; `false` removes it.
1072
+ * @example
1073
+ * engine.setScrimTapToClose(true); // tap outside sheet collapses it
1074
+ * engine.setScrimTapToClose(false); // require explicit close button
1075
+ */
1076
+ setScrimTapToClose(enabled) {
1077
+ if (this.isDestroyed()) return;
1078
+ if (!this.screenComponent) return;
1079
+ const installed = this.detachScrimTap !== null;
1080
+ if (enabled === installed) return;
1081
+ if (enabled) {
1082
+ const target = this.screenComponent;
1083
+ if (target.style.pointerEvents === "none") {
1084
+ target.style.pointerEvents = "auto";
1085
+ }
1086
+ const onClick = () => {
1087
+ if (this.isDestroyed()) return;
1088
+ const allowed = this.getAllowedIds();
1089
+ const snaps = this.getResolvedSnaps();
1090
+ const fallback = allowed.find((id) => {
1091
+ const snap = findById(id, snaps);
1092
+ return snap !== null && snap.size > 0;
1093
+ });
1094
+ if (fallback) this.snapToFn(fallback);
1095
+ };
1096
+ target.addEventListener("click", onClick);
1097
+ this.detachScrimTap = () => target.removeEventListener("click", onClick);
1098
+ this.scrimTapToCloseEnabled = true;
1099
+ } else {
1100
+ this.detachScrimTap?.();
1101
+ this.detachScrimTap = null;
1102
+ this.scrimTapToCloseEnabled = false;
1103
+ }
1104
+ }
1105
+ /**
1106
+ * Toggle the scrim layer without remounting. Disabling stashes current
1107
+ * `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
1108
+ * formula → 0 across the whole range); re-enabling restores the saved values.
1109
+ * @param enabled - `false` hides scrim, `true` restores prior ranges.
1110
+ * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1111
+ * @example
1112
+ * engine.setScrimEnabled(false); // hide scrim, ranges remembered
1113
+ * engine.setScrimEnabled(true); // restore prior ranges (no remount)
1114
+ */
1115
+ setScrimEnabled(enabled, applySize) {
1116
+ if (this.isDestroyed()) return;
1117
+ if (enabled === this.scrimEnabled) return;
1118
+ this.scrimEnabled = enabled;
1119
+ if (!enabled) {
1120
+ this.savedScrimRanges = {
1121
+ screen: this.screenRange,
1122
+ backdrop: this.backdropRange
1123
+ };
1124
+ this.screenRange = [1, 1];
1125
+ this.backdropRange = [1, 1];
1126
+ } else {
1127
+ this.screenRange = this.savedScrimRanges?.screen ?? [0, 1];
1128
+ this.backdropRange = this.savedScrimRanges?.backdrop ?? [0, 1];
1129
+ this.savedScrimRanges = null;
1130
+ }
1131
+ applySize();
1132
+ }
1133
+ /**
1134
+ * Batch-update multiple scrim fields with a single applySize() at the end.
1135
+ * If `preset` is set, it seeds color/blur/interactive/range first; explicit
1136
+ * fields override.
1137
+ * @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
1138
+ * color, blur, interactive, range.
1139
+ * @param applySize - Engine-supplied recompute hook; runs ONCE per call.
1140
+ * @example
1141
+ * engine.setScrim({
1142
+ * preset: "frosted",
1143
+ * mode: "above-sheet",
1144
+ * tapToClose: true,
1145
+ * });
1146
+ */
1147
+ setScrim(opts, applySize) {
1148
+ if (this.isDestroyed()) return;
1149
+ if (opts.preset) {
1150
+ const cfg = SCRIM_PRESETS[opts.preset];
1151
+ this.applyScrimStyles({
1152
+ color: cfg.color,
1153
+ blur: cfg.blur ?? null,
1154
+ interactive: cfg.interactive
1155
+ });
1156
+ this.screenRange = cfg.range;
1157
+ }
1158
+ const noop = () => {
1159
+ };
1160
+ if (opts.mode !== void 0) this.setScrimMode(opts.mode, noop);
1161
+ if (opts.enabled !== void 0) this.setScrimEnabled(opts.enabled, noop);
1162
+ if (opts.tapToClose !== void 0) this.setScrimTapToClose(opts.tapToClose);
1163
+ this.applyScrimStyles({
1164
+ color: opts.color,
1165
+ blur: opts.blur,
1166
+ interactive: opts.interactive
1167
+ });
1168
+ if (opts.range !== void 0) {
1169
+ this.screenRange = opts.range;
1170
+ }
1171
+ this.invalidateOpacityCache();
1172
+ applySize();
1173
+ }
1174
+ /**
1175
+ * Inject a positioned floating element into the scrim layer (e.g. a close
1176
+ * button anchored top-right). Replaces any prior overlay so repeated calls
1177
+ * (reactive effects re-running) don't accumulate wrappers.
1178
+ * @param opts - Children element, optional `inset` (default `"16px"`),
1179
+ * `position` (9-way grid, default `"top-right"`), and
1180
+ * `interactive` flag (default `true`).
1181
+ * @returns Teardown function that removes the wrapper from the DOM.
1182
+ * @example
1183
+ * const teardown = engine.setScrimOverlay({
1184
+ * children: closeBtn,
1185
+ * position: "top-right",
1186
+ * inset: "20px",
1187
+ * });
1188
+ * // later: teardown(); // remove the floating button
1189
+ */
1190
+ setScrimOverlay(opts) {
1191
+ if (this.isDestroyed()) return () => {
1192
+ };
1193
+ if (!this.screenComponent) return () => {
1194
+ };
1195
+ this.scrimOverlayTeardown?.();
1196
+ const screen = this.screenComponent;
1197
+ const inset = opts.inset ?? "16px";
1198
+ const position = opts.position ?? "top-right";
1199
+ const interactive = opts.interactive ?? true;
1200
+ const wrapper = screen.ownerDocument.createElement("div");
1201
+ wrapper.className = "bs-scrim-overlay";
1202
+ const ws = wrapper.style;
1203
+ ws.position = "absolute";
1204
+ switch (position) {
1205
+ case "top-left":
1206
+ ws.top = inset;
1207
+ ws.left = inset;
1208
+ break;
1209
+ case "top-center":
1210
+ ws.top = inset;
1211
+ ws.left = "50%";
1212
+ ws.transform = "translateX(-50%)";
1213
+ break;
1214
+ case "top-right":
1215
+ ws.top = inset;
1216
+ ws.right = inset;
1217
+ break;
1218
+ case "center-left":
1219
+ ws.top = "50%";
1220
+ ws.left = inset;
1221
+ ws.transform = "translateY(-50%)";
1222
+ break;
1223
+ case "center":
1224
+ ws.top = "50%";
1225
+ ws.left = "50%";
1226
+ ws.transform = "translate(-50%, -50%)";
1227
+ break;
1228
+ case "center-right":
1229
+ ws.top = "50%";
1230
+ ws.right = inset;
1231
+ ws.transform = "translateY(-50%)";
1232
+ break;
1233
+ case "bottom-left":
1234
+ ws.bottom = inset;
1235
+ ws.left = inset;
1236
+ break;
1237
+ case "bottom-center":
1238
+ ws.bottom = inset;
1239
+ ws.left = "50%";
1240
+ ws.transform = "translateX(-50%)";
1241
+ break;
1242
+ case "bottom-right":
1243
+ ws.bottom = inset;
1244
+ ws.right = inset;
1245
+ break;
1246
+ }
1247
+ if (interactive) {
1248
+ ws.pointerEvents = "auto";
1249
+ }
1250
+ wrapper.appendChild(opts.children);
1251
+ screen.appendChild(wrapper);
1252
+ const teardown = () => {
1253
+ if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
1254
+ if (this.scrimOverlayTeardown === teardown) {
1255
+ this.scrimOverlayTeardown = null;
1256
+ }
1257
+ };
1258
+ this.scrimOverlayTeardown = teardown;
1259
+ return teardown;
1260
+ }
1261
+ /** Tear down toggle-state listeners + remove any injected overlay wrapper. */
1262
+ destroy() {
1263
+ this.detachScrimTap?.();
1264
+ this.detachScrimTap = null;
1265
+ this.scrimOverlayTeardown?.();
1266
+ this.scrimOverlayTeardown = null;
1267
+ if (this.screenComponent) {
1268
+ const s = this.screenComponent.style;
1269
+ s.opacity = "";
1270
+ s.display = "";
1271
+ }
1272
+ if (this.backdrop) {
1273
+ this.backdrop.style.opacity = "";
1274
+ this.backdrop.style.pointerEvents = "";
1275
+ }
1276
+ this.invalidateOpacityCache();
1277
+ this.lastBackdropPointer = "";
1278
+ this.lastScreenDisplay = "";
1279
+ }
1280
+ // ── Private helpers ────────────────────────────────────────────────
1281
+ /** Centralized scrim style writer — atomic 3-property write across all setters. */
1282
+ applyScrimStyles(opts) {
1283
+ if (!this.screenComponent) return;
1284
+ const s = this.screenComponent.style;
1285
+ if (opts.color !== void 0) {
1286
+ s.background = opts.color === null ? "" : opts.color;
1287
+ }
1288
+ if (opts.blur !== void 0) {
1289
+ const w = s;
1290
+ if (opts.blur === null) {
1291
+ s.backdropFilter = "";
1292
+ w.webkitBackdropFilter = "";
1293
+ } else {
1294
+ const filter = `blur(${opts.blur})`;
1295
+ s.backdropFilter = filter;
1296
+ w.webkitBackdropFilter = filter;
1297
+ }
1298
+ }
1299
+ if (opts.interactive !== void 0) {
1300
+ s.pointerEvents = opts.interactive ? "auto" : "none";
1301
+ }
1302
+ }
1303
+ /** Pin the screen overlay to the area NOT occupied by the sheet. */
1304
+ applyAboveSheetInset() {
1305
+ if (!this.screenComponent) return;
1306
+ const insetByMode = {
1307
+ bottom: "0 0 var(--bs-size) 0",
1308
+ top: "var(--bs-size) 0 0 0",
1309
+ left: "0 0 0 var(--bs-size)",
1310
+ right: "0 var(--bs-size) 0 0"
1311
+ };
1312
+ const s = this.screenComponent.style;
1313
+ s.position = "fixed";
1314
+ s.inset = insetByMode[this.mode];
1315
+ s.pointerEvents = "none";
1316
+ }
1317
+ };
1318
+ }
1319
+ });
1320
+
1321
+ // src/core/animation/animation.ts
1322
+ var easeOutBack, easeOutCubic, easeLinear, easeOutQuint, tween;
1323
+ var init_animation = __esm({
1324
+ "src/core/animation/animation.ts"() {
1325
+ easeOutBack = (t) => {
1326
+ const c1 = 1.40158;
1327
+ const c3 = c1 + 1;
1328
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
1329
+ };
1330
+ easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
1331
+ easeLinear = (t) => t;
1332
+ easeOutQuint = (t) => 1 - Math.pow(1 - t, 5);
1333
+ tween = ({
1334
+ from,
1335
+ to,
1336
+ duration,
1337
+ easing = easeOutCubic,
1338
+ onUpdate
1339
+ }) => {
1340
+ let cancelled = false;
1341
+ let rafId = 0;
1342
+ let resolveFn = null;
1343
+ const start = performance.now();
1344
+ const promise = new Promise((resolve) => {
1345
+ resolveFn = resolve;
1346
+ if (duration <= 0) {
1347
+ onUpdate(to);
1348
+ resolve();
1349
+ return;
1350
+ }
1351
+ const step = (now) => {
1352
+ if (cancelled) return;
1353
+ const elapsed = now - start;
1354
+ const t = Math.min(elapsed / duration, 1);
1355
+ const value = from + (to - from) * easing(t);
1356
+ onUpdate(value);
1357
+ if (t < 1) {
1358
+ rafId = requestAnimationFrame(step);
1359
+ } else {
1360
+ resolve();
1361
+ }
1362
+ };
1363
+ rafId = requestAnimationFrame(step);
1364
+ });
1365
+ return {
1366
+ cancel: () => {
1367
+ cancelled = true;
1368
+ cancelAnimationFrame(rafId);
1369
+ resolveFn?.();
1370
+ },
1371
+ promise
1372
+ };
1373
+ };
1374
+ }
1375
+ });
1376
+
1377
+ // src/core/animation/spring.ts
1378
+ var DEFAULT_SPRING, MAX_DT, runSpring;
1379
+ var init_spring = __esm({
1380
+ "src/core/animation/spring.ts"() {
1381
+ DEFAULT_SPRING = {
1382
+ stiffness: 220,
1383
+ damping: 26,
1384
+ mass: 1,
1385
+ restDelta: 0.5,
1386
+ restSpeed: 0.5
1387
+ };
1388
+ MAX_DT = 1 / 30;
1389
+ runSpring = ({
1390
+ from,
1391
+ to,
1392
+ velocity = 0,
1393
+ config,
1394
+ onUpdate
1395
+ }) => {
1396
+ const cfg = { ...DEFAULT_SPRING, ...config };
1397
+ let cancelled = false;
1398
+ let rafId = 0;
1399
+ let resolveFn = null;
1400
+ let x = from;
1401
+ let v = velocity;
1402
+ let lastTime = performance.now();
1403
+ const promise = new Promise((resolve) => {
1404
+ resolveFn = resolve;
1405
+ const step = (now) => {
1406
+ if (cancelled) return;
1407
+ const dt = Math.min((now - lastTime) / 1e3, MAX_DT);
1408
+ lastTime = now;
1409
+ const subDt = Math.min(dt, 1 / 240);
1410
+ let stepsLeft = Math.max(1, Math.ceil(dt / subDt));
1411
+ while (stepsLeft-- > 0) {
1412
+ const force = -cfg.stiffness * (x - to) - cfg.damping * v;
1413
+ const accel = force / cfg.mass;
1414
+ v += accel * subDt;
1415
+ x += v * subDt;
1416
+ }
1417
+ onUpdate(x, v);
1418
+ const isAtRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
1419
+ if (isAtRest) {
1420
+ onUpdate(to, 0);
1421
+ resolve();
1422
+ } else {
1423
+ rafId = requestAnimationFrame(step);
1424
+ }
1425
+ };
1426
+ rafId = requestAnimationFrame(step);
1427
+ });
1428
+ return {
1429
+ cancel: () => {
1430
+ cancelled = true;
1431
+ cancelAnimationFrame(rafId);
1432
+ resolveFn?.();
1433
+ },
1434
+ promise
1435
+ };
1436
+ };
1437
+ }
1438
+ });
1439
+
1440
+ // src/core/animation/animation-presets.ts
1441
+ function resolveAnimationPreset(animation) {
1442
+ switch (animation) {
1443
+ case "ios-spring":
1444
+ return {
1445
+ kind: "spring",
1446
+ spring: { stiffness: 300, damping: 30, mass: 1 }
1447
+ };
1448
+ case "material-bounce":
1449
+ return {
1450
+ kind: "spring",
1451
+ spring: { stiffness: 200, damping: 22, mass: 1 }
1452
+ };
1453
+ case "linear":
1454
+ return { kind: "tween", duration: DEFAULT_DURATION, easing: easeLinear };
1455
+ case "snappy":
1456
+ return { kind: "tween", duration: 180, easing: easeOutQuint };
1457
+ case "tween":
1458
+ return { kind: "tween" };
1459
+ case "spring":
1460
+ case void 0:
1461
+ default:
1462
+ return { kind: "spring" };
1463
+ }
1464
+ }
1465
+ var DEFAULT_DURATION;
1466
+ var init_animation_presets = __esm({
1467
+ "src/core/animation/animation-presets.ts"() {
1468
+ init_animation();
1469
+ DEFAULT_DURATION = 220;
1470
+ }
1471
+ });
1472
+
1473
+ // src/core/controllers/animation-runner.ts
1474
+ var DEFAULT_DURATION2, VELOCITY_PX_PER_S, AnimationRunner;
1475
+ var init_animation_runner = __esm({
1476
+ "src/core/controllers/animation-runner.ts"() {
1477
+ init_animation();
1478
+ init_spring();
1479
+ init_animation_presets();
1480
+ DEFAULT_DURATION2 = 220;
1481
+ VELOCITY_PX_PER_S = 1e3;
1482
+ AnimationRunner = class {
1483
+ constructor(deps, opts) {
1484
+ __publicField(this, "element");
1485
+ __publicField(this, "getRootEl");
1486
+ __publicField(this, "applySizeFn");
1487
+ __publicField(this, "getSize");
1488
+ __publicField(this, "isDragging");
1489
+ __publicField(this, "animationKind");
1490
+ __publicField(this, "springConfig");
1491
+ __publicField(this, "duration");
1492
+ __publicField(this, "easing");
1493
+ __publicField(this, "respectReducedMotion");
1494
+ /** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
1495
+ __publicField(this, "reducedMotion", false);
1496
+ /** Disposes the matchMedia change listener — drained from destroy(). */
1497
+ __publicField(this, "detachReducedMotion", null);
1498
+ /** Computed once — both inputs (option + API presence) stable for life. */
1499
+ __publicField(this, "viewTransitionsAvailable");
1500
+ /** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
1501
+ __publicField(this, "currentTween", null);
1502
+ __publicField(this, "currentSpring", null);
1503
+ this.element = deps.element;
1504
+ this.getRootEl = deps.getRootEl;
1505
+ this.applySizeFn = deps.applySize;
1506
+ this.getSize = deps.getSize;
1507
+ this.isDragging = deps.isDragging;
1508
+ const preset = resolveAnimationPreset(opts.animation);
1509
+ this.animationKind = preset.kind;
1510
+ this.duration = opts.duration ?? preset.duration ?? DEFAULT_DURATION2;
1511
+ this.easing = opts.easing ?? preset.easing ?? easeOutBack;
1512
+ this.springConfig = { ...preset.spring ?? {}, ...opts.spring ?? {} };
1513
+ this.respectReducedMotion = opts.respectReducedMotion ?? true;
1514
+ const viewTransitionsEnabled = opts.viewTransitions ?? false;
1515
+ this.viewTransitionsAvailable = viewTransitionsEnabled && typeof document !== "undefined" && typeof document.startViewTransition === "function";
1516
+ if (this.respectReducedMotion && typeof window !== "undefined" && typeof window.matchMedia === "function") {
1517
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
1518
+ this.reducedMotion = mq.matches;
1519
+ const onMqChange = (e) => {
1520
+ this.reducedMotion = e.matches;
1521
+ };
1522
+ if (typeof mq.addEventListener === "function") {
1523
+ mq.addEventListener("change", onMqChange);
1524
+ this.detachReducedMotion = () => mq.removeEventListener("change", onMqChange);
1525
+ } else if (typeof mq.addListener === "function") {
1526
+ mq.addListener(onMqChange);
1527
+ this.detachReducedMotion = () => mq.removeListener(onMqChange);
1528
+ }
1529
+ }
1530
+ }
1531
+ /**
1532
+ * Live derived flag — `true` whenever a spring/tween handle is in flight.
1533
+ * Reading the canonical handles (instead of mirroring into a separate
1534
+ * boolean) eliminates the reduced-motion early-return latent bug where
1535
+ * the flag stayed true forever, and removes the cross-controller
1536
+ * setIsAnimating callback.
1537
+ */
1538
+ get isAnimating() {
1539
+ return this.currentTween !== null || this.currentSpring !== null;
1540
+ }
1541
+ /** Cancel any in-flight tween/spring without touching engine state. */
1542
+ cancel() {
1543
+ this.currentTween?.cancel();
1544
+ this.currentSpring?.cancel();
1545
+ }
1546
+ /**
1547
+ * Animate from current size to `target`. Resolves when the animation
1548
+ * completes; resolves early if cancelled mid-flight (caller checks an
1549
+ * abort signal post-await for cycle-invariant correctness).
1550
+ */
1551
+ async animateTo(target, velocityPxPerMs) {
1552
+ if (target === this.getSize()) return;
1553
+ this.currentTween?.cancel();
1554
+ this.currentSpring?.cancel();
1555
+ this.getRootEl()?.setAttribute("data-animating", "true");
1556
+ this.element.style.willChange = "transform";
1557
+ if (this.reducedMotion) {
1558
+ this.applySizeFn(target);
1559
+ this.getRootEl()?.removeAttribute("data-animating");
1560
+ if (!this.isDragging()) this.element.style.willChange = "auto";
1561
+ return;
1562
+ }
1563
+ if (this.animationKind === "spring") {
1564
+ const spring = runSpring({
1565
+ from: this.getSize(),
1566
+ to: target,
1567
+ velocity: velocityPxPerMs * VELOCITY_PX_PER_S,
1568
+ config: this.springConfig,
1569
+ onUpdate: (v) => this.applySizeFn(v)
1570
+ });
1571
+ this.currentSpring = spring;
1572
+ await spring.promise;
1573
+ if (this.currentSpring === spring) this.currentSpring = null;
1574
+ } else {
1575
+ const tw = tween({
1576
+ from: this.getSize(),
1577
+ to: target,
1578
+ duration: this.duration,
1579
+ easing: this.easing,
1580
+ onUpdate: (v) => this.applySizeFn(v)
1581
+ });
1582
+ this.currentTween = tw;
1583
+ await tw.promise;
1584
+ if (this.currentTween === tw) this.currentTween = null;
1585
+ }
1586
+ if (!this.isAnimating) {
1587
+ this.getRootEl()?.removeAttribute("data-animating");
1588
+ if (!this.isDragging()) this.element.style.willChange = "auto";
1589
+ }
1590
+ }
1591
+ /** Tear down the matchMedia listener. Engine calls from destroy(). */
1592
+ destroy() {
1593
+ this.detachReducedMotion?.();
1594
+ this.detachReducedMotion = null;
1595
+ this.currentTween = null;
1596
+ this.currentSpring = null;
1597
+ }
1598
+ };
1599
+ }
1600
+ });
1601
+
1602
+ // src/core/lifecycle/focusTrap.ts
1603
+ var FOCUSABLE, isVisible, focusables, installFocusTrap;
1604
+ var init_focusTrap = __esm({
1605
+ "src/core/lifecycle/focusTrap.ts"() {
1606
+ FOCUSABLE = [
1607
+ "a[href]",
1608
+ "area[href]",
1609
+ "button:not([disabled])",
1610
+ "input:not([disabled]):not([type='hidden'])",
1611
+ "select:not([disabled])",
1612
+ "textarea:not([disabled])",
1613
+ "iframe",
1614
+ "object",
1615
+ "embed",
1616
+ "[contenteditable='true']",
1617
+ "[tabindex]:not([tabindex='-1'])"
1618
+ ].join(",");
1619
+ isVisible = (el) => {
1620
+ if (typeof el.checkVisibility === "function") {
1621
+ return el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true });
1622
+ }
1623
+ return el.offsetParent !== null;
1624
+ };
1625
+ focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
1626
+ (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
1627
+ );
1628
+ installFocusTrap = (container, options = {}) => {
1629
+ if (typeof document === "undefined") return () => {
1630
+ };
1631
+ const previouslyFocused = document.activeElement;
1632
+ const focusInitial = () => {
1633
+ const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
1634
+ initial?.focus({ preventScroll: true });
1635
+ };
1636
+ focusInitial();
1637
+ const handleKey = (e) => {
1638
+ if (e.key === "Escape" && options.onEscape) {
1639
+ options.onEscape();
1640
+ return;
1641
+ }
1642
+ if (e.key !== "Tab") return;
1643
+ const list = focusables(container);
1644
+ if (list.length === 0) {
1645
+ e.preventDefault();
1646
+ return;
1647
+ }
1648
+ const first = list[0];
1649
+ const last = list[list.length - 1];
1650
+ const active = document.activeElement;
1651
+ if (e.shiftKey && active === first) {
1652
+ e.preventDefault();
1653
+ last.focus();
1654
+ } else if (!e.shiftKey && active === last) {
1655
+ e.preventDefault();
1656
+ first.focus();
1657
+ }
1658
+ };
1659
+ const handleFocusIn = (e) => {
1660
+ const target = e.target;
1661
+ if (target && !container.contains(target)) {
1662
+ const list = focusables(container);
1663
+ list[0]?.focus({ preventScroll: true });
1664
+ }
1665
+ };
1666
+ document.addEventListener("keydown", handleKey, true);
1667
+ document.addEventListener("focusin", handleFocusIn, true);
1668
+ let released = false;
1669
+ return () => {
1670
+ if (released) return;
1671
+ released = true;
1672
+ document.removeEventListener("keydown", handleKey, true);
1673
+ document.removeEventListener("focusin", handleFocusIn, true);
1674
+ if (previouslyFocused && document.contains(previouslyFocused)) {
1675
+ previouslyFocused.focus({ preventScroll: true });
1676
+ } else {
1677
+ const fallback = document.querySelector("[data-bs-restore-fallback]");
1678
+ fallback?.focus({ preventScroll: true });
1679
+ }
1680
+ };
1681
+ };
1682
+ }
1683
+ });
1684
+
1685
+ // src/core/lifecycle/scrollLock.ts
1686
+ var lockCount, savedStyles, lockBodyScroll;
1687
+ var init_scrollLock = __esm({
1688
+ "src/core/lifecycle/scrollLock.ts"() {
1689
+ lockCount = 0;
1690
+ savedStyles = null;
1691
+ lockBodyScroll = () => {
1692
+ if (typeof document === "undefined") return () => {
1693
+ };
1694
+ if (lockCount === 0) {
1695
+ const body = document.body;
1696
+ savedStyles = {
1697
+ overflow: body.style.overflow,
1698
+ position: body.style.position,
1699
+ top: body.style.top,
1700
+ width: body.style.width,
1701
+ scrollY: window.scrollY
1702
+ };
1703
+ body.style.overflow = "hidden";
1704
+ body.style.position = "fixed";
1705
+ body.style.top = `-${savedStyles.scrollY}px`;
1706
+ body.style.width = "100%";
1707
+ }
1708
+ lockCount++;
1709
+ let released = false;
1710
+ return () => {
1711
+ if (released) return;
1712
+ released = true;
1713
+ lockCount = Math.max(0, lockCount - 1);
1714
+ if (lockCount === 0 && savedStyles) {
1715
+ const body = document.body;
1716
+ body.style.overflow = savedStyles.overflow;
1717
+ body.style.position = savedStyles.position;
1718
+ body.style.top = savedStyles.top;
1719
+ body.style.width = savedStyles.width;
1720
+ window.scrollTo(0, savedStyles.scrollY);
1721
+ savedStyles = null;
1722
+ }
1723
+ };
1724
+ };
1725
+ }
1726
+ });
1727
+
1728
+ // src/core/features/inert-siblings.ts
1729
+ function createInertSiblings(rootProvider) {
1730
+ const tracked = [];
1731
+ return {
1732
+ apply() {
1733
+ if (typeof document === "undefined") return;
1734
+ const root = rootProvider();
1735
+ const parent = root.parentElement ?? document.body;
1736
+ for (const child of Array.from(parent.children)) {
1737
+ if (child === root) continue;
1738
+ if (child.hasAttribute("inert")) continue;
1739
+ child.setAttribute("inert", "");
1740
+ tracked.push(child);
1741
+ }
1742
+ },
1743
+ remove() {
1744
+ for (const el of tracked) el.removeAttribute("inert");
1745
+ tracked.length = 0;
1746
+ }
1747
+ };
1748
+ }
1749
+ var init_inert_siblings = __esm({
1750
+ "src/core/features/inert-siblings.ts"() {
1751
+ }
1752
+ });
1753
+
1754
+ // src/core/controllers/lifecycle-controller.ts
1755
+ var LifecycleController;
1756
+ var init_lifecycle_controller = __esm({
1757
+ "src/core/controllers/lifecycle-controller.ts"() {
1758
+ init_focusTrap();
1759
+ init_scrollLock();
1760
+ init_inert_siblings();
1761
+ LifecycleController = class {
1762
+ constructor(deps, opts) {
1763
+ __publicField(this, "element");
1764
+ __publicField(this, "closeFn");
1765
+ __publicField(this, "focusTrapEnabled");
1766
+ __publicField(this, "initialFocus");
1767
+ __publicField(this, "closeOnEscape");
1768
+ __publicField(this, "bodyScrollLockEnabled");
1769
+ __publicField(this, "inertSiblingsEnabled");
1770
+ __publicField(this, "inertSiblings");
1771
+ __publicField(this, "shouldApplyInertSiblings");
1772
+ __publicField(this, "returnFocus");
1773
+ /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
1774
+ __publicField(this, "releaseFocusTrap", null);
1775
+ __publicField(this, "releaseScrollLock", null);
1776
+ /** True between install() and release(). Guards release()/destroy() from
1777
+ * re-running side effects (notably `returnFocus.focus()`) on already-
1778
+ * released or never-installed instances. */
1779
+ __publicField(this, "installed", false);
1780
+ __publicField(this, "destroyed", false);
1781
+ this.element = deps.element;
1782
+ this.closeFn = deps.close;
1783
+ this.focusTrapEnabled = opts.focusTrap ?? false;
1784
+ this.initialFocus = opts.initialFocus;
1785
+ this.closeOnEscape = opts.closeOnEscape ?? true;
1786
+ this.bodyScrollLockEnabled = opts.lockBodyScroll ?? true;
1787
+ this.inertSiblingsEnabled = opts.inertSiblings ?? false;
1788
+ this.shouldApplyInertSiblings = opts.shouldApplyInertSiblings ?? (() => true);
1789
+ this.returnFocus = opts.returnFocus;
1790
+ this.inertSiblings = createInertSiblings(() => this.element);
1791
+ }
1792
+ /**
1793
+ * Run the open-time install steps. Idempotent: each step checks its
1794
+ * existing teardown ref so a re-open during a half-cancelled close
1795
+ * doesn't double-install. Sets `installed=true` BEFORE the first side
1796
+ * effect so a partial throw (detached / shadow-DOM target) leaves the
1797
+ * caller's `release()` rollback path armed to undo whatever did succeed.
1798
+ * No-op when destroyed.
1799
+ */
1800
+ install() {
1801
+ if (this.destroyed) return;
1802
+ this.installed = true;
1803
+ if (this.bodyScrollLockEnabled && !this.releaseScrollLock) {
1804
+ this.releaseScrollLock = lockBodyScroll();
1805
+ }
1806
+ if (this.focusTrapEnabled && !this.releaseFocusTrap) {
1807
+ this.releaseFocusTrap = installFocusTrap(this.element, {
1808
+ initialFocus: this.initialFocus,
1809
+ onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
1810
+ });
1811
+ }
1812
+ if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
1813
+ this.inertSiblings.apply();
1814
+ }
1815
+ }
1816
+ /**
1817
+ * Update the post-release focus target. Overlay re-binds this when the
1818
+ * consumer changes `returnFocus` after construction; the new value applies
1819
+ * on the next `release()`. No-op when destroyed.
1820
+ */
1821
+ setReturnFocus(target) {
1822
+ if (this.destroyed) return;
1823
+ this.returnFocus = target;
1824
+ }
1825
+ /**
1826
+ * Release all install-time teardowns. Short-circuits when not installed,
1827
+ * so `close() → destroy()` (each routing through here) only restores
1828
+ * focus once. The `installed` guard lets `inertSiblings.remove()` and
1829
+ * the focus-trap/scroll-lock teardown nullings stay simple — they were
1830
+ * already idempotent, but `returnFocus.focus()` was firing on every call.
1831
+ */
1832
+ release() {
1833
+ if (!this.installed) return;
1834
+ this.installed = false;
1835
+ this.releaseFocusTrap?.();
1836
+ this.releaseFocusTrap = null;
1837
+ this.releaseScrollLock?.();
1838
+ this.releaseScrollLock = null;
1839
+ this.inertSiblings.remove();
1840
+ if (this.returnFocus) {
1841
+ const target = typeof this.returnFocus === "function" ? this.returnFocus() : this.returnFocus;
1842
+ target?.focus?.();
1843
+ }
1844
+ }
1845
+ /**
1846
+ * Final teardown — releases any still-installed listeners and seals the
1847
+ * controller. After destroy(), install() is a no-op and setReturnFocus()
1848
+ * is rejected, so post-destroy mutations can't queue up a stale focus
1849
+ * restore.
1850
+ */
1851
+ destroy() {
1852
+ this.release();
1853
+ this.destroyed = true;
1854
+ }
1855
+ };
1856
+ }
1857
+ });
1858
+
1859
+ // src/core/gestures.ts
1860
+ var isAxisVertical, SampleRing, installGestures;
1861
+ var init_gestures = __esm({
1862
+ "src/core/gestures.ts"() {
1863
+ isAxisVertical = (mode) => mode === "bottom" || mode === "top";
1864
+ SampleRing = class {
1865
+ // current count, ≤ capacity
1866
+ constructor(capacity) {
1867
+ __publicField(this, "slots");
1868
+ __publicField(this, "capacity");
1869
+ __publicField(this, "head", 0);
1870
+ // next write index
1871
+ __publicField(this, "size", 0);
1872
+ this.capacity = capacity;
1873
+ this.slots = new Array(capacity);
1874
+ for (let i = 0; i < capacity; i++) this.slots[i] = { t: 0, v: 0 };
1875
+ }
1876
+ push(t, v) {
1877
+ const slot = this.slots[this.head];
1878
+ slot.t = t;
1879
+ slot.v = v;
1880
+ this.head = (this.head + 1) % this.capacity;
1881
+ if (this.size < this.capacity) this.size++;
1882
+ }
1883
+ /**
1884
+ * Drop oldest entries whose timestamp is below `cutoff`, but never shrink
1885
+ * below `minKeep`. Mirrors the original
1886
+ * `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
1887
+ * but without copying — just advances the logical tail.
1888
+ */
1889
+ pruneBefore(cutoff, minKeep) {
1890
+ while (this.size > minKeep) {
1891
+ const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
1892
+ if (this.slots[oldestIdx].t < cutoff) this.size--;
1893
+ else break;
1894
+ }
1895
+ }
1896
+ get length() {
1897
+ return this.size;
1898
+ }
1899
+ oldest() {
1900
+ if (this.size === 0) return void 0;
1901
+ return this.slots[(this.head - this.size + this.capacity) % this.capacity];
1902
+ }
1903
+ newest() {
1904
+ if (this.size === 0) return void 0;
1905
+ return this.slots[(this.head - 1 + this.capacity) % this.capacity];
1906
+ }
1907
+ reset() {
1908
+ this.head = 0;
1909
+ this.size = 0;
1910
+ }
1911
+ };
1912
+ installGestures = (handle, mode, callbacks) => {
1913
+ let activePointerId = null;
1914
+ let activePointerType = "touch";
1915
+ let startCoord = 0;
1916
+ let lastCoord = 0;
1917
+ const VELOCITY_WINDOW_MS = (kind) => kind === "mouse" ? 160 : 120;
1918
+ const MAX_SAMPLES = 32;
1919
+ const samples = new SampleRing(MAX_SAMPLES);
1920
+ const axis = isAxisVertical(mode) ? "Y" : "X";
1921
+ const sign = mode === "bottom" || mode === "right" ? -1 : 1;
1922
+ const coordOf = (e) => axis === "Y" ? e.clientY : e.clientX;
1923
+ const onPointerDown = (e) => {
1924
+ if (activePointerId !== null) return;
1925
+ if (e.button !== void 0 && e.button !== 0) return;
1926
+ activePointerId = e.pointerId;
1927
+ activePointerType = e.pointerType || "touch";
1928
+ startCoord = coordOf(e);
1929
+ lastCoord = startCoord;
1930
+ samples.reset();
1931
+ samples.push(e.timeStamp, startCoord);
1932
+ try {
1933
+ handle.setPointerCapture(e.pointerId);
1934
+ } catch {
1935
+ }
1936
+ callbacks.onStart(startCoord, activePointerType);
1937
+ };
1938
+ const onPointerMove = (e) => {
1939
+ if (e.pointerId !== activePointerId) return;
1940
+ const coord = coordOf(e);
1941
+ const rawDelta = coord - startCoord;
1942
+ const delta = rawDelta * sign;
1943
+ lastCoord = coord;
1944
+ samples.push(e.timeStamp, coord);
1945
+ const cutoff = e.timeStamp - VELOCITY_WINDOW_MS(activePointerType);
1946
+ samples.pruneBefore(cutoff, 2);
1947
+ callbacks.onMove(delta);
1948
+ };
1949
+ const finishGesture = (e) => {
1950
+ if (e.pointerId !== activePointerId) return;
1951
+ const rawDelta = lastCoord - startCoord;
1952
+ const delta = rawDelta * sign;
1953
+ let velocity = 0;
1954
+ if (samples.length >= 3) {
1955
+ const first = samples.oldest();
1956
+ const last = samples.newest();
1957
+ const dt = last.t - first.t;
1958
+ if (dt > 0) velocity = (last.v - first.v) / dt * sign;
1959
+ }
1960
+ const finishedKind = activePointerType;
1961
+ activePointerId = null;
1962
+ samples.reset();
1963
+ try {
1964
+ if (handle.hasPointerCapture(e.pointerId)) {
1965
+ handle.releasePointerCapture(e.pointerId);
1966
+ }
1967
+ } catch {
1968
+ }
1969
+ callbacks.onEnd(delta, velocity, finishedKind);
1970
+ };
1971
+ const onPointerCancel = (e) => {
1972
+ if (e.pointerId !== activePointerId) return;
1973
+ activePointerId = null;
1974
+ samples.reset();
1975
+ callbacks.onCancel?.();
1976
+ };
1977
+ handle.addEventListener("pointerdown", onPointerDown);
1978
+ handle.addEventListener("pointermove", onPointerMove);
1979
+ handle.addEventListener("pointerup", finishGesture);
1980
+ handle.addEventListener("pointercancel", onPointerCancel);
1981
+ handle.style.touchAction = isAxisVertical(mode) ? "pan-x" : "pan-y";
1982
+ return () => {
1983
+ handle.removeEventListener("pointerdown", onPointerDown);
1984
+ handle.removeEventListener("pointermove", onPointerMove);
1985
+ handle.removeEventListener("pointerup", finishGesture);
1986
+ handle.removeEventListener("pointercancel", onPointerCancel);
1987
+ if (activePointerId !== null && handle.hasPointerCapture(activePointerId)) {
1988
+ handle.releasePointerCapture(activePointerId);
1989
+ }
1990
+ };
1991
+ };
1992
+ }
1993
+ });
1994
+
1995
+ // src/core/primitives/rubber-band.ts
1996
+ function rubberBand(overshoot, maxAxisSize) {
1997
+ const cap = Math.min(maxAxisSize * 0.15, 80);
1998
+ return Math.sign(overshoot) * Math.pow(Math.abs(overshoot), RUBBER_BAND_FACTOR) * Math.min(6, cap / Math.max(Math.abs(overshoot), 1));
1999
+ }
2000
+ var RUBBER_BAND_FACTOR;
2001
+ var init_rubber_band = __esm({
2002
+ "src/core/primitives/rubber-band.ts"() {
2003
+ RUBBER_BAND_FACTOR = 0.55;
2004
+ }
2005
+ });
2006
+
2007
+ // src/core/features/soft-keyboard.ts
2008
+ function dismissSoftKeyboardIfFocused(root) {
2009
+ if (typeof document === "undefined") return false;
2010
+ const active = document.activeElement;
2011
+ if (!active) return false;
2012
+ if (!root.contains(active)) return false;
2013
+ const tag = active.tagName;
2014
+ const isEditable = tag === "INPUT" || tag === "TEXTAREA" || active.isContentEditable === true;
2015
+ if (!isEditable) return false;
2016
+ try {
2017
+ active.blur();
2018
+ return true;
2019
+ } catch {
2020
+ return false;
2021
+ }
2022
+ }
2023
+ var init_soft_keyboard = __esm({
2024
+ "src/core/features/soft-keyboard.ts"() {
2025
+ }
2026
+ });
2027
+
2028
+ // src/core/controllers/gesture-controller.ts
2029
+ var GestureController;
2030
+ var init_gesture_controller = __esm({
2031
+ "src/core/controllers/gesture-controller.ts"() {
2032
+ init_gestures();
2033
+ init_rubber_band();
2034
+ init_soft_keyboard();
2035
+ GestureController = class {
2036
+ constructor(deps) {
2037
+ __publicField(this, "deps");
2038
+ __publicField(this, "detach", null);
2039
+ __publicField(this, "isDragging_", false);
2040
+ __publicField(this, "dragStartSize", 0);
2041
+ // Only "touch" gestures are eligible for soft-keyboard dismissal —
2042
+ // mouse/trackpad users don't have a keyboard to dismiss.
2043
+ __publicField(this, "currentPointerKind", "touch");
2044
+ // One blur per drag — flipping focus every move event races the keyboard's
2045
+ // own animations on iOS and makes the page jitter.
2046
+ __publicField(this, "keyboardDismissed", false);
2047
+ // Reused payload object for the "drag" event — mutated each onMove instead
2048
+ // of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
2049
+ // would otherwise churn tens of KB through the GC. Mirrors browser
2050
+ // PointerEvent semantics: consumers must NOT retain the reference across
2051
+ // frames (clone if needed). See SheetEventMap.drag JSDoc.
2052
+ __publicField(this, "dragPayload", { size: 0, delta: 0 });
2053
+ this.deps = deps;
2054
+ }
2055
+ /** True between `dragstart` and `dragend`/`onCancel`. */
2056
+ get isDragging() {
2057
+ return this.isDragging_;
2058
+ }
2059
+ /**
2060
+ * Bind pointer listeners on the handle and return a teardown. Engine pushes
2061
+ * the teardown into its TeardownStack so destroy() drains it once.
2062
+ */
2063
+ install() {
2064
+ if (this.detach) return this.detach;
2065
+ this.detach = installGestures(this.deps.handle, this.deps.mode, {
2066
+ onStart: (_coord, kind) => {
2067
+ this.deps.cancelAnimation();
2068
+ this.isDragging_ = true;
2069
+ this.dragStartSize = this.deps.getDragContext().size;
2070
+ this.currentPointerKind = kind;
2071
+ this.keyboardDismissed = false;
2072
+ this.deps.getRoot()?.setAttribute("data-dragging", "true");
2073
+ this.deps.element.style.willChange = "transform";
2074
+ this.deps.emit("dragstart", { size: this.dragStartSize });
2075
+ },
2076
+ onMove: (delta) => {
2077
+ const ctx = this.deps.getDragContext();
2078
+ const { min, max } = ctx.range;
2079
+ const maxAxis = ctx.maxAxisSize;
2080
+ const rubberOn = ctx.rubberBandEnabled;
2081
+ let next = this.dragStartSize + delta;
2082
+ if (next > max) {
2083
+ next = rubberOn ? max + rubberBand(next - max, maxAxis) : max;
2084
+ } else if (next < min) {
2085
+ next = rubberOn ? min - rubberBand(min - next, maxAxis) : min;
2086
+ }
2087
+ this.deps.applySize(next);
2088
+ if (!this.keyboardDismissed && this.currentPointerKind === "touch" && this.deps.mode === "bottom" && next < this.dragStartSize) {
2089
+ if (dismissSoftKeyboardIfFocused(this.deps.element)) {
2090
+ this.keyboardDismissed = true;
2091
+ }
2092
+ }
2093
+ if (this.deps.listenerCount("drag") > 0) {
2094
+ this.dragPayload.size = next;
2095
+ this.dragPayload.delta = delta;
2096
+ this.deps.emit("drag", this.dragPayload);
2097
+ }
2098
+ },
2099
+ onEnd: (delta, velocity, kind) => {
2100
+ this.isDragging_ = false;
2101
+ this.keyboardDismissed = false;
2102
+ this.deps.getRoot()?.removeAttribute("data-dragging");
2103
+ this.deps.emit("dragend", {
2104
+ size: this.deps.getDragContext().size,
2105
+ velocity
2106
+ });
2107
+ this.deps.settleAfterDrag(delta, velocity, kind);
2108
+ },
2109
+ onCancel: () => {
2110
+ this.isDragging_ = false;
2111
+ this.keyboardDismissed = false;
2112
+ this.deps.getRoot()?.removeAttribute("data-dragging");
2113
+ const restoreTo = this.dragStartSize;
2114
+ void this.deps.animateTo(restoreTo, 0);
2115
+ }
2116
+ });
2117
+ return this.detach;
2118
+ }
2119
+ /**
2120
+ * Force-clear drag visual state. Engine calls from destroy() when destroyed
2121
+ * mid-drag — gesture detach won't synthesize an onCancel, so we have to
2122
+ * roll back the data-attribute + will-change ourselves.
2123
+ */
2124
+ forceClearDragState() {
2125
+ if (!this.isDragging_) return;
2126
+ this.isDragging_ = false;
2127
+ this.keyboardDismissed = false;
2128
+ this.deps.getRoot()?.removeAttribute("data-dragging");
2129
+ this.deps.element.style.willChange = "auto";
2130
+ }
2131
+ };
2132
+ }
2133
+ });
2134
+
2135
+ // src/core/primitives/aria-slider-writer.ts
2136
+ var AriaSliderWriter;
2137
+ var init_aria_slider_writer = __esm({
2138
+ "src/core/primitives/aria-slider-writer.ts"() {
2139
+ AriaSliderWriter = class {
2140
+ constructor(handle, mode) {
2141
+ __publicField(this, "handle", handle);
2142
+ handle.setAttribute(
2143
+ "aria-orientation",
2144
+ mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
2145
+ );
2146
+ }
2147
+ /**
2148
+ * Update slider position. `allowedIds` length determines `aria-valuemax`;
2149
+ * `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
2150
+ * Empty list → strip every dynamic attr so AT skips the handle entirely.
2151
+ */
2152
+ setValue(allowedIds, activeId) {
2153
+ if (allowedIds.length === 0) {
2154
+ this.handle.removeAttribute("aria-valuemin");
2155
+ this.handle.removeAttribute("aria-valuemax");
2156
+ this.handle.removeAttribute("aria-valuenow");
2157
+ this.handle.removeAttribute("aria-valuetext");
2158
+ return;
2159
+ }
2160
+ const max = Math.max(allowedIds.length - 1, 0);
2161
+ const idx = Math.max(allowedIds.indexOf(activeId), 0);
2162
+ this.handle.setAttribute("aria-valuemin", "0");
2163
+ this.handle.setAttribute("aria-valuemax", String(max));
2164
+ this.handle.setAttribute("aria-valuenow", String(idx));
2165
+ this.handle.setAttribute("aria-valuetext", activeId);
2166
+ }
2167
+ };
2168
+ }
2169
+ });
2170
+
2171
+ // src/core/features/scroll-cache.ts
2172
+ function createScrollCache(deps) {
2173
+ const positions = /* @__PURE__ */ new Map();
2174
+ const isSmallSize = (size) => {
2175
+ const max = deps.getMaxAxisSize();
2176
+ if (max <= 0) return false;
2177
+ return size < max / 2;
2178
+ };
2179
+ return {
2180
+ cache(fromId, prevSize, nextSize) {
2181
+ const container = deps.scrollContainer;
2182
+ if (!container) return;
2183
+ if (isSmallSize(prevSize)) return;
2184
+ if (!isSmallSize(nextSize)) return;
2185
+ positions.set(fromId, container.scrollTop);
2186
+ },
2187
+ restore(toId, prevSize, nextSize) {
2188
+ const container = deps.scrollContainer;
2189
+ if (!container) return;
2190
+ if (isSmallSize(nextSize)) return;
2191
+ if (!isSmallSize(prevSize)) return;
2192
+ const cached = positions.get(toId);
2193
+ if (cached === void 0) return;
2194
+ container.scrollTop = cached;
2195
+ positions.delete(toId);
2196
+ },
2197
+ clear() {
2198
+ positions.clear();
2199
+ }
2200
+ };
2201
+ }
2202
+ var init_scroll_cache = __esm({
2203
+ "src/core/features/scroll-cache.ts"() {
2204
+ }
2205
+ });
2206
+
2207
+ // src/core/primitives/engine-options.ts
2208
+ function resolveEngineOptions(opts, extras) {
2209
+ const initialAllowed = opts.allowed ?? opts.snapPoints.map((p) => p.id);
2210
+ let initialId = opts.initial ?? initialAllowed[0] ?? opts.snapPoints[0]?.id ?? "default";
2211
+ if (opts.persistKey) {
2212
+ const restored = readPersistedId(opts.persistKey);
2213
+ if (restored && initialAllowed.includes(restored)) {
2214
+ initialId = restored;
2215
+ }
2216
+ }
2217
+ return {
2218
+ mode: opts.mode ?? "bottom",
2219
+ flickVelocity: opts.flickVelocity ?? DEFAULT_FLICK_VELOCITY,
2220
+ dragThreshold: opts.dragThreshold ?? DEFAULT_DRAG_THRESHOLD,
2221
+ rubberBandEnabled: opts.rubberBand ?? true,
2222
+ closeOnBack: opts.closeOnBack ?? false,
2223
+ initialAllowed,
2224
+ initialId,
2225
+ scrim: {
2226
+ scrimMode: opts.scrimMode,
2227
+ scrimColor: opts.scrimColor,
2228
+ scrimBlur: opts.scrimBlur,
2229
+ // Auto-promote pointer-events when tap-to-close is on so the click
2230
+ // listener actually receives events. Caller can still force `false`
2231
+ // explicitly to opt out — `??` only kicks in for `undefined`.
2232
+ scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
2233
+ scrimTapToClose: opts.scrimTapToClose,
2234
+ scrimPreset: opts.scrimPreset,
2235
+ screenRange: opts.screenRange,
2236
+ backdropRange: opts.backdropRange
2237
+ },
2238
+ animation: {
2239
+ animation: opts.animation,
2240
+ duration: opts.duration,
2241
+ easing: opts.easing,
2242
+ spring: opts.spring,
2243
+ respectReducedMotion: opts.respectReducedMotion,
2244
+ viewTransitions: opts.viewTransitions
2245
+ },
2246
+ lifecycle: {
2247
+ focusTrap: opts.focusTrap,
2248
+ initialFocus: opts.initialFocus,
2249
+ closeOnEscape: opts.closeOnEscape,
2250
+ lockBodyScroll: opts.lockBodyScroll,
2251
+ inertSiblings: opts.inertSiblings,
2252
+ // Folded in from `extras` so the lifecycle bag is fully assembled at
2253
+ // the call site — engine no longer needs a spread+override at
2254
+ // controller construction. When `extras` is omitted, the field stays
2255
+ // undefined and the controller uses its own default predicate.
2256
+ shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
2257
+ }
2258
+ };
2259
+ }
2260
+ var DEFAULT_FLICK_VELOCITY, DEFAULT_DRAG_THRESHOLD;
2261
+ var init_engine_options = __esm({
2262
+ "src/core/primitives/engine-options.ts"() {
2263
+ init_persist();
2264
+ DEFAULT_FLICK_VELOCITY = 0.65;
2265
+ DEFAULT_DRAG_THRESHOLD = 18;
2266
+ }
2267
+ });
2268
+
2269
+ // src/core/BottomSheetEngine.ts
2270
+ var HAPTIC_DURATION_MS, TeardownStack, BottomSheetEngine;
2271
+ var init_BottomSheetEngine = __esm({
2272
+ "src/core/BottomSheetEngine.ts"() {
2273
+ init_sheetStack();
2274
+ init_transform();
2275
+ init_instance_id();
2276
+ init_event_bus();
2277
+ init_snap_points();
2278
+ init_snap_resolver();
2279
+ init_persist();
2280
+ init_auto_collapse();
2281
+ init_linked_sheets();
2282
+ init_route();
2283
+ init_content_swipe();
2284
+ init_visual_viewport();
2285
+ init_resize_observer();
2286
+ init_slider_keyboard();
2287
+ init_scrim_controller();
2288
+ init_animation_runner();
2289
+ init_lifecycle_controller();
2290
+ init_gesture_controller();
2291
+ init_aria_slider_writer();
2292
+ init_hot_path_thresholds();
2293
+ init_scroll_cache();
2294
+ init_engine_options();
2295
+ init_opacity_dedup();
2296
+ HAPTIC_DURATION_MS = 8;
2297
+ TeardownStack = class {
2298
+ constructor() {
2299
+ __publicField(this, "fns", []);
2300
+ }
2301
+ add(fn) {
2302
+ if (fn) this.fns.push(fn);
2303
+ }
2304
+ drain() {
2305
+ while (this.fns.length) {
2306
+ try {
2307
+ this.fns.pop()();
2308
+ } catch (err) {
2309
+ queueMicrotask(() => {
2310
+ throw err;
2311
+ });
2312
+ }
2313
+ }
2314
+ }
2315
+ };
2316
+ BottomSheetEngine = class {
2317
+ constructor(opts) {
2318
+ __publicField(this, "id", nextInstanceId("bs"));
2319
+ __publicField(this, "element");
2320
+ __publicField(this, "handle");
2321
+ __publicField(this, "scrollContainer");
2322
+ __publicField(this, "backdrop");
2323
+ __publicField(this, "screenComponent");
2324
+ __publicField(this, "mode");
2325
+ __publicField(this, "flickVelocity");
2326
+ __publicField(this, "dragThreshold");
2327
+ __publicField(this, "rubberBandEnabled");
2328
+ __publicField(this, "scrim");
2329
+ __publicField(this, "aria");
2330
+ __publicField(this, "animation");
2331
+ __publicField(this, "lifecycle");
2332
+ __publicField(this, "closeOnBack");
2333
+ __publicField(this, "routedTo");
2334
+ // Engine retains `snapPointsRaw` for the "find a closed-id" query in
2335
+ // close() (raw input, not resolved geometry). Resolver owns geometry.
2336
+ __publicField(this, "snapPointsRaw");
2337
+ __publicField(this, "snaps");
2338
+ __publicField(this, "activeId");
2339
+ __publicField(this, "size", 0);
2340
+ // Constructed in attach(); a plugin's install() could read `state` before
2341
+ // attach completes if the constructor throws partway through. State getter
2342
+ // null-guards via `this.gesture?.isDragging ?? false`.
2343
+ __publicField(this, "gesture");
2344
+ __publicField(this, "rootEl", null);
2345
+ __publicField(this, "destroyed", false);
2346
+ // In-flight ViewTransition handle. A second snapTo while a VT is in flight
2347
+ // would otherwise let Chromium reject the second's promise and flicker —
2348
+ // newCycle() calls skipTransition() to abort cleanly.
2349
+ __publicField(this, "currentViewTransition", null);
2350
+ // Each cycle captures the signal at entry; when a newer cycle aborts the
2351
+ // controller, the captured signal flips to `aborted=true` and post-await
2352
+ // guards short-circuit (prevents stale snap/open/close emits).
2353
+ __publicField(this, "currentAbort", new AbortController());
2354
+ // Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
2355
+ // visually distinguishable; gating CSSOM writes drops 30-50% of writes
2356
+ // during the spring's settle tail.
2357
+ __publicField(this, "sizeWriteSentinel", new WriteSentinel());
2358
+ __publicField(this, "progressWriteSentinel", new WriteSentinel());
2359
+ __publicField(this, "isTopSheet", true);
2360
+ // Pooled payload — identity documented in SheetEventMap.progress JSDoc;
2361
+ // consumers must not retain across frames.
2362
+ __publicField(this, "progressPayload", {
2363
+ value: 0,
2364
+ size: 0
2365
+ });
2366
+ // Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
2367
+ // Inner `range` object identity is also stable (mutated in place).
2368
+ __publicField(this, "dragContextBuf", {
2369
+ size: 0,
2370
+ maxAxisSize: 0,
2371
+ range: { min: 0, max: 0 },
2372
+ rubberBandEnabled: false
2373
+ });
2374
+ __publicField(this, "bus", createEventBus());
2375
+ __publicField(this, "persistKey");
2376
+ __publicField(this, "linkedSheets", []);
2377
+ __publicField(this, "scrollCache");
2378
+ // Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
2379
+ // tear down first before the infrastructure they depend on. Toggle-semantic
2380
+ // listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
2381
+ // fields on their controllers.
2382
+ __publicField(this, "teardowns", new TeardownStack());
2383
+ // Pre-computed transform template — built once per engine since `mode` is
2384
+ // fixed for its lifetime. Avoids the 4-arm switch at 60-120 Hz.
2385
+ __publicField(this, "transformTemplate");
2386
+ const resolved = resolveEngineOptions(opts, {
2387
+ shouldApplyInertSiblings: () => {
2388
+ if (typeof document === "undefined") return false;
2389
+ const bodyChildren = Array.from(document.body.children);
2390
+ return bodyChildren.some(
2391
+ (c) => c === opts.element || c.contains(opts.element)
2392
+ );
2393
+ }
2394
+ });
2395
+ this.element = opts.element;
2396
+ this.handle = opts.handle ?? opts.element;
2397
+ this.scrollContainer = opts.scrollContainer;
2398
+ this.backdrop = opts.backdrop;
2399
+ this.screenComponent = opts.screenComponent;
2400
+ this.mode = resolved.mode;
2401
+ this.transformTemplate = buildTransformTemplate(this.mode);
2402
+ this.aria = new AriaSliderWriter(this.handle, this.mode);
2403
+ this.flickVelocity = resolved.flickVelocity;
2404
+ this.dragThreshold = resolved.dragThreshold;
2405
+ this.rubberBandEnabled = resolved.rubberBandEnabled;
2406
+ this.scrim = new ScrimController(
2407
+ {
2408
+ mode: this.mode,
2409
+ screenComponent: this.screenComponent,
2410
+ backdrop: this.backdrop,
2411
+ isDestroyed: () => this.destroyed,
2412
+ isTopSheet: () => this.isTopSheet,
2413
+ getAllowedIds: () => this.snaps.getAllowedIds().slice(),
2414
+ getResolvedSnaps: () => this.snaps.getResolvedSnaps().slice(),
2415
+ snapTo: (id) => {
2416
+ void this.snapTo(id);
2417
+ }
2418
+ },
2419
+ resolved.scrim
2420
+ );
2421
+ this.animation = new AnimationRunner(
2422
+ {
2423
+ element: this.element,
2424
+ getRootEl: () => this.rootEl,
2425
+ applySize: (size) => this.applySize(size),
2426
+ getSize: () => this.size,
2427
+ // Optional chain protects synthetic test paths that drive animateTo
2428
+ // before attach() (gesture is constructed there).
2429
+ isDragging: () => this.gesture?.isDragging ?? false
2430
+ },
2431
+ resolved.animation
2432
+ );
2433
+ this.lifecycle = new LifecycleController(
2434
+ {
2435
+ element: this.element,
2436
+ close: () => this.close()
2437
+ },
2438
+ resolved.lifecycle
2439
+ );
2440
+ this.closeOnBack = resolved.closeOnBack;
2441
+ this.routedTo = opts.routedTo;
2442
+ this.snapPointsRaw = opts.snapPoints;
2443
+ this.persistKey = opts.persistKey;
2444
+ if (opts.linkedSheets) {
2445
+ this.linkedSheets = opts.linkedSheets;
2446
+ }
2447
+ this.activeId = resolved.initialId;
2448
+ auditVhUsage(this.snapPointsRaw);
2449
+ this.rootEl = this.element.closest(".bs-root");
2450
+ this.snaps = new SnapResolver(
2451
+ this.snapPointsRaw,
2452
+ resolved.initialAllowed,
2453
+ this.mode,
2454
+ () => this.handle.offsetHeight,
2455
+ (maxAxisSize) => {
2456
+ this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
2457
+ }
2458
+ );
2459
+ const initial = this.snaps.findById(this.activeId);
2460
+ if (initial) this.size = initial.size;
2461
+ this.applySize(this.size);
2462
+ this.updateAriaSlider();
2463
+ this.scrollCache = createScrollCache({
2464
+ scrollContainer: this.scrollContainer,
2465
+ getMaxAxisSize: () => this.snaps.getMaxAxisSize()
2466
+ });
2467
+ this.attach();
2468
+ this.registerInStack();
2469
+ if (this.snaps.getMaxAxisSize() === 0 && typeof IntersectionObserver !== "undefined" && typeof document !== "undefined") {
2470
+ const io = new IntersectionObserver((entries) => {
2471
+ for (const entry of entries) {
2472
+ if (!entry.isIntersecting) continue;
2473
+ if (this.destroyed) return;
2474
+ this.snaps.recompute();
2475
+ const current = this.snaps.findById(this.activeId);
2476
+ if (current) {
2477
+ this.size = current.size;
2478
+ this.applySize(this.size);
2479
+ this.updateAriaSlider();
2480
+ }
2481
+ io.disconnect();
2482
+ return;
2483
+ }
2484
+ });
2485
+ io.observe(this.element);
2486
+ this.teardowns.add(() => io.disconnect());
2487
+ }
2488
+ if (this.persistKey) {
2489
+ this.teardowns.add(installPersist(this, this.persistKey));
2490
+ }
2491
+ this.teardowns.add(
2492
+ installAutoCollapse({
2493
+ ms: opts.autoCollapseAfter,
2494
+ isDestroyed: () => this.destroyed,
2495
+ isDragging: () => this.gesture?.isDragging ?? false,
2496
+ getAllowedIds: () => this.snaps.getAllowedIds().slice(),
2497
+ getActiveId: () => this.activeId,
2498
+ resolveSnap: (id) => this.snaps.findById(id),
2499
+ snapTo: (id) => {
2500
+ void this.snapTo(id);
2501
+ },
2502
+ on: (event, fn) => this.on(event, fn)
2503
+ })
2504
+ );
2505
+ this.teardowns.add(
2506
+ installRoute({
2507
+ routedTo: this.routedTo,
2508
+ closeOnBack: this.closeOnBack,
2509
+ isTopSheet: () => this.isTopSheet,
2510
+ getSize: () => this.size,
2511
+ isDestroyed: () => this.destroyed,
2512
+ close: () => this.close(),
2513
+ on: (event, fn) => this.on(event, fn),
2514
+ sheetId: this.id
2515
+ })
2516
+ );
2517
+ if (this.size > 0) {
2518
+ this.handleOpen();
2519
+ }
2520
+ }
2521
+ get state() {
2522
+ return {
2523
+ size: this.size,
2524
+ activeId: this.activeId,
2525
+ isDragging: this.gesture?.isDragging ?? false,
2526
+ isAnimating: this.animation.isAnimating,
2527
+ progress: this.computeProgress(this.size)
2528
+ };
2529
+ }
2530
+ on(event, fn) {
2531
+ return this.bus.on(event, fn);
2532
+ }
2533
+ /**
2534
+ * Register a plugin. `install` runs synchronously; if it returns a function,
2535
+ * it's captured for teardown during `destroy()`. No-op if already destroyed
2536
+ * — installing into a torn-down engine would leak listeners.
2537
+ *
2538
+ * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
2539
+ * so a buggy plugin can't take the engine down with it: the engine stays
2540
+ * usable, sibling plugins still install, and the error surfaces in dev tools
2541
+ * on the next tick instead of being swallowed. Mirrors the per-fn isolation
2542
+ * model used for teardowns.
2543
+ *
2544
+ * Plugins that accept the second `scope` argument get transactional cleanup:
2545
+ * push partial-install cleanups via `scope.add(fn)` and the engine drains
2546
+ * them on throw, OR merges them into destroy-time teardowns on success.
2547
+ */
2548
+ use(plugin) {
2549
+ if (this.destroyed) {
2550
+ console.warn(
2551
+ `[BottomSheet] use("${plugin.name}") called on destroyed engine \u2014 ignored.`
2552
+ );
2553
+ return this;
2554
+ }
2555
+ const scoped = [];
2556
+ const scope = {
2557
+ // typeof check — not just truthiness — so a JS-only plugin pushing a
2558
+ // bogus value is silently rejected instead of queueing a confusing
2559
+ // TypeError on destroy() drain.
2560
+ add: (fn) => {
2561
+ if (typeof fn === "function") scoped.push(fn);
2562
+ }
2563
+ };
2564
+ let teardown;
2565
+ try {
2566
+ teardown = plugin.install(this, scope);
2567
+ } catch (err) {
2568
+ for (let i = scoped.length - 1; i >= 0; i--) {
2569
+ try {
2570
+ scoped[i]();
2571
+ } catch (cleanupErr) {
2572
+ queueMicrotask(() => {
2573
+ throw cleanupErr;
2574
+ });
2575
+ }
2576
+ }
2577
+ queueMicrotask(() => {
2578
+ throw err;
2579
+ });
2580
+ return this;
2581
+ }
2582
+ for (const fn of scoped) this.teardowns.add(fn);
2583
+ if (typeof teardown === "function") {
2584
+ this.teardowns.add(teardown);
2585
+ }
2586
+ return this;
2587
+ }
2588
+ /**
2589
+ * Replace the linked-sheets list at runtime. Use when peer engines aren't
2590
+ * available at construction time (mounted later in the tree). The wiring
2591
+ * to the `open` event is independent of this list — it reads `linkedSheets`
2592
+ * lazily on emit, so this setter is a simple field swap.
2593
+ */
2594
+ setLinkedSheets(sheets) {
2595
+ if (this.destroyed) return;
2596
+ this.linkedSheets = sheets;
2597
+ }
2598
+ /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
2599
+ getAllowedIds() {
2600
+ return this.snaps.getAllowedIds().slice();
2601
+ }
2602
+ /**
2603
+ * Animate to a snap point by id. Resolves when the animation completes.
2604
+ *
2605
+ * Two call shapes:
2606
+ * ```ts
2607
+ * engine.snapTo("full"); // default velocity
2608
+ * engine.snapTo("full", 0.5); // px/ms initial velocity
2609
+ * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
2610
+ * ```
2611
+ *
2612
+ * When `opts.signal` is provided, calling `controller.abort()` cancels the
2613
+ * in-flight tween/spring and the returned promise resolves without firing
2614
+ * `snap`. The captured `AbortSignal` is checked after each await so external
2615
+ * aborts ride the same cycle invariant as internal ones (resize, destroy,
2616
+ * concurrent snapTo).
2617
+ */
2618
+ async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
2619
+ if (this.destroyed) return;
2620
+ const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
2621
+ const velocityPxPerMs = opts.velocity ?? 0;
2622
+ const externalSignal = opts.signal;
2623
+ if (externalSignal?.aborted) return;
2624
+ const target = this.snaps.findById(id);
2625
+ if (!target) {
2626
+ console.warn(`[BottomSheet] unknown snap id: ${id}`);
2627
+ return;
2628
+ }
2629
+ if (!this.snaps.getAllowedIds().includes(id)) {
2630
+ console.warn(`[BottomSheet] snap "${id}" is not in allowed list`);
2631
+ return;
2632
+ }
2633
+ if (!_skipBeforeSnap && this.emitBeforeSnap(target, this.activeId)) {
2634
+ return;
2635
+ }
2636
+ const signal = this.newCycle();
2637
+ const onExternalAbort = () => {
2638
+ this.animation.cancel();
2639
+ this.newCycle();
2640
+ };
2641
+ externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
2642
+ const wasClosed = this.size === 0;
2643
+ const previousId = this.activeId;
2644
+ const previousSize = this.size;
2645
+ this.scrollCache.cache(previousId, previousSize, target.size);
2646
+ this.activeId = id;
2647
+ if (this.animation.viewTransitionsAvailable) {
2648
+ this.currentViewTransition?.skipTransition?.();
2649
+ const vt = document.startViewTransition(() => {
2650
+ this.applySize(target.size);
2651
+ });
2652
+ this.currentViewTransition = vt;
2653
+ try {
2654
+ await vt.finished;
2655
+ } catch {
2656
+ }
2657
+ if (this.currentViewTransition === vt) this.currentViewTransition = null;
2658
+ } else {
2659
+ await this.animation.animateTo(target.size, velocityPxPerMs);
2660
+ }
2661
+ externalSignal?.removeEventListener("abort", onExternalAbort);
2662
+ if (signal.aborted) return;
2663
+ this.scrollCache.restore(id, previousSize, target.size);
2664
+ this.updateAriaSlider();
2665
+ this.emit("snap", { id, size: target.size });
2666
+ if (wasClosed && target.size > 0) {
2667
+ this.emit("open", { id });
2668
+ this.handleOpen();
2669
+ notifyLinkedSheets(this.linkedSheets, this);
2670
+ }
2671
+ if (target.size === 0) {
2672
+ this.emit("close", void 0);
2673
+ this.handleClose();
2674
+ }
2675
+ }
2676
+ /**
2677
+ * Drive the sheet to an arbitrary px size, bypassing snap matching.
2678
+ * Useful for preview scrubs, animation chaining, or programmatic gestures.
2679
+ * Animates with the configured spring/tween. Resolves when the animation
2680
+ * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
2681
+ *
2682
+ * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
2683
+ * nor `snap` (the sheet isn't settling to a known snap). `progress` events
2684
+ * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
2685
+ */
2686
+ async dragTo(targetSize, velocityOrOpts = 0) {
2687
+ if (this.destroyed) return;
2688
+ const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
2689
+ const velocityPxPerMs = opts.velocity ?? 0;
2690
+ const externalSignal = opts.signal;
2691
+ if (externalSignal?.aborted) return;
2692
+ this.newCycle();
2693
+ const onExternalAbort = () => {
2694
+ this.animation.cancel();
2695
+ this.newCycle();
2696
+ };
2697
+ externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
2698
+ const clamped = Math.max(0, Math.min(targetSize, this.snaps.getMaxAxisSize()));
2699
+ await this.animation.animateTo(clamped, velocityPxPerMs);
2700
+ externalSignal?.removeEventListener("abort", onExternalAbort);
2701
+ if (this.destroyed || externalSignal?.aborted) return;
2702
+ }
2703
+ open(id) {
2704
+ if (this.destroyed) return Promise.resolve();
2705
+ const target = id ?? this.snaps.getAllowedIds().find((a) => a !== "closed") ?? this.activeId;
2706
+ return this.snapTo(target);
2707
+ }
2708
+ close() {
2709
+ if (this.destroyed) return Promise.resolve();
2710
+ const closedId = this.snapPointsRaw.find((p) => p.id === "closed")?.id ?? this.snaps.getAllowedIds()[0];
2711
+ return this.snapTo(closedId ?? this.activeId);
2712
+ }
2713
+ setAllowed(ids, snap) {
2714
+ if (this.destroyed) return;
2715
+ const willSnap = snap !== void 0 && ids.includes(snap) || !ids.includes(this.activeId) && ids.length > 0;
2716
+ if (!willSnap) {
2717
+ this.animation.cancel();
2718
+ this.newCycle();
2719
+ }
2720
+ this.snaps.setAllowedIds(ids);
2721
+ this.updateAriaSlider();
2722
+ if (snap && ids.includes(snap)) {
2723
+ void this.snapTo(
2724
+ snap,
2725
+ 0,
2726
+ /* internal */
2727
+ true
2728
+ );
2729
+ } else if (!ids.includes(this.activeId)) {
2730
+ const fallback = ids[0];
2731
+ if (fallback) void this.snapTo(
2732
+ fallback,
2733
+ 0,
2734
+ /* internal */
2735
+ true
2736
+ );
2737
+ }
2738
+ }
2739
+ setBackdropRange(range) {
2740
+ this.scrim.setBackdropRange(range, () => this.applySize(this.size));
2741
+ }
2742
+ setScreenRange(range) {
2743
+ this.scrim.setScreenRange(range, () => this.applySize(this.size));
2744
+ }
2745
+ setScrimColor(color) {
2746
+ this.scrim.setScrimColor(color);
2747
+ }
2748
+ setScrimBlur(blur) {
2749
+ this.scrim.setScrimBlur(blur);
2750
+ }
2751
+ setScrimInteractive(interactive) {
2752
+ this.scrim.setScrimInteractive(interactive);
2753
+ }
2754
+ /**
2755
+ * Batch-update multiple scrim fields in one call. Apply order is:
2756
+ * 1. `preset` — fills baseline color/blur/range/interactive.
2757
+ * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
2758
+ * 3. `enabled` — flips the opacity contribution on/off.
2759
+ * 4. `tapToClose` — installs/tears the click listener.
2760
+ * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
2761
+ * 6. ONE `applySize` so the per-frame opacity stays consistent.
2762
+ *
2763
+ * Performance: collecting changes here (vs calling individual setters)
2764
+ * avoids repeated `applySize` calls during a multi-field swap. Setters
2765
+ * called here suppress their own internal `applySize` either by passing
2766
+ * through fast-paths (mode/enabled no-op when value unchanged) or by
2767
+ * being visual-only (color/blur/interactive don't influence opacity).
2768
+ */
2769
+ setScrim(opts) {
2770
+ this.scrim.setScrim(opts, () => this.applySize(this.size));
2771
+ }
2772
+ setScrimMode(mode) {
2773
+ this.scrim.setScrimMode(mode, () => this.applySize(this.size));
2774
+ }
2775
+ setScrimTapToClose(enabled) {
2776
+ this.scrim.setScrimTapToClose(enabled);
2777
+ }
2778
+ setScrimEnabled(enabled) {
2779
+ this.scrim.setScrimEnabled(enabled, () => this.applySize(this.size));
2780
+ }
2781
+ setScrimOverlay(opts) {
2782
+ return this.scrim.setScrimOverlay(opts);
2783
+ }
2784
+ /**
2785
+ * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
2786
+ * Stable public API — internals (e.g. cached opacity sentinels) are NOT
2787
+ * exposed so consumers can't accidentally couple to ScrimController shape.
2788
+ */
2789
+ getScrimState() {
2790
+ return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
2791
+ }
2792
+ setSnapPoints(points, allowed) {
2793
+ if (this.destroyed) return;
2794
+ this.animation.cancel();
2795
+ this.newCycle();
2796
+ this.snapPointsRaw = points;
2797
+ auditVhUsage(points);
2798
+ this.snaps.setRaw(points);
2799
+ if (allowed) this.snaps.setAllowedIds(allowed);
2800
+ this.scrollCache.clear();
2801
+ this.scrim.invalidateOpacityCache();
2802
+ const current = this.snaps.findById(this.activeId);
2803
+ if (current) {
2804
+ this.applySize(current.size);
2805
+ }
2806
+ this.updateAriaSlider();
2807
+ }
2808
+ destroy() {
2809
+ this.destroyed = true;
2810
+ this.currentAbort.abort();
2811
+ this.currentViewTransition?.skipTransition?.();
2812
+ this.currentViewTransition = null;
2813
+ this.bus.clear();
2814
+ this.animation.cancel();
2815
+ this.gesture?.forceClearDragState();
2816
+ this.teardowns.drain();
2817
+ this.scrim.destroy();
2818
+ this.animation.destroy();
2819
+ this.lifecycle.destroy();
2820
+ if (this.backdrop) {
2821
+ this.backdrop.style.opacity = "";
2822
+ this.backdrop.style.pointerEvents = "";
2823
+ }
2824
+ if (this.screenComponent) {
2825
+ this.screenComponent.style.opacity = "";
2826
+ this.screenComponent.style.display = "";
2827
+ }
2828
+ this.sizeWriteSentinel.invalidate();
2829
+ this.progressWriteSentinel.invalidate();
2830
+ }
2831
+ emit(event, payload) {
2832
+ this.bus.emit(event, payload);
2833
+ }
2834
+ /**
2835
+ * Abort the current cycle's signal and replace the controller. Returns the
2836
+ * fresh signal so the caller can capture it locally before any await — when
2837
+ * a future newCycle() / destroy aborts our controller, the captured signal
2838
+ * flips to aborted and post-await guards short-circuit.
2839
+ */
2840
+ newCycle() {
2841
+ this.currentAbort.abort();
2842
+ this.currentAbort = new AbortController();
2843
+ return this.currentAbort.signal;
2844
+ }
2845
+ /**
2846
+ * Fire `before-snap` and report whether a listener cancelled it. The
2847
+ * `cancel()` callback exposed to listeners is frozen after the synchronous
2848
+ * emit phase so async callers can't silently miss — they get a console
2849
+ * warning. Returns `true` when cancelled so callers can early-return /
2850
+ * spring-back without re-implementing the cancel/frozen pattern.
2851
+ */
2852
+ emitBeforeSnap(target, previousId) {
2853
+ let cancelled = false;
2854
+ let frozen = false;
2855
+ this.emit("before-snap", {
2856
+ id: target.id,
2857
+ size: target.size,
2858
+ previousId,
2859
+ cancel: () => {
2860
+ if (frozen) {
2861
+ console.warn(
2862
+ "[BottomSheet] before-snap.cancel() called asynchronously \u2014 ignored. cancel() must be invoked synchronously inside the listener."
2863
+ );
2864
+ return;
2865
+ }
2866
+ cancelled = true;
2867
+ }
2868
+ });
2869
+ frozen = true;
2870
+ return cancelled;
2871
+ }
2872
+ recomputeSnaps() {
2873
+ const seen = /* @__PURE__ */ new Set();
2874
+ for (const p of this.snapPointsRaw) {
2875
+ if (seen.has(p.id)) {
2876
+ console.warn(
2877
+ `[BottomSheet] duplicate snap id "${p.id}" \u2014 only the first occurrence is reachable.`
2878
+ );
2879
+ }
2880
+ seen.add(p.id);
2881
+ }
2882
+ this.snaps.setRaw(this.snapPointsRaw);
2883
+ this.scrim.invalidateOpacityCache();
2884
+ }
2885
+ /**
2886
+ * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
2887
+ * Engine wrapper kept so existing call sites don't all need updating;
2888
+ * resolver-level memoization survives across method invocations.
2889
+ */
2890
+ getAllowedRange() {
2891
+ return this.snaps.getAllowedRange();
2892
+ }
2893
+ /**
2894
+ * Write ARIA slider value attributes onto the handle so the engine itself
2895
+ * is the source of truth for screen-reader announcements. Called from the
2896
+ * constructor and from every code path that mutates `activeId` /
2897
+ * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
2898
+ * without it screen readers can't announce drag/snap progress to keyboard
2899
+ * + AT users (WCAG 4.1.2 Name, Role, Value).
2900
+ *
2901
+ * Engine does NOT set `role="slider"` itself — that's the adapter's or
2902
+ * consumer markup's responsibility. We only decorate with dynamic values.
2903
+ *
2904
+ * Empty allowed-list is malformed input (settable via `setAllowed([])`);
2905
+ * short-circuit to remove ARIA value attributes so screen readers see a
2906
+ * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
2907
+ * requires `valuemax > valuemin` for `role="slider"`).
2908
+ */
2909
+ updateAriaSlider() {
2910
+ this.aria.setValue(this.snaps.getAllowedIds(), this.activeId);
2911
+ }
2912
+ registerInStack() {
2913
+ this.teardowns.add(sheetStack.push({
2914
+ id: this.id,
2915
+ setZIndex: (z) => {
2916
+ this.element.style.zIndex = String(z);
2917
+ if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
2918
+ },
2919
+ setIsTop: (isTop) => {
2920
+ this.isTopSheet = isTop;
2921
+ if (this.backdrop) {
2922
+ this.backdrop.style.display = isTop ? "" : "none";
2923
+ }
2924
+ }
2925
+ }));
2926
+ }
2927
+ attach() {
2928
+ this.gesture = new GestureController({
2929
+ handle: this.handle,
2930
+ element: this.element,
2931
+ mode: this.mode,
2932
+ getRoot: () => this.rootEl,
2933
+ // Single coalesced reader — narrows the DI surface and removes 3 getter
2934
+ // dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
2935
+ // (this.dragContextBuf) — see field JSDoc for the retention contract.
2936
+ // Inner `range` object is mutated in place too so callers caching its
2937
+ // identity between frames stay valid.
2938
+ getDragContext: () => {
2939
+ const buf = this.dragContextBuf;
2940
+ const r = this.getAllowedRange();
2941
+ buf.size = this.size;
2942
+ buf.maxAxisSize = this.snaps.getMaxAxisSize();
2943
+ buf.range.min = r.min;
2944
+ buf.range.max = r.max;
2945
+ buf.rubberBandEnabled = this.rubberBandEnabled;
2946
+ return buf;
2947
+ },
2948
+ cancelAnimation: () => this.animation.cancel(),
2949
+ applySize: (size) => this.applySize(size),
2950
+ animateTo: (size, velocity) => this.animation.animateTo(size, velocity),
2951
+ settleAfterDrag: (delta, velocity, kind) => this.settleAfterDrag(delta, velocity, kind),
2952
+ emit: (event, payload) => this.emit(event, payload),
2953
+ listenerCount: (event) => this.bus.listenerCount(event)
2954
+ });
2955
+ this.teardowns.add(this.gesture.install());
2956
+ try {
2957
+ this.attachFeatures();
2958
+ } catch (err) {
2959
+ this.teardowns.drain();
2960
+ try {
2961
+ this.scrim.destroy();
2962
+ } catch (destroyErr) {
2963
+ queueMicrotask(() => {
2964
+ throw destroyErr;
2965
+ });
2966
+ }
2967
+ try {
2968
+ this.animation.destroy();
2969
+ } catch (destroyErr) {
2970
+ queueMicrotask(() => {
2971
+ throw destroyErr;
2972
+ });
2973
+ }
2974
+ try {
2975
+ this.lifecycle.destroy();
2976
+ } catch (destroyErr) {
2977
+ queueMicrotask(() => {
2978
+ throw destroyErr;
2979
+ });
2980
+ }
2981
+ this.destroyed = true;
2982
+ throw err;
2983
+ }
2984
+ }
2985
+ attachFeatures() {
2986
+ if (this.scrollContainer) {
2987
+ this.teardowns.add(installContentSwipe({
2988
+ container: this.scrollContainer,
2989
+ isDragging: () => this.gesture?.isDragging ?? false,
2990
+ isAnimating: () => this.animation.isAnimating,
2991
+ getAllowedIds: () => this.snaps.getAllowedIds(),
2992
+ getActiveId: () => this.activeId,
2993
+ snapTo: (id) => {
2994
+ void this.snapTo(id);
2995
+ }
2996
+ }));
2997
+ }
2998
+ if (typeof window !== "undefined") {
2999
+ this.teardowns.add(installResizeObserver({
3000
+ element: this.element,
3001
+ getMode: () => this.mode,
3002
+ isDestroyed: () => this.destroyed,
3003
+ isDragging: () => this.gesture?.isDragging ?? false,
3004
+ resolveActiveSnap: () => this.snaps.findById(this.activeId),
3005
+ getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
3006
+ getSize: () => this.size,
3007
+ setMaxAxisSize: (size) => {
3008
+ this.snaps.setMaxAxisSize(size);
3009
+ },
3010
+ setSize: (size) => {
3011
+ this.size = size;
3012
+ },
3013
+ recomputeSnaps: () => this.recomputeSnaps(),
3014
+ applySize: (size) => this.applySize(size),
3015
+ cancelInFlight: () => {
3016
+ this.animation.cancel();
3017
+ },
3018
+ newCycle: () => {
3019
+ this.newCycle();
3020
+ }
3021
+ }));
3022
+ if (this.lifecycle.closeOnEscape) {
3023
+ const onKey = (e) => {
3024
+ if (this.destroyed) return;
3025
+ if (e.key === "Escape" && this.isTopSheet && this.size > 0) {
3026
+ void this.close();
3027
+ }
3028
+ };
3029
+ document.addEventListener("keydown", onKey);
3030
+ this.teardowns.add(
3031
+ () => document.removeEventListener("keydown", onKey)
3032
+ );
3033
+ }
3034
+ this.teardowns.add(installVisualViewport({
3035
+ element: this.element,
3036
+ isVerticalAxis: () => this.mode === "bottom" || this.mode === "top",
3037
+ isDestroyed: () => this.destroyed,
3038
+ isDragging: () => this.gesture?.isDragging ?? false,
3039
+ recomputeSnaps: () => this.recomputeSnaps(),
3040
+ resolveActiveSnap: () => this.snaps.findById(this.activeId),
3041
+ getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
3042
+ getSize: () => this.size,
3043
+ setMaxAxisSize: (size) => {
3044
+ this.snaps.setMaxAxisSize(size);
3045
+ },
3046
+ setSize: (size) => {
3047
+ this.size = size;
3048
+ },
3049
+ applySize: (size) => this.applySize(size)
3050
+ }));
3051
+ }
3052
+ this.teardowns.add(installSliderKeyboard({
3053
+ handle: this.handle,
3054
+ mode: this.mode,
3055
+ isDestroyed: () => this.destroyed,
3056
+ getAllowedIds: () => this.snaps.getAllowedIds(),
3057
+ getActiveId: () => this.activeId,
3058
+ snapTo: (id) => {
3059
+ void this.snapTo(id);
3060
+ }
3061
+ }));
3062
+ this.scrim.attach();
3063
+ }
3064
+ settleAfterDrag(delta, velocity, kind = "touch") {
3065
+ const signal = this.newCycle();
3066
+ const target = this.snaps.findDragSettleTarget({
3067
+ delta,
3068
+ velocity,
3069
+ pointerKind: kind,
3070
+ size: this.size,
3071
+ activeId: this.activeId,
3072
+ flickVelocity: this.flickVelocity,
3073
+ dragThreshold: this.dragThreshold
3074
+ });
3075
+ if (!target) return;
3076
+ const previousId = this.activeId;
3077
+ if (this.emitBeforeSnap(target, previousId)) {
3078
+ const restore = this.snaps.findById(previousId);
3079
+ if (restore) {
3080
+ void this.animation.animateTo(restore.size, 0);
3081
+ }
3082
+ return;
3083
+ }
3084
+ const previousSize = this.size;
3085
+ this.scrollCache.cache(previousId, previousSize, target.size);
3086
+ this.activeId = target.id;
3087
+ void this.animation.animateTo(target.size, velocity).then(() => {
3088
+ if (signal.aborted) return;
3089
+ this.scrollCache.restore(target.id, previousSize, target.size);
3090
+ this.updateAriaSlider();
3091
+ this.emit("snap", { id: target.id, size: target.size });
3092
+ this.haptic();
3093
+ if (previousId === "closed" && target.size > 0) {
3094
+ this.emit("open", { id: target.id });
3095
+ this.handleOpen();
3096
+ }
3097
+ if (target.size === 0) {
3098
+ this.emit("close", void 0);
3099
+ this.handleClose();
3100
+ }
3101
+ });
3102
+ }
3103
+ /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
3104
+ haptic() {
3105
+ if (typeof navigator !== "undefined" && "vibrate" in navigator) {
3106
+ try {
3107
+ navigator.vibrate(HAPTIC_DURATION_MS);
3108
+ } catch {
3109
+ }
3110
+ }
3111
+ }
3112
+ applySize(size) {
3113
+ this.size = size;
3114
+ const offset = this.snaps.getMaxAxisSize() - size;
3115
+ const style = this.element.style;
3116
+ style.transform = this.transformTemplate(offset);
3117
+ if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
3118
+ style.setProperty("--bs-size", `${size}px`);
3119
+ }
3120
+ const progress = this.computeProgress(size);
3121
+ const progressChanged = this.progressWriteSentinel.shouldWrite(
3122
+ progress,
3123
+ OPACITY_WRITE_EPSILON
3124
+ );
3125
+ if (progressChanged) {
3126
+ style.setProperty("--bs-progress", String(progress));
3127
+ }
3128
+ this.scrim.applyOpacity(progress, progressChanged);
3129
+ if (progressChanged && this.bus.listenerCount("progress") > 0) {
3130
+ this.progressPayload.value = progress;
3131
+ this.progressPayload.size = size;
3132
+ this.emit("progress", this.progressPayload);
3133
+ }
3134
+ }
3135
+ computeProgress(size) {
3136
+ const { min, max } = this.getAllowedRange();
3137
+ if (max <= min) return 0;
3138
+ return Math.min(Math.max((size - min) / (max - min), 0), 1);
3139
+ }
3140
+ handleOpen() {
3141
+ try {
3142
+ this.lifecycle.install();
3143
+ } catch (err) {
3144
+ if (!this.destroyed) this.newCycle();
3145
+ this.handleClose();
3146
+ this.applySize(0);
3147
+ this.activeId = this.snapPointsRaw.find((p) => p.id === "closed")?.id ?? this.activeId;
3148
+ this.size = 0;
3149
+ queueMicrotask(() => {
3150
+ throw err;
3151
+ });
3152
+ }
3153
+ }
3154
+ handleClose() {
3155
+ this.lifecycle.release();
3156
+ }
3157
+ };
3158
+ }
3159
+ });
3160
+
3161
+ // src/web-component/baseStyles.ts
3162
+ var baseStyles;
3163
+ var init_baseStyles = __esm({
3164
+ "src/web-component/baseStyles.ts"() {
3165
+ baseStyles = `:where(.bs-root){--bs-surface:#ffffff;--bs-surface-border:rgba(225,227,237,1);--bs-shadow:0 -3px 12px rgba(0,0,0,0.12);--bs-radius:20px;--bs-handle-color:#b5b3a8;--bs-handle-width:40px;--bs-handle-height:5px;--bs-z-backdrop:90;--bs-z-sheet:100;--bs-header-min-height:80px;--bs-backdrop-color:rgba(0,0,0,0.5);--bs-scrim-color:rgba(0,0,0,0.4);--bs-scrim-blur:0px;--bs-z-scrim:calc(var(--bs-z-sheet) - 1);--bs-size:0px;--bs-progress:0}.bs-backdrop{position:fixed;inset:0;background:var(--bs-backdrop-color);opacity:0;pointer-events:none;z-index:var(--bs-z-backdrop);transition:opacity 0.15s linear;will-change:opacity}.bs-root[data-dragging="true"] .bs-backdrop,.bs-root[data-animating="true"] .bs-backdrop{transition:none}.bs-sheet{position:fixed;background:var(--bs-surface);box-shadow:var(--bs-shadow);z-index:var(--bs-z-sheet);overflow:hidden;display:flex;flex-direction:column;-webkit-tap-highlight-color:transparent;user-select:none;overscroll-behavior:contain}.bs-sheet[data-mode="bottom"]{left:0;right:0;bottom:0;width:100%;border-top-left-radius:var(--bs-radius);border-top-right-radius:var(--bs-radius);border-top:1px solid var(--bs-surface-border);padding-bottom:env(safe-area-inset-bottom);box-shadow:0 calc(var(--bs-progress,0) * -2px - 3px) calc(var(--bs-progress,0) * 18px + 12px) rgba(0,0,0,calc(var(--bs-progress,0) * 0.15 + 0.12))}.bs-sheet[data-mode="top"]{left:0;right:0;top:0;width:100%;border-bottom-left-radius:var(--bs-radius);border-bottom-right-radius:var(--bs-radius);border-bottom:1px solid var(--bs-surface-border);flex-direction:column-reverse;box-shadow:0 3px 12px rgba(0,0,0,0.12);padding-top:env(safe-area-inset-top)}.bs-sheet[data-mode="left"]{top:0;bottom:0;left:0;height:100%;border-top-right-radius:var(--bs-radius);border-bottom-right-radius:var(--bs-radius);border-right:1px solid var(--bs-surface-border);box-shadow:3px 0 12px rgba(0,0,0,0.12)}.bs-sheet[data-mode="right"]{top:0;bottom:0;right:0;height:100%;border-top-left-radius:var(--bs-radius);border-bottom-left-radius:var(--bs-radius);border-left:1px solid var(--bs-surface-border);box-shadow:-3px 0 12px rgba(0,0,0,0.12)}.bs-sheet[data-mode="left"] .bs-content{padding-right:36px}.bs-sheet[data-mode="right"] .bs-content{padding-left:36px}.bs-handle{flex-shrink:0;min-height:44px;display:flex;flex-direction:column;justify-content:center;padding:8px 16px 4px;cursor:grab;touch-action:none;user-select:none;background:var(--bs-surface);-webkit-tap-highlight-color:transparent;outline:none;position:relative}.bs-handle:active{cursor:grabbing}.bs-handle:focus-visible{outline:2px solid currentColor;outline-offset:-2px;border-radius:var(--bs-radius)}.bs-handle::before{content:"";display:block;width:var(--bs-handle-width);height:var(--bs-handle-height);background:var(--bs-handle-color);border-radius:calc(var(--bs-handle-height) / 2);margin:0 auto 8px;transition:width 0.15s ease,background 0.15s ease,transform 0.15s ease;pointer-events:none}.bs-sheet[data-mode="left"] .bs-handle,.bs-sheet[data-mode="right"] .bs-handle{position:absolute;top:0;bottom:0;width:28px;min-width:0;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0;background:var(--bs-surface);z-index:1;overflow:hidden}.bs-sheet[data-mode="left"] .bs-handle>*,.bs-sheet[data-mode="right"] .bs-handle>*{display:none}.bs-sheet[data-mode="left"] .bs-handle{right:0;border-left:1px solid var(--bs-surface-border)}.bs-sheet[data-mode="right"] .bs-handle{left:0;border-right:1px solid var(--bs-surface-border)}.bs-sheet[data-mode="left"] .bs-handle::before,.bs-sheet[data-mode="right"] .bs-handle::before{width:var(--bs-handle-height);height:var(--bs-handle-width);margin:0}.bs-sheet:hover .bs-handle::before{background:color-mix(in oklab,var(--bs-handle-color),#000 15%)}.bs-handle:active::before{background:color-mix(in oklab,var(--bs-handle-color),#000 35%);transform:scaleX(1.15)}.bs-handle.bs-handle--ticks::after{content:"";display:block;position:absolute;left:50%;bottom:6px;transform:translateX(-50%);width:calc(var(--bs-handle-width) * 2);height:3px;background-image:radial-gradient( circle,var(--bs-handle-color) 0 1.5px,transparent 1.6px );background-size:8px 3px;background-repeat:repeat-x;background-position:center;opacity:0;transition:opacity 0.18s ease-out;pointer-events:none}.bs-root[data-dragging="true"] .bs-handle.bs-handle--ticks::after{opacity:0.4}.bs-sheet[data-mode="top"] .bs-handle.bs-handle--ticks::after{bottom:auto;top:6px}.bs-sheet[data-mode="left"] .bs-handle.bs-handle--ticks::after,.bs-sheet[data-mode="right"] .bs-handle.bs-handle--ticks::after{left:50%;top:50%;bottom:auto;transform:translate(-50%,-50%);width:3px;height:calc(var(--bs-handle-width) * 2);background-size:3px 8px;background-repeat:repeat-y;margin-left:calc(var(--bs-handle-height) + 6px)}.bs-sheet[data-mode="right"] .bs-handle.bs-handle--ticks::after{margin-left:calc(-1 * (var(--bs-handle-height) + 6px))}.bs-content{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;padding:0 16px 16px}.bs-screen{position:absolute;inset:0;pointer-events:none;display:none;z-index:var(--bs-z-scrim)}.bs-scrim{background:var(--bs-scrim-color);transition:opacity 0.15s linear;will-change:opacity}.bs-root[data-dragging="true"] .bs-scrim,.bs-root[data-animating="true"] .bs-scrim{transition:none}.bs-scrim{backdrop-filter:blur(var(--bs-scrim-blur));-webkit-backdrop-filter:blur(var(--bs-scrim-blur))}.bs-button-slot{position:absolute;z-index:calc(var(--bs-z-sheet) + 1);pointer-events:auto}.bs-button-slot[data-side="left"]{left:16px;bottom:calc(var(--bs-size,0px) + 12px);transition:bottom 0.15s ease}.bs-button-slot[data-side="right"]{right:16px;bottom:calc(var(--bs-size,0px) + 12px);transition:bottom 0.15s ease}@media (prefers-reduced-motion:reduce){.bs-backdrop,.bs-handle::before,.bs-button-slot[data-side="left"],.bs-button-slot[data-side="right"]{transition:none !important}}@media (prefers-color-scheme:dark){:where(.bs-root){--bs-surface:#1a1a1d;--bs-surface-border:rgba(255,255,255,0.08);--bs-handle-color:rgba(255,255,255,0.45);--bs-shadow:0 -3px 16px rgba(0,0,0,0.5)}}.bs-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}:where(.ovl-root){--ovl-surface:var(--bs-surface,#ffffff);--ovl-surface-border:var(--bs-surface-border,rgba(0,0,0,0.08));--ovl-shadow:0 -8px 40px rgba(0,0,0,0.35);--ovl-radius:var(--bs-radius,20px);--ovl-backdrop-color:rgba(0,0,0,0.5);--ovl-z-backdrop:200;--ovl-z-panel:201}.ovl-backdrop{position:fixed;inset:0;background:var(--ovl-backdrop-color);opacity:0;pointer-events:none;z-index:var(--ovl-z-backdrop)}.ovl-panel{position:fixed;background:var(--ovl-surface);box-shadow:var(--ovl-shadow);z-index:var(--ovl-z-panel);-webkit-tap-highlight-color:transparent;outline:none;transition:transform 0.32s cubic-bezier(0.29,1.24,0.84,0.99),opacity 0.32s ease-out}.ovl-panel[data-state="open"]{transform:translate3d(0,0,0);opacity:1}@starting-style{.ovl-panel[data-edge="bottom"][data-state="open"]{transform:translate3d(0,100%,0);opacity:0}.ovl-panel[data-edge="top"][data-state="open"]{transform:translate3d(0,-100%,0);opacity:0}.ovl-panel[data-edge="left"][data-state="open"]{transform:translate3d(-100%,0,0);opacity:0}.ovl-panel[data-edge="right"][data-state="open"]{transform:translate3d(100%,0,0);opacity:0}}.ovl-panel[data-edge="bottom"]{left:0;right:0;bottom:0;width:100%;border-top-left-radius:var(--ovl-radius);border-top-right-radius:var(--ovl-radius);border-top:1px solid var(--ovl-surface-border);padding-bottom:env(safe-area-inset-bottom)}.ovl-panel[data-edge="top"]{left:0;right:0;top:0;width:100%;border-bottom-left-radius:var(--ovl-radius);border-bottom-right-radius:var(--ovl-radius);border-bottom:1px solid var(--ovl-surface-border);padding-top:env(safe-area-inset-top)}.ovl-panel[data-edge="left"]{top:0;bottom:0;left:0;height:100%;border-top-right-radius:var(--ovl-radius);border-bottom-right-radius:var(--ovl-radius);border-right:1px solid var(--ovl-surface-border)}.ovl-panel[data-edge="right"]{top:0;bottom:0;right:0;height:100%;border-top-left-radius:var(--ovl-radius);border-bottom-left-radius:var(--ovl-radius);border-left:1px solid var(--ovl-surface-border)}@media (prefers-reduced-motion:reduce){.ovl-panel,.ovl-backdrop{transition:none !important}}`;
3166
+ }
3167
+ });
3168
+
3169
+ // src/web-component/BottomSheetElement.ts
3170
+ var BottomSheetElement_exports = {};
3171
+ __export(BottomSheetElement_exports, {
3172
+ BottomSheetElement: () => exports.BottomSheetElement,
3173
+ defineBottomSheet: () => exports.defineBottomSheet
3174
+ });
3175
+ var ATTR_SNAP_POINTS, ATTR_ALLOWED, ATTR_INITIAL, ATTR_MODE, ATTR_BACKDROP, ATTR_STYLESHEET, ATTR_ANIMATION, ATTR_FOCUS_TRAP, ATTR_CLOSE_ON_ESCAPE, ATTR_LOCK_BODY_SCROLL, isValidSnapPoint, DEFAULT_SNAP_POINTS, parseSnapPoints, VALID_MODES, VALID_ANIMATIONS, parseMode, parseAnimation, parseList, buildSlot, buildShadowTree; exports.BottomSheetElement = void 0; var registered; exports.defineBottomSheet = void 0;
3176
+ var init_BottomSheetElement = __esm({
3177
+ "src/web-component/BottomSheetElement.ts"() {
3178
+ init_BottomSheetEngine();
3179
+ init_baseStyles();
3180
+ ATTR_SNAP_POINTS = "snap-points";
3181
+ ATTR_ALLOWED = "allowed";
3182
+ ATTR_INITIAL = "initial";
3183
+ ATTR_MODE = "mode";
3184
+ ATTR_BACKDROP = "backdrop";
3185
+ ATTR_STYLESHEET = "stylesheet";
3186
+ ATTR_ANIMATION = "animation";
3187
+ ATTR_FOCUS_TRAP = "focus-trap";
3188
+ ATTR_CLOSE_ON_ESCAPE = "close-on-escape";
3189
+ ATTR_LOCK_BODY_SCROLL = "lock-body-scroll";
3190
+ isValidSnapPoint = (p) => !!p && typeof p === "object" && typeof p.id === "string" && (typeof p.size === "number" || typeof p.size === "string");
3191
+ DEFAULT_SNAP_POINTS = [{ id: "full", size: "full" }];
3192
+ parseSnapPoints = (raw) => {
3193
+ if (!raw) return DEFAULT_SNAP_POINTS;
3194
+ try {
3195
+ const parsed = JSON.parse(raw);
3196
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed.every(isValidSnapPoint)) {
3197
+ return parsed;
3198
+ }
3199
+ if (Array.isArray(parsed)) return DEFAULT_SNAP_POINTS;
3200
+ } catch {
3201
+ }
3202
+ return raw.split(",").map((s) => s.trim()).filter(Boolean).map((part) => {
3203
+ const [id, size] = part.split(":").map((s) => s.trim());
3204
+ const numeric = Number(size);
3205
+ const resolvedSize = Number.isFinite(numeric) ? numeric : size;
3206
+ return { id: id ?? "default", size: resolvedSize };
3207
+ });
3208
+ };
3209
+ VALID_MODES = [
3210
+ "bottom",
3211
+ "top",
3212
+ "left",
3213
+ "right"
3214
+ ];
3215
+ VALID_ANIMATIONS = ["spring", "tween", "ios-spring", "material-bounce", "linear", "snappy"];
3216
+ parseMode = (raw) => raw && VALID_MODES.includes(raw) ? raw : "bottom";
3217
+ parseAnimation = (raw) => raw && VALID_ANIMATIONS.includes(raw) ? raw : void 0;
3218
+ parseList = (raw) => raw ? raw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
3219
+ buildSlot = (name) => {
3220
+ const slot = document.createElement("slot");
3221
+ slot.name = name;
3222
+ return slot;
3223
+ };
3224
+ buildShadowTree = () => {
3225
+ const root = document.createElement("div");
3226
+ root.className = "bs-root";
3227
+ const backdrop = document.createElement("div");
3228
+ backdrop.className = "bs-backdrop";
3229
+ backdrop.setAttribute("part", "backdrop");
3230
+ const screen = document.createElement("div");
3231
+ screen.className = "bs-screen";
3232
+ screen.setAttribute("part", "screen");
3233
+ screen.appendChild(buildSlot("screen"));
3234
+ const sheet = document.createElement("section");
3235
+ sheet.className = "bs-sheet";
3236
+ sheet.setAttribute("part", "sheet");
3237
+ sheet.setAttribute("role", "dialog");
3238
+ sheet.setAttribute("aria-modal", "false");
3239
+ const leftButton = document.createElement("div");
3240
+ leftButton.className = "bs-button-slot";
3241
+ leftButton.setAttribute("data-side", "left");
3242
+ leftButton.setAttribute("part", "left-button");
3243
+ leftButton.appendChild(buildSlot("leftButton"));
3244
+ const rightButton = document.createElement("div");
3245
+ rightButton.className = "bs-button-slot";
3246
+ rightButton.setAttribute("data-side", "right");
3247
+ rightButton.setAttribute("part", "right-button");
3248
+ rightButton.appendChild(buildSlot("rightButton"));
3249
+ const handle = document.createElement("div");
3250
+ handle.className = "bs-handle";
3251
+ handle.setAttribute("part", "handle");
3252
+ handle.setAttribute("role", "slider");
3253
+ handle.setAttribute("tabindex", "0");
3254
+ handle.setAttribute("aria-label", "Resize sheet");
3255
+ handle.appendChild(buildSlot("header"));
3256
+ const content = document.createElement("div");
3257
+ content.className = "bs-content";
3258
+ content.setAttribute("part", "content");
3259
+ content.setAttribute("tabindex", "0");
3260
+ content.setAttribute("role", "region");
3261
+ content.setAttribute("aria-label", "Sheet content");
3262
+ const defaultSlot = document.createElement("slot");
3263
+ content.appendChild(defaultSlot);
3264
+ const announcer = document.createElement("span");
3265
+ announcer.className = "bs-sr-announce";
3266
+ announcer.setAttribute("role", "status");
3267
+ announcer.setAttribute("aria-live", "polite");
3268
+ announcer.setAttribute("aria-atomic", "true");
3269
+ announcer.style.cssText = "position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);";
3270
+ sheet.append(leftButton, rightButton, handle, content);
3271
+ root.append(backdrop, screen, sheet, announcer);
3272
+ const fragment = document.createDocumentFragment();
3273
+ fragment.appendChild(root);
3274
+ return {
3275
+ fragment,
3276
+ refs: {
3277
+ sheet,
3278
+ handle,
3279
+ content,
3280
+ backdrop,
3281
+ screen,
3282
+ leftButton,
3283
+ rightButton,
3284
+ announcer
3285
+ }
3286
+ };
3287
+ };
3288
+ exports.BottomSheetElement = class extends HTMLElement {
3289
+ constructor() {
3290
+ super();
3291
+ __publicField(this, "engine", null);
3292
+ __publicField(this, "root");
3293
+ __publicField(this, "offHandlers", []);
3294
+ __publicField(this, "refs");
3295
+ __publicField(this, "backdropClickHandler", null);
3296
+ this.root = this.attachShadow({ mode: "open" });
3297
+ const baseStyle = document.createElement("style");
3298
+ baseStyle.textContent = baseStyles;
3299
+ this.root.appendChild(baseStyle);
3300
+ const stylesheet = this.getAttribute(ATTR_STYLESHEET);
3301
+ if (stylesheet) {
3302
+ const link = document.createElement("link");
3303
+ link.rel = "stylesheet";
3304
+ link.href = stylesheet;
3305
+ this.root.appendChild(link);
3306
+ }
3307
+ const inlineStyle = document.createElement("style");
3308
+ inlineStyle.textContent = [
3309
+ ":host { display: contents; }",
3310
+ ".bs-handle:focus-visible {",
3311
+ " outline: 2px solid currentColor;",
3312
+ " outline-offset: -2px;",
3313
+ " border-radius: var(--bs-radius, 20px);",
3314
+ "}",
3315
+ "@media (forced-colors: active) {",
3316
+ " .bs-handle:focus-visible { outline: 2px solid CanvasText; }",
3317
+ "}"
3318
+ ].join("\n");
3319
+ this.root.appendChild(inlineStyle);
3320
+ const { fragment, refs } = buildShadowTree();
3321
+ this.refs = refs;
3322
+ this.root.appendChild(fragment);
3323
+ }
3324
+ static get observedAttributes() {
3325
+ return [
3326
+ ATTR_SNAP_POINTS,
3327
+ ATTR_ALLOWED,
3328
+ ATTR_INITIAL,
3329
+ ATTR_MODE,
3330
+ ATTR_BACKDROP,
3331
+ ATTR_ANIMATION,
3332
+ ATTR_FOCUS_TRAP,
3333
+ ATTR_CLOSE_ON_ESCAPE,
3334
+ ATTR_LOCK_BODY_SCROLL
3335
+ ];
3336
+ }
3337
+ connectedCallback() {
3338
+ this.initEngine();
3339
+ }
3340
+ disconnectedCallback() {
3341
+ this.teardownEngine();
3342
+ }
3343
+ attributeChangedCallback() {
3344
+ if (!this.engine) return;
3345
+ this.teardownEngine();
3346
+ this.initEngine();
3347
+ }
3348
+ teardownEngine() {
3349
+ this.offHandlers.forEach((off) => off());
3350
+ this.offHandlers = [];
3351
+ if (this.backdropClickHandler) {
3352
+ this.refs.backdrop.removeEventListener("click", this.backdropClickHandler);
3353
+ this.backdropClickHandler = null;
3354
+ }
3355
+ this.engine?.destroy();
3356
+ this.engine = null;
3357
+ }
3358
+ initEngine() {
3359
+ const { sheet, handle, content, backdrop, announcer } = this.refs;
3360
+ const mode = parseMode(this.getAttribute(ATTR_MODE));
3361
+ sheet.setAttribute("data-mode", mode);
3362
+ const focusTrap = this.getAttribute(ATTR_FOCUS_TRAP) === "true";
3363
+ if (focusTrap) {
3364
+ sheet.setAttribute("aria-modal", "true");
3365
+ } else {
3366
+ sheet.removeAttribute("aria-modal");
3367
+ }
3368
+ const showBackdrop = this.getAttribute(ATTR_BACKDROP) !== "false";
3369
+ backdrop.style.display = showBackdrop ? "" : "none";
3370
+ if (showBackdrop) {
3371
+ this.backdropClickHandler = () => void this.engine?.close();
3372
+ backdrop.addEventListener("click", this.backdropClickHandler);
3373
+ }
3374
+ const opts = {
3375
+ element: sheet,
3376
+ handle,
3377
+ scrollContainer: content,
3378
+ backdrop: showBackdrop ? backdrop : void 0,
3379
+ screenComponent: this.refs.screen,
3380
+ mode,
3381
+ snapPoints: parseSnapPoints(this.getAttribute(ATTR_SNAP_POINTS)),
3382
+ allowed: parseList(this.getAttribute(ATTR_ALLOWED)),
3383
+ initial: this.getAttribute(ATTR_INITIAL) ?? void 0,
3384
+ animation: parseAnimation(this.getAttribute(ATTR_ANIMATION)),
3385
+ focusTrap,
3386
+ closeOnEscape: this.getAttribute(ATTR_CLOSE_ON_ESCAPE) !== "false",
3387
+ lockBodyScroll: this.getAttribute(ATTR_LOCK_BODY_SCROLL) !== "false"
3388
+ };
3389
+ this.engine = new BottomSheetEngine(opts);
3390
+ this.offHandlers = [
3391
+ this.engine.on("snap", (payload) => {
3392
+ announcer.textContent = payload.id;
3393
+ this.dispatchEvent(new CustomEvent("snap", { detail: payload }));
3394
+ }),
3395
+ this.engine.on(
3396
+ "open",
3397
+ (payload) => this.dispatchEvent(new CustomEvent("open", { detail: payload }))
3398
+ ),
3399
+ this.engine.on(
3400
+ "close",
3401
+ () => this.dispatchEvent(new CustomEvent("close"))
3402
+ ),
3403
+ this.engine.on(
3404
+ "progress",
3405
+ (payload) => this.dispatchEvent(new CustomEvent("progress", { detail: payload }))
3406
+ )
3407
+ ];
3408
+ }
3409
+ snapTo(id) {
3410
+ return this.engine?.snapTo(id) ?? Promise.resolve();
3411
+ }
3412
+ open(id) {
3413
+ return this.engine?.open(id) ?? Promise.resolve();
3414
+ }
3415
+ close() {
3416
+ return this.engine?.close() ?? Promise.resolve();
3417
+ }
3418
+ setAllowed(ids, snap) {
3419
+ this.engine?.setAllowed(ids, snap);
3420
+ }
3421
+ get sheetState() {
3422
+ return this.engine?.state ?? null;
3423
+ }
3424
+ };
3425
+ registered = false;
3426
+ exports.defineBottomSheet = (tag = "bottom-sheet") => {
3427
+ if (registered || customElements.get(tag)) return;
3428
+ customElements.define(tag, exports.BottomSheetElement);
3429
+ registered = true;
3430
+ };
3431
+ }
3432
+ });
3433
+
3434
+ // src/web-component/index.ts
3435
+ init_BottomSheetElement();
3436
+ if (typeof window !== "undefined" && typeof customElements !== "undefined") {
3437
+ Promise.resolve().then(() => (init_BottomSheetElement(), BottomSheetElement_exports)).then(({ defineBottomSheet: defineBottomSheet2 }) => {
3438
+ defineBottomSheet2();
3439
+ });
3440
+ }
3441
+ //# sourceMappingURL=element.cjs.map
3442
+ //# sourceMappingURL=element.cjs.map