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