@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.
- package/dist/CommentSheet.css-DuBy01rU.d.ts +219 -0
- package/dist/VideoSlotPlayIndicator-DPs8Xt5C.d.ts +51 -0
- package/dist/chunk-3OB3OVYR.js +349 -0
- package/dist/chunk-4RIMQOBR.js +58 -0
- package/dist/chunk-5MKYDI4X.js +1 -0
- package/dist/chunk-5Y43HNRG.js +296 -0
- package/dist/chunk-7WXAQHJI.js +350 -0
- package/dist/chunk-BADA7OLG.js +564 -0
- package/dist/chunk-BEJAJFV6.js +191 -0
- package/dist/chunk-BNI7CYRI.js +334 -0
- package/dist/{chunk-RMLTPW5S.js → chunk-CAWE42LH.js} +4 -3
- package/dist/chunk-DGKMO3AE.js +717 -0
- package/dist/chunk-DR7KR7OT.js +103 -0
- package/dist/chunk-GSNIZ6DF.js +605 -0
- package/dist/chunk-HXQPEZRG.js +105 -0
- package/dist/{chunk-FNXTPQ6L.js → chunk-IC2KUU4V.js} +31 -1340
- package/dist/chunk-IWSBYOSS.js +91 -0
- package/dist/chunk-MFJS65C5.js +368 -0
- package/dist/{chunk-AC2IFAJR.js → chunk-NJXIYSDZ.js} +12 -1
- package/dist/{chunk-CL6BS7GB.js → chunk-OM4L7RE5.js} +12 -2
- package/dist/chunk-QCRRF76W.js +75 -0
- package/dist/chunk-QUEJHA24.js +508 -0
- package/dist/chunk-UYBQTE4M.js +337 -0
- package/dist/chunk-VJ744W5N.js +603 -0
- package/dist/chunk-VXW7AOGM.js +285 -0
- package/dist/{chunk-AQHD6LPS.js → chunk-YB7AXTX7.js} +1 -1
- package/dist/components/ActionBar/index.d.ts +7 -2
- package/dist/components/ActionBar/index.js +1 -1
- package/dist/components/AdvanceMenu/index.d.ts +80 -0
- package/dist/components/AdvanceMenu/index.js +1 -0
- package/dist/components/ArticleSlot/index.d.ts +213 -0
- package/dist/components/ArticleSlot/index.js +1 -0
- package/dist/components/AuthorInfo/index.js +1 -1
- package/dist/components/BottomSheet/index.d.ts +87 -0
- package/dist/components/BottomSheet/index.js +1 -0
- package/dist/components/CleanModeOverlay/index.d.ts +60 -0
- package/dist/components/CleanModeOverlay/index.js +1 -0
- package/dist/components/CommentSheet/index.d.ts +1 -1
- package/dist/components/CommentSheet/index.js +1 -1
- package/dist/components/DetailView/index.d.ts +314 -0
- package/dist/components/DetailView/index.js +1 -0
- package/dist/components/ErrorBoundary/index.js +1 -1
- package/dist/components/{VideoFeed → Feed}/index.d.ts +64 -45
- package/dist/components/Feed/index.js +1 -0
- package/dist/components/ImageCarousel/index.d.ts +50 -0
- package/dist/components/ImageCarousel/index.js +1 -0
- package/dist/components/Playlist/index.d.ts +117 -0
- package/dist/components/Playlist/index.js +1 -0
- package/dist/components/ProgressBar/index.js +1 -1
- package/dist/components/QualityPicker/index.d.ts +35 -0
- package/dist/components/QualityPicker/index.js +1 -0
- package/dist/components/ReportSheet/index.d.ts +74 -0
- package/dist/components/ReportSheet/index.js +1 -0
- package/dist/components/Skeleton/index.js +1 -1
- package/dist/components/SpeedPicker/index.d.ts +32 -0
- package/dist/components/SpeedPicker/index.js +1 -0
- package/dist/components/VideoInfo/index.d.ts +7 -3
- package/dist/components/VideoInfo/index.js +1 -1
- package/dist/components/VideoPlayer/index.js +1 -1
- package/dist/components/VideoSlot/index.d.ts +31 -68
- package/dist/components/VideoSlot/index.js +2 -1
- package/dist/components/VirtualSlider/index.js +1 -1
- package/dist/index.d.ts +40 -10
- package/dist/index.js +23 -13
- package/package.json +8 -4
- package/dist/CommentSheet.css-BeCrEaUG.d.ts +0 -221
- package/dist/chunk-2FSDVYER.js +0 -737
- package/dist/chunk-ECR42RKK.js +0 -571
- package/dist/chunk-KWHMZ6H5.js +0 -562
- package/dist/chunk-SZXFH334.js +0 -204
- package/dist/chunk-UNV3NWN6.js +0 -148
- package/dist/chunk-WCRDTBCZ.js +0 -357
- package/dist/chunk-XDIH66C4.js +0 -1519
- 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-
|
|
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-
|
|
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
|
-
|
|
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 };
|