@xhub-short/ui 0.1.0-beta.0

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.
@@ -0,0 +1,45 @@
1
+ // src/utils/injectComponentCSS.ts
2
+ var injectedStyles = /* @__PURE__ */ new Set();
3
+ var STYLE_ID_PREFIX = "sv-styles-";
4
+ function injectComponentCSS(componentId, css) {
5
+ if (typeof document === "undefined") {
6
+ return () => {
7
+ };
8
+ }
9
+ if (injectedStyles.has(componentId)) {
10
+ return () => {
11
+ };
12
+ }
13
+ const styleId = `${STYLE_ID_PREFIX}${componentId}`;
14
+ const existingStyle = document.getElementById(styleId);
15
+ if (existingStyle) {
16
+ injectedStyles.add(componentId);
17
+ return () => {
18
+ };
19
+ }
20
+ const style = document.createElement("style");
21
+ style.id = styleId;
22
+ style.setAttribute("data-sv-component", componentId);
23
+ style.textContent = css;
24
+ document.head.appendChild(style);
25
+ injectedStyles.add(componentId);
26
+ return () => {
27
+ removeComponentCSS(componentId);
28
+ };
29
+ }
30
+ function removeComponentCSS(componentId) {
31
+ if (typeof document === "undefined") {
32
+ return;
33
+ }
34
+ const styleId = `${STYLE_ID_PREFIX}${componentId}`;
35
+ const style = document.getElementById(styleId);
36
+ if (style) {
37
+ style.remove();
38
+ injectedStyles.delete(componentId);
39
+ }
40
+ }
41
+ function isComponentCSSInjected(componentId) {
42
+ return injectedStyles.has(componentId);
43
+ }
44
+
45
+ export { injectComponentCSS, isComponentCSSInjected, removeComponentCSS };
@@ -0,0 +1,98 @@
1
+ // src/utils/lazyGesture.ts
2
+ var gestureModule = null;
3
+ var loadPromise = null;
4
+ async function loadGesture() {
5
+ if (gestureModule) {
6
+ return gestureModule;
7
+ }
8
+ if (loadPromise) {
9
+ return loadPromise;
10
+ }
11
+ loadPromise = import('./use-gesture-react.esm-3SV4QLEJ.js').then((mod) => {
12
+ gestureModule = mod;
13
+ return mod;
14
+ }).catch((error) => {
15
+ loadPromise = null;
16
+ throw error;
17
+ });
18
+ return loadPromise;
19
+ }
20
+ function isGestureLoaded() {
21
+ return gestureModule !== null;
22
+ }
23
+ function preloadGesture() {
24
+ loadGesture().catch(() => {
25
+ });
26
+ }
27
+
28
+ // src/utils/formatCount.ts
29
+ var DEFAULT_SUFFIXES = {
30
+ thousand: "K",
31
+ million: "M",
32
+ billion: "B"
33
+ };
34
+ function formatCount(count, options = {}) {
35
+ const { decimals = 1, trailingZeros = false, suffixes = {} } = options;
36
+ const { thousand, million, billion } = { ...DEFAULT_SUFFIXES, ...suffixes };
37
+ if (!Number.isFinite(count)) {
38
+ return "0";
39
+ }
40
+ const absCount = Math.abs(count);
41
+ const sign = count < 0 ? "-" : "";
42
+ if (absCount < 1e3) {
43
+ return `${sign}${Math.floor(absCount)}`;
44
+ }
45
+ let value;
46
+ let suffix;
47
+ if (absCount >= 1e9) {
48
+ value = absCount / 1e9;
49
+ suffix = billion;
50
+ } else if (absCount >= 1e6) {
51
+ value = absCount / 1e6;
52
+ suffix = million;
53
+ } else {
54
+ value = absCount / 1e3;
55
+ suffix = thousand;
56
+ }
57
+ const formatted = value.toFixed(decimals);
58
+ const result = trailingZeros ? formatted : formatted.replace(/\.?0+$/, "");
59
+ return `${sign}${result}${suffix}`;
60
+ }
61
+ function formatCountWithSeparators(count, locale = "en-US") {
62
+ return count.toLocaleString(locale);
63
+ }
64
+ function parseFormattedCount(formatted) {
65
+ const cleaned = formatted.trim().toUpperCase();
66
+ const match = cleaned.match(/^(-?[\d.]+)\s*([KMB])?$/);
67
+ if (!match) {
68
+ return 0;
69
+ }
70
+ const [, numStr, suffix] = match;
71
+ const num = Number.parseFloat(numStr ?? "0");
72
+ if (Number.isNaN(num)) {
73
+ return 0;
74
+ }
75
+ switch (suffix) {
76
+ case "K":
77
+ return num * 1e3;
78
+ case "M":
79
+ return num * 1e6;
80
+ case "B":
81
+ return num * 1e9;
82
+ default:
83
+ return num;
84
+ }
85
+ }
86
+ function cn(...inputs) {
87
+ return inputs.filter(Boolean).map((input) => {
88
+ if (typeof input === "string") {
89
+ return input;
90
+ }
91
+ if (typeof input === "object" && input !== null) {
92
+ return Object.entries(input).filter(([, value]) => value).map(([key]) => key).join(" ");
93
+ }
94
+ return "";
95
+ }).join(" ").trim();
96
+ }
97
+
98
+ export { cn, formatCount, formatCountWithSeparators, isGestureLoaded, loadGesture, parseFormattedCount, preloadGesture };
@@ -0,0 +1,530 @@
1
+ import { cn } from './chunk-WKX2WBVO.js';
2
+ import { injectComponentCSS } from './chunk-UXMA4KJZ.js';
3
+ import { useInsertionEffect, useMemo, useEffect, useCallback, useRef, useState } from 'react';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ // src/components/ProgressBar/ProgressBar.css.ts
7
+ var PROGRESS_BAR_CSS = `
8
+ /* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
9
+ * ProgressBar Container
10
+ * \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
11
+
12
+ .sv-progress-bar {
13
+ /* Layout */
14
+ position: relative;
15
+ width: 100%;
16
+
17
+ /* Interactive area - larger than visual for easier touch */
18
+ height: var(--sv-progress-bar-touch-height, 24px);
19
+ display: flex;
20
+ align-items: center;
21
+
22
+ /* Cursor */
23
+ cursor: pointer;
24
+
25
+ /* Prevent text selection during seek */
26
+ user-select: none;
27
+ -webkit-user-select: none;
28
+ touch-action: none;
29
+ }
30
+
31
+ .sv-progress-bar--disabled {
32
+ cursor: default;
33
+ pointer-events: none;
34
+ }
35
+
36
+ /* \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\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
37
+ * Track Wrapper (contains track + tooltip)
38
+ * \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\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 */
39
+
40
+ .sv-progress-bar__track-wrapper {
41
+ position: relative;
42
+ flex: 1;
43
+ display: flex;
44
+ align-items: center;
45
+ }
46
+
47
+ /* \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\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
48
+ * Track (background bar)
49
+ * \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\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 */
50
+
51
+ .sv-progress-bar__track {
52
+ position: relative;
53
+ width: 100%;
54
+ height: var(--sv-progress-bar-height, 3px);
55
+
56
+ /* Visual */
57
+ background: var(--sv-progress-bar-track-bg, rgba(255, 255, 255, 0.2));
58
+ border-radius: var(--sv-progress-bar-radius, 1.5px);
59
+ overflow: hidden;
60
+ }
61
+
62
+ /* Expand track on hover/active for better visibility */
63
+ .sv-progress-bar:hover .sv-progress-bar__track,
64
+ .sv-progress-bar--seeking .sv-progress-bar__track {
65
+ height: var(--sv-progress-bar-height-active, 5px);
66
+ }
67
+
68
+ /* Disable transition during seeking for immediate feedback */
69
+ .sv-progress-bar--seeking .sv-progress-bar__fill,
70
+ .sv-progress-bar--seeking .sv-progress-bar__handle {
71
+ transition: none;
72
+ }
73
+
74
+ /* \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\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
75
+ * Buffered Fill (shows buffered progress)
76
+ * \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\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 */
77
+
78
+ .sv-progress-bar__buffered {
79
+ position: absolute;
80
+ top: 0;
81
+ left: 0;
82
+ height: 100%;
83
+
84
+ /* Visual */
85
+ background: var(--sv-progress-bar-buffered-bg, rgba(255, 255, 255, 0.3));
86
+ border-radius: inherit;
87
+
88
+ /* Performance: Use scaleX for GPU acceleration */
89
+ transform-origin: left center;
90
+ transform: scaleX(0);
91
+
92
+ /* Smooth transition for buffered updates */
93
+ transition: transform 0.3s ease-out;
94
+ }
95
+
96
+ /* \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\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
97
+ * Progress Fill (shows playback progress)
98
+ * IMPORTANT: Designed for Direct DOM Update (ADR 005)
99
+ * - Uses scaleX() for 60fps smooth animation
100
+ * - Wired component will update transform directly via DOM manipulation
101
+ * \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\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 */
102
+
103
+ .sv-progress-bar__fill {
104
+ position: absolute;
105
+ top: 0;
106
+ left: 0;
107
+ height: 100%;
108
+ width: 100%;
109
+
110
+ /* Visual */
111
+ background: var(--sv-progress-bar-fill-bg, var(--sv-color-accent, #fe2c55));
112
+ border-radius: inherit;
113
+
114
+ /* Performance: Use scaleX for GPU acceleration */
115
+ transform-origin: left center;
116
+ transform: scaleX(0);
117
+
118
+ /* Smooth transition for headless mode (timeupdate ~250ms intervals)
119
+ * For RAF-based Direct DOM Update (wired mode), this has minimal impact
120
+ * because transform updates every 16ms anyway */
121
+ transition: var(--sv-progress-bar-fill-transition, transform 0.25s linear);
122
+ will-change: transform;
123
+ }
124
+
125
+ /* \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\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
126
+ * Seek Handle (thumb indicator)
127
+ * \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\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 */
128
+
129
+ .sv-progress-bar__handle {
130
+ position: absolute;
131
+ top: 50%;
132
+
133
+ /* Size */
134
+ width: var(--sv-progress-bar-handle-size, 12px);
135
+ height: var(--sv-progress-bar-handle-size, 12px);
136
+
137
+ /* Positioning - will be set via left % */
138
+ left: 0;
139
+ transform: translate(-50%, -50%) scale(0);
140
+
141
+ /* Visual */
142
+ background: var(--sv-progress-bar-handle-bg, #fff);
143
+ border-radius: 50%;
144
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
145
+
146
+ /* Animation - smooth position + show/hide */
147
+ transition: left 0.25s linear, transform 0.15s ease-out;
148
+ }
149
+
150
+ /* Show handle on hover/seeking */
151
+ .sv-progress-bar:hover .sv-progress-bar__handle,
152
+ .sv-progress-bar--seeking .sv-progress-bar__handle {
153
+ transform: translate(-50%, -50%) scale(1);
154
+ }
155
+
156
+ /* \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\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
157
+ * Time Display (optional, shows current/duration)
158
+ * \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\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 */
159
+
160
+ .sv-progress-bar__time {
161
+ display: flex;
162
+ justify-content: space-between;
163
+ padding: var(--sv-spacing-xs, 4px) 0;
164
+
165
+ font-family: var(--sv-font-family, 'Urbanist', sans-serif);
166
+ font-size: var(--sv-font-size-xs, 11px);
167
+ font-variant-numeric: tabular-nums;
168
+ color: var(--sv-text-secondary, rgba(255, 255, 255, 0.7));
169
+ }
170
+
171
+ .sv-progress-bar__time-current,
172
+ .sv-progress-bar__time-duration {
173
+ /* Ensure consistent width for changing numbers */
174
+ min-width: 2.5em;
175
+ }
176
+
177
+ .sv-progress-bar__time-current {
178
+ text-align: left;
179
+ }
180
+
181
+ .sv-progress-bar__time-duration {
182
+ text-align: right;
183
+ }
184
+
185
+ /* \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\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
186
+ * Seek Tooltip (shows time at cursor position during hover)
187
+ * \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\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 */
188
+
189
+ .sv-progress-bar__tooltip {
190
+ position: absolute;
191
+ /* Position above the container (not inside track to avoid overflow:hidden) */
192
+ top: 0;
193
+ left: 0;
194
+ transform: translate(-50%, -100%);
195
+ margin-top: -8px;
196
+
197
+ /* Visual */
198
+ padding: 4px 8px;
199
+ background: var(--sv-bg-overlay, rgba(0, 0, 0, 0.8));
200
+ border-radius: var(--sv-border-radius-sm, 4px);
201
+
202
+ /* Typography */
203
+ font-family: var(--sv-font-family, 'Urbanist', sans-serif);
204
+ font-size: var(--sv-font-size-xs, 11px);
205
+ font-variant-numeric: tabular-nums;
206
+ color: var(--sv-text-primary, #fff);
207
+ white-space: nowrap;
208
+
209
+ /* Hidden by default */
210
+ opacity: 0;
211
+ visibility: hidden;
212
+ transition: opacity 0.15s ease, visibility 0.15s ease;
213
+
214
+ /* Prevent tooltip from capturing pointer events */
215
+ pointer-events: none;
216
+
217
+ /* Ensure tooltip is above other elements */
218
+ z-index: 10;
219
+ }
220
+
221
+ .sv-progress-bar:hover .sv-progress-bar__tooltip,
222
+ .sv-progress-bar--seeking .sv-progress-bar__tooltip {
223
+ opacity: 1;
224
+ visibility: visible;
225
+ }
226
+
227
+ /* \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\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
228
+ * Minimal Variant (for use inside VideoSlot overlay)
229
+ * \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\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 */
230
+
231
+ .sv-progress-bar--minimal {
232
+ height: auto;
233
+ }
234
+
235
+ .sv-progress-bar--minimal .sv-progress-bar__track {
236
+ height: var(--sv-progress-bar-height-minimal, 2px);
237
+ }
238
+
239
+ .sv-progress-bar--minimal:hover .sv-progress-bar__track {
240
+ height: var(--sv-progress-bar-height-minimal, 2px);
241
+ }
242
+
243
+ .sv-progress-bar--minimal .sv-progress-bar__handle {
244
+ display: none;
245
+ }
246
+
247
+ /* \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\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
248
+ * Reduced Motion (Accessibility)
249
+ * \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\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 */
250
+
251
+ @media (prefers-reduced-motion: reduce) {
252
+ .sv-progress-bar__fill,
253
+ .sv-progress-bar__buffered,
254
+ .sv-progress-bar__handle,
255
+ .sv-progress-bar__tooltip {
256
+ transition: none;
257
+ }
258
+ }
259
+ `;
260
+ var CSS_PREFIX = "sv-progress-bar";
261
+ function formatTime(seconds) {
262
+ if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
263
+ const h = Math.floor(seconds / 3600);
264
+ const m = Math.floor(seconds % 3600 / 60);
265
+ const s = Math.floor(seconds % 60);
266
+ if (h > 0) {
267
+ return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
268
+ }
269
+ return `${m}:${s.toString().padStart(2, "0")}`;
270
+ }
271
+ function calculateProgress(currentTime, duration) {
272
+ if (!duration || duration <= 0) return 0;
273
+ return Math.min(100, Math.max(0, currentTime / duration * 100));
274
+ }
275
+ function calculateTimeFromPosition(clientX, rect, duration) {
276
+ const x = clientX - rect.left;
277
+ const percentage = Math.min(1, Math.max(0, x / rect.width));
278
+ return percentage * duration;
279
+ }
280
+ function useProgressBarRefs({
281
+ externalFillRef,
282
+ externalBufferedRef,
283
+ externalHandleRef,
284
+ externalCurrentTimeRef,
285
+ externalDurationRef
286
+ }) {
287
+ const containerRef = useRef(null);
288
+ const internalFillRef = useRef(null);
289
+ const internalBufferedRef = useRef(null);
290
+ const internalHandleRef = useRef(null);
291
+ const internalCurrentTimeRef = useRef(null);
292
+ const internalDurationRef = useRef(null);
293
+ const tooltipRef = useRef(null);
294
+ return {
295
+ containerRef,
296
+ tooltipRef,
297
+ fillRef: externalFillRef ?? internalFillRef,
298
+ bufferedRef: externalBufferedRef ?? internalBufferedRef,
299
+ handleRef: externalHandleRef ?? internalHandleRef,
300
+ currentTimeRef: externalCurrentTimeRef ?? internalCurrentTimeRef,
301
+ durationRef: externalDurationRef ?? internalDurationRef,
302
+ hasExternalCurrentTimeRef: !!externalCurrentTimeRef,
303
+ hasExternalDurationRef: !!externalDurationRef
304
+ };
305
+ }
306
+ function useSeekHandlers({
307
+ containerRef,
308
+ duration,
309
+ isSeekable,
310
+ onSeek,
311
+ onSeekStart,
312
+ onSeekEnd
313
+ }) {
314
+ const [isSeeking, setIsSeeking] = useState(false);
315
+ const [hoverTime, setHoverTime] = useState(null);
316
+ const [hoverPosition, setHoverPosition] = useState(0);
317
+ const handleSeek = useCallback(
318
+ (clientX) => {
319
+ if (!isSeekable || !containerRef.current) return;
320
+ const rect = containerRef.current.getBoundingClientRect();
321
+ const time = calculateTimeFromPosition(clientX, rect, duration);
322
+ onSeek?.(time);
323
+ },
324
+ [isSeekable, duration, onSeek, containerRef]
325
+ );
326
+ const handlePointerDown = useCallback(
327
+ (e) => {
328
+ if (!isSeekable) return;
329
+ e.preventDefault();
330
+ e.stopPropagation();
331
+ if (typeof e.currentTarget.setPointerCapture === "function") {
332
+ e.currentTarget.setPointerCapture(e.pointerId);
333
+ }
334
+ setIsSeeking(true);
335
+ onSeekStart?.();
336
+ handleSeek(e.clientX);
337
+ },
338
+ [isSeekable, onSeekStart, handleSeek]
339
+ );
340
+ const handlePointerMove = useCallback(
341
+ (e) => {
342
+ if (!containerRef.current) return;
343
+ const rect = containerRef.current.getBoundingClientRect();
344
+ const time = calculateTimeFromPosition(e.clientX, rect, duration);
345
+ const position = (e.clientX - rect.left) / rect.width * 100;
346
+ setHoverTime(time);
347
+ setHoverPosition(Math.min(100, Math.max(0, position)));
348
+ if (isSeeking) handleSeek(e.clientX);
349
+ },
350
+ [duration, isSeeking, handleSeek, containerRef]
351
+ );
352
+ const handlePointerUp = useCallback(
353
+ (e) => {
354
+ e.stopPropagation();
355
+ if (isSeeking) {
356
+ if (typeof e.currentTarget.releasePointerCapture === "function") {
357
+ e.currentTarget.releasePointerCapture(e.pointerId);
358
+ }
359
+ setIsSeeking(false);
360
+ onSeekEnd?.();
361
+ }
362
+ },
363
+ [isSeeking, onSeekEnd]
364
+ );
365
+ const handlePointerLeave = useCallback(() => {
366
+ setHoverTime(null);
367
+ if (isSeeking) {
368
+ setIsSeeking(false);
369
+ onSeekEnd?.();
370
+ }
371
+ }, [isSeeking, onSeekEnd]);
372
+ return {
373
+ isSeeking,
374
+ hoverTime,
375
+ hoverPosition,
376
+ handlePointerDown,
377
+ handlePointerMove,
378
+ handlePointerUp,
379
+ handlePointerLeave
380
+ };
381
+ }
382
+ function ProgressBarHeadless({
383
+ currentTime,
384
+ duration,
385
+ buffered = 0,
386
+ seekable = false,
387
+ className,
388
+ onSeek,
389
+ onSeekStart,
390
+ onSeekEnd,
391
+ showTime = false,
392
+ showTooltip,
393
+ minimal = false,
394
+ fillRef: externalFillRef,
395
+ bufferedRef: externalBufferedRef,
396
+ handleRef: externalHandleRef,
397
+ currentTimeRef: externalCurrentTimeRef,
398
+ durationRef: externalDurationRef
399
+ }) {
400
+ useInsertionEffect(() => {
401
+ return injectComponentCSS("progress-bar", PROGRESS_BAR_CSS);
402
+ }, []);
403
+ const refs = useProgressBarRefs({
404
+ externalFillRef,
405
+ externalBufferedRef,
406
+ externalHandleRef,
407
+ externalCurrentTimeRef,
408
+ externalDurationRef
409
+ });
410
+ const progress = useMemo(() => calculateProgress(currentTime, duration), [currentTime, duration]);
411
+ const bufferedPercent = useMemo(() => Math.min(100, Math.max(0, buffered * 100)), [buffered]);
412
+ const isSeekable = seekable && !!onSeek && duration > 0;
413
+ const effectiveShowTooltip = showTooltip ?? isSeekable;
414
+ const {
415
+ isSeeking,
416
+ hoverTime,
417
+ hoverPosition,
418
+ handlePointerDown,
419
+ handlePointerMove,
420
+ handlePointerUp,
421
+ handlePointerLeave
422
+ } = useSeekHandlers({
423
+ containerRef: refs.containerRef,
424
+ duration,
425
+ isSeekable,
426
+ onSeek,
427
+ onSeekStart,
428
+ onSeekEnd
429
+ });
430
+ useEffect(() => {
431
+ if (!refs.hasExternalCurrentTimeRef && refs.currentTimeRef.current) {
432
+ refs.currentTimeRef.current.textContent = formatTime(currentTime);
433
+ }
434
+ }, [currentTime, refs]);
435
+ useEffect(() => {
436
+ if (!refs.hasExternalDurationRef && refs.durationRef.current) {
437
+ refs.durationRef.current.textContent = formatTime(duration);
438
+ }
439
+ }, [duration, refs]);
440
+ const stopBubble = useCallback((e) => {
441
+ e.stopPropagation();
442
+ }, []);
443
+ return /* @__PURE__ */ jsxs(
444
+ "div",
445
+ {
446
+ className: cn(
447
+ CSS_PREFIX,
448
+ !isSeekable && `${CSS_PREFIX}--disabled`,
449
+ isSeeking && `${CSS_PREFIX}--seeking`,
450
+ minimal && `${CSS_PREFIX}--minimal`,
451
+ className
452
+ ),
453
+ onClick: stopBubble,
454
+ onKeyDown: stopBubble,
455
+ onPointerDown: stopBubble,
456
+ onPointerUp: stopBubble,
457
+ onTouchStart: stopBubble,
458
+ onTouchEnd: stopBubble,
459
+ children: [
460
+ /* @__PURE__ */ jsxs("div", { className: `${CSS_PREFIX}__track-wrapper`, children: [
461
+ /* @__PURE__ */ jsxs(
462
+ "div",
463
+ {
464
+ ref: refs.containerRef,
465
+ className: `${CSS_PREFIX}__track`,
466
+ onPointerDown: handlePointerDown,
467
+ onPointerMove: handlePointerMove,
468
+ onPointerUp: handlePointerUp,
469
+ onPointerLeave: handlePointerLeave,
470
+ onClick: (e) => e.stopPropagation(),
471
+ onKeyDown: (e) => e.stopPropagation(),
472
+ role: isSeekable ? "slider" : "progressbar",
473
+ "aria-label": "Video progress",
474
+ "aria-valuemin": 0,
475
+ "aria-valuemax": duration || 100,
476
+ "aria-valuenow": currentTime,
477
+ "aria-valuetext": `${formatTime(currentTime)} of ${formatTime(duration)}`,
478
+ tabIndex: isSeekable ? 0 : -1,
479
+ children: [
480
+ /* @__PURE__ */ jsx(
481
+ "div",
482
+ {
483
+ ref: refs.bufferedRef,
484
+ className: `${CSS_PREFIX}__buffered`,
485
+ style: { transform: `scaleX(${bufferedPercent / 100})` },
486
+ "aria-hidden": "true"
487
+ }
488
+ ),
489
+ /* @__PURE__ */ jsx(
490
+ "div",
491
+ {
492
+ ref: refs.fillRef,
493
+ className: `${CSS_PREFIX}__fill`,
494
+ style: { transform: `scaleX(${progress / 100})` },
495
+ "aria-hidden": "true"
496
+ }
497
+ ),
498
+ isSeekable && !minimal && /* @__PURE__ */ jsx(
499
+ "div",
500
+ {
501
+ ref: refs.handleRef,
502
+ className: `${CSS_PREFIX}__handle`,
503
+ style: { left: `${progress}%` },
504
+ "aria-hidden": "true"
505
+ }
506
+ )
507
+ ]
508
+ }
509
+ ),
510
+ effectiveShowTooltip && hoverTime !== null && /* @__PURE__ */ jsx(
511
+ "div",
512
+ {
513
+ ref: refs.tooltipRef,
514
+ className: `${CSS_PREFIX}__tooltip`,
515
+ style: { left: `${hoverPosition}%` },
516
+ "aria-hidden": "true",
517
+ children: formatTime(hoverTime)
518
+ }
519
+ )
520
+ ] }),
521
+ showTime && /* @__PURE__ */ jsxs("div", { className: `${CSS_PREFIX}__time`, children: [
522
+ /* @__PURE__ */ jsx("span", { ref: refs.currentTimeRef, className: `${CSS_PREFIX}__time-current`, children: formatTime(currentTime) }),
523
+ /* @__PURE__ */ jsx("span", { ref: refs.durationRef, className: `${CSS_PREFIX}__time-duration`, children: formatTime(duration) })
524
+ ] })
525
+ ]
526
+ }
527
+ );
528
+ }
529
+
530
+ export { PROGRESS_BAR_CSS, ProgressBarHeadless, calculateProgress, formatTime };