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