@intellectif/lk-react 23.1.0 → 23.1.2

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 (360) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +9 -5
  3. package/dist/ActivityErrorBoundary-B379hvtL.cjs +39 -0
  4. package/dist/ActivityErrorBoundary-B379hvtL.cjs.map +1 -0
  5. package/dist/ActivityErrorBoundary-Dw6SbWv3.js +34 -0
  6. package/dist/ActivityErrorBoundary-Dw6SbWv3.js.map +1 -0
  7. package/dist/ActivityMedia-CQQTRNl7.cjs +641 -0
  8. package/dist/ActivityMedia-CQQTRNl7.cjs.map +1 -0
  9. package/dist/ActivityMedia-vDiP-Psa.js +636 -0
  10. package/dist/ActivityMedia-vDiP-Psa.js.map +1 -0
  11. package/dist/ActivityPreview-1hhTkYuj.js +240 -0
  12. package/dist/ActivityPreview-1hhTkYuj.js.map +1 -0
  13. package/dist/ActivityPreview-BKr34exz.cjs +245 -0
  14. package/dist/ActivityPreview-BKr34exz.cjs.map +1 -0
  15. package/dist/ActivitySequence-CkX87pfB.js +839 -0
  16. package/dist/ActivitySequence-CkX87pfB.js.map +1 -0
  17. package/dist/ActivitySequence-CmoSyDnp.cjs +844 -0
  18. package/dist/ActivitySequence-CmoSyDnp.cjs.map +1 -0
  19. package/dist/Dictation-BlxRp-3k.js +926 -0
  20. package/dist/Dictation-BlxRp-3k.js.map +1 -0
  21. package/dist/Dictation-jECzL88E.cjs +931 -0
  22. package/dist/Dictation-jECzL88E.cjs.map +1 -0
  23. package/dist/FeedbackRegion-BNNMtTY-.cjs +108 -0
  24. package/dist/FeedbackRegion-BNNMtTY-.cjs.map +1 -0
  25. package/dist/FeedbackRegion-CyPSp8bA.js +85 -0
  26. package/dist/FeedbackRegion-CyPSp8bA.js.map +1 -0
  27. package/dist/FillInTheBlanks-B6xzqYXx.js +474 -0
  28. package/dist/FillInTheBlanks-B6xzqYXx.js.map +1 -0
  29. package/dist/FillInTheBlanks-BpKhdHg2.cjs +479 -0
  30. package/dist/FillInTheBlanks-BpKhdHg2.cjs.map +1 -0
  31. package/dist/GapSelect-DUSiaEQH.js +441 -0
  32. package/dist/GapSelect-DUSiaEQH.js.map +1 -0
  33. package/dist/GapSelect-payTq_Zn.cjs +446 -0
  34. package/dist/GapSelect-payTq_Zn.cjs.map +1 -0
  35. package/dist/InteractiveVideo-BguGv-HA.cjs +2955 -0
  36. package/dist/InteractiveVideo-BguGv-HA.cjs.map +1 -0
  37. package/dist/InteractiveVideo-Cg2jbU3j.js +2944 -0
  38. package/dist/InteractiveVideo-Cg2jbU3j.js.map +1 -0
  39. package/dist/LkAiProvider-DAPpA3PQ.d.cts +84 -0
  40. package/dist/LkAiProvider-DAPpA3PQ.d.cts.map +1 -0
  41. package/dist/LkAiProvider-QdEcIhoo.d.ts +84 -0
  42. package/dist/LkAiProvider-QdEcIhoo.d.ts.map +1 -0
  43. package/dist/LkIntlProvider-D_FdYXjQ.js +375 -0
  44. package/dist/LkIntlProvider-D_FdYXjQ.js.map +1 -0
  45. package/dist/LkIntlProvider-oi57lYB0.cjs +410 -0
  46. package/dist/LkIntlProvider-oi57lYB0.cjs.map +1 -0
  47. package/dist/MultipleChoice-COn5MtKY.cjs +433 -0
  48. package/dist/MultipleChoice-COn5MtKY.cjs.map +1 -0
  49. package/dist/MultipleChoice-Dra0_KYD.js +428 -0
  50. package/dist/MultipleChoice-Dra0_KYD.js.map +1 -0
  51. package/dist/PronunciationFeedback-DlMPGjkR.cjs +831 -0
  52. package/dist/PronunciationFeedback-DlMPGjkR.cjs.map +1 -0
  53. package/dist/PronunciationFeedback-Dxd6XPkO.js +772 -0
  54. package/dist/PronunciationFeedback-Dxd6XPkO.js.map +1 -0
  55. package/dist/ReadAloud-C8RhaUqV.js +1092 -0
  56. package/dist/{chunk-DC4CQCK5.js.map → ReadAloud-C8RhaUqV.js.map} +1 -1
  57. package/dist/ReadAloud-akDehitT.cjs +1103 -0
  58. package/dist/ReadAloud-akDehitT.cjs.map +1 -0
  59. package/dist/StimulusPanel-BgzByfkb.js +74 -0
  60. package/dist/StimulusPanel-BgzByfkb.js.map +1 -0
  61. package/dist/StimulusPanel-b66gaN3i.cjs +79 -0
  62. package/dist/StimulusPanel-b66gaN3i.cjs.map +1 -0
  63. package/dist/ThemeProvider-B6DyGqzh.d.cts +62 -0
  64. package/dist/ThemeProvider-B6DyGqzh.d.cts.map +1 -0
  65. package/dist/ThemeProvider-BCrxBaY-.d.ts +62 -0
  66. package/dist/ThemeProvider-BCrxBaY-.d.ts.map +1 -0
  67. package/dist/ThemeProvider-BRN1tj4n.cjs +187 -0
  68. package/dist/ThemeProvider-BRN1tj4n.cjs.map +1 -0
  69. package/dist/ThemeProvider-C7K9iMLX.js +158 -0
  70. package/dist/ThemeProvider-C7K9iMLX.js.map +1 -0
  71. package/dist/WrittenResponse-Bj3eFmGl.cjs +491 -0
  72. package/dist/WrittenResponse-Bj3eFmGl.cjs.map +1 -0
  73. package/dist/WrittenResponse-Cl8G6nO_.js +486 -0
  74. package/dist/WrittenResponse-Cl8G6nO_.js.map +1 -0
  75. package/dist/ai/LkAiProvider.cjs +34 -14
  76. package/dist/ai/LkAiProvider.cjs.map +1 -1
  77. package/dist/ai/LkAiProvider.d.cts +2 -83
  78. package/dist/ai/LkAiProvider.d.ts +2 -83
  79. package/dist/ai/LkAiProvider.js +33 -2
  80. package/dist/ai/LkAiProvider.js.map +1 -1
  81. package/dist/ai/useAiHelp.cjs +7 -25
  82. package/dist/ai/useAiHelp.d.cts +175 -177
  83. package/dist/ai/useAiHelp.d.cts.map +1 -0
  84. package/dist/ai/useAiHelp.d.ts +175 -177
  85. package/dist/ai/useAiHelp.d.ts.map +1 -0
  86. package/dist/ai/useAiHelp.js +3 -4
  87. package/dist/components/ActivityPreview.cjs +3 -29
  88. package/dist/components/ActivityPreview.d.cts +2 -81
  89. package/dist/components/ActivityPreview.d.ts +2 -81
  90. package/dist/components/ActivityPreview.js +2 -20
  91. package/dist/components/ActivitySequence.cjs +3 -30
  92. package/dist/components/ActivitySequence.d.cts +2 -432
  93. package/dist/components/ActivitySequence.d.ts +2 -432
  94. package/dist/components/ActivitySequence.js +2 -21
  95. package/dist/components/Dictation.cjs +4 -21
  96. package/dist/components/Dictation.d.cts +2 -19
  97. package/dist/components/Dictation.d.ts +2 -19
  98. package/dist/components/Dictation.js +3 -12
  99. package/dist/components/FillInTheBlanks.cjs +4 -20
  100. package/dist/components/FillInTheBlanks.d.cts +2 -30
  101. package/dist/components/FillInTheBlanks.d.ts +2 -30
  102. package/dist/components/FillInTheBlanks.js +3 -11
  103. package/dist/components/GapSelect.cjs +4 -20
  104. package/dist/components/GapSelect.d.cts +2 -27
  105. package/dist/components/GapSelect.d.ts +2 -27
  106. package/dist/components/GapSelect.js +3 -11
  107. package/dist/components/InteractiveVideo.cjs +5 -32
  108. package/dist/components/InteractiveVideo.d.cts +3 -291
  109. package/dist/components/InteractiveVideo.d.ts +3 -291
  110. package/dist/components/InteractiveVideo.js +3 -19
  111. package/dist/components/MultipleChoice.cjs +4 -20
  112. package/dist/components/MultipleChoice.d.cts +2 -26
  113. package/dist/components/MultipleChoice.d.ts +2 -26
  114. package/dist/components/MultipleChoice.js +3 -11
  115. package/dist/components/PronunciationFeedback.cjs +4 -17
  116. package/dist/components/PronunciationFeedback.d.cts +2 -90
  117. package/dist/components/PronunciationFeedback.d.ts +2 -90
  118. package/dist/components/PronunciationFeedback.js +3 -8
  119. package/dist/components/ReadAloud.cjs +4 -22
  120. package/dist/components/ReadAloud.d.cts +7 -11
  121. package/dist/components/ReadAloud.d.cts.map +1 -0
  122. package/dist/components/ReadAloud.d.ts +7 -11
  123. package/dist/components/ReadAloud.d.ts.map +1 -0
  124. package/dist/components/ReadAloud.js +3 -13
  125. package/dist/components/StimulusPanel.cjs +3 -14
  126. package/dist/components/StimulusPanel.d.cts +2 -51
  127. package/dist/components/StimulusPanel.d.ts +2 -51
  128. package/dist/components/StimulusPanel.js +2 -5
  129. package/dist/components/WrittenResponse.cjs +4 -19
  130. package/dist/components/WrittenResponse.d.cts +3 -18
  131. package/dist/components/WrittenResponse.d.ts +3 -18
  132. package/dist/components/WrittenResponse.js +3 -10
  133. package/dist/direction-Dq0_HtA5.js +183 -0
  134. package/dist/direction-Dq0_HtA5.js.map +1 -0
  135. package/dist/direction-K1pj0HCA.cjs +200 -0
  136. package/dist/direction-K1pj0HCA.cjs.map +1 -0
  137. package/dist/hooks/useActivityState.cjs +28 -10
  138. package/dist/hooks/useActivityState.cjs.map +1 -1
  139. package/dist/hooks/useActivityState.d.cts +25 -24
  140. package/dist/hooks/useActivityState.d.cts.map +1 -0
  141. package/dist/hooks/useActivityState.d.ts +25 -24
  142. package/dist/hooks/useActivityState.d.ts.map +1 -0
  143. package/dist/hooks/useActivityState.js +28 -2
  144. package/dist/hooks/useActivityState.js.map +1 -1
  145. package/dist/hooks/useSpeechRecorder.cjs +5 -16
  146. package/dist/hooks/useSpeechRecorder.d.cts +2 -127
  147. package/dist/hooks/useSpeechRecorder.d.ts +2 -127
  148. package/dist/hooks/useSpeechRecorder.js +3 -3
  149. package/dist/hooks/useXAPI.cjs +126 -10
  150. package/dist/hooks/useXAPI.cjs.map +1 -1
  151. package/dist/hooks/useXAPI.d.cts +14 -14
  152. package/dist/hooks/useXAPI.d.cts.map +1 -0
  153. package/dist/hooks/useXAPI.d.ts +14 -14
  154. package/dist/hooks/useXAPI.d.ts.map +1 -0
  155. package/dist/hooks/useXAPI.js +126 -2
  156. package/dist/hooks/useXAPI.js.map +1 -1
  157. package/dist/i18n/LkIntlProvider.cjs +9 -32
  158. package/dist/i18n/LkIntlProvider.d.cts +34 -38
  159. package/dist/i18n/LkIntlProvider.d.cts.map +1 -0
  160. package/dist/i18n/LkIntlProvider.d.ts +34 -38
  161. package/dist/i18n/LkIntlProvider.d.ts.map +1 -0
  162. package/dist/i18n/LkIntlProvider.js +3 -3
  163. package/dist/index-6P4rJHrJ.d.cts +432 -0
  164. package/dist/index-6P4rJHrJ.d.cts.map +1 -0
  165. package/dist/index-90CX504k.d.ts +90 -0
  166. package/dist/index-90CX504k.d.ts.map +1 -0
  167. package/dist/index-B_SgnjsC.d.cts +291 -0
  168. package/dist/index-B_SgnjsC.d.cts.map +1 -0
  169. package/dist/index-BavVW5z5.d.cts +25 -0
  170. package/dist/index-BavVW5z5.d.cts.map +1 -0
  171. package/dist/index-BhHv6Fiu.d.cts +28 -0
  172. package/dist/index-BhHv6Fiu.d.cts.map +1 -0
  173. package/dist/index-Bsu8VciA.d.ts +432 -0
  174. package/dist/index-Bsu8VciA.d.ts.map +1 -0
  175. package/dist/index-C1KGpD1k.d.ts +49 -0
  176. package/dist/index-C1KGpD1k.d.ts.map +1 -0
  177. package/dist/index-CKTpYTCC.d.cts +49 -0
  178. package/dist/index-CKTpYTCC.d.cts.map +1 -0
  179. package/dist/index-CO_SRFJj.d.ts +16 -0
  180. package/dist/index-CO_SRFJj.d.ts.map +1 -0
  181. package/dist/index-CSsuUsP7.d.cts +124 -0
  182. package/dist/index-CSsuUsP7.d.cts.map +1 -0
  183. package/dist/index-CunUyeuN.d.cts +78 -0
  184. package/dist/index-CunUyeuN.d.cts.map +1 -0
  185. package/dist/index-CzfRzks6.d.cts +90 -0
  186. package/dist/index-CzfRzks6.d.cts.map +1 -0
  187. package/dist/index-D14xtpBS.d.cts +24 -0
  188. package/dist/index-D14xtpBS.d.cts.map +1 -0
  189. package/dist/index-DHRPAPZf.d.ts +291 -0
  190. package/dist/index-DHRPAPZf.d.ts.map +1 -0
  191. package/dist/index-DLV6cK26.d.cts +16 -0
  192. package/dist/index-DLV6cK26.d.cts.map +1 -0
  193. package/dist/index-DbCCQ9cP.d.ts +124 -0
  194. package/dist/index-DbCCQ9cP.d.ts.map +1 -0
  195. package/dist/index-DeV5B2m5.d.ts +24 -0
  196. package/dist/index-DeV5B2m5.d.ts.map +1 -0
  197. package/dist/index-TZCJVSge.d.ts +28 -0
  198. package/dist/index-TZCJVSge.d.ts.map +1 -0
  199. package/dist/index-ZVvLHsd3.d.ts +25 -0
  200. package/dist/index-ZVvLHsd3.d.ts.map +1 -0
  201. package/dist/index-eQwBgpSw.d.ts +78 -0
  202. package/dist/index-eQwBgpSw.d.ts.map +1 -0
  203. package/dist/index.cjs +118 -163
  204. package/dist/index.cjs.map +1 -1
  205. package/dist/index.d.cts +20 -24
  206. package/dist/index.d.ts +20 -24
  207. package/dist/index.js +84 -29
  208. package/dist/index.js.map +1 -1
  209. package/dist/theme/ThemeProvider.cjs +8 -28
  210. package/dist/theme/ThemeProvider.d.cts +2 -59
  211. package/dist/theme/ThemeProvider.d.ts +2 -59
  212. package/dist/theme/ThemeProvider.js +3 -3
  213. package/dist/tries-CbbtZkdG.cjs +363 -0
  214. package/dist/tries-CbbtZkdG.cjs.map +1 -0
  215. package/dist/tries-DTHSrLCm.js +310 -0
  216. package/dist/tries-DTHSrLCm.js.map +1 -0
  217. package/dist/types-BOQd93x3.d.cts +994 -0
  218. package/dist/types-BOQd93x3.d.cts.map +1 -0
  219. package/dist/types-HIrgF_mS.d.ts +994 -0
  220. package/dist/types-HIrgF_mS.d.ts.map +1 -0
  221. package/dist/useAiHelp-B-ccDL5Q.js +906 -0
  222. package/dist/useAiHelp-B-ccDL5Q.js.map +1 -0
  223. package/dist/useAiHelp-h9yOLFqt.cjs +995 -0
  224. package/dist/useAiHelp-h9yOLFqt.cjs.map +1 -0
  225. package/dist/useSpeechRecorder--O_f-xvt.cjs +720 -0
  226. package/dist/useSpeechRecorder--O_f-xvt.cjs.map +1 -0
  227. package/dist/useSpeechRecorder-DtAYvl02.d.cts +154 -0
  228. package/dist/useSpeechRecorder-DtAYvl02.d.cts.map +1 -0
  229. package/dist/useSpeechRecorder-DtAYvl02.d.ts +154 -0
  230. package/dist/useSpeechRecorder-DtAYvl02.d.ts.map +1 -0
  231. package/dist/useSpeechRecorder-zIi9Dlw8.js +709 -0
  232. package/dist/useSpeechRecorder-zIi9Dlw8.js.map +1 -0
  233. package/package.json +10 -11
  234. package/dist/WrittenResponse-B7A5gHMj.d.ts +0 -115
  235. package/dist/WrittenResponse-BpL9p2Hn.d.cts +0 -115
  236. package/dist/ai/useAiHelp.cjs.map +0 -1
  237. package/dist/ai/useAiHelp.js.map +0 -1
  238. package/dist/chunk-2PNSVBLQ.cjs +0 -54
  239. package/dist/chunk-2PNSVBLQ.cjs.map +0 -1
  240. package/dist/chunk-2TW6WLBC.js +0 -80
  241. package/dist/chunk-2TW6WLBC.js.map +0 -1
  242. package/dist/chunk-4ZZWZSWI.js +0 -61
  243. package/dist/chunk-4ZZWZSWI.js.map +0 -1
  244. package/dist/chunk-6N43WDVG.js +0 -29
  245. package/dist/chunk-6N43WDVG.js.map +0 -1
  246. package/dist/chunk-7ALBSPYR.cjs +0 -436
  247. package/dist/chunk-7ALBSPYR.cjs.map +0 -1
  248. package/dist/chunk-7NFY3M3W.js +0 -234
  249. package/dist/chunk-7NFY3M3W.js.map +0 -1
  250. package/dist/chunk-7SB2MJA4.js +0 -29
  251. package/dist/chunk-7SB2MJA4.js.map +0 -1
  252. package/dist/chunk-ABJJ3LII.js +0 -434
  253. package/dist/chunk-ABJJ3LII.js.map +0 -1
  254. package/dist/chunk-BIGGXHAW.cjs +0 -342
  255. package/dist/chunk-BIGGXHAW.cjs.map +0 -1
  256. package/dist/chunk-C7QZE5BI.js +0 -430
  257. package/dist/chunk-C7QZE5BI.js.map +0 -1
  258. package/dist/chunk-CWTUWNV3.cjs +0 -2937
  259. package/dist/chunk-CWTUWNV3.cjs.map +0 -1
  260. package/dist/chunk-DC4CQCK5.js +0 -915
  261. package/dist/chunk-DS6HN3YR.cjs +0 -496
  262. package/dist/chunk-DS6HN3YR.cjs.map +0 -1
  263. package/dist/chunk-FDQR4GAZ.js +0 -240
  264. package/dist/chunk-FDQR4GAZ.js.map +0 -1
  265. package/dist/chunk-FKGMSOJF.js +0 -394
  266. package/dist/chunk-FKGMSOJF.js.map +0 -1
  267. package/dist/chunk-G6PXJ2SA.js +0 -823
  268. package/dist/chunk-G6PXJ2SA.js.map +0 -1
  269. package/dist/chunk-HNLRVRQR.js +0 -335
  270. package/dist/chunk-HNLRVRQR.js.map +0 -1
  271. package/dist/chunk-J3C72RPK.cjs +0 -31
  272. package/dist/chunk-J3C72RPK.cjs.map +0 -1
  273. package/dist/chunk-K63VPFOV.cjs +0 -134
  274. package/dist/chunk-K63VPFOV.cjs.map +0 -1
  275. package/dist/chunk-KFLIDTDU.cjs +0 -825
  276. package/dist/chunk-KFLIDTDU.cjs.map +0 -1
  277. package/dist/chunk-KHKH5LCT.cjs +0 -250
  278. package/dist/chunk-KHKH5LCT.cjs.map +0 -1
  279. package/dist/chunk-LBEKRN6Y.cjs +0 -664
  280. package/dist/chunk-LBEKRN6Y.cjs.map +0 -1
  281. package/dist/chunk-LPUWXIPR.cjs +0 -578
  282. package/dist/chunk-LPUWXIPR.cjs.map +0 -1
  283. package/dist/chunk-MCO52PV5.js +0 -520
  284. package/dist/chunk-MCO52PV5.js.map +0 -1
  285. package/dist/chunk-MKXEM2C5.cjs +0 -396
  286. package/dist/chunk-MKXEM2C5.cjs.map +0 -1
  287. package/dist/chunk-MXNC5UNO.js +0 -563
  288. package/dist/chunk-MXNC5UNO.js.map +0 -1
  289. package/dist/chunk-NAK55BYU.cjs +0 -859
  290. package/dist/chunk-NAK55BYU.cjs.map +0 -1
  291. package/dist/chunk-NDBMKGQ5.cjs +0 -599
  292. package/dist/chunk-NDBMKGQ5.cjs.map +0 -1
  293. package/dist/chunk-OD65OZIW.cjs +0 -159
  294. package/dist/chunk-OD65OZIW.cjs.map +0 -1
  295. package/dist/chunk-ORT3KLGI.js +0 -50
  296. package/dist/chunk-ORT3KLGI.js.map +0 -1
  297. package/dist/chunk-PNNUPUH4.cjs +0 -82
  298. package/dist/chunk-PNNUPUH4.cjs.map +0 -1
  299. package/dist/chunk-PWHECW6N.js +0 -2934
  300. package/dist/chunk-PWHECW6N.js.map +0 -1
  301. package/dist/chunk-RBS5A2DF.js +0 -857
  302. package/dist/chunk-RBS5A2DF.js.map +0 -1
  303. package/dist/chunk-RPWUDXG5.cjs +0 -63
  304. package/dist/chunk-RPWUDXG5.cjs.map +0 -1
  305. package/dist/chunk-TCDJY3H6.js +0 -494
  306. package/dist/chunk-TCDJY3H6.js.map +0 -1
  307. package/dist/chunk-TP5UMRAU.cjs +0 -918
  308. package/dist/chunk-TP5UMRAU.cjs.map +0 -1
  309. package/dist/chunk-TS7J6DTY.js +0 -597
  310. package/dist/chunk-TS7J6DTY.js.map +0 -1
  311. package/dist/chunk-TUVENMXB.cjs +0 -20
  312. package/dist/chunk-TUVENMXB.cjs.map +0 -1
  313. package/dist/chunk-UDCQTOVU.cjs +0 -523
  314. package/dist/chunk-UDCQTOVU.cjs.map +0 -1
  315. package/dist/chunk-UHKZXOXD.js +0 -128
  316. package/dist/chunk-UHKZXOXD.js.map +0 -1
  317. package/dist/chunk-V6BA44DS.cjs +0 -236
  318. package/dist/chunk-V6BA44DS.cjs.map +0 -1
  319. package/dist/chunk-XO5R73Y4.js +0 -653
  320. package/dist/chunk-XO5R73Y4.js.map +0 -1
  321. package/dist/chunk-XOR4MPEN.cjs +0 -31
  322. package/dist/chunk-XOR4MPEN.cjs.map +0 -1
  323. package/dist/chunk-YORQQXGO.js +0 -155
  324. package/dist/chunk-YORQQXGO.js.map +0 -1
  325. package/dist/chunk-YY4PFR3Z.cjs +0 -432
  326. package/dist/chunk-YY4PFR3Z.cjs.map +0 -1
  327. package/dist/chunk-ZPI6Q42R.js +0 -17
  328. package/dist/chunk-ZPI6Q42R.js.map +0 -1
  329. package/dist/components/ActivityPreview.cjs.map +0 -1
  330. package/dist/components/ActivityPreview.js.map +0 -1
  331. package/dist/components/ActivitySequence.cjs.map +0 -1
  332. package/dist/components/ActivitySequence.js.map +0 -1
  333. package/dist/components/Dictation.cjs.map +0 -1
  334. package/dist/components/Dictation.js.map +0 -1
  335. package/dist/components/FillInTheBlanks.cjs.map +0 -1
  336. package/dist/components/FillInTheBlanks.js.map +0 -1
  337. package/dist/components/GapSelect.cjs.map +0 -1
  338. package/dist/components/GapSelect.js.map +0 -1
  339. package/dist/components/InteractiveVideo.cjs.map +0 -1
  340. package/dist/components/InteractiveVideo.js.map +0 -1
  341. package/dist/components/MultipleChoice.cjs.map +0 -1
  342. package/dist/components/MultipleChoice.js.map +0 -1
  343. package/dist/components/PronunciationFeedback.cjs.map +0 -1
  344. package/dist/components/PronunciationFeedback.js.map +0 -1
  345. package/dist/components/ReadAloud.cjs.map +0 -1
  346. package/dist/components/ReadAloud.js.map +0 -1
  347. package/dist/components/StimulusPanel.cjs.map +0 -1
  348. package/dist/components/StimulusPanel.js.map +0 -1
  349. package/dist/components/WrittenResponse.cjs.map +0 -1
  350. package/dist/components/WrittenResponse.js.map +0 -1
  351. package/dist/hooks/useSpeechRecorder.cjs.map +0 -1
  352. package/dist/hooks/useSpeechRecorder.js.map +0 -1
  353. package/dist/i18n/LkIntlProvider.cjs.map +0 -1
  354. package/dist/i18n/LkIntlProvider.js.map +0 -1
  355. package/dist/machine-CuwKqWXC.d.cts +0 -26
  356. package/dist/machine-CuwKqWXC.d.ts +0 -26
  357. package/dist/theme/ThemeProvider.cjs.map +0 -1
  358. package/dist/theme/ThemeProvider.js.map +0 -1
  359. package/dist/types-C4IsUQsK.d.ts +0 -989
  360. package/dist/types-CJgZWtl4.d.cts +0 -989
@@ -0,0 +1,2955 @@
1
+ 'use client';
2
+ const require_ai_LkAiProvider = require("./ai/LkAiProvider.cjs");
3
+ const require_useAiHelp = require("./useAiHelp-h9yOLFqt.cjs");
4
+ const require_LkIntlProvider = require("./LkIntlProvider-oi57lYB0.cjs");
5
+ const require_ActivityErrorBoundary = require("./ActivityErrorBoundary-B379hvtL.cjs");
6
+ const require_Dictation = require("./Dictation-jECzL88E.cjs");
7
+ const require_FillInTheBlanks = require("./FillInTheBlanks-BpKhdHg2.cjs");
8
+ const require_GapSelect = require("./GapSelect-payTq_Zn.cjs");
9
+ const require_MultipleChoice = require("./MultipleChoice-COn5MtKY.cjs");
10
+ const require_ReadAloud = require("./ReadAloud-akDehitT.cjs");
11
+ let react = require("react");
12
+ let react_jsx_runtime = require("react/jsx-runtime");
13
+ let _intellectif_lk_core = require("@intellectif/lk-core");
14
+ //#region src/components/InteractiveVideo/vtt.ts
15
+ /** Bytes of WebVTT read at most. A caption file is text; this is several hours of it. */
16
+ const MAX_VTT_LENGTH = 2e6;
17
+ /** Cues kept at most, for the same reason: the transcript renders every one. */
18
+ const MAX_CUES = 1e4;
19
+ const BYTE_ORDER_MARK = String.fromCharCode(65279);
20
+ const TIMESTAMP = /^(?:(\d+):)?([0-5]\d):([0-5]\d)\.(\d{3})$/;
21
+ function seconds(stamp) {
22
+ const match = TIMESTAMP.exec(stamp.trim());
23
+ if (match === null) return;
24
+ const [, hours = "0", minutes = "0", secs = "0", millis = "0"] = match;
25
+ return Number(hours) * 3600 + Number(minutes) * 60 + Number(secs) + Number(millis) / 1e3;
26
+ }
27
+ const ENTITIES = {
28
+ amp: "&",
29
+ lt: "<",
30
+ gt: ">",
31
+ quot: "\"",
32
+ apos: "'",
33
+ nbsp: String.fromCharCode(160),
34
+ lrm: String.fromCharCode(8206),
35
+ rlm: String.fromCharCode(8207)
36
+ };
37
+ /**
38
+ * A cue payload as the text a learner reads: markup removed (`<v Speaker>`,
39
+ * `<i>`, `<c.class>`, inline timestamps), entities decoded, lines joined with
40
+ * a space.
41
+ */
42
+ function plainText(payload) {
43
+ return payload.replace(/<[^>]*>/g, "").replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, name) => {
44
+ if (name.startsWith("#")) {
45
+ const code = name[1] === "x" || name[1] === "X" ? Number.parseInt(name.slice(2), 16) : Number.parseInt(name.slice(1), 10);
46
+ return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
47
+ }
48
+ return ENTITIES[name.toLowerCase()] ?? whole;
49
+ }).replace(/\s*\n\s*/g, " ").trim();
50
+ }
51
+ /**
52
+ * Reads a WebVTT file into cues, sorted by start. Never throws: a malformed cue
53
+ * is skipped, and text that is not WebVTT at all answers an empty list.
54
+ *
55
+ * The SDK parses captions itself rather than handing them to a `<track>`:
56
+ * the caption preview on the progress bar, the transcript and its search all
57
+ * need the cues as data, a host whose caption URL needs its credentials can
58
+ * pass the text in, and a test environment with no media stack can read them.
59
+ * Cue settings (position, alignment) are ignored — the player draws captions
60
+ * in one place, above its own controls.
61
+ */
62
+ function parseWebVtt(input) {
63
+ if (typeof input !== "string") return [];
64
+ const raw = input.slice(0, MAX_VTT_LENGTH);
65
+ const text = (raw.startsWith(BYTE_ORDER_MARK) ? raw.slice(1) : raw).replace(/\r\n?/g, "\n");
66
+ if (!/^WEBVTT(?:[ \t].*)?(?:\n|$)/.test(text)) return [];
67
+ const cues = [];
68
+ for (const block of text.split(/\n{2,}/)) {
69
+ if (cues.length >= MAX_CUES) break;
70
+ const lines = block.split("\n");
71
+ const timingIndex = lines.findIndex((line) => line.includes("-->"));
72
+ if (timingIndex === -1) continue;
73
+ const [from = "", rest = ""] = lines[timingIndex].split("-->");
74
+ const start = seconds(from);
75
+ const end = seconds(rest.trim().split(/\s+/)[0] ?? "");
76
+ if (start === void 0 || end === void 0 || end <= start) continue;
77
+ const payload = plainText(lines.slice(timingIndex + 1).join("\n"));
78
+ if (payload === "") continue;
79
+ cues.push({
80
+ start,
81
+ end,
82
+ text: payload
83
+ });
84
+ }
85
+ return cues.map((cue, index) => ({
86
+ cue,
87
+ index
88
+ })).sort((a, b) => a.cue.start - b.cue.start || a.index - b.index).map(({ cue }) => cue);
89
+ }
90
+ /**
91
+ * The cue showing at `time`, or -1. When cues overlap, the one that started
92
+ * last wins — the newest line is the one being spoken.
93
+ */
94
+ function cueIndexAt(cues, time) {
95
+ let low = 0;
96
+ let high = cues.length - 1;
97
+ let found = -1;
98
+ while (low <= high) {
99
+ const middle = low + high >> 1;
100
+ if (cues[middle].start <= time) {
101
+ found = middle;
102
+ low = middle + 1;
103
+ } else high = middle - 1;
104
+ }
105
+ for (let index = found; index >= 0 && index > found - 8; index -= 1) if (time < cues[index].end) return index;
106
+ return -1;
107
+ }
108
+ //#endregion
109
+ //#region src/components/InteractiveVideo/caption-cues.ts
110
+ /** A track as the cue cache knows it. */
111
+ function trackKey(track) {
112
+ return `${track.kind}:${track.srclang}:${track.src}`;
113
+ }
114
+ /** No cues, as one shared array: a line without any keeps one identity. */
115
+ const NO_CUES = [];
116
+ /**
117
+ * One caption line's cues: loaded when its track changes, and never another
118
+ * track's. A late answer for a track the line has since left is dropped — a
119
+ * slow Spanish file must never land in a line that now shows Portuguese — and
120
+ * until the new track's cues arrive the line shows nothing rather than the old
121
+ * language, unless they are cached already.
122
+ */
123
+ function useCaptionLine(track, load, peek) {
124
+ const key = track === void 0 ? void 0 : trackKey(track);
125
+ const [line, setLine] = (0, react.useState)({
126
+ cues: NO_CUES,
127
+ failed: false
128
+ });
129
+ const trackRef = (0, react.useRef)(track);
130
+ trackRef.current = track;
131
+ (0, react.useEffect)(() => {
132
+ const wanted = trackRef.current;
133
+ if (key === void 0 || wanted === void 0) return;
134
+ let live = true;
135
+ load(wanted).then((cues) => {
136
+ if (live) setLine({
137
+ key,
138
+ cues,
139
+ failed: false
140
+ });
141
+ }, () => {
142
+ if (live) setLine({
143
+ key,
144
+ cues: NO_CUES,
145
+ failed: true
146
+ });
147
+ });
148
+ return () => {
149
+ live = false;
150
+ };
151
+ }, [key, load]);
152
+ if (key === void 0) return {
153
+ cues: NO_CUES,
154
+ failed: false
155
+ };
156
+ if (line.key === key) return line;
157
+ return {
158
+ cues: peek(key) ?? NO_CUES,
159
+ failed: false
160
+ };
161
+ }
162
+ /**
163
+ * Parsed cues per track, for the life of the mount: switching languages back
164
+ * and forth never refetches. The promise while a file loads — so a swap, which
165
+ * asks for a file both lines want, makes one request — and the cues once it
166
+ * has. A failure is forgotten, so coming back to that language tries again.
167
+ */
168
+ function useCueLoader(captionsLoader) {
169
+ const loaderRef = (0, react.useRef)(captionsLoader);
170
+ loaderRef.current = captionsLoader;
171
+ const loading = (0, react.useRef)(/* @__PURE__ */ new Map());
172
+ const loaded = (0, react.useRef)(/* @__PURE__ */ new Map());
173
+ return {
174
+ loadCues: (0, react.useCallback)((track) => {
175
+ const key = trackKey(track);
176
+ const pending = loading.current.get(key);
177
+ if (pending !== void 0) return pending;
178
+ const request = Promise.resolve().then(() => loaderRef.current ? loaderRef.current(track) : fetch(track.src, { credentials: "same-origin" }).then((response) => {
179
+ if (!response.ok) throw new Error(String(response.status));
180
+ return response.text();
181
+ })).then((text) => {
182
+ const parsed = parseWebVtt(text);
183
+ if (parsed.length === 0) throw new Error("The caption file held no cues.");
184
+ loaded.current.set(key, parsed);
185
+ return parsed;
186
+ });
187
+ loading.current.set(key, request);
188
+ request.catch(() => {
189
+ loading.current.delete(key);
190
+ });
191
+ return request;
192
+ }, []),
193
+ peekCues: (0, react.useCallback)((key) => loaded.current.get(key), [])
194
+ };
195
+ }
196
+ //#endregion
197
+ //#region src/components/InteractiveVideo/format.ts
198
+ /** The speeds a learner can choose, slowest first. */
199
+ const SPEEDS = [
200
+ .5,
201
+ .75,
202
+ 1,
203
+ 1.25,
204
+ 1.5,
205
+ 1.75,
206
+ 2,
207
+ 2.5
208
+ ];
209
+ /**
210
+ * A time as a clock: `4:05`, or `1:02:03` past an hour. Whole seconds,
211
+ * rounded down, so the clock never shows a second that has not happened yet.
212
+ */
213
+ function clock(value) {
214
+ const total = Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
215
+ const hours = Math.floor(total / 3600);
216
+ const minutes = Math.floor(total % 3600 / 60);
217
+ const secs = total % 60;
218
+ const pad = (n) => String(n).padStart(2, "0");
219
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(secs)}` : `${minutes}:${pad(secs)}`;
220
+ }
221
+ /** The next speed in `direction` from `current`, held inside the list. */
222
+ function nudgeSpeed(current, direction) {
223
+ const at = SPEEDS.indexOf(current);
224
+ const from = at === -1 ? SPEEDS.indexOf(1) : at;
225
+ return SPEEDS[Math.min(SPEEDS.length - 1, Math.max(0, from + direction))];
226
+ }
227
+ //#endregion
228
+ //#region src/components/InteractiveVideo/icons.tsx
229
+ /**
230
+ * The player's icons: inline SVG, one stroke weight, drawn in `currentColor`,
231
+ * so a theme recolours them and forced-colours mode repaints them. Inline
232
+ * because the package has no runtime dependencies, and a 24 × 24 grid keeps
233
+ * every icon on one optical size. Always decorative: the button that holds
234
+ * one carries the name.
235
+ */
236
+ function Icon({ children, filled = false }) {
237
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
238
+ className: "lk-iv-icon",
239
+ viewBox: "0 0 24 24",
240
+ width: "20",
241
+ height: "20",
242
+ fill: filled ? "currentColor" : "none",
243
+ stroke: "currentColor",
244
+ strokeWidth: "1.8",
245
+ strokeLinecap: "round",
246
+ strokeLinejoin: "round",
247
+ "aria-hidden": "true",
248
+ focusable: "false",
249
+ children
250
+ });
251
+ }
252
+ const PlayIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
253
+ filled: true,
254
+ children: /* @__PURE__ */ (0, react_jsx_runtime.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" })
255
+ });
256
+ const PauseIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, {
257
+ filled: true,
258
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
259
+ x: "6.5",
260
+ y: "5",
261
+ width: "3.5",
262
+ height: "14",
263
+ rx: "1"
264
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
265
+ x: "14",
266
+ y: "5",
267
+ width: "3.5",
268
+ height: "14",
269
+ rx: "1"
270
+ })]
271
+ });
272
+ const ReplayIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 12a8 8 0 1 0 2.4-5.7" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4v4h4" })] });
273
+ const BackIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M11 6.5 5.5 12l5.5 5.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18.5 6.5 13 12l5.5 5.5" })] });
274
+ const ForwardIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m13 6.5 5.5 5.5-5.5 5.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.5 6.5 11 12l-5.5 5.5" })] });
275
+ const VolumeIcon = ({ level }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 9.5v5h3.5L12 18.5v-13L7.5 9.5H4Z" }), level === "muted" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m16 9.5 5 5m0-5-5 5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15.5 9.5a3.5 3.5 0 0 1 0 5" }), level === "high" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 7a7 7 0 0 1 0 10" }) : null] })] });
276
+ const CaptionsIcon = ({ on }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, { children: [
277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
278
+ x: "3",
279
+ y: "5.5",
280
+ width: "18",
281
+ height: "13",
282
+ rx: "2.5"
283
+ }),
284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 10.5a2 2 0 1 0 0 3M16 10.5a2 2 0 1 0 0 3" }),
285
+ on ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m4 20 16-16" })
286
+ ] });
287
+ const SettingsIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
288
+ cx: "12",
289
+ cy: "12",
290
+ r: "3"
291
+ }), /* @__PURE__ */ (0, react_jsx_runtime.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" })] });
292
+ const ContentsIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M9 6.5h11M9 12h11M9 17.5h11" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.5 6.5h.01M4.5 12h.01M4.5 17.5h.01" })] });
293
+ const PictureInPictureIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
294
+ x: "3",
295
+ y: "5",
296
+ width: "18",
297
+ height: "14",
298
+ rx: "2"
299
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
300
+ x: "12",
301
+ y: "11.5",
302
+ width: "6.5",
303
+ height: "5",
304
+ rx: "1"
305
+ })] });
306
+ const FullscreenIcon = ({ on }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { children: on ? /* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.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" }) });
307
+ const CheckIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m5 12.5 4.5 4.5L19 7.5" }) });
308
+ const ChevronIcon = ({ direction }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: direction === "right" ? "m9.5 6 6 6-6 6" : "m14.5 6-6 6 6 6" }) });
309
+ const CloseIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6.5 6.5 11 11m0-11-11 11" }) });
310
+ const AlertIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Icon, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 4 2.8 19.5h18.4L12 4Z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 10v4M12 17h.01" })] });
311
+ //#endregion
312
+ //#region src/components/InteractiveVideo/keyed-state.ts
313
+ /**
314
+ * A set held in state — what the player draws — beside a ref that always holds
315
+ * its latest value, for the callbacks that read it between renders, and a
316
+ * toggle that keeps one identity for the life of the player. Toggling a key to
317
+ * where it already is changes nothing, so no render follows.
318
+ */
319
+ function useKeySet(initial) {
320
+ const [keys, setKeys] = (0, react.useState)(initial);
321
+ const latest = (0, react.useRef)(keys);
322
+ latest.current = keys;
323
+ return [
324
+ keys,
325
+ latest,
326
+ (0, react.useCallback)((key, on) => {
327
+ setKeys((previous) => {
328
+ if (previous.has(key) === on) return previous;
329
+ const next = new Set(previous);
330
+ if (on) next.add(key);
331
+ else next.delete(key);
332
+ latest.current = next;
333
+ return next;
334
+ });
335
+ }, [])
336
+ ];
337
+ }
338
+ /**
339
+ * A map held in state, with a setter that keeps one identity: `undefined`
340
+ * removes the key, and setting a value it already has changes nothing.
341
+ */
342
+ function useKeyMap() {
343
+ const [entries, setEntries] = (0, react.useState)(() => /* @__PURE__ */ new Map());
344
+ return [entries, (0, react.useCallback)((key, value) => {
345
+ setEntries((previous) => {
346
+ if (previous.get(key) === value) return previous;
347
+ const next = new Map(previous);
348
+ if (value === void 0) next.delete(key);
349
+ else next.set(key, value);
350
+ return next;
351
+ });
352
+ }, [])];
353
+ }
354
+ //#endregion
355
+ //#region src/components/InteractiveVideo/shortcuts.ts
356
+ /** The keys of each row, as shown in the list. */
357
+ const SHORTCUT_KEYS = [
358
+ {
359
+ action: "play-pause",
360
+ keys: "Space / K"
361
+ },
362
+ {
363
+ action: "jump-10",
364
+ keys: "J / L"
365
+ },
366
+ {
367
+ action: "jump-5",
368
+ keys: "← / →"
369
+ },
370
+ {
371
+ action: "jump-1",
372
+ keys: "Shift + ← / →"
373
+ },
374
+ {
375
+ action: "frame",
376
+ keys: ", / ."
377
+ },
378
+ {
379
+ action: "speed",
380
+ keys: "< / >"
381
+ },
382
+ {
383
+ action: "percent",
384
+ keys: "0 – 9"
385
+ },
386
+ {
387
+ action: "start-end",
388
+ keys: "Home / End"
389
+ },
390
+ {
391
+ action: "chapter",
392
+ keys: "[ / ]"
393
+ },
394
+ {
395
+ action: "mute",
396
+ keys: "M"
397
+ },
398
+ {
399
+ action: "captions",
400
+ keys: "C"
401
+ },
402
+ {
403
+ action: "secondary-captions",
404
+ keys: "Shift + C"
405
+ },
406
+ {
407
+ action: "transcript",
408
+ keys: "T"
409
+ },
410
+ {
411
+ action: "picture-in-picture",
412
+ keys: "P"
413
+ },
414
+ {
415
+ action: "fullscreen",
416
+ keys: "F"
417
+ },
418
+ {
419
+ action: "shortcut-list",
420
+ keys: "?"
421
+ }
422
+ ];
423
+ /** Frame step: an approximation, since a video does not report its frame rate. */
424
+ const FRAME_SECONDS = 1 / 30;
425
+ //#endregion
426
+ //#region src/components/InteractiveVideo/tracks.ts
427
+ const primarySubtag = (tag) => (tag.split("-")[0] ?? tag).toLowerCase();
428
+ /** Two tracks in one language: the `captions` one, which transcribes rather than translates. */
429
+ function preferCaptions(tracks) {
430
+ return tracks.find((track) => track.kind === "captions") ?? tracks[0];
431
+ }
432
+ /** The track for `language`: an exact match, else one of the same primary language (`pt` ↔ `pt-BR`). */
433
+ function matchLanguage(tracks, language) {
434
+ const wanted = language.toLowerCase();
435
+ const exact = tracks.filter((track) => track.srclang.toLowerCase() === wanted);
436
+ if (exact.length > 0) return preferCaptions(exact);
437
+ const family = primarySubtag(wanted);
438
+ return preferCaptions(tracks.filter((track) => primarySubtag(track.srclang) === family));
439
+ }
440
+ /** Tracks a player draws: the caption and subtitle kinds. */
441
+ function drawable(tracks) {
442
+ return tracks.filter((track) => track.kind === "captions" || track.kind === "subtitles");
443
+ }
444
+ /**
445
+ * The tracks that can be a second line beside `primary`: every drawable track
446
+ * in a different language from it. A second track in the primary's own
447
+ * language — the `subtitles` beside its `captions` — would only print the same
448
+ * words twice, so it is never offered and never chosen.
449
+ */
450
+ function secondaryCandidates(tracks, primary) {
451
+ const taken = primary?.srclang.toLowerCase();
452
+ return drawable(tracks).filter((track) => track.srclang.toLowerCase() !== taken);
453
+ }
454
+ /**
455
+ * Which track each caption line shows, from the learner's two choices.
456
+ *
457
+ * The primary line always shows something while there are tracks:
458
+ * 1. the track in `captionLanguage` (case-insensitive);
459
+ * 2. else one of the same primary language — `pt` finds `pt-BR`, `es` finds
460
+ * `es-419`;
461
+ * 3. else the `default` track;
462
+ * 4. else the first.
463
+ *
464
+ * Where two tracks share a language, the `captions` one wins.
465
+ *
466
+ * The secondary line is stricter: it appears only when `secondaryCaptionLanguage`
467
+ * is set and matches a track in another language, by steps 1 and 2 alone —
468
+ * never the default, never the first. A second line the learner did not choose
469
+ * must never appear.
470
+ */
471
+ function resolveCaptionTracks(tracks, preferences) {
472
+ const usable = drawable(tracks);
473
+ if (usable.length === 0) return {};
474
+ const primary = (preferences.captionLanguage === null ? void 0 : matchLanguage(usable, preferences.captionLanguage)) ?? usable.find((track) => track.default === true) ?? usable[0];
475
+ const second = preferences.secondaryCaptionLanguage;
476
+ const secondary = second === null ? void 0 : matchLanguage(secondaryCandidates(usable, primary), second);
477
+ return secondary === void 0 ? { primary } : {
478
+ primary,
479
+ secondary
480
+ };
481
+ }
482
+ /** How long two cues share, in seconds; zero or less when they do not meet. */
483
+ const overlap = (a, b) => Math.min(a.end, b.end) - Math.max(a.start, b.start);
484
+ /**
485
+ * How far back past the last cue that starts before a second-language cue to
486
+ * look for one still running. Captions overlap rarely and briefly; this is the
487
+ * bound `cueIndexAt` uses for the same reason.
488
+ */
489
+ const LOOKBACK = 8;
490
+ /**
491
+ * The second-language cues that belong under each first-language line of the
492
+ * transcript, by index of the first-language cue.
493
+ *
494
+ * A second-language cue belongs to the line it overlaps MOST — the two tracks
495
+ * need not share timings, and a translation segmented differently still pairs
496
+ * sensibly — and to the earlier line on a tie. A cue that overlaps no line is
497
+ * left out: it is still drawn over the video at its own time, but a transcript
498
+ * row is a first-language line, and there is none for it to sit under.
499
+ *
500
+ * Both lists are sorted by start, as `parseWebVtt` returns them.
501
+ */
502
+ function pairSecondaryCues(primary, secondary) {
503
+ const rows = primary.map(() => []);
504
+ for (const cue of secondary) {
505
+ let low = 0;
506
+ let high = primary.length;
507
+ while (low < high) {
508
+ const middle = low + high >> 1;
509
+ if (primary[middle].start < cue.end) low = middle + 1;
510
+ else high = middle;
511
+ }
512
+ let best = -1;
513
+ let bestShare = 0;
514
+ let misses = 0;
515
+ for (let index = low - 1; index >= 0 && misses <= LOOKBACK; index -= 1) {
516
+ const share = overlap(primary[index], cue);
517
+ if (share > 0) {
518
+ misses = 0;
519
+ if (share >= bestShare) {
520
+ best = index;
521
+ bestShare = share;
522
+ }
523
+ } else misses += 1;
524
+ }
525
+ if (best !== -1) rows[best].push(cue);
526
+ }
527
+ return rows;
528
+ }
529
+ //#endregion
530
+ //#region src/components/InteractiveVideo/panels.tsx
531
+ /**
532
+ * A menu of items, with the arrow keys moving between them and Escape closing
533
+ * it — focus then goes back to the button that opened it. When the menu opens
534
+ * focus goes to the row a sub-page was left for, when coming back from one;
535
+ * else the checked item; else the first.
536
+ */
537
+ function Menu({ label, onClose, children, returnTo }) {
538
+ const ref = (0, react.useRef)(null);
539
+ (0, react.useEffect)(() => {
540
+ const items = ref.current?.querySelectorAll("[role^=\"menuitem\"]");
541
+ const back = returnTo === void 0 ? null : ref.current?.querySelector(`[data-menu-key="${returnTo}"]`);
542
+ const checked = ref.current?.querySelector("[aria-checked=\"true\"]");
543
+ (back ?? checked ?? items?.[0])?.focus();
544
+ }, [returnTo]);
545
+ const onKeyDown = (event) => {
546
+ const items = Array.from(ref.current?.querySelectorAll("[role^=\"menuitem\"]") ?? []);
547
+ const at = items.indexOf(document.activeElement);
548
+ const move = (to) => {
549
+ event.preventDefault();
550
+ items[(to + items.length) % items.length]?.focus();
551
+ };
552
+ if (event.key === "ArrowDown") move(at + 1);
553
+ else if (event.key === "ArrowUp") move(at - 1);
554
+ else if (event.key === "Home") move(0);
555
+ else if (event.key === "End") move(items.length - 1);
556
+ else if (event.key === "Escape") {
557
+ event.preventDefault();
558
+ event.stopPropagation();
559
+ onClose();
560
+ }
561
+ event.stopPropagation();
562
+ };
563
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
564
+ ref,
565
+ className: "lk-iv-menu",
566
+ role: "menu",
567
+ "aria-label": label,
568
+ onKeyDown,
569
+ children
570
+ });
571
+ }
572
+ function SpeedMenu({ speed, strings, onChoose, onClose }) {
573
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Menu, {
574
+ label: strings.media.speed,
575
+ onClose,
576
+ children: SPEEDS.map((rate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
577
+ type: "button",
578
+ role: "menuitemradio",
579
+ "aria-checked": rate === speed,
580
+ className: "lk-iv-menu-item",
581
+ onClick: () => onChoose(rate),
582
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoSpeedValue(rate) }), rate === speed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, {}) : null]
583
+ }, rate))
584
+ });
585
+ }
586
+ function SettingsMenu({ preferences, tracks, primaryTrack, secondaryTrack, captionsFailed, strings, onChange, onCaptionTrack, onSecondaryTrack, onShowShortcuts, onClose }) {
587
+ const [page, setPage] = (0, react.useState)("main");
588
+ const [returnTo, setReturnTo] = (0, react.useState)(void 0);
589
+ const open = (next) => {
590
+ setReturnTo(void 0);
591
+ setPage(next);
592
+ };
593
+ const leave = () => {
594
+ setReturnTo(page);
595
+ setPage(page === "second" ? "captions" : "main");
596
+ };
597
+ const captionsValue = !preferences.captions || primaryTrack === void 0 ? strings.videoOff : secondaryTrack === void 0 ? primaryTrack.label : strings.videoCaptionPair(primaryTrack.label, secondaryTrack.label);
598
+ const partners = secondaryCandidates(tracks, primaryTrack);
599
+ const failedNotes = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [captionsFailed.primary && page === "captions" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
600
+ className: "lk-iv-menu-note",
601
+ role: "status",
602
+ children: strings.videoCaptionsFailed
603
+ }) : null, captionsFailed.secondary ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
604
+ className: "lk-iv-menu-note",
605
+ role: "status",
606
+ children: strings.videoSecondaryCaptionsFailed
607
+ }) : null] });
608
+ const back = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
609
+ type: "button",
610
+ role: "menuitem",
611
+ className: "lk-iv-menu-item lk-iv-menu-back",
612
+ onClick: leave,
613
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronIcon, { direction: "left" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: page === "captions" ? strings.videoCaptionLanguage : page === "second" ? strings.videoSecondCaptionLanguage : strings.videoCaptionSize })]
614
+ });
615
+ /** A radio naming a track in its own language and writing direction. */
616
+ const trackRadio = (track, on, choose) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
617
+ type: "button",
618
+ role: "menuitemradio",
619
+ "aria-checked": on,
620
+ className: "lk-iv-menu-item",
621
+ lang: track.srclang,
622
+ onClick: choose,
623
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
624
+ dir: "auto",
625
+ children: track.label
626
+ }), on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, {}) : null]
627
+ }, `${track.kind}:${track.srclang}`);
628
+ if (page === "captions") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Menu, {
629
+ label: strings.videoCaptionLanguage,
630
+ onClose,
631
+ returnTo,
632
+ children: [
633
+ back,
634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
635
+ type: "button",
636
+ role: "menuitemradio",
637
+ "aria-checked": !preferences.captions,
638
+ className: "lk-iv-menu-item",
639
+ onClick: () => onChange({ captions: false }),
640
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoOff }), !preferences.captions ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, {}) : null]
641
+ }),
642
+ tracks.map((track) => trackRadio(track, preferences.captions && primaryTrack === track, () => onCaptionTrack(track))),
643
+ partners.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
644
+ type: "button",
645
+ role: "menuitem",
646
+ className: "lk-iv-menu-item",
647
+ "data-menu-key": "second",
648
+ onClick: () => open("second"),
649
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoSecondCaptionLanguage }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
650
+ className: "lk-iv-menu-value",
651
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
652
+ dir: "auto",
653
+ lang: secondaryTrack?.srclang,
654
+ children: secondaryTrack === void 0 ? strings.videoOff : secondaryTrack.label
655
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronIcon, { direction: "right" })]
656
+ })]
657
+ }) : null,
658
+ failedNotes
659
+ ]
660
+ }, "captions");
661
+ if (page === "second") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Menu, {
662
+ label: strings.videoSecondCaptionLanguage,
663
+ onClose,
664
+ children: [
665
+ back,
666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
667
+ type: "button",
668
+ role: "menuitemradio",
669
+ "aria-checked": secondaryTrack === void 0,
670
+ className: "lk-iv-menu-item",
671
+ onClick: () => onSecondaryTrack(null),
672
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoOff }), secondaryTrack === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, {}) : null]
673
+ }),
674
+ partners.map((track) => trackRadio(track, secondaryTrack === track, () => onSecondaryTrack(track))),
675
+ failedNotes
676
+ ]
677
+ }, "second");
678
+ if (page === "size") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Menu, {
679
+ label: strings.videoCaptionSize,
680
+ onClose,
681
+ children: [back, [
682
+ "small",
683
+ "medium",
684
+ "large"
685
+ ].map((size) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
686
+ type: "button",
687
+ role: "menuitemradio",
688
+ "aria-checked": preferences.captionSize === size,
689
+ className: "lk-iv-menu-item",
690
+ onClick: () => onChange({ captionSize: size }),
691
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoCaptionSizeValue(size) }), preferences.captionSize === size ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, {}) : null]
692
+ }, size))]
693
+ }, "size");
694
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Menu, {
695
+ label: strings.videoSettings,
696
+ onClose,
697
+ returnTo,
698
+ children: [
699
+ tracks.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
700
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
701
+ type: "button",
702
+ role: "menuitem",
703
+ className: "lk-iv-menu-item",
704
+ "data-menu-key": "captions",
705
+ onClick: () => open("captions"),
706
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoCaptionLanguage }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
707
+ className: "lk-iv-menu-value",
708
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
709
+ dir: "auto",
710
+ children: captionsValue
711
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronIcon, { direction: "right" })]
712
+ })]
713
+ }),
714
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
715
+ type: "button",
716
+ role: "menuitem",
717
+ className: "lk-iv-menu-item",
718
+ "data-menu-key": "size",
719
+ onClick: () => open("size"),
720
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoCaptionSize }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
721
+ className: "lk-iv-menu-value",
722
+ children: [strings.videoCaptionSizeValue(preferences.captionSize), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronIcon, { direction: "right" })]
723
+ })]
724
+ }),
725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
726
+ type: "button",
727
+ role: "menuitemcheckbox",
728
+ "aria-checked": preferences.captionBackground,
729
+ className: "lk-iv-menu-item",
730
+ onClick: () => onChange({ captionBackground: !preferences.captionBackground }),
731
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoCaptionBackground }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
732
+ className: "lk-iv-menu-value",
733
+ children: preferences.captionBackground ? strings.videoOn : strings.videoOff
734
+ })]
735
+ })
736
+ ] }) : null,
737
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
738
+ type: "button",
739
+ role: "menuitemcheckbox",
740
+ "aria-checked": preferences.shortcuts,
741
+ className: "lk-iv-menu-item",
742
+ onClick: () => onChange({ shortcuts: !preferences.shortcuts }),
743
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoKeyboardShortcuts }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
744
+ className: "lk-iv-menu-value",
745
+ children: preferences.shortcuts ? strings.videoOn : strings.videoOff
746
+ })]
747
+ }),
748
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
749
+ type: "button",
750
+ role: "menuitem",
751
+ className: "lk-iv-menu-item",
752
+ onClick: onShowShortcuts,
753
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: strings.videoShortcutList }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("kbd", {
754
+ className: "lk-iv-kbd",
755
+ children: "?"
756
+ })]
757
+ })
758
+ ]
759
+ }, "main");
760
+ }
761
+ /** The shortcut list, over the video. The same table the key handler reads. */
762
+ function ShortcutSheet({ strings, onClose }) {
763
+ const closeRef = (0, react.useRef)(null);
764
+ (0, react.useEffect)(() => {
765
+ closeRef.current?.focus();
766
+ }, []);
767
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
768
+ className: "lk-iv-sheet",
769
+ role: "dialog",
770
+ "aria-modal": "false",
771
+ "aria-label": strings.videoKeyboardShortcuts,
772
+ onKeyDown: (event) => {
773
+ if (event.key === "Escape" || event.key === "?") {
774
+ event.preventDefault();
775
+ event.stopPropagation();
776
+ onClose();
777
+ }
778
+ },
779
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
780
+ className: "lk-iv-sheet-head",
781
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
782
+ className: "lk-iv-sheet-title",
783
+ children: strings.videoKeyboardShortcuts
784
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
785
+ ref: closeRef,
786
+ type: "button",
787
+ className: "lk-iv-button",
788
+ "aria-label": strings.videoClose,
789
+ onClick: onClose,
790
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, {})
791
+ })]
792
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
793
+ className: "lk-iv-shortcuts",
794
+ children: SHORTCUT_KEYS.map(({ action, keys }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
795
+ className: "lk-iv-shortcut",
796
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("kbd", {
797
+ className: "lk-iv-kbd",
798
+ children: keys
799
+ }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: strings.videoShortcut(action) })]
800
+ }, action))
801
+ })]
802
+ });
803
+ }
804
+ /**
805
+ * The panel below the video: the chapters and quizzes in time order, and the
806
+ * transcript. Nothing here costs a request — the transcript is the captions,
807
+ * already parsed.
808
+ */
809
+ function ContentsPanel({ idPrefix, tab, onTab, chapters, quizzes, cues, primaryLanguage, secondaryCues, secondaryLanguage, activeCue, limit, strings, onSeek, onOpenQuiz }) {
810
+ const [query, setQuery] = (0, react.useState)("");
811
+ const listRef = (0, react.useRef)(null);
812
+ const followUntil = (0, react.useRef)(0);
813
+ const hasTranscript = cues.length > 0;
814
+ const shownTab = hasTranscript ? tab : "contents";
815
+ const paired = (0, react.useMemo)(() => pairSecondaryCues(cues, secondaryCues), [cues, secondaryCues]);
816
+ const lines = (0, react.useMemo)(() => {
817
+ const trimmed = query.trim().toLowerCase();
818
+ return cues.map((cue, index) => ({
819
+ cue,
820
+ index,
821
+ second: (paired[index] ?? []).filter((line) => line.start <= limit).map((line) => line.text).join(" ")
822
+ })).filter(({ cue }) => cue.start <= limit).filter(({ cue, second }) => trimmed === "" || cue.text.toLowerCase().includes(trimmed) || second.toLowerCase().includes(trimmed));
823
+ }, [
824
+ cues,
825
+ paired,
826
+ limit,
827
+ query
828
+ ]);
829
+ (0, react.useEffect)(() => {
830
+ if (shownTab !== "transcript" || query.trim() !== "" || Date.now() < followUntil.current) return;
831
+ const active = listRef.current?.querySelector("[data-active=\"true\"]");
832
+ if (active !== null && active !== void 0 && listRef.current !== null) {
833
+ const list = listRef.current;
834
+ const top = active.offsetTop - list.clientHeight / 3;
835
+ list.scrollTo?.({
836
+ top,
837
+ behavior: "smooth"
838
+ });
839
+ }
840
+ }, [
841
+ activeCue,
842
+ shownTab,
843
+ query
844
+ ]);
845
+ const rows = [...chapters.map((chapter) => ({
846
+ kind: "chapter",
847
+ at: chapter.at,
848
+ chapter
849
+ })), ...quizzes.map((quiz) => ({
850
+ kind: "quiz",
851
+ at: quiz.at,
852
+ quiz
853
+ }))].sort((a, b) => a.at - b.at || (a.kind === "chapter" ? -1 : 1));
854
+ const highlight = (text) => {
855
+ const trimmed = query.trim();
856
+ const at = trimmed === "" ? -1 : text.toLowerCase().indexOf(trimmed.toLowerCase());
857
+ if (at === -1) return text;
858
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
859
+ text.slice(0, at),
860
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("mark", { children: text.slice(at, at + trimmed.length) }),
861
+ text.slice(at + trimmed.length)
862
+ ] });
863
+ };
864
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
865
+ className: "lk-iv-panel",
866
+ "aria-label": strings.videoPanel,
867
+ children: [hasTranscript ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
868
+ className: "lk-iv-tabs",
869
+ role: "tablist",
870
+ "aria-label": strings.videoPanel,
871
+ children: ["contents", "transcript"].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
872
+ type: "button",
873
+ role: "tab",
874
+ id: `${idPrefix}-tab-${name}`,
875
+ "aria-controls": `${idPrefix}-tabpanel`,
876
+ "aria-selected": shownTab === name,
877
+ tabIndex: shownTab === name ? 0 : -1,
878
+ className: "lk-iv-tab",
879
+ onClick: () => onTab(name),
880
+ onKeyDown: (event) => {
881
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
882
+ event.preventDefault();
883
+ onTab(name === "contents" ? "transcript" : "contents");
884
+ }
885
+ },
886
+ children: name === "contents" ? strings.videoContents : strings.videoTranscript
887
+ }, name))
888
+ }) : null, shownTab === "contents" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
889
+ className: "lk-iv-contents",
890
+ ...hasTranscript ? {
891
+ role: "tabpanel",
892
+ id: `${idPrefix}-tabpanel`,
893
+ "aria-labelledby": `${idPrefix}-tab-contents`
894
+ } : { "aria-label": strings.videoContents },
895
+ children: rows.map((row) => row.kind === "chapter" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
896
+ className: "lk-iv-contents-chapter",
897
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
898
+ type: "button",
899
+ className: "lk-iv-row",
900
+ onClick: () => onSeek(row.chapter.at),
901
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
902
+ className: "lk-iv-row-time",
903
+ children: clock(row.chapter.at)
904
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
905
+ className: "lk-iv-row-title",
906
+ children: row.chapter.title
907
+ })]
908
+ })
909
+ }, `c-${row.at}`) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
910
+ className: "lk-iv-contents-quiz",
911
+ "data-state": row.quiz.state,
912
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
913
+ type: "button",
914
+ className: "lk-iv-row",
915
+ onClick: () => onOpenQuiz(row.quiz.id),
916
+ children: [
917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
918
+ className: "lk-iv-row-time",
919
+ children: clock(row.quiz.at)
920
+ }),
921
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
922
+ className: "lk-iv-row-title",
923
+ children: [
924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
925
+ className: "lk-iv-marker-dot",
926
+ "data-state": row.quiz.state,
927
+ "aria-hidden": "true"
928
+ }),
929
+ row.quiz.title,
930
+ row.quiz.required ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
931
+ className: "lk-iv-tag",
932
+ children: strings.videoRequired
933
+ })] }) : null
934
+ ]
935
+ }),
936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
937
+ className: "lk-iv-row-status",
938
+ children: strings.videoQuizProgress(row.quiz.answered, row.quiz.total)
939
+ })
940
+ ]
941
+ })
942
+ }, `q-${row.quiz.id}`))
943
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
944
+ className: "lk-iv-transcript",
945
+ role: "tabpanel",
946
+ id: `${idPrefix}-tabpanel`,
947
+ "aria-labelledby": `${idPrefix}-tab-transcript`,
948
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
949
+ id: `${idPrefix}-search`,
950
+ className: "lk-iv-search",
951
+ type: "search",
952
+ value: query,
953
+ placeholder: strings.videoTranscriptSearch,
954
+ "aria-label": strings.videoTranscriptSearch,
955
+ onChange: (event) => setQuery(event.target.value)
956
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
957
+ ref: listRef,
958
+ className: "lk-iv-lines",
959
+ onWheel: () => {
960
+ followUntil.current = Date.now() + 4e3;
961
+ },
962
+ onTouchMove: () => {
963
+ followUntil.current = Date.now() + 4e3;
964
+ },
965
+ children: lines.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
966
+ className: "lk-iv-empty",
967
+ children: strings.videoTranscriptNoMatch
968
+ }) : lines.map(({ cue, index, second }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
969
+ type: "button",
970
+ className: "lk-iv-line",
971
+ "data-active": index === activeCue || void 0,
972
+ onClick: () => onSeek(cue.start),
973
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
974
+ className: "lk-iv-row-time",
975
+ children: clock(cue.start)
976
+ }), secondaryCues.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
977
+ lang: primaryLanguage,
978
+ dir: "auto",
979
+ children: highlight(cue.text)
980
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
981
+ className: "lk-iv-line-text",
982
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
983
+ lang: primaryLanguage,
984
+ dir: "auto",
985
+ children: highlight(cue.text)
986
+ }), second === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
987
+ className: "lk-iv-line-secondary",
988
+ lang: secondaryLanguage,
989
+ dir: "auto",
990
+ children: highlight(second)
991
+ })]
992
+ })]
993
+ }, `${cue.start}-${index}`))
994
+ })]
995
+ })]
996
+ });
997
+ }
998
+ /** Over the last frame: what the learner did, and where to go from here. */
999
+ function EndScreen({ idPrefix, renderMode, quizzes, answered, total, percent, strings, onOpenQuiz, onWatchAgain, pending, onFinish }) {
1000
+ const pendingId = `${idPrefix}-end-pending`;
1001
+ const headingRef = (0, react.useRef)(null);
1002
+ (0, react.useEffect)(() => {
1003
+ headingRef.current?.focus();
1004
+ }, []);
1005
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
1006
+ className: "lk-iv-end",
1007
+ "aria-labelledby": `${idPrefix}-end-title`,
1008
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1009
+ className: "lk-iv-end-card",
1010
+ children: [
1011
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1012
+ id: `${idPrefix}-end-title`,
1013
+ ref: headingRef,
1014
+ tabIndex: -1,
1015
+ className: "lk-iv-end-title",
1016
+ children: strings.videoEnded
1017
+ }),
1018
+ total > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1019
+ className: "lk-iv-end-summary",
1020
+ children: percent !== null && renderMode !== "exam" ? strings.videoEndScore(answered, total, percent) : strings.videoEndAnswered(answered, total)
1021
+ }) : null,
1022
+ quizzes.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1023
+ className: "lk-iv-end-list",
1024
+ children: quizzes.map((quiz) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1025
+ type: "button",
1026
+ className: "lk-iv-row",
1027
+ "data-state": quiz.state,
1028
+ onClick: () => onOpenQuiz(quiz.id),
1029
+ children: [
1030
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1031
+ className: "lk-iv-row-time",
1032
+ children: clock(quiz.at)
1033
+ }),
1034
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1035
+ className: "lk-iv-row-title",
1036
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1037
+ className: "lk-iv-marker-dot",
1038
+ "data-state": quiz.state,
1039
+ "aria-hidden": "true"
1040
+ }), quiz.title]
1041
+ }),
1042
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1043
+ className: "lk-iv-row-status",
1044
+ children: strings.videoQuizProgress(quiz.answered, quiz.total)
1045
+ })
1046
+ ]
1047
+ }) }, quiz.id))
1048
+ }) : null,
1049
+ onFinish !== void 0 && pending ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1050
+ id: pendingId,
1051
+ className: "lk-iv-end-pending",
1052
+ role: "status",
1053
+ children: strings.videoAnswerPending
1054
+ }) : null,
1055
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1056
+ className: "lk-iv-end-actions",
1057
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1058
+ type: "button",
1059
+ className: "lk-iv-action",
1060
+ onClick: onWatchAgain,
1061
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReplayIcon, {}), strings.videoWatchAgain]
1062
+ }), onFinish !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1063
+ type: "button",
1064
+ className: "lk-iv-action lk-iv-action-primary",
1065
+ "aria-disabled": pending || void 0,
1066
+ "aria-describedby": pending ? pendingId : void 0,
1067
+ onClick: onFinish,
1068
+ children: strings.videoFinish
1069
+ }) : null]
1070
+ })
1071
+ ]
1072
+ })
1073
+ });
1074
+ }
1075
+ //#endregion
1076
+ //#region src/components/InteractiveVideo/prefs.ts
1077
+ const DEFAULT_PREFERENCES = {
1078
+ speed: 1,
1079
+ volume: 1,
1080
+ muted: false,
1081
+ captions: true,
1082
+ captionLanguage: null,
1083
+ secondaryCaptionLanguage: null,
1084
+ captionSize: "medium",
1085
+ captionBackground: true,
1086
+ panel: false,
1087
+ shortcuts: true
1088
+ };
1089
+ const STORAGE_KEY = "lk.video.v1";
1090
+ /** A BCP 47 tag as a track's `srclang` carries one: `en`, `pt-BR`, `es-419`. */
1091
+ const LANGUAGE_TAG = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
1092
+ const BOOLEAN_FIELDS = [
1093
+ "muted",
1094
+ "captions",
1095
+ "captionBackground",
1096
+ "panel",
1097
+ "shortcuts"
1098
+ ];
1099
+ const LANGUAGE_FIELDS = ["captionLanguage", "secondaryCaptionLanguage"];
1100
+ /**
1101
+ * Every field of `raw` that is present and usable, clamped — and nothing at
1102
+ * all for a field that is absent or unusable, so a layer below it applies.
1103
+ *
1104
+ * Anything in storage is writable by the learner and survives a schema
1105
+ * change, so it is input from a stranger: one `volume: 50` must not mute every
1106
+ * video, and a `speed: null` must not pin playback at a rate the menu cannot
1107
+ * undo. A host's layers go through here too — a `speed: 3` is no more a rate
1108
+ * the menu can undo for having come from code.
1109
+ *
1110
+ * `null` is a value for the two language fields (the default track; no second
1111
+ * line), so it counts as present; an unparseable tag does not.
1112
+ */
1113
+ function sanitizePartialPreferences(raw) {
1114
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
1115
+ const value = raw;
1116
+ const has = (key) => Object.hasOwn(value, key);
1117
+ const out = {};
1118
+ if (has("speed") && SPEEDS.includes(value.speed)) out.speed = value.speed;
1119
+ if (has("volume") && typeof value.volume === "number" && Number.isFinite(value.volume)) out.volume = Math.min(1, Math.max(0, value.volume));
1120
+ for (const field of BOOLEAN_FIELDS) if (has(field) && typeof value[field] === "boolean") out[field] = value[field];
1121
+ for (const field of LANGUAGE_FIELDS) {
1122
+ const tag = value[field];
1123
+ if (has(field) && (tag === null || typeof tag === "string" && LANGUAGE_TAG.test(tag))) out[field] = tag;
1124
+ }
1125
+ if (has("captionSize") && (value.captionSize === "small" || value.captionSize === "medium" || value.captionSize === "large")) out.captionSize = value.captionSize;
1126
+ return out;
1127
+ }
1128
+ /** A pair of one language is no pair: the second line is dropped, case-insensitively. */
1129
+ function normalised(preferences) {
1130
+ const { captionLanguage: first, secondaryCaptionLanguage: second } = preferences;
1131
+ return first !== null && second !== null && first.toLowerCase() === second.toLowerCase() ? {
1132
+ ...preferences,
1133
+ secondaryCaptionLanguage: null
1134
+ } : preferences;
1135
+ }
1136
+ /**
1137
+ * The preferences in force, decided field by field, highest first:
1138
+ *
1139
+ * 1. `force` — the host's `preferences` prop, which overrides what the
1140
+ * learner chose, on every mount;
1141
+ * 2. `stored` — what the learner chose in this browser;
1142
+ * 3. `defaults` — the host's `defaultPreferences` prop: a starting point, such
1143
+ * as a language pair kept on the learner's account;
1144
+ * 4. the SDK's own defaults.
1145
+ *
1146
+ * Deciding per field is the point: a learner who has only ever changed the
1147
+ * volume has chosen no caption language, so the host's suggested pair applies
1148
+ * to them — where a whole stored object would have beaten it.
1149
+ */
1150
+ function resolvePreferences(layers) {
1151
+ return normalised({
1152
+ ...DEFAULT_PREFERENCES,
1153
+ ...sanitizePartialPreferences(layers.defaults),
1154
+ ...sanitizePartialPreferences(layers.stored),
1155
+ ...sanitizePartialPreferences(layers.force),
1156
+ ...sanitizePartialPreferences(layers.chosen)
1157
+ });
1158
+ }
1159
+ /** `previous` with a learner's change applied, clamped and normalised like everything else. */
1160
+ function applyPreferenceChange(previous, change) {
1161
+ return normalised({
1162
+ ...previous,
1163
+ ...sanitizePartialPreferences(change)
1164
+ });
1165
+ }
1166
+ /**
1167
+ * What the learner chose in this browser: only the fields present in storage.
1168
+ * Every read goes through a `try`: a private window, blocked site data or a
1169
+ * sandboxed frame throws on `localStorage` itself.
1170
+ */
1171
+ function readStoredPreferences() {
1172
+ try {
1173
+ const stored = globalThis.localStorage?.getItem(STORAGE_KEY);
1174
+ return stored ? sanitizePartialPreferences(JSON.parse(stored)) : {};
1175
+ } catch {
1176
+ return {};
1177
+ }
1178
+ }
1179
+ /**
1180
+ * Remembers a learner's change: the fields they changed, merged into what they
1181
+ * chose before. Only what the learner chose is stored — never a value the host
1182
+ * forced or suggested — so a host that later suggests a different pair still
1183
+ * reaches a learner who never picked one. A payload written by 15.0, which
1184
+ * stored every field, reads as every field chosen: the behaviour it had.
1185
+ *
1186
+ * A browser that will not store them loses nothing but the memory.
1187
+ */
1188
+ function rememberPreferences(change) {
1189
+ try {
1190
+ const next = {
1191
+ ...readStoredPreferences(),
1192
+ ...sanitizePartialPreferences(change)
1193
+ };
1194
+ globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
1195
+ } catch {}
1196
+ }
1197
+ //#endregion
1198
+ //#region src/components/InteractiveVideo/quiz-engine.ts
1199
+ /**
1200
+ * The quiz playback has just reached, or undefined: the EARLIEST quiz with
1201
+ * `previous < at ≤ now` that has not been handled in this pass.
1202
+ *
1203
+ * Crossing, never equality: at 2.5× speed about 0.6 s of video passes between
1204
+ * two `timeupdate` events, so a quiz checked for `now === at` is missed. And
1205
+ * the earliest wins, so when a janky frame carries playback past two quizzes
1206
+ * at once, the player opens the first, stops there, and meets the second as
1207
+ * playback continues — no quiz is ever skipped by speed.
1208
+ */
1209
+ function crossedQuiz(cues, previous, now, handled) {
1210
+ let earliest;
1211
+ for (const cue of cues) {
1212
+ if (handled.has(cue.id) || !(previous < cue.at && cue.at <= now)) continue;
1213
+ if (earliest === void 0 || cue.at < earliest.at) earliest = cue;
1214
+ }
1215
+ return earliest;
1216
+ }
1217
+ /** The quizzes that open when the video ends: every one at or past its duration. */
1218
+ function quizzesAtEnd(cues, duration, handled) {
1219
+ return cues.filter((cue) => !handled.has(cue.id) && cue.at >= duration - .05).sort((a, b) => a.at - b.at);
1220
+ }
1221
+ /**
1222
+ * Where a seek from `from` to `to` may actually land.
1223
+ *
1224
+ * Backward is always free. Forward stops at the first `required` quiz whose
1225
+ * questions are not all answered — landing ON it, so it opens — and, under
1226
+ * `no-skip-ahead`, at the furthest point the learner has reached. The earlier
1227
+ * of the two limits wins, and the answer says which, so the player can tell
1228
+ * the learner why the seek stopped short.
1229
+ */
1230
+ function limitSeek(timeline, from, to, furthest, finished) {
1231
+ if (to <= from) return { at: to };
1232
+ let at = to;
1233
+ let hold;
1234
+ if (timeline.navigation === "no-skip-ahead" && to > furthest) {
1235
+ at = Math.max(from, furthest);
1236
+ hold = {
1237
+ kind: "no-skip-ahead",
1238
+ furthest
1239
+ };
1240
+ }
1241
+ 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];
1242
+ if (blocking !== void 0 && blocking.at <= at) return {
1243
+ at: blocking.at,
1244
+ hold: {
1245
+ kind: "required",
1246
+ cue: blocking
1247
+ }
1248
+ };
1249
+ return hold === void 0 ? { at } : {
1250
+ at,
1251
+ hold
1252
+ };
1253
+ }
1254
+ /**
1255
+ * The earliest point a resumed learner may start from: never past the first
1256
+ * `required` quiz with a question still unanswered, whatever the stored
1257
+ * position says — a resume is never a way past a required quiz.
1258
+ */
1259
+ function limitResume(timeline, at, finished) {
1260
+ const blocking = timeline.cues.filter((cue) => cue.required === true && cue.at < at && !finished(cue.id)).sort((a, b) => a.at - b.at)[0];
1261
+ return blocking === void 0 ? at : Math.max(0, blocking.at - .25);
1262
+ }
1263
+ //#endregion
1264
+ //#region src/components/InteractiveVideo/Scrubber.tsx
1265
+ /** Half the widest the bubble may grow, in px: keeps it inside the track at the ends. */
1266
+ const BUBBLE_HALF = 120;
1267
+ /** How near the pointer must be to a marker, in px, for the bubble to name its quiz. */
1268
+ const MARKER_REACH = 8;
1269
+ /**
1270
+ * The progress bar.
1271
+ *
1272
+ * A thin line at rest and a thicker one under the pointer — the line, not the
1273
+ * control, is what should be visible — over a generous hit strip, so a thin bar
1274
+ * is still easy to grab. Pointer events with capture, so a drag that leaves the
1275
+ * bar keeps scrubbing. It is one slider for assistive technology; its markers
1276
+ * are drawn, not focusable, and a keyboard learner reaches quizzes through
1277
+ * `[` / `]` and the contents panel instead.
1278
+ */
1279
+ function Scrubber({ current, duration, buffered, chapters, markers, cues, previewLimit, strings, onSeek, onScrubChange }) {
1280
+ const trackRef = (0, react.useRef)(null);
1281
+ const [hover, setHover] = (0, react.useState)(null);
1282
+ const [dragging, setDragging] = (0, react.useState)(false);
1283
+ const ready = duration > 0;
1284
+ const percent = (value) => `${ready ? Math.min(100, Math.max(0, value / duration * 100)) : 0}%`;
1285
+ const measure = (0, react.useCallback)((clientX) => {
1286
+ const rect = trackRef.current?.getBoundingClientRect();
1287
+ if (rect === void 0 || rect.width === 0 || !ready) return null;
1288
+ const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
1289
+ return {
1290
+ x: ratio * rect.width,
1291
+ width: rect.width,
1292
+ time: ratio * duration
1293
+ };
1294
+ }, [duration, ready]);
1295
+ const onPointerDown = (event) => {
1296
+ const at = measure(event.clientX);
1297
+ if (at === null) return;
1298
+ event.currentTarget.setPointerCapture(event.pointerId);
1299
+ setDragging(true);
1300
+ onScrubChange(true);
1301
+ setHover(at);
1302
+ onSeek(at.time);
1303
+ };
1304
+ const onPointerMove = (event) => {
1305
+ const at = measure(event.clientX);
1306
+ if (at === null) return;
1307
+ setHover(at);
1308
+ if (dragging) onSeek(at.time);
1309
+ };
1310
+ const endDrag = (event) => {
1311
+ if (!dragging) return;
1312
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
1313
+ setDragging(false);
1314
+ onScrubChange(false);
1315
+ };
1316
+ const onKeyDown = (event) => {
1317
+ if (!ready) return;
1318
+ const step = event.shiftKey ? 1 : 5;
1319
+ const move = {
1320
+ ArrowLeft: -step,
1321
+ ArrowRight: step,
1322
+ ArrowDown: -step,
1323
+ ArrowUp: step,
1324
+ PageDown: -30,
1325
+ PageUp: 30
1326
+ }[event.key];
1327
+ if (move !== void 0) {
1328
+ event.preventDefault();
1329
+ event.stopPropagation();
1330
+ onSeek(current + move);
1331
+ } else if (event.key === "Home" || event.key === "End") {
1332
+ event.preventDefault();
1333
+ event.stopPropagation();
1334
+ onSeek(event.key === "Home" ? 0 : duration);
1335
+ }
1336
+ };
1337
+ const chapterAt = (time) => [...chapters].reverse().find((chapter) => chapter.at <= time);
1338
+ const hoverCue = hover !== null && hover.time <= previewLimit ? cues[cueIndexAt(cues, hover.time)] : void 0;
1339
+ const hoverMarker = hover === null ? void 0 : markers.find((marker) => Math.abs(marker.at / duration * hover.width - hover.x) <= MARKER_REACH);
1340
+ const hoverChapter = hover === null ? void 0 : chapterAt(hover.time);
1341
+ const bubbleLeft = hover === null ? 0 : hover.width > 240 ? Math.min(hover.width - BUBBLE_HALF, Math.max(BUBBLE_HALF, hover.x)) : hover.width / 2;
1342
+ const currentChapter = chapterAt(current);
1343
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1344
+ ref: trackRef,
1345
+ className: "lk-iv-scrubber",
1346
+ role: "slider",
1347
+ tabIndex: ready ? 0 : -1,
1348
+ "aria-label": strings.media.seek,
1349
+ "aria-valuemin": 0,
1350
+ "aria-valuemax": Math.round(duration),
1351
+ "aria-valuenow": Math.round(current),
1352
+ "aria-valuetext": currentChapter === void 0 ? strings.media.timeValue(clock(current), clock(duration)) : `${strings.media.timeValue(clock(current), clock(duration))}, ${currentChapter.title}`,
1353
+ "data-dragging": dragging || void 0,
1354
+ onPointerDown,
1355
+ onPointerMove,
1356
+ onPointerUp: endDrag,
1357
+ onPointerCancel: endDrag,
1358
+ onPointerLeave: () => {
1359
+ if (!dragging) setHover(null);
1360
+ },
1361
+ onKeyDown,
1362
+ children: [
1363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1364
+ className: "lk-iv-scrubber-track",
1365
+ "aria-hidden": "true",
1366
+ children: [
1367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1368
+ className: "lk-iv-scrubber-buffered",
1369
+ style: { width: percent(buffered) }
1370
+ }),
1371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1372
+ className: "lk-iv-scrubber-played",
1373
+ style: { width: percent(current) }
1374
+ }),
1375
+ chapters.filter((chapter) => chapter.at > 0 && chapter.at < duration).map((chapter) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1376
+ className: "lk-iv-scrubber-gap",
1377
+ style: { insetInlineStart: percent(chapter.at) }
1378
+ }, `chapter-${chapter.at}`))
1379
+ ]
1380
+ }),
1381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1382
+ className: "lk-iv-scrubber-thumb",
1383
+ "aria-hidden": "true",
1384
+ style: { insetInlineStart: percent(current) }
1385
+ }),
1386
+ markers.map((marker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1387
+ className: "lk-iv-marker",
1388
+ "data-state": marker.state,
1389
+ "data-required": marker.required || void 0,
1390
+ "aria-hidden": "true",
1391
+ style: { insetInlineStart: percent(Math.min(marker.at, duration)) }
1392
+ }, marker.id)),
1393
+ hover !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1394
+ className: "lk-iv-bubble",
1395
+ "aria-hidden": "true",
1396
+ style: { insetInlineStart: `${bubbleLeft}px` },
1397
+ children: [
1398
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1399
+ className: "lk-iv-bubble-time",
1400
+ children: [clock(hover.time), hoverChapter !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1401
+ className: "lk-iv-bubble-chapter",
1402
+ children: [" · ", hoverChapter.title]
1403
+ }) : null]
1404
+ }),
1405
+ hoverMarker !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1406
+ className: "lk-iv-bubble-quiz",
1407
+ children: hoverMarker.label
1408
+ }) : null,
1409
+ hoverCue !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1410
+ className: "lk-iv-bubble-caption",
1411
+ children: hoverCue.text
1412
+ }) : null
1413
+ ]
1414
+ }) : null
1415
+ ]
1416
+ });
1417
+ }
1418
+ //#endregion
1419
+ //#region src/components/InteractiveVideo/InteractiveVideo.tsx
1420
+ /** What distinguishes one video's content from another's: when it changes, the player starts over. */
1421
+ function contentKey(group) {
1422
+ try {
1423
+ return JSON.stringify([
1424
+ group.id,
1425
+ group.stimulus,
1426
+ group.items,
1427
+ group.timeline
1428
+ ]);
1429
+ } catch {
1430
+ return String(group?.id);
1431
+ }
1432
+ }
1433
+ /**
1434
+ * An interactive video: a video that pauses at each quiz so the learner can
1435
+ * answer it inside the player, then carries on.
1436
+ *
1437
+ * A formative tool, for lessons and practice — not for official summative
1438
+ * exams, which `<ActivitySequence>` delivers. See `renderMode`.
1439
+ *
1440
+ * The player state is keyed on the CONTENT, not on the object: a host that
1441
+ * rebuilds its activity objects on every render — `redactItemGroup` returns a
1442
+ * new one each call — must not restart the video or lose an answer. Only a
1443
+ * different video, different questions or a moved quiz starts it over.
1444
+ */
1445
+ function InteractiveVideo$1(props) {
1446
+ const key = (0, react.useMemo)(() => contentKey(props.group), [props.group]);
1447
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Player, { ...props }, key);
1448
+ }
1449
+ /**
1450
+ * The next frame, or a timer where there are no frames: a page rendered in a
1451
+ * document that never paints — a test environment, a hidden iframe — must
1452
+ * still track the playhead and still announce.
1453
+ */
1454
+ function onNextFrame(run) {
1455
+ if (typeof requestAnimationFrame === "function") {
1456
+ const frame = requestAnimationFrame(run);
1457
+ return () => cancelAnimationFrame(frame);
1458
+ }
1459
+ const timer = setTimeout(run, 33);
1460
+ return () => clearTimeout(timer);
1461
+ }
1462
+ /** What `video-captions-changed` reports: the language of each line on screen, or `null`. */
1463
+ function captionsShown(tracks, preferences) {
1464
+ if (!preferences.captions) return {
1465
+ srclang: null,
1466
+ secondary: null
1467
+ };
1468
+ const shown = resolveCaptionTracks(tracks, preferences);
1469
+ return {
1470
+ srclang: shown.primary?.srclang ?? null,
1471
+ secondary: shown.secondary?.srclang ?? null
1472
+ };
1473
+ }
1474
+ /** One id per mount, for the quiz capture groups. Module-level: never a render-time `useId`. */
1475
+ let playerCount = 0;
1476
+ function Player(props) {
1477
+ const { renderMode = "practice", onProgress, onSubmit, onActivityComplete, onFinished, onInteraction, recordingBinding, workletUrl, captionsLoader, label, shuffleSeed, locale, theme, sanitizeHtml } = props;
1478
+ const strings = require_LkIntlProvider.useLkStrings(props.strings);
1479
+ const aiInForce = require_ai_LkAiProvider.useLearnerAi(props.ai);
1480
+ const delivery = (0, _intellectif_lk_core.resolveDeliveryPolicy)(props.delivery);
1481
+ const scoringKey = JSON.stringify(props.scoring ?? null);
1482
+ const scoring = (0, react.useMemo)(() => (0, _intellectif_lk_core.resolveItemScoringPolicy)(props.scoring), [scoringKey]);
1483
+ const scoringGiven = props.scoring !== void 0 && props.scoring !== null;
1484
+ const group = (0, react.useRef)(props.group).current;
1485
+ const timeline = group.timeline ?? { cues: [] };
1486
+ const media = group.stimulus.media;
1487
+ const chapters = timeline.chapters ?? [];
1488
+ const devError = (0, react.useMemo)(() => {
1489
+ if (!require_useAiHelp.isDevelopment()) return null;
1490
+ if (group.timeline === void 0) return /* @__PURE__ */ new Error(`InteractiveVideo "${group.id}" was given an item group with no timeline. Give it a timeline — a list of quizzes, each at a moment of the video — or render it with <ActivitySequence>.`);
1491
+ if (group.redacted === true) {
1492
+ try {
1493
+ (0, _intellectif_lk_core.assertRedactedItemGroup)(group);
1494
+ } catch (error) {
1495
+ return error;
1496
+ }
1497
+ return null;
1498
+ }
1499
+ const result = (0, _intellectif_lk_core.validateItemGroup)(group);
1500
+ return result.success ? null : new _intellectif_lk_core.ActivitySchemaError("item-group", result.errors);
1501
+ }, [group]);
1502
+ const slots = (0, react.useMemo)(() => {
1503
+ try {
1504
+ return (0, _intellectif_lk_core.flattenSequence)([group]);
1505
+ } catch {
1506
+ return [];
1507
+ }
1508
+ }, [group]);
1509
+ const cues = (0, react.useMemo)(() => timeline.cues.filter((cue) => slots.some((slot) => slot.group?.cue?.id === cue.id && (0, _intellectif_lk_core.isInteractiveVideoItemType)(slot.activity.type))), [timeline.cues, slots]);
1510
+ const slotsByCue = (0, react.useMemo)(() => {
1511
+ const byCue = /* @__PURE__ */ new Map();
1512
+ for (const slot of slots) {
1513
+ const cueId = slot.group?.cue?.id;
1514
+ if (cueId !== void 0 && (0, _intellectif_lk_core.isInteractiveVideoItemType)(slot.activity.type)) byCue.set(cueId, [...byCue.get(cueId) ?? [], slot]);
1515
+ }
1516
+ return byCue;
1517
+ }, [slots]);
1518
+ const [playerId] = (0, react.useState)(() => {
1519
+ playerCount += 1;
1520
+ return `lk-iv-${playerCount}`;
1521
+ });
1522
+ const forced = (0, react.useRef)(props.preferences).current;
1523
+ const defaultsKey = JSON.stringify(props.defaultPreferences ?? null);
1524
+ const defaults = (0, react.useMemo)(() => JSON.parse(defaultsKey), [defaultsKey]);
1525
+ const chosen = (0, react.useRef)({});
1526
+ const [preferences, setPreferences] = (0, react.useState)(() => resolvePreferences({
1527
+ force: forced,
1528
+ defaults: props.defaultPreferences
1529
+ }));
1530
+ const preferencesRef = (0, react.useRef)(preferences);
1531
+ preferencesRef.current = preferences;
1532
+ (0, react.useEffect)(() => {
1533
+ setPreferences(resolvePreferences({
1534
+ force: forced,
1535
+ stored: readStoredPreferences(),
1536
+ defaults,
1537
+ chosen: chosen.current
1538
+ }));
1539
+ }, [forced, defaults]);
1540
+ const [submitted] = (0, react.useState)(() => new Set(props.submittedSlotIds ?? []));
1541
+ const [answered, answeredRef, setSlotAnswered] = useKeySet(() => {
1542
+ const seeded = /* @__PURE__ */ new Set();
1543
+ for (const slot of slots) if (submitted.has(slot.slotId) || props.outcomes !== void 0 && Object.hasOwn(props.outcomes, slot.slotId)) seeded.add(slot.slotId);
1544
+ return seeded;
1545
+ });
1546
+ const [scores, setSlotScore] = useKeyMap();
1547
+ const [pending, pendingRef, setPendingKey] = useKeySet(() => /* @__PURE__ */ new Set());
1548
+ const latestTakes = (0, react.useRef)(/* @__PURE__ */ new Map());
1549
+ const alive = (0, react.useRef)(true);
1550
+ (0, react.useEffect)(() => {
1551
+ alive.current = true;
1552
+ return () => {
1553
+ alive.current = false;
1554
+ };
1555
+ }, []);
1556
+ const finished = (0, react.useCallback)((cueId) => (slotsByCue.get(cueId) ?? []).every((slot) => answeredRef.current.has(slot.slotId)), [slotsByCue]);
1557
+ const shellRef = (0, react.useRef)(null);
1558
+ const videoRef = (0, react.useRef)(null);
1559
+ const playButtonRef = (0, react.useRef)(null);
1560
+ const quizHeadingRef = (0, react.useRef)(null);
1561
+ const [portalContainer, setPortalContainer] = (0, react.useState)(null);
1562
+ const [playing, setPlaying] = (0, react.useState)(false);
1563
+ const [waiting, setWaiting] = (0, react.useState)(false);
1564
+ const [errorCode, setErrorCode] = (0, react.useState)(null);
1565
+ const [current, setCurrent] = (0, react.useState)(0);
1566
+ const [duration, setDuration] = (0, react.useState)(0);
1567
+ const [buffered, setBuffered] = (0, react.useState)(0);
1568
+ const [scrubbing, setScrubbing] = (0, react.useState)(false);
1569
+ const [ended, setEnded] = (0, react.useState)(false);
1570
+ const [fullscreen, setFullscreen] = (0, react.useState)(false);
1571
+ const [pictureInPicture, setPictureInPicture] = (0, react.useState)(false);
1572
+ const [countdown, setCountdown] = (0, react.useState)(false);
1573
+ const [menu, setMenu] = (0, react.useState)(null);
1574
+ const [sheet, setSheet] = (0, react.useState)(false);
1575
+ const [panelTab, setPanelTab] = (0, react.useState)("contents");
1576
+ const [chromeShown, setChromeShown] = (0, react.useState)(true);
1577
+ const [focusInChrome, setFocusInChrome] = (0, react.useState)(false);
1578
+ const [announcement, setAnnouncement] = (0, react.useState)("");
1579
+ const previousTime = (0, react.useRef)(0);
1580
+ const furthest = (0, react.useRef)(0);
1581
+ const [furthestShown, setFurthestShown] = (0, react.useState)(0);
1582
+ const lastProgressAt = (0, react.useRef)(0);
1583
+ const hideTimer = (0, react.useRef)(null);
1584
+ const lastPointer = (0, react.useRef)("mouse");
1585
+ const finishedOnce = (0, react.useRef)(false);
1586
+ const [, setFinishedState] = (0, react.useState)(false);
1587
+ const [openQuiz, setOpenQuiz] = (0, react.useState)(null);
1588
+ const openQuizRef = (0, react.useRef)(openQuiz);
1589
+ openQuizRef.current = openQuiz;
1590
+ const [mounted, setMounted] = (0, react.useState)(/* @__PURE__ */ new Set());
1591
+ const handled = (0, react.useRef)(/* @__PURE__ */ new Set());
1592
+ const announce = (0, react.useCallback)((text) => {
1593
+ setAnnouncement("");
1594
+ onNextFrame(() => setAnnouncement(text));
1595
+ }, []);
1596
+ const emit = (0, react.useCallback)((type, payload) => {
1597
+ onInteraction?.({
1598
+ type,
1599
+ activityId: group.id,
1600
+ timestamp: Date.now(),
1601
+ ...payload !== void 0 ? { payload } : {}
1602
+ });
1603
+ }, [onInteraction, group.id]);
1604
+ const reportProgress = (0, react.useCallback)((force) => {
1605
+ const video = videoRef.current;
1606
+ if (onProgress === void 0 || video === null) return;
1607
+ const now = Date.now();
1608
+ if (!force && now - lastProgressAt.current < 5e3) return;
1609
+ lastProgressAt.current = now;
1610
+ onProgress({
1611
+ progressVersion: "1.0",
1612
+ at: video.currentTime,
1613
+ furthest: furthest.current
1614
+ });
1615
+ }, [onProgress]);
1616
+ const quizTitle = (0, react.useCallback)((cue) => cue.title ?? strings.videoQuiz, [strings.videoQuiz]);
1617
+ const openAt = (0, react.useCallback)((cue) => {
1618
+ const video = videoRef.current;
1619
+ const quizSlots = slotsByCue.get(cue.id) ?? [];
1620
+ if (video !== null) {
1621
+ video.pause();
1622
+ if (Math.abs(video.currentTime - cue.at) > .25 && cue.at <= (video.duration || cue.at)) video.currentTime = Math.min(cue.at, video.duration || cue.at);
1623
+ previousTime.current = cue.at;
1624
+ const webkit = video;
1625
+ if (webkit.webkitDisplayingFullscreen) webkit.webkitExitFullscreen?.();
1626
+ if (typeof document !== "undefined" && document.pictureInPictureElement === video) document.exitPictureInPicture?.().catch(() => void 0);
1627
+ }
1628
+ const firstOpen = quizSlots.findIndex((slot) => !answeredRef.current.has(slot.slotId));
1629
+ setMenu(null);
1630
+ setSheet(false);
1631
+ setEnded(false);
1632
+ setMounted((previous) => previous.has(cue.id) ? previous : /* @__PURE__ */ new Set([...previous, cue.id]));
1633
+ setOpenQuiz({
1634
+ cueId: cue.id,
1635
+ step: firstOpen === -1 ? 0 : firstOpen
1636
+ });
1637
+ announce(strings.videoQuizOpened(clock(cue.at), quizSlots.length));
1638
+ emit("video-quiz-opened", {
1639
+ cueId: cue.id,
1640
+ at: cue.at
1641
+ });
1642
+ reportProgress(true);
1643
+ }, [
1644
+ slotsByCue,
1645
+ announce,
1646
+ strings,
1647
+ emit,
1648
+ reportProgress
1649
+ ]);
1650
+ (0, react.useEffect)(() => {
1651
+ if (openQuiz !== null) quizHeadingRef.current?.focus();
1652
+ }, [openQuiz?.cueId, openQuiz]);
1653
+ const closeQuiz = (0, react.useCallback)((how) => {
1654
+ const open = openQuizRef.current;
1655
+ if (open === null) return;
1656
+ const cue = cues.find((candidate) => candidate.id === open.cueId);
1657
+ setOpenQuiz(null);
1658
+ const video = videoRef.current;
1659
+ if (how === "rewatch" && cue !== void 0 && video !== null) {
1660
+ const to = [...cues.map((c) => c.at), ...chapters.map((c) => c.at)].filter((at) => at < cue.at - .5).sort((a, b) => b - a)[0] ?? 0;
1661
+ video.currentTime = to;
1662
+ previousTime.current = to;
1663
+ setCurrent(to);
1664
+ } else if (cue !== void 0) {
1665
+ handled.current.add(cue.id);
1666
+ emit(how === "skip" ? "video-quiz-skipped" : "video-quiz-closed", { cueId: cue.id });
1667
+ }
1668
+ playButtonRef.current?.focus();
1669
+ if (video !== null && !(video.ended || duration > 0 && video.currentTime >= duration - .05)) video.play().catch(() => setPlaying(false));
1670
+ else if (video !== null) {
1671
+ const next = quizzesAtEnd(cues, duration, handled.current)[0];
1672
+ if (next !== void 0) openAt(next);
1673
+ else setEnded(true);
1674
+ }
1675
+ }, [
1676
+ cues,
1677
+ chapters,
1678
+ emit,
1679
+ duration,
1680
+ openAt
1681
+ ]);
1682
+ const onTick = (0, react.useCallback)((now) => {
1683
+ const before = previousTime.current;
1684
+ previousTime.current = now;
1685
+ if (now > furthest.current) furthest.current = now;
1686
+ if (openQuizRef.current !== null || scrubbing) return;
1687
+ const crossed = crossedQuiz(cues, before, now, handled.current);
1688
+ if (crossed !== void 0) openAt(crossed);
1689
+ }, [
1690
+ cues,
1691
+ scrubbing,
1692
+ openAt
1693
+ ]);
1694
+ (0, react.useEffect)(() => {
1695
+ if (!playing || scrubbing) return;
1696
+ let cancel = () => {};
1697
+ const tick = () => {
1698
+ const video = videoRef.current;
1699
+ if (video !== null) {
1700
+ setCurrent(video.currentTime);
1701
+ setBuffered(bufferedAhead(video));
1702
+ onTick(video.currentTime);
1703
+ if (furthest.current > furthestShown + 1) setFurthestShown(furthest.current);
1704
+ reportProgress(false);
1705
+ }
1706
+ cancel = onNextFrame(tick);
1707
+ };
1708
+ cancel = onNextFrame(tick);
1709
+ return () => cancel();
1710
+ }, [
1711
+ playing,
1712
+ scrubbing,
1713
+ onTick,
1714
+ reportProgress,
1715
+ furthestShown
1716
+ ]);
1717
+ const seek = (0, react.useCallback)((to) => {
1718
+ const video = videoRef.current;
1719
+ if (video === null || duration <= 0) return;
1720
+ const from = video.currentTime;
1721
+ const target = Math.min(Math.max(0, to), duration);
1722
+ const limited = renderMode === "review" ? { at: target } : limitSeek(timeline, from, target, furthest.current, finished);
1723
+ video.currentTime = limited.at;
1724
+ previousTime.current = limited.at;
1725
+ setCurrent(limited.at);
1726
+ setEnded(false);
1727
+ emit("video-seeked", {
1728
+ from,
1729
+ to: limited.at,
1730
+ clamped: limited.hold !== void 0
1731
+ });
1732
+ if (limited.hold?.kind === "required") {
1733
+ openAt(limited.hold.cue);
1734
+ announce(strings.videoHeldAtQuiz(clock(limited.hold.cue.at)));
1735
+ } else if (limited.hold?.kind === "no-skip-ahead") announce(strings.videoHeldAhead);
1736
+ reportProgress(true);
1737
+ }, [
1738
+ duration,
1739
+ renderMode,
1740
+ timeline,
1741
+ finished,
1742
+ emit,
1743
+ announce,
1744
+ strings,
1745
+ openAt,
1746
+ reportProgress
1747
+ ]);
1748
+ const togglePlay = (0, react.useCallback)(() => {
1749
+ const video = videoRef.current;
1750
+ if (video === null) return;
1751
+ if (ended) {
1752
+ setEnded(false);
1753
+ handled.current.clear();
1754
+ video.currentTime = 0;
1755
+ previousTime.current = -1;
1756
+ video.play().catch(() => setPlaying(false));
1757
+ return;
1758
+ }
1759
+ if (video.paused) {
1760
+ if (video.currentTime === 0 && previousTime.current <= 0) {
1761
+ const atStart = crossedQuiz(cues, -1, 0, handled.current);
1762
+ if (atStart !== void 0) {
1763
+ previousTime.current = 0;
1764
+ openAt(atStart);
1765
+ return;
1766
+ }
1767
+ }
1768
+ video.play().catch(() => setPlaying(false));
1769
+ } else video.pause();
1770
+ }, [
1771
+ ended,
1772
+ cues,
1773
+ openAt
1774
+ ]);
1775
+ const tracks = (0, react.useMemo)(() => {
1776
+ if (media?.tracks !== void 0 && media.tracks.length > 0) return media.tracks.filter((track) => track.kind === "captions" || track.kind === "subtitles");
1777
+ return media?.captionsUrl !== void 0 ? [{
1778
+ kind: "captions",
1779
+ src: media.captionsUrl,
1780
+ srclang: locale ?? "en",
1781
+ label: strings.videoCaptionLanguage
1782
+ }] : [];
1783
+ }, [
1784
+ media,
1785
+ locale,
1786
+ strings.videoCaptionLanguage
1787
+ ]);
1788
+ const { primary: primaryTrack, secondary: secondaryTrack } = (0, react.useMemo)(() => resolveCaptionTracks(tracks, {
1789
+ captionLanguage: preferences.captionLanguage,
1790
+ secondaryCaptionLanguage: preferences.secondaryCaptionLanguage
1791
+ }), [
1792
+ tracks,
1793
+ preferences.captionLanguage,
1794
+ preferences.secondaryCaptionLanguage
1795
+ ]);
1796
+ const { loadCues, peekCues } = useCueLoader(captionsLoader);
1797
+ const primaryLine = useCaptionLine(primaryTrack, loadCues, peekCues);
1798
+ const secondaryLine = useCaptionLine(secondaryTrack, loadCues, peekCues);
1799
+ const primaryCues = primaryLine.cues;
1800
+ const secondaryCues = secondaryLine.cues;
1801
+ const captionsFailed = {
1802
+ primary: primaryLine.failed,
1803
+ secondary: secondaryLine.failed
1804
+ };
1805
+ const hasCaptions = primaryCues.length > 0 || secondaryCues.length > 0;
1806
+ const hasTranscript = primaryCues.length > 0;
1807
+ const activeCueIndex = (0, react.useMemo)(() => cueIndexAt(primaryCues, current), [primaryCues, current]);
1808
+ const secondaryCueIndex = (0, react.useMemo)(() => cueIndexAt(secondaryCues, current), [secondaryCues, current]);
1809
+ const spoilerLimit = timeline.navigation === "no-skip-ahead" && renderMode !== "review" ? Math.max(furthestShown, current) : Number.POSITIVE_INFINITY;
1810
+ const onPreferencesChangeRef = (0, react.useRef)(props.onPreferencesChange);
1811
+ onPreferencesChangeRef.current = props.onPreferencesChange;
1812
+ const tracksRef = (0, react.useRef)(tracks);
1813
+ tracksRef.current = tracks;
1814
+ /** The last second language the learner had, for Shift + C to bring back. */
1815
+ const lastSecondary = (0, react.useRef)(null);
1816
+ /**
1817
+ * A change the learner made: applied, remembered in this browser, handed to
1818
+ * the host, and — when it changes the captions on screen — reported.
1819
+ */
1820
+ const changePreferences = (0, react.useCallback)((change) => {
1821
+ const previous = preferencesRef.current;
1822
+ const next = applyPreferenceChange(previous, change);
1823
+ if (previous.secondaryCaptionLanguage !== null) lastSecondary.current = previous.secondaryCaptionLanguage;
1824
+ chosen.current = {
1825
+ ...chosen.current,
1826
+ ...change
1827
+ };
1828
+ preferencesRef.current = next;
1829
+ setPreferences(next);
1830
+ rememberPreferences(change);
1831
+ onPreferencesChangeRef.current?.(next, change);
1832
+ const before = captionsShown(tracksRef.current, previous);
1833
+ const after = captionsShown(tracksRef.current, next);
1834
+ if (before.srclang !== after.srclang || before.secondary !== after.secondary) emit("video-captions-changed", after);
1835
+ }, [emit]);
1836
+ /**
1837
+ * A first language picked from the menu. Picking the language the second
1838
+ * line shows swaps the two — English + Español becomes Español + English —
1839
+ * rather than silently dropping one.
1840
+ */
1841
+ const chooseCaptionTrack = (track) => {
1842
+ const swaps = primaryTrack !== void 0 && secondaryTrack !== void 0 && track.srclang.toLowerCase() === secondaryTrack.srclang.toLowerCase();
1843
+ changePreferences({
1844
+ captions: true,
1845
+ captionLanguage: track.srclang,
1846
+ ...swaps ? { secondaryCaptionLanguage: primaryTrack.srclang } : {}
1847
+ });
1848
+ };
1849
+ /** A second language picked from the menu, or none. Picking one shows the captions. */
1850
+ const chooseSecondaryTrack = (track) => {
1851
+ changePreferences(track === null ? { secondaryCaptionLanguage: null } : {
1852
+ secondaryCaptionLanguage: track.srclang,
1853
+ captions: true
1854
+ });
1855
+ };
1856
+ /**
1857
+ * Shift + C. With a second line showing, it goes; with one chosen but the
1858
+ * captions off, the pair comes back; otherwise a second line comes on — the
1859
+ * last language the learner had, else the host's suggestion, else the first
1860
+ * other language the video has. Pressing the key is asking for a second
1861
+ * line, so that first language is the learner's choice, not a fallback the
1862
+ * player made for them. With no other language at all, it says so and
1863
+ * changes nothing.
1864
+ */
1865
+ const toggleSecondary = () => {
1866
+ if (secondaryTrack !== void 0) {
1867
+ changePreferences(preferences.captions ? { secondaryCaptionLanguage: null } : { captions: true });
1868
+ return;
1869
+ }
1870
+ const wanted = [
1871
+ lastSecondary.current,
1872
+ preferences.secondaryCaptionLanguage,
1873
+ sanitizePartialPreferences(defaults).secondaryCaptionLanguage ?? null,
1874
+ secondaryCandidates(tracks, primaryTrack)[0]?.srclang ?? null
1875
+ ];
1876
+ for (const language of wanted) {
1877
+ if (language === null) continue;
1878
+ const { secondary } = resolveCaptionTracks(tracks, {
1879
+ captionLanguage: preferences.captionLanguage,
1880
+ secondaryCaptionLanguage: language
1881
+ });
1882
+ if (secondary !== void 0) {
1883
+ changePreferences({
1884
+ secondaryCaptionLanguage: secondary.srclang,
1885
+ captions: true
1886
+ });
1887
+ return;
1888
+ }
1889
+ }
1890
+ announce(strings.videoNoSecondLanguage);
1891
+ };
1892
+ (0, react.useEffect)(() => {
1893
+ const video = videoRef.current;
1894
+ if (video !== null) {
1895
+ video.playbackRate = preferences.speed;
1896
+ video.volume = preferences.volume;
1897
+ video.muted = preferences.muted;
1898
+ }
1899
+ }, [
1900
+ preferences.speed,
1901
+ preferences.volume,
1902
+ preferences.muted
1903
+ ]);
1904
+ (0, react.useEffect)(() => {
1905
+ const onChange = () => setFullscreen(document.fullscreenElement === shellRef.current);
1906
+ document.addEventListener("fullscreenchange", onChange);
1907
+ return () => document.removeEventListener("fullscreenchange", onChange);
1908
+ }, []);
1909
+ (0, react.useEffect)(() => {
1910
+ const video = videoRef.current;
1911
+ if (video === null) return;
1912
+ const enter = () => {
1913
+ setPictureInPicture(true);
1914
+ emit("video-pip-changed", { on: true });
1915
+ };
1916
+ const leave = () => {
1917
+ setPictureInPicture(false);
1918
+ emit("video-pip-changed", { on: false });
1919
+ };
1920
+ video.addEventListener("enterpictureinpicture", enter);
1921
+ video.addEventListener("leavepictureinpicture", leave);
1922
+ return () => {
1923
+ video.removeEventListener("enterpictureinpicture", enter);
1924
+ video.removeEventListener("leavepictureinpicture", leave);
1925
+ };
1926
+ }, [emit]);
1927
+ (0, react.useEffect)(() => () => {
1928
+ if (hideTimer.current !== null) clearTimeout(hideTimer.current);
1929
+ }, []);
1930
+ const revealChrome = (0, react.useCallback)(() => {
1931
+ setChromeShown(true);
1932
+ if (hideTimer.current !== null) clearTimeout(hideTimer.current);
1933
+ hideTimer.current = setTimeout(() => setChromeShown(false), 2400);
1934
+ }, []);
1935
+ const toggleFullscreen = (0, react.useCallback)(() => {
1936
+ const shell = shellRef.current;
1937
+ const video = videoRef.current;
1938
+ if (typeof document === "undefined" || shell === null) return;
1939
+ if (document.fullscreenElement) document.exitFullscreen().catch(() => void 0);
1940
+ else if (typeof shell.requestFullscreen === "function" && document.fullscreenEnabled !== false) shell.requestFullscreen().catch(() => void 0);
1941
+ else video?.webkitEnterFullscreen?.();
1942
+ emit("video-fullscreen-changed");
1943
+ }, [emit]);
1944
+ const pictureInPictureSupported = typeof document !== "undefined" && document.pictureInPictureEnabled === true;
1945
+ const togglePictureInPicture = (0, react.useCallback)(() => {
1946
+ const video = videoRef.current;
1947
+ if (video === null) return;
1948
+ if (document.pictureInPictureElement) document.exitPictureInPicture().catch(() => void 0);
1949
+ else video.requestPictureInPicture?.().catch(() => void 0);
1950
+ }, []);
1951
+ const jumpPoints = (0, react.useMemo)(() => [.../* @__PURE__ */ new Set([...chapters.map((c) => c.at), ...cues.map((c) => c.at)])].sort((a, b) => a - b), [chapters, cues]);
1952
+ const onKeyDown = (event) => {
1953
+ if (openQuiz !== null || !preferences.shortcuts) return;
1954
+ const target = event.target;
1955
+ if (target.isContentEditable || [
1956
+ "INPUT",
1957
+ "TEXTAREA",
1958
+ "SELECT"
1959
+ ].includes(target.tagName) || target.tagName === "BUTTON" && (event.key === " " || event.key === "Enter")) return;
1960
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
1961
+ const video = videoRef.current;
1962
+ const key = event.key;
1963
+ const done = () => {
1964
+ event.preventDefault();
1965
+ revealChrome();
1966
+ };
1967
+ if (/^[0-9]$/.test(key)) {
1968
+ done();
1969
+ seek(Number(key) / 10 * duration);
1970
+ return;
1971
+ }
1972
+ switch (key) {
1973
+ case " ":
1974
+ case "k":
1975
+ case "K":
1976
+ done();
1977
+ togglePlay();
1978
+ return;
1979
+ case "j":
1980
+ case "J":
1981
+ done();
1982
+ seek(current - 10);
1983
+ return;
1984
+ case "l":
1985
+ case "L":
1986
+ done();
1987
+ seek(current + 10);
1988
+ return;
1989
+ case "ArrowLeft":
1990
+ done();
1991
+ seek(current - (event.shiftKey ? 1 : 5));
1992
+ return;
1993
+ case "ArrowRight":
1994
+ done();
1995
+ seek(current + (event.shiftKey ? 1 : 5));
1996
+ return;
1997
+ case "Home":
1998
+ done();
1999
+ seek(0);
2000
+ return;
2001
+ case "End":
2002
+ done();
2003
+ seek(duration);
2004
+ return;
2005
+ case ",":
2006
+ case ".":
2007
+ done();
2008
+ video?.pause();
2009
+ seek(current + (key === "," ? -.03333333333333333 : FRAME_SECONDS));
2010
+ return;
2011
+ case "<":
2012
+ case ">":
2013
+ done();
2014
+ changePreferences({ speed: nudgeSpeed(preferences.speed, key === "<" ? -1 : 1) });
2015
+ return;
2016
+ case "[": {
2017
+ done();
2018
+ const previous = [...jumpPoints].reverse().find((at) => at < current - 1);
2019
+ seek(previous ?? 0);
2020
+ return;
2021
+ }
2022
+ case "]": {
2023
+ done();
2024
+ const next = jumpPoints.find((at) => at > current + .5);
2025
+ if (next !== void 0) seek(next);
2026
+ return;
2027
+ }
2028
+ case "m":
2029
+ case "M":
2030
+ done();
2031
+ changePreferences({ muted: !preferences.muted });
2032
+ return;
2033
+ case "c":
2034
+ case "C":
2035
+ if (event.shiftKey) {
2036
+ done();
2037
+ toggleSecondary();
2038
+ } else if (hasCaptions) {
2039
+ done();
2040
+ changePreferences({ captions: !preferences.captions });
2041
+ }
2042
+ return;
2043
+ case "t":
2044
+ case "T":
2045
+ if (hasTranscript) {
2046
+ done();
2047
+ setPanelTab("transcript");
2048
+ changePreferences({ panel: !(preferences.panel && panelTab === "transcript") });
2049
+ }
2050
+ return;
2051
+ case "p":
2052
+ case "P":
2053
+ if (pictureInPictureSupported) {
2054
+ done();
2055
+ togglePictureInPicture();
2056
+ }
2057
+ return;
2058
+ case "f":
2059
+ case "F":
2060
+ done();
2061
+ toggleFullscreen();
2062
+ return;
2063
+ case "?":
2064
+ done();
2065
+ setSheet((open) => !open);
2066
+ return;
2067
+ default: return;
2068
+ }
2069
+ };
2070
+ const quizzes = cues.map((cue) => {
2071
+ const quizSlots = slotsByCue.get(cue.id) ?? [];
2072
+ const count = quizSlots.filter((slot) => answered.has(slot.slotId)).length;
2073
+ return {
2074
+ id: cue.id,
2075
+ at: cue.at,
2076
+ title: quizTitle(cue),
2077
+ answered: count,
2078
+ total: quizSlots.length,
2079
+ required: cue.required === true && renderMode !== "review",
2080
+ state: markerState(quizSlots, answered, renderMode, props.outcomes)
2081
+ };
2082
+ });
2083
+ const markers = quizzes.map((quiz) => ({
2084
+ id: quiz.id,
2085
+ at: quiz.at,
2086
+ label: `${quiz.title} · ${strings.videoQuizProgress(quiz.answered, quiz.total)}`,
2087
+ state: quiz.state,
2088
+ required: quiz.required
2089
+ }));
2090
+ const totalQuestions = quizzes.reduce((sum, quiz) => sum + quiz.total, 0);
2091
+ const answeredCount = quizzes.reduce((sum, quiz) => sum + quiz.answered, 0);
2092
+ const graded = [...scores.values()];
2093
+ const percent = graded.length === 0 ? null : Math.round(graded.reduce((a, b) => a + b, 0) / graded.length * 100);
2094
+ const summary = (0, react.useCallback)(() => {
2095
+ return { slots: slots.filter((slot) => slot.group?.cue !== void 0).map((slot) => {
2096
+ const cueId = slot.group?.cue?.id;
2097
+ return {
2098
+ slotId: slot.slotId,
2099
+ activityId: slot.activity.id,
2100
+ cueId,
2101
+ status: answeredRef.current.has(slot.slotId) ? "answered" : handled.current.has(cueId) ? "skipped" : "unreached"
2102
+ };
2103
+ }) };
2104
+ }, [slots]);
2105
+ const finish = (0, react.useCallback)(() => {
2106
+ if (finishedOnce.current) return;
2107
+ finishedOnce.current = true;
2108
+ setFinishedState(true);
2109
+ onFinished?.(summary());
2110
+ }, [onFinished, summary]);
2111
+ const answerPending = pending.size > 0;
2112
+ (0, react.useEffect)(() => {
2113
+ if (ended && !answerPending && totalQuestions > 0 && answeredCount === totalQuestions && renderMode !== "review") finish();
2114
+ }, [
2115
+ ended,
2116
+ answerPending,
2117
+ totalQuestions,
2118
+ answeredCount,
2119
+ renderMode,
2120
+ finish
2121
+ ]);
2122
+ const slotBindings = (0, react.useMemo)(() => {
2123
+ const bindings = /* @__PURE__ */ new Map();
2124
+ if (recordingBinding === void 0) return bindings;
2125
+ const { assess, playbackUrl } = recordingBinding;
2126
+ for (const slot of slots) {
2127
+ const place = {
2128
+ slotId: slot.slotId,
2129
+ index: slot.index,
2130
+ activityId: slot.activity.id
2131
+ };
2132
+ bindings.set(slot.slotId, {
2133
+ upload: (take) => recordingBinding.upload(take, place),
2134
+ ...assess !== void 0 ? { assess: (ref) => assess.call(recordingBinding, ref, place) } : {},
2135
+ ...playbackUrl !== void 0 ? { playbackUrl: (ref) => playbackUrl.call(recordingBinding, ref, place) } : {}
2136
+ });
2137
+ }
2138
+ return bindings;
2139
+ }, [slots, recordingBinding]);
2140
+ const places = (0, react.useMemo)(() => {
2141
+ const byId = /* @__PURE__ */ new Map();
2142
+ for (const [cueId, cueSlots] of slotsByCue) for (const slot of cueSlots) byId.set(slot.slotId, {
2143
+ slotId: slot.slotId,
2144
+ index: slot.index,
2145
+ activityId: slot.activity.id,
2146
+ cueId
2147
+ });
2148
+ return byId;
2149
+ }, [slotsByCue]);
2150
+ const reportTo = (0, react.useRef)({
2151
+ onSubmit,
2152
+ onActivityComplete,
2153
+ onInteraction,
2154
+ renderMode,
2155
+ delivery,
2156
+ scoring: scoringGiven ? scoring : void 0
2157
+ });
2158
+ reportTo.current = {
2159
+ onSubmit,
2160
+ onActivityComplete,
2161
+ onInteraction,
2162
+ renderMode,
2163
+ delivery,
2164
+ scoring: scoringGiven ? scoring : void 0
2165
+ };
2166
+ /**
2167
+ * The calls a question reports what the learner did through — the SDK's own
2168
+ * components and a host's alike, so nothing downstream can tell them apart.
2169
+ * Made once per question.
2170
+ */
2171
+ const calls = (0, react.useRef)(/* @__PURE__ */ new Map());
2172
+ const callsFor = (place) => {
2173
+ const made = calls.current.get(place.slotId);
2174
+ if (made !== void 0) return made;
2175
+ const { slotId } = place;
2176
+ const warned = /* @__PURE__ */ new Set();
2177
+ const refused = (call) => {
2178
+ if (reportTo.current.renderMode !== "review") return false;
2179
+ if (require_useAiHelp.isDevelopment() && !warned.has(call)) {
2180
+ warned.add(call);
2181
+ console.warn(`InteractiveVideo: \`${call}()\` was called for question "${slotId}" in renderMode "review", where nothing is submittable. It was ignored.`);
2182
+ }
2183
+ return true;
2184
+ };
2185
+ const next = {
2186
+ submit: (response) => {
2187
+ if (!alive.current || refused("submit")) return;
2188
+ setSlotAnswered(slotId, true);
2189
+ reportTo.current.onSubmit?.(response, place);
2190
+ },
2191
+ complete: (result) => {
2192
+ if (!alive.current || refused("complete")) return;
2193
+ setSlotAnswered(slotId, true);
2194
+ setSlotScore(slotId, result.maxScore > 0 ? result.score / result.maxScore : void 0);
2195
+ reportTo.current.onActivityComplete?.(result, place);
2196
+ },
2197
+ clear: () => {
2198
+ if (!alive.current || refused("clear")) return;
2199
+ setSlotAnswered(slotId, false);
2200
+ setSlotScore(slotId, void 0);
2201
+ },
2202
+ setPending: (on) => {
2203
+ if (!alive.current) return;
2204
+ setPendingKey(`host:${slotId}`, on === true);
2205
+ },
2206
+ emit: (type, payload) => {
2207
+ if (!alive.current) return;
2208
+ reportTo.current.onInteraction?.({
2209
+ type,
2210
+ activityId: place.activityId,
2211
+ timestamp: Date.now(),
2212
+ ...payload !== void 0 ? { payload } : {}
2213
+ });
2214
+ }
2215
+ };
2216
+ calls.current.set(slotId, next);
2217
+ return next;
2218
+ };
2219
+ /**
2220
+ * What an SDK read-aloud reports about each take, through its slot's channel.
2221
+ * A take being stored or judged is on its way; every other state is not. A
2222
+ * failure the learner may retry is not on its way either: nothing comes of
2223
+ * it unless they press Try again, which reports the take in flight again.
2224
+ * (A pager holds its set for one while the learner is on that question; the
2225
+ * video's Finish is on the end card, where no question is.)
2226
+ */
2227
+ const reportTake = (slotId, take, state) => {
2228
+ if (!alive.current) return;
2229
+ const latest = latestTakes.current.get(slotId);
2230
+ if (latest !== void 0 && take < latest) return;
2231
+ latestTakes.current.set(slotId, take);
2232
+ setPendingKey(`take:${slotId}`, state === "in-flight");
2233
+ };
2234
+ const channels = (0, react.useRef)(/* @__PURE__ */ new Map());
2235
+ const channelFor = (slotId) => {
2236
+ let channel = channels.current.get(slotId);
2237
+ if (channel === void 0) {
2238
+ channel = {
2239
+ captureGroup: `${playerId}::${slotId}`,
2240
+ get renderMode() {
2241
+ return reportTo.current.renderMode;
2242
+ },
2243
+ get delivery() {
2244
+ return reportTo.current.delivery;
2245
+ },
2246
+ get scoring() {
2247
+ return reportTo.current.scoring;
2248
+ },
2249
+ get triesClosed() {
2250
+ return finishedOnce.current;
2251
+ },
2252
+ triesState: () => {},
2253
+ takeState: (take, state) => reportTake(slotId, take, state)
2254
+ };
2255
+ channels.current.set(slotId, channel);
2256
+ }
2257
+ return channel;
2258
+ };
2259
+ /** What the host restored for a question: read the way the SDK's components read it. */
2260
+ const seedsFor = (slotId) => ({
2261
+ restored: props.responses !== void 0 && Object.hasOwn(props.responses, slotId) ? props.responses[slotId] : void 0,
2262
+ outcome: props.outcomes !== void 0 && Object.hasOwn(props.outcomes, slotId) ? props.outcomes[slotId] : void 0
2263
+ });
2264
+ /** The question as `renderQuestion` is handed it. */
2265
+ const questionFor = (slot, place, active) => {
2266
+ const { restored, outcome } = seedsFor(slot.slotId);
2267
+ return {
2268
+ activity: slot.activity,
2269
+ slot: place,
2270
+ renderMode,
2271
+ active,
2272
+ ...locale !== void 0 ? { locale } : {},
2273
+ ...restored !== void 0 ? { defaultValue: restored } : {},
2274
+ defaultSubmitted: submitted.has(slot.slotId),
2275
+ ...outcome !== void 0 ? { outcome } : {},
2276
+ portalContainer,
2277
+ ...aiInForce !== void 0 && renderMode !== "exam" ? { ai: aiInForce } : {},
2278
+ delivery,
2279
+ scoring,
2280
+ ...callsFor(place)
2281
+ };
2282
+ };
2283
+ /** The SDK's own component for a question. */
2284
+ const sdkQuestion = (slot, place) => {
2285
+ const activity = slot.activity;
2286
+ const { restored, outcome } = seedsFor(slot.slotId);
2287
+ const reported = callsFor(place);
2288
+ const common = {
2289
+ renderMode,
2290
+ ...restored !== void 0 ? { defaultValue: restored } : {},
2291
+ ...submitted.has(slot.slotId) ? { defaultSubmitted: true } : {},
2292
+ ...outcome !== void 0 ? { outcome } : {},
2293
+ onSubmit: reported.submit,
2294
+ onComplete: reported.complete,
2295
+ ...onInteraction !== void 0 ? { onInteraction } : {},
2296
+ ...sanitizeHtml !== void 0 ? { sanitizeHtml } : {},
2297
+ ...props.strings !== void 0 ? { strings: props.strings } : {},
2298
+ ...props.ai !== void 0 ? { ai: props.ai } : {},
2299
+ ...props.delivery !== void 0 && props.delivery !== null ? { delivery: props.delivery } : {},
2300
+ ...scoringGiven ? { scoring: props.scoring } : {},
2301
+ ...locale !== void 0 ? { locale } : {}
2302
+ };
2303
+ switch (activity.type) {
2304
+ case "multiple-choice": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_MultipleChoice.MultipleChoice, {
2305
+ data: activity,
2306
+ ...common,
2307
+ ...shuffleSeed !== void 0 ? { shuffleSeed } : {}
2308
+ });
2309
+ case "fill-in-the-blanks": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_FillInTheBlanks.FillInTheBlanks, {
2310
+ data: activity,
2311
+ ...common
2312
+ });
2313
+ case "gap-select": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_GapSelect.GapSelect, {
2314
+ data: activity,
2315
+ ...common,
2316
+ ...shuffleSeed !== void 0 ? { shuffleSeed } : {}
2317
+ });
2318
+ case "dictation": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Dictation.Dictation, {
2319
+ data: activity,
2320
+ ...common
2321
+ });
2322
+ case "read-aloud": {
2323
+ const binding = slotBindings.get(slot.slotId);
2324
+ const assessment = props.assessments !== void 0 && Object.hasOwn(props.assessments, slot.slotId) ? props.assessments[slot.slotId] : void 0;
2325
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ReadAloud.ReadAloud, {
2326
+ data: activity,
2327
+ ...common,
2328
+ ...binding !== void 0 ? { recordingBinding: binding } : {},
2329
+ ...assessment !== void 0 ? { assessment } : {},
2330
+ ...workletUrl !== void 0 ? { workletUrl } : {}
2331
+ });
2332
+ }
2333
+ case "written-response": return null;
2334
+ default: return null;
2335
+ }
2336
+ };
2337
+ if (devError !== null) throw devError;
2338
+ const openCue = openQuiz === null ? void 0 : cues.find((cue) => cue.id === openQuiz.cueId);
2339
+ const openSlots = openCue === void 0 ? [] : slotsByCue.get(openCue.id) ?? [];
2340
+ const step = openQuiz?.step ?? 0;
2341
+ const stepSlot = openSlots[step];
2342
+ const stepAnswered = stepSlot !== void 0 && answered.has(stepSlot.slotId);
2343
+ const lastStep = step >= openSlots.length - 1;
2344
+ const required = openCue?.required === true && renderMode !== "review";
2345
+ const canAdvance = !required || stepAnswered;
2346
+ const goToStep = (next) => {
2347
+ setOpenQuiz((open) => open === null ? open : {
2348
+ ...open,
2349
+ step: next
2350
+ });
2351
+ const slot = openSlots[next];
2352
+ if (slot !== void 0 && openCue !== void 0) {
2353
+ announce(strings.videoQuestionProgress(next + 1, openSlots.length));
2354
+ emit("video-quiz-question-shown", {
2355
+ cueId: openCue.id,
2356
+ slotId: slot.slotId
2357
+ });
2358
+ }
2359
+ };
2360
+ const captionsHidden = !preferences.captions || openQuiz !== null || ended || errorCode !== null;
2361
+ const primaryText = captionsHidden ? void 0 : primaryCues[activeCueIndex]?.text;
2362
+ const secondaryText = captionsHidden ? void 0 : secondaryCues[secondaryCueIndex]?.text;
2363
+ const chromeVisible = !playing || chromeShown || menu !== null || scrubbing || focusInChrome || sheet || openQuiz !== null;
2364
+ const volumeLevel = preferences.muted || preferences.volume === 0 ? "muted" : preferences.volume < .5 ? "low" : "high";
2365
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2366
+ ref: shellRef,
2367
+ className: "lk-iv",
2368
+ tabIndex: -1,
2369
+ lang: locale,
2370
+ style: theme,
2371
+ "data-render-mode": renderMode,
2372
+ "data-quiz-open": openQuiz !== null || void 0,
2373
+ "data-fullscreen": fullscreen || void 0,
2374
+ "data-chrome": chromeVisible ? "shown" : "hidden",
2375
+ onKeyDown,
2376
+ children: [
2377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2378
+ className: "lk-iv-stage",
2379
+ onPointerDown: (event) => {
2380
+ lastPointer.current = event.pointerType;
2381
+ },
2382
+ onPointerMove: (event) => {
2383
+ if (event.pointerType !== "touch") revealChrome();
2384
+ },
2385
+ onPointerLeave: () => {
2386
+ if (playing) setChromeShown(false);
2387
+ },
2388
+ children: [
2389
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
2390
+ ref: videoRef,
2391
+ className: "lk-iv-video",
2392
+ src: media?.url,
2393
+ ...media?.poster !== void 0 ? { poster: media.poster } : {},
2394
+ preload: "metadata",
2395
+ playsInline: true,
2396
+ "aria-label": group.title ?? media?.alt,
2397
+ onClick: () => {
2398
+ if (lastPointer.current === "touch" && !chromeVisible) {
2399
+ revealChrome();
2400
+ return;
2401
+ }
2402
+ togglePlay();
2403
+ },
2404
+ onDoubleClick: toggleFullscreen,
2405
+ onPlay: () => {
2406
+ setPlaying(true);
2407
+ setEnded(false);
2408
+ revealChrome();
2409
+ emit("video-played", { at: videoRef.current?.currentTime ?? 0 });
2410
+ },
2411
+ onPause: () => {
2412
+ setPlaying(false);
2413
+ emit("video-paused", { at: videoRef.current?.currentTime ?? 0 });
2414
+ reportProgress(true);
2415
+ },
2416
+ onWaiting: () => setWaiting(true),
2417
+ onPlaying: () => setWaiting(false),
2418
+ onProgress: (event) => setBuffered(bufferedAhead(event.currentTarget)),
2419
+ onTimeUpdate: (event) => {
2420
+ if (!playing && !scrubbing) setCurrent(event.currentTarget.currentTime);
2421
+ },
2422
+ onRateChange: (event) => emit("video-rate-changed", { rate: event.currentTarget.playbackRate }),
2423
+ onLoadedMetadata: (event) => {
2424
+ const video = event.currentTarget;
2425
+ const length = Number.isFinite(video.duration) ? video.duration : 0;
2426
+ setDuration(length);
2427
+ video.playbackRate = preferences.speed;
2428
+ video.volume = preferences.volume;
2429
+ video.muted = preferences.muted;
2430
+ const stored = (0, _intellectif_lk_core.readMediaProgress)(props.progress, length);
2431
+ if (stored !== null) {
2432
+ const at = renderMode === "review" ? stored.at : limitResume(timeline, stored.at, finished);
2433
+ furthest.current = Math.max(stored.furthest, at);
2434
+ setFurthestShown(furthest.current);
2435
+ video.currentTime = at;
2436
+ previousTime.current = at;
2437
+ setCurrent(at);
2438
+ }
2439
+ },
2440
+ onEnded: () => {
2441
+ setPlaying(false);
2442
+ setCurrent(duration);
2443
+ emit("video-ended");
2444
+ reportProgress(true);
2445
+ const first = quizzesAtEnd(cues, duration, handled.current)[0];
2446
+ if (first !== void 0) openAt(first);
2447
+ else setEnded(true);
2448
+ },
2449
+ onError: (event) => {
2450
+ setWaiting(false);
2451
+ setErrorCode(event.currentTarget.error?.code ?? 0);
2452
+ }
2453
+ }),
2454
+ label !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2455
+ className: "lk-iv-label",
2456
+ children: label
2457
+ }) : null,
2458
+ primaryText !== void 0 || secondaryText !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2459
+ className: "lk-iv-captions",
2460
+ "data-size": preferences.captionSize,
2461
+ "data-background": preferences.captionBackground || void 0,
2462
+ children: [primaryText !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2463
+ className: "lk-iv-caption",
2464
+ "data-role": "primary",
2465
+ "data-size": preferences.captionSize,
2466
+ "data-background": preferences.captionBackground || void 0,
2467
+ lang: primaryTrack?.srclang,
2468
+ dir: "auto",
2469
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: primaryText })
2470
+ }) : null, secondaryText !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2471
+ className: "lk-iv-caption",
2472
+ "data-role": "secondary",
2473
+ "data-size": preferences.captionSize,
2474
+ "data-background": preferences.captionBackground || void 0,
2475
+ lang: secondaryTrack?.srclang,
2476
+ dir: "auto",
2477
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: secondaryText })
2478
+ }) : null]
2479
+ }) : null,
2480
+ waiting && errorCode === null && openQuiz === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2481
+ className: "lk-iv-spinner",
2482
+ "aria-hidden": "true"
2483
+ }) : null,
2484
+ errorCode !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2485
+ className: "lk-iv-error",
2486
+ role: "alert",
2487
+ children: [
2488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlertIcon, {}),
2489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: strings.videoError(errorCode) }),
2490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2491
+ type: "button",
2492
+ className: "lk-iv-action",
2493
+ onClick: () => {
2494
+ setErrorCode(null);
2495
+ videoRef.current?.load();
2496
+ },
2497
+ children: strings.videoTryAgain
2498
+ })
2499
+ ]
2500
+ }) : null,
2501
+ !playing && !waiting && errorCode === null && openQuiz === null && !ended ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2502
+ type: "button",
2503
+ className: "lk-iv-big-play",
2504
+ "aria-label": strings.media.play,
2505
+ onClick: togglePlay,
2506
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayIcon, {})
2507
+ }) : null,
2508
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2509
+ className: "lk-iv-chrome",
2510
+ inert: openQuiz !== null,
2511
+ onFocus: () => setFocusInChrome(true),
2512
+ onBlur: (event) => {
2513
+ if (!event.currentTarget.contains(event.relatedTarget)) setFocusInChrome(false);
2514
+ },
2515
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Scrubber, {
2516
+ current,
2517
+ duration,
2518
+ buffered,
2519
+ chapters,
2520
+ markers,
2521
+ cues: primaryCues,
2522
+ previewLimit: spoilerLimit,
2523
+ strings,
2524
+ onSeek: seek,
2525
+ onScrubChange: setScrubbing
2526
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2527
+ className: "lk-iv-bar",
2528
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2529
+ className: "lk-iv-bar-group",
2530
+ children: [
2531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2532
+ ref: playButtonRef,
2533
+ type: "button",
2534
+ className: "lk-iv-button",
2535
+ "aria-label": ended ? strings.videoReplay : playing ? strings.media.pause : strings.media.play,
2536
+ onClick: togglePlay,
2537
+ children: ended ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReplayIcon, {}) : playing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PauseIcon, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayIcon, {})
2538
+ }),
2539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2540
+ type: "button",
2541
+ className: "lk-iv-button lk-iv-wide",
2542
+ "aria-label": strings.videoBack(10),
2543
+ onClick: () => seek(current - 10),
2544
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BackIcon, {})
2545
+ }),
2546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2547
+ type: "button",
2548
+ className: "lk-iv-button lk-iv-wide",
2549
+ "aria-label": strings.videoForward(10),
2550
+ onClick: () => seek(current + 10),
2551
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ForwardIcon, {})
2552
+ }),
2553
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2554
+ className: "lk-iv-volume",
2555
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2556
+ type: "button",
2557
+ className: "lk-iv-button",
2558
+ "aria-label": preferences.muted ? strings.media.unmute : strings.media.mute,
2559
+ "aria-pressed": preferences.muted,
2560
+ onClick: () => changePreferences({ muted: !preferences.muted }),
2561
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VolumeIcon, { level: volumeLevel })
2562
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2563
+ id: `${playerId}-volume`,
2564
+ className: "lk-iv-volume-slider",
2565
+ type: "range",
2566
+ min: 0,
2567
+ max: 1,
2568
+ step: .05,
2569
+ value: preferences.muted ? 0 : preferences.volume,
2570
+ "aria-label": strings.media.volume,
2571
+ onChange: (event) => changePreferences({
2572
+ volume: Number(event.target.value),
2573
+ muted: false
2574
+ })
2575
+ })]
2576
+ }),
2577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2578
+ type: "button",
2579
+ className: "lk-iv-time",
2580
+ "aria-label": countdown ? strings.videoShowElapsed : strings.videoShowRemaining,
2581
+ onClick: () => setCountdown((on) => !on),
2582
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2583
+ "aria-hidden": "true",
2584
+ children: [countdown ? `-${clock(Math.max(0, duration - current))}` : clock(current), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2585
+ className: "lk-iv-time-total",
2586
+ children: [" / ", clock(duration)]
2587
+ })]
2588
+ })
2589
+ })
2590
+ ]
2591
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2592
+ className: "lk-iv-bar-group",
2593
+ children: [
2594
+ quizzes.length > 0 || chapters.length > 0 || hasTranscript ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2595
+ type: "button",
2596
+ className: "lk-iv-button",
2597
+ "aria-label": strings.videoPanel,
2598
+ "aria-pressed": preferences.panel,
2599
+ onClick: () => changePreferences({ panel: !preferences.panel }),
2600
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContentsIcon, {})
2601
+ }) : null,
2602
+ hasCaptions ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2603
+ type: "button",
2604
+ className: "lk-iv-button",
2605
+ "aria-label": preferences.captions ? strings.videoCaptionsHide : strings.videoCaptionsShow,
2606
+ "aria-pressed": preferences.captions,
2607
+ onClick: () => changePreferences({ captions: !preferences.captions }),
2608
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CaptionsIcon, { on: preferences.captions })
2609
+ }) : null,
2610
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2611
+ className: "lk-iv-menu-anchor",
2612
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2613
+ type: "button",
2614
+ className: "lk-iv-button lk-iv-speed",
2615
+ "aria-label": strings.media.speed,
2616
+ "aria-haspopup": "menu",
2617
+ "aria-expanded": menu === "speed",
2618
+ onClick: () => setMenu((open) => open === "speed" ? null : "speed"),
2619
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2620
+ "aria-hidden": "true",
2621
+ children: `${preferences.speed}×`
2622
+ })
2623
+ }), menu === "speed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeedMenu, {
2624
+ speed: preferences.speed,
2625
+ strings,
2626
+ onChoose: (speed) => {
2627
+ changePreferences({ speed });
2628
+ setMenu(null);
2629
+ },
2630
+ onClose: () => setMenu(null)
2631
+ }) : null]
2632
+ }),
2633
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2634
+ className: "lk-iv-menu-anchor",
2635
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2636
+ type: "button",
2637
+ className: "lk-iv-button",
2638
+ "aria-label": strings.videoSettings,
2639
+ "aria-haspopup": "menu",
2640
+ "aria-expanded": menu === "settings",
2641
+ onClick: () => setMenu((open) => open === "settings" ? null : "settings"),
2642
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsIcon, {})
2643
+ }), menu === "settings" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsMenu, {
2644
+ preferences,
2645
+ tracks,
2646
+ primaryTrack,
2647
+ secondaryTrack,
2648
+ captionsFailed,
2649
+ onCaptionTrack: chooseCaptionTrack,
2650
+ onSecondaryTrack: chooseSecondaryTrack,
2651
+ strings,
2652
+ onChange: changePreferences,
2653
+ onShowShortcuts: () => {
2654
+ setMenu(null);
2655
+ setSheet(true);
2656
+ },
2657
+ onClose: () => setMenu(null)
2658
+ }) : null]
2659
+ }),
2660
+ pictureInPictureSupported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2661
+ type: "button",
2662
+ className: "lk-iv-button lk-iv-wide",
2663
+ "aria-label": strings.videoPictureInPicture,
2664
+ "aria-pressed": pictureInPicture,
2665
+ onClick: togglePictureInPicture,
2666
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PictureInPictureIcon, {})
2667
+ }) : null,
2668
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2669
+ type: "button",
2670
+ className: "lk-iv-button",
2671
+ "aria-label": fullscreen ? strings.videoExitFullscreen : strings.videoFullscreen,
2672
+ onClick: toggleFullscreen,
2673
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FullscreenIcon, { on: fullscreen })
2674
+ })
2675
+ ]
2676
+ })]
2677
+ })]
2678
+ }),
2679
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2680
+ className: "lk-iv-quiz",
2681
+ role: "dialog",
2682
+ "aria-modal": "false",
2683
+ "aria-labelledby": `${playerId}-quiz-title`,
2684
+ hidden: openQuiz === null,
2685
+ onKeyDown: (event) => {
2686
+ if (event.key === "Escape" && !event.defaultPrevented && !required && openQuiz !== null) {
2687
+ event.preventDefault();
2688
+ closeQuiz("skip");
2689
+ }
2690
+ },
2691
+ children: [
2692
+ openCue !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2693
+ className: "lk-iv-quiz-head",
2694
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
2695
+ id: `${playerId}-quiz-title`,
2696
+ ref: quizHeadingRef,
2697
+ tabIndex: -1,
2698
+ className: "lk-iv-quiz-title",
2699
+ children: [
2700
+ quizTitle(openCue),
2701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2702
+ className: "lk-iv-quiz-time",
2703
+ children: [" · ", clock(openCue.at)]
2704
+ }),
2705
+ required ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2706
+ className: "lk-iv-tag",
2707
+ children: strings.videoRequired
2708
+ })] }) : null
2709
+ ]
2710
+ }), openSlots.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2711
+ className: "lk-iv-steps",
2712
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2713
+ className: "lk-iv-steps-label",
2714
+ children: strings.videoQuestionProgress(step + 1, openSlots.length)
2715
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
2716
+ className: "lk-iv-steps-dots",
2717
+ children: openSlots.map((slot, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2718
+ type: "button",
2719
+ className: "lk-iv-step",
2720
+ "data-current": index === step || void 0,
2721
+ "data-answered": answered.has(slot.slotId) || void 0,
2722
+ "aria-current": index === step ? "step" : void 0,
2723
+ "aria-label": strings.videoQuestionStep(index + 1, answered.has(slot.slotId)),
2724
+ onClick: () => goToStep(index)
2725
+ }) }, slot.slotId))
2726
+ })]
2727
+ }) : null]
2728
+ }) : null,
2729
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2730
+ className: "lk-iv-quiz-body",
2731
+ children: cues.filter((cue) => mounted.has(cue.id)).map((cue) => (slotsByCue.get(cue.id) ?? []).map((slot, index) => {
2732
+ const shown = openQuiz?.cueId === cue.id && step === index;
2733
+ const place = places.get(slot.slotId);
2734
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuestionPane, {
2735
+ hidden: !shown,
2736
+ channel: channelFor(slot.slotId),
2737
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlotQuestion, {
2738
+ title: slot.activity.title,
2739
+ strings,
2740
+ render: props.renderQuestion,
2741
+ question: props.renderQuestion === void 0 ? void 0 : questionFor(slot, place, shown),
2742
+ sdk: () => sdkQuestion(slot, place),
2743
+ setHostPending: callsFor(place).setPending
2744
+ })
2745
+ }, slot.slotId);
2746
+ }))
2747
+ }),
2748
+ openCue !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2749
+ className: "lk-iv-quiz-foot",
2750
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2751
+ className: "lk-iv-quiz-foot-start",
2752
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2753
+ type: "button",
2754
+ className: "lk-iv-link",
2755
+ onClick: () => closeQuiz("rewatch"),
2756
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReplayIcon, {}), strings.videoRewatch]
2757
+ }), !required && !lastStep ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2758
+ type: "button",
2759
+ className: "lk-iv-link",
2760
+ onClick: () => closeQuiz("skip"),
2761
+ children: strings.videoSkipQuiz
2762
+ }) : null]
2763
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2764
+ className: "lk-iv-quiz-foot-end",
2765
+ children: [renderMode === "exam" && stepAnswered ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2766
+ className: "lk-iv-saved",
2767
+ role: "status",
2768
+ children: strings.videoAnswerSaved
2769
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2770
+ type: "button",
2771
+ className: stepAnswered || renderMode === "review" ? "lk-iv-action lk-iv-action-primary" : "lk-iv-action",
2772
+ "aria-disabled": !canAdvance || void 0,
2773
+ onClick: () => {
2774
+ if (!canAdvance) return;
2775
+ if (lastStep) closeQuiz("continue");
2776
+ else goToStep(step + 1);
2777
+ },
2778
+ children: [lastStep ? strings.videoContinue : strings.videoNextQuestion, lastStep ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayIcon, {}) : null]
2779
+ })]
2780
+ })]
2781
+ }) : null
2782
+ ]
2783
+ }),
2784
+ ended && openQuiz === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EndScreen, {
2785
+ idPrefix: playerId,
2786
+ renderMode,
2787
+ quizzes,
2788
+ answered: answeredCount,
2789
+ total: totalQuestions,
2790
+ percent: delivery.feedback ? percent : null,
2791
+ strings,
2792
+ onOpenQuiz: (id) => {
2793
+ const cue = cues.find((candidate) => candidate.id === id);
2794
+ if (cue !== void 0) openAt(cue);
2795
+ },
2796
+ onWatchAgain: togglePlay,
2797
+ pending: answerPending,
2798
+ onFinish: renderMode === "review" ? void 0 : () => {
2799
+ if (pendingRef.current.size > 0) {
2800
+ announce(strings.videoAnswerPending);
2801
+ return;
2802
+ }
2803
+ finish();
2804
+ }
2805
+ }) : null,
2806
+ sheet ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShortcutSheet, {
2807
+ strings,
2808
+ onClose: () => {
2809
+ setSheet(false);
2810
+ playButtonRef.current?.focus();
2811
+ }
2812
+ }) : null
2813
+ ]
2814
+ }),
2815
+ preferences.panel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContentsPanel, {
2816
+ idPrefix: playerId,
2817
+ tab: panelTab,
2818
+ onTab: setPanelTab,
2819
+ chapters,
2820
+ quizzes,
2821
+ cues: primaryCues,
2822
+ primaryLanguage: primaryTrack?.srclang,
2823
+ secondaryCues: secondaryTrack === void 0 ? NO_CUES : secondaryCues,
2824
+ secondaryLanguage: secondaryTrack?.srclang,
2825
+ activeCue: activeCueIndex,
2826
+ limit: spoilerLimit,
2827
+ strings,
2828
+ onSeek: seek,
2829
+ onOpenQuiz: (id) => {
2830
+ const cue = cues.find((candidate) => candidate.id === id);
2831
+ if (cue === void 0) return;
2832
+ if (renderMode !== "review" && limitSeek(timeline, videoRef.current?.currentTime ?? 0, cue.at, furthest.current, finished).at < cue.at - .25) {
2833
+ seek(cue.at);
2834
+ return;
2835
+ }
2836
+ openAt(cue);
2837
+ }
2838
+ }) : null,
2839
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2840
+ className: "lk-iv-live",
2841
+ "aria-live": "polite",
2842
+ "aria-atomic": "true",
2843
+ children: announcement
2844
+ }),
2845
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2846
+ ref: setPortalContainer,
2847
+ className: "lk-iv-portal"
2848
+ })
2849
+ ]
2850
+ });
2851
+ }
2852
+ /**
2853
+ * One question's content: the host's, when `renderQuestion` returns anything
2854
+ * but `undefined`, else the SDK's own component. Inside the boundary either
2855
+ * way, and `renderQuestion` is called inside it too, so a host that throws —
2856
+ * while rendering, or while deciding what to render — loses that one question
2857
+ * and never the video.
2858
+ */
2859
+ function SlotQuestion({ title, strings, ...content }) {
2860
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ActivityErrorBoundary.ActivityErrorBoundary, {
2861
+ ...title !== void 0 ? { activityTitle: title } : {},
2862
+ strings,
2863
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlotContent, { ...content })
2864
+ });
2865
+ }
2866
+ function SlotContent({ render, question, sdk, setHostPending }) {
2867
+ const hosted = render !== void 0 && question !== void 0 ? render(question) : void 0;
2868
+ const hosting = hosted !== void 0;
2869
+ (0, react.useEffect)(() => {
2870
+ if (!hosting) return;
2871
+ return () => setHostPending(false);
2872
+ }, [hosting, setHostPending]);
2873
+ return hosting ? hosted : sdk();
2874
+ }
2875
+ /**
2876
+ * One question of a quiz. Every question stays mounted once its quiz has
2877
+ * opened, so an answer survives stepping back or rewinding the video; hiding
2878
+ * it — another question, or the quiz closing — stops whatever it was
2879
+ * recording or playing, as a sequence's pane does.
2880
+ */
2881
+ function QuestionPane({ hidden, channel, children }) {
2882
+ const ref = (0, react.useRef)(null);
2883
+ const captureGroup = channel.captureGroup;
2884
+ (0, react.useEffect)(() => {
2885
+ if (!hidden) return;
2886
+ require_ReadAloud.stopCaptureGroup(captureGroup);
2887
+ for (const media of ref.current?.querySelectorAll("audio, video") ?? []) {
2888
+ const element = media;
2889
+ if (!element.paused) element.pause();
2890
+ }
2891
+ }, [hidden, captureGroup]);
2892
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2893
+ ref,
2894
+ className: "lk-iv-question",
2895
+ hidden,
2896
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_useAiHelp.SequenceSlotContext.Provider, {
2897
+ value: channel,
2898
+ children
2899
+ })
2900
+ });
2901
+ }
2902
+ /** How far the browser has downloaded past the playhead, in seconds from 0. */
2903
+ function bufferedAhead(video) {
2904
+ const ranges = video.buffered;
2905
+ if (ranges === void 0) return 0;
2906
+ for (let index = 0; index < ranges.length; index += 1) if (ranges.start(index) <= video.currentTime && video.currentTime <= ranges.end(index)) return ranges.end(index);
2907
+ return 0;
2908
+ }
2909
+ /** A quiz's marker state: its shape on the progress bar and its dot in the lists. */
2910
+ function markerState(quizSlots, answered, renderMode, outcomes) {
2911
+ if (renderMode === "review" && outcomes !== void 0) {
2912
+ const results = quizSlots.map((slot) => Object.hasOwn(outcomes, slot.slotId) ? outcomes[slot.slotId] : void 0);
2913
+ if (results.some((outcome) => outcome === void 0 || outcome.status === "deferred")) return "pending";
2914
+ const scored = results.filter((outcome) => outcome?.status === "scored");
2915
+ if (scored.length === results.length && scored.every((outcome) => outcome.passed)) return "correct";
2916
+ return scored.some((outcome) => outcome.passed) ? "partial" : "incorrect";
2917
+ }
2918
+ const count = quizSlots.filter((slot) => answered.has(slot.slotId)).length;
2919
+ if (count === quizSlots.length && count > 0) return "answered";
2920
+ if (count > 0) return "open";
2921
+ return "unreached";
2922
+ }
2923
+ //#endregion
2924
+ //#region src/components/InteractiveVideo/index.tsx
2925
+ /**
2926
+ * Public interactive video: the player wrapped in `ActivityErrorBoundary`, so a
2927
+ * render failure — the development-time schema check among them — degrades to
2928
+ * an accessible fallback instead of taking the host page down.
2929
+ *
2930
+ * A formative tool, for lessons and practice — not for official summative
2931
+ * exams, which `<ActivitySequence>` delivers.
2932
+ */
2933
+ function InteractiveVideo(props) {
2934
+ const strings = require_LkIntlProvider.useLkStrings(props.strings);
2935
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ActivityErrorBoundary.ActivityErrorBoundary, {
2936
+ ...props.group?.title !== void 0 ? { activityTitle: props.group.title } : {},
2937
+ strings,
2938
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InteractiveVideo$1, { ...props })
2939
+ });
2940
+ }
2941
+ //#endregion
2942
+ Object.defineProperty(exports, "InteractiveVideo", {
2943
+ enumerable: true,
2944
+ get: function() {
2945
+ return InteractiveVideo;
2946
+ }
2947
+ });
2948
+ Object.defineProperty(exports, "resolveCaptionTracks", {
2949
+ enumerable: true,
2950
+ get: function() {
2951
+ return resolveCaptionTracks;
2952
+ }
2953
+ });
2954
+
2955
+ //# sourceMappingURL=InteractiveVideo-BguGv-HA.cjs.map