@buoy-gg/bottom-sheet 7.0.15 → 7.0.17

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.
@@ -0,0 +1,781 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * BottomSheet — Web renderer (React DOM).
5
+ *
6
+ * Same public API and behavior as the native BottomSheet.tsx:
7
+ * - two modes: "bottomSheet" (full-width, continuous drag-resize via header,
8
+ * fling-down to close) and "floating" (draggable window, 4-corner resize,
9
+ * tap topRight corner = close, tap topLeft = onBack)
10
+ * - double-tap header toggles mode, triple-tap closes
11
+ * - no backdrop; bottom-sheet container lets taps pass through beside it
12
+ *
13
+ * Web adaptations (each annotated inline with the native token it mirrors):
14
+ * - Pointer Events + setPointerCapture replace PanResponder (one code path
15
+ * for mouse and touch; capture gives "never release the responder"
16
+ * semantics). touch-action:none on gesture surfaces or WKWebView/Safari
17
+ * steals drags for page scroll.
18
+ * - During gestures we write styles directly to the DOM node (native rule:
19
+ * "direct setValue, no state updates") and commit to React state on release.
20
+ * - Native spring tension:180/friction:22 ≈ cubic-bezier(0.16,1,0.3,1) 320ms;
21
+ * the 200ms opacity fade is copied as-is.
22
+ * - Safe areas come from env(safe-area-inset-*) via a probe element (the
23
+ * native iPhone-dimension table is deleted). Requires viewport-fit=cover
24
+ * in the host page's <meta name="viewport"> to be non-zero on iOS.
25
+ * - Web-only additive props: `disableScrollWrapper` (tools with virtualized
26
+ * bodies need it — JsModal has it natively) and `zIndex` (AppHost stacking).
27
+ *
28
+ * Intentionally NOT ported: the visibility close animation on prop change —
29
+ * native returns null immediately when visible=false (only the gesture-close
30
+ * animates out), and we mirror that.
31
+ */
32
+
33
+ import { useState, useRef, useEffect } from 'react';
34
+ import { MIN_HEIGHT, DEFAULT_HEIGHT, FLOATING_WIDTH, FLOATING_HEIGHT, FLOATING_MIN_HEIGHT, floatingMinWidth, resizeCorner, shouldCloseOnRelease } from './bottomSheetGeometry';
35
+ import { useMultiTap } from './useMultiTap';
36
+
37
+ // ============================================================================
38
+ // THEME — same palette as native BottomSheet.tsx defaultTheme
39
+ // ============================================================================
40
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
41
+ const defaultTheme = {
42
+ background: 'rgba(8, 12, 21, 0.98)',
43
+ panel: 'rgba(16, 22, 35, 0.98)',
44
+ border: '#00B8E666',
45
+ primary: '#FFFFFF',
46
+ secondary: '#B8BFC9',
47
+ muted: '#7A8599',
48
+ success: '#4AFF9F',
49
+ error: '#FF5252',
50
+ info: '#00B8E6'
51
+ };
52
+
53
+ // ============================================================================
54
+ // TYPES — identical names to the native module
55
+ // ============================================================================
56
+
57
+ // ============================================================================
58
+ // VIEWPORT + SAFE AREA
59
+ // ============================================================================
60
+ function subscribeToViewport(onChange) {
61
+ window.addEventListener('resize', onChange);
62
+ window.visualViewport?.addEventListener('resize', onChange);
63
+ return () => {
64
+ window.removeEventListener('resize', onChange);
65
+ window.visualViewport?.removeEventListener('resize', onChange);
66
+ };
67
+ }
68
+ function readViewport() {
69
+ return {
70
+ width: window.innerWidth,
71
+ height: window.innerHeight
72
+ };
73
+ }
74
+
75
+ /** env(safe-area-inset-*) via a probe element (replaces the native iPhone table). */
76
+ function readSafeAreaInsets() {
77
+ const probe = document.createElement('div');
78
+ probe.style.cssText = 'position:fixed;visibility:hidden;pointer-events:none;' + 'top:env(safe-area-inset-top);bottom:env(safe-area-inset-bottom);' + 'left:env(safe-area-inset-left);right:env(safe-area-inset-right)';
79
+ document.body.appendChild(probe);
80
+ // Computed top/bottom/left/right resolve env() to px offsets from their
81
+ // respective edges — the raw parse IS the inset value.
82
+ const cs = getComputedStyle(probe);
83
+ const insets = {
84
+ top: parseFloat(cs.top) || 0,
85
+ bottom: parseFloat(cs.bottom) || 0,
86
+ left: parseFloat(cs.left) || 0,
87
+ right: parseFloat(cs.right) || 0
88
+ };
89
+ document.body.removeChild(probe);
90
+ return insets;
91
+ }
92
+ function useViewportAndInsets() {
93
+ const [state, setState] = useState(() => ({
94
+ viewport: readViewport(),
95
+ insets: readSafeAreaInsets()
96
+ }));
97
+ useEffect(() => {
98
+ return subscribeToViewport(() => {
99
+ setState({
100
+ viewport: readViewport(),
101
+ insets: readSafeAreaInsets()
102
+ });
103
+ });
104
+ }, []);
105
+ return state;
106
+ }
107
+
108
+ // ============================================================================
109
+ // GESTURE PLUMBING — pointer drag with tap detection
110
+ // ============================================================================
111
+
112
+ /**
113
+ * Shared pointerdown handler factory. Captures the pointer on the target,
114
+ * tracks total deltas + a velocity sample (px/ms, matching RN gestureState.vy
115
+ * units), and reports wasTap when total movement stayed within the native
116
+ * 5px tap threshold (DraggableHeader: dragDistance <= 5).
117
+ */
118
+ function startPointerDrag(e, callbacks) {
119
+ if (!e.isPrimary) return;
120
+ const target = e.currentTarget;
121
+ const startX = e.clientX;
122
+ const startY = e.clientY;
123
+ let lastY = e.clientY;
124
+ let lastT = performance.now();
125
+ let vy = 0;
126
+ let maxDistance = 0;
127
+ callbacks.onStart?.(e);
128
+ target.setPointerCapture(e.pointerId);
129
+ const handleMove = ev => {
130
+ if (ev.pointerId !== e.pointerId) return;
131
+ const dx = ev.clientX - startX;
132
+ const dy = ev.clientY - startY;
133
+ maxDistance = Math.max(maxDistance, Math.abs(dx) + Math.abs(dy));
134
+ const now = performance.now();
135
+ const dt = now - lastT;
136
+ if (dt > 0) {
137
+ vy = (ev.clientY - lastY) / dt;
138
+ lastY = ev.clientY;
139
+ lastT = now;
140
+ }
141
+ callbacks.onMove(ev, {
142
+ dx,
143
+ dy
144
+ });
145
+ };
146
+ const handleUp = ev => {
147
+ if (ev.pointerId !== e.pointerId) return;
148
+ cleanup();
149
+ callbacks.onEnd(ev, {
150
+ dx: ev.clientX - startX,
151
+ dy: ev.clientY - startY,
152
+ vy,
153
+ wasTap: maxDistance <= 5
154
+ });
155
+ };
156
+ const cleanup = () => {
157
+ target.removeEventListener('pointermove', handleMove);
158
+ target.removeEventListener('pointerup', handleUp);
159
+ target.removeEventListener('pointercancel', handleUp);
160
+ };
161
+ target.addEventListener('pointermove', handleMove);
162
+ target.addEventListener('pointerup', handleUp);
163
+ target.addEventListener('pointercancel', handleUp);
164
+ }
165
+
166
+ // ============================================================================
167
+ // HEADER PIECES
168
+ // ============================================================================
169
+ function DragIndicator({
170
+ isResizing,
171
+ mode,
172
+ hasCustomContent = false,
173
+ theme
174
+ }) {
175
+ return /*#__PURE__*/_jsxs("div", {
176
+ style: {
177
+ display: 'flex',
178
+ flexDirection: 'column',
179
+ alignItems: 'center',
180
+ // native: paddingVertical 8 / custom: 6 top, 2 bottom
181
+ padding: hasCustomContent ? '6px 0 2px' : '8px 0',
182
+ position: 'relative'
183
+ },
184
+ children: [/*#__PURE__*/_jsx("div", {
185
+ style: {
186
+ // native dragIndicator: 40×3 info@99 / floating: 50×5 muted / active: success
187
+ width: isResizing ? 40 : mode === 'floating' ? 50 : 40,
188
+ height: mode === 'floating' ? 5 : 3,
189
+ backgroundColor: isResizing ? theme.success : mode === 'floating' ? theme.muted : theme.info + '99',
190
+ borderRadius: 2,
191
+ boxShadow: `0 0 4px ${theme.info}CC`
192
+ }
193
+ }), isResizing && mode === 'bottomSheet' && /*#__PURE__*/_jsx("div", {
194
+ style: {
195
+ position: 'absolute',
196
+ display: 'flex',
197
+ flexDirection: 'row',
198
+ gap: 2,
199
+ marginTop: 12
200
+ },
201
+ children: [0, 1, 2].map(i => /*#__PURE__*/_jsx("div", {
202
+ style: {
203
+ width: 12,
204
+ height: 1,
205
+ backgroundColor: theme.success,
206
+ opacity: 0.6
207
+ }
208
+ }, i))
209
+ })]
210
+ });
211
+ }
212
+ function HeaderBody({
213
+ header,
214
+ isResizing,
215
+ mode,
216
+ theme
217
+ }) {
218
+ if (header?.customContent) {
219
+ return /*#__PURE__*/_jsxs("div", {
220
+ style: {
221
+ flex: 1,
222
+ display: 'flex',
223
+ flexDirection: 'column',
224
+ justifyContent: 'center'
225
+ },
226
+ children: [/*#__PURE__*/_jsx(DragIndicator, {
227
+ isResizing: isResizing,
228
+ mode: mode,
229
+ hasCustomContent: true,
230
+ theme: theme
231
+ }), header.customContent]
232
+ });
233
+ }
234
+ return /*#__PURE__*/_jsxs("div", {
235
+ style: {
236
+ flex: 1,
237
+ display: 'flex',
238
+ flexDirection: 'column',
239
+ justifyContent: 'center',
240
+ position: 'relative'
241
+ },
242
+ children: [/*#__PURE__*/_jsx(DragIndicator, {
243
+ isResizing: isResizing,
244
+ mode: mode,
245
+ theme: theme
246
+ }), /*#__PURE__*/_jsxs("div", {
247
+ style: {
248
+ padding: '0 16px',
249
+ display: 'flex',
250
+ flexDirection: 'column',
251
+ alignItems: 'center'
252
+ },
253
+ children: [header?.title &&
254
+ /*#__PURE__*/
255
+ // native headerTitle: 16px / 600 / primary
256
+ _jsx("span", {
257
+ style: {
258
+ fontSize: 16,
259
+ fontWeight: 600,
260
+ color: theme.primary
261
+ },
262
+ children: header.title
263
+ }), header?.subtitle &&
264
+ /*#__PURE__*/
265
+ // native headerSubtitle: 12px / secondary / paddingTop 4
266
+ _jsx("span", {
267
+ style: {
268
+ fontSize: 12,
269
+ color: theme.secondary,
270
+ paddingTop: 4
271
+ },
272
+ children: header.subtitle
273
+ })]
274
+ }), /*#__PURE__*/_jsx("div", {
275
+ style: {
276
+ position: 'absolute',
277
+ inset: 0,
278
+ display: 'flex',
279
+ alignItems: 'center',
280
+ justifyContent: 'center',
281
+ pointerEvents: 'none'
282
+ },
283
+ children: /*#__PURE__*/_jsx("span", {
284
+ style: {
285
+ fontSize: 10,
286
+ color: theme.muted,
287
+ fontStyle: 'italic'
288
+ },
289
+ children: "Double tap: Toggle \u2022 Triple tap: Close"
290
+ })
291
+ })]
292
+ });
293
+ }
294
+
295
+ // ============================================================================
296
+ // CONTENT — scroll wrapper + sticky footer (native content/footerContainer)
297
+ // ============================================================================
298
+ function SheetContent({
299
+ children,
300
+ footer,
301
+ footerHeight,
302
+ disableScrollWrapper,
303
+ contentStyle,
304
+ theme
305
+ }) {
306
+ return /*#__PURE__*/_jsxs("div", {
307
+ style: {
308
+ // native content: flex 1, background, bottom radius 16, overflow hidden
309
+ flex: 1,
310
+ minHeight: 0,
311
+ display: 'flex',
312
+ flexDirection: 'column',
313
+ backgroundColor: theme.background,
314
+ borderBottomLeftRadius: 16,
315
+ borderBottomRightRadius: 16,
316
+ overflow: 'hidden',
317
+ position: 'relative',
318
+ ...contentStyle
319
+ },
320
+ children: [disableScrollWrapper ? children : /*#__PURE__*/_jsx("div", {
321
+ style: {
322
+ flex: 1,
323
+ minHeight: 0,
324
+ overflowY: 'auto',
325
+ WebkitOverflowScrolling: 'touch',
326
+ paddingBottom: footerHeight
327
+ },
328
+ children: children
329
+ }), footer ? /*#__PURE__*/_jsx("div", {
330
+ style: {
331
+ position: 'absolute',
332
+ left: 0,
333
+ right: 0,
334
+ bottom: 0,
335
+ backgroundColor: theme.background,
336
+ borderBottomLeftRadius: 16,
337
+ borderBottomRightRadius: 16
338
+ },
339
+ children: footer
340
+ }) : null]
341
+ });
342
+ }
343
+
344
+ // ============================================================================
345
+ // BOTTOM SHEET MODE
346
+ // ============================================================================
347
+ const SPRING_TRANSITION = 'transform 320ms cubic-bezier(0.16, 1, 0.3, 1)';
348
+ const FADE_TRANSITION = 'opacity 200ms ease';
349
+ function SheetPanel({
350
+ props,
351
+ theme,
352
+ viewport,
353
+ effectiveMaxHeight,
354
+ panelHeight,
355
+ setPanelHeight,
356
+ onHeaderTap
357
+ }) {
358
+ const {
359
+ onClose,
360
+ children,
361
+ header,
362
+ styles: customStyles = {},
363
+ minHeight = MIN_HEIGHT,
364
+ footer,
365
+ footerHeight = 0,
366
+ disableScrollWrapper,
367
+ zIndex = 1000
368
+ } = props;
369
+ const sheetRef = useRef(null);
370
+ const wrapperRef = useRef(null);
371
+ const [isResizing, setIsResizing] = useState(false);
372
+ const [entered, setEntered] = useState(false);
373
+ const closingRef = useRef(false);
374
+ const heightRef = useRef(panelHeight);
375
+
376
+ // Enter animation: translateY 100% → 0 (native spring 180/22) + fade 200ms.
377
+ useEffect(() => {
378
+ const raf = requestAnimationFrame(() => setEntered(true));
379
+ return () => cancelAnimationFrame(raf);
380
+ }, []);
381
+ const handleHeaderPointerDown = e => {
382
+ if (closingRef.current) return;
383
+ // native: headerTouchOffset = locationY (finger position inside header)
384
+ const headerTop = e.currentTarget.getBoundingClientRect().top;
385
+ const touchOffset = e.clientY - headerTop;
386
+ startPointerDrag(e, {
387
+ onStart: () => setIsResizing(true),
388
+ onMove: ev => {
389
+ // native: targetHeight = SCREEN.height - (pageY - headerTouchOffset)
390
+ const sheetTop = ev.clientY - touchOffset;
391
+ const target = Math.max(minHeight, Math.min(viewport.height - sheetTop, effectiveMaxHeight));
392
+ heightRef.current = target;
393
+ if (sheetRef.current) sheetRef.current.style.height = `${target}px`;
394
+ },
395
+ onEnd: (_ev, {
396
+ dy,
397
+ vy,
398
+ wasTap
399
+ }) => {
400
+ setIsResizing(false);
401
+ if (wasTap) {
402
+ onHeaderTap();
403
+ return;
404
+ }
405
+ if (shouldCloseOnRelease(vy, dy, heightRef.current, minHeight)) {
406
+ // Gesture close animates out, then fires onClose (native parity).
407
+ closingRef.current = true;
408
+ const wrapper = wrapperRef.current;
409
+ if (wrapper) {
410
+ wrapper.style.transition = `${SPRING_TRANSITION}, ${FADE_TRANSITION}`;
411
+ wrapper.style.transform = 'translateY(100%)';
412
+ wrapper.style.opacity = '0';
413
+ }
414
+ setTimeout(onClose, 220);
415
+ return;
416
+ }
417
+ setPanelHeight(heightRef.current);
418
+ }
419
+ });
420
+ };
421
+ return /*#__PURE__*/_jsx("div", {
422
+ style: {
423
+ // native fullScreenContainer: absoluteFill + zIndex, box-none touches
424
+ position: 'fixed',
425
+ inset: 0,
426
+ zIndex,
427
+ pointerEvents: 'none'
428
+ },
429
+ children: /*#__PURE__*/_jsx("div", {
430
+ ref: wrapperRef,
431
+ style: {
432
+ // native bottomSheetWrapper: absolute bottom/left/right 0
433
+ position: 'absolute',
434
+ bottom: 0,
435
+ left: 0,
436
+ right: 0,
437
+ pointerEvents: 'auto',
438
+ transform: entered ? 'translateY(0)' : 'translateY(100%)',
439
+ opacity: entered ? 1 : 0,
440
+ transition: `${SPRING_TRANSITION}, ${FADE_TRANSITION}`
441
+ },
442
+ children: /*#__PURE__*/_jsxs("div", {
443
+ ref: sheetRef,
444
+ style: {
445
+ // native bottomSheet: panel bg, top radius 16, 1px border, glow shadow
446
+ display: 'flex',
447
+ flexDirection: 'column',
448
+ height: panelHeight,
449
+ backgroundColor: theme.panel,
450
+ borderTopLeftRadius: 16,
451
+ borderTopRightRadius: 16,
452
+ border: `1px solid ${theme.border}`,
453
+ boxShadow: `0 -4px 12px ${theme.info}4D, 0 -2px 24px rgba(0,0,0,0.4)`,
454
+ ...customStyles.container
455
+ },
456
+ children: [/*#__PURE__*/_jsx("div", {
457
+ onPointerDown: handleHeaderPointerDown,
458
+ style: {
459
+ // native header: minHeight 56, top radius, hairline bottom border
460
+ minHeight: 56,
461
+ display: 'flex',
462
+ backgroundColor: theme.panel,
463
+ borderTopLeftRadius: 16,
464
+ borderTopRightRadius: 16,
465
+ borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
466
+ cursor: isResizing ? 'ns-resize' : 'grab',
467
+ touchAction: 'none',
468
+ userSelect: 'none'
469
+ },
470
+ children: /*#__PURE__*/_jsx(HeaderBody, {
471
+ header: header,
472
+ isResizing: isResizing,
473
+ mode: "bottomSheet",
474
+ theme: theme
475
+ })
476
+ }), /*#__PURE__*/_jsx(SheetContent, {
477
+ footer: footer,
478
+ footerHeight: footerHeight,
479
+ disableScrollWrapper: disableScrollWrapper,
480
+ contentStyle: customStyles.content,
481
+ theme: theme,
482
+ children: children
483
+ })]
484
+ })
485
+ })
486
+ });
487
+ }
488
+
489
+ // ============================================================================
490
+ // FLOATING MODE
491
+ // ============================================================================
492
+ function CornerHandleView({
493
+ isActive,
494
+ theme
495
+ }) {
496
+ return /*#__PURE__*/_jsx("div", {
497
+ style: {
498
+ width: 20,
499
+ height: 20,
500
+ borderRadius: 10,
501
+ margin: 5,
502
+ // native handlerActive: success tint + 2px border + glow
503
+ backgroundColor: isActive ? theme.success + '1A' : 'transparent',
504
+ border: isActive ? `2px solid ${theme.success}` : '2px solid transparent',
505
+ boxShadow: isActive ? `0 0 8px ${theme.success}99` : 'none'
506
+ }
507
+ });
508
+ }
509
+ function FloatingWindow({
510
+ props,
511
+ theme,
512
+ viewport,
513
+ insets,
514
+ rect,
515
+ setRect,
516
+ onHeaderTap
517
+ }) {
518
+ const {
519
+ onClose,
520
+ children,
521
+ header,
522
+ styles: customStyles = {},
523
+ footer,
524
+ footerHeight = 0,
525
+ onBack,
526
+ disableScrollWrapper,
527
+ zIndex = 1000
528
+ } = props;
529
+ const windowRef = useRef(null);
530
+ const [isDragging, setIsDragging] = useState(false);
531
+ const [isResizing, setIsResizing] = useState(false);
532
+ const [entered, setEntered] = useState(false);
533
+ const rectRef = useRef(rect);
534
+ rectRef.current = rect;
535
+ const liveRectRef = useRef(rect);
536
+ useEffect(() => {
537
+ const raf = requestAnimationFrame(() => setEntered(true));
538
+ return () => cancelAnimationFrame(raf);
539
+ }, []);
540
+ const applyRect = r => {
541
+ liveRectRef.current = r;
542
+ const el = windowRef.current;
543
+ if (!el) return;
544
+ el.style.transform = `translate(${r.left}px, ${r.top}px)`;
545
+ el.style.width = `${r.width}px`;
546
+ el.style.height = `${r.height}px`;
547
+ };
548
+
549
+ // Header drag: absolute finger anchoring (native DraggableHeader:
550
+ // position = pageX/Y - touch offset inside the element).
551
+ const handleHeaderPointerDown = e => {
552
+ const start = rectRef.current;
553
+ const offsetX = e.clientX - start.left;
554
+ const offsetY = e.clientY - start.top;
555
+ startPointerDrag(e, {
556
+ onMove: (ev, {
557
+ dx,
558
+ dy
559
+ }) => {
560
+ if (Math.abs(dx) + Math.abs(dy) > 5) setIsDragging(true);
561
+ applyRect({
562
+ ...liveRectRef.current,
563
+ left: ev.clientX - offsetX,
564
+ top: ev.clientY - offsetY
565
+ });
566
+ },
567
+ onEnd: (_ev, {
568
+ wasTap
569
+ }) => {
570
+ setIsDragging(false);
571
+ if (wasTap) {
572
+ onHeaderTap();
573
+ return;
574
+ }
575
+ // Release clamp keeps the whole window inside the safe area (all
576
+ // four insets — deliberate improvement over native, which only
577
+ // clamps the top so windows could sit under the home indicator).
578
+ const live = liveRectRef.current;
579
+ const clamped = {
580
+ ...live,
581
+ left: Math.max(insets.left, Math.min(live.left, viewport.width - insets.right - live.width)),
582
+ top: Math.max(insets.top, Math.min(live.top, viewport.height - insets.bottom - live.height))
583
+ };
584
+ applyRect(clamped);
585
+ setRect(clamped);
586
+ }
587
+ });
588
+ };
589
+ const handleCornerPointerDown = corner => e => {
590
+ e.stopPropagation();
591
+ const start = {
592
+ ...rectRef.current
593
+ };
594
+ let didResize = false;
595
+ startPointerDrag(e, {
596
+ onStart: () => setIsResizing(true),
597
+ onMove: (_ev, {
598
+ dx,
599
+ dy
600
+ }) => {
601
+ // native: ignores sub-half-pixel jitter
602
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return;
603
+ didResize = true;
604
+ applyRect(resizeCorner(corner, start, dx, dy, viewport, insets, floatingMinWidth(viewport.width), FLOATING_MIN_HEIGHT));
605
+ },
606
+ onEnd: () => {
607
+ setIsResizing(false);
608
+ // native: tap (no resize) on topRight = close, topLeft = back
609
+ if (!didResize) {
610
+ if (corner === 'topRight') {
611
+ onClose();
612
+ return;
613
+ }
614
+ if (corner === 'topLeft' && onBack) {
615
+ onBack();
616
+ return;
617
+ }
618
+ }
619
+ setRect(liveRectRef.current);
620
+ }
621
+ });
622
+ };
623
+ const active = isDragging || isResizing;
624
+ const corners = [{
625
+ corner: 'topLeft',
626
+ style: {
627
+ top: 4,
628
+ left: 4
629
+ }
630
+ }, {
631
+ corner: 'topRight',
632
+ style: {
633
+ top: 4,
634
+ right: 4
635
+ }
636
+ }, {
637
+ corner: 'bottomLeft',
638
+ style: {
639
+ bottom: 4,
640
+ left: 4
641
+ }
642
+ }, {
643
+ corner: 'bottomRight',
644
+ style: {
645
+ bottom: 4,
646
+ right: 4
647
+ }
648
+ }];
649
+ return /*#__PURE__*/_jsxs("div", {
650
+ ref: windowRef,
651
+ style: {
652
+ // native floatingModal: absolute, panel bg, radius 16, glow shadow
653
+ position: 'fixed',
654
+ top: 0,
655
+ left: 0,
656
+ transform: `translate(${rect.left}px, ${rect.top}px)`,
657
+ width: rect.width,
658
+ height: rect.height,
659
+ display: 'flex',
660
+ flexDirection: 'column',
661
+ backgroundColor: theme.panel,
662
+ borderRadius: 16,
663
+ // native floatingModalDragging: success border while active
664
+ border: active ? `2px solid ${theme.success}` : `1px solid ${theme.border}`,
665
+ boxShadow: active ? `0 0 12px ${theme.success}99` : `0 0 20px ${theme.info}80, 0 8px 32px rgba(0,0,0,0.5)`,
666
+ zIndex,
667
+ opacity: entered ? 1 : 0,
668
+ transition: FADE_TRANSITION,
669
+ ...customStyles.container
670
+ },
671
+ children: [/*#__PURE__*/_jsx("div", {
672
+ onPointerDown: handleHeaderPointerDown,
673
+ style: {
674
+ minHeight: 56,
675
+ display: 'flex',
676
+ backgroundColor: theme.panel,
677
+ borderTopLeftRadius: 14,
678
+ borderTopRightRadius: 14,
679
+ borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
680
+ cursor: isDragging ? 'grabbing' : 'grab',
681
+ touchAction: 'none',
682
+ userSelect: 'none'
683
+ },
684
+ children: /*#__PURE__*/_jsx(HeaderBody, {
685
+ header: header,
686
+ isResizing: active,
687
+ mode: "floating",
688
+ theme: theme
689
+ })
690
+ }), /*#__PURE__*/_jsx(SheetContent, {
691
+ footer: footer,
692
+ footerHeight: footerHeight,
693
+ disableScrollWrapper: disableScrollWrapper,
694
+ contentStyle: customStyles.content,
695
+ theme: theme,
696
+ children: children
697
+ }), corners.map(({
698
+ corner,
699
+ style
700
+ }) => /*#__PURE__*/_jsx("div", {
701
+ onPointerDown: handleCornerPointerDown(corner),
702
+ style: {
703
+ // native cornerHandleWrapper: 30×30 hit area (hitSlop folded in)
704
+ position: 'absolute',
705
+ width: 30,
706
+ height: 30,
707
+ zIndex: 10,
708
+ touchAction: 'none',
709
+ cursor: corner === 'topLeft' || corner === 'bottomRight' ? 'nwse-resize' : 'nesw-resize',
710
+ ...style
711
+ },
712
+ children: /*#__PURE__*/_jsx(CornerHandleView, {
713
+ isActive: active,
714
+ theme: theme
715
+ })
716
+ }, corner))]
717
+ });
718
+ }
719
+
720
+ // ============================================================================
721
+ // MAIN COMPONENT — mode router (each mode is its own subtree; rule4 style)
722
+ // ============================================================================
723
+ export const BottomSheet = props => {
724
+ const {
725
+ visible,
726
+ onClose,
727
+ maxHeight,
728
+ initialHeight = DEFAULT_HEIGHT,
729
+ initialMode = 'bottomSheet',
730
+ onModeChange,
731
+ theme: customTheme
732
+ } = props;
733
+ const theme = {
734
+ ...defaultTheme,
735
+ ...customTheme
736
+ };
737
+ const {
738
+ viewport,
739
+ insets
740
+ } = useViewportAndInsets();
741
+ const [mode, setMode] = useState(initialMode);
742
+ const [panelHeight, setPanelHeight] = useState(initialHeight);
743
+ const [floatingRect, setFloatingRect] = useState(() => ({
744
+ width: FLOATING_WIDTH,
745
+ height: FLOATING_HEIGHT,
746
+ left: (window.innerWidth - FLOATING_WIDTH) / 2,
747
+ top: (window.innerHeight - FLOATING_HEIGHT) / 2
748
+ }));
749
+
750
+ // native: effectiveMaxHeight = maxHeight || SCREEN.height - insets.top
751
+ const effectiveMaxHeight = maxHeight || viewport.height - insets.top;
752
+ const toggleMode = () => {
753
+ const newMode = mode === 'bottomSheet' ? 'floating' : 'bottomSheet';
754
+ setMode(newMode);
755
+ onModeChange?.(newMode);
756
+ };
757
+ const handleHeaderTap = useMultiTap(toggleMode, onClose);
758
+ if (!visible) {
759
+ return null;
760
+ }
761
+ if (mode === 'floating') {
762
+ return /*#__PURE__*/_jsx(FloatingWindow, {
763
+ props: props,
764
+ theme: theme,
765
+ viewport: viewport,
766
+ insets: insets,
767
+ rect: floatingRect,
768
+ setRect: setFloatingRect,
769
+ onHeaderTap: handleHeaderTap
770
+ });
771
+ }
772
+ return /*#__PURE__*/_jsx(SheetPanel, {
773
+ props: props,
774
+ theme: theme,
775
+ viewport: viewport,
776
+ effectiveMaxHeight: effectiveMaxHeight,
777
+ panelHeight: Math.min(panelHeight, effectiveMaxHeight),
778
+ setPanelHeight: setPanelHeight,
779
+ onHeaderTap: handleHeaderTap
780
+ });
781
+ };