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\.com|youtu\.be)/i.test(e) ? "youtube" : /vimeo\.com/i.test(e) ? "vimeo" : "file";
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 s(e) {
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 c(e, t) {
16
- if (e === "youtube" && t) return `https://i.ytimg.com/vi/${o(t)}/hqdefault.jpg`;
18
+ function l(e, t) {
19
+ if (e === "youtube" && t) return `https://i.ytimg.com/vi/${s(t)}/hqdefault.jpg`;
17
20
  }
18
- var l = null;
19
- function u() {
21
+ var u = null;
22
+ function d() {
20
23
  let e = window;
21
- return e.YT?.Player ? Promise.resolve(e.YT) : l || (l = new Promise((t) => {
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
- }), l);
31
+ }), u);
29
32
  }
30
- var d = null;
31
- function f() {
33
+ var f = null;
34
+ function p() {
32
35
  let e = window;
33
- return e.Vimeo?.Player ? Promise.resolve(e.Vimeo) : d || (d = new Promise((e, t) => {
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
- }), d);
39
+ }), f);
37
40
  }
38
- var p = (e) => ({
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 m({ provider: a, src: c, muted: l, autoPlay: d, loop: m }) {
49
- let [h, g] = r(() => p(l)), _ = n(1), v = e((e) => {
50
- e.volume != null && e.volume > 0 && (_.current = e.volume), g((t) => ({
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
- }, []), y = n(h);
55
- y.current = h;
56
- let b = n({
57
- muted: l,
58
- autoPlay: d,
59
- loop: m
57
+ }, []), b = n(g);
58
+ b.current = g;
59
+ let x = n({
60
+ muted: u,
61
+ autoPlay: f,
62
+ loop: h
60
63
  });
61
- b.current = {
62
- muted: l,
63
- autoPlay: d,
64
- loop: m
64
+ x.current = {
65
+ muted: u,
66
+ autoPlay: f,
67
+ loop: h
65
68
  };
66
- let x = n(!1), S = n(null), C = n(null), w = n(null), T = e((e) => {
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 = C.current;
76
+ let e = w.current;
74
77
  if (!e) return;
75
- S.current = {
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 = () => v({
91
+ let t = () => y({
89
92
  playing: !0,
90
93
  ended: !1
91
- }), n = () => v({ playing: !1 }), r = () => v({
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
- v({
97
- ...x.current ? {} : { currentTime: e.currentTime },
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 = () => v({ duration: Number.isFinite(e.duration) ? e.duration : 0 }), c = () => v({
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), v({
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), S.current = null;
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, v]), t(() => {
112
- if (a !== "youtube" || !c) return;
113
- let e = w.current;
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: s, loop: l } = b.current, d = o(c);
116
- return u().then((a) => {
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: d,
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: +!!s,
134
+ autoplay: +!!c,
131
135
  mute: +!!i,
132
- loop: +!!l,
133
- ...l ? { playlist: d } : {}
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(), S.current = {
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
- }, v({
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 && v({
153
- ...x.current ? {} : { currentTime: t.getCurrentTime() || 0 },
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 ? v({
166
+ e.data === 1 ? y({
163
167
  playing: !0,
164
168
  ended: !1
165
- }) : e.data === 2 ? v({ playing: !1 }) : e.data === 0 && v({
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), S.current = null;
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
- c,
182
- v
185
+ l,
186
+ y
183
187
  ]), t(() => {
184
- if (a !== "vimeo" || !c) return;
185
- let e = w.current;
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: l } = b.current;
188
- return f().then((a) => {
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: s(c),
195
+ id: c(l),
192
196
  controls: !1,
193
197
  autoplay: o,
194
198
  muted: r,
195
- loop: l,
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
- S.current = {
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
- v({
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", () => v({
220
+ }), u.on("play", () => y({
217
221
  playing: !0,
218
222
  ended: !1
219
- })), u.on("pause", () => v({ playing: !1 })), u.on("ended", () => v({
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
- v({
224
- ...x.current ? {} : { currentTime: e.seconds },
227
+ y({
228
+ ...S.current ? {} : { currentTime: e.seconds },
225
229
  duration: e.duration
226
230
  });
227
- }), u.on("progress", (e) => v({ buffered: i(e.percent) })), u.on("volumechange", (e) => v({
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, S.current = null;
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
- c,
241
- v
244
+ l,
245
+ y
242
246
  ]), {
243
- state: h,
244
- videoRef: T,
245
- mountRef: E,
247
+ state: g,
248
+ videoRef: E,
249
+ mountRef: D,
246
250
  toggle: e(() => {
247
- y.current.playing ? S.current?.pause() : S.current?.play();
251
+ b.current.playing ? C.current?.pause() : C.current?.play();
248
252
  }, []),
249
253
  seekFraction: e((e) => {
250
- let t = y.current.duration;
254
+ let t = b.current.duration;
251
255
  if (!t) return;
252
256
  let n = i(e) * t;
253
- v({ currentTime: n }), S.current?.seek(n);
254
- }, [v]),
257
+ y({ currentTime: n }), C.current?.seek(n);
258
+ }, [y]),
255
259
  seekBy: e((e) => {
256
- let { currentTime: t, duration: n } = y.current;
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
- v({ currentTime: r }), S.current?.seek(r);
260
- }, [v]),
263
+ y({ currentTime: r }), C.current?.seek(r);
264
+ }, [y]),
261
265
  setVolume: e((e) => {
262
266
  let t = i(e);
263
- t > 0 && (_.current = t), S.current?.setVolume(t);
267
+ t > 0 && (v.current = t), C.current?.setVolume(t);
264
268
  }, []),
265
269
  toggleMute: e(() => {
266
- let e = y.current, t = e.muted || e.volume === 0;
267
- t && e.volume === 0 && S.current?.setVolume(_.current), S.current?.setMuted(!t);
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
- x.current = !0;
274
+ S.current = !0;
271
275
  }, []),
272
276
  endSeek: e(() => {
273
- x.current = !1;
277
+ S.current = !1;
274
278
  }, [])
275
279
  };
276
280
  }
277
281
  //#endregion
278
- export { a as detectProvider, c as providerPosterUrl, m as useMediaEngine };
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "highsoft-ui",
3
- "version": "1.0.165",
3
+ "version": "1.0.166",
4
4
  "description": "Highsoft UI",
5
5
  "type": "module",
6
6
  "repository": {