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