highsoft-ui 1.0.165 → 1.0.166
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/CLAUDE.md
CHANGED
|
@@ -563,14 +563,14 @@ Sutro-inspired media player with a brand-accent scrubber, floating volume, auto-
|
|
|
563
563
|
transcript={<HistoryTranscript />}
|
|
564
564
|
/>
|
|
565
565
|
```
|
|
566
|
-
- **`src`** (or **`sources`**): self-hosted URL, or a YouTube/Vimeo link (auto-detected)
|
|
566
|
+
- **`src`** (or **`sources`**): self-hosted URL, or a YouTube/Vimeo link (auto-detected). Privacy-enhanced `youtube-nocookie.com` URLs (what the Highcharts docs use for GDPR) are detected as YouTube and the embed keeps the nocookie host rather than downgrading to `youtube.com`.
|
|
567
567
|
- **`poster`, `muted`, `autoPlay`, `loop`, `playsInline`, `preload`**: standard video props (`autoPlay` needs `muted`)
|
|
568
568
|
- **`aspectRatio`** (default `'16 / 9'`), **`radius`** (default `16`)
|
|
569
569
|
- **`accentGradient`**: scrubber/volume fill — defaults to the brand color; accepts any CSS color or gradient
|
|
570
570
|
- **`tracks`**: WebVTT text tracks (`{ src, srclang, label, kind?, default? }`) for self-hosted video. Adds a CC toggle that cycles `captions`/`subtitles` tracks; `descriptions` tracks are loaded for assistive tech but skipped by the toggle. Ignored for YouTube/Vimeo — provider embeds render their own captions.
|
|
571
571
|
- **`transcript`** (+ **`transcriptTitle`**, default `'Transcript'`): any React content, docked as a panel below the player and toggled from the control bar
|
|
572
572
|
- Keyboard: space/`k` play, `f` fullscreen, `m` mute, `c` captions, `t` transcript, arrows seek; forwards a ref to the underlying `<video>`
|
|
573
|
-
- **Accessibility:** labelled `role="region"` (with an `sr-only` name); scrubber + volume are `role="slider"` with `aria-valuemin/max/now/valuetext`; mute exposes `aria-pressed`; decorative layers are `aria-hidden`; controls respect `prefers-reduced-motion`.
|
|
573
|
+
- **Accessibility:** labelled `role="region"` (with an `sr-only` name); scrubber + volume are `role="slider"` with `aria-valuemin/max/now/valuetext`; mute exposes `aria-pressed`; decorative layers are `aria-hidden`; controls respect `prefers-reduced-motion`. A nocookie source gets the same custom control bar as any YouTube source, so keyboard shortcuts, ARIA and focus handling are unchanged; only the embed host differs.
|
|
574
574
|
- Captions satisfy WCAG 1.2.2 and the transcript 1.2.3 / 1.2.8. The CC button exposes `aria-pressed` and the transcript button `aria-expanded` + `aria-controls`; the panel is a labelled `role="region"`. Active toggles are marked by *both* a background wash and an accent-tinted glyph, so the state never rests on color alone.
|
|
575
575
|
- **Cue styling is deliberately left to the browser.** Users' OS/browser caption preferences (macOS Accessibility → Captions, `chrome://settings/captions`) are user-origin styles that beat author CSS at any priority — authoring a cue background/box either doubles up with or fights that layer. Only `font-family`/`line-height` are set. Cue *position* is ours: the overlay animates up while the chrome is visible and back down when it idles (WebKit/Blink via the text-track container pseudo, Firefox via `VTTCue.line`).
|
|
576
576
|
- The collapsed transcript panel is `inert`, so it stays out of the tab order and the AT tree; its reveal animates on the motion tokens and collapses to an instant toggle under `prefers-reduced-motion`.
|
|
@@ -2,40 +2,43 @@ import { useCallback as e, useEffect as t, useRef as n, useState as r } from "re
|
|
|
2
2
|
//#region lib/components/VideoPlayer/useMediaEngine.ts
|
|
3
3
|
var i = (e) => Math.max(0, Math.min(1, e));
|
|
4
4
|
function a(e, t) {
|
|
5
|
-
return !e || t ? "file" : /(?:youtube
|
|
5
|
+
return !e || t ? "file" : /(?:youtube(?:-nocookie)?\.com|youtu\.be)/i.test(e) ? "youtube" : /vimeo\.com/i.test(e) ? "vimeo" : "file";
|
|
6
6
|
}
|
|
7
7
|
function o(e) {
|
|
8
|
+
return e && /youtube-nocookie\.com/i.test(e) ? "https://www.youtube-nocookie.com" : void 0;
|
|
9
|
+
}
|
|
10
|
+
function s(e) {
|
|
8
11
|
let t = e.match(/(?:youtu\.be\/|[?&]v=|\/embed\/|\/shorts\/)([\w-]{11})/);
|
|
9
12
|
return t ? t[1] : e;
|
|
10
13
|
}
|
|
11
|
-
function
|
|
14
|
+
function c(e) {
|
|
12
15
|
let t = e.match(/vimeo\.com\/(?:video\/)?(\d+)/);
|
|
13
16
|
return t ? t[1] : e;
|
|
14
17
|
}
|
|
15
|
-
function
|
|
16
|
-
if (e === "youtube" && t) return `https://i.ytimg.com/vi/${
|
|
18
|
+
function l(e, t) {
|
|
19
|
+
if (e === "youtube" && t) return `https://i.ytimg.com/vi/${s(t)}/hqdefault.jpg`;
|
|
17
20
|
}
|
|
18
|
-
var
|
|
19
|
-
function
|
|
21
|
+
var u = null;
|
|
22
|
+
function d() {
|
|
20
23
|
let e = window;
|
|
21
|
-
return e.YT?.Player ? Promise.resolve(e.YT) :
|
|
24
|
+
return e.YT?.Player ? Promise.resolve(e.YT) : u || (u = new Promise((t) => {
|
|
22
25
|
let n = e.onYouTubeIframeAPIReady;
|
|
23
26
|
e.onYouTubeIframeAPIReady = () => {
|
|
24
27
|
n?.(), t(e.YT);
|
|
25
28
|
};
|
|
26
29
|
let r = document.createElement("script");
|
|
27
30
|
r.src = "https://www.youtube.com/iframe_api", document.head.appendChild(r);
|
|
28
|
-
}),
|
|
31
|
+
}), u);
|
|
29
32
|
}
|
|
30
|
-
var
|
|
31
|
-
function
|
|
33
|
+
var f = null;
|
|
34
|
+
function p() {
|
|
32
35
|
let e = window;
|
|
33
|
-
return e.Vimeo?.Player ? Promise.resolve(e.Vimeo) :
|
|
36
|
+
return e.Vimeo?.Player ? Promise.resolve(e.Vimeo) : f || (f = new Promise((e, t) => {
|
|
34
37
|
let n = document.createElement("script");
|
|
35
38
|
n.src = "https://player.vimeo.com/api/player.js", n.onload = () => e(window.Vimeo), n.onerror = t, document.head.appendChild(n);
|
|
36
|
-
}),
|
|
39
|
+
}), f);
|
|
37
40
|
}
|
|
38
|
-
var
|
|
41
|
+
var m = (e) => ({
|
|
39
42
|
ready: !1,
|
|
40
43
|
playing: !1,
|
|
41
44
|
ended: !1,
|
|
@@ -45,34 +48,34 @@ var p = (e) => ({
|
|
|
45
48
|
volume: +!e,
|
|
46
49
|
muted: e
|
|
47
50
|
});
|
|
48
|
-
function
|
|
49
|
-
let [
|
|
50
|
-
e.volume != null && e.volume > 0 && (
|
|
51
|
+
function h({ provider: a, src: l, muted: u, autoPlay: f, loop: h }) {
|
|
52
|
+
let [g, _] = r(() => m(u)), v = n(1), y = e((e) => {
|
|
53
|
+
e.volume != null && e.volume > 0 && (v.current = e.volume), _((t) => ({
|
|
51
54
|
...t,
|
|
52
55
|
...e
|
|
53
56
|
}));
|
|
54
|
-
}, []),
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
muted:
|
|
58
|
-
autoPlay:
|
|
59
|
-
loop:
|
|
57
|
+
}, []), b = n(g);
|
|
58
|
+
b.current = g;
|
|
59
|
+
let x = n({
|
|
60
|
+
muted: u,
|
|
61
|
+
autoPlay: f,
|
|
62
|
+
loop: h
|
|
60
63
|
});
|
|
61
|
-
|
|
62
|
-
muted:
|
|
63
|
-
autoPlay:
|
|
64
|
-
loop:
|
|
64
|
+
x.current = {
|
|
65
|
+
muted: u,
|
|
66
|
+
autoPlay: f,
|
|
67
|
+
loop: h
|
|
65
68
|
};
|
|
66
|
-
let
|
|
67
|
-
C.current = e;
|
|
68
|
-
}, []), E = e((e) => {
|
|
69
|
+
let S = n(!1), C = n(null), w = n(null), T = n(null), E = e((e) => {
|
|
69
70
|
w.current = e;
|
|
71
|
+
}, []), D = e((e) => {
|
|
72
|
+
T.current = e;
|
|
70
73
|
}, []);
|
|
71
74
|
return t(() => {
|
|
72
75
|
if (a !== "file") return;
|
|
73
|
-
let e =
|
|
76
|
+
let e = w.current;
|
|
74
77
|
if (!e) return;
|
|
75
|
-
|
|
78
|
+
C.current = {
|
|
76
79
|
play: () => void e.play().catch(() => {}),
|
|
77
80
|
pause: () => e.pause(),
|
|
78
81
|
seek: (t) => {
|
|
@@ -85,41 +88,42 @@ function m({ provider: a, src: c, muted: l, autoPlay: d, loop: m }) {
|
|
|
85
88
|
e.muted = t, !t && e.volume === 0 && (e.volume = 1);
|
|
86
89
|
}
|
|
87
90
|
};
|
|
88
|
-
let t = () =>
|
|
91
|
+
let t = () => y({
|
|
89
92
|
playing: !0,
|
|
90
93
|
ended: !1
|
|
91
|
-
}), n = () =>
|
|
94
|
+
}), n = () => y({ playing: !1 }), r = () => y({
|
|
92
95
|
playing: !1,
|
|
93
96
|
ended: !0
|
|
94
97
|
}), o = () => {
|
|
95
98
|
let t = e.buffered;
|
|
96
|
-
|
|
97
|
-
...
|
|
99
|
+
y({
|
|
100
|
+
...S.current ? {} : { currentTime: e.currentTime },
|
|
98
101
|
buffered: t.length && e.duration ? i(t.end(t.length - 1) / e.duration) : 0
|
|
99
102
|
});
|
|
100
|
-
}, s = () =>
|
|
103
|
+
}, s = () => y({ duration: Number.isFinite(e.duration) ? e.duration : 0 }), c = () => y({
|
|
101
104
|
volume: e.muted ? 0 : e.volume,
|
|
102
105
|
muted: e.muted || e.volume === 0
|
|
103
106
|
});
|
|
104
|
-
return e.addEventListener("play", t), e.addEventListener("pause", n), e.addEventListener("ended", r), e.addEventListener("timeupdate", o), e.addEventListener("progress", o), e.addEventListener("durationchange", s), e.addEventListener("loadedmetadata", s), e.addEventListener("volumechange", c),
|
|
107
|
+
return e.addEventListener("play", t), e.addEventListener("pause", n), e.addEventListener("ended", r), e.addEventListener("timeupdate", o), e.addEventListener("progress", o), e.addEventListener("durationchange", s), e.addEventListener("loadedmetadata", s), e.addEventListener("volumechange", c), y({
|
|
105
108
|
ready: !0,
|
|
106
109
|
muted: e.muted,
|
|
107
110
|
volume: e.muted ? 0 : e.volume
|
|
108
111
|
}), () => {
|
|
109
|
-
e.removeEventListener("play", t), e.removeEventListener("pause", n), e.removeEventListener("ended", r), e.removeEventListener("timeupdate", o), e.removeEventListener("progress", o), e.removeEventListener("durationchange", s), e.removeEventListener("loadedmetadata", s), e.removeEventListener("volumechange", c),
|
|
112
|
+
e.removeEventListener("play", t), e.removeEventListener("pause", n), e.removeEventListener("ended", r), e.removeEventListener("timeupdate", o), e.removeEventListener("progress", o), e.removeEventListener("durationchange", s), e.removeEventListener("loadedmetadata", s), e.removeEventListener("volumechange", c), C.current = null;
|
|
110
113
|
};
|
|
111
|
-
}, [a,
|
|
112
|
-
if (a !== "youtube" || !
|
|
113
|
-
let e =
|
|
114
|
+
}, [a, y]), t(() => {
|
|
115
|
+
if (a !== "youtube" || !l) return;
|
|
116
|
+
let e = T.current;
|
|
114
117
|
if (!e) return;
|
|
115
|
-
let t = null, n, r = !1, { muted: i, autoPlay:
|
|
116
|
-
return
|
|
118
|
+
let t = null, n, r = !1, { muted: i, autoPlay: c, loop: u } = x.current, f = s(l), p = o(l);
|
|
119
|
+
return d().then((a) => {
|
|
117
120
|
if (r) return;
|
|
118
121
|
let o = document.createElement("div");
|
|
119
122
|
e.appendChild(o), t = new a.Player(o, {
|
|
120
|
-
videoId:
|
|
123
|
+
videoId: f,
|
|
121
124
|
width: "100%",
|
|
122
125
|
height: "100%",
|
|
126
|
+
...p ? { host: p } : {},
|
|
123
127
|
playerVars: {
|
|
124
128
|
controls: 0,
|
|
125
129
|
modestbranding: 1,
|
|
@@ -127,15 +131,15 @@ function m({ provider: a, src: c, muted: l, autoPlay: d, loop: m }) {
|
|
|
127
131
|
playsinline: 1,
|
|
128
132
|
iv_load_policy: 3,
|
|
129
133
|
fs: 0,
|
|
130
|
-
autoplay: +!!
|
|
134
|
+
autoplay: +!!c,
|
|
131
135
|
mute: +!!i,
|
|
132
|
-
loop: +!!
|
|
133
|
-
...
|
|
136
|
+
loop: +!!u,
|
|
137
|
+
...u ? { playlist: f } : {}
|
|
134
138
|
},
|
|
135
139
|
events: {
|
|
136
140
|
onReady: (e) => {
|
|
137
141
|
let r = e.target;
|
|
138
|
-
i && r.mute(),
|
|
142
|
+
i && r.mute(), C.current = {
|
|
139
143
|
play: () => r.playVideo(),
|
|
140
144
|
pause: () => r.pauseVideo(),
|
|
141
145
|
seek: (e) => r.seekTo(e, !0),
|
|
@@ -143,14 +147,14 @@ function m({ provider: a, src: c, muted: l, autoPlay: d, loop: m }) {
|
|
|
143
147
|
e === 0 ? r.mute() : (r.unMute(), r.setVolume(e * 100));
|
|
144
148
|
},
|
|
145
149
|
setMuted: (e) => e ? r.mute() : r.unMute()
|
|
146
|
-
},
|
|
150
|
+
}, y({
|
|
147
151
|
ready: !0,
|
|
148
152
|
duration: r.getDuration() || 0,
|
|
149
153
|
muted: r.isMuted(),
|
|
150
154
|
volume: r.isMuted() ? 0 : r.getVolume() / 100
|
|
151
155
|
}), n = window.setInterval(() => {
|
|
152
|
-
t &&
|
|
153
|
-
...
|
|
156
|
+
t && y({
|
|
157
|
+
...S.current ? {} : { currentTime: t.getCurrentTime() || 0 },
|
|
154
158
|
duration: t.getDuration() || 0,
|
|
155
159
|
buffered: t.getVideoLoadedFraction() || 0,
|
|
156
160
|
muted: t.isMuted(),
|
|
@@ -159,10 +163,10 @@ function m({ provider: a, src: c, muted: l, autoPlay: d, loop: m }) {
|
|
|
159
163
|
}, 250);
|
|
160
164
|
},
|
|
161
165
|
onStateChange: (e) => {
|
|
162
|
-
e.data === 1 ?
|
|
166
|
+
e.data === 1 ? y({
|
|
163
167
|
playing: !0,
|
|
164
168
|
ended: !1
|
|
165
|
-
}) : e.data === 2 ?
|
|
169
|
+
}) : e.data === 2 ? y({ playing: !1 }) : e.data === 0 && y({
|
|
166
170
|
playing: !1,
|
|
167
171
|
ended: !0
|
|
168
172
|
});
|
|
@@ -170,7 +174,7 @@ function m({ provider: a, src: c, muted: l, autoPlay: d, loop: m }) {
|
|
|
170
174
|
}
|
|
171
175
|
});
|
|
172
176
|
}), () => {
|
|
173
|
-
r = !0, window.clearInterval(n),
|
|
177
|
+
r = !0, window.clearInterval(n), C.current = null;
|
|
174
178
|
try {
|
|
175
179
|
t?.destroy();
|
|
176
180
|
} catch {}
|
|
@@ -178,27 +182,27 @@ function m({ provider: a, src: c, muted: l, autoPlay: d, loop: m }) {
|
|
|
178
182
|
};
|
|
179
183
|
}, [
|
|
180
184
|
a,
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
l,
|
|
186
|
+
y
|
|
183
187
|
]), t(() => {
|
|
184
|
-
if (a !== "vimeo" || !
|
|
185
|
-
let e =
|
|
188
|
+
if (a !== "vimeo" || !l) return;
|
|
189
|
+
let e = T.current;
|
|
186
190
|
if (!e) return;
|
|
187
|
-
let t = null, n = !1, { muted: r, autoPlay: o, loop:
|
|
188
|
-
return
|
|
191
|
+
let t = null, n = !1, { muted: r, autoPlay: o, loop: s } = x.current;
|
|
192
|
+
return p().then((a) => {
|
|
189
193
|
if (n) return;
|
|
190
194
|
t = new a.Player(e, {
|
|
191
|
-
id:
|
|
195
|
+
id: c(l),
|
|
192
196
|
controls: !1,
|
|
193
197
|
autoplay: o,
|
|
194
198
|
muted: r,
|
|
195
|
-
loop:
|
|
199
|
+
loop: s,
|
|
196
200
|
playsinline: !0,
|
|
197
201
|
responsive: !1,
|
|
198
202
|
width: e.clientWidth || void 0
|
|
199
203
|
});
|
|
200
204
|
let u = t;
|
|
201
|
-
|
|
205
|
+
C.current = {
|
|
202
206
|
play: () => void u.play().catch(() => {}),
|
|
203
207
|
pause: () => void u.pause().catch(() => {}),
|
|
204
208
|
seek: (e) => void u.setCurrentTime(e).catch(() => {}),
|
|
@@ -207,29 +211,29 @@ function m({ provider: a, src: c, muted: l, autoPlay: d, loop: m }) {
|
|
|
207
211
|
},
|
|
208
212
|
setMuted: (e) => void u.setMuted(e)
|
|
209
213
|
}, u.ready().then(async () => {
|
|
210
|
-
|
|
214
|
+
y({
|
|
211
215
|
ready: !0,
|
|
212
216
|
duration: await u.getDuration().catch(() => 0),
|
|
213
217
|
muted: r,
|
|
214
218
|
volume: +!r
|
|
215
219
|
});
|
|
216
|
-
}), u.on("play", () =>
|
|
220
|
+
}), u.on("play", () => y({
|
|
217
221
|
playing: !0,
|
|
218
222
|
ended: !1
|
|
219
|
-
})), u.on("pause", () =>
|
|
223
|
+
})), u.on("pause", () => y({ playing: !1 })), u.on("ended", () => y({
|
|
220
224
|
playing: !1,
|
|
221
225
|
ended: !0
|
|
222
226
|
})), u.on("timeupdate", (e) => {
|
|
223
|
-
|
|
224
|
-
...
|
|
227
|
+
y({
|
|
228
|
+
...S.current ? {} : { currentTime: e.seconds },
|
|
225
229
|
duration: e.duration
|
|
226
230
|
});
|
|
227
|
-
}), u.on("progress", (e) =>
|
|
231
|
+
}), u.on("progress", (e) => y({ buffered: i(e.percent) })), u.on("volumechange", (e) => y({
|
|
228
232
|
volume: e.volume,
|
|
229
233
|
muted: e.volume === 0
|
|
230
234
|
}));
|
|
231
235
|
}), () => {
|
|
232
|
-
n = !0,
|
|
236
|
+
n = !0, C.current = null;
|
|
233
237
|
try {
|
|
234
238
|
t?.destroy();
|
|
235
239
|
} catch {}
|
|
@@ -237,42 +241,42 @@ function m({ provider: a, src: c, muted: l, autoPlay: d, loop: m }) {
|
|
|
237
241
|
};
|
|
238
242
|
}, [
|
|
239
243
|
a,
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
l,
|
|
245
|
+
y
|
|
242
246
|
]), {
|
|
243
|
-
state:
|
|
244
|
-
videoRef:
|
|
245
|
-
mountRef:
|
|
247
|
+
state: g,
|
|
248
|
+
videoRef: E,
|
|
249
|
+
mountRef: D,
|
|
246
250
|
toggle: e(() => {
|
|
247
|
-
|
|
251
|
+
b.current.playing ? C.current?.pause() : C.current?.play();
|
|
248
252
|
}, []),
|
|
249
253
|
seekFraction: e((e) => {
|
|
250
|
-
let t =
|
|
254
|
+
let t = b.current.duration;
|
|
251
255
|
if (!t) return;
|
|
252
256
|
let n = i(e) * t;
|
|
253
|
-
|
|
254
|
-
}, [
|
|
257
|
+
y({ currentTime: n }), C.current?.seek(n);
|
|
258
|
+
}, [y]),
|
|
255
259
|
seekBy: e((e) => {
|
|
256
|
-
let { currentTime: t, duration: n } =
|
|
260
|
+
let { currentTime: t, duration: n } = b.current;
|
|
257
261
|
if (!n) return;
|
|
258
262
|
let r = Math.max(0, Math.min(n, t + e));
|
|
259
|
-
|
|
260
|
-
}, [
|
|
263
|
+
y({ currentTime: r }), C.current?.seek(r);
|
|
264
|
+
}, [y]),
|
|
261
265
|
setVolume: e((e) => {
|
|
262
266
|
let t = i(e);
|
|
263
|
-
t > 0 && (
|
|
267
|
+
t > 0 && (v.current = t), C.current?.setVolume(t);
|
|
264
268
|
}, []),
|
|
265
269
|
toggleMute: e(() => {
|
|
266
|
-
let e =
|
|
267
|
-
t && e.volume === 0 &&
|
|
270
|
+
let e = b.current, t = e.muted || e.volume === 0;
|
|
271
|
+
t && e.volume === 0 && C.current?.setVolume(v.current), C.current?.setMuted(!t);
|
|
268
272
|
}, []),
|
|
269
273
|
beginSeek: e(() => {
|
|
270
|
-
|
|
274
|
+
S.current = !0;
|
|
271
275
|
}, []),
|
|
272
276
|
endSeek: e(() => {
|
|
273
|
-
|
|
277
|
+
S.current = !1;
|
|
274
278
|
}, [])
|
|
275
279
|
};
|
|
276
280
|
}
|
|
277
281
|
//#endregion
|
|
278
|
-
export { a as detectProvider,
|
|
282
|
+
export { a as detectProvider, l as providerPosterUrl, h as useMediaEngine, o as youtubeEmbedHost };
|
|
@@ -13,6 +13,12 @@ export interface MediaState {
|
|
|
13
13
|
muted: boolean;
|
|
14
14
|
}
|
|
15
15
|
export declare function detectProvider(src?: string, hasSources?: boolean): MediaProvider;
|
|
16
|
+
/**
|
|
17
|
+
* Sources served from youtube-nocookie.com opted into YouTube's
|
|
18
|
+
* privacy-enhanced mode; keep that domain for the embed rather than
|
|
19
|
+
* downgrading to youtube.com.
|
|
20
|
+
*/
|
|
21
|
+
export declare function youtubeEmbedHost(src?: string): string | undefined;
|
|
16
22
|
/**
|
|
17
23
|
* An unbranded still frame for a provider's start screen, used to mask the
|
|
18
24
|
* embed's own (branded) poster before playback begins. YouTube exposes a
|