@xhub-short/ui 0.1.0-beta.9 → 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 (74) 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-RMLTPW5S.js → chunk-CAWE42LH.js} +4 -3
  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-FNXTPQ6L.js → chunk-IC2KUU4V.js} +31 -1340
  17. package/dist/chunk-IWSBYOSS.js +91 -0
  18. package/dist/chunk-MFJS65C5.js +368 -0
  19. package/dist/{chunk-AC2IFAJR.js → chunk-NJXIYSDZ.js} +12 -1
  20. package/dist/{chunk-CL6BS7GB.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-AQHD6LPS.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 +1 -1
  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 +40 -10
  64. package/dist/index.js +23 -13
  65. package/package.json +8 -4
  66. package/dist/CommentSheet.css-BeCrEaUG.d.ts +0 -221
  67. package/dist/chunk-2FSDVYER.js +0 -737
  68. package/dist/chunk-ECR42RKK.js +0 -571
  69. package/dist/chunk-KWHMZ6H5.js +0 -562
  70. package/dist/chunk-SZXFH334.js +0 -204
  71. package/dist/chunk-UNV3NWN6.js +0 -148
  72. package/dist/chunk-WCRDTBCZ.js +0 -357
  73. package/dist/chunk-XDIH66C4.js +0 -1519
  74. package/dist/components/VideoFeed/index.js +0 -1
@@ -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 };
@@ -0,0 +1,368 @@
1
+ import { PauseIcon, PlayIcon } from './chunk-ANCP53F3.js';
2
+ import { createContext, memo, useState, useRef, useEffect, useCallback, useContext } from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ var DEFAULT_DOUBLE_TAP_DELAY = 300;
6
+ var DEFAULT_LONG_PRESS_DELAY = 500;
7
+ var TAP_DEBOUNCE_MS = 50;
8
+ var DEFAULT_MOVEMENT_THRESHOLD = 10;
9
+ function useDoubleTap({
10
+ containerRef,
11
+ onSingleTap,
12
+ onDoubleTap,
13
+ onLongPress,
14
+ doubleTapDelay = DEFAULT_DOUBLE_TAP_DELAY,
15
+ longPressDelay = DEFAULT_LONG_PRESS_DELAY,
16
+ disabled = false,
17
+ movementThreshold = DEFAULT_MOVEMENT_THRESHOLD
18
+ }) {
19
+ const lastTapTimeRef = useRef(0);
20
+ const lastDoubleTapTimeRef = useRef(0);
21
+ const tapTimeoutRef = useRef(null);
22
+ const longPressTimeoutRef = useRef(null);
23
+ const longPressTriggeredRef = useRef(false);
24
+ const lastPositionRef = useRef({ x: 50, y: 50 });
25
+ const lastExecutionTimeRef = useRef(0);
26
+ const pointerStartRef = useRef(null);
27
+ const cumulativeMovementRef = useRef(0);
28
+ useEffect(() => {
29
+ return () => {
30
+ if (tapTimeoutRef.current) {
31
+ clearTimeout(tapTimeoutRef.current);
32
+ tapTimeoutRef.current = null;
33
+ }
34
+ if (longPressTimeoutRef.current) {
35
+ clearTimeout(longPressTimeoutRef.current);
36
+ longPressTimeoutRef.current = null;
37
+ }
38
+ };
39
+ }, []);
40
+ const calculatePosition = useCallback(
41
+ (clientX, clientY) => {
42
+ const container = containerRef.current;
43
+ if (!container) return { x: 50, y: 50 };
44
+ const rect = container.getBoundingClientRect();
45
+ const x = (clientX - rect.left) / rect.width * 100;
46
+ const y = (clientY - rect.top) / rect.height * 100;
47
+ return {
48
+ x: Math.max(0, Math.min(100, x)),
49
+ y: Math.max(0, Math.min(100, y))
50
+ };
51
+ },
52
+ [containerRef]
53
+ );
54
+ const handleTapCore = useCallback(() => {
55
+ if (disabled) return;
56
+ const now = Date.now();
57
+ const timeSinceLastTap = now - lastTapTimeRef.current;
58
+ if (tapTimeoutRef.current) {
59
+ clearTimeout(tapTimeoutRef.current);
60
+ tapTimeoutRef.current = null;
61
+ }
62
+ if (timeSinceLastTap < doubleTapDelay) {
63
+ lastDoubleTapTimeRef.current = now;
64
+ lastTapTimeRef.current = 0;
65
+ onDoubleTap?.(lastPositionRef.current);
66
+ } else {
67
+ lastTapTimeRef.current = now;
68
+ tapTimeoutRef.current = setTimeout(() => {
69
+ tapTimeoutRef.current = null;
70
+ const timeSinceDoubleTap = Date.now() - lastDoubleTapTimeRef.current;
71
+ if (lastDoubleTapTimeRef.current > 0 && timeSinceDoubleTap < doubleTapDelay * 1.5) {
72
+ return;
73
+ }
74
+ if (lastTapTimeRef.current !== now) {
75
+ return;
76
+ }
77
+ onSingleTap?.();
78
+ }, doubleTapDelay);
79
+ }
80
+ }, [disabled, doubleTapDelay, onSingleTap, onDoubleTap]);
81
+ const isSwipe = useCallback(
82
+ (clientX, clientY) => {
83
+ if (cumulativeMovementRef.current > movementThreshold * 2) {
84
+ return true;
85
+ }
86
+ const start = pointerStartRef.current;
87
+ if (!start) return false;
88
+ const dx = Math.abs(clientX - start.x);
89
+ const dy = Math.abs(clientY - start.y);
90
+ return dx > movementThreshold || dy > movementThreshold;
91
+ },
92
+ [movementThreshold]
93
+ );
94
+ const executeTapWithDebounce = useCallback(
95
+ (clientX, clientY) => {
96
+ if (disabled) return;
97
+ if (isSwipe(clientX, clientY)) {
98
+ pointerStartRef.current = null;
99
+ return;
100
+ }
101
+ const now = Date.now();
102
+ if (now - lastExecutionTimeRef.current < TAP_DEBOUNCE_MS) {
103
+ return;
104
+ }
105
+ lastExecutionTimeRef.current = now;
106
+ lastPositionRef.current = calculatePosition(clientX, clientY);
107
+ handleTapCore();
108
+ pointerStartRef.current = null;
109
+ },
110
+ [disabled, calculatePosition, handleTapCore, isSwipe]
111
+ );
112
+ const cancelLongPress = useCallback(() => {
113
+ if (longPressTimeoutRef.current) {
114
+ clearTimeout(longPressTimeoutRef.current);
115
+ longPressTimeoutRef.current = null;
116
+ }
117
+ }, []);
118
+ const handlePointerDown = useCallback(
119
+ (e) => {
120
+ if (e.button !== 0) return;
121
+ pointerStartRef.current = { x: e.clientX, y: e.clientY };
122
+ cumulativeMovementRef.current = 0;
123
+ longPressTriggeredRef.current = false;
124
+ if (onLongPress && !disabled) {
125
+ cancelLongPress();
126
+ const position = calculatePosition(e.clientX, e.clientY);
127
+ longPressTimeoutRef.current = setTimeout(() => {
128
+ longPressTimeoutRef.current = null;
129
+ if (cumulativeMovementRef.current <= movementThreshold * 2) {
130
+ longPressTriggeredRef.current = true;
131
+ onLongPress(position);
132
+ }
133
+ }, longPressDelay);
134
+ }
135
+ },
136
+ [onLongPress, disabled, longPressDelay, movementThreshold, calculatePosition, cancelLongPress]
137
+ );
138
+ const handlePointerMove = useCallback(
139
+ (e) => {
140
+ if (!pointerStartRef.current) return;
141
+ cumulativeMovementRef.current += Math.abs(e.movementX) + Math.abs(e.movementY);
142
+ if (cumulativeMovementRef.current > movementThreshold * 2) {
143
+ cancelLongPress();
144
+ }
145
+ },
146
+ [movementThreshold, cancelLongPress]
147
+ );
148
+ const handlePointerUp = useCallback(
149
+ (e) => {
150
+ if (e.button !== 0) return;
151
+ cancelLongPress();
152
+ if (longPressTriggeredRef.current) {
153
+ longPressTriggeredRef.current = false;
154
+ pointerStartRef.current = null;
155
+ return;
156
+ }
157
+ executeTapWithDebounce(e.clientX, e.clientY);
158
+ },
159
+ [executeTapWithDebounce, cancelLongPress]
160
+ );
161
+ const handleClick = useCallback(
162
+ (e) => {
163
+ if (e.detail > 0) {
164
+ executeTapWithDebounce(e.clientX, e.clientY);
165
+ }
166
+ },
167
+ [executeTapWithDebounce]
168
+ );
169
+ return {
170
+ handlers: {
171
+ onPointerDown: handlePointerDown,
172
+ onPointerMove: handlePointerMove,
173
+ onPointerUp: handlePointerUp,
174
+ onClick: handleClick
175
+ },
176
+ lastPosition: lastPositionRef
177
+ };
178
+ }
179
+ function invariantContextError(hookName, componentName) {
180
+ const baseMessage = `${hookName} must be used within a VideoSlot component.`;
181
+ if (process.env.NODE_ENV !== "production") {
182
+ const debugInfo = [
183
+ "",
184
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
185
+ "\u274C VideoSlot Context Error",
186
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
187
+ "",
188
+ `Hook: ${hookName}`,
189
+ "",
190
+ "",
191
+ "Possible causes:",
192
+ " 1. You used a VideoSlot compound component outside of <VideoSlot>",
193
+ " 2. You used the headless component without SDK wiring",
194
+ " 3. The component tree was rendered in isolation (e.g., Storybook)",
195
+ "",
196
+ "Solutions:",
197
+ " 1. Wrap with <VideoSlot> or <VideoSlotHeadless>",
198
+ " 2. Use useOptionalVideoSlotContext() if context is optional",
199
+ " 3. Provide mock context in tests via <VideoSlotContext.Provider>",
200
+ "",
201
+ "Example:",
202
+ " // Correct usage",
203
+ " <VideoSlot video={video}>",
204
+ " <VideoSlotPoster /> \u2190 These need context",
205
+ " <VideoSlotOverlay />",
206
+ " </VideoSlot>",
207
+ "",
208
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"
209
+ ].filter(Boolean).join("\n");
210
+ throw new Error(baseMessage + debugInfo);
211
+ }
212
+ throw new Error(baseMessage);
213
+ }
214
+ var VideoSlotContext = createContext(null);
215
+ VideoSlotContext.displayName = "VideoSlotContext";
216
+ function useVideoSlotContext() {
217
+ const context = useContext(VideoSlotContext);
218
+ if (!context) {
219
+ invariantContextError("useVideoSlotContext");
220
+ }
221
+ return context;
222
+ }
223
+ function useOptionalVideoSlotContext() {
224
+ return useContext(VideoSlotContext);
225
+ }
226
+ function useVideoSlotVideo() {
227
+ const { video } = useVideoSlotContext();
228
+ return video;
229
+ }
230
+ function useVideoSlotPlayer() {
231
+ const { playerState, playerControls } = useVideoSlotContext();
232
+ return { state: playerState, controls: playerControls };
233
+ }
234
+ function useVideoSlotResource() {
235
+ const { resourceState } = useVideoSlotContext();
236
+ return resourceState;
237
+ }
238
+ function useVideoSlotIsActive() {
239
+ const { resourceState } = useVideoSlotContext();
240
+ return resourceState.isActive;
241
+ }
242
+ function useVideoSlotIsPlaying() {
243
+ const { playerState } = useVideoSlotContext();
244
+ return playerState.isPlaying;
245
+ }
246
+ function DefaultPlayIcon({ size }) {
247
+ return /* @__PURE__ */ jsx(PlayIcon, { size });
248
+ }
249
+ function DefaultPauseIcon({ size }) {
250
+ return /* @__PURE__ */ jsx(PauseIcon, { size });
251
+ }
252
+ var VideoSlotPlayIndicatorInner = memo(function VideoSlotPlayIndicatorInner2({
253
+ playIcon,
254
+ pauseIcon,
255
+ size = 48,
256
+ duration = 600,
257
+ persistWhenPaused = true,
258
+ className,
259
+ testId,
260
+ lastUserToggleTime,
261
+ isPlaying
262
+ }) {
263
+ const [showIndicator, setShowIndicator] = useState(false);
264
+ const [isPersisting, setIsPersisting] = useState(false);
265
+ const [isAnimatingOut, setIsAnimatingOut] = useState(false);
266
+ const [iconType, setIconType] = useState("play");
267
+ const hideTimeoutRef = useRef(null);
268
+ const lastHandledToggleTimeRef = useRef(0);
269
+ const isPlayingRef = useRef(isPlaying);
270
+ isPlayingRef.current = isPlaying;
271
+ const persistWhenPausedRef = useRef(persistWhenPaused);
272
+ persistWhenPausedRef.current = persistWhenPaused;
273
+ const prevIsPlayingRef = useRef(isPlaying);
274
+ const clearHideTimeout = () => {
275
+ if (hideTimeoutRef.current) {
276
+ clearTimeout(hideTimeoutRef.current);
277
+ hideTimeoutRef.current = null;
278
+ }
279
+ };
280
+ useEffect(() => {
281
+ const wasPlaying = prevIsPlayingRef.current;
282
+ prevIsPlayingRef.current = isPlaying;
283
+ if (!wasPlaying && isPlaying && isPersisting) {
284
+ const timeSinceLastToggle = Date.now() - lastUserToggleTime;
285
+ const isAutoResume = timeSinceLastToggle > 100;
286
+ if (isAutoResume) {
287
+ if (hideTimeoutRef.current) {
288
+ clearTimeout(hideTimeoutRef.current);
289
+ hideTimeoutRef.current = null;
290
+ }
291
+ setShowIndicator(false);
292
+ setIsPersisting(false);
293
+ setIsAnimatingOut(false);
294
+ }
295
+ }
296
+ }, [isPlaying, isPersisting, lastUserToggleTime]);
297
+ useEffect(() => {
298
+ if (lastUserToggleTime === 0 || lastUserToggleTime === lastHandledToggleTimeRef.current) {
299
+ return;
300
+ }
301
+ lastHandledToggleTimeRef.current = lastUserToggleTime;
302
+ if (isPersisting && isPlaying) {
303
+ setIconType("pause");
304
+ setIsPersisting(false);
305
+ setIsAnimatingOut(true);
306
+ clearHideTimeout();
307
+ hideTimeoutRef.current = setTimeout(() => {
308
+ setShowIndicator(false);
309
+ setIsAnimatingOut(false);
310
+ hideTimeoutRef.current = null;
311
+ }, duration);
312
+ return;
313
+ }
314
+ setShowIndicator(true);
315
+ setIsPersisting(false);
316
+ setIsAnimatingOut(false);
317
+ setIconType("play");
318
+ clearHideTimeout();
319
+ hideTimeoutRef.current = setTimeout(() => {
320
+ const currentIsPlaying = isPlayingRef.current;
321
+ const shouldPersist = persistWhenPausedRef.current;
322
+ if (shouldPersist && !currentIsPlaying) {
323
+ setIsPersisting(true);
324
+ return;
325
+ }
326
+ setShowIndicator(false);
327
+ hideTimeoutRef.current = null;
328
+ }, duration);
329
+ return clearHideTimeout;
330
+ }, [lastUserToggleTime, duration, isPersisting, isPlaying]);
331
+ if (!showIndicator && !isPersisting) return null;
332
+ const iconToShow = iconType === "pause" ? pauseIcon ?? /* @__PURE__ */ jsx(DefaultPauseIcon, { size }) : playIcon ?? /* @__PURE__ */ jsx(DefaultPlayIcon, { size });
333
+ const indicatorClass = [
334
+ "sv-video-slot__play-indicator",
335
+ showIndicator && "sv-video-slot__play-indicator--visible",
336
+ isPersisting && "sv-video-slot__play-indicator--persist",
337
+ isAnimatingOut && "sv-video-slot__play-indicator--animating-out",
338
+ className
339
+ ].filter(Boolean).join(" ");
340
+ return /* @__PURE__ */ jsx(
341
+ "div",
342
+ {
343
+ className: indicatorClass,
344
+ style: { "--sv-indicator-duration": `${duration}ms` },
345
+ "aria-hidden": "true",
346
+ "data-testid": testId,
347
+ children: /* @__PURE__ */ jsx("span", { className: "sv-video-slot__play-indicator-icon", children: iconToShow })
348
+ }
349
+ );
350
+ });
351
+ VideoSlotPlayIndicatorInner.displayName = "VideoSlotPlayIndicatorInner";
352
+ function VideoSlotPlayIndicator(props) {
353
+ const context = useOptionalVideoSlotContext();
354
+ if (!context) return null;
355
+ const lastUserToggleTime = context.lastUserToggleTime ?? 0;
356
+ const isPlaying = context.playerState?.isPlaying ?? false;
357
+ return /* @__PURE__ */ jsx(
358
+ VideoSlotPlayIndicatorInner,
359
+ {
360
+ ...props,
361
+ lastUserToggleTime,
362
+ isPlaying
363
+ }
364
+ );
365
+ }
366
+ VideoSlotPlayIndicator.displayName = "VideoSlotPlayIndicator";
367
+
368
+ export { VideoSlotContext, VideoSlotPlayIndicator, VideoSlotPlayIndicatorInner, useDoubleTap, useOptionalVideoSlotContext, useVideoSlotContext, useVideoSlotIsActive, useVideoSlotIsPlaying, useVideoSlotPlayer, useVideoSlotResource, useVideoSlotVideo };
@@ -1,5 +1,5 @@
1
1
  import { formatCount } from './chunk-3XPJHUYL.js';
2
- import { injectComponentCSS } from './chunk-RMLTPW5S.js';
2
+ import { injectComponentCSS } from './chunk-CAWE42LH.js';
3
3
  import { clsx } from 'clsx';
4
4
  import { createContext, useInsertionEffect, useState, useRef, useEffect, useCallback, useContext } from 'react';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -18,6 +18,15 @@ var ACTION_BAR_CSS = (
18
18
  align-items: center;
19
19
  gap: var(--sv-action-bar-gap, 12px);
20
20
  padding: var(--sv-action-bar-padding, 8px 0);
21
+
22
+ /* Auto-hide transition */
23
+ transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
24
+ }
25
+
26
+ /* Hidden state for auto-hide */
27
+ .sv-action-bar--hidden {
28
+ opacity: 0;
29
+ pointer-events: none;
21
30
  }
22
31
 
23
32
  /* Horizontal variant */
@@ -439,6 +448,7 @@ function ActionBarHeadlessRoot({
439
448
  children,
440
449
  direction = "vertical",
441
450
  buttonVariant = "default",
451
+ isVisible = true,
442
452
  testId
443
453
  }) {
444
454
  useInsertionEffect(() => {
@@ -451,6 +461,7 @@ function ActionBarHeadlessRoot({
451
461
  const containerClasses = clsx(
452
462
  CSS_PREFIX2,
453
463
  direction === "horizontal" && `${CSS_PREFIX2}--horizontal`,
464
+ !isVisible && `${CSS_PREFIX2}--hidden`,
454
465
  className
455
466
  );
456
467
  const stopPropagation = (e) => {
@@ -1,5 +1,5 @@
1
1
  import { clsx2 } from './chunk-EDWS2IPH.js';
2
- import { injectComponentCSS } from './chunk-RMLTPW5S.js';
2
+ import { injectComponentCSS } from './chunk-CAWE42LH.js';
3
3
  import { memo, useInsertionEffect, useCallback, useMemo, useRef, useState, useEffect } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
5
 
@@ -654,6 +654,7 @@ function useVideoElement(config) {
654
654
  } = config;
655
655
  const videoRef = useRef(null);
656
656
  const hlsRef = useRef(null);
657
+ const prevTimeRef = useRef(0);
657
658
  const [isLoading, setIsLoading] = useState(true);
658
659
  const [isBuffering, setIsBuffering] = useState(false);
659
660
  const [isReady, setIsReady] = useState(false);
@@ -753,7 +754,15 @@ function useVideoElement(config) {
753
754
  onEnded?.();
754
755
  };
755
756
  const handleTimeUpdate = () => {
756
- onTimeUpdate?.(video.currentTime);
757
+ const currentTime = video.currentTime;
758
+ const duration = video.duration;
759
+ const prevTime = prevTimeRef.current;
760
+ if (loop && duration > 0 && prevTime > duration * 0.9 && // Was near end (>90%)
761
+ currentTime < duration * 0.1) {
762
+ onEnded?.();
763
+ }
764
+ prevTimeRef.current = currentTime;
765
+ onTimeUpdate?.(currentTime);
757
766
  };
758
767
  const handleDurationChange = () => {
759
768
  onDurationChange?.(video.duration);
@@ -820,6 +829,7 @@ function useVideoElement(config) {
820
829
  };
821
830
  }, [
822
831
  autoPlay,
832
+ loop,
823
833
  onCanPlay,
824
834
  onPlay,
825
835
  onPlaying,
@@ -0,0 +1,75 @@
1
+ import { clsx } from 'clsx';
2
+ import { memo, useEffect, useCallback } from 'react';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ // src/components/SpeedPicker/SpeedPickerHeadless.tsx
6
+
7
+ // src/components/SpeedPicker/SpeedPicker.css.ts
8
+ var SPEED_PICKER_CSS = `.sv-speed-picker{--sv-speed-picker-bg:rgba(28,28,30,.95);--sv-speed-picker-text:#fff;--sv-speed-picker-text-secondary:rgba(255,255,255,.6);--sv-speed-picker-selected:#007aff;--sv-speed-picker-item-hover:rgba(255,255,255,.1);--sv-speed-picker-check:#007aff;--sv-speed-picker-item-height:48px;--sv-speed-picker-radius:12px;--sv-speed-picker-padding:8px}.sv-speed-picker{padding:var(--sv-speed-picker-padding)}.sv-speed-picker-title{padding:8px 16px;font-size:14px;font-weight:600;color:var(--sv-speed-picker-text-secondary);text-align:center}.sv-speed-picker-options{display:flex;flex-direction:column;gap:2px}.sv-speed-picker-option{display:flex;align-items:center;justify-content:space-between;height:var(--sv-speed-picker-item-height);padding:0 16px;border:0;background:transparent;color:var(--sv-speed-picker-text);font-size:16px;font-weight:normal;cursor:pointer;border-radius:var(--sv-speed-picker-radius);transition:background-color 150ms ease;-webkit-tap-highlight-color:transparent}.sv-speed-picker-option:hover{background:var(--sv-speed-picker-item-hover)}.sv-speed-picker-option[data-selected="true"]{color:var(--sv-speed-picker-selected);font-weight:500}.sv-speed-picker-option-label{flex:1}.sv-speed-picker-option-check{width:20px;height:20px;color:var(--sv-speed-picker-check);opacity:0;transition:opacity 150ms ease}.sv-speed-picker-option[data-selected="true"] .sv-speed-picker-option-check{opacity:1}`;
9
+ var cssInjected = false;
10
+ function injectCSS() {
11
+ if (cssInjected || typeof document === "undefined") return;
12
+ const styleId = "sv-speed-picker-styles";
13
+ if (!document.getElementById(styleId)) {
14
+ const style = document.createElement("style");
15
+ style.id = styleId;
16
+ style.textContent = SPEED_PICKER_CSS;
17
+ document.head.appendChild(style);
18
+ }
19
+ cssInjected = true;
20
+ }
21
+ var DEFAULT_SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
22
+ function CheckIcon() {
23
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) });
24
+ }
25
+ function defaultFormatLabel(speed) {
26
+ if (speed === 1) return "Normal";
27
+ return `${speed}x`;
28
+ }
29
+ function SpeedPickerHeadlessComponent({
30
+ speed,
31
+ onSpeedChange,
32
+ speeds = DEFAULT_SPEEDS,
33
+ title = "Playback Speed",
34
+ className,
35
+ formatLabel = defaultFormatLabel,
36
+ onClose,
37
+ autoClose = true
38
+ }) {
39
+ useEffect(() => {
40
+ injectCSS();
41
+ }, []);
42
+ const handleSelect = useCallback(
43
+ (selectedSpeed) => {
44
+ onSpeedChange(selectedSpeed);
45
+ if (autoClose && onClose) {
46
+ onClose();
47
+ }
48
+ },
49
+ [onSpeedChange, autoClose, onClose]
50
+ );
51
+ return /* @__PURE__ */ jsxs("div", { className: clsx("sv-speed-picker", className), "aria-label": title, children: [
52
+ title && /* @__PURE__ */ jsx("div", { className: "sv-speed-picker-title", children: title }),
53
+ /* @__PURE__ */ jsx("div", { className: "sv-speed-picker-options", children: speeds.map((s) => {
54
+ const isSelected = Math.abs(s - speed) < 0.01;
55
+ return /* @__PURE__ */ jsxs(
56
+ "button",
57
+ {
58
+ type: "button",
59
+ className: "sv-speed-picker-option",
60
+ "data-selected": isSelected,
61
+ onClick: () => handleSelect(s),
62
+ "aria-selected": isSelected,
63
+ children: [
64
+ /* @__PURE__ */ jsx("span", { className: "sv-speed-picker-option-label", children: formatLabel(s) }),
65
+ /* @__PURE__ */ jsx("span", { className: "sv-speed-picker-option-check", children: /* @__PURE__ */ jsx(CheckIcon, {}) })
66
+ ]
67
+ },
68
+ s
69
+ );
70
+ }) })
71
+ ] });
72
+ }
73
+ var SpeedPickerHeadless = memo(SpeedPickerHeadlessComponent);
74
+
75
+ export { DEFAULT_SPEEDS, SPEED_PICKER_CSS, SpeedPickerHeadless };