@intellectif/lk-react 14.1.0 → 16.0.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +5 -2
  3. package/dist/{WrittenResponse-rRpOr9W1.d.cts → WrittenResponse-C2zw-1jK.d.cts} +1 -1
  4. package/dist/{WrittenResponse-DrzI89Wg.d.ts → WrittenResponse-CTiNpSCC.d.ts} +1 -1
  5. package/dist/{chunk-KIO6QXGT.cjs → chunk-2WM2KSO7.cjs} +25 -25
  6. package/dist/{chunk-KIO6QXGT.cjs.map → chunk-2WM2KSO7.cjs.map} +1 -1
  7. package/dist/{chunk-WC4OD3EE.cjs → chunk-2YZZL5FV.cjs} +13 -13
  8. package/dist/{chunk-WC4OD3EE.cjs.map → chunk-2YZZL5FV.cjs.map} +1 -1
  9. package/dist/chunk-3IHVDQRC.js +2683 -0
  10. package/dist/chunk-3IHVDQRC.js.map +1 -0
  11. package/dist/{chunk-5CP2Z6IS.js → chunk-4AQ4SWJ3.js} +5 -5
  12. package/dist/{chunk-5CP2Z6IS.js.map → chunk-4AQ4SWJ3.js.map} +1 -1
  13. package/dist/{chunk-UDQS5INQ.js → chunk-4X7NFEDB.js} +5 -5
  14. package/dist/{chunk-UDQS5INQ.js.map → chunk-4X7NFEDB.js.map} +1 -1
  15. package/dist/chunk-A2CPCOJA.cjs +2686 -0
  16. package/dist/chunk-A2CPCOJA.cjs.map +1 -0
  17. package/dist/{chunk-U432HTUW.js → chunk-AEUP63CZ.js} +5 -5
  18. package/dist/{chunk-U432HTUW.js.map → chunk-AEUP63CZ.js.map} +1 -1
  19. package/dist/{chunk-MZITNWZ2.cjs → chunk-ALDGRDYD.cjs} +24 -24
  20. package/dist/{chunk-MZITNWZ2.cjs.map → chunk-ALDGRDYD.cjs.map} +1 -1
  21. package/dist/{chunk-YM64MS4G.cjs → chunk-BO64SLHL.cjs} +8 -8
  22. package/dist/{chunk-YM64MS4G.cjs.map → chunk-BO64SLHL.cjs.map} +1 -1
  23. package/dist/{chunk-JG4DWAUJ.cjs → chunk-DMCFARGP.cjs} +13 -13
  24. package/dist/{chunk-JG4DWAUJ.cjs.map → chunk-DMCFARGP.cjs.map} +1 -1
  25. package/dist/{chunk-TUVEJLOT.js → chunk-HJLVWZOM.js} +4 -4
  26. package/dist/{chunk-TUVEJLOT.js.map → chunk-HJLVWZOM.js.map} +1 -1
  27. package/dist/{chunk-B4BCEQNO.js → chunk-ICAYWVNF.js} +3 -3
  28. package/dist/{chunk-B4BCEQNO.js.map → chunk-ICAYWVNF.js.map} +1 -1
  29. package/dist/{chunk-4Y5TELVU.cjs → chunk-JYC2VV3Y.cjs} +12 -12
  30. package/dist/{chunk-4Y5TELVU.cjs.map → chunk-JYC2VV3Y.cjs.map} +1 -1
  31. package/dist/{chunk-D4DQFTA7.cjs → chunk-K63VPFOV.cjs} +10 -2
  32. package/dist/chunk-K63VPFOV.cjs.map +1 -0
  33. package/dist/{chunk-AS2QGND5.js → chunk-NOF6HJDV.js} +4 -4
  34. package/dist/{chunk-AS2QGND5.js.map → chunk-NOF6HJDV.js.map} +1 -1
  35. package/dist/{chunk-UDZWYHDV.js → chunk-OOYKKO3V.js} +11 -11
  36. package/dist/{chunk-UDZWYHDV.js.map → chunk-OOYKKO3V.js.map} +1 -1
  37. package/dist/{chunk-UMDWVMB5.cjs → chunk-OXUUE3HX.cjs} +5 -5
  38. package/dist/{chunk-UMDWVMB5.cjs.map → chunk-OXUUE3HX.cjs.map} +1 -1
  39. package/dist/{chunk-DPEY7DSE.cjs → chunk-P5FGJMMI.cjs} +5 -5
  40. package/dist/{chunk-DPEY7DSE.cjs.map → chunk-P5FGJMMI.cjs.map} +1 -1
  41. package/dist/{chunk-5JCTJFQU.cjs → chunk-QADR2XMQ.cjs} +18 -18
  42. package/dist/{chunk-5JCTJFQU.cjs.map → chunk-QADR2XMQ.cjs.map} +1 -1
  43. package/dist/{chunk-AWQ53DON.js → chunk-QIA5FAAW.js} +89 -2
  44. package/dist/chunk-QIA5FAAW.js.map +1 -0
  45. package/dist/{chunk-PSUKWVKU.cjs → chunk-RJGCT35F.cjs} +13 -13
  46. package/dist/{chunk-PSUKWVKU.cjs.map → chunk-RJGCT35F.cjs.map} +1 -1
  47. package/dist/{chunk-NJAF34GN.js → chunk-T33AJYLW.js} +5 -5
  48. package/dist/{chunk-NJAF34GN.js.map → chunk-T33AJYLW.js.map} +1 -1
  49. package/dist/{chunk-RXS455MR.js → chunk-UHKZXOXD.js} +10 -2
  50. package/dist/chunk-UHKZXOXD.js.map +1 -0
  51. package/dist/{chunk-DB4DLHN6.js → chunk-UIGSH2XB.js} +3 -3
  52. package/dist/{chunk-DB4DLHN6.js.map → chunk-UIGSH2XB.js.map} +1 -1
  53. package/dist/{chunk-VSFHJ7FB.cjs → chunk-UISZWFI7.cjs} +89 -2
  54. package/dist/chunk-UISZWFI7.cjs.map +1 -0
  55. package/dist/{chunk-IDYZ2FA2.js → chunk-UYRQEIB7.js} +5 -5
  56. package/dist/{chunk-IDYZ2FA2.js.map → chunk-UYRQEIB7.js.map} +1 -1
  57. package/dist/{chunk-5JFMXBH7.js → chunk-W32ND3ZR.js} +6 -6
  58. package/dist/{chunk-5JFMXBH7.js.map → chunk-W32ND3ZR.js.map} +1 -1
  59. package/dist/{chunk-RCPW6HVY.js → chunk-WBLRUJ77.js} +10 -10
  60. package/dist/{chunk-RCPW6HVY.js.map → chunk-WBLRUJ77.js.map} +1 -1
  61. package/dist/{chunk-CTW3G3EH.cjs → chunk-WGIMLXRW.cjs} +6 -6
  62. package/dist/{chunk-CTW3G3EH.cjs.map → chunk-WGIMLXRW.cjs.map} +1 -1
  63. package/dist/{chunk-N536RUCZ.cjs → chunk-ZA5QUQ2V.cjs} +13 -13
  64. package/dist/{chunk-N536RUCZ.cjs.map → chunk-ZA5QUQ2V.cjs.map} +1 -1
  65. package/dist/components/ActivityPreview.cjs +12 -12
  66. package/dist/components/ActivityPreview.d.cts +2 -2
  67. package/dist/components/ActivityPreview.d.ts +2 -2
  68. package/dist/components/ActivityPreview.js +11 -11
  69. package/dist/components/ActivitySequence.cjs +13 -13
  70. package/dist/components/ActivitySequence.d.cts +2 -2
  71. package/dist/components/ActivitySequence.d.ts +2 -2
  72. package/dist/components/ActivitySequence.js +12 -12
  73. package/dist/components/Dictation.cjs +5 -5
  74. package/dist/components/Dictation.d.cts +1 -1
  75. package/dist/components/Dictation.d.ts +1 -1
  76. package/dist/components/Dictation.js +4 -4
  77. package/dist/components/FillInTheBlanks.cjs +5 -5
  78. package/dist/components/FillInTheBlanks.d.cts +1 -1
  79. package/dist/components/FillInTheBlanks.d.ts +1 -1
  80. package/dist/components/FillInTheBlanks.js +4 -4
  81. package/dist/components/GapSelect.cjs +5 -5
  82. package/dist/components/GapSelect.d.cts +1 -1
  83. package/dist/components/GapSelect.d.ts +1 -1
  84. package/dist/components/GapSelect.js +4 -4
  85. package/dist/components/InteractiveVideo.cjs +30 -0
  86. package/dist/components/InteractiveVideo.cjs.map +1 -0
  87. package/dist/components/InteractiveVideo.d.cts +123 -0
  88. package/dist/components/InteractiveVideo.d.ts +123 -0
  89. package/dist/components/InteractiveVideo.js +17 -0
  90. package/dist/components/InteractiveVideo.js.map +1 -0
  91. package/dist/components/MultipleChoice.cjs +5 -5
  92. package/dist/components/MultipleChoice.d.cts +1 -1
  93. package/dist/components/MultipleChoice.d.ts +1 -1
  94. package/dist/components/MultipleChoice.js +4 -4
  95. package/dist/components/PronunciationFeedback.cjs +4 -4
  96. package/dist/components/PronunciationFeedback.d.cts +1 -1
  97. package/dist/components/PronunciationFeedback.d.ts +1 -1
  98. package/dist/components/PronunciationFeedback.js +3 -3
  99. package/dist/components/ReadAloud.cjs +6 -6
  100. package/dist/components/ReadAloud.d.cts +2 -2
  101. package/dist/components/ReadAloud.d.ts +2 -2
  102. package/dist/components/ReadAloud.js +5 -5
  103. package/dist/components/StimulusPanel.cjs +4 -4
  104. package/dist/components/StimulusPanel.d.cts +1 -1
  105. package/dist/components/StimulusPanel.d.ts +1 -1
  106. package/dist/components/StimulusPanel.js +3 -3
  107. package/dist/components/WrittenResponse.cjs +5 -5
  108. package/dist/components/WrittenResponse.d.cts +3 -3
  109. package/dist/components/WrittenResponse.d.ts +3 -3
  110. package/dist/components/WrittenResponse.js +4 -4
  111. package/dist/i18n/LkIntlProvider.cjs +7 -7
  112. package/dist/i18n/LkIntlProvider.d.cts +2 -2
  113. package/dist/i18n/LkIntlProvider.d.ts +2 -2
  114. package/dist/i18n/LkIntlProvider.js +1 -1
  115. package/dist/index.cjs +48 -39
  116. package/dist/index.cjs.map +1 -1
  117. package/dist/index.d.cts +3 -2
  118. package/dist/index.d.ts +3 -2
  119. package/dist/index.js +15 -14
  120. package/dist/index.js.map +1 -1
  121. package/dist/{strings-l8Wv1Xax.d.ts → strings-Dgc5-DeA.d.ts} +121 -1
  122. package/dist/{strings-wjAF2ONm.d.cts → strings-zY2QQYpU.d.cts} +121 -1
  123. package/dist/theme/ThemeProvider.cjs +6 -6
  124. package/dist/theme/ThemeProvider.js +1 -1
  125. package/dist/theme/defaults.css +9 -0
  126. package/dist/theme/skin.css +1386 -0
  127. package/package.json +13 -3
  128. package/dist/chunk-AWQ53DON.js.map +0 -1
  129. package/dist/chunk-D4DQFTA7.cjs.map +0 -1
  130. package/dist/chunk-RXS455MR.js.map +0 -1
  131. package/dist/chunk-VSFHJ7FB.cjs.map +0 -1
@@ -0,0 +1,2683 @@
1
+ 'use client';
2
+ import { MultipleChoice } from './chunk-4X7NFEDB.js';
3
+ import { FillInTheBlanks } from './chunk-4AQ4SWJ3.js';
4
+ import { GapSelect } from './chunk-UYRQEIB7.js';
5
+ import { Dictation } from './chunk-T33AJYLW.js';
6
+ import { stopCaptureGroup, SequenceSlotContext, ReadAloud } from './chunk-W32ND3ZR.js';
7
+ import { ActivityErrorBoundary, isDevelopment } from './chunk-ICAYWVNF.js';
8
+ import { useLkStrings } from './chunk-QIA5FAAW.js';
9
+ import { assertRedactedItemGroup, validateItemGroup, ActivitySchemaError, flattenSequence, readMediaProgress } from '@intellectif/lk-core';
10
+ import { useMemo, useRef, useState, useEffect, useCallback } from 'react';
11
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
+
13
+ // src/components/InteractiveVideo/format.ts
14
+ var SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5];
15
+ function clock(value) {
16
+ const total = Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
17
+ const hours = Math.floor(total / 3600);
18
+ const minutes = Math.floor(total % 3600 / 60);
19
+ const secs = total % 60;
20
+ const pad = (n) => String(n).padStart(2, "0");
21
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(secs)}` : `${minutes}:${pad(secs)}`;
22
+ }
23
+ function nudgeSpeed(current, direction) {
24
+ const at = SPEEDS.indexOf(current);
25
+ const from = at === -1 ? SPEEDS.indexOf(1) : at;
26
+ return SPEEDS[Math.min(SPEEDS.length - 1, Math.max(0, from + direction))];
27
+ }
28
+ function Icon({ children, filled = false }) {
29
+ return /* @__PURE__ */ jsx(
30
+ "svg",
31
+ {
32
+ className: "lk-iv-icon",
33
+ viewBox: "0 0 24 24",
34
+ width: "20",
35
+ height: "20",
36
+ fill: filled ? "currentColor" : "none",
37
+ stroke: "currentColor",
38
+ strokeWidth: "1.8",
39
+ strokeLinecap: "round",
40
+ strokeLinejoin: "round",
41
+ "aria-hidden": "true",
42
+ focusable: "false",
43
+ children
44
+ }
45
+ );
46
+ }
47
+ var PlayIcon = () => /* @__PURE__ */ jsx(Icon, { filled: true, children: /* @__PURE__ */ jsx("path", { d: "M8 5.5v13a1 1 0 0 0 1.5.86l10.2-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5Z" }) });
48
+ var PauseIcon = () => /* @__PURE__ */ jsxs(Icon, { filled: true, children: [
49
+ /* @__PURE__ */ jsx("rect", { x: "6.5", y: "5", width: "3.5", height: "14", rx: "1" }),
50
+ /* @__PURE__ */ jsx("rect", { x: "14", y: "5", width: "3.5", height: "14", rx: "1" })
51
+ ] });
52
+ var ReplayIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
53
+ /* @__PURE__ */ jsx("path", { d: "M4 12a8 8 0 1 0 2.4-5.7" }),
54
+ /* @__PURE__ */ jsx("path", { d: "M4 4v4h4" })
55
+ ] });
56
+ var BackIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
57
+ /* @__PURE__ */ jsx("path", { d: "M11 6.5 5.5 12l5.5 5.5" }),
58
+ /* @__PURE__ */ jsx("path", { d: "M18.5 6.5 13 12l5.5 5.5" })
59
+ ] });
60
+ var ForwardIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
61
+ /* @__PURE__ */ jsx("path", { d: "m13 6.5 5.5 5.5-5.5 5.5" }),
62
+ /* @__PURE__ */ jsx("path", { d: "M5.5 6.5 11 12l-5.5 5.5" })
63
+ ] });
64
+ var VolumeIcon = ({ level }) => /* @__PURE__ */ jsxs(Icon, { children: [
65
+ /* @__PURE__ */ jsx("path", { d: "M4 9.5v5h3.5L12 18.5v-13L7.5 9.5H4Z" }),
66
+ level === "muted" ? /* @__PURE__ */ jsx("path", { d: "m16 9.5 5 5m0-5-5 5" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
67
+ /* @__PURE__ */ jsx("path", { d: "M15.5 9.5a3.5 3.5 0 0 1 0 5" }),
68
+ level === "high" ? /* @__PURE__ */ jsx("path", { d: "M18 7a7 7 0 0 1 0 10" }) : null
69
+ ] })
70
+ ] });
71
+ var CaptionsIcon = ({ on }) => /* @__PURE__ */ jsxs(Icon, { children: [
72
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "5.5", width: "18", height: "13", rx: "2.5" }),
73
+ /* @__PURE__ */ jsx("path", { d: "M10 10.5a2 2 0 1 0 0 3M16 10.5a2 2 0 1 0 0 3" }),
74
+ on ? null : /* @__PURE__ */ jsx("path", { d: "m4 20 16-16" })
75
+ ] });
76
+ var SettingsIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
77
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
78
+ /* @__PURE__ */ jsx("path", { d: "M12 3.5v2M12 18.5v2M20.5 12h-2M5.5 12h-2M18 6l-1.4 1.4M7.4 16.6 6 18M18 18l-1.4-1.4M7.4 7.4 6 6" })
79
+ ] });
80
+ var ContentsIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
81
+ /* @__PURE__ */ jsx("path", { d: "M9 6.5h11M9 12h11M9 17.5h11" }),
82
+ /* @__PURE__ */ jsx("path", { d: "M4.5 6.5h.01M4.5 12h.01M4.5 17.5h.01" })
83
+ ] });
84
+ var PictureInPictureIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
85
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
86
+ /* @__PURE__ */ jsx("rect", { x: "12", y: "11.5", width: "6.5", height: "5", rx: "1" })
87
+ ] });
88
+ var FullscreenIcon = ({ on }) => /* @__PURE__ */ jsx(Icon, { children: on ? /* @__PURE__ */ jsx("path", { d: "M9 4v3a2 2 0 0 1-2 2H4M20 9h-3a2 2 0 0 1-2-2V4M4 15h3a2 2 0 0 1 2 2v3M15 20v-3a2 2 0 0 1 2-2h3" }) : /* @__PURE__ */ jsx("path", { d: "M4 9V6a2 2 0 0 1 2-2h3M15 4h3a2 2 0 0 1 2 2v3M20 15v3a2 2 0 0 1-2 2h-3M9 20H6a2 2 0 0 1-2-2v-3" }) });
89
+ var CheckIcon = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "m5 12.5 4.5 4.5L19 7.5" }) });
90
+ var ChevronIcon = ({ direction }) => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: direction === "right" ? "m9.5 6 6 6-6 6" : "m14.5 6-6 6 6 6" }) });
91
+ var CloseIcon = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "m6.5 6.5 11 11m0-11-11 11" }) });
92
+ var AlertIcon = () => /* @__PURE__ */ jsxs(Icon, { children: [
93
+ /* @__PURE__ */ jsx("path", { d: "M12 4 2.8 19.5h18.4L12 4Z" }),
94
+ /* @__PURE__ */ jsx("path", { d: "M12 10v4M12 17h.01" })
95
+ ] });
96
+
97
+ // src/components/InteractiveVideo/shortcuts.ts
98
+ var SHORTCUT_KEYS = [
99
+ { action: "play-pause", keys: "Space / K" },
100
+ { action: "jump-10", keys: "J / L" },
101
+ { action: "jump-5", keys: "\u2190 / \u2192" },
102
+ { action: "jump-1", keys: "Shift + \u2190 / \u2192" },
103
+ { action: "frame", keys: ", / ." },
104
+ { action: "speed", keys: "< / >" },
105
+ { action: "percent", keys: "0 \u2013 9" },
106
+ { action: "start-end", keys: "Home / End" },
107
+ { action: "chapter", keys: "[ / ]" },
108
+ { action: "mute", keys: "M" },
109
+ { action: "captions", keys: "C" },
110
+ { action: "secondary-captions", keys: "Shift + C" },
111
+ { action: "transcript", keys: "T" },
112
+ { action: "picture-in-picture", keys: "P" },
113
+ { action: "fullscreen", keys: "F" },
114
+ { action: "shortcut-list", keys: "?" }
115
+ ];
116
+ var JUMP_SECONDS = 10;
117
+ var FRAME_SECONDS = 1 / 30;
118
+
119
+ // src/components/InteractiveVideo/tracks.ts
120
+ var primarySubtag = (tag) => (tag.split("-")[0] ?? tag).toLowerCase();
121
+ function preferCaptions(tracks) {
122
+ return tracks.find((track) => track.kind === "captions") ?? tracks[0];
123
+ }
124
+ function matchLanguage(tracks, language) {
125
+ const wanted = language.toLowerCase();
126
+ const exact = tracks.filter((track) => track.srclang.toLowerCase() === wanted);
127
+ if (exact.length > 0) {
128
+ return preferCaptions(exact);
129
+ }
130
+ const family = primarySubtag(wanted);
131
+ return preferCaptions(tracks.filter((track) => primarySubtag(track.srclang) === family));
132
+ }
133
+ function drawable(tracks) {
134
+ return tracks.filter((track) => track.kind === "captions" || track.kind === "subtitles");
135
+ }
136
+ function secondaryCandidates(tracks, primary) {
137
+ const taken = primary?.srclang.toLowerCase();
138
+ return drawable(tracks).filter((track) => track.srclang.toLowerCase() !== taken);
139
+ }
140
+ function resolveCaptionTracks(tracks, preferences) {
141
+ const usable = drawable(tracks);
142
+ if (usable.length === 0) {
143
+ return {};
144
+ }
145
+ const primary = (preferences.captionLanguage === null ? void 0 : matchLanguage(usable, preferences.captionLanguage)) ?? usable.find((track) => track.default === true) ?? usable[0];
146
+ const second = preferences.secondaryCaptionLanguage;
147
+ const secondary = second === null ? void 0 : matchLanguage(secondaryCandidates(usable, primary), second);
148
+ return secondary === void 0 ? { primary } : { primary, secondary };
149
+ }
150
+ var overlap = (a, b) => Math.min(a.end, b.end) - Math.max(a.start, b.start);
151
+ var LOOKBACK = 8;
152
+ function pairSecondaryCues(primary, secondary) {
153
+ const rows = primary.map(() => []);
154
+ for (const cue of secondary) {
155
+ let low = 0;
156
+ let high = primary.length;
157
+ while (low < high) {
158
+ const middle = low + high >> 1;
159
+ if (primary[middle].start < cue.end) {
160
+ low = middle + 1;
161
+ } else {
162
+ high = middle;
163
+ }
164
+ }
165
+ let best = -1;
166
+ let bestShare = 0;
167
+ let misses = 0;
168
+ for (let index = low - 1; index >= 0 && misses <= LOOKBACK; index -= 1) {
169
+ const share = overlap(primary[index], cue);
170
+ if (share > 0) {
171
+ misses = 0;
172
+ if (share >= bestShare) {
173
+ best = index;
174
+ bestShare = share;
175
+ }
176
+ } else {
177
+ misses += 1;
178
+ }
179
+ }
180
+ if (best !== -1) {
181
+ rows[best].push(cue);
182
+ }
183
+ }
184
+ return rows;
185
+ }
186
+ function Menu({
187
+ label,
188
+ onClose,
189
+ children,
190
+ returnTo
191
+ }) {
192
+ const ref = useRef(null);
193
+ useEffect(() => {
194
+ const items = ref.current?.querySelectorAll('[role^="menuitem"]');
195
+ const back = returnTo === void 0 ? null : ref.current?.querySelector(`[data-menu-key="${returnTo}"]`);
196
+ const checked = ref.current?.querySelector('[aria-checked="true"]');
197
+ (back ?? checked ?? items?.[0])?.focus();
198
+ }, [returnTo]);
199
+ const onKeyDown = (event) => {
200
+ const items = Array.from(
201
+ ref.current?.querySelectorAll('[role^="menuitem"]') ?? []
202
+ );
203
+ const at = items.indexOf(document.activeElement);
204
+ const move = (to) => {
205
+ event.preventDefault();
206
+ items[(to + items.length) % items.length]?.focus();
207
+ };
208
+ if (event.key === "ArrowDown") {
209
+ move(at + 1);
210
+ } else if (event.key === "ArrowUp") {
211
+ move(at - 1);
212
+ } else if (event.key === "Home") {
213
+ move(0);
214
+ } else if (event.key === "End") {
215
+ move(items.length - 1);
216
+ } else if (event.key === "Escape") {
217
+ event.preventDefault();
218
+ event.stopPropagation();
219
+ onClose();
220
+ }
221
+ event.stopPropagation();
222
+ };
223
+ return /* @__PURE__ */ jsx("div", { ref, className: "lk-iv-menu", role: "menu", "aria-label": label, onKeyDown, children });
224
+ }
225
+ function SpeedMenu({
226
+ speed,
227
+ strings,
228
+ onChoose,
229
+ onClose
230
+ }) {
231
+ return /* @__PURE__ */ jsx(Menu, { label: strings.media.speed, onClose, children: SPEEDS.map((rate) => /* @__PURE__ */ jsxs(
232
+ "button",
233
+ {
234
+ type: "button",
235
+ role: "menuitemradio",
236
+ "aria-checked": rate === speed,
237
+ className: "lk-iv-menu-item",
238
+ onClick: () => onChoose(rate),
239
+ children: [
240
+ /* @__PURE__ */ jsx("span", { children: strings.videoSpeedValue(rate) }),
241
+ rate === speed ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
242
+ ]
243
+ },
244
+ rate
245
+ )) });
246
+ }
247
+ function SettingsMenu({
248
+ preferences,
249
+ tracks,
250
+ primaryTrack,
251
+ secondaryTrack,
252
+ captionsFailed,
253
+ strings,
254
+ onChange,
255
+ onCaptionTrack,
256
+ onSecondaryTrack,
257
+ onShowShortcuts,
258
+ onClose
259
+ }) {
260
+ const [page, setPage] = useState("main");
261
+ const [returnTo, setReturnTo] = useState(void 0);
262
+ const open = (next) => {
263
+ setReturnTo(void 0);
264
+ setPage(next);
265
+ };
266
+ const leave = () => {
267
+ setReturnTo(page);
268
+ setPage(page === "second" ? "captions" : "main");
269
+ };
270
+ const captionsValue = !preferences.captions || primaryTrack === void 0 ? strings.videoOff : secondaryTrack === void 0 ? primaryTrack.label : strings.videoCaptionPair(primaryTrack.label, secondaryTrack.label);
271
+ const partners = secondaryCandidates(tracks, primaryTrack);
272
+ const failedNotes = /* @__PURE__ */ jsxs(Fragment, { children: [
273
+ captionsFailed.primary && page === "captions" ? /* @__PURE__ */ jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoCaptionsFailed }) : null,
274
+ captionsFailed.secondary ? /* @__PURE__ */ jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoSecondaryCaptionsFailed }) : null
275
+ ] });
276
+ const back = /* @__PURE__ */ jsxs(
277
+ "button",
278
+ {
279
+ type: "button",
280
+ role: "menuitem",
281
+ className: "lk-iv-menu-item lk-iv-menu-back",
282
+ onClick: leave,
283
+ children: [
284
+ /* @__PURE__ */ jsx(ChevronIcon, { direction: "left" }),
285
+ /* @__PURE__ */ jsx("span", { children: page === "captions" ? strings.videoCaptionLanguage : page === "second" ? strings.videoSecondCaptionLanguage : strings.videoCaptionSize })
286
+ ]
287
+ }
288
+ );
289
+ const trackRadio = (track, on, choose) => /* @__PURE__ */ jsxs(
290
+ "button",
291
+ {
292
+ type: "button",
293
+ role: "menuitemradio",
294
+ "aria-checked": on,
295
+ className: "lk-iv-menu-item",
296
+ lang: track.srclang,
297
+ onClick: choose,
298
+ children: [
299
+ /* @__PURE__ */ jsx("span", { dir: "auto", children: track.label }),
300
+ on ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
301
+ ]
302
+ },
303
+ `${track.kind}:${track.srclang}`
304
+ );
305
+ if (page === "captions") {
306
+ return /* @__PURE__ */ jsxs(
307
+ Menu,
308
+ {
309
+ label: strings.videoCaptionLanguage,
310
+ onClose,
311
+ returnTo,
312
+ children: [
313
+ back,
314
+ /* @__PURE__ */ jsxs(
315
+ "button",
316
+ {
317
+ type: "button",
318
+ role: "menuitemradio",
319
+ "aria-checked": !preferences.captions,
320
+ className: "lk-iv-menu-item",
321
+ onClick: () => onChange({ captions: false }),
322
+ children: [
323
+ /* @__PURE__ */ jsx("span", { children: strings.videoOff }),
324
+ !preferences.captions ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
325
+ ]
326
+ }
327
+ ),
328
+ tracks.map(
329
+ (track) => trackRadio(
330
+ track,
331
+ preferences.captions && primaryTrack === track,
332
+ () => onCaptionTrack(track)
333
+ )
334
+ ),
335
+ partners.length > 0 ? /* @__PURE__ */ jsxs(
336
+ "button",
337
+ {
338
+ type: "button",
339
+ role: "menuitem",
340
+ className: "lk-iv-menu-item",
341
+ "data-menu-key": "second",
342
+ onClick: () => open("second"),
343
+ children: [
344
+ /* @__PURE__ */ jsx("span", { children: strings.videoSecondCaptionLanguage }),
345
+ /* @__PURE__ */ jsxs("span", { className: "lk-iv-menu-value", children: [
346
+ /* @__PURE__ */ jsx("span", { dir: "auto", lang: secondaryTrack?.srclang, children: secondaryTrack === void 0 ? strings.videoOff : secondaryTrack.label }),
347
+ /* @__PURE__ */ jsx(ChevronIcon, { direction: "right" })
348
+ ] })
349
+ ]
350
+ }
351
+ ) : null,
352
+ failedNotes
353
+ ]
354
+ },
355
+ "captions"
356
+ );
357
+ }
358
+ if (page === "second") {
359
+ return /* @__PURE__ */ jsxs(Menu, { label: strings.videoSecondCaptionLanguage, onClose, children: [
360
+ back,
361
+ /* @__PURE__ */ jsxs(
362
+ "button",
363
+ {
364
+ type: "button",
365
+ role: "menuitemradio",
366
+ "aria-checked": secondaryTrack === void 0,
367
+ className: "lk-iv-menu-item",
368
+ onClick: () => onSecondaryTrack(null),
369
+ children: [
370
+ /* @__PURE__ */ jsx("span", { children: strings.videoOff }),
371
+ secondaryTrack === void 0 ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
372
+ ]
373
+ }
374
+ ),
375
+ partners.map(
376
+ (track) => trackRadio(track, secondaryTrack === track, () => onSecondaryTrack(track))
377
+ ),
378
+ failedNotes
379
+ ] }, "second");
380
+ }
381
+ if (page === "size") {
382
+ return /* @__PURE__ */ jsxs(Menu, { label: strings.videoCaptionSize, onClose, children: [
383
+ back,
384
+ ["small", "medium", "large"].map((size) => /* @__PURE__ */ jsxs(
385
+ "button",
386
+ {
387
+ type: "button",
388
+ role: "menuitemradio",
389
+ "aria-checked": preferences.captionSize === size,
390
+ className: "lk-iv-menu-item",
391
+ onClick: () => onChange({ captionSize: size }),
392
+ children: [
393
+ /* @__PURE__ */ jsx("span", { children: strings.videoCaptionSizeValue(size) }),
394
+ preferences.captionSize === size ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
395
+ ]
396
+ },
397
+ size
398
+ ))
399
+ ] }, "size");
400
+ }
401
+ return /* @__PURE__ */ jsxs(Menu, { label: strings.videoSettings, onClose, returnTo, children: [
402
+ tracks.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
403
+ /* @__PURE__ */ jsxs(
404
+ "button",
405
+ {
406
+ type: "button",
407
+ role: "menuitem",
408
+ className: "lk-iv-menu-item",
409
+ "data-menu-key": "captions",
410
+ onClick: () => open("captions"),
411
+ children: [
412
+ /* @__PURE__ */ jsx("span", { children: strings.videoCaptionLanguage }),
413
+ /* @__PURE__ */ jsxs("span", { className: "lk-iv-menu-value", children: [
414
+ /* @__PURE__ */ jsx("span", { dir: "auto", children: captionsValue }),
415
+ /* @__PURE__ */ jsx(ChevronIcon, { direction: "right" })
416
+ ] })
417
+ ]
418
+ }
419
+ ),
420
+ /* @__PURE__ */ jsxs(
421
+ "button",
422
+ {
423
+ type: "button",
424
+ role: "menuitem",
425
+ className: "lk-iv-menu-item",
426
+ "data-menu-key": "size",
427
+ onClick: () => open("size"),
428
+ children: [
429
+ /* @__PURE__ */ jsx("span", { children: strings.videoCaptionSize }),
430
+ /* @__PURE__ */ jsxs("span", { className: "lk-iv-menu-value", children: [
431
+ strings.videoCaptionSizeValue(preferences.captionSize),
432
+ /* @__PURE__ */ jsx(ChevronIcon, { direction: "right" })
433
+ ] })
434
+ ]
435
+ }
436
+ ),
437
+ /* @__PURE__ */ jsxs(
438
+ "button",
439
+ {
440
+ type: "button",
441
+ role: "menuitemcheckbox",
442
+ "aria-checked": preferences.captionBackground,
443
+ className: "lk-iv-menu-item",
444
+ onClick: () => onChange({ captionBackground: !preferences.captionBackground }),
445
+ children: [
446
+ /* @__PURE__ */ jsx("span", { children: strings.videoCaptionBackground }),
447
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-menu-value", children: preferences.captionBackground ? strings.videoOn : strings.videoOff })
448
+ ]
449
+ }
450
+ )
451
+ ] }) : null,
452
+ /* @__PURE__ */ jsxs(
453
+ "button",
454
+ {
455
+ type: "button",
456
+ role: "menuitemcheckbox",
457
+ "aria-checked": preferences.shortcuts,
458
+ className: "lk-iv-menu-item",
459
+ onClick: () => onChange({ shortcuts: !preferences.shortcuts }),
460
+ children: [
461
+ /* @__PURE__ */ jsx("span", { children: strings.videoKeyboardShortcuts }),
462
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-menu-value", children: preferences.shortcuts ? strings.videoOn : strings.videoOff })
463
+ ]
464
+ }
465
+ ),
466
+ /* @__PURE__ */ jsxs("button", { type: "button", role: "menuitem", className: "lk-iv-menu-item", onClick: onShowShortcuts, children: [
467
+ /* @__PURE__ */ jsx("span", { children: strings.videoShortcutList }),
468
+ /* @__PURE__ */ jsx("kbd", { className: "lk-iv-kbd", children: "?" })
469
+ ] })
470
+ ] }, "main");
471
+ }
472
+ function ShortcutSheet({ strings, onClose }) {
473
+ const closeRef = useRef(null);
474
+ useEffect(() => {
475
+ closeRef.current?.focus();
476
+ }, []);
477
+ return /* @__PURE__ */ jsxs(
478
+ "div",
479
+ {
480
+ className: "lk-iv-sheet",
481
+ role: "dialog",
482
+ "aria-modal": "false",
483
+ "aria-label": strings.videoKeyboardShortcuts,
484
+ onKeyDown: (event) => {
485
+ if (event.key === "Escape" || event.key === "?") {
486
+ event.preventDefault();
487
+ event.stopPropagation();
488
+ onClose();
489
+ }
490
+ },
491
+ children: [
492
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-sheet-head", children: [
493
+ /* @__PURE__ */ jsx("h3", { className: "lk-iv-sheet-title", children: strings.videoKeyboardShortcuts }),
494
+ /* @__PURE__ */ jsx(
495
+ "button",
496
+ {
497
+ ref: closeRef,
498
+ type: "button",
499
+ className: "lk-iv-button",
500
+ "aria-label": strings.videoClose,
501
+ onClick: onClose,
502
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
503
+ }
504
+ )
505
+ ] }),
506
+ /* @__PURE__ */ jsx("dl", { className: "lk-iv-shortcuts", children: SHORTCUT_KEYS.map(({ action, keys }) => /* @__PURE__ */ jsxs("div", { className: "lk-iv-shortcut", children: [
507
+ /* @__PURE__ */ jsx("dt", { children: /* @__PURE__ */ jsx("kbd", { className: "lk-iv-kbd", children: keys }) }),
508
+ /* @__PURE__ */ jsx("dd", { children: strings.videoShortcut(action) })
509
+ ] }, action)) })
510
+ ]
511
+ }
512
+ );
513
+ }
514
+ function ContentsPanel({
515
+ idPrefix,
516
+ tab,
517
+ onTab,
518
+ chapters,
519
+ quizzes,
520
+ cues,
521
+ primaryLanguage,
522
+ secondaryCues,
523
+ secondaryLanguage,
524
+ activeCue,
525
+ limit,
526
+ strings,
527
+ onSeek,
528
+ onOpenQuiz
529
+ }) {
530
+ const [query, setQuery] = useState("");
531
+ const listRef = useRef(null);
532
+ const followUntil = useRef(0);
533
+ const hasTranscript = cues.length > 0;
534
+ const shownTab = hasTranscript ? tab : "contents";
535
+ const paired = useMemo(() => pairSecondaryCues(cues, secondaryCues), [cues, secondaryCues]);
536
+ const lines = useMemo(() => {
537
+ const trimmed = query.trim().toLowerCase();
538
+ return cues.map((cue, index) => ({
539
+ cue,
540
+ index,
541
+ // The spoiler limit holds for the second language as for the first.
542
+ second: (paired[index] ?? []).filter((line) => line.start <= limit).map((line) => line.text).join(" ")
543
+ })).filter(({ cue }) => cue.start <= limit).filter(
544
+ ({ cue, second }) => trimmed === "" || cue.text.toLowerCase().includes(trimmed) || second.toLowerCase().includes(trimmed)
545
+ );
546
+ }, [cues, paired, limit, query]);
547
+ useEffect(() => {
548
+ if (shownTab !== "transcript" || query.trim() !== "" || Date.now() < followUntil.current) {
549
+ return;
550
+ }
551
+ const active = listRef.current?.querySelector('[data-active="true"]');
552
+ if (active !== null && active !== void 0 && listRef.current !== null) {
553
+ const list = listRef.current;
554
+ const top = active.offsetTop - list.clientHeight / 3;
555
+ list.scrollTo?.({ top, behavior: "smooth" });
556
+ }
557
+ }, [activeCue, shownTab, query]);
558
+ const rows = [
559
+ ...chapters.map((chapter) => ({ kind: "chapter", at: chapter.at, chapter })),
560
+ ...quizzes.map((quiz) => ({ kind: "quiz", at: quiz.at, quiz }))
561
+ ].sort((a, b) => a.at - b.at || (a.kind === "chapter" ? -1 : 1));
562
+ const highlight = (text) => {
563
+ const trimmed = query.trim();
564
+ const at = trimmed === "" ? -1 : text.toLowerCase().indexOf(trimmed.toLowerCase());
565
+ if (at === -1) {
566
+ return text;
567
+ }
568
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
569
+ text.slice(0, at),
570
+ /* @__PURE__ */ jsx("mark", { children: text.slice(at, at + trimmed.length) }),
571
+ text.slice(at + trimmed.length)
572
+ ] });
573
+ };
574
+ return /* @__PURE__ */ jsxs("section", { className: "lk-iv-panel", "aria-label": strings.videoPanel, children: [
575
+ hasTranscript ? /* @__PURE__ */ jsx("div", { className: "lk-iv-tabs", role: "tablist", "aria-label": strings.videoPanel, children: ["contents", "transcript"].map((name) => /* @__PURE__ */ jsx(
576
+ "button",
577
+ {
578
+ type: "button",
579
+ role: "tab",
580
+ id: `${idPrefix}-tab-${name}`,
581
+ "aria-controls": `${idPrefix}-tabpanel`,
582
+ "aria-selected": shownTab === name,
583
+ tabIndex: shownTab === name ? 0 : -1,
584
+ className: "lk-iv-tab",
585
+ onClick: () => onTab(name),
586
+ onKeyDown: (event) => {
587
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
588
+ event.preventDefault();
589
+ onTab(name === "contents" ? "transcript" : "contents");
590
+ }
591
+ },
592
+ children: name === "contents" ? strings.videoContents : strings.videoTranscript
593
+ },
594
+ name
595
+ )) }) : null,
596
+ shownTab === "contents" ? /* @__PURE__ */ jsx(
597
+ "ol",
598
+ {
599
+ className: "lk-iv-contents",
600
+ ...hasTranscript ? {
601
+ role: "tabpanel",
602
+ id: `${idPrefix}-tabpanel`,
603
+ "aria-labelledby": `${idPrefix}-tab-contents`
604
+ } : { "aria-label": strings.videoContents },
605
+ children: rows.map(
606
+ (row) => row.kind === "chapter" ? /* @__PURE__ */ jsx("li", { className: "lk-iv-contents-chapter", children: /* @__PURE__ */ jsxs("button", { type: "button", className: "lk-iv-row", onClick: () => onSeek(row.chapter.at), children: [
607
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-row-time", children: clock(row.chapter.at) }),
608
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-row-title", children: row.chapter.title })
609
+ ] }) }, `c-${row.at}`) : /* @__PURE__ */ jsx(
610
+ "li",
611
+ {
612
+ className: "lk-iv-contents-quiz",
613
+ "data-state": row.quiz.state,
614
+ children: /* @__PURE__ */ jsxs("button", { type: "button", className: "lk-iv-row", onClick: () => onOpenQuiz(row.quiz.id), children: [
615
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-row-time", children: clock(row.quiz.at) }),
616
+ /* @__PURE__ */ jsxs("span", { className: "lk-iv-row-title", children: [
617
+ /* @__PURE__ */ jsx(
618
+ "span",
619
+ {
620
+ className: "lk-iv-marker-dot",
621
+ "data-state": row.quiz.state,
622
+ "aria-hidden": "true"
623
+ }
624
+ ),
625
+ row.quiz.title,
626
+ row.quiz.required ? /* @__PURE__ */ jsxs(Fragment, { children: [
627
+ " ",
628
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-tag", children: strings.videoRequired })
629
+ ] }) : null
630
+ ] }),
631
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-row-status", children: strings.videoQuizProgress(row.quiz.answered, row.quiz.total) })
632
+ ] })
633
+ },
634
+ `q-${row.quiz.id}`
635
+ )
636
+ )
637
+ }
638
+ ) : /* @__PURE__ */ jsxs(
639
+ "div",
640
+ {
641
+ className: "lk-iv-transcript",
642
+ role: "tabpanel",
643
+ id: `${idPrefix}-tabpanel`,
644
+ "aria-labelledby": `${idPrefix}-tab-transcript`,
645
+ children: [
646
+ /* @__PURE__ */ jsx(
647
+ "input",
648
+ {
649
+ id: `${idPrefix}-search`,
650
+ className: "lk-iv-search",
651
+ type: "search",
652
+ value: query,
653
+ placeholder: strings.videoTranscriptSearch,
654
+ "aria-label": strings.videoTranscriptSearch,
655
+ onChange: (event) => setQuery(event.target.value)
656
+ }
657
+ ),
658
+ /* @__PURE__ */ jsx(
659
+ "div",
660
+ {
661
+ ref: listRef,
662
+ className: "lk-iv-lines",
663
+ onWheel: () => {
664
+ followUntil.current = Date.now() + 4e3;
665
+ },
666
+ onTouchMove: () => {
667
+ followUntil.current = Date.now() + 4e3;
668
+ },
669
+ children: lines.length === 0 ? /* @__PURE__ */ jsx("p", { className: "lk-iv-empty", children: strings.videoTranscriptNoMatch }) : lines.map(({ cue, index, second }) => /* @__PURE__ */ jsxs(
670
+ "button",
671
+ {
672
+ type: "button",
673
+ className: "lk-iv-line",
674
+ "data-active": index === activeCue || void 0,
675
+ onClick: () => onSeek(cue.start),
676
+ children: [
677
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-row-time", children: clock(cue.start) }),
678
+ secondaryCues.length === 0 ? /* @__PURE__ */ jsx("span", { lang: primaryLanguage, dir: "auto", children: highlight(cue.text) }) : /* @__PURE__ */ jsxs("span", { className: "lk-iv-line-text", children: [
679
+ /* @__PURE__ */ jsx("span", { lang: primaryLanguage, dir: "auto", children: highlight(cue.text) }),
680
+ second === "" ? null : /* @__PURE__ */ jsx("span", { className: "lk-iv-line-secondary", lang: secondaryLanguage, dir: "auto", children: highlight(second) })
681
+ ] })
682
+ ]
683
+ },
684
+ `${cue.start}-${index}`
685
+ ))
686
+ }
687
+ )
688
+ ]
689
+ }
690
+ )
691
+ ] });
692
+ }
693
+ function EndScreen({
694
+ idPrefix,
695
+ renderMode,
696
+ quizzes,
697
+ answered,
698
+ total,
699
+ percent,
700
+ strings,
701
+ onOpenQuiz,
702
+ onWatchAgain,
703
+ onFinish
704
+ }) {
705
+ const headingRef = useRef(null);
706
+ useEffect(() => {
707
+ headingRef.current?.focus();
708
+ }, []);
709
+ return /* @__PURE__ */ jsx("section", { className: "lk-iv-end", "aria-labelledby": `${idPrefix}-end-title`, children: /* @__PURE__ */ jsxs("div", { className: "lk-iv-end-card", children: [
710
+ /* @__PURE__ */ jsx("h3", { id: `${idPrefix}-end-title`, ref: headingRef, tabIndex: -1, className: "lk-iv-end-title", children: strings.videoEnded }),
711
+ total > 0 ? /* @__PURE__ */ jsx("p", { className: "lk-iv-end-summary", children: percent !== null && renderMode !== "exam" ? strings.videoEndScore(answered, total, percent) : strings.videoEndAnswered(answered, total) }) : null,
712
+ quizzes.length > 0 ? /* @__PURE__ */ jsx("ol", { className: "lk-iv-end-list", children: quizzes.map((quiz) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
713
+ "button",
714
+ {
715
+ type: "button",
716
+ className: "lk-iv-row",
717
+ "data-state": quiz.state,
718
+ onClick: () => onOpenQuiz(quiz.id),
719
+ children: [
720
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-row-time", children: clock(quiz.at) }),
721
+ /* @__PURE__ */ jsxs("span", { className: "lk-iv-row-title", children: [
722
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-marker-dot", "data-state": quiz.state, "aria-hidden": "true" }),
723
+ quiz.title
724
+ ] }),
725
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-row-status", children: strings.videoQuizProgress(quiz.answered, quiz.total) })
726
+ ]
727
+ }
728
+ ) }, quiz.id)) }) : null,
729
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-end-actions", children: [
730
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "lk-iv-action", onClick: onWatchAgain, children: [
731
+ /* @__PURE__ */ jsx(ReplayIcon, {}),
732
+ strings.videoWatchAgain
733
+ ] }),
734
+ onFinish !== void 0 ? /* @__PURE__ */ jsx("button", { type: "button", className: "lk-iv-action lk-iv-action-primary", onClick: onFinish, children: strings.videoFinish }) : null
735
+ ] })
736
+ ] }) });
737
+ }
738
+
739
+ // src/components/InteractiveVideo/prefs.ts
740
+ var DEFAULT_PREFERENCES = {
741
+ speed: 1,
742
+ volume: 1,
743
+ muted: false,
744
+ captions: true,
745
+ captionLanguage: null,
746
+ secondaryCaptionLanguage: null,
747
+ captionSize: "medium",
748
+ captionBackground: true,
749
+ panel: false,
750
+ shortcuts: true
751
+ };
752
+ var STORAGE_KEY = "lk.video.v1";
753
+ var LANGUAGE_TAG = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
754
+ var BOOLEAN_FIELDS = ["muted", "captions", "captionBackground", "panel", "shortcuts"];
755
+ var LANGUAGE_FIELDS = ["captionLanguage", "secondaryCaptionLanguage"];
756
+ function sanitizePartialPreferences(raw) {
757
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
758
+ return {};
759
+ }
760
+ const value = raw;
761
+ const has = (key) => Object.hasOwn(value, key);
762
+ const out = {};
763
+ if (has("speed") && SPEEDS.includes(value.speed)) {
764
+ out.speed = value.speed;
765
+ }
766
+ if (has("volume") && typeof value.volume === "number" && Number.isFinite(value.volume)) {
767
+ out.volume = Math.min(1, Math.max(0, value.volume));
768
+ }
769
+ for (const field of BOOLEAN_FIELDS) {
770
+ if (has(field) && typeof value[field] === "boolean") {
771
+ out[field] = value[field];
772
+ }
773
+ }
774
+ for (const field of LANGUAGE_FIELDS) {
775
+ const tag = value[field];
776
+ if (has(field) && (tag === null || typeof tag === "string" && LANGUAGE_TAG.test(tag))) {
777
+ out[field] = tag;
778
+ }
779
+ }
780
+ if (has("captionSize") && (value.captionSize === "small" || value.captionSize === "medium" || value.captionSize === "large")) {
781
+ out.captionSize = value.captionSize;
782
+ }
783
+ return out;
784
+ }
785
+ function normalised(preferences) {
786
+ const { captionLanguage: first, secondaryCaptionLanguage: second } = preferences;
787
+ return first !== null && second !== null && first.toLowerCase() === second.toLowerCase() ? { ...preferences, secondaryCaptionLanguage: null } : preferences;
788
+ }
789
+ function resolvePreferences(layers) {
790
+ return normalised({
791
+ ...DEFAULT_PREFERENCES,
792
+ ...sanitizePartialPreferences(layers.defaults),
793
+ ...sanitizePartialPreferences(layers.stored),
794
+ ...sanitizePartialPreferences(layers.force),
795
+ ...sanitizePartialPreferences(layers.chosen)
796
+ });
797
+ }
798
+ function applyPreferenceChange(previous, change) {
799
+ return normalised({ ...previous, ...sanitizePartialPreferences(change) });
800
+ }
801
+ function readStoredPreferences() {
802
+ try {
803
+ const stored = globalThis.localStorage?.getItem(STORAGE_KEY);
804
+ return stored ? sanitizePartialPreferences(JSON.parse(stored)) : {};
805
+ } catch {
806
+ return {};
807
+ }
808
+ }
809
+ function rememberPreferences(change) {
810
+ try {
811
+ const next = { ...readStoredPreferences(), ...sanitizePartialPreferences(change) };
812
+ globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
813
+ } catch {
814
+ }
815
+ }
816
+
817
+ // src/components/InteractiveVideo/quiz-engine.ts
818
+ function crossedQuiz(cues, previous, now, handled) {
819
+ let earliest;
820
+ for (const cue of cues) {
821
+ if (handled.has(cue.id) || !(previous < cue.at && cue.at <= now)) {
822
+ continue;
823
+ }
824
+ if (earliest === void 0 || cue.at < earliest.at) {
825
+ earliest = cue;
826
+ }
827
+ }
828
+ return earliest;
829
+ }
830
+ function quizzesAtEnd(cues, duration, handled) {
831
+ return cues.filter((cue) => !handled.has(cue.id) && cue.at >= duration - 0.05).sort((a, b) => a.at - b.at);
832
+ }
833
+ function limitSeek(timeline, from, to, furthest, finished) {
834
+ if (to <= from) {
835
+ return { at: to };
836
+ }
837
+ let at = to;
838
+ let hold;
839
+ if (timeline.navigation === "no-skip-ahead" && to > furthest) {
840
+ at = Math.max(from, furthest);
841
+ hold = { kind: "no-skip-ahead", furthest };
842
+ }
843
+ const blocking = timeline.cues.filter((cue) => cue.required === true && cue.at > from && cue.at <= to && !finished(cue.id)).sort((a, b) => a.at - b.at)[0];
844
+ if (blocking !== void 0 && blocking.at <= at) {
845
+ return { at: blocking.at, hold: { kind: "required", cue: blocking } };
846
+ }
847
+ return hold === void 0 ? { at } : { at, hold };
848
+ }
849
+ function limitResume(timeline, at, finished) {
850
+ const blocking = timeline.cues.filter((cue) => cue.required === true && cue.at < at && !finished(cue.id)).sort((a, b) => a.at - b.at)[0];
851
+ return blocking === void 0 ? at : Math.max(0, blocking.at - 0.25);
852
+ }
853
+
854
+ // src/components/InteractiveVideo/vtt.ts
855
+ var MAX_VTT_LENGTH = 2e6;
856
+ var MAX_CUES = 1e4;
857
+ var BYTE_ORDER_MARK = String.fromCharCode(65279);
858
+ var TIMESTAMP = /^(?:(\d+):)?([0-5]\d):([0-5]\d)\.(\d{3})$/;
859
+ function seconds(stamp) {
860
+ const match = TIMESTAMP.exec(stamp.trim());
861
+ if (match === null) {
862
+ return void 0;
863
+ }
864
+ const [, hours = "0", minutes = "0", secs = "0", millis = "0"] = match;
865
+ return Number(hours) * 3600 + Number(minutes) * 60 + Number(secs) + Number(millis) / 1e3;
866
+ }
867
+ var ENTITIES = {
868
+ amp: "&",
869
+ lt: "<",
870
+ gt: ">",
871
+ quot: '"',
872
+ apos: "'",
873
+ // Built from code points: an escape can be rewritten as the raw, invisible
874
+ // character itself, by a formatter or an editor.
875
+ nbsp: String.fromCharCode(160),
876
+ lrm: String.fromCharCode(8206),
877
+ rlm: String.fromCharCode(8207)
878
+ };
879
+ function plainText(payload) {
880
+ return payload.replace(/<[^>]*>/g, "").replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, name) => {
881
+ if (name.startsWith("#")) {
882
+ const code = name[1] === "x" || name[1] === "X" ? Number.parseInt(name.slice(2), 16) : Number.parseInt(name.slice(1), 10);
883
+ return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
884
+ }
885
+ return ENTITIES[name.toLowerCase()] ?? whole;
886
+ }).replace(/\s*\n\s*/g, " ").trim();
887
+ }
888
+ function parseWebVtt(input) {
889
+ if (typeof input !== "string") {
890
+ return [];
891
+ }
892
+ const raw = input.slice(0, MAX_VTT_LENGTH);
893
+ const text = (raw.startsWith(BYTE_ORDER_MARK) ? raw.slice(1) : raw).replace(/\r\n?/g, "\n");
894
+ if (!/^WEBVTT(?:[ \t].*)?(?:\n|$)/.test(text)) {
895
+ return [];
896
+ }
897
+ const cues = [];
898
+ for (const block of text.split(/\n{2,}/)) {
899
+ if (cues.length >= MAX_CUES) {
900
+ break;
901
+ }
902
+ const lines = block.split("\n");
903
+ const timingIndex = lines.findIndex((line) => line.includes("-->"));
904
+ if (timingIndex === -1) {
905
+ continue;
906
+ }
907
+ const [from = "", rest = ""] = lines[timingIndex].split("-->");
908
+ const start = seconds(from);
909
+ const end = seconds(rest.trim().split(/\s+/)[0] ?? "");
910
+ if (start === void 0 || end === void 0 || end <= start) {
911
+ continue;
912
+ }
913
+ const payload = plainText(lines.slice(timingIndex + 1).join("\n"));
914
+ if (payload === "") {
915
+ continue;
916
+ }
917
+ cues.push({ start, end, text: payload });
918
+ }
919
+ return cues.map((cue, index) => ({ cue, index })).sort((a, b) => a.cue.start - b.cue.start || a.index - b.index).map(({ cue }) => cue);
920
+ }
921
+ function cueIndexAt(cues, time) {
922
+ let low = 0;
923
+ let high = cues.length - 1;
924
+ let found = -1;
925
+ while (low <= high) {
926
+ const middle = low + high >> 1;
927
+ if (cues[middle].start <= time) {
928
+ found = middle;
929
+ low = middle + 1;
930
+ } else {
931
+ high = middle - 1;
932
+ }
933
+ }
934
+ for (let index = found; index >= 0 && index > found - 8; index -= 1) {
935
+ const cue = cues[index];
936
+ if (time < cue.end) {
937
+ return index;
938
+ }
939
+ }
940
+ return -1;
941
+ }
942
+ var BUBBLE_HALF = 120;
943
+ var MARKER_REACH = 8;
944
+ function Scrubber({
945
+ current,
946
+ duration,
947
+ buffered,
948
+ chapters,
949
+ markers,
950
+ cues,
951
+ previewLimit,
952
+ strings,
953
+ onSeek,
954
+ onScrubChange
955
+ }) {
956
+ const trackRef = useRef(null);
957
+ const [hover, setHover] = useState(null);
958
+ const [dragging, setDragging] = useState(false);
959
+ const ready = duration > 0;
960
+ const percent = (value) => `${ready ? Math.min(100, Math.max(0, value / duration * 100)) : 0}%`;
961
+ const measure = useCallback(
962
+ (clientX) => {
963
+ const rect = trackRef.current?.getBoundingClientRect();
964
+ if (rect === void 0 || rect.width === 0 || !ready) {
965
+ return null;
966
+ }
967
+ const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
968
+ return { x: ratio * rect.width, width: rect.width, time: ratio * duration };
969
+ },
970
+ [duration, ready]
971
+ );
972
+ const onPointerDown = (event) => {
973
+ const at = measure(event.clientX);
974
+ if (at === null) {
975
+ return;
976
+ }
977
+ event.currentTarget.setPointerCapture(event.pointerId);
978
+ setDragging(true);
979
+ onScrubChange(true);
980
+ setHover(at);
981
+ onSeek(at.time);
982
+ };
983
+ const onPointerMove = (event) => {
984
+ const at = measure(event.clientX);
985
+ if (at === null) {
986
+ return;
987
+ }
988
+ setHover(at);
989
+ if (dragging) {
990
+ onSeek(at.time);
991
+ }
992
+ };
993
+ const endDrag = (event) => {
994
+ if (!dragging) {
995
+ return;
996
+ }
997
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
998
+ event.currentTarget.releasePointerCapture(event.pointerId);
999
+ }
1000
+ setDragging(false);
1001
+ onScrubChange(false);
1002
+ };
1003
+ const onKeyDown = (event) => {
1004
+ if (!ready) {
1005
+ return;
1006
+ }
1007
+ const step = event.shiftKey ? 1 : 5;
1008
+ const moves = {
1009
+ ArrowLeft: -step,
1010
+ ArrowRight: step,
1011
+ ArrowDown: -step,
1012
+ ArrowUp: step,
1013
+ PageDown: -30,
1014
+ PageUp: 30
1015
+ };
1016
+ const move = moves[event.key];
1017
+ if (move !== void 0) {
1018
+ event.preventDefault();
1019
+ event.stopPropagation();
1020
+ onSeek(current + move);
1021
+ } else if (event.key === "Home" || event.key === "End") {
1022
+ event.preventDefault();
1023
+ event.stopPropagation();
1024
+ onSeek(event.key === "Home" ? 0 : duration);
1025
+ }
1026
+ };
1027
+ const chapterAt = (time) => [...chapters].reverse().find((chapter) => chapter.at <= time);
1028
+ const hoverCue = hover !== null && hover.time <= previewLimit ? cues[cueIndexAt(cues, hover.time)] : void 0;
1029
+ const hoverMarker = hover === null ? void 0 : markers.find(
1030
+ (marker) => Math.abs(marker.at / duration * hover.width - hover.x) <= MARKER_REACH
1031
+ );
1032
+ const hoverChapter = hover === null ? void 0 : chapterAt(hover.time);
1033
+ const bubbleLeft = hover === null ? 0 : hover.width > BUBBLE_HALF * 2 ? Math.min(hover.width - BUBBLE_HALF, Math.max(BUBBLE_HALF, hover.x)) : hover.width / 2;
1034
+ const currentChapter = chapterAt(current);
1035
+ return /* @__PURE__ */ jsxs(
1036
+ "div",
1037
+ {
1038
+ ref: trackRef,
1039
+ className: "lk-iv-scrubber",
1040
+ role: "slider",
1041
+ tabIndex: ready ? 0 : -1,
1042
+ "aria-label": strings.media.seek,
1043
+ "aria-valuemin": 0,
1044
+ "aria-valuemax": Math.round(duration),
1045
+ "aria-valuenow": Math.round(current),
1046
+ "aria-valuetext": currentChapter === void 0 ? strings.media.timeValue(clock(current), clock(duration)) : `${strings.media.timeValue(clock(current), clock(duration))}, ${currentChapter.title}`,
1047
+ "data-dragging": dragging || void 0,
1048
+ onPointerDown,
1049
+ onPointerMove,
1050
+ onPointerUp: endDrag,
1051
+ onPointerCancel: endDrag,
1052
+ onPointerLeave: () => {
1053
+ if (!dragging) {
1054
+ setHover(null);
1055
+ }
1056
+ },
1057
+ onKeyDown,
1058
+ children: [
1059
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-scrubber-track", "aria-hidden": "true", children: [
1060
+ /* @__PURE__ */ jsx("div", { className: "lk-iv-scrubber-buffered", style: { width: percent(buffered) } }),
1061
+ /* @__PURE__ */ jsx("div", { className: "lk-iv-scrubber-played", style: { width: percent(current) } }),
1062
+ chapters.filter((chapter) => chapter.at > 0 && chapter.at < duration).map((chapter) => /* @__PURE__ */ jsx(
1063
+ "span",
1064
+ {
1065
+ className: "lk-iv-scrubber-gap",
1066
+ style: { insetInlineStart: percent(chapter.at) }
1067
+ },
1068
+ `chapter-${chapter.at}`
1069
+ ))
1070
+ ] }),
1071
+ /* @__PURE__ */ jsx(
1072
+ "div",
1073
+ {
1074
+ className: "lk-iv-scrubber-thumb",
1075
+ "aria-hidden": "true",
1076
+ style: { insetInlineStart: percent(current) }
1077
+ }
1078
+ ),
1079
+ markers.map((marker) => /* @__PURE__ */ jsx(
1080
+ "span",
1081
+ {
1082
+ className: "lk-iv-marker",
1083
+ "data-state": marker.state,
1084
+ "data-required": marker.required || void 0,
1085
+ "aria-hidden": "true",
1086
+ style: { insetInlineStart: percent(Math.min(marker.at, duration)) }
1087
+ },
1088
+ marker.id
1089
+ )),
1090
+ hover !== null ? /* @__PURE__ */ jsxs(
1091
+ "div",
1092
+ {
1093
+ className: "lk-iv-bubble",
1094
+ "aria-hidden": "true",
1095
+ style: { insetInlineStart: `${bubbleLeft}px` },
1096
+ children: [
1097
+ /* @__PURE__ */ jsxs("span", { className: "lk-iv-bubble-time", children: [
1098
+ clock(hover.time),
1099
+ hoverChapter !== void 0 ? /* @__PURE__ */ jsxs("span", { className: "lk-iv-bubble-chapter", children: [
1100
+ " \xB7 ",
1101
+ hoverChapter.title
1102
+ ] }) : null
1103
+ ] }),
1104
+ hoverMarker !== void 0 ? /* @__PURE__ */ jsx("span", { className: "lk-iv-bubble-quiz", children: hoverMarker.label }) : null,
1105
+ hoverCue !== void 0 ? /* @__PURE__ */ jsx("span", { className: "lk-iv-bubble-caption", children: hoverCue.text }) : null
1106
+ ]
1107
+ }
1108
+ ) : null
1109
+ ]
1110
+ }
1111
+ );
1112
+ }
1113
+ function contentKey(group) {
1114
+ try {
1115
+ return JSON.stringify([group.id, group.stimulus, group.items, group.timeline]);
1116
+ } catch {
1117
+ return String(group?.id);
1118
+ }
1119
+ }
1120
+ function InteractiveVideo(props) {
1121
+ const key = useMemo(() => contentKey(props.group), [props.group]);
1122
+ return /* @__PURE__ */ jsx(Player, { ...props }, key);
1123
+ }
1124
+ function onNextFrame(run) {
1125
+ if (typeof requestAnimationFrame === "function") {
1126
+ const frame = requestAnimationFrame(run);
1127
+ return () => cancelAnimationFrame(frame);
1128
+ }
1129
+ const timer = setTimeout(run, 33);
1130
+ return () => clearTimeout(timer);
1131
+ }
1132
+ function trackKey(track) {
1133
+ return `${track.kind}:${track.srclang}:${track.src}`;
1134
+ }
1135
+ var NO_CUES = [];
1136
+ function useCaptionLine(track, load, peek) {
1137
+ const key = track === void 0 ? void 0 : trackKey(track);
1138
+ const [line, setLine] = useState({
1139
+ cues: NO_CUES,
1140
+ failed: false
1141
+ });
1142
+ const trackRef = useRef(track);
1143
+ trackRef.current = track;
1144
+ useEffect(() => {
1145
+ const wanted = trackRef.current;
1146
+ if (key === void 0 || wanted === void 0) {
1147
+ return;
1148
+ }
1149
+ let live = true;
1150
+ load(wanted).then(
1151
+ (cues) => {
1152
+ if (live) {
1153
+ setLine({ key, cues, failed: false });
1154
+ }
1155
+ },
1156
+ () => {
1157
+ if (live) {
1158
+ setLine({ key, cues: NO_CUES, failed: true });
1159
+ }
1160
+ }
1161
+ );
1162
+ return () => {
1163
+ live = false;
1164
+ };
1165
+ }, [key, load]);
1166
+ if (key === void 0) {
1167
+ return { cues: NO_CUES, failed: false };
1168
+ }
1169
+ if (line.key === key) {
1170
+ return line;
1171
+ }
1172
+ return { cues: peek(key) ?? NO_CUES, failed: false };
1173
+ }
1174
+ function captionsShown(tracks, preferences) {
1175
+ if (!preferences.captions) {
1176
+ return { srclang: null, secondary: null };
1177
+ }
1178
+ const shown = resolveCaptionTracks(tracks, preferences);
1179
+ return { srclang: shown.primary?.srclang ?? null, secondary: shown.secondary?.srclang ?? null };
1180
+ }
1181
+ var playerCount = 0;
1182
+ var ITEM_TYPES = /* @__PURE__ */ new Set([
1183
+ "multiple-choice",
1184
+ "fill-in-the-blanks",
1185
+ "gap-select",
1186
+ "dictation",
1187
+ "read-aloud"
1188
+ ]);
1189
+ function Player(props) {
1190
+ const {
1191
+ renderMode = "practice",
1192
+ onProgress,
1193
+ onSubmit,
1194
+ onActivityComplete,
1195
+ onFinished,
1196
+ onInteraction,
1197
+ recordingBinding,
1198
+ workletUrl,
1199
+ captionsLoader,
1200
+ label,
1201
+ shuffleSeed,
1202
+ locale,
1203
+ theme,
1204
+ sanitizeHtml
1205
+ } = props;
1206
+ const strings = useLkStrings(props.strings);
1207
+ const group = useRef(props.group).current;
1208
+ const timeline = group.timeline ?? { cues: [] };
1209
+ const media = group.stimulus.media;
1210
+ const chapters = timeline.chapters ?? [];
1211
+ const devError = useMemo(() => {
1212
+ if (!isDevelopment()) {
1213
+ return null;
1214
+ }
1215
+ if (group.timeline === void 0) {
1216
+ return new Error(
1217
+ `InteractiveVideo "${group.id}" was given an item group with no timeline. Give it a timeline \u2014 a list of quizzes, each at a moment of the video \u2014 or render it with <ActivitySequence>.`
1218
+ );
1219
+ }
1220
+ if (group.redacted === true) {
1221
+ try {
1222
+ assertRedactedItemGroup(group);
1223
+ } catch (error) {
1224
+ return error;
1225
+ }
1226
+ return null;
1227
+ }
1228
+ const result = validateItemGroup(group);
1229
+ return result.success ? null : new ActivitySchemaError("item-group", result.errors);
1230
+ }, [group]);
1231
+ const slots = useMemo(() => {
1232
+ try {
1233
+ return flattenSequence([group]);
1234
+ } catch {
1235
+ return [];
1236
+ }
1237
+ }, [group]);
1238
+ const cues = useMemo(
1239
+ () => timeline.cues.filter(
1240
+ (cue) => slots.some((slot) => slot.group?.cue?.id === cue.id && ITEM_TYPES.has(slot.activity.type))
1241
+ ),
1242
+ [timeline.cues, slots]
1243
+ );
1244
+ const slotsByCue = useMemo(() => {
1245
+ const byCue = /* @__PURE__ */ new Map();
1246
+ for (const slot of slots) {
1247
+ const cueId = slot.group?.cue?.id;
1248
+ if (cueId !== void 0 && ITEM_TYPES.has(slot.activity.type)) {
1249
+ byCue.set(cueId, [...byCue.get(cueId) ?? [], slot]);
1250
+ }
1251
+ }
1252
+ return byCue;
1253
+ }, [slots]);
1254
+ const [playerId] = useState(() => {
1255
+ playerCount += 1;
1256
+ return `lk-iv-${playerCount}`;
1257
+ });
1258
+ const forced = useRef(props.preferences).current;
1259
+ const defaultsKey = JSON.stringify(props.defaultPreferences ?? null);
1260
+ const defaults = useMemo(() => JSON.parse(defaultsKey), [defaultsKey]);
1261
+ const chosen = useRef({});
1262
+ const [preferences, setPreferences] = useState(
1263
+ () => resolvePreferences({ force: forced, defaults: props.defaultPreferences })
1264
+ );
1265
+ const preferencesRef = useRef(preferences);
1266
+ preferencesRef.current = preferences;
1267
+ useEffect(() => {
1268
+ setPreferences(
1269
+ resolvePreferences({
1270
+ force: forced,
1271
+ stored: readStoredPreferences(),
1272
+ defaults,
1273
+ chosen: chosen.current
1274
+ })
1275
+ );
1276
+ }, [forced, defaults]);
1277
+ const [submitted] = useState(() => new Set(props.submittedSlotIds ?? []));
1278
+ const [answered, setAnswered] = useState(() => {
1279
+ const seeded = /* @__PURE__ */ new Set();
1280
+ for (const slot of slots) {
1281
+ if (submitted.has(slot.slotId) || props.outcomes !== void 0 && Object.hasOwn(props.outcomes, slot.slotId)) {
1282
+ seeded.add(slot.slotId);
1283
+ }
1284
+ }
1285
+ return seeded;
1286
+ });
1287
+ const answeredRef = useRef(answered);
1288
+ answeredRef.current = answered;
1289
+ const scores = useRef(/* @__PURE__ */ new Map());
1290
+ const markAnswered = useCallback((slotId) => {
1291
+ setAnswered((previous) => {
1292
+ if (previous.has(slotId)) {
1293
+ return previous;
1294
+ }
1295
+ const next = new Set(previous);
1296
+ next.add(slotId);
1297
+ answeredRef.current = next;
1298
+ return next;
1299
+ });
1300
+ }, []);
1301
+ const finished = useCallback(
1302
+ (cueId) => (slotsByCue.get(cueId) ?? []).every((slot) => answeredRef.current.has(slot.slotId)),
1303
+ [slotsByCue]
1304
+ );
1305
+ const shellRef = useRef(null);
1306
+ const videoRef = useRef(null);
1307
+ const playButtonRef = useRef(null);
1308
+ const quizHeadingRef = useRef(null);
1309
+ const [playing, setPlaying] = useState(false);
1310
+ const [waiting, setWaiting] = useState(false);
1311
+ const [errorCode, setErrorCode] = useState(null);
1312
+ const [current, setCurrent] = useState(0);
1313
+ const [duration, setDuration] = useState(0);
1314
+ const [buffered, setBuffered] = useState(0);
1315
+ const [scrubbing, setScrubbing] = useState(false);
1316
+ const [ended, setEnded] = useState(false);
1317
+ const [fullscreen, setFullscreen] = useState(false);
1318
+ const [pictureInPicture, setPictureInPicture] = useState(false);
1319
+ const [countdown, setCountdown] = useState(false);
1320
+ const [menu, setMenu] = useState(null);
1321
+ const [sheet, setSheet] = useState(false);
1322
+ const [panelTab, setPanelTab] = useState("contents");
1323
+ const [chromeShown, setChromeShown] = useState(true);
1324
+ const [focusInChrome, setFocusInChrome] = useState(false);
1325
+ const [announcement, setAnnouncement] = useState("");
1326
+ const previousTime = useRef(0);
1327
+ const furthest = useRef(0);
1328
+ const [furthestShown, setFurthestShown] = useState(0);
1329
+ const lastProgressAt = useRef(0);
1330
+ const hideTimer = useRef(null);
1331
+ const lastPointer = useRef("mouse");
1332
+ const finishedOnce = useRef(false);
1333
+ const [openQuiz, setOpenQuiz] = useState(null);
1334
+ const openQuizRef = useRef(openQuiz);
1335
+ openQuizRef.current = openQuiz;
1336
+ const [mounted, setMounted] = useState(/* @__PURE__ */ new Set());
1337
+ const handled = useRef(/* @__PURE__ */ new Set());
1338
+ const announce = useCallback((text) => {
1339
+ setAnnouncement("");
1340
+ onNextFrame(() => setAnnouncement(text));
1341
+ }, []);
1342
+ const emit = useCallback(
1343
+ (type, payload) => {
1344
+ onInteraction?.({
1345
+ type,
1346
+ activityId: group.id,
1347
+ timestamp: Date.now(),
1348
+ ...payload !== void 0 ? { payload } : {}
1349
+ });
1350
+ },
1351
+ [onInteraction, group.id]
1352
+ );
1353
+ const reportProgress = useCallback(
1354
+ (force) => {
1355
+ const video = videoRef.current;
1356
+ if (onProgress === void 0 || video === null) {
1357
+ return;
1358
+ }
1359
+ const now = Date.now();
1360
+ if (!force && now - lastProgressAt.current < 5e3) {
1361
+ return;
1362
+ }
1363
+ lastProgressAt.current = now;
1364
+ onProgress({ progressVersion: "1.0", at: video.currentTime, furthest: furthest.current });
1365
+ },
1366
+ [onProgress]
1367
+ );
1368
+ const quizTitle = useCallback(
1369
+ (cue) => cue.title ?? strings.videoQuiz,
1370
+ [strings.videoQuiz]
1371
+ );
1372
+ const openAt = useCallback(
1373
+ (cue) => {
1374
+ const video = videoRef.current;
1375
+ const quizSlots = slotsByCue.get(cue.id) ?? [];
1376
+ if (video !== null) {
1377
+ video.pause();
1378
+ if (Math.abs(video.currentTime - cue.at) > 0.25 && cue.at <= (video.duration || cue.at)) {
1379
+ video.currentTime = Math.min(cue.at, video.duration || cue.at);
1380
+ }
1381
+ previousTime.current = cue.at;
1382
+ const webkit = video;
1383
+ if (webkit.webkitDisplayingFullscreen) {
1384
+ webkit.webkitExitFullscreen?.();
1385
+ }
1386
+ if (typeof document !== "undefined" && document.pictureInPictureElement === video) {
1387
+ void document.exitPictureInPicture?.().catch(() => void 0);
1388
+ }
1389
+ }
1390
+ const firstOpen = quizSlots.findIndex((slot) => !answeredRef.current.has(slot.slotId));
1391
+ setMenu(null);
1392
+ setSheet(false);
1393
+ setEnded(false);
1394
+ setMounted((previous) => previous.has(cue.id) ? previous : /* @__PURE__ */ new Set([...previous, cue.id]));
1395
+ setOpenQuiz({ cueId: cue.id, step: firstOpen === -1 ? 0 : firstOpen });
1396
+ announce(strings.videoQuizOpened(clock(cue.at), quizSlots.length));
1397
+ emit("video-quiz-opened", { cueId: cue.id, at: cue.at });
1398
+ reportProgress(true);
1399
+ },
1400
+ [slotsByCue, announce, strings, emit, reportProgress]
1401
+ );
1402
+ useEffect(() => {
1403
+ if (openQuiz !== null) {
1404
+ quizHeadingRef.current?.focus();
1405
+ }
1406
+ }, [openQuiz?.cueId, openQuiz]);
1407
+ const closeQuiz = useCallback(
1408
+ (how) => {
1409
+ const open = openQuizRef.current;
1410
+ if (open === null) {
1411
+ return;
1412
+ }
1413
+ const cue = cues.find((candidate) => candidate.id === open.cueId);
1414
+ setOpenQuiz(null);
1415
+ const video = videoRef.current;
1416
+ if (how === "rewatch" && cue !== void 0 && video !== null) {
1417
+ const earlier = [...cues.map((c) => c.at), ...chapters.map((c) => c.at)].filter((at) => at < cue.at - 0.5).sort((a, b) => b - a)[0];
1418
+ const to = earlier ?? 0;
1419
+ video.currentTime = to;
1420
+ previousTime.current = to;
1421
+ setCurrent(to);
1422
+ } else if (cue !== void 0) {
1423
+ handled.current.add(cue.id);
1424
+ emit(how === "skip" ? "video-quiz-skipped" : "video-quiz-closed", { cueId: cue.id });
1425
+ }
1426
+ playButtonRef.current?.focus();
1427
+ if (video !== null && !(video.ended || duration > 0 && video.currentTime >= duration - 0.05)) {
1428
+ void video.play().catch(() => setPlaying(false));
1429
+ } else if (video !== null) {
1430
+ const next = quizzesAtEnd(cues, duration, handled.current)[0];
1431
+ if (next !== void 0) {
1432
+ openAt(next);
1433
+ } else {
1434
+ setEnded(true);
1435
+ }
1436
+ }
1437
+ },
1438
+ [cues, chapters, emit, duration, openAt]
1439
+ );
1440
+ const onTick = useCallback(
1441
+ (now) => {
1442
+ const before = previousTime.current;
1443
+ previousTime.current = now;
1444
+ if (now > furthest.current) {
1445
+ furthest.current = now;
1446
+ }
1447
+ if (openQuizRef.current !== null || scrubbing) {
1448
+ return;
1449
+ }
1450
+ const crossed = crossedQuiz(cues, before, now, handled.current);
1451
+ if (crossed !== void 0) {
1452
+ openAt(crossed);
1453
+ }
1454
+ },
1455
+ [cues, scrubbing, openAt]
1456
+ );
1457
+ useEffect(() => {
1458
+ if (!playing || scrubbing) {
1459
+ return;
1460
+ }
1461
+ let cancel = () => {
1462
+ };
1463
+ const tick = () => {
1464
+ const video = videoRef.current;
1465
+ if (video !== null) {
1466
+ setCurrent(video.currentTime);
1467
+ setBuffered(bufferedAhead(video));
1468
+ onTick(video.currentTime);
1469
+ if (furthest.current > furthestShown + 1) {
1470
+ setFurthestShown(furthest.current);
1471
+ }
1472
+ reportProgress(false);
1473
+ }
1474
+ cancel = onNextFrame(tick);
1475
+ };
1476
+ cancel = onNextFrame(tick);
1477
+ return () => cancel();
1478
+ }, [playing, scrubbing, onTick, reportProgress, furthestShown]);
1479
+ const seek = useCallback(
1480
+ (to) => {
1481
+ const video = videoRef.current;
1482
+ if (video === null || duration <= 0) {
1483
+ return;
1484
+ }
1485
+ const from = video.currentTime;
1486
+ const target = Math.min(Math.max(0, to), duration);
1487
+ const limited = renderMode === "review" ? { at: target } : limitSeek(timeline, from, target, furthest.current, finished);
1488
+ video.currentTime = limited.at;
1489
+ previousTime.current = limited.at;
1490
+ setCurrent(limited.at);
1491
+ setEnded(false);
1492
+ emit("video-seeked", { from, to: limited.at, clamped: limited.hold !== void 0 });
1493
+ if (limited.hold?.kind === "required") {
1494
+ openAt(limited.hold.cue);
1495
+ announce(strings.videoHeldAtQuiz(clock(limited.hold.cue.at)));
1496
+ } else if (limited.hold?.kind === "no-skip-ahead") {
1497
+ announce(strings.videoHeldAhead);
1498
+ }
1499
+ reportProgress(true);
1500
+ },
1501
+ [duration, renderMode, timeline, finished, emit, announce, strings, openAt, reportProgress]
1502
+ );
1503
+ const togglePlay = useCallback(() => {
1504
+ const video = videoRef.current;
1505
+ if (video === null) {
1506
+ return;
1507
+ }
1508
+ if (ended) {
1509
+ setEnded(false);
1510
+ handled.current.clear();
1511
+ video.currentTime = 0;
1512
+ previousTime.current = -1;
1513
+ void video.play().catch(() => setPlaying(false));
1514
+ return;
1515
+ }
1516
+ if (video.paused) {
1517
+ if (video.currentTime === 0 && previousTime.current <= 0) {
1518
+ const atStart = crossedQuiz(cues, -1, 0, handled.current);
1519
+ if (atStart !== void 0) {
1520
+ previousTime.current = 0;
1521
+ openAt(atStart);
1522
+ return;
1523
+ }
1524
+ }
1525
+ void video.play().catch(() => setPlaying(false));
1526
+ } else {
1527
+ video.pause();
1528
+ }
1529
+ }, [ended, cues, openAt]);
1530
+ const tracks = useMemo(() => {
1531
+ if (media?.tracks !== void 0 && media.tracks.length > 0) {
1532
+ return media.tracks.filter(
1533
+ (track) => track.kind === "captions" || track.kind === "subtitles"
1534
+ );
1535
+ }
1536
+ return media?.captionsUrl !== void 0 ? [
1537
+ {
1538
+ kind: "captions",
1539
+ src: media.captionsUrl,
1540
+ srclang: locale ?? "en",
1541
+ label: strings.videoCaptionLanguage
1542
+ }
1543
+ ] : [];
1544
+ }, [media, locale, strings.videoCaptionLanguage]);
1545
+ const { primary: primaryTrack, secondary: secondaryTrack } = useMemo(
1546
+ () => resolveCaptionTracks(tracks, {
1547
+ captionLanguage: preferences.captionLanguage,
1548
+ secondaryCaptionLanguage: preferences.secondaryCaptionLanguage
1549
+ }),
1550
+ [tracks, preferences.captionLanguage, preferences.secondaryCaptionLanguage]
1551
+ );
1552
+ const loaderRef = useRef(captionsLoader);
1553
+ loaderRef.current = captionsLoader;
1554
+ const loading = useRef(/* @__PURE__ */ new Map());
1555
+ const loaded = useRef(/* @__PURE__ */ new Map());
1556
+ const loadCues = useCallback((track) => {
1557
+ const key = trackKey(track);
1558
+ const pending = loading.current.get(key);
1559
+ if (pending !== void 0) {
1560
+ return pending;
1561
+ }
1562
+ const request = Promise.resolve().then(
1563
+ () => loaderRef.current ? loaderRef.current(track) : fetch(track.src, { credentials: "same-origin" }).then((response) => {
1564
+ if (!response.ok) {
1565
+ throw new Error(String(response.status));
1566
+ }
1567
+ return response.text();
1568
+ })
1569
+ ).then((text) => {
1570
+ const parsed = parseWebVtt(text);
1571
+ if (parsed.length === 0) {
1572
+ throw new Error("The caption file held no cues.");
1573
+ }
1574
+ loaded.current.set(key, parsed);
1575
+ return parsed;
1576
+ });
1577
+ loading.current.set(key, request);
1578
+ request.catch(() => {
1579
+ loading.current.delete(key);
1580
+ });
1581
+ return request;
1582
+ }, []);
1583
+ const peekCues = useCallback((key) => loaded.current.get(key), []);
1584
+ const primaryLine = useCaptionLine(primaryTrack, loadCues, peekCues);
1585
+ const secondaryLine = useCaptionLine(secondaryTrack, loadCues, peekCues);
1586
+ const primaryCues = primaryLine.cues;
1587
+ const secondaryCues = secondaryLine.cues;
1588
+ const captionsFailed = { primary: primaryLine.failed, secondary: secondaryLine.failed };
1589
+ const hasCaptions = primaryCues.length > 0 || secondaryCues.length > 0;
1590
+ const hasTranscript = primaryCues.length > 0;
1591
+ const activeCueIndex = useMemo(() => cueIndexAt(primaryCues, current), [primaryCues, current]);
1592
+ const secondaryCueIndex = useMemo(
1593
+ () => cueIndexAt(secondaryCues, current),
1594
+ [secondaryCues, current]
1595
+ );
1596
+ const spoilerLimit = timeline.navigation === "no-skip-ahead" && renderMode !== "review" ? Math.max(furthestShown, current) : Number.POSITIVE_INFINITY;
1597
+ const onPreferencesChangeRef = useRef(props.onPreferencesChange);
1598
+ onPreferencesChangeRef.current = props.onPreferencesChange;
1599
+ const tracksRef = useRef(tracks);
1600
+ tracksRef.current = tracks;
1601
+ const lastSecondary = useRef(null);
1602
+ const changePreferences = useCallback(
1603
+ (change) => {
1604
+ const previous = preferencesRef.current;
1605
+ const next = applyPreferenceChange(previous, change);
1606
+ if (previous.secondaryCaptionLanguage !== null) {
1607
+ lastSecondary.current = previous.secondaryCaptionLanguage;
1608
+ }
1609
+ chosen.current = { ...chosen.current, ...change };
1610
+ preferencesRef.current = next;
1611
+ setPreferences(next);
1612
+ rememberPreferences(change);
1613
+ onPreferencesChangeRef.current?.(next, change);
1614
+ const before = captionsShown(tracksRef.current, previous);
1615
+ const after = captionsShown(tracksRef.current, next);
1616
+ if (before.srclang !== after.srclang || before.secondary !== after.secondary) {
1617
+ emit("video-captions-changed", after);
1618
+ }
1619
+ },
1620
+ [emit]
1621
+ );
1622
+ const chooseCaptionTrack = (track) => {
1623
+ const swaps = primaryTrack !== void 0 && secondaryTrack !== void 0 && track.srclang.toLowerCase() === secondaryTrack.srclang.toLowerCase();
1624
+ changePreferences({
1625
+ captions: true,
1626
+ captionLanguage: track.srclang,
1627
+ ...swaps ? { secondaryCaptionLanguage: primaryTrack.srclang } : {}
1628
+ });
1629
+ };
1630
+ const chooseSecondaryTrack = (track) => {
1631
+ changePreferences(
1632
+ track === null ? { secondaryCaptionLanguage: null } : { secondaryCaptionLanguage: track.srclang, captions: true }
1633
+ );
1634
+ };
1635
+ const toggleSecondary = () => {
1636
+ if (secondaryTrack !== void 0) {
1637
+ changePreferences(
1638
+ preferences.captions ? { secondaryCaptionLanguage: null } : { captions: true }
1639
+ );
1640
+ return;
1641
+ }
1642
+ const wanted = [
1643
+ lastSecondary.current,
1644
+ preferences.secondaryCaptionLanguage,
1645
+ sanitizePartialPreferences(defaults).secondaryCaptionLanguage ?? null,
1646
+ secondaryCandidates(tracks, primaryTrack)[0]?.srclang ?? null
1647
+ ];
1648
+ for (const language of wanted) {
1649
+ if (language === null) {
1650
+ continue;
1651
+ }
1652
+ const { secondary } = resolveCaptionTracks(tracks, {
1653
+ captionLanguage: preferences.captionLanguage,
1654
+ secondaryCaptionLanguage: language
1655
+ });
1656
+ if (secondary !== void 0) {
1657
+ changePreferences({ secondaryCaptionLanguage: secondary.srclang, captions: true });
1658
+ return;
1659
+ }
1660
+ }
1661
+ announce(strings.videoNoSecondLanguage);
1662
+ };
1663
+ useEffect(() => {
1664
+ const video = videoRef.current;
1665
+ if (video !== null) {
1666
+ video.playbackRate = preferences.speed;
1667
+ video.volume = preferences.volume;
1668
+ video.muted = preferences.muted;
1669
+ }
1670
+ }, [preferences.speed, preferences.volume, preferences.muted]);
1671
+ useEffect(() => {
1672
+ const onChange = () => setFullscreen(document.fullscreenElement === shellRef.current);
1673
+ document.addEventListener("fullscreenchange", onChange);
1674
+ return () => document.removeEventListener("fullscreenchange", onChange);
1675
+ }, []);
1676
+ useEffect(() => {
1677
+ const video = videoRef.current;
1678
+ if (video === null) {
1679
+ return;
1680
+ }
1681
+ const enter = () => {
1682
+ setPictureInPicture(true);
1683
+ emit("video-pip-changed", { on: true });
1684
+ };
1685
+ const leave = () => {
1686
+ setPictureInPicture(false);
1687
+ emit("video-pip-changed", { on: false });
1688
+ };
1689
+ video.addEventListener("enterpictureinpicture", enter);
1690
+ video.addEventListener("leavepictureinpicture", leave);
1691
+ return () => {
1692
+ video.removeEventListener("enterpictureinpicture", enter);
1693
+ video.removeEventListener("leavepictureinpicture", leave);
1694
+ };
1695
+ }, [emit]);
1696
+ useEffect(
1697
+ () => () => {
1698
+ if (hideTimer.current !== null) {
1699
+ clearTimeout(hideTimer.current);
1700
+ }
1701
+ },
1702
+ []
1703
+ );
1704
+ const revealChrome = useCallback(() => {
1705
+ setChromeShown(true);
1706
+ if (hideTimer.current !== null) {
1707
+ clearTimeout(hideTimer.current);
1708
+ }
1709
+ hideTimer.current = setTimeout(() => setChromeShown(false), 2400);
1710
+ }, []);
1711
+ const toggleFullscreen = useCallback(() => {
1712
+ const shell = shellRef.current;
1713
+ const video = videoRef.current;
1714
+ if (typeof document === "undefined" || shell === null) {
1715
+ return;
1716
+ }
1717
+ if (document.fullscreenElement) {
1718
+ void document.exitFullscreen().catch(() => void 0);
1719
+ } else if (typeof shell.requestFullscreen === "function" && document.fullscreenEnabled !== false) {
1720
+ void shell.requestFullscreen().catch(() => void 0);
1721
+ } else {
1722
+ video?.webkitEnterFullscreen?.();
1723
+ }
1724
+ emit("video-fullscreen-changed");
1725
+ }, [emit]);
1726
+ const pictureInPictureSupported = typeof document !== "undefined" && document.pictureInPictureEnabled === true;
1727
+ const togglePictureInPicture = useCallback(() => {
1728
+ const video = videoRef.current;
1729
+ if (video === null) {
1730
+ return;
1731
+ }
1732
+ if (document.pictureInPictureElement) {
1733
+ void document.exitPictureInPicture().catch(() => void 0);
1734
+ } else {
1735
+ void video.requestPictureInPicture?.().catch(() => void 0);
1736
+ }
1737
+ }, []);
1738
+ const jumpPoints = useMemo(
1739
+ () => [.../* @__PURE__ */ new Set([...chapters.map((c) => c.at), ...cues.map((c) => c.at)])].sort((a, b) => a - b),
1740
+ [chapters, cues]
1741
+ );
1742
+ const onKeyDown = (event) => {
1743
+ if (openQuiz !== null || !preferences.shortcuts) {
1744
+ return;
1745
+ }
1746
+ const target = event.target;
1747
+ if (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName) || target.tagName === "BUTTON" && (event.key === " " || event.key === "Enter")) {
1748
+ return;
1749
+ }
1750
+ if (event.ctrlKey || event.metaKey || event.altKey) {
1751
+ return;
1752
+ }
1753
+ const video = videoRef.current;
1754
+ const key = event.key;
1755
+ const done = () => {
1756
+ event.preventDefault();
1757
+ revealChrome();
1758
+ };
1759
+ if (/^[0-9]$/.test(key)) {
1760
+ done();
1761
+ seek(Number(key) / 10 * duration);
1762
+ return;
1763
+ }
1764
+ switch (key) {
1765
+ case " ":
1766
+ case "k":
1767
+ case "K":
1768
+ done();
1769
+ togglePlay();
1770
+ return;
1771
+ case "j":
1772
+ case "J":
1773
+ done();
1774
+ seek(current - JUMP_SECONDS);
1775
+ return;
1776
+ case "l":
1777
+ case "L":
1778
+ done();
1779
+ seek(current + JUMP_SECONDS);
1780
+ return;
1781
+ case "ArrowLeft":
1782
+ done();
1783
+ seek(current - (event.shiftKey ? 1 : 5));
1784
+ return;
1785
+ case "ArrowRight":
1786
+ done();
1787
+ seek(current + (event.shiftKey ? 1 : 5));
1788
+ return;
1789
+ case "Home":
1790
+ done();
1791
+ seek(0);
1792
+ return;
1793
+ case "End":
1794
+ done();
1795
+ seek(duration);
1796
+ return;
1797
+ case ",":
1798
+ case ".":
1799
+ done();
1800
+ video?.pause();
1801
+ seek(current + (key === "," ? -FRAME_SECONDS : FRAME_SECONDS));
1802
+ return;
1803
+ case "<":
1804
+ case ">":
1805
+ done();
1806
+ changePreferences({ speed: nudgeSpeed(preferences.speed, key === "<" ? -1 : 1) });
1807
+ return;
1808
+ case "[": {
1809
+ done();
1810
+ const previous = [...jumpPoints].reverse().find((at) => at < current - 1);
1811
+ seek(previous ?? 0);
1812
+ return;
1813
+ }
1814
+ case "]": {
1815
+ done();
1816
+ const next = jumpPoints.find((at) => at > current + 0.5);
1817
+ if (next !== void 0) {
1818
+ seek(next);
1819
+ }
1820
+ return;
1821
+ }
1822
+ case "m":
1823
+ case "M":
1824
+ done();
1825
+ changePreferences({ muted: !preferences.muted });
1826
+ return;
1827
+ case "c":
1828
+ case "C":
1829
+ if (event.shiftKey) {
1830
+ done();
1831
+ toggleSecondary();
1832
+ } else if (hasCaptions) {
1833
+ done();
1834
+ changePreferences({ captions: !preferences.captions });
1835
+ }
1836
+ return;
1837
+ case "t":
1838
+ case "T":
1839
+ if (hasTranscript) {
1840
+ done();
1841
+ setPanelTab("transcript");
1842
+ changePreferences({ panel: !(preferences.panel && panelTab === "transcript") });
1843
+ }
1844
+ return;
1845
+ case "p":
1846
+ case "P":
1847
+ if (pictureInPictureSupported) {
1848
+ done();
1849
+ togglePictureInPicture();
1850
+ }
1851
+ return;
1852
+ case "f":
1853
+ case "F":
1854
+ done();
1855
+ toggleFullscreen();
1856
+ return;
1857
+ case "?":
1858
+ done();
1859
+ setSheet((open) => !open);
1860
+ return;
1861
+ default:
1862
+ return;
1863
+ }
1864
+ };
1865
+ const quizzes = cues.map((cue) => {
1866
+ const quizSlots = slotsByCue.get(cue.id) ?? [];
1867
+ const count = quizSlots.filter((slot) => answered.has(slot.slotId)).length;
1868
+ return {
1869
+ id: cue.id,
1870
+ at: cue.at,
1871
+ title: quizTitle(cue),
1872
+ answered: count,
1873
+ total: quizSlots.length,
1874
+ // In review the attempt is over: nothing is required of the learner any
1875
+ // more, in the list or on the bar.
1876
+ required: cue.required === true && renderMode !== "review",
1877
+ state: markerState(quizSlots, answered, renderMode, props.outcomes)
1878
+ };
1879
+ });
1880
+ const markers = quizzes.map((quiz) => ({
1881
+ id: quiz.id,
1882
+ at: quiz.at,
1883
+ label: `${quiz.title} \xB7 ${strings.videoQuizProgress(quiz.answered, quiz.total)}`,
1884
+ state: quiz.state,
1885
+ required: quiz.required
1886
+ }));
1887
+ const totalQuestions = quizzes.reduce((sum, quiz) => sum + quiz.total, 0);
1888
+ const answeredCount = quizzes.reduce((sum, quiz) => sum + quiz.answered, 0);
1889
+ const graded = [...scores.current.values()];
1890
+ const percent = graded.length === 0 ? null : Math.round(graded.reduce((a, b) => a + b, 0) / graded.length * 100);
1891
+ const summary = useCallback(() => {
1892
+ return {
1893
+ slots: slots.filter((slot) => slot.group?.cue !== void 0).map((slot) => {
1894
+ const cueId = slot.group?.cue?.id;
1895
+ return {
1896
+ slotId: slot.slotId,
1897
+ activityId: slot.activity.id,
1898
+ cueId,
1899
+ status: answeredRef.current.has(slot.slotId) ? "answered" : handled.current.has(cueId) ? "skipped" : "unreached"
1900
+ };
1901
+ })
1902
+ };
1903
+ }, [slots]);
1904
+ const finish = useCallback(() => {
1905
+ if (finishedOnce.current) {
1906
+ return;
1907
+ }
1908
+ finishedOnce.current = true;
1909
+ onFinished?.(summary());
1910
+ }, [onFinished, summary]);
1911
+ useEffect(() => {
1912
+ if (ended && totalQuestions > 0 && answeredCount === totalQuestions && renderMode !== "review") {
1913
+ finish();
1914
+ }
1915
+ }, [ended, totalQuestions, answeredCount, renderMode, finish]);
1916
+ const slotBindings = useMemo(() => {
1917
+ const bindings = /* @__PURE__ */ new Map();
1918
+ if (recordingBinding === void 0) {
1919
+ return bindings;
1920
+ }
1921
+ const { assess, playbackUrl } = recordingBinding;
1922
+ for (const slot of slots) {
1923
+ const place = {
1924
+ slotId: slot.slotId,
1925
+ index: slot.index,
1926
+ activityId: slot.activity.id
1927
+ };
1928
+ bindings.set(slot.slotId, {
1929
+ upload: (take) => recordingBinding.upload(take, place),
1930
+ ...assess !== void 0 ? { assess: (ref) => assess.call(recordingBinding, ref, place) } : {},
1931
+ ...playbackUrl !== void 0 ? { playbackUrl: (ref) => playbackUrl.call(recordingBinding, ref, place) } : {}
1932
+ });
1933
+ }
1934
+ return bindings;
1935
+ }, [slots, recordingBinding]);
1936
+ const renderQuestion = (slot, cueId) => {
1937
+ const activity = slot.activity;
1938
+ const place = {
1939
+ slotId: slot.slotId,
1940
+ index: slot.index,
1941
+ activityId: activity.id,
1942
+ cueId
1943
+ };
1944
+ const restored = props.responses !== void 0 && Object.hasOwn(props.responses, slot.slotId) ? props.responses[slot.slotId] : void 0;
1945
+ const outcome = props.outcomes !== void 0 && Object.hasOwn(props.outcomes, slot.slotId) ? props.outcomes[slot.slotId] : void 0;
1946
+ const common = {
1947
+ renderMode,
1948
+ ...restored !== void 0 ? { defaultValue: restored } : {},
1949
+ ...submitted.has(slot.slotId) ? { defaultSubmitted: true } : {},
1950
+ ...outcome !== void 0 ? { outcome } : {},
1951
+ onSubmit: (response) => {
1952
+ markAnswered(slot.slotId);
1953
+ onSubmit?.(response, place);
1954
+ },
1955
+ onComplete: (result) => {
1956
+ markAnswered(slot.slotId);
1957
+ if (result.maxScore > 0) {
1958
+ scores.current.set(slot.slotId, result.score / result.maxScore);
1959
+ }
1960
+ onActivityComplete?.(result, place);
1961
+ },
1962
+ ...onInteraction !== void 0 ? { onInteraction } : {},
1963
+ ...sanitizeHtml !== void 0 ? { sanitizeHtml } : {},
1964
+ ...props.strings !== void 0 ? { strings: props.strings } : {},
1965
+ ...locale !== void 0 ? { locale } : {}
1966
+ };
1967
+ let question;
1968
+ switch (activity.type) {
1969
+ case "multiple-choice":
1970
+ question = /* @__PURE__ */ jsx(
1971
+ MultipleChoice,
1972
+ {
1973
+ data: activity,
1974
+ ...common,
1975
+ ...shuffleSeed !== void 0 ? { shuffleSeed } : {}
1976
+ }
1977
+ );
1978
+ break;
1979
+ case "fill-in-the-blanks":
1980
+ question = /* @__PURE__ */ jsx(FillInTheBlanks, { data: activity, ...common });
1981
+ break;
1982
+ case "gap-select":
1983
+ question = /* @__PURE__ */ jsx(
1984
+ GapSelect,
1985
+ {
1986
+ data: activity,
1987
+ ...common,
1988
+ ...shuffleSeed !== void 0 ? { shuffleSeed } : {}
1989
+ }
1990
+ );
1991
+ break;
1992
+ case "dictation":
1993
+ question = /* @__PURE__ */ jsx(Dictation, { data: activity, ...common });
1994
+ break;
1995
+ case "read-aloud": {
1996
+ const binding = slotBindings.get(slot.slotId);
1997
+ const assessment = props.assessments !== void 0 && Object.hasOwn(props.assessments, slot.slotId) ? props.assessments[slot.slotId] : void 0;
1998
+ question = /* @__PURE__ */ jsx(
1999
+ ReadAloud,
2000
+ {
2001
+ data: activity,
2002
+ ...common,
2003
+ ...binding !== void 0 ? { recordingBinding: binding } : {},
2004
+ ...assessment !== void 0 ? { assessment } : {},
2005
+ ...workletUrl !== void 0 ? { workletUrl } : {}
2006
+ }
2007
+ );
2008
+ break;
2009
+ }
2010
+ default:
2011
+ question = null;
2012
+ }
2013
+ return question;
2014
+ };
2015
+ const channels = useRef(/* @__PURE__ */ new Map());
2016
+ const channelFor = (slotId) => {
2017
+ let channel = channels.current.get(slotId);
2018
+ if (channel === void 0) {
2019
+ channel = { captureGroup: `${playerId}::${slotId}`, takeState: () => void 0 };
2020
+ channels.current.set(slotId, channel);
2021
+ }
2022
+ return channel;
2023
+ };
2024
+ if (devError !== null) {
2025
+ throw devError;
2026
+ }
2027
+ const openCue = openQuiz === null ? void 0 : cues.find((cue) => cue.id === openQuiz.cueId);
2028
+ const openSlots = openCue === void 0 ? [] : slotsByCue.get(openCue.id) ?? [];
2029
+ const step = openQuiz?.step ?? 0;
2030
+ const stepSlot = openSlots[step];
2031
+ const stepAnswered = stepSlot !== void 0 && answered.has(stepSlot.slotId);
2032
+ const lastStep = step >= openSlots.length - 1;
2033
+ const required = openCue?.required === true && renderMode !== "review";
2034
+ const canAdvance = !required || stepAnswered;
2035
+ const goToStep = (next) => {
2036
+ setOpenQuiz((open) => open === null ? open : { ...open, step: next });
2037
+ const slot = openSlots[next];
2038
+ if (slot !== void 0 && openCue !== void 0) {
2039
+ announce(strings.videoQuestionProgress(next + 1, openSlots.length));
2040
+ emit("video-quiz-question-shown", { cueId: openCue.id, slotId: slot.slotId });
2041
+ }
2042
+ };
2043
+ const captionsHidden = !preferences.captions || openQuiz !== null || ended || errorCode !== null;
2044
+ const primaryText = captionsHidden ? void 0 : primaryCues[activeCueIndex]?.text;
2045
+ const secondaryText = captionsHidden ? void 0 : secondaryCues[secondaryCueIndex]?.text;
2046
+ const chromeVisible = !playing || chromeShown || menu !== null || scrubbing || focusInChrome || sheet || openQuiz !== null;
2047
+ const volumeLevel = preferences.muted || preferences.volume === 0 ? "muted" : preferences.volume < 0.5 ? "low" : "high";
2048
+ return (
2049
+ // biome-ignore lint/a11y/noStaticElementInteractions: the player's keyboard shortcuts, scoped to the player; each is also a button
2050
+ /* @__PURE__ */ jsxs(
2051
+ "div",
2052
+ {
2053
+ ref: shellRef,
2054
+ className: "lk-iv",
2055
+ tabIndex: -1,
2056
+ lang: locale,
2057
+ style: theme,
2058
+ "data-render-mode": renderMode,
2059
+ "data-quiz-open": openQuiz !== null || void 0,
2060
+ "data-fullscreen": fullscreen || void 0,
2061
+ "data-chrome": chromeVisible ? "shown" : "hidden",
2062
+ onKeyDown,
2063
+ children: [
2064
+ /* @__PURE__ */ jsxs(
2065
+ "div",
2066
+ {
2067
+ className: "lk-iv-stage",
2068
+ onPointerDown: (event) => {
2069
+ lastPointer.current = event.pointerType;
2070
+ },
2071
+ onPointerMove: (event) => {
2072
+ if (event.pointerType !== "touch") {
2073
+ revealChrome();
2074
+ }
2075
+ },
2076
+ onPointerLeave: () => {
2077
+ if (playing) {
2078
+ setChromeShown(false);
2079
+ }
2080
+ },
2081
+ children: [
2082
+ /* @__PURE__ */ jsx(
2083
+ "video",
2084
+ {
2085
+ ref: videoRef,
2086
+ className: "lk-iv-video",
2087
+ src: media?.url,
2088
+ ...media?.poster !== void 0 ? { poster: media.poster } : {},
2089
+ preload: "metadata",
2090
+ playsInline: true,
2091
+ "aria-label": group.title ?? media?.alt,
2092
+ onClick: () => {
2093
+ if (lastPointer.current === "touch" && !chromeVisible) {
2094
+ revealChrome();
2095
+ return;
2096
+ }
2097
+ togglePlay();
2098
+ },
2099
+ onDoubleClick: toggleFullscreen,
2100
+ onPlay: () => {
2101
+ setPlaying(true);
2102
+ setEnded(false);
2103
+ revealChrome();
2104
+ emit("video-played", { at: videoRef.current?.currentTime ?? 0 });
2105
+ },
2106
+ onPause: () => {
2107
+ setPlaying(false);
2108
+ emit("video-paused", { at: videoRef.current?.currentTime ?? 0 });
2109
+ reportProgress(true);
2110
+ },
2111
+ onWaiting: () => setWaiting(true),
2112
+ onPlaying: () => setWaiting(false),
2113
+ onProgress: (event) => setBuffered(bufferedAhead(event.currentTarget)),
2114
+ onTimeUpdate: (event) => {
2115
+ if (!playing && !scrubbing) {
2116
+ setCurrent(event.currentTarget.currentTime);
2117
+ }
2118
+ },
2119
+ onRateChange: (event) => emit("video-rate-changed", { rate: event.currentTarget.playbackRate }),
2120
+ onLoadedMetadata: (event) => {
2121
+ const video = event.currentTarget;
2122
+ const length = Number.isFinite(video.duration) ? video.duration : 0;
2123
+ setDuration(length);
2124
+ video.playbackRate = preferences.speed;
2125
+ video.volume = preferences.volume;
2126
+ video.muted = preferences.muted;
2127
+ const stored = readMediaProgress(props.progress, length);
2128
+ if (stored !== null) {
2129
+ const at = renderMode === "review" ? stored.at : limitResume(timeline, stored.at, finished);
2130
+ furthest.current = Math.max(stored.furthest, at);
2131
+ setFurthestShown(furthest.current);
2132
+ video.currentTime = at;
2133
+ previousTime.current = at;
2134
+ setCurrent(at);
2135
+ }
2136
+ },
2137
+ onEnded: () => {
2138
+ setPlaying(false);
2139
+ setCurrent(duration);
2140
+ emit("video-ended");
2141
+ reportProgress(true);
2142
+ const atEnd = quizzesAtEnd(cues, duration, handled.current);
2143
+ const first = atEnd[0];
2144
+ if (first !== void 0) {
2145
+ openAt(first);
2146
+ } else {
2147
+ setEnded(true);
2148
+ }
2149
+ },
2150
+ onError: (event) => {
2151
+ setWaiting(false);
2152
+ setErrorCode(event.currentTarget.error?.code ?? 0);
2153
+ }
2154
+ }
2155
+ ),
2156
+ label !== void 0 ? /* @__PURE__ */ jsx("span", { className: "lk-iv-label", children: label }) : null,
2157
+ primaryText !== void 0 || secondaryText !== void 0 ? (
2158
+ // The lines keep `.lk-iv-caption`, `data-size` and `data-background`
2159
+ // as well, so a stylesheet written against 15.x still matches them.
2160
+ /* @__PURE__ */ jsxs(
2161
+ "div",
2162
+ {
2163
+ className: "lk-iv-captions",
2164
+ "data-size": preferences.captionSize,
2165
+ "data-background": preferences.captionBackground || void 0,
2166
+ children: [
2167
+ primaryText !== void 0 ? /* @__PURE__ */ jsx(
2168
+ "div",
2169
+ {
2170
+ className: "lk-iv-caption",
2171
+ "data-role": "primary",
2172
+ "data-size": preferences.captionSize,
2173
+ "data-background": preferences.captionBackground || void 0,
2174
+ lang: primaryTrack?.srclang,
2175
+ dir: "auto",
2176
+ children: /* @__PURE__ */ jsx("span", { children: primaryText })
2177
+ }
2178
+ ) : null,
2179
+ secondaryText !== void 0 ? /* @__PURE__ */ jsx(
2180
+ "div",
2181
+ {
2182
+ className: "lk-iv-caption",
2183
+ "data-role": "secondary",
2184
+ "data-size": preferences.captionSize,
2185
+ "data-background": preferences.captionBackground || void 0,
2186
+ lang: secondaryTrack?.srclang,
2187
+ dir: "auto",
2188
+ children: /* @__PURE__ */ jsx("span", { children: secondaryText })
2189
+ }
2190
+ ) : null
2191
+ ]
2192
+ }
2193
+ )
2194
+ ) : null,
2195
+ waiting && errorCode === null && openQuiz === null ? /* @__PURE__ */ jsx("span", { className: "lk-iv-spinner", "aria-hidden": "true" }) : null,
2196
+ errorCode !== null ? /* @__PURE__ */ jsxs("div", { className: "lk-iv-error", role: "alert", children: [
2197
+ /* @__PURE__ */ jsx(AlertIcon, {}),
2198
+ /* @__PURE__ */ jsx("p", { children: strings.videoError(errorCode) }),
2199
+ /* @__PURE__ */ jsx(
2200
+ "button",
2201
+ {
2202
+ type: "button",
2203
+ className: "lk-iv-action",
2204
+ onClick: () => {
2205
+ setErrorCode(null);
2206
+ videoRef.current?.load();
2207
+ },
2208
+ children: strings.videoTryAgain
2209
+ }
2210
+ )
2211
+ ] }) : null,
2212
+ !playing && !waiting && errorCode === null && openQuiz === null && !ended ? /* @__PURE__ */ jsx(
2213
+ "button",
2214
+ {
2215
+ type: "button",
2216
+ className: "lk-iv-big-play",
2217
+ "aria-label": strings.media.play,
2218
+ onClick: togglePlay,
2219
+ children: /* @__PURE__ */ jsx(PlayIcon, {})
2220
+ }
2221
+ ) : null,
2222
+ /* @__PURE__ */ jsxs(
2223
+ "div",
2224
+ {
2225
+ className: "lk-iv-chrome",
2226
+ inert: openQuiz !== null,
2227
+ onFocus: () => setFocusInChrome(true),
2228
+ onBlur: (event) => {
2229
+ if (!event.currentTarget.contains(event.relatedTarget)) {
2230
+ setFocusInChrome(false);
2231
+ }
2232
+ },
2233
+ children: [
2234
+ /* @__PURE__ */ jsx(
2235
+ Scrubber,
2236
+ {
2237
+ current,
2238
+ duration,
2239
+ buffered,
2240
+ chapters,
2241
+ markers,
2242
+ cues: primaryCues,
2243
+ previewLimit: spoilerLimit,
2244
+ strings,
2245
+ onSeek: seek,
2246
+ onScrubChange: setScrubbing
2247
+ }
2248
+ ),
2249
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-bar", children: [
2250
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-bar-group", children: [
2251
+ /* @__PURE__ */ jsx(
2252
+ "button",
2253
+ {
2254
+ ref: playButtonRef,
2255
+ type: "button",
2256
+ className: "lk-iv-button",
2257
+ "aria-label": ended ? strings.videoReplay : playing ? strings.media.pause : strings.media.play,
2258
+ onClick: togglePlay,
2259
+ children: ended ? /* @__PURE__ */ jsx(ReplayIcon, {}) : playing ? /* @__PURE__ */ jsx(PauseIcon, {}) : /* @__PURE__ */ jsx(PlayIcon, {})
2260
+ }
2261
+ ),
2262
+ /* @__PURE__ */ jsx(
2263
+ "button",
2264
+ {
2265
+ type: "button",
2266
+ className: "lk-iv-button lk-iv-wide",
2267
+ "aria-label": strings.videoBack(JUMP_SECONDS),
2268
+ onClick: () => seek(current - JUMP_SECONDS),
2269
+ children: /* @__PURE__ */ jsx(BackIcon, {})
2270
+ }
2271
+ ),
2272
+ /* @__PURE__ */ jsx(
2273
+ "button",
2274
+ {
2275
+ type: "button",
2276
+ className: "lk-iv-button lk-iv-wide",
2277
+ "aria-label": strings.videoForward(JUMP_SECONDS),
2278
+ onClick: () => seek(current + JUMP_SECONDS),
2279
+ children: /* @__PURE__ */ jsx(ForwardIcon, {})
2280
+ }
2281
+ ),
2282
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-volume", children: [
2283
+ /* @__PURE__ */ jsx(
2284
+ "button",
2285
+ {
2286
+ type: "button",
2287
+ className: "lk-iv-button",
2288
+ "aria-label": preferences.muted ? strings.media.unmute : strings.media.mute,
2289
+ "aria-pressed": preferences.muted,
2290
+ onClick: () => changePreferences({ muted: !preferences.muted }),
2291
+ children: /* @__PURE__ */ jsx(VolumeIcon, { level: volumeLevel })
2292
+ }
2293
+ ),
2294
+ /* @__PURE__ */ jsx(
2295
+ "input",
2296
+ {
2297
+ id: `${playerId}-volume`,
2298
+ className: "lk-iv-volume-slider",
2299
+ type: "range",
2300
+ min: 0,
2301
+ max: 1,
2302
+ step: 0.05,
2303
+ value: preferences.muted ? 0 : preferences.volume,
2304
+ "aria-label": strings.media.volume,
2305
+ onChange: (event) => changePreferences({ volume: Number(event.target.value), muted: false })
2306
+ }
2307
+ )
2308
+ ] }),
2309
+ /* @__PURE__ */ jsx(
2310
+ "button",
2311
+ {
2312
+ type: "button",
2313
+ className: "lk-iv-time",
2314
+ "aria-label": countdown ? strings.videoShowElapsed : strings.videoShowRemaining,
2315
+ onClick: () => setCountdown((on) => !on),
2316
+ children: /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", children: [
2317
+ countdown ? `-${clock(Math.max(0, duration - current))}` : clock(current),
2318
+ /* @__PURE__ */ jsxs("span", { className: "lk-iv-time-total", children: [
2319
+ " / ",
2320
+ clock(duration)
2321
+ ] })
2322
+ ] })
2323
+ }
2324
+ )
2325
+ ] }),
2326
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-bar-group", children: [
2327
+ quizzes.length > 0 || chapters.length > 0 || hasTranscript ? /* @__PURE__ */ jsx(
2328
+ "button",
2329
+ {
2330
+ type: "button",
2331
+ className: "lk-iv-button",
2332
+ "aria-label": strings.videoPanel,
2333
+ "aria-pressed": preferences.panel,
2334
+ onClick: () => changePreferences({ panel: !preferences.panel }),
2335
+ children: /* @__PURE__ */ jsx(ContentsIcon, {})
2336
+ }
2337
+ ) : null,
2338
+ hasCaptions ? /* @__PURE__ */ jsx(
2339
+ "button",
2340
+ {
2341
+ type: "button",
2342
+ className: "lk-iv-button",
2343
+ "aria-label": preferences.captions ? strings.videoCaptionsHide : strings.videoCaptionsShow,
2344
+ "aria-pressed": preferences.captions,
2345
+ onClick: () => changePreferences({ captions: !preferences.captions }),
2346
+ children: /* @__PURE__ */ jsx(CaptionsIcon, { on: preferences.captions })
2347
+ }
2348
+ ) : null,
2349
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-menu-anchor", children: [
2350
+ /* @__PURE__ */ jsx(
2351
+ "button",
2352
+ {
2353
+ type: "button",
2354
+ className: "lk-iv-button lk-iv-speed",
2355
+ "aria-label": strings.media.speed,
2356
+ "aria-haspopup": "menu",
2357
+ "aria-expanded": menu === "speed",
2358
+ onClick: () => setMenu((open) => open === "speed" ? null : "speed"),
2359
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: `${preferences.speed}\xD7` })
2360
+ }
2361
+ ),
2362
+ menu === "speed" ? /* @__PURE__ */ jsx(
2363
+ SpeedMenu,
2364
+ {
2365
+ speed: preferences.speed,
2366
+ strings,
2367
+ onChoose: (speed) => {
2368
+ changePreferences({ speed });
2369
+ setMenu(null);
2370
+ },
2371
+ onClose: () => setMenu(null)
2372
+ }
2373
+ ) : null
2374
+ ] }),
2375
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-menu-anchor", children: [
2376
+ /* @__PURE__ */ jsx(
2377
+ "button",
2378
+ {
2379
+ type: "button",
2380
+ className: "lk-iv-button",
2381
+ "aria-label": strings.videoSettings,
2382
+ "aria-haspopup": "menu",
2383
+ "aria-expanded": menu === "settings",
2384
+ onClick: () => setMenu((open) => open === "settings" ? null : "settings"),
2385
+ children: /* @__PURE__ */ jsx(SettingsIcon, {})
2386
+ }
2387
+ ),
2388
+ menu === "settings" ? /* @__PURE__ */ jsx(
2389
+ SettingsMenu,
2390
+ {
2391
+ preferences,
2392
+ tracks,
2393
+ primaryTrack,
2394
+ secondaryTrack,
2395
+ captionsFailed,
2396
+ onCaptionTrack: chooseCaptionTrack,
2397
+ onSecondaryTrack: chooseSecondaryTrack,
2398
+ strings,
2399
+ onChange: changePreferences,
2400
+ onShowShortcuts: () => {
2401
+ setMenu(null);
2402
+ setSheet(true);
2403
+ },
2404
+ onClose: () => setMenu(null)
2405
+ }
2406
+ ) : null
2407
+ ] }),
2408
+ pictureInPictureSupported ? /* @__PURE__ */ jsx(
2409
+ "button",
2410
+ {
2411
+ type: "button",
2412
+ className: "lk-iv-button lk-iv-wide",
2413
+ "aria-label": strings.videoPictureInPicture,
2414
+ "aria-pressed": pictureInPicture,
2415
+ onClick: togglePictureInPicture,
2416
+ children: /* @__PURE__ */ jsx(PictureInPictureIcon, {})
2417
+ }
2418
+ ) : null,
2419
+ /* @__PURE__ */ jsx(
2420
+ "button",
2421
+ {
2422
+ type: "button",
2423
+ className: "lk-iv-button",
2424
+ "aria-label": fullscreen ? strings.videoExitFullscreen : strings.videoFullscreen,
2425
+ onClick: toggleFullscreen,
2426
+ children: /* @__PURE__ */ jsx(FullscreenIcon, { on: fullscreen })
2427
+ }
2428
+ )
2429
+ ] })
2430
+ ] })
2431
+ ]
2432
+ }
2433
+ ),
2434
+ /* @__PURE__ */ jsxs(
2435
+ "div",
2436
+ {
2437
+ className: "lk-iv-quiz",
2438
+ role: "dialog",
2439
+ "aria-modal": "false",
2440
+ "aria-labelledby": `${playerId}-quiz-title`,
2441
+ hidden: openQuiz === null,
2442
+ onKeyDown: (event) => {
2443
+ if (event.key === "Escape" && !required && openQuiz !== null) {
2444
+ event.preventDefault();
2445
+ closeQuiz("skip");
2446
+ }
2447
+ },
2448
+ children: [
2449
+ openCue !== void 0 ? /* @__PURE__ */ jsxs("div", { className: "lk-iv-quiz-head", children: [
2450
+ /* @__PURE__ */ jsxs(
2451
+ "h3",
2452
+ {
2453
+ id: `${playerId}-quiz-title`,
2454
+ ref: quizHeadingRef,
2455
+ tabIndex: -1,
2456
+ className: "lk-iv-quiz-title",
2457
+ children: [
2458
+ quizTitle(openCue),
2459
+ /* @__PURE__ */ jsxs("span", { className: "lk-iv-quiz-time", children: [
2460
+ " \xB7 ",
2461
+ clock(openCue.at)
2462
+ ] }),
2463
+ required ? /* @__PURE__ */ jsxs(Fragment, { children: [
2464
+ " ",
2465
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-tag", children: strings.videoRequired })
2466
+ ] }) : null
2467
+ ]
2468
+ }
2469
+ ),
2470
+ openSlots.length > 1 ? /* @__PURE__ */ jsxs("div", { className: "lk-iv-steps", children: [
2471
+ /* @__PURE__ */ jsx("span", { className: "lk-iv-steps-label", children: strings.videoQuestionProgress(step + 1, openSlots.length) }),
2472
+ /* @__PURE__ */ jsx("ol", { className: "lk-iv-steps-dots", children: openSlots.map((slot, index) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
2473
+ "button",
2474
+ {
2475
+ type: "button",
2476
+ className: "lk-iv-step",
2477
+ "data-current": index === step || void 0,
2478
+ "data-answered": answered.has(slot.slotId) || void 0,
2479
+ "aria-current": index === step ? "step" : void 0,
2480
+ "aria-label": strings.videoQuestionStep(
2481
+ index + 1,
2482
+ answered.has(slot.slotId)
2483
+ ),
2484
+ onClick: () => goToStep(index)
2485
+ }
2486
+ ) }, slot.slotId)) })
2487
+ ] }) : null
2488
+ ] }) : null,
2489
+ /* @__PURE__ */ jsx("div", { className: "lk-iv-quiz-body", children: cues.filter((cue) => mounted.has(cue.id)).map(
2490
+ (cue) => (slotsByCue.get(cue.id) ?? []).map((slot, index) => /* @__PURE__ */ jsx(
2491
+ QuestionPane,
2492
+ {
2493
+ hidden: !(openQuiz?.cueId === cue.id && step === index),
2494
+ channel: channelFor(slot.slotId),
2495
+ children: renderQuestion(slot, cue.id)
2496
+ },
2497
+ slot.slotId
2498
+ ))
2499
+ ) }),
2500
+ openCue !== void 0 ? /* @__PURE__ */ jsxs("div", { className: "lk-iv-quiz-foot", children: [
2501
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-quiz-foot-start", children: [
2502
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "lk-iv-link", onClick: () => closeQuiz("rewatch"), children: [
2503
+ /* @__PURE__ */ jsx(ReplayIcon, {}),
2504
+ strings.videoRewatch
2505
+ ] }),
2506
+ !required && !lastStep ? /* @__PURE__ */ jsx("button", { type: "button", className: "lk-iv-link", onClick: () => closeQuiz("skip"), children: strings.videoSkipQuiz }) : null
2507
+ ] }),
2508
+ /* @__PURE__ */ jsxs("div", { className: "lk-iv-quiz-foot-end", children: [
2509
+ renderMode === "exam" && stepAnswered ? /* @__PURE__ */ jsx("span", { className: "lk-iv-saved", role: "status", children: strings.videoAnswerSaved }) : null,
2510
+ /* @__PURE__ */ jsxs(
2511
+ "button",
2512
+ {
2513
+ type: "button",
2514
+ className: stepAnswered || renderMode === "review" ? "lk-iv-action lk-iv-action-primary" : "lk-iv-action",
2515
+ "aria-disabled": !canAdvance || void 0,
2516
+ onClick: () => {
2517
+ if (!canAdvance) {
2518
+ return;
2519
+ }
2520
+ if (lastStep) {
2521
+ closeQuiz("continue");
2522
+ } else {
2523
+ goToStep(step + 1);
2524
+ }
2525
+ },
2526
+ children: [
2527
+ lastStep ? strings.videoContinue : strings.videoNextQuestion,
2528
+ lastStep ? /* @__PURE__ */ jsx(PlayIcon, {}) : null
2529
+ ]
2530
+ }
2531
+ )
2532
+ ] })
2533
+ ] }) : null
2534
+ ]
2535
+ }
2536
+ ),
2537
+ ended && openQuiz === null ? /* @__PURE__ */ jsx(
2538
+ EndScreen,
2539
+ {
2540
+ idPrefix: playerId,
2541
+ renderMode,
2542
+ quizzes,
2543
+ answered: answeredCount,
2544
+ total: totalQuestions,
2545
+ percent,
2546
+ strings,
2547
+ onOpenQuiz: (id) => {
2548
+ const cue = cues.find((candidate) => candidate.id === id);
2549
+ if (cue !== void 0) {
2550
+ openAt(cue);
2551
+ }
2552
+ },
2553
+ onWatchAgain: togglePlay,
2554
+ onFinish: renderMode === "review" ? void 0 : finish
2555
+ }
2556
+ ) : null,
2557
+ sheet ? /* @__PURE__ */ jsx(
2558
+ ShortcutSheet,
2559
+ {
2560
+ strings,
2561
+ onClose: () => {
2562
+ setSheet(false);
2563
+ playButtonRef.current?.focus();
2564
+ }
2565
+ }
2566
+ ) : null
2567
+ ]
2568
+ }
2569
+ ),
2570
+ preferences.panel ? /* @__PURE__ */ jsx(
2571
+ ContentsPanel,
2572
+ {
2573
+ idPrefix: playerId,
2574
+ tab: panelTab,
2575
+ onTab: setPanelTab,
2576
+ chapters,
2577
+ quizzes,
2578
+ cues: primaryCues,
2579
+ primaryLanguage: primaryTrack?.srclang,
2580
+ secondaryCues: secondaryTrack === void 0 ? NO_CUES : secondaryCues,
2581
+ secondaryLanguage: secondaryTrack?.srclang,
2582
+ activeCue: activeCueIndex,
2583
+ limit: spoilerLimit,
2584
+ strings,
2585
+ onSeek: seek,
2586
+ onOpenQuiz: (id) => {
2587
+ const cue = cues.find((candidate) => candidate.id === id);
2588
+ if (cue === void 0) {
2589
+ return;
2590
+ }
2591
+ if (renderMode !== "review" && limitSeek(
2592
+ timeline,
2593
+ videoRef.current?.currentTime ?? 0,
2594
+ cue.at,
2595
+ furthest.current,
2596
+ finished
2597
+ ).at < cue.at - 0.25) {
2598
+ seek(cue.at);
2599
+ return;
2600
+ }
2601
+ openAt(cue);
2602
+ }
2603
+ }
2604
+ ) : null,
2605
+ /* @__PURE__ */ jsx("div", { className: "lk-iv-live", "aria-live": "polite", "aria-atomic": "true", children: announcement })
2606
+ ]
2607
+ }
2608
+ )
2609
+ );
2610
+ }
2611
+ function QuestionPane({
2612
+ hidden,
2613
+ channel,
2614
+ children
2615
+ }) {
2616
+ const ref = useRef(null);
2617
+ const captureGroup = channel.captureGroup;
2618
+ useEffect(() => {
2619
+ if (!hidden) {
2620
+ return;
2621
+ }
2622
+ stopCaptureGroup(captureGroup);
2623
+ for (const media of ref.current?.querySelectorAll("audio, video") ?? []) {
2624
+ const element = media;
2625
+ if (!element.paused) {
2626
+ element.pause();
2627
+ }
2628
+ }
2629
+ }, [hidden, captureGroup]);
2630
+ return /* @__PURE__ */ jsx("div", { ref, className: "lk-iv-question", hidden, children: /* @__PURE__ */ jsx(SequenceSlotContext.Provider, { value: channel, children }) });
2631
+ }
2632
+ function bufferedAhead(video) {
2633
+ const ranges = video.buffered;
2634
+ if (ranges === void 0) {
2635
+ return 0;
2636
+ }
2637
+ for (let index = 0; index < ranges.length; index += 1) {
2638
+ if (ranges.start(index) <= video.currentTime && video.currentTime <= ranges.end(index)) {
2639
+ return ranges.end(index);
2640
+ }
2641
+ }
2642
+ return 0;
2643
+ }
2644
+ function markerState(quizSlots, answered, renderMode, outcomes) {
2645
+ if (renderMode === "review" && outcomes !== void 0) {
2646
+ const results = quizSlots.map(
2647
+ (slot) => Object.hasOwn(outcomes, slot.slotId) ? outcomes[slot.slotId] : void 0
2648
+ );
2649
+ if (results.some((outcome) => outcome === void 0 || outcome.status === "deferred")) {
2650
+ return "pending";
2651
+ }
2652
+ const scored = results.filter(
2653
+ (outcome) => outcome?.status === "scored"
2654
+ );
2655
+ if (scored.length === results.length && scored.every((outcome) => outcome.passed)) {
2656
+ return "correct";
2657
+ }
2658
+ return scored.some((outcome) => outcome.passed) ? "partial" : "incorrect";
2659
+ }
2660
+ const count = quizSlots.filter((slot) => answered.has(slot.slotId)).length;
2661
+ if (count === quizSlots.length && count > 0) {
2662
+ return "answered";
2663
+ }
2664
+ if (count > 0) {
2665
+ return "open";
2666
+ }
2667
+ return "unreached";
2668
+ }
2669
+ function InteractiveVideo2(props) {
2670
+ const strings = useLkStrings(props.strings);
2671
+ return /* @__PURE__ */ jsx(
2672
+ ActivityErrorBoundary,
2673
+ {
2674
+ ...props.group?.title !== void 0 ? { activityTitle: props.group.title } : {},
2675
+ strings,
2676
+ children: /* @__PURE__ */ jsx(InteractiveVideo, { ...props })
2677
+ }
2678
+ );
2679
+ }
2680
+
2681
+ export { InteractiveVideo2 as InteractiveVideo, resolveCaptionTracks };
2682
+ //# sourceMappingURL=chunk-3IHVDQRC.js.map
2683
+ //# sourceMappingURL=chunk-3IHVDQRC.js.map