@xhub-short/ui 0.1.0-beta.1 → 0.1.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/CommentSheet.css-DyEc3Sro.d.ts +217 -0
  2. package/dist/VideoSlotPlayIndicator-DPs8Xt5C.d.ts +51 -0
  3. package/dist/chunk-3OB3OVYR.js +349 -0
  4. package/dist/{chunk-WKX2WBVO.js → chunk-3XPJHUYL.js} +1 -39
  5. package/dist/chunk-4RIMQOBR.js +58 -0
  6. package/dist/chunk-4TUBNA2X.js +180 -0
  7. package/dist/{chunk-4YDIRPIN.js → chunk-ANCP53F3.js} +3 -3
  8. package/dist/{chunk-UXMA4KJZ.js → chunk-CAWE42LH.js} +5 -3
  9. package/dist/{chunk-ANGBSV7L.js → chunk-CIIZ3IHV.js} +10 -5
  10. package/dist/chunk-DR7KR7OT.js +103 -0
  11. package/dist/chunk-DXLCQ4FH.js +102 -0
  12. package/dist/chunk-EDWS2IPH.js +1 -0
  13. package/dist/chunk-FR7UQSZP.js +570 -0
  14. package/dist/chunk-IWSBYOSS.js +91 -0
  15. package/dist/chunk-JEY6R4KJ.js +334 -0
  16. package/dist/chunk-KMJ3PQ7M.js +1262 -0
  17. package/dist/chunk-MFJS65C5.js +368 -0
  18. package/dist/{chunk-HW4LXTFT.js → chunk-OM4L7RE5.js} +18 -6
  19. package/dist/chunk-PBIH2F2Q.js +344 -0
  20. package/dist/chunk-PJ4NMVMY.js +326 -0
  21. package/dist/chunk-Q6MG7AVG.js +531 -0
  22. package/dist/chunk-QCKVF2DR.js +713 -0
  23. package/dist/chunk-QCRRF76W.js +75 -0
  24. package/dist/chunk-QUEJHA24.js +508 -0
  25. package/dist/chunk-VXW7AOGM.js +285 -0
  26. package/dist/chunk-YB7AXTX7.js +430 -0
  27. package/dist/chunk-ZGWSJ6Z5.js +601 -0
  28. package/dist/components/ActionBar/index.js +1 -1
  29. package/dist/components/AdvanceMenu/index.d.ts +78 -0
  30. package/dist/components/AdvanceMenu/index.js +1 -0
  31. package/dist/components/AuthorInfo/index.d.ts +5 -1
  32. package/dist/components/AuthorInfo/index.js +1 -1
  33. package/dist/components/BottomSheet/index.d.ts +82 -0
  34. package/dist/components/BottomSheet/index.js +1 -0
  35. package/dist/components/CleanModeOverlay/index.d.ts +60 -0
  36. package/dist/components/CleanModeOverlay/index.js +1 -0
  37. package/dist/components/CommentSheet/index.d.ts +164 -0
  38. package/dist/components/CommentSheet/index.js +1 -0
  39. package/dist/components/DetailView/index.d.ts +311 -0
  40. package/dist/components/DetailView/index.js +1 -0
  41. package/dist/components/ErrorBoundary/index.js +1 -1
  42. package/dist/components/ImageCarousel/index.d.ts +50 -0
  43. package/dist/components/ImageCarousel/index.js +1 -0
  44. package/dist/components/ImagePostSlot/index.d.ts +207 -0
  45. package/dist/components/ImagePostSlot/index.js +1 -0
  46. package/dist/components/ProgressBar/index.d.ts +30 -2
  47. package/dist/components/ProgressBar/index.js +1 -1
  48. package/dist/components/QualityPicker/index.d.ts +35 -0
  49. package/dist/components/QualityPicker/index.js +1 -0
  50. package/dist/components/ReportSheet/index.d.ts +68 -0
  51. package/dist/components/ReportSheet/index.js +1 -0
  52. package/dist/components/Skeleton/index.js +1 -1
  53. package/dist/components/SpeedPicker/index.d.ts +32 -0
  54. package/dist/components/SpeedPicker/index.js +1 -0
  55. package/dist/components/VideoFeed/index.d.ts +12 -1
  56. package/dist/components/VideoFeed/index.js +1 -1
  57. package/dist/components/VideoInfo/index.d.ts +4 -2
  58. package/dist/components/VideoInfo/index.js +1 -1
  59. package/dist/components/VideoPlayer/index.d.ts +14 -41
  60. package/dist/components/VideoPlayer/index.js +1 -1
  61. package/dist/components/VideoSlot/index.d.ts +84 -65
  62. package/dist/components/VideoSlot/index.js +2 -1
  63. package/dist/components/VirtualSlider/index.d.ts +339 -0
  64. package/dist/components/VirtualSlider/index.js +1 -0
  65. package/dist/components/icons/index.js +1 -1
  66. package/dist/index.d.ts +107 -95
  67. package/dist/index.js +84 -27
  68. package/package.json +51 -7
  69. package/dist/chunk-2PTMP65P.js +0 -738
  70. package/dist/chunk-4MN72OZH.js +0 -148
  71. package/dist/chunk-DHQJBXQW.js +0 -562
  72. package/dist/chunk-SSJDO24Q.js +0 -204
  73. package/dist/chunk-XAOEHLOX.js +0 -1326
  74. package/dist/chunk-YW23IBKF.js +0 -530
  75. package/dist/chunk-ZZDQKP4R.js +0 -418
  76. package/dist/use-gesture-react.esm-3SV4QLEJ.js +0 -1893
@@ -0,0 +1,344 @@
1
+ import { injectComponentCSS } from './chunk-CAWE42LH.js';
2
+ import { clsx } from 'clsx';
3
+ import { createContext, useInsertionEffect, useMemo, useState, useContext } from 'react';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+
6
+ var VideoInfoContext = createContext(void 0);
7
+ function useVideoInfoContext() {
8
+ const context = useContext(VideoInfoContext);
9
+ if (context === void 0) {
10
+ throw new Error("useVideoInfoContext must be used within a VideoInfoHeadless component");
11
+ }
12
+ return context;
13
+ }
14
+ function useOptionalVideoInfoContext() {
15
+ return useContext(VideoInfoContext);
16
+ }
17
+ function DefaultVerifiedIcon() {
18
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "10", height: "10", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" }) });
19
+ }
20
+ function VideoAuthorName({
21
+ name,
22
+ showAtPrefix = true,
23
+ showVerified = true,
24
+ isVerified,
25
+ verifiedIcon,
26
+ onClick,
27
+ className,
28
+ children
29
+ }) {
30
+ const context = useOptionalVideoInfoContext();
31
+ const authorName = name ?? context?.state.authorName;
32
+ const verified = isVerified ?? context?.state.isVerified ?? false;
33
+ const handleClick = onClick ?? context?.actions.openProfile;
34
+ if (!authorName) {
35
+ return null;
36
+ }
37
+ const handleKeyDown = (e) => {
38
+ if ((e.key === "Enter" || e.key === " ") && handleClick) {
39
+ e.preventDefault();
40
+ e.stopPropagation();
41
+ handleClick();
42
+ }
43
+ };
44
+ return /* @__PURE__ */ jsx(
45
+ "div",
46
+ {
47
+ className: clsx("sv-video-info__author", className),
48
+ onClick: (e) => {
49
+ e.stopPropagation();
50
+ handleClick?.();
51
+ },
52
+ onKeyDown: handleKeyDown,
53
+ role: handleClick ? "button" : void 0,
54
+ tabIndex: handleClick ? 0 : void 0,
55
+ "aria-label": handleClick ? `View ${authorName}'s profile` : void 0,
56
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ /* @__PURE__ */ jsxs("span", { className: "sv-video-info__author-name", children: [
58
+ showAtPrefix && /* @__PURE__ */ jsx("span", { className: "sv-video-info__author-prefix", children: "@" }),
59
+ authorName
60
+ ] }),
61
+ showVerified && verified && /* @__PURE__ */ jsx("span", { className: "sv-video-info__verified-badge", "aria-label": "Verified", children: verifiedIcon ?? /* @__PURE__ */ jsx(DefaultVerifiedIcon, {}) })
62
+ ] })
63
+ }
64
+ );
65
+ }
66
+ function VideoCaption({
67
+ caption,
68
+ maxLines = 2,
69
+ expandable = false,
70
+ moreText = "more",
71
+ lessText = "less",
72
+ onClick,
73
+ className,
74
+ children
75
+ }) {
76
+ const context = useOptionalVideoInfoContext();
77
+ const [isExpanded, setIsExpanded] = useState(false);
78
+ const captionText = caption ?? context?.state.caption;
79
+ if (!captionText) {
80
+ return null;
81
+ }
82
+ const lineClampClass = isExpanded ? "sv-video-info__caption--expanded" : `sv-video-info__caption--lines-${maxLines}`;
83
+ const handleToggleExpand = (e) => {
84
+ e.stopPropagation();
85
+ setIsExpanded(!isExpanded);
86
+ };
87
+ const handleClick = (e) => {
88
+ e.stopPropagation();
89
+ onClick?.();
90
+ };
91
+ const isClickable = onClick && !expandable;
92
+ return /* @__PURE__ */ jsx(
93
+ "div",
94
+ {
95
+ className: clsx(
96
+ "sv-video-info__caption",
97
+ lineClampClass,
98
+ isClickable && "sv-video-info__caption--clickable",
99
+ className
100
+ ),
101
+ onClick: isClickable ? handleClick : void 0,
102
+ onKeyDown: isClickable ? (e) => {
103
+ if (e.key === "Enter" || e.key === " ") {
104
+ e.stopPropagation();
105
+ onClick?.();
106
+ }
107
+ } : void 0,
108
+ role: isClickable ? "button" : void 0,
109
+ tabIndex: isClickable ? 0 : void 0,
110
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
111
+ captionText,
112
+ expandable && /* @__PURE__ */ jsx(
113
+ "button",
114
+ {
115
+ type: "button",
116
+ className: "sv-video-info__caption-more",
117
+ onClick: handleToggleExpand,
118
+ children: isExpanded ? lessText : moreText
119
+ }
120
+ )
121
+ ] })
122
+ }
123
+ );
124
+ }
125
+ function VideoHashtags({
126
+ hashtags,
127
+ clickable = true,
128
+ onHashtagClick,
129
+ display = "block",
130
+ className,
131
+ children
132
+ }) {
133
+ const context = useOptionalVideoInfoContext();
134
+ const hashtagsArray = hashtags ?? context?.state.hashtags ?? [];
135
+ const handleClick = onHashtagClick ?? context?.actions.onHashtagClick;
136
+ if (hashtagsArray.length === 0) {
137
+ return null;
138
+ }
139
+ const handleHashtagClick = (tag) => (e) => {
140
+ e.stopPropagation();
141
+ if (clickable && handleClick) {
142
+ handleClick(tag);
143
+ }
144
+ };
145
+ const handleKeyDown = (tag) => (e) => {
146
+ if ((e.key === "Enter" || e.key === " ") && clickable && handleClick) {
147
+ e.preventDefault();
148
+ handleClick(tag);
149
+ }
150
+ };
151
+ const formatTag = (tag) => tag.startsWith("#") ? tag : `#${tag}`;
152
+ if (children) {
153
+ return /* @__PURE__ */ jsx(Fragment, { children });
154
+ }
155
+ return /* @__PURE__ */ jsx(
156
+ "div",
157
+ {
158
+ className: clsx(
159
+ "sv-video-info__hashtags",
160
+ display === "inline" && "sv-video-info__hashtags--inline",
161
+ className
162
+ ),
163
+ children: hashtagsArray.map((tag) => /* @__PURE__ */ jsx(
164
+ "button",
165
+ {
166
+ type: "button",
167
+ className: clsx(
168
+ "sv-video-info__hashtag",
169
+ !clickable && "sv-video-info__hashtag--not-clickable"
170
+ ),
171
+ onClick: handleHashtagClick(tag),
172
+ onKeyDown: handleKeyDown(tag),
173
+ tabIndex: clickable ? 0 : -1,
174
+ "aria-label": clickable ? `View hashtag ${tag}` : void 0,
175
+ children: formatTag(tag)
176
+ },
177
+ tag
178
+ ))
179
+ }
180
+ );
181
+ }
182
+
183
+ // src/components/VideoInfo/VideoInfo.css.ts
184
+ var VIDEO_INFO_CSS = `.sv-video-info{display:flex;flex-direction:column;gap:var(--sv-video-info-gap,6px);color:var(--sv-video-info-color,#fff);font-family:var(--sv-font-family,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif);text-shadow:var(--sv-video-info-text-shadow,0 1px 2px rgba(0,0,0,.5));max-width:100%;overflow:hidden}.sv-video-info--overlay{pointer-events:none}.sv-video-info--overlay>*{pointer-events:auto}.sv-video-info__author{display:flex;align-items:center;gap:var(--sv-video-info-author-gap,6px);cursor:pointer;transition:opacity .15s ease}.sv-video-info__author:hover{opacity:.85}.sv-video-info__author:active{opacity:.7}.sv-video-info__author-name{font-size:var(--sv-video-info-author-font-size,16px);font-weight:var(--sv-video-info-author-font-weight,700);line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}.sv-video-info__author-prefix{opacity:.95}.sv-video-info__verified-badge{display:inline-flex;align-items:center;justify-content:center;width:var(--sv-video-info-verified-size,14px);height:var(--sv-video-info-verified-size,14px);background:var(--sv-video-info-verified-bg,#20d5ec);border-radius:50%;flex-shrink:0}.sv-video-info__verified-badge svg,.sv-video-info__verified-badge span{font-size:10px;color:#fff;font-weight:bold}.sv-video-info__caption{font-size:var(--sv-video-info-caption-font-size,14px);line-height:var(--sv-video-info-caption-line-height,1.4);color:var(--sv-video-info-caption-color,#fff);display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}.sv-video-info__caption--lines-1{-webkit-line-clamp:1}.sv-video-info__caption--lines-2{-webkit-line-clamp:2}.sv-video-info__caption--lines-3{-webkit-line-clamp:3}.sv-video-info__caption--expanded{-webkit-line-clamp:unset}.sv-video-info__caption--clickable{cursor:pointer;transition:opacity .15s ease}.sv-video-info__caption--clickable:hover{opacity:.85}.sv-video-info__caption--clickable:active{opacity:.7}.sv-video-info__caption-more{background:0 0;border:0;color:var(--sv-video-info-caption-more-color,rgba(255,255,255,.7));font-size:var(--sv-video-info-caption-font-size,14px);padding:0;margin-left:4px;cursor:pointer}.sv-video-info__caption-more:hover{color:var(--sv-video-info-caption-color,#fff)}.sv-video-info__hashtags{display:inline-flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--sv-video-info-hashtag-gap,6px);font-size:var(--sv-video-info-hashtag-font-size,14px)}.sv-video-info__hashtags--inline{gap:var(--sv-video-info-hashtag-gap-inline,4px)}.sv-video-info__hashtag{display:inline-flex;color:var(--sv-video-info-hashtag-color,#fff);font-weight:var(--sv-video-info-hashtag-font-weight,500);background:0 0;border:0;padding:0;cursor:pointer;transition:opacity .15s ease;white-space:nowrap}.sv-video-info__hashtag:hover{opacity:.8;text-decoration:underline}.sv-video-info__hashtag--not-clickable{cursor:default}.sv-video-info__hashtag--not-clickable:hover{opacity:1;text-decoration:none}.sv-video-info__caption .sv-video-info__hashtag{display:inline}.sv-video-info__location{display:flex;align-items:center;gap:var(--sv-video-info-location-gap,4px);font-size:var(--sv-video-info-location-font-size,13px);color:var(--sv-video-info-location-color,rgba(255,255,255,.9));cursor:pointer;transition:opacity .15s ease}.sv-video-info__location:hover{opacity:.8}.sv-video-info__location-icon{font-size:14px;flex-shrink:0}.sv-video-info__location-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sv-video-info__music{display:flex;align-items:center;gap:var(--sv-video-info-music-gap,8px);font-size:var(--sv-video-info-music-font-size,13px);color:var(--sv-video-info-music-color,#fff);cursor:pointer;transition:opacity .15s ease;overflow:hidden}.sv-video-info__music:hover{opacity:.8}.sv-video-info__music-icon{font-size:14px;flex-shrink:0}.sv-video-info__music-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sv-video-info__music-text--marquee{animation:sv-video-info-marquee 8s linear infinite}@keyframes sv-video-info-marquee{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}`;
185
+ function DefaultLocationIcon() {
186
+ return /* @__PURE__ */ jsx("span", { className: "sv-video-info__location-icon", children: "\u{1F4CD}" });
187
+ }
188
+ function VideoLocation({
189
+ location,
190
+ icon,
191
+ onClick,
192
+ className,
193
+ children
194
+ }) {
195
+ const context = useOptionalVideoInfoContext();
196
+ const locationText = location ?? context?.state.location;
197
+ const handleClick = onClick ?? context?.actions.onLocationClick;
198
+ if (!locationText) {
199
+ return null;
200
+ }
201
+ const handleKeyDown = (e) => {
202
+ if ((e.key === "Enter" || e.key === " ") && handleClick) {
203
+ e.preventDefault();
204
+ e.stopPropagation();
205
+ handleClick();
206
+ }
207
+ };
208
+ return /* @__PURE__ */ jsx(
209
+ "div",
210
+ {
211
+ className: clsx("sv-video-info__location", className),
212
+ onClick: (e) => {
213
+ e.stopPropagation();
214
+ handleClick?.();
215
+ },
216
+ onKeyDown: handleKeyDown,
217
+ role: handleClick ? "button" : void 0,
218
+ tabIndex: handleClick ? 0 : void 0,
219
+ "aria-label": handleClick ? `View location: ${locationText}` : void 0,
220
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
221
+ icon ?? /* @__PURE__ */ jsx(DefaultLocationIcon, {}),
222
+ /* @__PURE__ */ jsx("span", { className: "sv-video-info__location-text", children: locationText })
223
+ ] })
224
+ }
225
+ );
226
+ }
227
+ function DefaultMusicIcon() {
228
+ return /* @__PURE__ */ jsx("span", { className: "sv-video-info__music-icon", children: "\u{1F3B5}" });
229
+ }
230
+ function VideoMusic({
231
+ title,
232
+ artist,
233
+ icon,
234
+ marquee = false,
235
+ onClick,
236
+ className,
237
+ children
238
+ }) {
239
+ const context = useOptionalVideoInfoContext();
240
+ const musicTitle = title ?? context?.state.music?.title;
241
+ const musicArtist = artist ?? context?.state.music?.artist;
242
+ const handleClick = onClick ?? context?.actions.onMusicClick;
243
+ if (!musicTitle && !musicArtist) {
244
+ return null;
245
+ }
246
+ const displayText = musicArtist ? `${musicTitle} - ${musicArtist}` : musicTitle;
247
+ const handleKeyDown = (e) => {
248
+ if ((e.key === "Enter" || e.key === " ") && handleClick) {
249
+ e.preventDefault();
250
+ e.stopPropagation();
251
+ handleClick();
252
+ }
253
+ };
254
+ return /* @__PURE__ */ jsx(
255
+ "div",
256
+ {
257
+ className: clsx("sv-video-info__music", className),
258
+ onClick: (e) => {
259
+ e.stopPropagation();
260
+ handleClick?.();
261
+ },
262
+ onKeyDown: handleKeyDown,
263
+ role: handleClick ? "button" : void 0,
264
+ tabIndex: handleClick ? 0 : void 0,
265
+ "aria-label": handleClick ? `View sound: ${displayText}` : void 0,
266
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
267
+ icon ?? /* @__PURE__ */ jsx(DefaultMusicIcon, {}),
268
+ /* @__PURE__ */ jsx(
269
+ "span",
270
+ {
271
+ className: clsx(
272
+ "sv-video-info__music-text",
273
+ marquee && "sv-video-info__music-text--marquee"
274
+ ),
275
+ children: displayText
276
+ }
277
+ )
278
+ ] })
279
+ }
280
+ );
281
+ }
282
+ function VideoInfoHeadlessRoot({
283
+ videoInfoState,
284
+ videoInfoActions,
285
+ overlay = false,
286
+ className,
287
+ children,
288
+ testId
289
+ }) {
290
+ useInsertionEffect(() => {
291
+ return injectComponentCSS("video-info", VIDEO_INFO_CSS);
292
+ }, []);
293
+ const contextValue = useMemo(
294
+ () => ({
295
+ state: videoInfoState,
296
+ actions: videoInfoActions
297
+ }),
298
+ [videoInfoState, videoInfoActions]
299
+ );
300
+ const stopPropagation = (e) => {
301
+ e.stopPropagation();
302
+ };
303
+ return /* @__PURE__ */ jsx(VideoInfoContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
304
+ "div",
305
+ {
306
+ className: clsx("sv-video-info", overlay && "sv-video-info--overlay", className),
307
+ onClick: stopPropagation,
308
+ onKeyDown: stopPropagation,
309
+ onPointerDown: stopPropagation,
310
+ onPointerUp: stopPropagation,
311
+ onTouchStart: stopPropagation,
312
+ onTouchEnd: stopPropagation,
313
+ "data-testid": testId,
314
+ children: children ?? // Default layout
315
+ /* @__PURE__ */ jsxs(Fragment, { children: [
316
+ /* @__PURE__ */ jsx(VideoAuthorName, { showAtPrefix: true, showVerified: true }),
317
+ /* @__PURE__ */ jsx(VideoCaption, { maxLines: 2 }),
318
+ /* @__PURE__ */ jsx(VideoHashtags, { clickable: true })
319
+ ] })
320
+ }
321
+ ) });
322
+ }
323
+ function VideoInfoContent({
324
+ children,
325
+ className
326
+ }) {
327
+ return /* @__PURE__ */ jsx("div", { className: clsx("sv-video-info__content", className), children });
328
+ }
329
+ var VideoInfoHeadless = Object.assign(VideoInfoHeadlessRoot, {
330
+ /** Author name with @ prefix and verified badge */
331
+ AuthorName: VideoAuthorName,
332
+ /** Video caption/title with line clamping */
333
+ Caption: VideoCaption,
334
+ /** Hashtags (inline or block) */
335
+ Hashtags: VideoHashtags,
336
+ /** Location info */
337
+ Location: VideoLocation,
338
+ /** Music/sound info */
339
+ Music: VideoMusic,
340
+ /** Content wrapper */
341
+ Content: VideoInfoContent
342
+ });
343
+
344
+ export { VIDEO_INFO_CSS, VideoAuthorName, VideoCaption, VideoHashtags, VideoInfoContext, VideoInfoHeadless, VideoLocation, VideoMusic, useOptionalVideoInfoContext, useVideoInfoContext };
@@ -0,0 +1,326 @@
1
+ import { clsx } from 'clsx';
2
+ import { createContext, memo, useState, useRef, useEffect, useCallback, useContext } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ // src/components/BottomSheet/BottomSheetHeadless.tsx
7
+
8
+ // src/components/BottomSheet/BottomSheet.css.ts
9
+ var BOTTOM_SHEET_CSS = `:root{--sv-sheet-bg:#fff;--sv-sheet-backdrop-bg:rgba(0,0,0,.5);--sv-sheet-handle-bg:rgba(0,0,0,.2);--sv-sheet-border-radius:24px;--sv-sheet-z-index:1000;--sv-sheet-animation-duration:300ms;--sv-sheet-animation-easing:cubic-bezier(.32,.72,0,1)}.sv-bottom-sheet-backdrop{position:fixed;inset:0;background:var(--sv-sheet-backdrop-bg);z-index:var(--sv-sheet-z-index);opacity:0;visibility:hidden;pointer-events:none;transition:opacity var(--sv-sheet-animation-duration)ease,visibility 0s linear var(--sv-sheet-animation-duration)}.sv-bottom-sheet-backdrop[data-state="open"]{opacity:1;visibility:visible;pointer-events:auto;transition:opacity var(--sv-sheet-animation-duration)ease,visibility 0s linear 0s}@keyframes sv-sheet-slide-up{from{transform:translateY(100vh)}to{transform:translateY(0)}}@keyframes sv-sheet-slide-down{from{transform:translateY(0)}to{transform:translateY(100vh)}}.sv-bottom-sheet{position:fixed;left:0;right:0;bottom:0;background:var(--sv-sheet-bg);border-radius:var(--sv-sheet-border-radius)var(--sv-sheet-border-radius)0 0;z-index:calc(var(--sv-sheet-z-index)+1);display:flex;flex-direction:column;max-height:var(--sv-sheet-max-height,80vh);transform:translateY(100vh);overflow:hidden;touch-action:pan-y;isolation:isolate;pointer-events:none;will-change:transform;outline:none}.sv-bottom-sheet[data-state="open"]{transform:translateY(0);pointer-events:auto;animation:sv-sheet-slide-up var(--sv-sheet-animation-duration)var(--sv-sheet-animation-easing)forwards}.sv-bottom-sheet[data-state="closing"]{pointer-events:none;animation:sv-sheet-slide-down var(--sv-sheet-animation-duration)var(--sv-sheet-animation-easing)forwards !important}.sv-bottom-sheet__handle{display:flex;justify-content:center;padding:12px 0 8px;cursor:grab;touch-action:none;flex-shrink:0}.sv-bottom-sheet__handle:active{cursor:grabbing}.sv-bottom-sheet__handle-bar{width:36px;height:4px;background:var(--sv-sheet-handle-bg);border-radius:2px}.sv-bottom-sheet__content{flex:1;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}body.sv-bottom-sheet-open{overflow:hidden !important;position:fixed !important;width:100% !important}body.sv-bottom-sheet-open .sv-slider,body.sv-bottom-sheet-open .sv-video-feed{pointer-events:none !important;touch-action:none !important}body.sv-bottom-sheet-open .sv-bottom-sheet,body.sv-bottom-sheet-open .sv-bottom-sheet-backdrop{touch-action:auto !important}`;
10
+ function injectBottomSheetCSS() {
11
+ if (typeof document === "undefined") return;
12
+ const styleId = "sv-bottom-sheet-styles";
13
+ if (!document.getElementById(styleId)) {
14
+ const style = document.createElement("style");
15
+ style.id = styleId;
16
+ style.textContent = BOTTOM_SHEET_CSS;
17
+ document.head.appendChild(style);
18
+ }
19
+ }
20
+ if (typeof document !== "undefined") {
21
+ injectBottomSheetCSS();
22
+ }
23
+ injectBottomSheetCSS();
24
+ var BottomSheetContext = createContext(null);
25
+ function useBottomSheetContext() {
26
+ const ctx = useContext(BottomSheetContext);
27
+ if (!ctx) {
28
+ throw new Error("useBottomSheetContext must be used within BottomSheetHeadless");
29
+ }
30
+ return ctx;
31
+ }
32
+ var DEFAULT_ANIMATION_DURATION = 300;
33
+ function BottomSheetHeadlessComponent({
34
+ isOpen,
35
+ onClose,
36
+ children,
37
+ height = "80vh",
38
+ className,
39
+ backdropClassName,
40
+ contentClassName,
41
+ closeOnBackdropClick = true,
42
+ closeOnEscape = true,
43
+ showDragHandle = false,
44
+ enableDragToDismiss = true,
45
+ dragCloseThreshold = 150,
46
+ ariaLabel = "Bottom sheet",
47
+ zIndex,
48
+ animationDuration = DEFAULT_ANIMATION_DURATION,
49
+ dragHandleSelector = ".sv-bottom-sheet__handle",
50
+ scrollContainerSelector = ".sv-bottom-sheet__content",
51
+ dragExcludeSelector
52
+ }) {
53
+ const [isClosing, setIsClosing] = useState(false);
54
+ const [isVisible, setIsVisible] = useState(false);
55
+ const sheetRef = useRef(null);
56
+ const dragStartY = useRef(0);
57
+ const currentTranslateY = useRef(0);
58
+ const isDragging = useRef(false);
59
+ const isDragClosing = useRef(false);
60
+ const closeTimeoutRef = useRef(null);
61
+ const openedAtRef = useRef(0);
62
+ const scrollYRef = useRef(0);
63
+ const dragSourceRef = useRef(null);
64
+ useEffect(() => {
65
+ if (isOpen) {
66
+ setIsVisible(true);
67
+ openedAtRef.current = Date.now();
68
+ setIsClosing(false);
69
+ if (closeTimeoutRef.current) {
70
+ clearTimeout(closeTimeoutRef.current);
71
+ closeTimeoutRef.current = null;
72
+ }
73
+ }
74
+ }, [isOpen]);
75
+ const handleClose = useCallback(() => {
76
+ if (isClosing) return;
77
+ const timeSinceOpen = Date.now() - openedAtRef.current;
78
+ if (timeSinceOpen < 100) return;
79
+ setIsClosing(true);
80
+ closeTimeoutRef.current = setTimeout(() => {
81
+ setIsClosing(false);
82
+ setIsVisible(false);
83
+ onClose();
84
+ }, animationDuration);
85
+ }, [isClosing, onClose, animationDuration]);
86
+ useEffect(() => {
87
+ return () => {
88
+ if (closeTimeoutRef.current) {
89
+ clearTimeout(closeTimeoutRef.current);
90
+ }
91
+ };
92
+ }, []);
93
+ useEffect(() => {
94
+ if (isOpen && sheetRef.current) {
95
+ requestAnimationFrame(() => {
96
+ sheetRef.current?.focus();
97
+ });
98
+ }
99
+ }, [isOpen]);
100
+ useEffect(() => {
101
+ if (!isOpen || !closeOnEscape) return;
102
+ const handleKeyDown = (e) => {
103
+ if (e.key === "Escape") {
104
+ e.preventDefault();
105
+ handleClose();
106
+ }
107
+ };
108
+ document.addEventListener("keydown", handleKeyDown);
109
+ return () => document.removeEventListener("keydown", handleKeyDown);
110
+ }, [isOpen, closeOnEscape, handleClose]);
111
+ const handleBackdropClick = useCallback(
112
+ (e) => {
113
+ e.stopPropagation();
114
+ if (closeOnBackdropClick && e.target === e.currentTarget) {
115
+ handleClose();
116
+ }
117
+ },
118
+ [closeOnBackdropClick, handleClose]
119
+ );
120
+ const handleSheetClick = useCallback((e) => {
121
+ e.stopPropagation();
122
+ }, []);
123
+ useEffect(() => {
124
+ if (!isOpen) return;
125
+ scrollYRef.current = window.scrollY;
126
+ document.body.classList.add("sv-bottom-sheet-open");
127
+ document.body.style.top = `-${scrollYRef.current}px`;
128
+ const blockTouchMove = (e) => {
129
+ const target = e.target;
130
+ const isInsideSheet = target.closest(".sv-bottom-sheet");
131
+ if (!isInsideSheet) {
132
+ e.preventDefault();
133
+ e.stopPropagation();
134
+ }
135
+ };
136
+ document.addEventListener("touchmove", blockTouchMove, { passive: false, capture: true });
137
+ return () => {
138
+ document.body.classList.remove("sv-bottom-sheet-open");
139
+ document.body.style.top = "";
140
+ window.scrollTo(0, scrollYRef.current);
141
+ document.removeEventListener("touchmove", blockTouchMove, { capture: true });
142
+ };
143
+ }, [isOpen]);
144
+ const isScrollContainerAtTop = useCallback(() => {
145
+ const container = sheetRef.current?.querySelector(scrollContainerSelector);
146
+ if (!container) return true;
147
+ return container.scrollTop <= 0;
148
+ }, [scrollContainerSelector]);
149
+ const getDragSource = useCallback(
150
+ (target) => {
151
+ if (dragExcludeSelector && target.closest(dragExcludeSelector)) {
152
+ return null;
153
+ }
154
+ const isHandle = target.closest(dragHandleSelector);
155
+ if (isHandle) return "handle";
156
+ const isInScrollContainer = target.closest(scrollContainerSelector);
157
+ if (isInScrollContainer && isScrollContainerAtTop()) return "overscroll";
158
+ return null;
159
+ },
160
+ [dragHandleSelector, scrollContainerSelector, dragExcludeSelector, isScrollContainerAtTop]
161
+ );
162
+ useEffect(() => {
163
+ if (!isOpen || !sheetRef.current) return;
164
+ sheetRef.current.style.transform = "";
165
+ sheetRef.current.style.transition = "";
166
+ sheetRef.current.style.animation = "";
167
+ currentTranslateY.current = 0;
168
+ isDragging.current = false;
169
+ dragSourceRef.current = null;
170
+ isDragClosing.current = false;
171
+ }, [isOpen]);
172
+ const animateClose = useCallback(
173
+ (sheet) => {
174
+ isDragClosing.current = true;
175
+ sheet.style.transition = `transform ${animationDuration}ms cubic-bezier(0.32, 0.72, 0, 1)`;
176
+ sheet.style.transform = "translateY(100vh)";
177
+ closeTimeoutRef.current = setTimeout(() => {
178
+ isDragClosing.current = false;
179
+ setIsVisible(false);
180
+ onClose();
181
+ }, animationDuration);
182
+ },
183
+ [onClose, animationDuration]
184
+ );
185
+ const animateSnapBack = useCallback(
186
+ (sheet) => {
187
+ sheet.style.transition = `transform ${animationDuration}ms cubic-bezier(0.32, 0.72, 0, 1)`;
188
+ sheet.style.transform = "translateY(0)";
189
+ setTimeout(() => {
190
+ sheet.style.transition = "";
191
+ sheet.style.transform = "";
192
+ }, animationDuration);
193
+ },
194
+ [animationDuration]
195
+ );
196
+ useEffect(() => {
197
+ if (!isVisible || !enableDragToDismiss) return;
198
+ let mounted = true;
199
+ const handlersRef = {
200
+ onTouchStart: null,
201
+ onTouchMove: null,
202
+ onTouchEnd: null
203
+ };
204
+ requestAnimationFrame(() => {
205
+ if (!mounted) return;
206
+ const sheet = sheetRef.current;
207
+ if (!sheet) return;
208
+ const onTouchStart = (e) => {
209
+ e.stopPropagation();
210
+ const source = getDragSource(e.target);
211
+ if (!source) return;
212
+ dragSourceRef.current = source;
213
+ isDragging.current = true;
214
+ dragStartY.current = e.touches[0]?.clientY ?? 0;
215
+ currentTranslateY.current = 0;
216
+ sheet.style.animation = "none";
217
+ sheet.style.transition = "none";
218
+ void sheet.offsetHeight;
219
+ };
220
+ const onTouchMove = (e) => {
221
+ e.stopPropagation();
222
+ const clientY = e.touches[0]?.clientY ?? 0;
223
+ const deltaY = clientY - dragStartY.current;
224
+ if (dragSourceRef.current === "overscroll" && (deltaY <= 0 || !isScrollContainerAtTop())) {
225
+ isDragging.current = false;
226
+ return;
227
+ }
228
+ if (!isDragging.current) return;
229
+ currentTranslateY.current = Math.max(0, deltaY);
230
+ if (currentTranslateY.current > 0) {
231
+ e.preventDefault();
232
+ }
233
+ sheet.style.transform = `translateY(${currentTranslateY.current}px)`;
234
+ };
235
+ const onTouchEnd = (e) => {
236
+ e.stopPropagation();
237
+ dragSourceRef.current = null;
238
+ if (!isDragging.current) return;
239
+ isDragging.current = false;
240
+ const dragDistance = currentTranslateY.current;
241
+ currentTranslateY.current = 0;
242
+ if (dragDistance > dragCloseThreshold) {
243
+ animateClose(sheet);
244
+ } else if (dragDistance > 0) {
245
+ animateSnapBack(sheet);
246
+ }
247
+ };
248
+ handlersRef.onTouchStart = onTouchStart;
249
+ handlersRef.onTouchMove = onTouchMove;
250
+ handlersRef.onTouchEnd = onTouchEnd;
251
+ sheet.addEventListener("touchstart", onTouchStart, { passive: false });
252
+ sheet.addEventListener("touchmove", onTouchMove, { passive: false });
253
+ sheet.addEventListener("touchend", onTouchEnd, { passive: false });
254
+ });
255
+ return () => {
256
+ mounted = false;
257
+ const sheet = sheetRef.current;
258
+ if (sheet && handlersRef.onTouchStart && handlersRef.onTouchMove && handlersRef.onTouchEnd) {
259
+ sheet.removeEventListener("touchstart", handlersRef.onTouchStart);
260
+ sheet.removeEventListener("touchmove", handlersRef.onTouchMove);
261
+ sheet.removeEventListener("touchend", handlersRef.onTouchEnd);
262
+ }
263
+ };
264
+ }, [
265
+ isVisible,
266
+ enableDragToDismiss,
267
+ dragCloseThreshold,
268
+ isScrollContainerAtTop,
269
+ getDragSource,
270
+ animateClose,
271
+ animateSnapBack
272
+ ]);
273
+ if (!isVisible || typeof document === "undefined") return null;
274
+ const backdropState = isClosing || isDragClosing.current ? "closing" : isOpen ? "open" : "closed";
275
+ const sheetState = isClosing && !isDragClosing.current ? "closing" : isOpen ? "open" : "closed";
276
+ const sheetStyle = {
277
+ "--sv-sheet-max-height": height,
278
+ ...zIndex !== void 0 && { "--sv-sheet-z-index": zIndex },
279
+ ...animationDuration !== DEFAULT_ANIMATION_DURATION && {
280
+ "--sv-sheet-animation-duration": `${animationDuration}ms`
281
+ }
282
+ };
283
+ const backdropStyle = zIndex !== void 0 ? { "--sv-sheet-z-index": zIndex } : {};
284
+ return createPortal(
285
+ /* @__PURE__ */ jsxs(BottomSheetContext.Provider, { value: { isOpen, onClose: handleClose }, children: [
286
+ /* @__PURE__ */ jsx(
287
+ "div",
288
+ {
289
+ className: clsx("sv-bottom-sheet-backdrop", backdropClassName),
290
+ "data-state": backdropState,
291
+ onClick: handleBackdropClick,
292
+ onPointerDown: (e) => e.stopPropagation(),
293
+ onPointerUp: (e) => e.stopPropagation(),
294
+ onTouchStart: (e) => e.stopPropagation(),
295
+ onTouchEnd: (e) => e.stopPropagation(),
296
+ style: backdropStyle,
297
+ "aria-hidden": "true"
298
+ }
299
+ ),
300
+ /* @__PURE__ */ jsxs(
301
+ "div",
302
+ {
303
+ ref: sheetRef,
304
+ className: clsx("sv-bottom-sheet", className),
305
+ "data-state": sheetState,
306
+ style: sheetStyle,
307
+ "aria-modal": "true",
308
+ "aria-label": ariaLabel,
309
+ tabIndex: -1,
310
+ onClick: handleSheetClick,
311
+ onPointerDown: (e) => e.stopPropagation(),
312
+ onPointerUp: (e) => e.stopPropagation(),
313
+ children: [
314
+ showDragHandle && /* @__PURE__ */ jsx("div", { className: "sv-bottom-sheet__handle", children: /* @__PURE__ */ jsx("div", { className: "sv-bottom-sheet__handle-bar" }) }),
315
+ /* @__PURE__ */ jsx("div", { className: clsx("sv-bottom-sheet__content", contentClassName), children })
316
+ ]
317
+ }
318
+ )
319
+ ] }),
320
+ document.body
321
+ );
322
+ }
323
+ var BottomSheetHeadless = memo(BottomSheetHeadlessComponent);
324
+ BottomSheetHeadless.displayName = "BottomSheetHeadless";
325
+
326
+ export { BOTTOM_SHEET_CSS, BottomSheetHeadless, injectBottomSheetCSS, useBottomSheetContext };