@intellectif/lk-react 15.0.0 → 16.1.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 (129) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +3 -3
  3. package/dist/{WrittenResponse-BPbhERgk.d.cts → WrittenResponse-6tSj6U3G.d.cts} +1 -1
  4. package/dist/{WrittenResponse-ZAxLR_AU.d.ts → WrittenResponse-xJ7I_5yq.d.ts} +1 -1
  5. package/dist/{chunk-53YOZOGN.cjs → chunk-2S3MK4VX.cjs} +5 -5
  6. package/dist/{chunk-53YOZOGN.cjs.map → chunk-2S3MK4VX.cjs.map} +1 -1
  7. package/dist/{chunk-4H3JTX74.cjs → chunk-2TFNDHJ5.cjs} +13 -13
  8. package/dist/{chunk-4H3JTX74.cjs.map → chunk-2TFNDHJ5.cjs.map} +1 -1
  9. package/dist/{chunk-AF72QUBI.js → chunk-2XNCPLHU.js} +5 -5
  10. package/dist/{chunk-AF72QUBI.js.map → chunk-2XNCPLHU.js.map} +1 -1
  11. package/dist/{chunk-RT5FTHUU.js → chunk-3UXRX4KU.js} +5 -5
  12. package/dist/{chunk-RT5FTHUU.js.map → chunk-3UXRX4KU.js.map} +1 -1
  13. package/dist/{chunk-CE6OGJE6.js → chunk-5QHVYOME.js} +5 -5
  14. package/dist/{chunk-CE6OGJE6.js.map → chunk-5QHVYOME.js.map} +1 -1
  15. package/dist/{chunk-EKBEKDWO.cjs → chunk-B6S5ADHR.cjs} +13 -13
  16. package/dist/{chunk-EKBEKDWO.cjs.map → chunk-B6S5ADHR.cjs.map} +1 -1
  17. package/dist/{chunk-QM4AV6EG.js → chunk-BI547RIA.js} +4 -4
  18. package/dist/{chunk-QM4AV6EG.js.map → chunk-BI547RIA.js.map} +1 -1
  19. package/dist/{chunk-WKOV25NP.cjs → chunk-DFMBVA7D.cjs} +8 -8
  20. package/dist/{chunk-WKOV25NP.cjs.map → chunk-DFMBVA7D.cjs.map} +1 -1
  21. package/dist/{chunk-ZJMTHPHY.js → chunk-DKZWUIQD.js} +3 -3
  22. package/dist/{chunk-ZJMTHPHY.js.map → chunk-DKZWUIQD.js.map} +1 -1
  23. package/dist/{chunk-2HAEQDHQ.js → chunk-DVOPHC2Q.js} +755 -201
  24. package/dist/chunk-DVOPHC2Q.js.map +1 -0
  25. package/dist/{chunk-DQ3VVU5T.cjs → chunk-FZWWVJE4.cjs} +24 -24
  26. package/dist/{chunk-DQ3VVU5T.cjs.map → chunk-FZWWVJE4.cjs.map} +1 -1
  27. package/dist/{chunk-I2QGW4GD.js → chunk-GDMM2WEG.js} +11 -11
  28. package/dist/{chunk-I2QGW4GD.js.map → chunk-GDMM2WEG.js.map} +1 -1
  29. package/dist/{chunk-QEB6O6GU.js → chunk-GUPBIEUA.js} +6 -6
  30. package/dist/{chunk-QEB6O6GU.js.map → chunk-GUPBIEUA.js.map} +1 -1
  31. package/dist/{chunk-33BQNWAQ.js → chunk-IQI5I2X5.js} +5 -5
  32. package/dist/{chunk-33BQNWAQ.js.map → chunk-IQI5I2X5.js.map} +1 -1
  33. package/dist/{chunk-YWQH35VO.js → chunk-JPDR6TTA.js} +4 -4
  34. package/dist/{chunk-YWQH35VO.js.map → chunk-JPDR6TTA.js.map} +1 -1
  35. package/dist/{chunk-UCKA22ZJ.cjs → chunk-K63VPFOV.cjs} +9 -2
  36. package/dist/chunk-K63VPFOV.cjs.map +1 -0
  37. package/dist/{chunk-52GXJNQ3.js → chunk-KSAINXRG.js} +9 -2
  38. package/dist/chunk-KSAINXRG.js.map +1 -0
  39. package/dist/{chunk-ZH5WT2IE.cjs → chunk-ML6L4LJH.cjs} +25 -25
  40. package/dist/{chunk-ZH5WT2IE.cjs.map → chunk-ML6L4LJH.cjs.map} +1 -1
  41. package/dist/{chunk-Z6TVOAO5.cjs → chunk-MQAZJQRE.cjs} +9 -2
  42. package/dist/chunk-MQAZJQRE.cjs.map +1 -0
  43. package/dist/{chunk-UAYWLYFW.cjs → chunk-OO24JGYH.cjs} +18 -18
  44. package/dist/{chunk-UAYWLYFW.cjs.map → chunk-OO24JGYH.cjs.map} +1 -1
  45. package/dist/{chunk-26GQO5I2.cjs → chunk-PTJTC6A3.cjs} +12 -12
  46. package/dist/{chunk-26GQO5I2.cjs.map → chunk-PTJTC6A3.cjs.map} +1 -1
  47. package/dist/{chunk-DP3DM7FR.cjs → chunk-PXGJHZKJ.cjs} +6 -6
  48. package/dist/{chunk-DP3DM7FR.cjs.map → chunk-PXGJHZKJ.cjs.map} +1 -1
  49. package/dist/{chunk-XMJE34FY.cjs → chunk-RSQFGF5W.cjs} +13 -13
  50. package/dist/{chunk-XMJE34FY.cjs.map → chunk-RSQFGF5W.cjs.map} +1 -1
  51. package/dist/{chunk-FKGUKFC7.cjs → chunk-TRP5Z3XW.cjs} +5 -5
  52. package/dist/{chunk-FKGUKFC7.cjs.map → chunk-TRP5Z3XW.cjs.map} +1 -1
  53. package/dist/{chunk-EVBYEWHO.js → chunk-UHKZXOXD.js} +9 -2
  54. package/dist/chunk-UHKZXOXD.js.map +1 -0
  55. package/dist/{chunk-IE5ZNCWN.js → chunk-VC2MPB2Y.js} +5 -5
  56. package/dist/{chunk-IE5ZNCWN.js.map → chunk-VC2MPB2Y.js.map} +1 -1
  57. package/dist/{chunk-SRD4KVAP.cjs → chunk-WOYCT5M6.cjs} +764 -209
  58. package/dist/chunk-WOYCT5M6.cjs.map +1 -0
  59. package/dist/{chunk-H6I2JDNW.js → chunk-ZEDMFTKD.js} +10 -10
  60. package/dist/{chunk-H6I2JDNW.js.map → chunk-ZEDMFTKD.js.map} +1 -1
  61. package/dist/{chunk-HA3VRF6E.js → chunk-ZPCO3CEB.js} +3 -3
  62. package/dist/{chunk-HA3VRF6E.js.map → chunk-ZPCO3CEB.js.map} +1 -1
  63. package/dist/{chunk-L7NE7PR4.cjs → chunk-ZSY4VANQ.cjs} +13 -13
  64. package/dist/{chunk-L7NE7PR4.cjs.map → chunk-ZSY4VANQ.cjs.map} +1 -1
  65. package/dist/components/ActivityPreview.cjs +12 -12
  66. package/dist/components/ActivityPreview.d.cts +2 -2
  67. package/dist/components/ActivityPreview.d.ts +2 -2
  68. package/dist/components/ActivityPreview.js +11 -11
  69. package/dist/components/ActivitySequence.cjs +13 -13
  70. package/dist/components/ActivitySequence.d.cts +2 -2
  71. package/dist/components/ActivitySequence.d.ts +2 -2
  72. package/dist/components/ActivitySequence.js +12 -12
  73. package/dist/components/Dictation.cjs +5 -5
  74. package/dist/components/Dictation.d.cts +1 -1
  75. package/dist/components/Dictation.d.ts +1 -1
  76. package/dist/components/Dictation.js +4 -4
  77. package/dist/components/FillInTheBlanks.cjs +5 -5
  78. package/dist/components/FillInTheBlanks.d.cts +1 -1
  79. package/dist/components/FillInTheBlanks.d.ts +1 -1
  80. package/dist/components/FillInTheBlanks.js +4 -4
  81. package/dist/components/GapSelect.cjs +5 -5
  82. package/dist/components/GapSelect.d.cts +1 -1
  83. package/dist/components/GapSelect.d.ts +1 -1
  84. package/dist/components/GapSelect.js +4 -4
  85. package/dist/components/InteractiveVideo.cjs +15 -11
  86. package/dist/components/InteractiveVideo.d.cts +164 -7
  87. package/dist/components/InteractiveVideo.d.ts +164 -7
  88. package/dist/components/InteractiveVideo.js +10 -10
  89. package/dist/components/MultipleChoice.cjs +5 -5
  90. package/dist/components/MultipleChoice.d.cts +1 -1
  91. package/dist/components/MultipleChoice.d.ts +1 -1
  92. package/dist/components/MultipleChoice.js +4 -4
  93. package/dist/components/PronunciationFeedback.cjs +4 -4
  94. package/dist/components/PronunciationFeedback.d.cts +1 -1
  95. package/dist/components/PronunciationFeedback.d.ts +1 -1
  96. package/dist/components/PronunciationFeedback.js +3 -3
  97. package/dist/components/ReadAloud.cjs +6 -6
  98. package/dist/components/ReadAloud.d.cts +2 -2
  99. package/dist/components/ReadAloud.d.ts +2 -2
  100. package/dist/components/ReadAloud.js +5 -5
  101. package/dist/components/StimulusPanel.cjs +4 -4
  102. package/dist/components/StimulusPanel.d.cts +1 -1
  103. package/dist/components/StimulusPanel.d.ts +1 -1
  104. package/dist/components/StimulusPanel.js +3 -3
  105. package/dist/components/WrittenResponse.cjs +5 -5
  106. package/dist/components/WrittenResponse.d.cts +3 -3
  107. package/dist/components/WrittenResponse.d.ts +3 -3
  108. package/dist/components/WrittenResponse.js +4 -4
  109. package/dist/i18n/LkIntlProvider.cjs +7 -7
  110. package/dist/i18n/LkIntlProvider.d.cts +2 -2
  111. package/dist/i18n/LkIntlProvider.d.ts +2 -2
  112. package/dist/i18n/LkIntlProvider.js +1 -1
  113. package/dist/index.cjs +41 -37
  114. package/dist/index.d.cts +3 -3
  115. package/dist/index.d.ts +3 -3
  116. package/dist/index.js +15 -15
  117. package/dist/{strings-5588Q4Te.d.cts → strings-C6wM5aUQ.d.cts} +23 -2
  118. package/dist/{strings-D0IWcsUm.d.ts → strings-Cd_2F-Bw.d.ts} +23 -2
  119. package/dist/theme/ThemeProvider.cjs +6 -6
  120. package/dist/theme/ThemeProvider.js +1 -1
  121. package/dist/theme/defaults.css +7 -0
  122. package/dist/theme/skin.css +60 -6
  123. package/package.json +3 -3
  124. package/dist/chunk-2HAEQDHQ.js.map +0 -1
  125. package/dist/chunk-52GXJNQ3.js.map +0 -1
  126. package/dist/chunk-EVBYEWHO.js.map +0 -1
  127. package/dist/chunk-SRD4KVAP.cjs.map +0 -1
  128. package/dist/chunk-UCKA22ZJ.cjs.map +0 -1
  129. package/dist/chunk-Z6TVOAO5.cjs.map +0 -1
@@ -1,13 +1,13 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkL7NE7PR4_cjs = require('./chunk-L7NE7PR4.cjs');
5
- var chunk4H3JTX74_cjs = require('./chunk-4H3JTX74.cjs');
6
- var chunkEKBEKDWO_cjs = require('./chunk-EKBEKDWO.cjs');
7
- var chunkXMJE34FY_cjs = require('./chunk-XMJE34FY.cjs');
8
- var chunkZH5WT2IE_cjs = require('./chunk-ZH5WT2IE.cjs');
9
- var chunk53YOZOGN_cjs = require('./chunk-53YOZOGN.cjs');
10
- var chunkZ6TVOAO5_cjs = require('./chunk-Z6TVOAO5.cjs');
4
+ var chunkZSY4VANQ_cjs = require('./chunk-ZSY4VANQ.cjs');
5
+ var chunk2TFNDHJ5_cjs = require('./chunk-2TFNDHJ5.cjs');
6
+ var chunkB6S5ADHR_cjs = require('./chunk-B6S5ADHR.cjs');
7
+ var chunkRSQFGF5W_cjs = require('./chunk-RSQFGF5W.cjs');
8
+ var chunkML6L4LJH_cjs = require('./chunk-ML6L4LJH.cjs');
9
+ var chunk2S3MK4VX_cjs = require('./chunk-2S3MK4VX.cjs');
10
+ var chunkMQAZJQRE_cjs = require('./chunk-MQAZJQRE.cjs');
11
11
  var lkCore = require('@intellectif/lk-core');
12
12
  var react = require('react');
13
13
  var jsxRuntime = require('react/jsx-runtime');
@@ -109,6 +109,7 @@ var SHORTCUT_KEYS = [
109
109
  { action: "chapter", keys: "[ / ]" },
110
110
  { action: "mute", keys: "M" },
111
111
  { action: "captions", keys: "C" },
112
+ { action: "secondary-captions", keys: "Shift + C" },
112
113
  { action: "transcript", keys: "T" },
113
114
  { action: "picture-in-picture", keys: "P" },
114
115
  { action: "fullscreen", keys: "F" },
@@ -116,17 +117,87 @@ var SHORTCUT_KEYS = [
116
117
  ];
117
118
  var JUMP_SECONDS = 10;
118
119
  var FRAME_SECONDS = 1 / 30;
120
+
121
+ // src/components/InteractiveVideo/tracks.ts
122
+ var primarySubtag = (tag) => (tag.split("-")[0] ?? tag).toLowerCase();
123
+ function preferCaptions(tracks) {
124
+ return tracks.find((track) => track.kind === "captions") ?? tracks[0];
125
+ }
126
+ function matchLanguage(tracks, language) {
127
+ const wanted = language.toLowerCase();
128
+ const exact = tracks.filter((track) => track.srclang.toLowerCase() === wanted);
129
+ if (exact.length > 0) {
130
+ return preferCaptions(exact);
131
+ }
132
+ const family = primarySubtag(wanted);
133
+ return preferCaptions(tracks.filter((track) => primarySubtag(track.srclang) === family));
134
+ }
135
+ function drawable(tracks) {
136
+ return tracks.filter((track) => track.kind === "captions" || track.kind === "subtitles");
137
+ }
138
+ function secondaryCandidates(tracks, primary) {
139
+ const taken = primary?.srclang.toLowerCase();
140
+ return drawable(tracks).filter((track) => track.srclang.toLowerCase() !== taken);
141
+ }
142
+ function resolveCaptionTracks(tracks, preferences) {
143
+ const usable = drawable(tracks);
144
+ if (usable.length === 0) {
145
+ return {};
146
+ }
147
+ const primary = (preferences.captionLanguage === null ? void 0 : matchLanguage(usable, preferences.captionLanguage)) ?? usable.find((track) => track.default === true) ?? usable[0];
148
+ const second = preferences.secondaryCaptionLanguage;
149
+ const secondary = second === null ? void 0 : matchLanguage(secondaryCandidates(usable, primary), second);
150
+ return secondary === void 0 ? { primary } : { primary, secondary };
151
+ }
152
+ var overlap = (a, b) => Math.min(a.end, b.end) - Math.max(a.start, b.start);
153
+ var LOOKBACK = 8;
154
+ function pairSecondaryCues(primary, secondary) {
155
+ const rows = primary.map(() => []);
156
+ for (const cue of secondary) {
157
+ let low = 0;
158
+ let high = primary.length;
159
+ while (low < high) {
160
+ const middle = low + high >> 1;
161
+ if (primary[middle].start < cue.end) {
162
+ low = middle + 1;
163
+ } else {
164
+ high = middle;
165
+ }
166
+ }
167
+ let best = -1;
168
+ let bestShare = 0;
169
+ let misses = 0;
170
+ for (let index = low - 1; index >= 0 && misses <= LOOKBACK; index -= 1) {
171
+ const share = overlap(primary[index], cue);
172
+ if (share > 0) {
173
+ misses = 0;
174
+ if (share >= bestShare) {
175
+ best = index;
176
+ bestShare = share;
177
+ }
178
+ } else {
179
+ misses += 1;
180
+ }
181
+ }
182
+ if (best !== -1) {
183
+ rows[best].push(cue);
184
+ }
185
+ }
186
+ return rows;
187
+ }
119
188
  function Menu({
120
189
  label,
121
190
  onClose,
122
- children
191
+ children,
192
+ returnTo
123
193
  }) {
124
194
  const ref = react.useRef(null);
125
195
  react.useEffect(() => {
126
196
  const items = ref.current?.querySelectorAll('[role^="menuitem"]');
197
+ const back = returnTo === void 0 ? null : ref.current?.querySelector(`[data-menu-key="${returnTo}"]`);
127
198
  const checked = ref.current?.querySelector('[aria-checked="true"]');
128
- (checked ?? items?.[0])?.focus();
129
- }, []);
199
+ (back ?? checked ?? items?.[0])?.focus();
200
+ }, [returnTo]);
130
201
  const onKeyDown = (event) => {
131
202
  const items = Array.from(
132
203
  ref.current?.querySelectorAll('[role^="menuitem"]') ?? []
@@ -178,66 +249,136 @@ function SpeedMenu({
178
249
  function SettingsMenu({
179
250
  preferences,
180
251
  tracks,
181
- activeTrack,
252
+ primaryTrack,
253
+ secondaryTrack,
182
254
  captionsFailed,
183
255
  strings,
184
256
  onChange,
257
+ onCaptionTrack,
258
+ onSecondaryTrack,
185
259
  onShowShortcuts,
186
260
  onClose
187
261
  }) {
188
262
  const [page, setPage] = react.useState("main");
189
- const captionsValue = !preferences.captions || activeTrack === void 0 ? strings.videoOff : activeTrack.label;
263
+ const [returnTo, setReturnTo] = react.useState(void 0);
264
+ const open = (next) => {
265
+ setReturnTo(void 0);
266
+ setPage(next);
267
+ };
268
+ const leave = () => {
269
+ setReturnTo(page);
270
+ setPage(page === "second" ? "captions" : "main");
271
+ };
272
+ const captionsValue = !preferences.captions || primaryTrack === void 0 ? strings.videoOff : secondaryTrack === void 0 ? primaryTrack.label : strings.videoCaptionPair(primaryTrack.label, secondaryTrack.label);
273
+ const partners = secondaryCandidates(tracks, primaryTrack);
274
+ const failedNotes = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
275
+ captionsFailed.primary && page === "captions" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoCaptionsFailed }) : null,
276
+ captionsFailed.secondary ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoSecondaryCaptionsFailed }) : null
277
+ ] });
190
278
  const back = /* @__PURE__ */ jsxRuntime.jsxs(
191
279
  "button",
192
280
  {
193
281
  type: "button",
194
282
  role: "menuitem",
195
283
  className: "lk-iv-menu-item lk-iv-menu-back",
196
- onClick: () => setPage("main"),
284
+ onClick: leave,
197
285
  children: [
198
286
  /* @__PURE__ */ jsxRuntime.jsx(ChevronIcon, { direction: "left" }),
199
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: page === "captions" ? strings.videoCaptionLanguage : strings.videoCaptionSize })
287
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: page === "captions" ? strings.videoCaptionLanguage : page === "second" ? strings.videoSecondCaptionLanguage : strings.videoCaptionSize })
200
288
  ]
201
289
  }
202
290
  );
291
+ const trackRadio = (track, on, choose) => /* @__PURE__ */ jsxRuntime.jsxs(
292
+ "button",
293
+ {
294
+ type: "button",
295
+ role: "menuitemradio",
296
+ "aria-checked": on,
297
+ className: "lk-iv-menu-item",
298
+ lang: track.srclang,
299
+ onClick: choose,
300
+ children: [
301
+ /* @__PURE__ */ jsxRuntime.jsx("span", { dir: "auto", children: track.label }),
302
+ on ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
303
+ ]
304
+ },
305
+ `${track.kind}:${track.srclang}`
306
+ );
203
307
  if (page === "captions") {
204
- return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoCaptionLanguage, onClose, children: [
308
+ return /* @__PURE__ */ jsxRuntime.jsxs(
309
+ Menu,
310
+ {
311
+ label: strings.videoCaptionLanguage,
312
+ onClose,
313
+ returnTo,
314
+ children: [
315
+ back,
316
+ /* @__PURE__ */ jsxRuntime.jsxs(
317
+ "button",
318
+ {
319
+ type: "button",
320
+ role: "menuitemradio",
321
+ "aria-checked": !preferences.captions,
322
+ className: "lk-iv-menu-item",
323
+ onClick: () => onChange({ captions: false }),
324
+ children: [
325
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoOff }),
326
+ !preferences.captions ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
327
+ ]
328
+ }
329
+ ),
330
+ tracks.map(
331
+ (track) => trackRadio(
332
+ track,
333
+ preferences.captions && primaryTrack === track,
334
+ () => onCaptionTrack(track)
335
+ )
336
+ ),
337
+ partners.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
338
+ "button",
339
+ {
340
+ type: "button",
341
+ role: "menuitem",
342
+ className: "lk-iv-menu-item",
343
+ "data-menu-key": "second",
344
+ onClick: () => open("second"),
345
+ children: [
346
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoSecondCaptionLanguage }),
347
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-iv-menu-value", children: [
348
+ /* @__PURE__ */ jsxRuntime.jsx("span", { dir: "auto", lang: secondaryTrack?.srclang, children: secondaryTrack === void 0 ? strings.videoOff : secondaryTrack.label }),
349
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronIcon, { direction: "right" })
350
+ ] })
351
+ ]
352
+ }
353
+ ) : null,
354
+ failedNotes
355
+ ]
356
+ },
357
+ "captions"
358
+ );
359
+ }
360
+ if (page === "second") {
361
+ return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoSecondCaptionLanguage, onClose, children: [
205
362
  back,
206
363
  /* @__PURE__ */ jsxRuntime.jsxs(
207
364
  "button",
208
365
  {
209
366
  type: "button",
210
367
  role: "menuitemradio",
211
- "aria-checked": !preferences.captions,
368
+ "aria-checked": secondaryTrack === void 0,
212
369
  className: "lk-iv-menu-item",
213
- onClick: () => onChange({ captions: false }),
370
+ onClick: () => onSecondaryTrack(null),
214
371
  children: [
215
372
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoOff }),
216
- !preferences.captions ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
373
+ secondaryTrack === void 0 ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
217
374
  ]
218
375
  }
219
376
  ),
220
- tracks.map((track) => {
221
- const on = preferences.captions && activeTrack === track;
222
- return /* @__PURE__ */ jsxRuntime.jsxs(
223
- "button",
224
- {
225
- type: "button",
226
- role: "menuitemradio",
227
- "aria-checked": on,
228
- className: "lk-iv-menu-item",
229
- lang: track.srclang,
230
- onClick: () => onChange({ captions: true, captionLanguage: track.srclang }),
231
- children: [
232
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: track.label }),
233
- on ? /* @__PURE__ */ jsxRuntime.jsx(CheckIcon, {}) : null
234
- ]
235
- },
236
- `${track.kind}:${track.srclang}`
237
- );
238
- }),
239
- captionsFailed ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoCaptionsFailed }) : null
240
- ] }, "captions");
377
+ partners.map(
378
+ (track) => trackRadio(track, secondaryTrack === track, () => onSecondaryTrack(track))
379
+ ),
380
+ failedNotes
381
+ ] }, "second");
241
382
  }
242
383
  if (page === "size") {
243
384
  return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoCaptionSize, onClose, children: [
@@ -259,7 +400,7 @@ function SettingsMenu({
259
400
  ))
260
401
  ] }, "size");
261
402
  }
262
- return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoSettings, onClose, children: [
403
+ return /* @__PURE__ */ jsxRuntime.jsxs(Menu, { label: strings.videoSettings, onClose, returnTo, children: [
263
404
  tracks.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
264
405
  /* @__PURE__ */ jsxRuntime.jsxs(
265
406
  "button",
@@ -267,11 +408,12 @@ function SettingsMenu({
267
408
  type: "button",
268
409
  role: "menuitem",
269
410
  className: "lk-iv-menu-item",
270
- onClick: () => setPage("captions"),
411
+ "data-menu-key": "captions",
412
+ onClick: () => open("captions"),
271
413
  children: [
272
414
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoCaptionLanguage }),
273
415
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-iv-menu-value", children: [
274
- captionsValue,
416
+ /* @__PURE__ */ jsxRuntime.jsx("span", { dir: "auto", children: captionsValue }),
275
417
  /* @__PURE__ */ jsxRuntime.jsx(ChevronIcon, { direction: "right" })
276
418
  ] })
277
419
  ]
@@ -283,7 +425,8 @@ function SettingsMenu({
283
425
  type: "button",
284
426
  role: "menuitem",
285
427
  className: "lk-iv-menu-item",
286
- onClick: () => setPage("size"),
428
+ "data-menu-key": "size",
429
+ onClick: () => open("size"),
287
430
  children: [
288
431
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: strings.videoCaptionSize }),
289
432
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-iv-menu-value", children: [
@@ -377,6 +520,9 @@ function ContentsPanel({
377
520
  chapters,
378
521
  quizzes,
379
522
  cues,
523
+ primaryLanguage,
524
+ secondaryCues,
525
+ secondaryLanguage,
380
526
  activeCue,
381
527
  limit,
382
528
  strings,
@@ -388,10 +534,18 @@ function ContentsPanel({
388
534
  const followUntil = react.useRef(0);
389
535
  const hasTranscript = cues.length > 0;
390
536
  const shownTab = hasTranscript ? tab : "contents";
537
+ const paired = react.useMemo(() => pairSecondaryCues(cues, secondaryCues), [cues, secondaryCues]);
391
538
  const lines = react.useMemo(() => {
392
539
  const trimmed = query.trim().toLowerCase();
393
- return cues.map((cue, index) => ({ cue, index })).filter(({ cue }) => cue.start <= limit).filter(({ cue }) => trimmed === "" || cue.text.toLowerCase().includes(trimmed));
394
- }, [cues, limit, query]);
540
+ return cues.map((cue, index) => ({
541
+ cue,
542
+ index,
543
+ // The spoiler limit holds for the second language as for the first.
544
+ second: (paired[index] ?? []).filter((line) => line.start <= limit).map((line) => line.text).join(" ")
545
+ })).filter(({ cue }) => cue.start <= limit).filter(
546
+ ({ cue, second }) => trimmed === "" || cue.text.toLowerCase().includes(trimmed) || second.toLowerCase().includes(trimmed)
547
+ );
548
+ }, [cues, paired, limit, query]);
395
549
  react.useEffect(() => {
396
550
  if (shownTab !== "transcript" || query.trim() !== "" || Date.now() < followUntil.current) {
397
551
  return;
@@ -514,7 +668,7 @@ function ContentsPanel({
514
668
  onTouchMove: () => {
515
669
  followUntil.current = Date.now() + 4e3;
516
670
  },
517
- children: lines.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-empty", children: strings.videoTranscriptNoMatch }) : lines.map(({ cue, index }) => /* @__PURE__ */ jsxRuntime.jsxs(
671
+ children: lines.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-iv-empty", children: strings.videoTranscriptNoMatch }) : lines.map(({ cue, index, second }) => /* @__PURE__ */ jsxRuntime.jsxs(
518
672
  "button",
519
673
  {
520
674
  type: "button",
@@ -523,7 +677,10 @@ function ContentsPanel({
523
677
  onClick: () => onSeek(cue.start),
524
678
  children: [
525
679
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-iv-row-time", children: clock(cue.start) }),
526
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: highlight(cue.text) })
680
+ secondaryCues.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { lang: primaryLanguage, dir: "auto", children: highlight(cue.text) }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-iv-line-text", children: [
681
+ /* @__PURE__ */ jsxRuntime.jsx("span", { lang: primaryLanguage, dir: "auto", children: highlight(cue.text) }),
682
+ second === "" ? null : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-iv-line-secondary", lang: secondaryLanguage, dir: "auto", children: highlight(second) })
683
+ ] })
527
684
  ]
528
685
  },
529
686
  `${cue.start}-${index}`
@@ -545,8 +702,10 @@ function EndScreen({
545
702
  strings,
546
703
  onOpenQuiz,
547
704
  onWatchAgain,
705
+ pending,
548
706
  onFinish
549
707
  }) {
708
+ const pendingId = `${idPrefix}-end-pending`;
550
709
  const headingRef = react.useRef(null);
551
710
  react.useEffect(() => {
552
711
  headingRef.current?.focus();
@@ -571,12 +730,23 @@ function EndScreen({
571
730
  ]
572
731
  }
573
732
  ) }, quiz.id)) }) : null,
733
+ onFinish !== void 0 && pending ? /* @__PURE__ */ jsxRuntime.jsx("p", { id: pendingId, className: "lk-iv-end-pending", role: "status", children: strings.videoAnswerPending }) : null,
574
734
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-iv-end-actions", children: [
575
735
  /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "lk-iv-action", onClick: onWatchAgain, children: [
576
736
  /* @__PURE__ */ jsxRuntime.jsx(ReplayIcon, {}),
577
737
  strings.videoWatchAgain
578
738
  ] }),
579
- onFinish !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "lk-iv-action lk-iv-action-primary", onClick: onFinish, children: strings.videoFinish }) : null
739
+ onFinish !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
740
+ "button",
741
+ {
742
+ type: "button",
743
+ className: "lk-iv-action lk-iv-action-primary",
744
+ "aria-disabled": pending || void 0,
745
+ "aria-describedby": pending ? pendingId : void 0,
746
+ onClick: onFinish,
747
+ children: strings.videoFinish
748
+ }
749
+ ) : null
580
750
  ] })
581
751
  ] }) });
582
752
  }
@@ -588,45 +758,73 @@ var DEFAULT_PREFERENCES = {
588
758
  muted: false,
589
759
  captions: true,
590
760
  captionLanguage: null,
761
+ secondaryCaptionLanguage: null,
591
762
  captionSize: "medium",
592
763
  captionBackground: true,
593
764
  panel: false,
594
765
  shortcuts: true
595
766
  };
596
767
  var STORAGE_KEY = "lk.video.v1";
597
- function sanitizePreferences(raw) {
598
- if (typeof raw !== "object" || raw === null) {
599
- return DEFAULT_PREFERENCES;
768
+ var LANGUAGE_TAG = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
769
+ var BOOLEAN_FIELDS = ["muted", "captions", "captionBackground", "panel", "shortcuts"];
770
+ var LANGUAGE_FIELDS = ["captionLanguage", "secondaryCaptionLanguage"];
771
+ function sanitizePartialPreferences(raw) {
772
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
773
+ return {};
600
774
  }
601
775
  const value = raw;
602
- const bool = (input, fallback) => typeof input === "boolean" ? input : fallback;
603
- const speed = SPEEDS.includes(value.speed) ? value.speed : DEFAULT_PREFERENCES.speed;
604
- const volume = typeof value.volume === "number" && Number.isFinite(value.volume) ? Math.min(1, Math.max(0, value.volume)) : DEFAULT_PREFERENCES.volume;
605
- const size = value.captionSize === "small" || value.captionSize === "medium" || value.captionSize === "large" ? value.captionSize : DEFAULT_PREFERENCES.captionSize;
606
- const language = typeof value.captionLanguage === "string" && /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(value.captionLanguage) ? value.captionLanguage : null;
607
- return {
608
- speed,
609
- volume,
610
- muted: bool(value.muted, DEFAULT_PREFERENCES.muted),
611
- captions: bool(value.captions, DEFAULT_PREFERENCES.captions),
612
- captionLanguage: language,
613
- captionSize: size,
614
- captionBackground: bool(value.captionBackground, DEFAULT_PREFERENCES.captionBackground),
615
- panel: bool(value.panel, DEFAULT_PREFERENCES.panel),
616
- shortcuts: bool(value.shortcuts, DEFAULT_PREFERENCES.shortcuts)
617
- };
776
+ const has = (key) => Object.hasOwn(value, key);
777
+ const out = {};
778
+ if (has("speed") && SPEEDS.includes(value.speed)) {
779
+ out.speed = value.speed;
780
+ }
781
+ if (has("volume") && typeof value.volume === "number" && Number.isFinite(value.volume)) {
782
+ out.volume = Math.min(1, Math.max(0, value.volume));
783
+ }
784
+ for (const field of BOOLEAN_FIELDS) {
785
+ if (has(field) && typeof value[field] === "boolean") {
786
+ out[field] = value[field];
787
+ }
788
+ }
789
+ for (const field of LANGUAGE_FIELDS) {
790
+ const tag = value[field];
791
+ if (has(field) && (tag === null || typeof tag === "string" && LANGUAGE_TAG.test(tag))) {
792
+ out[field] = tag;
793
+ }
794
+ }
795
+ if (has("captionSize") && (value.captionSize === "small" || value.captionSize === "medium" || value.captionSize === "large")) {
796
+ out.captionSize = value.captionSize;
797
+ }
798
+ return out;
799
+ }
800
+ function normalised(preferences) {
801
+ const { captionLanguage: first, secondaryCaptionLanguage: second } = preferences;
802
+ return first !== null && second !== null && first.toLowerCase() === second.toLowerCase() ? { ...preferences, secondaryCaptionLanguage: null } : preferences;
803
+ }
804
+ function resolvePreferences(layers) {
805
+ return normalised({
806
+ ...DEFAULT_PREFERENCES,
807
+ ...sanitizePartialPreferences(layers.defaults),
808
+ ...sanitizePartialPreferences(layers.stored),
809
+ ...sanitizePartialPreferences(layers.force),
810
+ ...sanitizePartialPreferences(layers.chosen)
811
+ });
812
+ }
813
+ function applyPreferenceChange(previous, change) {
814
+ return normalised({ ...previous, ...sanitizePartialPreferences(change) });
618
815
  }
619
- function readPreferences() {
816
+ function readStoredPreferences() {
620
817
  try {
621
818
  const stored = globalThis.localStorage?.getItem(STORAGE_KEY);
622
- return stored ? sanitizePreferences(JSON.parse(stored)) : DEFAULT_PREFERENCES;
819
+ return stored ? sanitizePartialPreferences(JSON.parse(stored)) : {};
623
820
  } catch {
624
- return DEFAULT_PREFERENCES;
821
+ return {};
625
822
  }
626
823
  }
627
- function writePreferences(preferences) {
824
+ function rememberPreferences(change) {
628
825
  try {
629
- globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(preferences));
826
+ const next = { ...readStoredPreferences(), ...sanitizePartialPreferences(change) };
827
+ globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
630
828
  } catch {
631
829
  }
632
830
  }
@@ -946,6 +1144,55 @@ function onNextFrame(run) {
946
1144
  const timer = setTimeout(run, 33);
947
1145
  return () => clearTimeout(timer);
948
1146
  }
1147
+ function trackKey(track) {
1148
+ return `${track.kind}:${track.srclang}:${track.src}`;
1149
+ }
1150
+ var NO_CUES = [];
1151
+ function useCaptionLine(track, load, peek) {
1152
+ const key = track === void 0 ? void 0 : trackKey(track);
1153
+ const [line, setLine] = react.useState({
1154
+ cues: NO_CUES,
1155
+ failed: false
1156
+ });
1157
+ const trackRef = react.useRef(track);
1158
+ trackRef.current = track;
1159
+ react.useEffect(() => {
1160
+ const wanted = trackRef.current;
1161
+ if (key === void 0 || wanted === void 0) {
1162
+ return;
1163
+ }
1164
+ let live = true;
1165
+ load(wanted).then(
1166
+ (cues) => {
1167
+ if (live) {
1168
+ setLine({ key, cues, failed: false });
1169
+ }
1170
+ },
1171
+ () => {
1172
+ if (live) {
1173
+ setLine({ key, cues: NO_CUES, failed: true });
1174
+ }
1175
+ }
1176
+ );
1177
+ return () => {
1178
+ live = false;
1179
+ };
1180
+ }, [key, load]);
1181
+ if (key === void 0) {
1182
+ return { cues: NO_CUES, failed: false };
1183
+ }
1184
+ if (line.key === key) {
1185
+ return line;
1186
+ }
1187
+ return { cues: peek(key) ?? NO_CUES, failed: false };
1188
+ }
1189
+ function captionsShown(tracks, preferences) {
1190
+ if (!preferences.captions) {
1191
+ return { srclang: null, secondary: null };
1192
+ }
1193
+ const shown = resolveCaptionTracks(tracks, preferences);
1194
+ return { srclang: shown.primary?.srclang ?? null, secondary: shown.secondary?.srclang ?? null };
1195
+ }
949
1196
  var playerCount = 0;
950
1197
  var ITEM_TYPES = /* @__PURE__ */ new Set([
951
1198
  "multiple-choice",
@@ -971,13 +1218,13 @@ function Player(props) {
971
1218
  theme,
972
1219
  sanitizeHtml
973
1220
  } = props;
974
- const strings = chunkZ6TVOAO5_cjs.useLkStrings(props.strings);
1221
+ const strings = chunkMQAZJQRE_cjs.useLkStrings(props.strings);
975
1222
  const group = react.useRef(props.group).current;
976
1223
  const timeline = group.timeline ?? { cues: [] };
977
1224
  const media = group.stimulus.media;
978
1225
  const chapters = timeline.chapters ?? [];
979
1226
  const devError = react.useMemo(() => {
980
- if (!chunk53YOZOGN_cjs.isDevelopment()) {
1227
+ if (!chunk2S3MK4VX_cjs.isDevelopment()) {
981
1228
  return null;
982
1229
  }
983
1230
  if (group.timeline === void 0) {
@@ -1023,21 +1270,25 @@ function Player(props) {
1023
1270
  playerCount += 1;
1024
1271
  return `lk-iv-${playerCount}`;
1025
1272
  });
1026
- const [preferences, setPreferences] = react.useState(() => ({
1027
- ...DEFAULT_PREFERENCES,
1028
- ...props.preferences
1029
- }));
1030
- const preferenceOverride = react.useRef(props.preferences);
1273
+ const forced = react.useRef(props.preferences).current;
1274
+ const defaultsKey = JSON.stringify(props.defaultPreferences ?? null);
1275
+ const defaults = react.useMemo(() => JSON.parse(defaultsKey), [defaultsKey]);
1276
+ const chosen = react.useRef({});
1277
+ const [preferences, setPreferences] = react.useState(
1278
+ () => resolvePreferences({ force: forced, defaults: props.defaultPreferences })
1279
+ );
1280
+ const preferencesRef = react.useRef(preferences);
1281
+ preferencesRef.current = preferences;
1031
1282
  react.useEffect(() => {
1032
- setPreferences({ ...readPreferences(), ...preferenceOverride.current });
1033
- }, []);
1034
- const changePreferences = react.useCallback((change) => {
1035
- setPreferences((previous) => {
1036
- const next = { ...previous, ...change };
1037
- writePreferences(next);
1038
- return next;
1039
- });
1040
- }, []);
1283
+ setPreferences(
1284
+ resolvePreferences({
1285
+ force: forced,
1286
+ stored: readStoredPreferences(),
1287
+ defaults,
1288
+ chosen: chosen.current
1289
+ })
1290
+ );
1291
+ }, [forced, defaults]);
1041
1292
  const [submitted] = react.useState(() => new Set(props.submittedSlotIds ?? []));
1042
1293
  const [answered, setAnswered] = react.useState(() => {
1043
1294
  const seeded = /* @__PURE__ */ new Set();
@@ -1050,18 +1301,61 @@ function Player(props) {
1050
1301
  });
1051
1302
  const answeredRef = react.useRef(answered);
1052
1303
  answeredRef.current = answered;
1053
- const scores = react.useRef(/* @__PURE__ */ new Map());
1054
- const markAnswered = react.useCallback((slotId) => {
1304
+ const setSlotAnswered = react.useCallback((slotId, on) => {
1055
1305
  setAnswered((previous) => {
1056
- if (previous.has(slotId)) {
1306
+ if (previous.has(slotId) === on) {
1057
1307
  return previous;
1058
1308
  }
1059
1309
  const next = new Set(previous);
1060
- next.add(slotId);
1310
+ if (on) {
1311
+ next.add(slotId);
1312
+ } else {
1313
+ next.delete(slotId);
1314
+ }
1061
1315
  answeredRef.current = next;
1062
1316
  return next;
1063
1317
  });
1064
1318
  }, []);
1319
+ const [scores, setScores] = react.useState(() => /* @__PURE__ */ new Map());
1320
+ const setSlotScore = react.useCallback((slotId, score) => {
1321
+ setScores((previous) => {
1322
+ if (previous.get(slotId) === score) {
1323
+ return previous;
1324
+ }
1325
+ const next = new Map(previous);
1326
+ if (score === void 0) {
1327
+ next.delete(slotId);
1328
+ } else {
1329
+ next.set(slotId, score);
1330
+ }
1331
+ return next;
1332
+ });
1333
+ }, []);
1334
+ const [pending, setPendingState] = react.useState(() => /* @__PURE__ */ new Set());
1335
+ const pendingRef = react.useRef(pending);
1336
+ const setPendingKey = react.useCallback((key, on) => {
1337
+ setPendingState((previous) => {
1338
+ if (previous.has(key) === on) {
1339
+ return previous;
1340
+ }
1341
+ const next = new Set(previous);
1342
+ if (on) {
1343
+ next.add(key);
1344
+ } else {
1345
+ next.delete(key);
1346
+ }
1347
+ pendingRef.current = next;
1348
+ return next;
1349
+ });
1350
+ }, []);
1351
+ const latestTakes = react.useRef(/* @__PURE__ */ new Map());
1352
+ const alive = react.useRef(true);
1353
+ react.useEffect(() => {
1354
+ alive.current = true;
1355
+ return () => {
1356
+ alive.current = false;
1357
+ };
1358
+ }, []);
1065
1359
  const finished = react.useCallback(
1066
1360
  (cueId) => (slotsByCue.get(cueId) ?? []).every((slot) => answeredRef.current.has(slot.slotId)),
1067
1361
  [slotsByCue]
@@ -1070,6 +1364,7 @@ function Player(props) {
1070
1364
  const videoRef = react.useRef(null);
1071
1365
  const playButtonRef = react.useRef(null);
1072
1366
  const quizHeadingRef = react.useRef(null);
1367
+ const [portalContainer, setPortalContainer] = react.useState(null);
1073
1368
  const [playing, setPlaying] = react.useState(false);
1074
1369
  const [waiting, setWaiting] = react.useState(false);
1075
1370
  const [errorCode, setErrorCode] = react.useState(null);
@@ -1306,43 +1601,124 @@ function Player(props) {
1306
1601
  }
1307
1602
  ] : [];
1308
1603
  }, [media, locale, strings.videoCaptionLanguage]);
1309
- const activeTrack = tracks.find((track) => track.srclang === preferences.captionLanguage) ?? tracks.find((track) => track.default === true) ?? tracks[0];
1310
- const [captionCues, setCaptionCues] = react.useState([]);
1311
- const [captionsFailed, setCaptionsFailed] = react.useState(false);
1604
+ const { primary: primaryTrack, secondary: secondaryTrack } = react.useMemo(
1605
+ () => resolveCaptionTracks(tracks, {
1606
+ captionLanguage: preferences.captionLanguage,
1607
+ secondaryCaptionLanguage: preferences.secondaryCaptionLanguage
1608
+ }),
1609
+ [tracks, preferences.captionLanguage, preferences.secondaryCaptionLanguage]
1610
+ );
1312
1611
  const loaderRef = react.useRef(captionsLoader);
1313
1612
  loaderRef.current = captionsLoader;
1314
- react.useEffect(() => {
1315
- if (activeTrack === void 0) {
1316
- setCaptionCues([]);
1317
- return;
1613
+ const loading = react.useRef(/* @__PURE__ */ new Map());
1614
+ const loaded = react.useRef(/* @__PURE__ */ new Map());
1615
+ const loadCues = react.useCallback((track) => {
1616
+ const key = trackKey(track);
1617
+ const pending2 = loading.current.get(key);
1618
+ if (pending2 !== void 0) {
1619
+ return pending2;
1318
1620
  }
1319
- let live = true;
1320
- setCaptionsFailed(false);
1321
- const load = loaderRef.current ? loaderRef.current(activeTrack) : fetch(activeTrack.src, { credentials: "same-origin" }).then((response) => {
1322
- if (!response.ok) {
1323
- throw new Error(String(response.status));
1621
+ const request = Promise.resolve().then(
1622
+ () => loaderRef.current ? loaderRef.current(track) : fetch(track.src, { credentials: "same-origin" }).then((response) => {
1623
+ if (!response.ok) {
1624
+ throw new Error(String(response.status));
1625
+ }
1626
+ return response.text();
1627
+ })
1628
+ ).then((text) => {
1629
+ const parsed = parseWebVtt(text);
1630
+ if (parsed.length === 0) {
1631
+ throw new Error("The caption file held no cues.");
1324
1632
  }
1325
- return response.text();
1633
+ loaded.current.set(key, parsed);
1634
+ return parsed;
1635
+ });
1636
+ loading.current.set(key, request);
1637
+ request.catch(() => {
1638
+ loading.current.delete(key);
1326
1639
  });
1327
- Promise.resolve(load).then((text) => {
1328
- if (live) {
1329
- const parsed = parseWebVtt(text);
1330
- setCaptionCues(parsed);
1331
- setCaptionsFailed(parsed.length === 0);
1640
+ return request;
1641
+ }, []);
1642
+ const peekCues = react.useCallback((key) => loaded.current.get(key), []);
1643
+ const primaryLine = useCaptionLine(primaryTrack, loadCues, peekCues);
1644
+ const secondaryLine = useCaptionLine(secondaryTrack, loadCues, peekCues);
1645
+ const primaryCues = primaryLine.cues;
1646
+ const secondaryCues = secondaryLine.cues;
1647
+ const captionsFailed = { primary: primaryLine.failed, secondary: secondaryLine.failed };
1648
+ const hasCaptions = primaryCues.length > 0 || secondaryCues.length > 0;
1649
+ const hasTranscript = primaryCues.length > 0;
1650
+ const activeCueIndex = react.useMemo(() => cueIndexAt(primaryCues, current), [primaryCues, current]);
1651
+ const secondaryCueIndex = react.useMemo(
1652
+ () => cueIndexAt(secondaryCues, current),
1653
+ [secondaryCues, current]
1654
+ );
1655
+ const spoilerLimit = timeline.navigation === "no-skip-ahead" && renderMode !== "review" ? Math.max(furthestShown, current) : Number.POSITIVE_INFINITY;
1656
+ const onPreferencesChangeRef = react.useRef(props.onPreferencesChange);
1657
+ onPreferencesChangeRef.current = props.onPreferencesChange;
1658
+ const tracksRef = react.useRef(tracks);
1659
+ tracksRef.current = tracks;
1660
+ const lastSecondary = react.useRef(null);
1661
+ const changePreferences = react.useCallback(
1662
+ (change) => {
1663
+ const previous = preferencesRef.current;
1664
+ const next = applyPreferenceChange(previous, change);
1665
+ if (previous.secondaryCaptionLanguage !== null) {
1666
+ lastSecondary.current = previous.secondaryCaptionLanguage;
1332
1667
  }
1333
- }).catch(() => {
1334
- if (live) {
1335
- setCaptionCues([]);
1336
- setCaptionsFailed(true);
1668
+ chosen.current = { ...chosen.current, ...change };
1669
+ preferencesRef.current = next;
1670
+ setPreferences(next);
1671
+ rememberPreferences(change);
1672
+ onPreferencesChangeRef.current?.(next, change);
1673
+ const before = captionsShown(tracksRef.current, previous);
1674
+ const after = captionsShown(tracksRef.current, next);
1675
+ if (before.srclang !== after.srclang || before.secondary !== after.secondary) {
1676
+ emit("video-captions-changed", after);
1337
1677
  }
1678
+ },
1679
+ [emit]
1680
+ );
1681
+ const chooseCaptionTrack = (track) => {
1682
+ const swaps = primaryTrack !== void 0 && secondaryTrack !== void 0 && track.srclang.toLowerCase() === secondaryTrack.srclang.toLowerCase();
1683
+ changePreferences({
1684
+ captions: true,
1685
+ captionLanguage: track.srclang,
1686
+ ...swaps ? { secondaryCaptionLanguage: primaryTrack.srclang } : {}
1338
1687
  });
1339
- return () => {
1340
- live = false;
1341
- };
1342
- }, [activeTrack]);
1343
- const hasCaptions = captionCues.length > 0;
1344
- const activeCueIndex = react.useMemo(() => cueIndexAt(captionCues, current), [captionCues, current]);
1345
- const spoilerLimit = timeline.navigation === "no-skip-ahead" && renderMode !== "review" ? Math.max(furthestShown, current) : Number.POSITIVE_INFINITY;
1688
+ };
1689
+ const chooseSecondaryTrack = (track) => {
1690
+ changePreferences(
1691
+ track === null ? { secondaryCaptionLanguage: null } : { secondaryCaptionLanguage: track.srclang, captions: true }
1692
+ );
1693
+ };
1694
+ const toggleSecondary = () => {
1695
+ if (secondaryTrack !== void 0) {
1696
+ changePreferences(
1697
+ preferences.captions ? { secondaryCaptionLanguage: null } : { captions: true }
1698
+ );
1699
+ return;
1700
+ }
1701
+ const wanted = [
1702
+ lastSecondary.current,
1703
+ preferences.secondaryCaptionLanguage,
1704
+ sanitizePartialPreferences(defaults).secondaryCaptionLanguage ?? null,
1705
+ secondaryCandidates(tracks, primaryTrack)[0]?.srclang ?? null
1706
+ ];
1707
+ for (const language of wanted) {
1708
+ if (language === null) {
1709
+ continue;
1710
+ }
1711
+ const { secondary } = resolveCaptionTracks(tracks, {
1712
+ captionLanguage: preferences.captionLanguage,
1713
+ secondaryCaptionLanguage: language
1714
+ });
1715
+ if (secondary !== void 0) {
1716
+ changePreferences({ secondaryCaptionLanguage: secondary.srclang, captions: true });
1717
+ return;
1718
+ }
1719
+ }
1720
+ announce(strings.videoNoSecondLanguage);
1721
+ };
1346
1722
  react.useEffect(() => {
1347
1723
  const video = videoRef.current;
1348
1724
  if (video !== null) {
@@ -1509,14 +1885,17 @@ function Player(props) {
1509
1885
  return;
1510
1886
  case "c":
1511
1887
  case "C":
1512
- if (hasCaptions) {
1888
+ if (event.shiftKey) {
1889
+ done();
1890
+ toggleSecondary();
1891
+ } else if (hasCaptions) {
1513
1892
  done();
1514
1893
  changePreferences({ captions: !preferences.captions });
1515
1894
  }
1516
1895
  return;
1517
1896
  case "t":
1518
1897
  case "T":
1519
- if (hasCaptions) {
1898
+ if (hasTranscript) {
1520
1899
  done();
1521
1900
  setPanelTab("transcript");
1522
1901
  changePreferences({ panel: !(preferences.panel && panelTab === "transcript") });
@@ -1566,7 +1945,7 @@ function Player(props) {
1566
1945
  }));
1567
1946
  const totalQuestions = quizzes.reduce((sum, quiz) => sum + quiz.total, 0);
1568
1947
  const answeredCount = quizzes.reduce((sum, quiz) => sum + quiz.answered, 0);
1569
- const graded = [...scores.current.values()];
1948
+ const graded = [...scores.values()];
1570
1949
  const percent = graded.length === 0 ? null : Math.round(graded.reduce((a, b) => a + b, 0) / graded.length * 100);
1571
1950
  const summary = react.useCallback(() => {
1572
1951
  return {
@@ -1588,11 +1967,12 @@ function Player(props) {
1588
1967
  finishedOnce.current = true;
1589
1968
  onFinished?.(summary());
1590
1969
  }, [onFinished, summary]);
1970
+ const answerPending = pending.size > 0;
1591
1971
  react.useEffect(() => {
1592
- if (ended && totalQuestions > 0 && answeredCount === totalQuestions && renderMode !== "review") {
1972
+ if (ended && !answerPending && totalQuestions > 0 && answeredCount === totalQuestions && renderMode !== "review") {
1593
1973
  finish();
1594
1974
  }
1595
- }, [ended, totalQuestions, answeredCount, renderMode, finish]);
1975
+ }, [ended, answerPending, totalQuestions, answeredCount, renderMode, finish]);
1596
1976
  const slotBindings = react.useMemo(() => {
1597
1977
  const bindings = /* @__PURE__ */ new Map();
1598
1978
  if (recordingBinding === void 0) {
@@ -1613,70 +1993,172 @@ function Player(props) {
1613
1993
  }
1614
1994
  return bindings;
1615
1995
  }, [slots, recordingBinding]);
1616
- const renderQuestion = (slot, cueId) => {
1617
- const activity = slot.activity;
1618
- const place = {
1619
- slotId: slot.slotId,
1620
- index: slot.index,
1621
- activityId: activity.id,
1622
- cueId
1996
+ const places = react.useMemo(() => {
1997
+ const byId = /* @__PURE__ */ new Map();
1998
+ for (const [cueId, cueSlots] of slotsByCue) {
1999
+ for (const slot of cueSlots) {
2000
+ byId.set(slot.slotId, {
2001
+ slotId: slot.slotId,
2002
+ index: slot.index,
2003
+ activityId: slot.activity.id,
2004
+ cueId
2005
+ });
2006
+ }
2007
+ }
2008
+ return byId;
2009
+ }, [slotsByCue]);
2010
+ const reportTo = react.useRef({ onSubmit, onActivityComplete, onInteraction, renderMode });
2011
+ reportTo.current = { onSubmit, onActivityComplete, onInteraction, renderMode };
2012
+ const calls = react.useRef(/* @__PURE__ */ new Map());
2013
+ const callsFor = (place) => {
2014
+ const made = calls.current.get(place.slotId);
2015
+ if (made !== void 0) {
2016
+ return made;
2017
+ }
2018
+ const { slotId } = place;
2019
+ const warned = /* @__PURE__ */ new Set();
2020
+ const refused = (call) => {
2021
+ if (reportTo.current.renderMode !== "review") {
2022
+ return false;
2023
+ }
2024
+ if (chunk2S3MK4VX_cjs.isDevelopment() && !warned.has(call)) {
2025
+ warned.add(call);
2026
+ console.warn(
2027
+ `InteractiveVideo: \`${call}()\` was called for question "${slotId}" in renderMode "review", where nothing is submittable. It was ignored.`
2028
+ );
2029
+ }
2030
+ return true;
2031
+ };
2032
+ const next = {
2033
+ submit: (response) => {
2034
+ if (!alive.current || refused("submit")) {
2035
+ return;
2036
+ }
2037
+ setSlotAnswered(slotId, true);
2038
+ reportTo.current.onSubmit?.(response, place);
2039
+ },
2040
+ complete: (result) => {
2041
+ if (!alive.current || refused("complete")) {
2042
+ return;
2043
+ }
2044
+ setSlotAnswered(slotId, true);
2045
+ setSlotScore(slotId, result.maxScore > 0 ? result.score / result.maxScore : void 0);
2046
+ reportTo.current.onActivityComplete?.(result, place);
2047
+ },
2048
+ clear: () => {
2049
+ if (!alive.current || refused("clear")) {
2050
+ return;
2051
+ }
2052
+ setSlotAnswered(slotId, false);
2053
+ setSlotScore(slotId, void 0);
2054
+ },
2055
+ setPending: (on) => {
2056
+ if (!alive.current) {
2057
+ return;
2058
+ }
2059
+ setPendingKey(`host:${slotId}`, on === true);
2060
+ },
2061
+ emit: (type, payload) => {
2062
+ if (!alive.current) {
2063
+ return;
2064
+ }
2065
+ reportTo.current.onInteraction?.({
2066
+ type,
2067
+ activityId: place.activityId,
2068
+ timestamp: Date.now(),
2069
+ ...payload !== void 0 ? { payload } : {}
2070
+ });
2071
+ }
1623
2072
  };
1624
- const restored = props.responses !== void 0 && Object.hasOwn(props.responses, slot.slotId) ? props.responses[slot.slotId] : void 0;
1625
- const outcome = props.outcomes !== void 0 && Object.hasOwn(props.outcomes, slot.slotId) ? props.outcomes[slot.slotId] : void 0;
2073
+ calls.current.set(slotId, next);
2074
+ return next;
2075
+ };
2076
+ const reportTake = (slotId, take, state) => {
2077
+ if (!alive.current) {
2078
+ return;
2079
+ }
2080
+ const latest = latestTakes.current.get(slotId);
2081
+ if (latest !== void 0 && take < latest) {
2082
+ return;
2083
+ }
2084
+ latestTakes.current.set(slotId, take);
2085
+ setPendingKey(`take:${slotId}`, state === "in-flight");
2086
+ };
2087
+ const channels = react.useRef(/* @__PURE__ */ new Map());
2088
+ const channelFor = (slotId) => {
2089
+ let channel = channels.current.get(slotId);
2090
+ if (channel === void 0) {
2091
+ channel = {
2092
+ captureGroup: `${playerId}::${slotId}`,
2093
+ takeState: (take, state) => reportTake(slotId, take, state)
2094
+ };
2095
+ channels.current.set(slotId, channel);
2096
+ }
2097
+ return channel;
2098
+ };
2099
+ const seedsFor = (slotId) => ({
2100
+ restored: props.responses !== void 0 && Object.hasOwn(props.responses, slotId) ? props.responses[slotId] : void 0,
2101
+ outcome: props.outcomes !== void 0 && Object.hasOwn(props.outcomes, slotId) ? props.outcomes[slotId] : void 0
2102
+ });
2103
+ const questionFor = (slot, place, active) => {
2104
+ const { restored, outcome } = seedsFor(slot.slotId);
2105
+ return {
2106
+ activity: slot.activity,
2107
+ slot: place,
2108
+ renderMode,
2109
+ active,
2110
+ ...locale !== void 0 ? { locale } : {},
2111
+ ...restored !== void 0 ? { defaultValue: restored } : {},
2112
+ defaultSubmitted: submitted.has(slot.slotId),
2113
+ ...outcome !== void 0 ? { outcome } : {},
2114
+ portalContainer,
2115
+ ...callsFor(place)
2116
+ };
2117
+ };
2118
+ const sdkQuestion = (slot, place) => {
2119
+ const activity = slot.activity;
2120
+ const { restored, outcome } = seedsFor(slot.slotId);
2121
+ const reported = callsFor(place);
1626
2122
  const common = {
1627
2123
  renderMode,
1628
2124
  ...restored !== void 0 ? { defaultValue: restored } : {},
1629
2125
  ...submitted.has(slot.slotId) ? { defaultSubmitted: true } : {},
1630
2126
  ...outcome !== void 0 ? { outcome } : {},
1631
- onSubmit: (response) => {
1632
- markAnswered(slot.slotId);
1633
- onSubmit?.(response, place);
1634
- },
1635
- onComplete: (result) => {
1636
- markAnswered(slot.slotId);
1637
- if (result.maxScore > 0) {
1638
- scores.current.set(slot.slotId, result.score / result.maxScore);
1639
- }
1640
- onActivityComplete?.(result, place);
1641
- },
2127
+ onSubmit: reported.submit,
2128
+ onComplete: reported.complete,
1642
2129
  ...onInteraction !== void 0 ? { onInteraction } : {},
1643
2130
  ...sanitizeHtml !== void 0 ? { sanitizeHtml } : {},
1644
2131
  ...props.strings !== void 0 ? { strings: props.strings } : {},
1645
2132
  ...locale !== void 0 ? { locale } : {}
1646
2133
  };
1647
- let question;
1648
2134
  switch (activity.type) {
1649
2135
  case "multiple-choice":
1650
- question = /* @__PURE__ */ jsxRuntime.jsx(
1651
- chunkL7NE7PR4_cjs.MultipleChoice,
2136
+ return /* @__PURE__ */ jsxRuntime.jsx(
2137
+ chunkZSY4VANQ_cjs.MultipleChoice,
1652
2138
  {
1653
2139
  data: activity,
1654
2140
  ...common,
1655
2141
  ...shuffleSeed !== void 0 ? { shuffleSeed } : {}
1656
2142
  }
1657
2143
  );
1658
- break;
1659
2144
  case "fill-in-the-blanks":
1660
- question = /* @__PURE__ */ jsxRuntime.jsx(chunk4H3JTX74_cjs.FillInTheBlanks, { data: activity, ...common });
1661
- break;
2145
+ return /* @__PURE__ */ jsxRuntime.jsx(chunk2TFNDHJ5_cjs.FillInTheBlanks, { data: activity, ...common });
1662
2146
  case "gap-select":
1663
- question = /* @__PURE__ */ jsxRuntime.jsx(
1664
- chunkEKBEKDWO_cjs.GapSelect,
2147
+ return /* @__PURE__ */ jsxRuntime.jsx(
2148
+ chunkB6S5ADHR_cjs.GapSelect,
1665
2149
  {
1666
2150
  data: activity,
1667
2151
  ...common,
1668
2152
  ...shuffleSeed !== void 0 ? { shuffleSeed } : {}
1669
2153
  }
1670
2154
  );
1671
- break;
1672
2155
  case "dictation":
1673
- question = /* @__PURE__ */ jsxRuntime.jsx(chunkXMJE34FY_cjs.Dictation, { data: activity, ...common });
1674
- break;
2156
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkRSQFGF5W_cjs.Dictation, { data: activity, ...common });
1675
2157
  case "read-aloud": {
1676
2158
  const binding = slotBindings.get(slot.slotId);
1677
2159
  const assessment = props.assessments !== void 0 && Object.hasOwn(props.assessments, slot.slotId) ? props.assessments[slot.slotId] : void 0;
1678
- question = /* @__PURE__ */ jsxRuntime.jsx(
1679
- chunkZH5WT2IE_cjs.ReadAloud,
2160
+ return /* @__PURE__ */ jsxRuntime.jsx(
2161
+ chunkML6L4LJH_cjs.ReadAloud,
1680
2162
  {
1681
2163
  data: activity,
1682
2164
  ...common,
@@ -1685,21 +2167,10 @@ function Player(props) {
1685
2167
  ...workletUrl !== void 0 ? { workletUrl } : {}
1686
2168
  }
1687
2169
  );
1688
- break;
1689
2170
  }
1690
2171
  default:
1691
- question = null;
1692
- }
1693
- return question;
1694
- };
1695
- const channels = react.useRef(/* @__PURE__ */ new Map());
1696
- const channelFor = (slotId) => {
1697
- let channel = channels.current.get(slotId);
1698
- if (channel === void 0) {
1699
- channel = { captureGroup: `${playerId}::${slotId}`, takeState: () => void 0 };
1700
- channels.current.set(slotId, channel);
2172
+ return null;
1701
2173
  }
1702
- return channel;
1703
2174
  };
1704
2175
  if (devError !== null) {
1705
2176
  throw devError;
@@ -1720,7 +2191,9 @@ function Player(props) {
1720
2191
  emit("video-quiz-question-shown", { cueId: openCue.id, slotId: slot.slotId });
1721
2192
  }
1722
2193
  };
1723
- const showCaption = preferences.captions && hasCaptions && activeCueIndex !== -1 && openQuiz === null && !ended && errorCode === null;
2194
+ const captionsHidden = !preferences.captions || openQuiz !== null || ended || errorCode !== null;
2195
+ const primaryText = captionsHidden ? void 0 : primaryCues[activeCueIndex]?.text;
2196
+ const secondaryText = captionsHidden ? void 0 : secondaryCues[secondaryCueIndex]?.text;
1724
2197
  const chromeVisible = !playing || chromeShown || menu !== null || scrubbing || focusInChrome || sheet || openQuiz !== null;
1725
2198
  const volumeLevel = preferences.muted || preferences.volume === 0 ? "muted" : preferences.volume < 0.5 ? "low" : "high";
1726
2199
  return (
@@ -1832,15 +2305,43 @@ function Player(props) {
1832
2305
  }
1833
2306
  ),
1834
2307
  label !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-iv-label", children: label }) : null,
1835
- showCaption ? /* @__PURE__ */ jsxRuntime.jsx(
1836
- "div",
1837
- {
1838
- className: "lk-iv-caption",
1839
- "data-size": preferences.captionSize,
1840
- "data-background": preferences.captionBackground || void 0,
1841
- lang: activeTrack?.srclang,
1842
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: captionCues[activeCueIndex]?.text })
1843
- }
2308
+ primaryText !== void 0 || secondaryText !== void 0 ? (
2309
+ // The lines keep `.lk-iv-caption`, `data-size` and `data-background`
2310
+ // as well, so a stylesheet written against 15.x still matches them.
2311
+ /* @__PURE__ */ jsxRuntime.jsxs(
2312
+ "div",
2313
+ {
2314
+ className: "lk-iv-captions",
2315
+ "data-size": preferences.captionSize,
2316
+ "data-background": preferences.captionBackground || void 0,
2317
+ children: [
2318
+ primaryText !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2319
+ "div",
2320
+ {
2321
+ className: "lk-iv-caption",
2322
+ "data-role": "primary",
2323
+ "data-size": preferences.captionSize,
2324
+ "data-background": preferences.captionBackground || void 0,
2325
+ lang: primaryTrack?.srclang,
2326
+ dir: "auto",
2327
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: primaryText })
2328
+ }
2329
+ ) : null,
2330
+ secondaryText !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2331
+ "div",
2332
+ {
2333
+ className: "lk-iv-caption",
2334
+ "data-role": "secondary",
2335
+ "data-size": preferences.captionSize,
2336
+ "data-background": preferences.captionBackground || void 0,
2337
+ lang: secondaryTrack?.srclang,
2338
+ dir: "auto",
2339
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: secondaryText })
2340
+ }
2341
+ ) : null
2342
+ ]
2343
+ }
2344
+ )
1844
2345
  ) : null,
1845
2346
  waiting && errorCode === null && openQuiz === null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-iv-spinner", "aria-hidden": "true" }) : null,
1846
2347
  errorCode !== null ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-iv-error", role: "alert", children: [
@@ -1889,7 +2390,7 @@ function Player(props) {
1889
2390
  buffered,
1890
2391
  chapters,
1891
2392
  markers,
1892
- cues: captionCues,
2393
+ cues: primaryCues,
1893
2394
  previewLimit: spoilerLimit,
1894
2395
  strings,
1895
2396
  onSeek: seek,
@@ -1974,7 +2475,7 @@ function Player(props) {
1974
2475
  )
1975
2476
  ] }),
1976
2477
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-iv-bar-group", children: [
1977
- quizzes.length > 0 || chapters.length > 0 || hasCaptions ? /* @__PURE__ */ jsxRuntime.jsx(
2478
+ quizzes.length > 0 || chapters.length > 0 || hasTranscript ? /* @__PURE__ */ jsxRuntime.jsx(
1978
2479
  "button",
1979
2480
  {
1980
2481
  type: "button",
@@ -1992,12 +2493,7 @@ function Player(props) {
1992
2493
  className: "lk-iv-button",
1993
2494
  "aria-label": preferences.captions ? strings.videoCaptionsHide : strings.videoCaptionsShow,
1994
2495
  "aria-pressed": preferences.captions,
1995
- onClick: () => {
1996
- changePreferences({ captions: !preferences.captions });
1997
- emit("video-captions-changed", {
1998
- srclang: preferences.captions ? null : activeTrack?.srclang ?? null
1999
- });
2000
- },
2496
+ onClick: () => changePreferences({ captions: !preferences.captions }),
2001
2497
  children: /* @__PURE__ */ jsxRuntime.jsx(CaptionsIcon, { on: preferences.captions })
2002
2498
  }
2003
2499
  ) : null,
@@ -2045,8 +2541,11 @@ function Player(props) {
2045
2541
  {
2046
2542
  preferences,
2047
2543
  tracks,
2048
- activeTrack,
2544
+ primaryTrack,
2545
+ secondaryTrack,
2049
2546
  captionsFailed,
2547
+ onCaptionTrack: chooseCaptionTrack,
2548
+ onSecondaryTrack: chooseSecondaryTrack,
2050
2549
  strings,
2051
2550
  onChange: changePreferences,
2052
2551
  onShowShortcuts: () => {
@@ -2092,7 +2591,7 @@ function Player(props) {
2092
2591
  "aria-labelledby": `${playerId}-quiz-title`,
2093
2592
  hidden: openQuiz === null,
2094
2593
  onKeyDown: (event) => {
2095
- if (event.key === "Escape" && !required && openQuiz !== null) {
2594
+ if (event.key === "Escape" && !event.defaultPrevented && !required && openQuiz !== null) {
2096
2595
  event.preventDefault();
2097
2596
  closeQuiz("skip");
2098
2597
  }
@@ -2139,15 +2638,29 @@ function Player(props) {
2139
2638
  ] }) : null
2140
2639
  ] }) : null,
2141
2640
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-iv-quiz-body", children: cues.filter((cue) => mounted.has(cue.id)).map(
2142
- (cue) => (slotsByCue.get(cue.id) ?? []).map((slot, index) => /* @__PURE__ */ jsxRuntime.jsx(
2143
- QuestionPane,
2144
- {
2145
- hidden: !(openQuiz?.cueId === cue.id && step === index),
2146
- channel: channelFor(slot.slotId),
2147
- children: renderQuestion(slot, cue.id)
2148
- },
2149
- slot.slotId
2150
- ))
2641
+ (cue) => (slotsByCue.get(cue.id) ?? []).map((slot, index) => {
2642
+ const shown = openQuiz?.cueId === cue.id && step === index;
2643
+ const place = places.get(slot.slotId);
2644
+ return /* @__PURE__ */ jsxRuntime.jsx(
2645
+ QuestionPane,
2646
+ {
2647
+ hidden: !shown,
2648
+ channel: channelFor(slot.slotId),
2649
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2650
+ SlotQuestion,
2651
+ {
2652
+ title: slot.activity.title,
2653
+ strings,
2654
+ render: props.renderQuestion,
2655
+ question: props.renderQuestion === void 0 ? void 0 : questionFor(slot, place, shown),
2656
+ sdk: () => sdkQuestion(slot, place),
2657
+ setHostPending: callsFor(place).setPending
2658
+ }
2659
+ )
2660
+ },
2661
+ slot.slotId
2662
+ );
2663
+ })
2151
2664
  ) }),
2152
2665
  openCue !== void 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-iv-quiz-foot", children: [
2153
2666
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-iv-quiz-foot-start", children: [
@@ -2203,7 +2716,14 @@ function Player(props) {
2203
2716
  }
2204
2717
  },
2205
2718
  onWatchAgain: togglePlay,
2206
- onFinish: renderMode === "review" ? void 0 : finish
2719
+ pending: answerPending,
2720
+ onFinish: renderMode === "review" ? void 0 : () => {
2721
+ if (pendingRef.current.size > 0) {
2722
+ announce(strings.videoAnswerPending);
2723
+ return;
2724
+ }
2725
+ finish();
2726
+ }
2207
2727
  }
2208
2728
  ) : null,
2209
2729
  sheet ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -2227,7 +2747,10 @@ function Player(props) {
2227
2747
  onTab: setPanelTab,
2228
2748
  chapters,
2229
2749
  quizzes,
2230
- cues: captionCues,
2750
+ cues: primaryCues,
2751
+ primaryLanguage: primaryTrack?.srclang,
2752
+ secondaryCues: secondaryTrack === void 0 ? NO_CUES : secondaryCues,
2753
+ secondaryLanguage: secondaryTrack?.srclang,
2231
2754
  activeCue: activeCueIndex,
2232
2755
  limit: spoilerLimit,
2233
2756
  strings,
@@ -2251,12 +2774,43 @@ function Player(props) {
2251
2774
  }
2252
2775
  }
2253
2776
  ) : null,
2254
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-iv-live", "aria-live": "polite", "aria-atomic": "true", children: announcement })
2777
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-iv-live", "aria-live": "polite", "aria-atomic": "true", children: announcement }),
2778
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: setPortalContainer, className: "lk-iv-portal" })
2255
2779
  ]
2256
2780
  }
2257
2781
  )
2258
2782
  );
2259
2783
  }
2784
+ function SlotQuestion({
2785
+ title,
2786
+ strings,
2787
+ ...content
2788
+ }) {
2789
+ return /* @__PURE__ */ jsxRuntime.jsx(
2790
+ chunk2S3MK4VX_cjs.ActivityErrorBoundary,
2791
+ {
2792
+ ...title !== void 0 ? { activityTitle: title } : {},
2793
+ strings,
2794
+ children: /* @__PURE__ */ jsxRuntime.jsx(SlotContent, { ...content })
2795
+ }
2796
+ );
2797
+ }
2798
+ function SlotContent({
2799
+ render,
2800
+ question,
2801
+ sdk,
2802
+ setHostPending
2803
+ }) {
2804
+ const hosted = render !== void 0 && question !== void 0 ? render(question) : void 0;
2805
+ const hosting = hosted !== void 0;
2806
+ react.useEffect(() => {
2807
+ if (!hosting) {
2808
+ return;
2809
+ }
2810
+ return () => setHostPending(false);
2811
+ }, [hosting, setHostPending]);
2812
+ return hosting ? hosted : sdk();
2813
+ }
2260
2814
  function QuestionPane({
2261
2815
  hidden,
2262
2816
  channel,
@@ -2268,7 +2822,7 @@ function QuestionPane({
2268
2822
  if (!hidden) {
2269
2823
  return;
2270
2824
  }
2271
- chunkZH5WT2IE_cjs.stopCaptureGroup(captureGroup);
2825
+ chunkML6L4LJH_cjs.stopCaptureGroup(captureGroup);
2272
2826
  for (const media of ref.current?.querySelectorAll("audio, video") ?? []) {
2273
2827
  const element = media;
2274
2828
  if (!element.paused) {
@@ -2276,7 +2830,7 @@ function QuestionPane({
2276
2830
  }
2277
2831
  }
2278
2832
  }, [hidden, captureGroup]);
2279
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: "lk-iv-question", hidden, children: /* @__PURE__ */ jsxRuntime.jsx(chunkZH5WT2IE_cjs.SequenceSlotContext.Provider, { value: channel, children }) });
2833
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: "lk-iv-question", hidden, children: /* @__PURE__ */ jsxRuntime.jsx(chunkML6L4LJH_cjs.SequenceSlotContext.Provider, { value: channel, children }) });
2280
2834
  }
2281
2835
  function bufferedAhead(video) {
2282
2836
  const ranges = video.buffered;
@@ -2316,9 +2870,9 @@ function markerState(quizSlots, answered, renderMode, outcomes) {
2316
2870
  return "unreached";
2317
2871
  }
2318
2872
  function InteractiveVideo2(props) {
2319
- const strings = chunkZ6TVOAO5_cjs.useLkStrings(props.strings);
2873
+ const strings = chunkMQAZJQRE_cjs.useLkStrings(props.strings);
2320
2874
  return /* @__PURE__ */ jsxRuntime.jsx(
2321
- chunk53YOZOGN_cjs.ActivityErrorBoundary,
2875
+ chunk2S3MK4VX_cjs.ActivityErrorBoundary,
2322
2876
  {
2323
2877
  ...props.group?.title !== void 0 ? { activityTitle: props.group.title } : {},
2324
2878
  strings,
@@ -2328,5 +2882,6 @@ function InteractiveVideo2(props) {
2328
2882
  }
2329
2883
 
2330
2884
  exports.InteractiveVideo = InteractiveVideo2;
2331
- //# sourceMappingURL=chunk-SRD4KVAP.cjs.map
2332
- //# sourceMappingURL=chunk-SRD4KVAP.cjs.map
2885
+ exports.resolveCaptionTracks = resolveCaptionTracks;
2886
+ //# sourceMappingURL=chunk-WOYCT5M6.cjs.map
2887
+ //# sourceMappingURL=chunk-WOYCT5M6.cjs.map