@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,570 @@
1
+ import { ImageCarouselHeadless } from './chunk-Q6MG7AVG.js';
2
+ import { clsx2 } from './chunk-EDWS2IPH.js';
3
+ import { injectComponentCSS } from './chunk-CAWE42LH.js';
4
+ import { createContext, memo, useInsertionEffect, useState, useRef, useEffect, useCallback, useMemo, useContext } from 'react';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+
7
+ // src/components/DetailView/DetailView.css.ts
8
+ var DETAIL_VIEW_CSS = `.sv-detail-view{position:fixed;inset:0;z-index:var(--sv-detail-view-z-index,1000);background:var(--sv-detail-view-bg,#000);overflow:hidden;touch-action:none}.sv-detail-view--entering{animation:sv-detail-view-enter .3s cubic-bezier(.32,.72,0,1)forwards}.sv-detail-view--exiting{animation:sv-detail-view-exit .3s cubic-bezier(.32,.72,0,1)forwards}.sv-detail-view--dragging{transition:none}@keyframes sv-detail-view-enter{from{opacity:0;transform:translateY(100%)scale(.9)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes sv-detail-view-exit{from{opacity:1;transform:translateY(0)scale(1)}to{opacity:0;transform:translateY(100%)scale(.9)}}.sv-detail-view__backdrop{position:absolute;inset:0;background:var(--sv-detail-view-backdrop-bg,rgba(0,0,0,.8));opacity:1;transition:opacity .3s ease}.sv-detail-view--exiting .sv-detail-view__backdrop{opacity:0}.sv-detail-view__container{position:absolute;inset:0;display:flex;flex-direction:column;will-change:transform}.sv-detail-view__header{position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:var(--sv-detail-view-header-padding,12px 16px);background:var(--sv-detail-view-header-bg,rgba(0,0,0,.8));backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}.sv-detail-view__back-button{display:flex;align-items:center;justify-content:center;width:40px;height:40px;padding:0;border:0;border-radius:50%;background:var(--sv-detail-view-back-bg,rgba(255,255,255,.1));color:var(--sv-detail-view-back-color,#fff);cursor:pointer;transition:background .2s ease}.sv-detail-view__back-button:hover{background:var(--sv-detail-view-back-hover-bg,rgba(255,255,255,.2))}.sv-detail-view__back-button svg{width:24px;height:24px}.sv-detail-view__title{font-size:var(--sv-detail-view-title-font-size,16px);font-weight:600;color:var(--sv-detail-view-title-color,#fff)}.sv-detail-view__actions{display:flex;gap:8px}.sv-detail-view__drag-indicator{position:absolute;top:8px;left:50%;transform:translateX(-50%);width:36px;height:5px;background:var(--sv-detail-view-drag-indicator-bg,rgba(255,255,255,.3));border-radius:3px;z-index:20}.sv-detail-view__scroll{flex:1;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain;touch-action:pan-y;padding-bottom:70px}.sv-detail-view__content{min-height:100%}.sv-detail-view__gallery{position:relative;width:100%;aspect-ratio:1;background:var(--sv-detail-view-gallery-bg,#111);overflow:hidden}.sv-detail-view__gallery-image{width:100%;height:100%;object-fit:contain}.sv-detail-view__gallery-pagination{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:6px;padding:6px 12px;background:rgba(0,0,0,.5);border-radius:20px}.sv-detail-view__gallery-dot{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.5);border:0;padding:0;cursor:pointer;transition:all .2s ease}.sv-detail-view__gallery-dot--active{background:#fff;transform:scale(1.2)}.sv-detail-view__author{display:flex;align-items:center;gap:12px;padding:16px 0;border-bottom:1px solid var(--sv-detail-view-border-color,rgba(255,255,255,.1))}.sv-detail-view__author-avatar{width:44px;height:44px;border-radius:50%;object-fit:cover}.sv-detail-view__author-info{flex:1}.sv-detail-view__author-name{font-size:15px;font-weight:600;color:var(--sv-detail-view-text-color,#fff)}.sv-detail-view__author-date{font-size:13px;color:var(--sv-detail-view-secondary-color,rgba(255,255,255,.6));margin-top:2px}.sv-detail-view__follow-button{padding:8px 20px;background:var(--sv-detail-view-follow-bg,#fe2c55);border:0;border-radius:4px;font-size:14px;font-weight:600;color:#fff;cursor:pointer;transition:opacity .2s ease}.sv-detail-view__follow-button:hover{opacity:.9}.sv-detail-view__follow-button--following{background:var(--sv-detail-view-following-bg,rgba(255,255,255,.1));color:var(--sv-detail-view-following-color,#fff)}.sv-detail-view__caption{padding:16px;font-size:var(--sv-detail-view-caption-font-size,15px);line-height:1.5;color:var(--sv-detail-view-text-color,#fff);white-space:pre-wrap;word-break:break-word}.sv-detail-view__hashtag{color:var(--sv-detail-view-hashtag-color,#4dabff);cursor:pointer}.sv-detail-view__hashtag:hover{text-decoration:underline}.sv-detail-view__stats{display:flex;gap:24px;padding:16px 0;border-top:1px solid var(--sv-detail-view-border-color,rgba(255,255,255,.1))}.sv-detail-view__stat{display:flex;align-items:center;gap:6px;font-size:14px;color:var(--sv-detail-view-secondary-color,rgba(255,255,255,.6))}.sv-detail-view__stat-value{font-weight:600;color:var(--sv-detail-view-text-color,#fff)}.sv-detail-view__action-bar{position:sticky;bottom:0;display:flex;justify-content:space-around;padding:12px 16px;background:var(--sv-detail-view-action-bar-bg,rgba(0,0,0,.9));backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-top:1px solid var(--sv-detail-view-border-color,rgba(255,255,255,.1))}.sv-detail-view__action{display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 16px;border:0;background:transparent;color:var(--sv-detail-view-action-color,#fff);cursor:pointer;transition:opacity .2s ease}.sv-detail-view__action:hover{opacity:.8}.sv-detail-view__action--active{color:var(--sv-detail-view-action-active-color,#fe2c55)}.sv-detail-view__action svg{width:24px;height:24px}.sv-detail-view__action-label{font-size:12px}.sv-detail-view__music{display:flex;align-items:center;gap:12px;padding:12px;margin:0 16px 16px;background:var(--sv-detail-view-music-bg,rgba(255,255,255,.05));border-radius:8px;border:0;width:calc(100% - 32px);cursor:pointer;text-align:left}.sv-detail-view__music-cover{width:48px;height:48px;border-radius:4px;object-fit:cover}.sv-detail-view__music-info{flex:1;overflow:hidden}.sv-detail-view__music-title{font-size:14px;font-weight:500;color:var(--sv-detail-view-text-color,#fff);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sv-detail-view__music-artist{font-size:12px;color:var(--sv-detail-view-secondary-color,rgba(255,255,255,.6));margin-top:2px}.sv-detail-view__music-icon{width:24px;height:24px;color:var(--sv-detail-view-secondary-color,rgba(255,255,255,.6))}.sv-detail-view__header-author{display:flex;align-items:center;gap:10px;flex:1;min-width:0;margin-left:12px}.sv-detail-view__header-avatar{width:32px;height:32px;border-radius:50%;object-fit:cover;flex-shrink:0}.sv-detail-view__header-name{font-size:15px;font-weight:600;color:var(--sv-detail-view-text-color,#fff);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sv-detail-view__follow-btn{padding:6px 16px;font-size:13px;font-weight:600;border:0;border-radius:4px;background:var(--sv-detail-view-follow-bg,#fe2c55);color:#fff;cursor:pointer;transition:background .2s ease,opacity .2s ease;flex-shrink:0}.sv-detail-view__follow-btn:hover{opacity:.9}.sv-detail-view__follow-btn--following{background:var(--sv-detail-view-following-bg,rgba(255,255,255,.12));color:var(--sv-detail-view-text-color,#fff)}.sv-detail-view__bottom-bar{position:absolute;bottom:0;left:0;right:0;display:flex;align-items:center;gap:8px;padding:10px 12px;padding-bottom:max(10px,env(safe-area-inset-bottom));background:var(--sv-detail-view-bottom-bar-bg,rgba(0,0,0,.95));border-top:1px solid var(--sv-detail-view-border-color,rgba(255,255,255,.1));z-index:20}.sv-detail-view__comment-input{flex:1;display:flex;align-items:center;background:var(--sv-detail-view-input-bg,rgba(255,255,255,.08));border-radius:18px;min-width:0;overflow:hidden}.sv-detail-view__bottom-bar .sv-detail-view__comment-input input,.sv-detail-view__comment-field,input.sv-detail-view__comment-field{flex:1;width:100%;height:auto;padding:10px 14px;margin:0;border:0;border-radius:0;background:transparent;background-color:transparent;font-family:inherit;font-size:14px;line-height:1.4;color:var(--sv-detail-view-text-color,#fff);outline:none;box-shadow:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.sv-detail-view__bottom-bar .sv-detail-view__comment-input input::placeholder,.sv-detail-view__comment-field::placeholder,input.sv-detail-view__comment-field::placeholder{color:var(--sv-detail-view-placeholder-color,rgba(255,255,255,.5));opacity:1}.sv-detail-view__bottom-bar .sv-detail-view__comment-input input:focus,.sv-detail-view__comment-field:focus,input.sv-detail-view__comment-field:focus{outline:none;box-shadow:none;border:0}.sv-detail-view__bottom-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}.sv-detail-view__action-btn{display:flex;flex-direction:column;align-items:center;justify-content:center;min-width:40px;padding:4px 6px;background:0 0;border:0;color:var(--sv-detail-view-text-color,#fff);cursor:pointer;transition:transform .15s ease,color .15s ease}.sv-detail-view__action-btn:hover{transform:scale(1.05)}.sv-detail-view__action-btn:active{transform:scale(.95)}.sv-detail-view__action-btn--active{color:var(--sv-detail-view-active-color,#fe2c55)}.sv-detail-view__action-btn svg{width:22px;height:22px}.sv-detail-view__action-count{font-size:11px;margin-top:2px;color:var(--sv-detail-view-secondary-color,rgba(255,255,255,.7))}.sv-detail-view__action-btn--active .sv-detail-view__action-count{color:var(--sv-detail-view-active-color,#fe2c55)}.sv-detail-view:focus-visible{outline:2px solid var(--sv-detail-view-focus-color,#4dabff);outline-offset:-2px}.sv-detail-view__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}`;
9
+ var cssInjected = false;
10
+ function injectDetailViewCSS() {
11
+ if (cssInjected) return void 0;
12
+ cssInjected = true;
13
+ return injectComponentCSS("detail-view", DETAIL_VIEW_CSS);
14
+ }
15
+ var DetailViewContext = createContext(null);
16
+ function useDetailViewContext() {
17
+ const context = useContext(DetailViewContext);
18
+ if (!context) {
19
+ throw new Error("useDetailViewContext must be used within DetailViewHeadless");
20
+ }
21
+ return context;
22
+ }
23
+ function BackIcon() {
24
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M19 12H5M12 19l-7-7 7-7" }) });
25
+ }
26
+ function HeartIcon({ filled = false }) {
27
+ return filled ? /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "#fe2c55", children: /* @__PURE__ */ jsx("path", { d: "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" }) }) : /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" }) });
28
+ }
29
+ function CommentIcon() {
30
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" }) });
31
+ }
32
+ function BookmarkIcon({ filled = false }) {
33
+ return filled ? /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z" }) }) : /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" }) });
34
+ }
35
+ function ShareIcon() {
36
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13" }) });
37
+ }
38
+ var DEFAULT_ANIMATION_DURATION = 300;
39
+ var DEFAULT_SWIPE_THRESHOLD = 0.3;
40
+ function formatCount(count) {
41
+ if (count >= 1e6) {
42
+ return `${(count / 1e6).toFixed(1)}M`;
43
+ }
44
+ if (count >= 1e3) {
45
+ return `${(count / 1e3).toFixed(1)}K`;
46
+ }
47
+ return count.toString();
48
+ }
49
+ function DetailViewHeadlessBase({
50
+ isOpen,
51
+ onClose,
52
+ imagePost,
53
+ initialImageIndex = 0,
54
+ animationDuration = DEFAULT_ANIMATION_DURATION,
55
+ enableSwipeToClose = true,
56
+ swipeCloseThreshold = DEFAULT_SWIPE_THRESHOLD,
57
+ headerTitle = "Post",
58
+ showDragIndicator = false,
59
+ // Action states
60
+ isLiked = false,
61
+ isBookmarked = false,
62
+ isFollowing = false,
63
+ // Action callbacks
64
+ onLike,
65
+ onComment,
66
+ onShare,
67
+ onBookmark,
68
+ onFollow,
69
+ // Bottom bar customization
70
+ renderBottomBar,
71
+ hideBottomBar = false,
72
+ className,
73
+ children
74
+ }) {
75
+ useInsertionEffect(() => {
76
+ return injectDetailViewCSS();
77
+ }, []);
78
+ const [isVisible, setIsVisible] = useState(false);
79
+ const [isAnimating, setIsAnimating] = useState(false);
80
+ const [viewState, setViewState] = useState("entering");
81
+ const [currentImageIndex, setCurrentImageIndex] = useState(initialImageIndex);
82
+ const [dragOffset, setDragOffset] = useState(0);
83
+ const [isDragging, setIsDragging] = useState(false);
84
+ const dragStartRef = useRef(null);
85
+ const containerRef = useRef(null);
86
+ useEffect(() => {
87
+ if (isOpen && !isVisible) {
88
+ setIsVisible(true);
89
+ setIsAnimating(true);
90
+ setViewState("entering");
91
+ setCurrentImageIndex(initialImageIndex);
92
+ const timer = setTimeout(() => {
93
+ setViewState("visible");
94
+ setIsAnimating(false);
95
+ }, animationDuration);
96
+ return () => clearTimeout(timer);
97
+ }
98
+ if (!isOpen && isVisible) {
99
+ setIsAnimating(true);
100
+ setViewState("exiting");
101
+ const timer = setTimeout(() => {
102
+ setIsVisible(false);
103
+ setIsAnimating(false);
104
+ setViewState("entering");
105
+ setDragOffset(0);
106
+ }, animationDuration);
107
+ return () => clearTimeout(timer);
108
+ }
109
+ return void 0;
110
+ }, [isOpen, isVisible, animationDuration, initialImageIndex]);
111
+ useEffect(() => {
112
+ if (isVisible) {
113
+ const originalOverflow = document.body.style.overflow;
114
+ document.body.style.overflow = "hidden";
115
+ return () => {
116
+ document.body.style.overflow = originalOverflow;
117
+ };
118
+ }
119
+ return void 0;
120
+ }, [isVisible]);
121
+ const handleClose = useCallback(() => {
122
+ onClose();
123
+ }, [onClose]);
124
+ const handlePointerDown = useCallback(
125
+ (e) => {
126
+ e.stopPropagation();
127
+ if (!enableSwipeToClose) return;
128
+ const target = e.target;
129
+ const isInteractive = target.tagName === "BUTTON" || target.tagName === "A" || target.tagName === "INPUT" || target.closest("button") || target.closest("a") || target.closest('[role="button"]');
130
+ if (isInteractive) return;
131
+ dragStartRef.current = {
132
+ y: e.clientY,
133
+ time: Date.now()
134
+ };
135
+ setIsDragging(true);
136
+ },
137
+ [enableSwipeToClose]
138
+ );
139
+ const handlePointerMove = useCallback(
140
+ (e) => {
141
+ e.stopPropagation();
142
+ if (!isDragging || !dragStartRef.current) return;
143
+ const dy = e.clientY - dragStartRef.current.y;
144
+ if (dy > 0) {
145
+ setDragOffset(dy);
146
+ }
147
+ },
148
+ [isDragging]
149
+ );
150
+ const handlePointerUp = useCallback(
151
+ (e) => {
152
+ e.stopPropagation();
153
+ if (!isDragging || !dragStartRef.current) return;
154
+ const container = containerRef.current;
155
+ if (!container) {
156
+ setIsDragging(false);
157
+ setDragOffset(0);
158
+ dragStartRef.current = null;
159
+ return;
160
+ }
161
+ const containerHeight = container.offsetHeight;
162
+ const threshold = containerHeight * swipeCloseThreshold;
163
+ const velocity = dragOffset / (Date.now() - dragStartRef.current.time);
164
+ if (dragOffset > threshold || velocity > 0.5) {
165
+ handleClose();
166
+ }
167
+ setIsDragging(false);
168
+ setDragOffset(0);
169
+ dragStartRef.current = null;
170
+ },
171
+ [isDragging, dragOffset, swipeCloseThreshold, handleClose]
172
+ );
173
+ const stopEventPropagation = useCallback((e) => {
174
+ e.stopPropagation();
175
+ }, []);
176
+ useEffect(() => {
177
+ if (!isVisible) return;
178
+ const handleKeyDown = (e) => {
179
+ if (e.key === "Escape") {
180
+ handleClose();
181
+ }
182
+ };
183
+ window.addEventListener("keydown", handleKeyDown);
184
+ return () => window.removeEventListener("keydown", handleKeyDown);
185
+ }, [isVisible, handleClose]);
186
+ const contextValue = useMemo(
187
+ () => ({
188
+ imagePost,
189
+ currentImageIndex,
190
+ setCurrentImageIndex,
191
+ close: handleClose,
192
+ isAnimating,
193
+ actions: {
194
+ onLike,
195
+ onComment,
196
+ onShare,
197
+ onBookmark,
198
+ onFollow
199
+ },
200
+ states: {
201
+ isLiked,
202
+ isBookmarked,
203
+ isFollowing
204
+ }
205
+ }),
206
+ [
207
+ imagePost,
208
+ currentImageIndex,
209
+ handleClose,
210
+ isAnimating,
211
+ onLike,
212
+ onComment,
213
+ onShare,
214
+ onBookmark,
215
+ onFollow,
216
+ isLiked,
217
+ isBookmarked,
218
+ isFollowing
219
+ ]
220
+ );
221
+ if (!isVisible) return null;
222
+ const containerStyle = isDragging ? {
223
+ transform: `translateY(${dragOffset}px)`,
224
+ opacity: 1 - dragOffset / (containerRef.current?.offsetHeight ?? 1) / 2
225
+ } : void 0;
226
+ return /* @__PURE__ */ jsx(DetailViewContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs(
227
+ "div",
228
+ {
229
+ ref: containerRef,
230
+ className: clsx2(
231
+ "sv-detail-view",
232
+ viewState === "entering" && "sv-detail-view--entering",
233
+ viewState === "exiting" && "sv-detail-view--exiting",
234
+ isDragging && "sv-detail-view--dragging",
235
+ className
236
+ ),
237
+ "aria-modal": "true",
238
+ "aria-label": `Detail view for post by ${imagePost.author?.name ?? "Unknown"}`,
239
+ onClick: stopEventPropagation,
240
+ onPointerDown: stopEventPropagation,
241
+ onPointerMove: stopEventPropagation,
242
+ onPointerUp: stopEventPropagation,
243
+ onPointerCancel: stopEventPropagation,
244
+ onTouchStart: stopEventPropagation,
245
+ onTouchMove: stopEventPropagation,
246
+ onTouchEnd: stopEventPropagation,
247
+ onTouchCancel: stopEventPropagation,
248
+ children: [
249
+ /* @__PURE__ */ jsx("div", { className: "sv-detail-view__backdrop", onClick: handleClose }),
250
+ /* @__PURE__ */ jsxs(
251
+ "div",
252
+ {
253
+ className: "sv-detail-view__container",
254
+ style: containerStyle,
255
+ onPointerDown: handlePointerDown,
256
+ onPointerMove: handlePointerMove,
257
+ onPointerUp: handlePointerUp,
258
+ onPointerLeave: handlePointerUp,
259
+ children: [
260
+ showDragIndicator && /* @__PURE__ */ jsx("div", { className: "sv-detail-view__drag-indicator" }),
261
+ /* @__PURE__ */ jsxs("header", { className: "sv-detail-view__header", children: [
262
+ /* @__PURE__ */ jsx(
263
+ "button",
264
+ {
265
+ type: "button",
266
+ className: "sv-detail-view__back-button",
267
+ onClick: (e) => {
268
+ e.stopPropagation();
269
+ handleClose();
270
+ },
271
+ onPointerDown: (e) => e.stopPropagation(),
272
+ "aria-label": "Back",
273
+ children: /* @__PURE__ */ jsx(BackIcon, {})
274
+ }
275
+ ),
276
+ /* @__PURE__ */ jsxs("div", { className: "sv-detail-view__header-author", children: [
277
+ imagePost.author?.avatar && /* @__PURE__ */ jsx(
278
+ "img",
279
+ {
280
+ src: imagePost.author.avatar,
281
+ alt: imagePost.author.name,
282
+ className: "sv-detail-view__header-avatar"
283
+ }
284
+ ),
285
+ /* @__PURE__ */ jsx("span", { className: "sv-detail-view__header-name", children: imagePost.author?.name ?? headerTitle })
286
+ ] })
287
+ ] }),
288
+ /* @__PURE__ */ jsx("div", { className: "sv-detail-view__scroll", children: /* @__PURE__ */ jsx("div", { className: "sv-detail-view__content", children }) }),
289
+ !hideBottomBar && (renderBottomBar ? renderBottomBar() : /* @__PURE__ */ jsx(
290
+ DetailViewDefaultBottomBar,
291
+ {
292
+ imagePost,
293
+ isLiked,
294
+ isBookmarked,
295
+ onLike,
296
+ onComment,
297
+ onShare,
298
+ onBookmark
299
+ }
300
+ ))
301
+ ]
302
+ }
303
+ )
304
+ ]
305
+ }
306
+ ) });
307
+ }
308
+ var DetailViewHeadless = memo(DetailViewHeadlessBase);
309
+ DetailViewHeadless.displayName = "DetailViewHeadless";
310
+ function DetailViewGallery({
311
+ enableZoom = true,
312
+ className
313
+ }) {
314
+ const { imagePost, currentImageIndex, setCurrentImageIndex } = useDetailViewContext();
315
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-detail-view__gallery", className), children: /* @__PURE__ */ jsx(
316
+ ImageCarouselHeadless,
317
+ {
318
+ images: imagePost.images,
319
+ currentIndex: currentImageIndex,
320
+ onIndexChange: setCurrentImageIndex,
321
+ showIndicators: imagePost.images.length > 1,
322
+ indicatorStyle: "dots",
323
+ enableZoom,
324
+ lazyLoad: false
325
+ }
326
+ ) });
327
+ }
328
+ function DetailViewAuthor({
329
+ isFollowing = false,
330
+ onFollowClick,
331
+ showFollowButton = true,
332
+ formatDate = (date) => new Date(date).toLocaleDateString(),
333
+ followLabel = "Follow",
334
+ followingLabel = "Following",
335
+ className
336
+ }) {
337
+ const { imagePost } = useDetailViewContext();
338
+ const author = imagePost.author;
339
+ if (!author) return null;
340
+ return /* @__PURE__ */ jsxs("div", { className: clsx2("sv-detail-view__author", className), children: [
341
+ author.avatar && /* @__PURE__ */ jsx("img", { src: author.avatar, alt: author.name, className: "sv-detail-view__author-avatar" }),
342
+ /* @__PURE__ */ jsxs("div", { className: "sv-detail-view__author-info", children: [
343
+ /* @__PURE__ */ jsx("div", { className: "sv-detail-view__author-name", children: author.name }),
344
+ imagePost.createdAt && /* @__PURE__ */ jsx("div", { className: "sv-detail-view__author-date", children: formatDate(imagePost.createdAt) })
345
+ ] }),
346
+ showFollowButton && /* @__PURE__ */ jsx(
347
+ "button",
348
+ {
349
+ type: "button",
350
+ className: clsx2(
351
+ "sv-detail-view__follow-button",
352
+ isFollowing && "sv-detail-view__follow-button--following"
353
+ ),
354
+ onClick: onFollowClick,
355
+ children: isFollowing ? followingLabel : followLabel
356
+ }
357
+ )
358
+ ] });
359
+ }
360
+ function DetailViewCaption({
361
+ renderHashtag,
362
+ className
363
+ }) {
364
+ const { imagePost } = useDetailViewContext();
365
+ if (!imagePost.caption) return null;
366
+ const parts = imagePost.caption.split(/(#\w+)/g);
367
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-detail-view__caption", className), children: parts.map((part, index) => {
368
+ if (part.startsWith("#")) {
369
+ return renderHashtag ? renderHashtag(part) : (
370
+ // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
371
+ /* @__PURE__ */ jsx("span", { className: "sv-detail-view__hashtag", children: part }, index)
372
+ );
373
+ }
374
+ return part;
375
+ }) });
376
+ }
377
+ function DetailViewStats({
378
+ formatNumber = (n) => n.toLocaleString(),
379
+ labels = {},
380
+ className
381
+ }) {
382
+ const { imagePost } = useDetailViewContext();
383
+ const stats = imagePost.stats;
384
+ const { likes = "likes", comments = "comments", shares = "shares" } = labels;
385
+ return /* @__PURE__ */ jsxs("div", { className: clsx2("sv-detail-view__stats", className), children: [
386
+ stats.likes !== void 0 && /* @__PURE__ */ jsxs("div", { className: "sv-detail-view__stat", children: [
387
+ /* @__PURE__ */ jsx("span", { className: "sv-detail-view__stat-value", children: formatNumber(stats.likes) }),
388
+ /* @__PURE__ */ jsx("span", { children: likes })
389
+ ] }),
390
+ stats.comments !== void 0 && /* @__PURE__ */ jsxs("div", { className: "sv-detail-view__stat", children: [
391
+ /* @__PURE__ */ jsx("span", { className: "sv-detail-view__stat-value", children: formatNumber(stats.comments) }),
392
+ /* @__PURE__ */ jsx("span", { children: comments })
393
+ ] }),
394
+ stats.shares !== void 0 && /* @__PURE__ */ jsxs("div", { className: "sv-detail-view__stat", children: [
395
+ /* @__PURE__ */ jsx("span", { className: "sv-detail-view__stat-value", children: formatNumber(stats.shares) }),
396
+ /* @__PURE__ */ jsx("span", { children: shares })
397
+ ] })
398
+ ] });
399
+ }
400
+ function DetailViewMusic({
401
+ music,
402
+ isPlaying = false,
403
+ onClick,
404
+ className
405
+ }) {
406
+ if (!music) return null;
407
+ return /* @__PURE__ */ jsxs("button", { type: "button", className: clsx2("sv-detail-view__music", className), onClick, children: [
408
+ music.cover && /* @__PURE__ */ jsx("img", { src: music.cover, alt: music.title, className: "sv-detail-view__music-cover" }),
409
+ /* @__PURE__ */ jsxs("div", { className: "sv-detail-view__music-info", children: [
410
+ /* @__PURE__ */ jsx("div", { className: "sv-detail-view__music-title", children: music.title }),
411
+ /* @__PURE__ */ jsx("div", { className: "sv-detail-view__music-artist", children: music.artist })
412
+ ] }),
413
+ /* @__PURE__ */ jsx(
414
+ "svg",
415
+ {
416
+ "aria-hidden": "true",
417
+ className: "sv-detail-view__music-icon",
418
+ viewBox: "0 0 24 24",
419
+ fill: "currentColor",
420
+ children: isPlaying ? /* @__PURE__ */ jsxs(Fragment, { children: [
421
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "4", width: "4", height: "16" }),
422
+ /* @__PURE__ */ jsx("rect", { x: "14", y: "4", width: "4", height: "16" })
423
+ ] }) : /* @__PURE__ */ jsx("path", { d: "M8 5v14l11-7z" })
424
+ }
425
+ )
426
+ ] });
427
+ }
428
+ function DetailViewDefaultBottomBar({
429
+ imagePost,
430
+ isLiked,
431
+ isBookmarked,
432
+ onLike,
433
+ onComment,
434
+ onShare,
435
+ onBookmark
436
+ }) {
437
+ return /* @__PURE__ */ jsxs(DetailViewBottomBar, { children: [
438
+ /* @__PURE__ */ jsx(DetailViewCommentInput, { placeholder: "Add a comment..." }),
439
+ /* @__PURE__ */ jsxs(DetailViewActions, { children: [
440
+ /* @__PURE__ */ jsx(
441
+ DetailViewActionButton,
442
+ {
443
+ type: "like",
444
+ isActive: isLiked,
445
+ count: imagePost.stats.likes,
446
+ onClick: onLike
447
+ }
448
+ ),
449
+ /* @__PURE__ */ jsx(
450
+ DetailViewActionButton,
451
+ {
452
+ type: "comment",
453
+ count: imagePost.stats.comments,
454
+ onClick: onComment
455
+ }
456
+ ),
457
+ /* @__PURE__ */ jsx(DetailViewActionButton, { type: "bookmark", isActive: isBookmarked, onClick: onBookmark }),
458
+ /* @__PURE__ */ jsx(DetailViewActionButton, { type: "share", count: imagePost.stats.shares, onClick: onShare })
459
+ ] })
460
+ ] });
461
+ }
462
+ function DetailViewBottomBar({
463
+ children,
464
+ className
465
+ }) {
466
+ return /* @__PURE__ */ jsx(
467
+ "footer",
468
+ {
469
+ className: clsx2("sv-detail-view__bottom-bar", className),
470
+ onPointerDown: (e) => e.stopPropagation(),
471
+ children
472
+ }
473
+ );
474
+ }
475
+ function DetailViewCommentInput({
476
+ placeholder = "Add a comment...",
477
+ onSubmit,
478
+ className
479
+ }) {
480
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-detail-view__comment-input", className), children: /* @__PURE__ */ jsx(
481
+ "input",
482
+ {
483
+ type: "text",
484
+ className: "sv-detail-view__comment-field",
485
+ placeholder,
486
+ onKeyDown: (e) => {
487
+ e.stopPropagation();
488
+ if (e.key === "Enter" && e.currentTarget.value.trim()) {
489
+ onSubmit?.(e.currentTarget.value.trim());
490
+ e.currentTarget.value = "";
491
+ }
492
+ },
493
+ onClick: (e) => e.stopPropagation()
494
+ }
495
+ ) });
496
+ }
497
+ function DetailViewActions({ children, className }) {
498
+ return /* @__PURE__ */ jsx("div", { className: clsx2("sv-detail-view__bottom-actions", className), children });
499
+ }
500
+ function DetailViewActionButton({
501
+ type,
502
+ icon,
503
+ isActive = false,
504
+ count,
505
+ onClick,
506
+ ariaLabel,
507
+ className,
508
+ children
509
+ }) {
510
+ const renderIcon = () => {
511
+ if (icon) return icon;
512
+ switch (type) {
513
+ case "like":
514
+ return /* @__PURE__ */ jsx(HeartIcon, { filled: isActive });
515
+ case "comment":
516
+ return /* @__PURE__ */ jsx(CommentIcon, {});
517
+ case "bookmark":
518
+ return /* @__PURE__ */ jsx(BookmarkIcon, { filled: isActive });
519
+ case "share":
520
+ return /* @__PURE__ */ jsx(ShareIcon, {});
521
+ default:
522
+ return null;
523
+ }
524
+ };
525
+ const getAriaLabel = () => {
526
+ if (ariaLabel) return ariaLabel;
527
+ switch (type) {
528
+ case "like":
529
+ return isActive ? "Unlike" : "Like";
530
+ case "comment":
531
+ return "Comment";
532
+ case "bookmark":
533
+ return isActive ? "Remove bookmark" : "Bookmark";
534
+ case "share":
535
+ return "Share";
536
+ default:
537
+ return void 0;
538
+ }
539
+ };
540
+ return /* @__PURE__ */ jsxs(
541
+ "button",
542
+ {
543
+ type: "button",
544
+ className: clsx2(
545
+ "sv-detail-view__action-btn",
546
+ isActive && "sv-detail-view__action-btn--active",
547
+ className
548
+ ),
549
+ onClick: (e) => {
550
+ e.stopPropagation();
551
+ onClick?.();
552
+ },
553
+ "aria-label": getAriaLabel(),
554
+ children: [
555
+ renderIcon(),
556
+ count !== void 0 && /* @__PURE__ */ jsx("span", { className: "sv-detail-view__action-count", children: formatCount(count) }),
557
+ children
558
+ ]
559
+ }
560
+ );
561
+ }
562
+ var DetailViewIcons = {
563
+ Back: BackIcon,
564
+ Heart: HeartIcon,
565
+ Comment: CommentIcon,
566
+ Bookmark: BookmarkIcon,
567
+ Share: ShareIcon
568
+ };
569
+
570
+ export { DETAIL_VIEW_CSS, DetailViewActionButton, DetailViewActions, DetailViewAuthor, DetailViewBottomBar, DetailViewCaption, DetailViewCommentInput, DetailViewGallery, DetailViewHeadless, DetailViewIcons, DetailViewMusic, DetailViewStats, formatCount, useDetailViewContext };
@@ -0,0 +1,91 @@
1
+ import { injectComponentCSS } from './chunk-CAWE42LH.js';
2
+ import { useInsertionEffect, Component } from 'react';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+
5
+ // src/components/ErrorBoundary/ErrorBoundary.css.ts
6
+ var ERROR_BOUNDARY_CSS = `.sv-error-boundary{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--sv-bg-primary,#000);color:var(--sv-text-primary,#fff);font-family:var(--sv-font-family,'Urbanist',sans-serif)}.sv-error-boundary--error .sv-error-boundary__content{display:flex;flex-direction:column;align-items:center;gap:var(--sv-spacing-md,16px);padding:var(--sv-spacing-lg,24px);text-align:center}.sv-error-boundary__icon{font-size:48px}.sv-error-boundary__message{font-size:var(--sv-font-size-md,14px);color:var(--sv-text-secondary,#999)}.sv-error-boundary__retry{padding:var(--sv-spacing-sm,8px)var(--sv-spacing-lg,24px);border:0;border-radius:var(--sv-border-radius-md,8px);background:var(--sv-color-primary,#fe2c55);color:#fff;font-size:var(--sv-font-size-sm,13px);font-weight:var(--sv-font-weight-medium,500);cursor:pointer;transition:opacity .2s ease}.sv-error-boundary__retry:hover{opacity:.9}.sv-error-boundary__retry:active{opacity:.8}.sv-error-boundary--toast{position:relative}.sv-error-boundary__toast{position:absolute;bottom:var(--sv-spacing-xl,32px);left:50%;transform:translateX(-50%);padding:var(--sv-spacing-sm,8px)var(--sv-spacing-md,16px);border-radius:var(--sv-border-radius-md,8px);background:rgba(0,0,0,.8);color:#fff;font-size:var(--sv-font-size-sm,13px);white-space:nowrap;animation:sv-toast-fade-in .3s ease}@keyframes sv-toast-fade-in{from{opacity:0;transform:translateX(-50%)translateY(10px)}to{opacity:1;transform:translateX(-50%)translateY(0)}}.sv-error-boundary--loading{background:var(--sv-bg-primary,#000)}.sv-error-boundary__spinner{width:32px;height:32px;border:3px solid rgba(255,255,255,.2);border-top-color:var(--sv-color-primary,#fe2c55);border-radius:50%;animation:sv-spinner .8s linear infinite}@keyframes sv-spinner{to{transform:rotate(360deg)}}`;
7
+ var ErrorBoundaryInternal = class extends Component {
8
+ constructor(props) {
9
+ super(props);
10
+ this.skipTimeoutId = null;
11
+ this.reset = () => {
12
+ const { onReset } = this.props;
13
+ this.clearSkipTimeout();
14
+ this.setState({ hasError: false, error: null, errorInfo: null });
15
+ onReset?.();
16
+ };
17
+ this.state = { hasError: false, error: null, errorInfo: null };
18
+ }
19
+ static getDerivedStateFromError(error) {
20
+ return { hasError: true, error };
21
+ }
22
+ componentDidCatch(error, errorInfo) {
23
+ const { onError, mode = "show-error", skipDelay = 1e3 } = this.props;
24
+ this.setState({ errorInfo });
25
+ onError?.(error, errorInfo);
26
+ if (!this.skipTimeoutId && (mode === "auto-skip" || mode === "auto-skip-with-toast")) {
27
+ this.skipTimeoutId = setTimeout(() => {
28
+ this.handleSkip();
29
+ }, skipDelay);
30
+ }
31
+ }
32
+ componentWillUnmount() {
33
+ this.clearSkipTimeout();
34
+ }
35
+ /**
36
+ * Handle skip action with error safeguard
37
+ * Wraps onSkip in try/finally to ensure reset() is always called
38
+ */
39
+ handleSkip() {
40
+ const { onSkip } = this.props;
41
+ try {
42
+ onSkip?.();
43
+ } finally {
44
+ this.reset();
45
+ }
46
+ }
47
+ clearSkipTimeout() {
48
+ if (this.skipTimeoutId) {
49
+ clearTimeout(this.skipTimeoutId);
50
+ this.skipTimeoutId = null;
51
+ }
52
+ }
53
+ render() {
54
+ const {
55
+ children,
56
+ fallback,
57
+ mode = "show-error",
58
+ toastMessage = "Video unavailable, skipping..."
59
+ } = this.props;
60
+ const { hasError, error, errorInfo } = this.state;
61
+ if (!hasError) {
62
+ return children;
63
+ }
64
+ if (fallback) {
65
+ if (typeof fallback === "function") {
66
+ return fallback(error ?? new Error("Unknown error"), errorInfo, this.reset);
67
+ }
68
+ return fallback;
69
+ }
70
+ if (mode === "auto-skip-with-toast") {
71
+ return /* @__PURE__ */ jsx("div", { className: "sv-error-boundary sv-error-boundary--toast", children: /* @__PURE__ */ jsx("div", { className: "sv-error-boundary__toast", children: toastMessage }) });
72
+ }
73
+ if (mode === "auto-skip") {
74
+ return /* @__PURE__ */ jsx("div", { className: "sv-error-boundary sv-error-boundary--loading" });
75
+ }
76
+ return /* @__PURE__ */ jsx("div", { className: "sv-error-boundary sv-error-boundary--error", children: /* @__PURE__ */ jsxs("div", { className: "sv-error-boundary__content", children: [
77
+ /* @__PURE__ */ jsx("div", { className: "sv-error-boundary__icon", children: "\u26A0\uFE0F" }),
78
+ /* @__PURE__ */ jsx("div", { className: "sv-error-boundary__message", children: "Something went wrong" }),
79
+ /* @__PURE__ */ jsx("button", { type: "button", className: "sv-error-boundary__retry", onClick: this.reset, children: "Try Again" })
80
+ ] }) });
81
+ }
82
+ };
83
+ function ErrorBoundary(props) {
84
+ const { cssKey = "sv-error-boundary" } = props;
85
+ useInsertionEffect(() => {
86
+ return injectComponentCSS(cssKey, ERROR_BOUNDARY_CSS);
87
+ }, [cssKey]);
88
+ return /* @__PURE__ */ jsx(ErrorBoundaryInternal, { ...props });
89
+ }
90
+
91
+ export { ErrorBoundary };