@buoy-gg/bottom-sheet 7.0.41 → 7.0.43

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