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