@xhub-short/ui 0.1.0-beta.8 → 1.0.0-beta.18

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 (75) hide show
  1. package/dist/CommentSheet.css-DuBy01rU.d.ts +219 -0
  2. package/dist/VideoSlotPlayIndicator-DPs8Xt5C.d.ts +51 -0
  3. package/dist/chunk-3OB3OVYR.js +349 -0
  4. package/dist/chunk-4RIMQOBR.js +58 -0
  5. package/dist/chunk-5MKYDI4X.js +1 -0
  6. package/dist/chunk-5Y43HNRG.js +296 -0
  7. package/dist/chunk-7WXAQHJI.js +350 -0
  8. package/dist/chunk-BADA7OLG.js +564 -0
  9. package/dist/chunk-BEJAJFV6.js +191 -0
  10. package/dist/chunk-BNI7CYRI.js +334 -0
  11. package/dist/{chunk-UPCVSGWH.js → chunk-CAWE42LH.js} +5 -2
  12. package/dist/chunk-DGKMO3AE.js +717 -0
  13. package/dist/chunk-DR7KR7OT.js +103 -0
  14. package/dist/chunk-GSNIZ6DF.js +605 -0
  15. package/dist/chunk-HXQPEZRG.js +105 -0
  16. package/dist/chunk-IC2KUU4V.js +1264 -0
  17. package/dist/chunk-IWSBYOSS.js +91 -0
  18. package/dist/chunk-MFJS65C5.js +368 -0
  19. package/dist/{chunk-CXPNPSF7.js → chunk-NJXIYSDZ.js} +12 -1
  20. package/dist/{chunk-NRQXKZO3.js → chunk-OM4L7RE5.js} +12 -2
  21. package/dist/chunk-QCRRF76W.js +75 -0
  22. package/dist/chunk-QUEJHA24.js +508 -0
  23. package/dist/chunk-UYBQTE4M.js +337 -0
  24. package/dist/chunk-VJ744W5N.js +603 -0
  25. package/dist/chunk-VXW7AOGM.js +285 -0
  26. package/dist/{chunk-OQ7P5XC7.js → chunk-YB7AXTX7.js} +1 -1
  27. package/dist/components/ActionBar/index.d.ts +7 -2
  28. package/dist/components/ActionBar/index.js +1 -1
  29. package/dist/components/AdvanceMenu/index.d.ts +80 -0
  30. package/dist/components/AdvanceMenu/index.js +1 -0
  31. package/dist/components/ArticleSlot/index.d.ts +213 -0
  32. package/dist/components/ArticleSlot/index.js +1 -0
  33. package/dist/components/AuthorInfo/index.js +1 -1
  34. package/dist/components/BottomSheet/index.d.ts +87 -0
  35. package/dist/components/BottomSheet/index.js +1 -0
  36. package/dist/components/CleanModeOverlay/index.d.ts +60 -0
  37. package/dist/components/CleanModeOverlay/index.js +1 -0
  38. package/dist/components/CommentSheet/index.d.ts +155 -3
  39. package/dist/components/CommentSheet/index.js +1 -1
  40. package/dist/components/DetailView/index.d.ts +314 -0
  41. package/dist/components/DetailView/index.js +1 -0
  42. package/dist/components/ErrorBoundary/index.js +1 -1
  43. package/dist/components/{VideoFeed → Feed}/index.d.ts +64 -45
  44. package/dist/components/Feed/index.js +1 -0
  45. package/dist/components/ImageCarousel/index.d.ts +50 -0
  46. package/dist/components/ImageCarousel/index.js +1 -0
  47. package/dist/components/Playlist/index.d.ts +117 -0
  48. package/dist/components/Playlist/index.js +1 -0
  49. package/dist/components/ProgressBar/index.js +1 -1
  50. package/dist/components/QualityPicker/index.d.ts +35 -0
  51. package/dist/components/QualityPicker/index.js +1 -0
  52. package/dist/components/ReportSheet/index.d.ts +74 -0
  53. package/dist/components/ReportSheet/index.js +1 -0
  54. package/dist/components/Skeleton/index.js +1 -1
  55. package/dist/components/SpeedPicker/index.d.ts +32 -0
  56. package/dist/components/SpeedPicker/index.js +1 -0
  57. package/dist/components/VideoInfo/index.d.ts +7 -3
  58. package/dist/components/VideoInfo/index.js +1 -1
  59. package/dist/components/VideoPlayer/index.js +1 -1
  60. package/dist/components/VideoSlot/index.d.ts +31 -68
  61. package/dist/components/VideoSlot/index.js +2 -1
  62. package/dist/components/VirtualSlider/index.js +1 -1
  63. package/dist/index.d.ts +74 -11
  64. package/dist/index.js +39 -15
  65. package/package.json +8 -4
  66. package/dist/CommentSheet.css-BD6XbpU2.d.ts +0 -207
  67. package/dist/chunk-EBAMBI3O.js +0 -571
  68. package/dist/chunk-K3CETRCY.js +0 -737
  69. package/dist/chunk-PGHLVKXS.js +0 -148
  70. package/dist/chunk-QF7O26KZ.js +0 -357
  71. package/dist/chunk-RKS7YA7Z.js +0 -562
  72. package/dist/chunk-T4RQWGRY.js +0 -1519
  73. package/dist/chunk-UXNIXHII.js +0 -2040
  74. package/dist/chunk-YWAPI7JO.js +0 -204
  75. package/dist/components/VideoFeed/index.js +0 -1
@@ -1,1519 +0,0 @@
1
- import { PauseIcon, PlayIcon, HeartFilledIcon } from './chunk-ANCP53F3.js';
2
- import { clsx2 } from './chunk-EDWS2IPH.js';
3
- import { injectComponentCSS } from './chunk-UPCVSGWH.js';
4
- import { createContext, memo, useMemo, useState, useCallback, useEffect, useRef, useInsertionEffect, useContext } from 'react';
5
- import { jsx, jsxs } from 'react/jsx-runtime';
6
-
7
- var DEFAULT_DOUBLE_TAP_DELAY = 300;
8
- var TAP_DEBOUNCE_MS = 50;
9
- var DEFAULT_MOVEMENT_THRESHOLD = 10;
10
- function useDoubleTap({
11
- containerRef,
12
- onSingleTap,
13
- onDoubleTap,
14
- doubleTapDelay = DEFAULT_DOUBLE_TAP_DELAY,
15
- disabled = false,
16
- movementThreshold = DEFAULT_MOVEMENT_THRESHOLD
17
- }) {
18
- const lastTapTimeRef = useRef(0);
19
- const lastDoubleTapTimeRef = useRef(0);
20
- const tapTimeoutRef = useRef(null);
21
- const lastPositionRef = useRef({ x: 50, y: 50 });
22
- const lastExecutionTimeRef = useRef(0);
23
- const pointerStartRef = useRef(null);
24
- const cumulativeMovementRef = useRef(0);
25
- useEffect(() => {
26
- return () => {
27
- if (tapTimeoutRef.current) {
28
- clearTimeout(tapTimeoutRef.current);
29
- tapTimeoutRef.current = null;
30
- }
31
- };
32
- }, []);
33
- const calculatePosition = useCallback(
34
- (clientX, clientY) => {
35
- const container = containerRef.current;
36
- if (!container) return { x: 50, y: 50 };
37
- const rect = container.getBoundingClientRect();
38
- const x = (clientX - rect.left) / rect.width * 100;
39
- const y = (clientY - rect.top) / rect.height * 100;
40
- return {
41
- x: Math.max(0, Math.min(100, x)),
42
- y: Math.max(0, Math.min(100, y))
43
- };
44
- },
45
- [containerRef]
46
- );
47
- const handleTapCore = useCallback(() => {
48
- if (disabled) return;
49
- const now = Date.now();
50
- const timeSinceLastTap = now - lastTapTimeRef.current;
51
- if (tapTimeoutRef.current) {
52
- clearTimeout(tapTimeoutRef.current);
53
- tapTimeoutRef.current = null;
54
- }
55
- if (timeSinceLastTap < doubleTapDelay) {
56
- lastDoubleTapTimeRef.current = now;
57
- lastTapTimeRef.current = 0;
58
- onDoubleTap?.(lastPositionRef.current);
59
- } else {
60
- lastTapTimeRef.current = now;
61
- tapTimeoutRef.current = setTimeout(() => {
62
- tapTimeoutRef.current = null;
63
- const timeSinceDoubleTap = Date.now() - lastDoubleTapTimeRef.current;
64
- if (lastDoubleTapTimeRef.current > 0 && timeSinceDoubleTap < doubleTapDelay * 1.5) {
65
- return;
66
- }
67
- if (lastTapTimeRef.current !== now) {
68
- return;
69
- }
70
- onSingleTap?.();
71
- }, doubleTapDelay);
72
- }
73
- }, [disabled, doubleTapDelay, onSingleTap, onDoubleTap]);
74
- const isSwipe = useCallback(
75
- (clientX, clientY) => {
76
- if (cumulativeMovementRef.current > movementThreshold * 2) {
77
- return true;
78
- }
79
- const start = pointerStartRef.current;
80
- if (!start) return false;
81
- const dx = Math.abs(clientX - start.x);
82
- const dy = Math.abs(clientY - start.y);
83
- return dx > movementThreshold || dy > movementThreshold;
84
- },
85
- [movementThreshold]
86
- );
87
- const executeTapWithDebounce = useCallback(
88
- (clientX, clientY) => {
89
- if (disabled) return;
90
- if (isSwipe(clientX, clientY)) {
91
- pointerStartRef.current = null;
92
- return;
93
- }
94
- const now = Date.now();
95
- if (now - lastExecutionTimeRef.current < TAP_DEBOUNCE_MS) {
96
- return;
97
- }
98
- lastExecutionTimeRef.current = now;
99
- lastPositionRef.current = calculatePosition(clientX, clientY);
100
- handleTapCore();
101
- pointerStartRef.current = null;
102
- },
103
- [disabled, calculatePosition, handleTapCore, isSwipe]
104
- );
105
- const handlePointerDown = useCallback((e) => {
106
- if (e.button !== 0) return;
107
- pointerStartRef.current = { x: e.clientX, y: e.clientY };
108
- cumulativeMovementRef.current = 0;
109
- }, []);
110
- const handlePointerMove = useCallback((e) => {
111
- if (!pointerStartRef.current) return;
112
- cumulativeMovementRef.current += Math.abs(e.movementX) + Math.abs(e.movementY);
113
- }, []);
114
- const handlePointerUp = useCallback(
115
- (e) => {
116
- if (e.button !== 0) return;
117
- executeTapWithDebounce(e.clientX, e.clientY);
118
- },
119
- [executeTapWithDebounce]
120
- );
121
- const handleClick = useCallback(
122
- (e) => {
123
- if (e.detail > 0) {
124
- executeTapWithDebounce(e.clientX, e.clientY);
125
- }
126
- },
127
- [executeTapWithDebounce]
128
- );
129
- return {
130
- handlers: {
131
- onPointerDown: handlePointerDown,
132
- onPointerMove: handlePointerMove,
133
- onPointerUp: handlePointerUp,
134
- onClick: handleClick
135
- },
136
- lastPosition: lastPositionRef
137
- };
138
- }
139
-
140
- // src/components/VideoSlot/VideoSlot.css.ts
141
- var VIDEO_SLOT_CSS = `
142
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
143
- * Z-INDEX LAYERING CONTRACT
144
- *
145
- * Override these variables to customize layer stacking:
146
- * --sv-z-poster: Poster/Thumbnail layer (default: 3)
147
- * --sv-z-slot-spinner: Loading spinner layer (default: 2)
148
- * --sv-z-indicator: Error/Play indicator layer (default: 4)
149
- * --sv-z-progress: Progress bar layer (default: 5)
150
- *
151
- * Layer Stack (bottom to top):
152
- * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
153
- * \u2502 z-index: 5 - Progress Bar (topmost) \u2502
154
- * \u2502 z-index: 4 - Error UI / Play Indicator \u2502
155
- * \u2502 z-index: 3 - Poster / Thumbnail \u2502
156
- * \u2502 z-index: 2 - Loading Spinner \u2502
157
- * \u2502 z-index: 1 - Video Element \u2502
158
- * \u2502 z-index: 0 - Slot Container (base) \u2502
159
- * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
160
- * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
161
-
162
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
163
- * VideoSlot - Container for individual video in feed
164
- * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
165
-
166
- .sv-video-slot {
167
- position: relative;
168
- width: 100%;
169
- height: 100%;
170
- overflow: hidden;
171
- background-color: var(--sv-bg-primary, #000);
172
- /* Touch optimization */
173
- touch-action: none;
174
- user-select: none;
175
- -webkit-user-select: none;
176
- /* GPU acceleration */
177
- will-change: contents;
178
- contain: layout style paint;
179
- }
180
-
181
- /* Active slot (currently visible) */
182
- .sv-video-slot--active {
183
- z-index: 1;
184
- }
185
-
186
- /* Loading state */
187
- .sv-video-slot--loading .sv-video-slot__video {
188
- opacity: 0;
189
- }
190
-
191
- .sv-video-slot--loading .sv-video-slot__poster {
192
- opacity: 1;
193
- }
194
-
195
- /* Playing state */
196
- .sv-video-slot--playing .sv-video-slot__video {
197
- opacity: 1;
198
- }
199
-
200
- .sv-video-slot--playing .sv-video-slot__poster {
201
- opacity: 0;
202
- }
203
-
204
- /* Paused state - keep poster hidden (show paused video frame, not poster) */
205
- .sv-video-slot--paused .sv-video-slot__video {
206
- opacity: 1;
207
- }
208
-
209
- .sv-video-slot--paused .sv-video-slot__poster {
210
- opacity: 0;
211
- }
212
-
213
- /* Hidden poster (explicit class applied by VideoSlotPoster component) */
214
- .sv-video-slot__poster--hidden {
215
- opacity: 0;
216
- pointer-events: none;
217
- }
218
-
219
- /* Error state */
220
- .sv-video-slot--error {
221
- display: flex;
222
- align-items: center;
223
- justify-content: center;
224
- }
225
-
226
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
227
- * Video Element
228
- * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
229
-
230
- .sv-video-slot__video-container {
231
- position: absolute;
232
- inset: 0;
233
- display: flex;
234
- align-items: center;
235
- justify-content: center;
236
- }
237
-
238
- .sv-video-slot__video {
239
- width: 100%;
240
- height: 100%;
241
- object-fit: cover;
242
- background-color: transparent;
243
- /* Smooth fade transition */
244
- transition: opacity var(--sv-transition-duration, 300ms) ease-out;
245
- }
246
-
247
- /* Video loaded but not playing yet */
248
- .sv-video-slot__video[data-loaded="true"] {
249
- opacity: 1;
250
- }
251
-
252
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
253
- * Poster / Thumbnail / First Frame
254
- * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
255
-
256
- .sv-video-slot__poster {
257
- position: absolute;
258
- inset: 0;
259
- display: flex;
260
- align-items: center;
261
- justify-content: center;
262
- background-color: var(--sv-bg-primary, #000);
263
- /* Poster should be ABOVE spinner (z-index: 2) so thumbnail shows instead of spinner */
264
- z-index: var(--sv-z-poster, 3);
265
- /* Smooth fade transition */
266
- transition: opacity var(--sv-transition-duration, 300ms) ease-out;
267
- pointer-events: none;
268
- }
269
-
270
- .sv-video-slot__poster-image {
271
- width: 100%;
272
- height: 100%;
273
- object-fit: cover;
274
- }
275
-
276
- /* First frame (client-captured) */
277
- .sv-video-slot__poster--first-frame .sv-video-slot__poster-image {
278
- image-rendering: auto;
279
- }
280
-
281
- /* Skeleton placeholder */
282
- .sv-video-slot__poster--skeleton {
283
- background: linear-gradient(
284
- 110deg,
285
- var(--sv-skeleton-bg, rgba(255, 255, 255, 0.05)) 0%,
286
- var(--sv-skeleton-shimmer, rgba(255, 255, 255, 0.1)) 50%,
287
- var(--sv-skeleton-bg, rgba(255, 255, 255, 0.05)) 100%
288
- );
289
- background-size: 200% 100%;
290
- animation: sv-slot-skeleton-shimmer 1.5s ease-in-out infinite;
291
- }
292
-
293
- @keyframes sv-slot-skeleton-shimmer {
294
- 0% {
295
- background-position: 200% 0;
296
- }
297
- 100% {
298
- background-position: -200% 0;
299
- }
300
- }
301
-
302
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
303
- * Overlay Container
304
- * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
305
-
306
- .sv-video-slot__overlay {
307
- position: absolute;
308
- inset: 0;
309
- z-index: var(--sv-z-overlay, 2);
310
- /* Gradient background for better text visibility */
311
- background: linear-gradient(
312
- to top,
313
- rgba(0, 0, 0, 0.6) 0%,
314
- rgba(0, 0, 0, 0.3) 20%,
315
- transparent 40%,
316
- transparent 60%,
317
- rgba(0, 0, 0, 0.2) 80%,
318
- rgba(0, 0, 0, 0.4) 100%
319
- );
320
- pointer-events: none;
321
- }
322
-
323
- /*
324
- * Overlay sections should NOT block clicks (pass-through to container for play/pause)
325
- * Only actual interactive elements (buttons, links) inside sections should receive clicks.
326
- *
327
- * DO NOT use: .sv-video-slot__overlay > * { pointer-events: auto; }
328
- * This would make entire sections block clicks even on "empty" areas.
329
- */
330
- .sv-video-slot__overlay-top,
331
- .sv-video-slot__overlay-bottom,
332
- .sv-video-slot__overlay-left,
333
- .sv-video-slot__overlay-right,
334
- .sv-video-slot__actions,
335
- .sv-video-slot__author {
336
- pointer-events: none;
337
- }
338
-
339
- /* Only interactive elements inside overlay sections should receive clicks */
340
- .sv-video-slot__overlay button,
341
- .sv-video-slot__overlay a,
342
- .sv-video-slot__overlay [role="button"],
343
- .sv-video-slot__overlay input,
344
- .sv-video-slot__overlay label,
345
- .sv-video-slot__overlay .sv-progress-bar,
346
- .sv-video-slot__overlay .sv-author-info,
347
- .sv-video-slot__overlay .sv-video-info,
348
- .sv-video-slot__overlay .sv-action-bar {
349
- pointer-events: auto;
350
- }
351
-
352
- /* Overlay sections */
353
- .sv-video-slot__overlay-top {
354
- position: absolute;
355
- top: 0;
356
- left: 0;
357
- right: 0;
358
- padding: var(--sv-spacing-md, 16px);
359
- padding-top: calc(var(--sv-safe-area-top, 0px) + var(--sv-spacing-md, 16px));
360
- }
361
-
362
- .sv-video-slot__overlay-bottom {
363
- position: absolute;
364
- bottom: 0;
365
- left: 0;
366
- right: 0px;
367
- padding: var(--sv-spacing-md, 16px);
368
- padding-bottom: calc(var(--sv-safe-area-bottom, 0px) + var(--sv-spacing-md, 16px));
369
- display: flex;
370
- flex-direction: column;
371
- gap: var(--sv-spacing-sm, 8px);
372
- }
373
-
374
- .sv-video-slot__overlay-left {
375
- position: absolute;
376
- top: 50%;
377
- left: 0;
378
- transform: translateY(-50%);
379
- padding: var(--sv-spacing-md, 16px);
380
- }
381
-
382
- .sv-video-slot__overlay-right {
383
- position: absolute;
384
- top: 50%;
385
- right: 0;
386
- transform: translateY(-50%);
387
- padding: var(--sv-spacing-md, 16px);
388
- }
389
-
390
- /* Action bar (typically right side) */
391
- .sv-video-slot__actions {
392
- position: absolute;
393
- right: var(--sv-spacing-sm, 8px);
394
- bottom: calc(var(--sv-safe-area-bottom, 0px) + 80px);
395
- display: flex;
396
- flex-direction: column;
397
- align-items: center;
398
- }
399
-
400
- /* Author info (above bottom section, left side) */
401
- .sv-video-slot__author {
402
- position: absolute;
403
- left: var(--sv-spacing-md, 16px);
404
- /* Position above .sv-video-slot__overlay-bottom (which has ~80px content) */
405
- bottom: calc(var(--sv-safe-area-bottom, 0px) + 100px);
406
- right: 80px; /* Leave space for actions */
407
- max-width: calc(100% - 100px); /* Don't overlap with actions */
408
- }
409
-
410
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
411
- * Error State
412
- * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
413
-
414
- .sv-video-slot__error {
415
- position: absolute;
416
- inset: 0;
417
- display: flex;
418
- flex-direction: column;
419
- align-items: center;
420
- justify-content: center;
421
- background-color: var(--sv-bg-primary, #000);
422
- color: var(--sv-text-secondary, #999);
423
- text-align: center;
424
- padding: var(--sv-spacing-lg, 20px);
425
- gap: var(--sv-spacing-md, 16px);
426
- z-index: var(--sv-z-indicator, 3);
427
- }
428
-
429
- .sv-video-slot__error-icon {
430
- font-size: 48px;
431
- opacity: 0.6;
432
- }
433
-
434
- .sv-video-slot__error-message {
435
- font-size: var(--sv-font-size-md, 14px);
436
- max-width: 200px;
437
- }
438
-
439
- .sv-video-slot__error-retry {
440
- padding: var(--sv-spacing-sm, 8px) var(--sv-spacing-md, 16px);
441
- background-color: var(--sv-color-primary, #fe2c55);
442
- color: #fff;
443
- border: none;
444
- border-radius: var(--sv-radius-md, 8px);
445
- font-size: var(--sv-font-size-sm, 12px);
446
- font-weight: 600;
447
- cursor: pointer;
448
- transition: background-color 150ms ease;
449
- }
450
-
451
- .sv-video-slot__error-retry:hover {
452
- background-color: var(--sv-color-primary-hover, #e02850);
453
- }
454
-
455
- .sv-video-slot__error-retry:active {
456
- transform: scale(0.96);
457
- }
458
-
459
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
460
- * Loading Spinner
461
- * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
462
-
463
- .sv-video-slot__spinner {
464
- position: absolute;
465
- top: 50%;
466
- left: 50%;
467
- transform: translate(-50%, -50%);
468
- /* Use slot-specific z-index (not global --sv-z-overlay which may conflict) */
469
- z-index: var(--sv-z-slot-spinner, 2);
470
- }
471
-
472
- .sv-video-slot__spinner-circle {
473
- width: 40px;
474
- height: 40px;
475
- border: 3px solid rgba(255, 255, 255, 0.2);
476
- border-top-color: var(--sv-color-primary, #fe2c55);
477
- border-radius: 50%;
478
- animation: sv-slot-spin 0.8s linear infinite;
479
- }
480
-
481
- @keyframes sv-slot-spin {
482
- to {
483
- transform: rotate(360deg);
484
- }
485
- }
486
-
487
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
488
- * Play/Pause Visual Indicator
489
- * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
490
-
491
- .sv-video-slot__play-indicator {
492
- position: absolute;
493
- top: 50%;
494
- left: 50%;
495
- transform: translate(-50%, -50%);
496
- width: 64px;
497
- height: 64px;
498
- display: flex;
499
- align-items: center;
500
- justify-content: center;
501
- border-radius: 50%;
502
- z-index: var(--sv-z-indicator, 3);
503
- opacity: 0;
504
- transition: opacity 200ms ease-out;
505
- pointer-events: none;
506
- }
507
-
508
- /* Show indicator */
509
- .sv-video-slot__play-indicator--visible {
510
- opacity: 0.65;
511
- }
512
-
513
- /* Persist mode: stay visible, no animation */
514
- .sv-video-slot__play-indicator--persist {
515
- opacity: 0.65;
516
- }
517
-
518
- /* Animating out: scale up + fade out */
519
- .sv-video-slot__play-indicator--animating-out {
520
- animation: sv-slot-indicator-out var(--sv-indicator-duration, 200ms) ease-out forwards;
521
- }
522
-
523
- @keyframes sv-slot-indicator-out {
524
- 0% {
525
- opacity: 1;
526
- transform: translate(-50%, -50%) scale(1);
527
- }
528
- 100% {
529
- opacity: 0;
530
- transform: translate(-50%, -50%) scale(1.5);
531
- }
532
- }
533
-
534
- .sv-video-slot__play-indicator-icon {
535
- color: #fff;
536
- font-size: 28px;
537
- width: 100%;
538
- height: 100%;
539
- display: flex;
540
- align-items: center;
541
- justify-content: center;
542
- }
543
-
544
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
545
- * Progress Bar
546
- * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
547
-
548
- .sv-video-slot__progress {
549
- position: absolute;
550
- bottom: 0;
551
- left: 0;
552
- right: 0;
553
- height: 3px;
554
- background-color: rgba(255, 255, 255, 0.2);
555
- z-index: var(--sv-z-progress, 4);
556
- }
557
-
558
- .sv-video-slot__progress-bar {
559
- height: 100%;
560
- background-color: var(--sv-color-primary, #fe2c55);
561
- transition: width 100ms linear;
562
- }
563
-
564
- .sv-video-slot__progress-buffered {
565
- position: absolute;
566
- top: 0;
567
- left: 0;
568
- height: 100%;
569
- background-color: rgba(255, 255, 255, 0.3);
570
- }
571
- `;
572
- function invariantContextError(hookName, componentName) {
573
- const baseMessage = `${hookName} must be used within a VideoSlot component.`;
574
- if (process.env.NODE_ENV !== "production") {
575
- const debugInfo = [
576
- "",
577
- "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
578
- "\u274C VideoSlot Context Error",
579
- "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
580
- "",
581
- `Hook: ${hookName}`,
582
- "",
583
- "",
584
- "Possible causes:",
585
- " 1. You used a VideoSlot compound component outside of <VideoSlot>",
586
- " 2. You used the headless component without SDK wiring",
587
- " 3. The component tree was rendered in isolation (e.g., Storybook)",
588
- "",
589
- "Solutions:",
590
- " 1. Wrap with <VideoSlot> or <VideoSlotHeadless>",
591
- " 2. Use useOptionalVideoSlotContext() if context is optional",
592
- " 3. Provide mock context in tests via <VideoSlotContext.Provider>",
593
- "",
594
- "Example:",
595
- " // Correct usage",
596
- " <VideoSlot video={video}>",
597
- " <VideoSlotPoster /> \u2190 These need context",
598
- " <VideoSlotOverlay />",
599
- " </VideoSlot>",
600
- "",
601
- "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"
602
- ].filter(Boolean).join("\n");
603
- throw new Error(baseMessage + debugInfo);
604
- }
605
- throw new Error(baseMessage);
606
- }
607
- var VideoSlotContext = createContext(null);
608
- VideoSlotContext.displayName = "VideoSlotContext";
609
- function useVideoSlotContext() {
610
- const context = useContext(VideoSlotContext);
611
- if (!context) {
612
- invariantContextError("useVideoSlotContext");
613
- }
614
- return context;
615
- }
616
- function useOptionalVideoSlotContext() {
617
- return useContext(VideoSlotContext);
618
- }
619
- function useVideoSlotVideo() {
620
- const { video } = useVideoSlotContext();
621
- return video;
622
- }
623
- function useVideoSlotPlayer() {
624
- const { playerState, playerControls } = useVideoSlotContext();
625
- return { state: playerState, controls: playerControls };
626
- }
627
- function useVideoSlotResource() {
628
- const { resourceState } = useVideoSlotContext();
629
- return resourceState;
630
- }
631
- function useVideoSlotIsActive() {
632
- const { resourceState } = useVideoSlotContext();
633
- return resourceState.isActive;
634
- }
635
- function useVideoSlotIsPlaying() {
636
- const { playerState } = useVideoSlotContext();
637
- return playerState.isPlaying;
638
- }
639
-
640
- // src/components/VideoSlot/constants.ts
641
- var VIDEO_ID_ATTR = "data-video-id";
642
- var VIDEO_ID_DATASET_KEY = "videoId";
643
- var SLOT_ACTIVE_ATTR = "data-slot-active";
644
- var SLOT_ACTIVE_DATASET_KEY = "slotActive";
645
- var PLAYER_STATE_ATTR = "data-player-state";
646
- var PLAYER_STATE_DATASET_KEY = "playerState";
647
- var LOADING_ATTR = "data-loading";
648
- var LOADING_DATASET_KEY = "loading";
649
- var SLOT_CLASS = "sv-video-slot";
650
- var SLOT_ACTIVE_CLASS = "sv-video-slot--active";
651
- var SLOT_LOADING_CLASS = "sv-video-slot--loading";
652
- var SLOT_ERROR_CLASS = "sv-video-slot--error";
653
- var SLOT_PLAYING_CLASS = "sv-video-slot--playing";
654
- var SLOT_PAUSED_CLASS = "sv-video-slot--paused";
655
- var Z_INDEX = {
656
- /** Base layer - slot container */
657
- SLOT: 0,
658
- /** Video element */
659
- VIDEO: 1,
660
- /** Loading spinner */
661
- SPINNER: 2,
662
- /** Poster/thumbnail (above spinner so thumbnail shows during loading) */
663
- POSTER: 3,
664
- /** Error UI and play/pause indicator */
665
- INDICATOR: 4,
666
- /** Progress bar (always on top) */
667
- PROGRESS: 5
668
- };
669
- var Z_INDEX_CSS_VARS = {
670
- POSTER: "--sv-z-poster",
671
- SPINNER: "--sv-z-slot-spinner",
672
- INDICATOR: "--sv-z-indicator",
673
- PROGRESS: "--sv-z-progress"
674
- };
675
- var ANIMATION_SELECTOR = '[data-like-animation="true"]';
676
- function VideoSlotHeadlessBase({
677
- video,
678
- resourceState,
679
- playerState,
680
- playerControls,
681
- className,
682
- children,
683
- // TODO: Implement visibility detection with IntersectionObserver in v2
684
- // These callbacks are reserved for future implementation:
685
- // - onVisible: Called when slot enters viewport (for analytics, autoplay)
686
- // - onHidden: Called when slot leaves viewport (for pause, cleanup)
687
- onVisible: _onVisible,
688
- onHidden: _onHidden,
689
- onTap,
690
- onDoubleTap,
691
- renderError,
692
- renderLoading,
693
- restoreFrame
694
- }) {
695
- useInsertionEffect(() => {
696
- return injectComponentCSS("video-slot", VIDEO_SLOT_CSS);
697
- }, []);
698
- const containerRef = useRef(null);
699
- const animationElementRef = useRef(null);
700
- const prevVideoIdRef = useRef(video.id);
701
- const [firstFrame, setFirstFrame] = useState(null);
702
- const [lastUserToggleTime, setLastUserToggleTime] = useState(0);
703
- if (prevVideoIdRef.current !== video.id) {
704
- prevVideoIdRef.current = video.id;
705
- animationElementRef.current = null;
706
- if (firstFrame !== null) {
707
- setFirstFrame(null);
708
- }
709
- }
710
- const getAnimationElement = useCallback(() => {
711
- if (animationElementRef.current) {
712
- return animationElementRef.current;
713
- }
714
- const container = containerRef.current;
715
- if (!container) return null;
716
- const element = container.querySelector(ANIMATION_SELECTOR);
717
- if (element) {
718
- animationElementRef.current = element;
719
- }
720
- return element;
721
- }, []);
722
- const triggerLikeAnimation = useCallback(
723
- (position) => {
724
- const animationElement = getAnimationElement();
725
- if (animationElement?.__likeAnimation) {
726
- animationElement.__likeAnimation.triggerHeart(position.x, position.y);
727
- }
728
- },
729
- [getAnimationElement]
730
- );
731
- const shouldShowAnimationWhenLiked = useCallback(() => {
732
- const animationElement = getAnimationElement();
733
- return animationElement?.__showWhenAlreadyLiked ?? true;
734
- }, [getAnimationElement]);
735
- const handleSingleTap = useCallback(() => {
736
- setLastUserToggleTime(Date.now());
737
- if (onTap) {
738
- onTap();
739
- } else {
740
- playerControls.toggle();
741
- }
742
- }, [onTap, playerControls]);
743
- const handleDoubleTap = useCallback(
744
- (position) => {
745
- if (shouldShowAnimationWhenLiked() || !playerState.isPlaying) {
746
- triggerLikeAnimation(position);
747
- }
748
- onDoubleTap?.();
749
- },
750
- [onDoubleTap, shouldShowAnimationWhenLiked, playerState.isPlaying, triggerLikeAnimation]
751
- );
752
- const { handlers: tapHandlers } = useDoubleTap({
753
- containerRef,
754
- onSingleTap: handleSingleTap,
755
- onDoubleTap: handleDoubleTap
756
- });
757
- const handleRetry = useCallback(() => {
758
- playerControls.play();
759
- }, [playerControls]);
760
- const contextValue = useMemo(
761
- () => ({
762
- video,
763
- resourceState,
764
- playerState,
765
- playerControls,
766
- firstFrame,
767
- setFirstFrame,
768
- lastUserToggleTime,
769
- setLastUserToggleTime
770
- }),
771
- [video, resourceState, playerState, playerControls, firstFrame, lastUserToggleTime]
772
- );
773
- const stateClasses = useMemo(() => {
774
- const classes = [SLOT_CLASS];
775
- if (resourceState.isActive) {
776
- classes.push(SLOT_ACTIVE_CLASS);
777
- }
778
- if (playerState.isLoading) {
779
- classes.push(SLOT_LOADING_CLASS);
780
- }
781
- if (playerState.error) {
782
- classes.push(SLOT_ERROR_CLASS);
783
- }
784
- if (playerState.isPlaying) {
785
- classes.push(SLOT_PLAYING_CLASS);
786
- } else {
787
- classes.push(SLOT_PAUSED_CLASS);
788
- }
789
- return classes;
790
- }, [resourceState.isActive, playerState.isLoading, playerState.error, playerState.isPlaying]);
791
- const playerStateString = useMemo(() => {
792
- if (playerState.error) return "error";
793
- if (playerState.isLoading) return "loading";
794
- if (playerState.isPlaying) return "playing";
795
- return "paused";
796
- }, [playerState]);
797
- return /* @__PURE__ */ jsx(VideoSlotContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs(
798
- "div",
799
- {
800
- ref: containerRef,
801
- className: clsx2(...stateClasses, className),
802
- ...tapHandlers,
803
- ...{
804
- [VIDEO_ID_ATTR]: video.id,
805
- [SLOT_ACTIVE_ATTR]: resourceState.isActive ? "true" : "false",
806
- [PLAYER_STATE_ATTR]: playerStateString,
807
- [LOADING_ATTR]: playerState.isLoading ? "true" : "false"
808
- },
809
- children: [
810
- restoreFrame && !playerState.isPlaying && !playerState.error && /* @__PURE__ */ jsx(
811
- "div",
812
- {
813
- className: "sv-video-slot__restore-frame",
814
- style: {
815
- position: "absolute",
816
- inset: 0,
817
- zIndex: 5,
818
- // Above video, below overlay
819
- backgroundImage: `url(${restoreFrame})`,
820
- backgroundSize: "contain",
821
- backgroundPosition: "center",
822
- backgroundRepeat: "no-repeat",
823
- backgroundColor: "#000"
824
- },
825
- "aria-hidden": "true"
826
- }
827
- ),
828
- playerState.error && (renderError ? renderError(playerState.error, handleRetry) : /* @__PURE__ */ jsx(DefaultErrorUI, { error: playerState.error, onRetry: handleRetry })),
829
- playerState.isLoading && !playerState.error && !restoreFrame && (renderLoading ? renderLoading() : /* @__PURE__ */ jsx(DefaultLoadingUI, {})),
830
- children
831
- ]
832
- }
833
- ) });
834
- }
835
- function getUserFriendlyErrorMessage(error) {
836
- const message = error.message?.toLowerCase() ?? "";
837
- if (message.includes("network") || message.includes("fetch") || message.includes("offline")) {
838
- return "Unable to connect. Please check your internet connection.";
839
- }
840
- if (message.includes("timeout") || message.includes("timed out")) {
841
- return "Video took too long to load. Please try again.";
842
- }
843
- if (message.includes("format") || message.includes("codec") || message.includes("unsupported") || message.includes("media")) {
844
- return "This video format is not supported.";
845
- }
846
- if (message.includes("blocked") || message.includes("denied") || message.includes("cors")) {
847
- return "Video playback is restricted.";
848
- }
849
- if (message.includes("404") || message.includes("not found")) {
850
- return "Video not available.";
851
- }
852
- if (message.includes("500") || message.includes("server")) {
853
- return "Server error. Please try again later.";
854
- }
855
- if (message.includes("hls") || message.includes("manifest") || message.includes("segment")) {
856
- return "Video streaming error. Please try again.";
857
- }
858
- return "Video failed to load. Tap to retry.";
859
- }
860
- function DefaultErrorUI({
861
- error,
862
- onRetry
863
- }) {
864
- const displayMessage = getUserFriendlyErrorMessage(error);
865
- return /* @__PURE__ */ jsxs("div", { className: "sv-video-slot__error", children: [
866
- /* @__PURE__ */ jsx("div", { className: "sv-video-slot__error-icon", children: "\u26A0\uFE0F" }),
867
- /* @__PURE__ */ jsx("div", { className: "sv-video-slot__error-message", children: displayMessage }),
868
- /* @__PURE__ */ jsx(
869
- "button",
870
- {
871
- type: "button",
872
- className: "sv-video-slot__error-retry",
873
- onClick: (e) => {
874
- e.stopPropagation();
875
- onRetry();
876
- },
877
- children: "Retry"
878
- }
879
- )
880
- ] });
881
- }
882
- function DefaultLoadingUI() {
883
- return /* @__PURE__ */ jsx("div", { className: "sv-video-slot__spinner", children: /* @__PURE__ */ jsx("div", { className: "sv-video-slot__spinner-circle", "aria-label": "Loading video" }) });
884
- }
885
- var VideoSlotHeadless = memo(VideoSlotHeadlessBase);
886
- VideoSlotHeadless.displayName = "VideoSlotHeadless";
887
- var VideoSlotPosterInner = memo(function VideoSlotPosterInner2({
888
- className,
889
- forceSkeleton = false,
890
- posterUrl,
891
- loading = "eager",
892
- alt,
893
- onError,
894
- videoPoster,
895
- videoTitle,
896
- firstFrame,
897
- isLoading,
898
- currentTime
899
- }) {
900
- const imageSource = useMemo(() => {
901
- if (forceSkeleton) return null;
902
- if (posterUrl) return posterUrl;
903
- if (firstFrame) return firstFrame;
904
- if (videoPoster) return videoPoster;
905
- return null;
906
- }, [forceSkeleton, posterUrl, firstFrame, videoPoster]);
907
- const sourceKey = imageSource ?? "skeleton";
908
- const [errorForSource, setErrorForSource] = useState(null);
909
- const imageError = errorForSource === sourceKey;
910
- const handleImageError = useCallback(() => {
911
- setErrorForSource(sourceKey);
912
- onError?.(new Error("Failed to load poster image"));
913
- }, [sourceKey, onError]);
914
- const showSkeleton = !imageSource || imageError;
915
- const posterType = useMemo(() => {
916
- if (showSkeleton) return "skeleton";
917
- if (imageSource === firstFrame) return "first-frame";
918
- return "poster";
919
- }, [showSkeleton, imageSource, firstFrame]);
920
- const imageAlt = alt ?? videoTitle ?? "";
921
- const isVisible = isLoading && currentTime === 0;
922
- return /* @__PURE__ */ jsx(
923
- "div",
924
- {
925
- className: clsx2(
926
- "sv-video-slot__poster",
927
- `sv-video-slot__poster--${posterType}`,
928
- !isVisible && "sv-video-slot__poster--hidden",
929
- className
930
- ),
931
- "aria-hidden": !isVisible,
932
- children: imageSource && !imageError ? /* @__PURE__ */ jsx(
933
- "img",
934
- {
935
- className: "sv-video-slot__poster-image",
936
- src: imageSource,
937
- alt: imageAlt,
938
- loading,
939
- decoding: "async",
940
- onError: handleImageError
941
- }
942
- ) : (
943
- // Skeleton shimmer - pure CSS animation, no JS
944
- /* @__PURE__ */ jsx("div", { className: "sv-video-slot__poster-skeleton", "aria-hidden": "true" })
945
- )
946
- }
947
- );
948
- });
949
- VideoSlotPosterInner.displayName = "VideoSlotPosterInner";
950
- function VideoSlotPoster(props) {
951
- const { video, firstFrame, playerState } = useVideoSlotContext();
952
- return /* @__PURE__ */ jsx(
953
- VideoSlotPosterInner,
954
- {
955
- ...props,
956
- videoPoster: video.poster,
957
- videoTitle: video.title,
958
- firstFrame,
959
- isLoading: playerState.isLoading,
960
- currentTime: playerState.currentTime
961
- }
962
- );
963
- }
964
- VideoSlotPoster.displayName = "VideoSlotPoster";
965
- function VideoSlotSkeleton({
966
- className
967
- }) {
968
- return /* @__PURE__ */ jsx(
969
- "div",
970
- {
971
- className: clsx2("sv-video-slot__poster", "sv-video-slot__poster--skeleton", className),
972
- "aria-hidden": "true",
973
- children: /* @__PURE__ */ jsx("div", { className: "sv-video-slot__poster-skeleton" })
974
- }
975
- );
976
- }
977
- VideoSlotSkeleton.displayName = "VideoSlotSkeleton";
978
- function VideoSlotOverlayRoot({
979
- children,
980
- className,
981
- noGradient = false
982
- }) {
983
- return /* @__PURE__ */ jsx(
984
- "div",
985
- {
986
- className: clsx2(
987
- "sv-video-slot__overlay",
988
- noGradient && "sv-video-slot__overlay--no-gradient",
989
- className
990
- ),
991
- children
992
- }
993
- );
994
- }
995
- function VideoSlotOverlayTop({
996
- children,
997
- className
998
- }) {
999
- return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__overlay-top", className), children });
1000
- }
1001
- function VideoSlotOverlayBottom({
1002
- children,
1003
- className
1004
- }) {
1005
- return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__overlay-bottom", className), children });
1006
- }
1007
- function VideoSlotOverlayLeft({
1008
- children,
1009
- className
1010
- }) {
1011
- return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__overlay-left", className), children });
1012
- }
1013
- function VideoSlotOverlayRight({
1014
- children,
1015
- className
1016
- }) {
1017
- return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__overlay-right", className), children });
1018
- }
1019
- function VideoSlotOverlayActions({
1020
- children,
1021
- className
1022
- }) {
1023
- return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__actions", className), children });
1024
- }
1025
- function VideoSlotOverlayAuthor({
1026
- children,
1027
- className
1028
- }) {
1029
- return /* @__PURE__ */ jsx("div", { className: clsx2("sv-video-slot__author", className), children });
1030
- }
1031
- VideoSlotOverlayRoot.displayName = "VideoSlotOverlay";
1032
- VideoSlotOverlayTop.displayName = "VideoSlotOverlay.Top";
1033
- VideoSlotOverlayBottom.displayName = "VideoSlotOverlay.Bottom";
1034
- VideoSlotOverlayLeft.displayName = "VideoSlotOverlay.Left";
1035
- VideoSlotOverlayRight.displayName = "VideoSlotOverlay.Right";
1036
- VideoSlotOverlayActions.displayName = "VideoSlotOverlay.Actions";
1037
- VideoSlotOverlayAuthor.displayName = "VideoSlotOverlay.Author";
1038
- var VideoSlotOverlay = Object.assign(VideoSlotOverlayRoot, {
1039
- Top: VideoSlotOverlayTop,
1040
- Bottom: VideoSlotOverlayBottom,
1041
- Left: VideoSlotOverlayLeft,
1042
- Right: VideoSlotOverlayRight,
1043
- Actions: VideoSlotOverlayActions,
1044
- Author: VideoSlotOverlayAuthor
1045
- });
1046
-
1047
- // src/components/VideoSlot/VideoSlotLikeAnimation.css.ts
1048
- var VIDEO_SLOT_LIKE_ANIMATION_CSS = `
1049
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1050
- Container
1051
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
1052
-
1053
- .sv-like-animation-container {
1054
- position: absolute;
1055
- inset: 0;
1056
- overflow: hidden;
1057
- pointer-events: none;
1058
- z-index: var(--sv-z-indicator, 3);
1059
- }
1060
-
1061
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1062
- Single Heart
1063
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
1064
-
1065
- .sv-like-heart {
1066
- position: absolute;
1067
- left: var(--sv-heart-x, 50%);
1068
- top: var(--sv-heart-y, 50%);
1069
- transform: translate(-50%, -50%);
1070
- pointer-events: none;
1071
- will-change: transform, opacity;
1072
- }
1073
-
1074
- .sv-like-heart__icon {
1075
- display: flex;
1076
- align-items: center;
1077
- justify-content: center;
1078
- width: var(--sv-heart-size, 80px);
1079
- height: var(--sv-heart-size, 80px);
1080
- transform-origin: center;
1081
- animation-duration: var(--sv-heart-duration, 1000ms);
1082
- animation-fill-mode: forwards;
1083
- animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
1084
- }
1085
-
1086
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1087
- Float Animation (TikTok style)
1088
- - Hearts float upward while wiggling
1089
- - Fades out near the top
1090
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
1091
-
1092
- .sv-like-heart--float .sv-like-heart__icon {
1093
- animation-name: sv-heart-float;
1094
- animation-timing-function: ease-out;
1095
- }
1096
-
1097
- @keyframes sv-heart-float {
1098
- 0% {
1099
- opacity: 0;
1100
- transform: scale(0) rotate(var(--sv-heart-rotation, 0deg));
1101
- }
1102
- 10% {
1103
- opacity: 1;
1104
- transform: scale(calc(var(--sv-heart-scale, 1) * 1.2)) rotate(var(--sv-heart-rotation, 0deg));
1105
- }
1106
- 20% {
1107
- transform:
1108
- scale(var(--sv-heart-scale, 1))
1109
- rotate(var(--sv-heart-rotation, 0deg))
1110
- translateY(-20px)
1111
- translateX(10px);
1112
- }
1113
- 40% {
1114
- transform:
1115
- scale(var(--sv-heart-scale, 1))
1116
- rotate(calc(var(--sv-heart-rotation, 0deg) - 10deg))
1117
- translateY(-60px)
1118
- translateX(-15px);
1119
- }
1120
- 60% {
1121
- opacity: 1;
1122
- transform:
1123
- scale(var(--sv-heart-scale, 1))
1124
- rotate(calc(var(--sv-heart-rotation, 0deg) + 15deg))
1125
- translateY(-100px)
1126
- translateX(10px);
1127
- }
1128
- 80% {
1129
- opacity: 0.6;
1130
- transform:
1131
- scale(calc(var(--sv-heart-scale, 1) * 0.9))
1132
- rotate(calc(var(--sv-heart-rotation, 0deg) - 5deg))
1133
- translateY(-140px)
1134
- translateX(-5px);
1135
- }
1136
- 100% {
1137
- opacity: 0;
1138
- transform:
1139
- scale(calc(var(--sv-heart-scale, 1) * 0.7))
1140
- rotate(var(--sv-heart-rotation, 0deg))
1141
- translateY(-180px)
1142
- translateX(0);
1143
- }
1144
- }
1145
-
1146
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1147
- Burst Animation
1148
- - Heart bursts outward with energy
1149
- - Scales up quickly then fades
1150
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
1151
-
1152
- .sv-like-heart--burst .sv-like-heart__icon {
1153
- animation-name: sv-heart-burst;
1154
- animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
1155
- }
1156
-
1157
- @keyframes sv-heart-burst {
1158
- 0% {
1159
- opacity: 0;
1160
- transform: scale(0) rotate(var(--sv-heart-rotation, 0deg));
1161
- }
1162
- 20% {
1163
- opacity: 1;
1164
- transform: scale(calc(var(--sv-heart-scale, 1) * 1.5)) rotate(calc(var(--sv-heart-rotation, 0deg) + 15deg));
1165
- }
1166
- 40% {
1167
- transform: scale(calc(var(--sv-heart-scale, 1) * 1.2)) rotate(calc(var(--sv-heart-rotation, 0deg) - 10deg));
1168
- }
1169
- 60% {
1170
- opacity: 1;
1171
- transform: scale(var(--sv-heart-scale, 1)) rotate(var(--sv-heart-rotation, 0deg)) translateY(-30px);
1172
- }
1173
- 80% {
1174
- opacity: 0.5;
1175
- transform: scale(calc(var(--sv-heart-scale, 1) * 0.9)) rotate(var(--sv-heart-rotation, 0deg)) translateY(-60px);
1176
- }
1177
- 100% {
1178
- opacity: 0;
1179
- transform: scale(calc(var(--sv-heart-scale, 1) * 0.6)) rotate(var(--sv-heart-rotation, 0deg)) translateY(-80px);
1180
- }
1181
- }
1182
-
1183
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1184
- Scale Animation
1185
- - Simple scale up and fade
1186
- - Clean and minimal
1187
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
1188
-
1189
- .sv-like-heart--scale .sv-like-heart__icon {
1190
- animation-name: sv-heart-scale;
1191
- animation-timing-function: ease-out;
1192
- }
1193
-
1194
- @keyframes sv-heart-scale {
1195
- 0% {
1196
- opacity: 0;
1197
- transform: scale(0);
1198
- }
1199
- 20% {
1200
- opacity: 1;
1201
- transform: scale(calc(var(--sv-heart-scale, 1) * 1.3));
1202
- }
1203
- 40% {
1204
- transform: scale(var(--sv-heart-scale, 1));
1205
- }
1206
- 70% {
1207
- opacity: 1;
1208
- transform: scale(var(--sv-heart-scale, 1));
1209
- }
1210
- 100% {
1211
- opacity: 0;
1212
- transform: scale(calc(var(--sv-heart-scale, 1) * 1.1));
1213
- }
1214
- }
1215
-
1216
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1217
- Bounce Animation
1218
- - Heart bounces in place with energy
1219
- - Playful and fun
1220
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
1221
-
1222
- .sv-like-heart--bounce .sv-like-heart__icon {
1223
- animation-name: sv-heart-bounce;
1224
- animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
1225
- }
1226
-
1227
- @keyframes sv-heart-bounce {
1228
- 0% {
1229
- opacity: 0;
1230
- transform: scale(0) rotate(var(--sv-heart-rotation, 0deg)) translateY(0);
1231
- }
1232
- 15% {
1233
- opacity: 1;
1234
- transform: scale(calc(var(--sv-heart-scale, 1) * 1.4)) rotate(calc(var(--sv-heart-rotation, 0deg) + 10deg)) translateY(-30px);
1235
- }
1236
- 30% {
1237
- transform: scale(calc(var(--sv-heart-scale, 1) * 0.9)) rotate(calc(var(--sv-heart-rotation, 0deg) - 5deg)) translateY(10px);
1238
- }
1239
- 45% {
1240
- transform: scale(calc(var(--sv-heart-scale, 1) * 1.15)) rotate(calc(var(--sv-heart-rotation, 0deg) + 5deg)) translateY(-15px);
1241
- }
1242
- 60% {
1243
- opacity: 1;
1244
- transform: scale(var(--sv-heart-scale, 1)) rotate(var(--sv-heart-rotation, 0deg)) translateY(0);
1245
- }
1246
- 80% {
1247
- opacity: 0.7;
1248
- transform: scale(var(--sv-heart-scale, 1)) rotate(var(--sv-heart-rotation, 0deg)) translateY(-10px);
1249
- }
1250
- 100% {
1251
- opacity: 0;
1252
- transform: scale(calc(var(--sv-heart-scale, 1) * 0.8)) rotate(var(--sv-heart-rotation, 0deg)) translateY(-20px);
1253
- }
1254
- }
1255
-
1256
- /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1257
- Accessibility: Reduce motion
1258
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
1259
-
1260
- @media (prefers-reduced-motion: reduce) {
1261
- .sv-like-heart__icon {
1262
- animation-duration: 0.5s !important;
1263
- }
1264
-
1265
- .sv-like-heart--float .sv-like-heart__icon,
1266
- .sv-like-heart--burst .sv-like-heart__icon,
1267
- .sv-like-heart--bounce .sv-like-heart__icon {
1268
- animation-name: sv-heart-scale;
1269
- }
1270
- }
1271
- `;
1272
- var SingleHeart = memo(function SingleHeart2({
1273
- heart,
1274
- icon,
1275
- size,
1276
- animation,
1277
- duration,
1278
- onComplete
1279
- }) {
1280
- useEffect(() => {
1281
- const timer = setTimeout(() => {
1282
- onComplete(heart.id);
1283
- }, duration);
1284
- return () => clearTimeout(timer);
1285
- }, [heart.id, duration, onComplete]);
1286
- return /* @__PURE__ */ jsx(
1287
- "div",
1288
- {
1289
- className: clsx2("sv-like-heart", `sv-like-heart--${animation}`),
1290
- style: {
1291
- "--sv-heart-x": `${heart.x}%`,
1292
- "--sv-heart-y": `${heart.y}%`,
1293
- "--sv-heart-rotation": `${heart.rotation}deg`,
1294
- "--sv-heart-scale": heart.scale,
1295
- "--sv-heart-duration": `${duration}ms`,
1296
- "--sv-heart-size": `${size}px`
1297
- },
1298
- "aria-hidden": "true",
1299
- children: /* @__PURE__ */ jsx("div", { className: "sv-like-heart__icon", children: icon })
1300
- }
1301
- );
1302
- });
1303
- function VideoSlotLikeAnimation({
1304
- icon,
1305
- duration = 1e3,
1306
- animation = "float",
1307
- size = 80,
1308
- color = "#fe2c55",
1309
- showWhenAlreadyLiked = true,
1310
- maxHearts = 10,
1311
- className,
1312
- onHeartCreated
1313
- }) {
1314
- useInsertionEffect(() => {
1315
- return injectComponentCSS("VideoSlotLikeAnimation", VIDEO_SLOT_LIKE_ANIMATION_CSS);
1316
- }, []);
1317
- const heartIdCounterRef = useRef(0);
1318
- const [hearts, setHearts] = useState([]);
1319
- const containerRef = useRef(null);
1320
- const triggerHeart = useCallback(
1321
- (x, y) => {
1322
- heartIdCounterRef.current += 1;
1323
- const newHeart = {
1324
- id: heartIdCounterRef.current,
1325
- x,
1326
- y,
1327
- createdAt: Date.now(),
1328
- rotation: Math.random() * 60 - 30,
1329
- // -30 to 30 degrees
1330
- scale: 0.8 + Math.random() * 0.4
1331
- // 0.8 to 1.2
1332
- };
1333
- setHearts((prev) => {
1334
- const updated = prev.length >= maxHearts ? prev.slice(1) : prev;
1335
- return [...updated, newHeart];
1336
- });
1337
- onHeartCreated?.(newHeart);
1338
- },
1339
- [maxHearts, onHeartCreated]
1340
- );
1341
- const handleHeartComplete = useCallback((id) => {
1342
- setHearts((prev) => prev.filter((h) => h.id !== id));
1343
- }, []);
1344
- const clearAll = useCallback(() => {
1345
- setHearts([]);
1346
- }, []);
1347
- useEffect(() => {
1348
- const container = containerRef.current;
1349
- if (!container) {
1350
- if (process.env.NODE_ENV !== "production") {
1351
- console.warn("[VideoSlotLikeAnimation] Container ref is null. Animation will not work.");
1352
- }
1353
- return;
1354
- }
1355
- const el = container;
1356
- el.__likeAnimation = {
1357
- triggerHeart,
1358
- clearAll
1359
- };
1360
- el.__showWhenAlreadyLiked = showWhenAlreadyLiked;
1361
- return () => {
1362
- el.__likeAnimation = void 0;
1363
- el.__showWhenAlreadyLiked = void 0;
1364
- };
1365
- }, [triggerHeart, clearAll, showWhenAlreadyLiked]);
1366
- const iconElement = icon ?? (typeof HeartFilledIcon === "function" ? /* @__PURE__ */ jsx(
1367
- HeartFilledIcon,
1368
- {
1369
- width: size,
1370
- height: size,
1371
- style: { color, filter: "drop-shadow(0 2px 4px rgba(0,0,0,0.3))" }
1372
- }
1373
- ) : /* @__PURE__ */ jsx("span", { style: { fontSize: size, color, textShadow: "0 2px 4px rgba(0,0,0,0.3)" }, children: "\u2764\uFE0F" }));
1374
- return /* @__PURE__ */ jsx(
1375
- "div",
1376
- {
1377
- ref: containerRef,
1378
- className: clsx2("sv-like-animation-container", className),
1379
- "data-like-animation": "true",
1380
- "aria-hidden": "true",
1381
- children: hearts.map((heart) => /* @__PURE__ */ jsx(
1382
- SingleHeart,
1383
- {
1384
- heart,
1385
- icon: iconElement,
1386
- size,
1387
- animation,
1388
- duration,
1389
- onComplete: handleHeartComplete
1390
- },
1391
- heart.id
1392
- ))
1393
- }
1394
- );
1395
- }
1396
- VideoSlotLikeAnimation.displayName = "VideoSlotLikeAnimation";
1397
- function DefaultPlayIcon({ size }) {
1398
- return /* @__PURE__ */ jsx(PlayIcon, { size });
1399
- }
1400
- function DefaultPauseIcon({ size }) {
1401
- return /* @__PURE__ */ jsx(PauseIcon, { size });
1402
- }
1403
- var VideoSlotPlayIndicatorInner = memo(function VideoSlotPlayIndicatorInner2({
1404
- playIcon,
1405
- pauseIcon,
1406
- size = 48,
1407
- duration = 600,
1408
- persistWhenPaused = true,
1409
- className,
1410
- testId,
1411
- lastUserToggleTime,
1412
- isPlaying
1413
- }) {
1414
- const [showIndicator, setShowIndicator] = useState(false);
1415
- const [isPersisting, setIsPersisting] = useState(false);
1416
- const [isAnimatingOut, setIsAnimatingOut] = useState(false);
1417
- const [iconType, setIconType] = useState("play");
1418
- const hideTimeoutRef = useRef(null);
1419
- const lastHandledToggleTimeRef = useRef(0);
1420
- const isPlayingRef = useRef(isPlaying);
1421
- isPlayingRef.current = isPlaying;
1422
- const persistWhenPausedRef = useRef(persistWhenPaused);
1423
- persistWhenPausedRef.current = persistWhenPaused;
1424
- const prevIsPlayingRef = useRef(isPlaying);
1425
- const clearHideTimeout = () => {
1426
- if (hideTimeoutRef.current) {
1427
- clearTimeout(hideTimeoutRef.current);
1428
- hideTimeoutRef.current = null;
1429
- }
1430
- };
1431
- useEffect(() => {
1432
- const wasPlaying = prevIsPlayingRef.current;
1433
- prevIsPlayingRef.current = isPlaying;
1434
- if (!wasPlaying && isPlaying && isPersisting) {
1435
- const timeSinceLastToggle = Date.now() - lastUserToggleTime;
1436
- const isAutoResume = timeSinceLastToggle > 100;
1437
- if (isAutoResume) {
1438
- if (hideTimeoutRef.current) {
1439
- clearTimeout(hideTimeoutRef.current);
1440
- hideTimeoutRef.current = null;
1441
- }
1442
- setShowIndicator(false);
1443
- setIsPersisting(false);
1444
- setIsAnimatingOut(false);
1445
- }
1446
- }
1447
- }, [isPlaying, isPersisting, lastUserToggleTime]);
1448
- useEffect(() => {
1449
- if (lastUserToggleTime === 0 || lastUserToggleTime === lastHandledToggleTimeRef.current) {
1450
- return;
1451
- }
1452
- lastHandledToggleTimeRef.current = lastUserToggleTime;
1453
- if (isPersisting && isPlaying) {
1454
- setIconType("pause");
1455
- setIsPersisting(false);
1456
- setIsAnimatingOut(true);
1457
- clearHideTimeout();
1458
- hideTimeoutRef.current = setTimeout(() => {
1459
- setShowIndicator(false);
1460
- setIsAnimatingOut(false);
1461
- hideTimeoutRef.current = null;
1462
- }, duration);
1463
- return;
1464
- }
1465
- setShowIndicator(true);
1466
- setIsPersisting(false);
1467
- setIsAnimatingOut(false);
1468
- setIconType("play");
1469
- clearHideTimeout();
1470
- hideTimeoutRef.current = setTimeout(() => {
1471
- const currentIsPlaying = isPlayingRef.current;
1472
- const shouldPersist = persistWhenPausedRef.current;
1473
- if (shouldPersist && !currentIsPlaying) {
1474
- setIsPersisting(true);
1475
- return;
1476
- }
1477
- setShowIndicator(false);
1478
- hideTimeoutRef.current = null;
1479
- }, duration);
1480
- return clearHideTimeout;
1481
- }, [lastUserToggleTime, duration, isPersisting, isPlaying]);
1482
- if (!showIndicator && !isPersisting) return null;
1483
- const iconToShow = iconType === "pause" ? pauseIcon ?? /* @__PURE__ */ jsx(DefaultPauseIcon, { size }) : playIcon ?? /* @__PURE__ */ jsx(DefaultPlayIcon, { size });
1484
- const indicatorClass = [
1485
- "sv-video-slot__play-indicator",
1486
- showIndicator && "sv-video-slot__play-indicator--visible",
1487
- isPersisting && "sv-video-slot__play-indicator--persist",
1488
- isAnimatingOut && "sv-video-slot__play-indicator--animating-out",
1489
- className
1490
- ].filter(Boolean).join(" ");
1491
- return /* @__PURE__ */ jsx(
1492
- "div",
1493
- {
1494
- className: indicatorClass,
1495
- style: { "--sv-indicator-duration": `${duration}ms` },
1496
- "aria-hidden": "true",
1497
- "data-testid": testId,
1498
- children: /* @__PURE__ */ jsx("span", { className: "sv-video-slot__play-indicator-icon", children: iconToShow })
1499
- }
1500
- );
1501
- });
1502
- VideoSlotPlayIndicatorInner.displayName = "VideoSlotPlayIndicatorInner";
1503
- function VideoSlotPlayIndicator(props) {
1504
- const context = useOptionalVideoSlotContext();
1505
- if (!context) return null;
1506
- const lastUserToggleTime = context.lastUserToggleTime ?? 0;
1507
- const isPlaying = context.playerState?.isPlaying ?? false;
1508
- return /* @__PURE__ */ jsx(
1509
- VideoSlotPlayIndicatorInner,
1510
- {
1511
- ...props,
1512
- lastUserToggleTime,
1513
- isPlaying
1514
- }
1515
- );
1516
- }
1517
- VideoSlotPlayIndicator.displayName = "VideoSlotPlayIndicator";
1518
-
1519
- export { LOADING_ATTR, LOADING_DATASET_KEY, PLAYER_STATE_ATTR, PLAYER_STATE_DATASET_KEY, SLOT_ACTIVE_ATTR, SLOT_ACTIVE_CLASS, SLOT_ACTIVE_DATASET_KEY, SLOT_CLASS, SLOT_ERROR_CLASS, SLOT_LOADING_CLASS, SLOT_PAUSED_CLASS, SLOT_PLAYING_CLASS, VIDEO_ID_ATTR, VIDEO_ID_DATASET_KEY, VIDEO_SLOT_CSS, VIDEO_SLOT_LIKE_ANIMATION_CSS, VideoSlotContext, VideoSlotHeadless, VideoSlotLikeAnimation, VideoSlotOverlay, VideoSlotPlayIndicator, VideoSlotPoster, VideoSlotSkeleton, Z_INDEX, Z_INDEX_CSS_VARS, useDoubleTap, useOptionalVideoSlotContext, useVideoSlotContext, useVideoSlotIsActive, useVideoSlotIsPlaying, useVideoSlotPlayer, useVideoSlotResource, useVideoSlotVideo };