analytica-frontend-lib 1.7.21 → 1.7.23
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.
- package/dist/AccessReportModal/index.js +3 -3
- package/dist/AccessReportModal/index.mjs +2 -2
- package/dist/ActivitiesHistory/index.js +49 -49
- package/dist/ActivitiesHistory/index.mjs +48 -48
- package/dist/ActivityCardQuestionBanks/index.js +49 -49
- package/dist/ActivityCardQuestionBanks/index.mjs +48 -48
- package/dist/ActivityCardQuestionPreview/index.js +49 -49
- package/dist/ActivityCardQuestionPreview/index.mjs +48 -48
- package/dist/ActivityCreate/ActivityCreateContent/index.js +49 -49
- package/dist/ActivityCreate/ActivityCreateContent/index.mjs +48 -48
- package/dist/ActivityCreate/index.js +49 -49
- package/dist/ActivityCreate/index.mjs +48 -48
- package/dist/ActivityDetails/index.js +49 -49
- package/dist/ActivityDetails/index.mjs +48 -48
- package/dist/ActivityFilters/index.js +49 -49
- package/dist/ActivityFilters/index.mjs +48 -48
- package/dist/ActivityPageLayout/index.js +49 -49
- package/dist/ActivityPageLayout/index.mjs +48 -48
- package/dist/ActivityPreview/index.js +49 -49
- package/dist/ActivityPreview/index.mjs +48 -48
- package/dist/AlertManager/index.js +49 -49
- package/dist/AlertManager/index.mjs +48 -48
- package/dist/AlertManagerView/index.js +3 -3
- package/dist/AlertManagerView/index.mjs +2 -2
- package/dist/AnnouncementModal/index.js +3 -3
- package/dist/AnnouncementModal/index.mjs +2 -2
- package/dist/AnnouncementsGate/index.js +4 -4
- package/dist/AnnouncementsGate/index.mjs +3 -3
- package/dist/AnswerKeyModal/index.js +3 -3
- package/dist/AnswerKeyModal/index.mjs +2 -2
- package/dist/AppHeader/index.js +6 -6
- package/dist/AppHeader/index.mjs +5 -5
- package/dist/CalendarCard/index.js +4 -4
- package/dist/CalendarCard/index.mjs +3 -3
- package/dist/Chat/index.js +3 -3
- package/dist/Chat/index.mjs +2 -2
- package/dist/CheckBox/index.d.ts.map +1 -1
- package/dist/CheckBox/index.js +2 -2
- package/dist/CheckBox/index.mjs +1 -1
- package/dist/CheckBoxGroup/index.js +49 -49
- package/dist/CheckBoxGroup/index.mjs +48 -48
- package/dist/ComparatorChart/ComparatorView/index.js +3 -3
- package/dist/ComparatorChart/ComparatorView/index.mjs +2 -2
- package/dist/CorrectActivityModal/index.js +7 -7
- package/dist/CorrectActivityModal/index.mjs +6 -6
- package/dist/DownloadButton/index.js +4 -4
- package/dist/DownloadButton/index.mjs +3 -3
- package/dist/DownloadModal/index.js +3 -3
- package/dist/DownloadModal/index.mjs +2 -2
- package/dist/DropdownMenu/index.js +3 -3
- package/dist/DropdownMenu/index.mjs +2 -2
- package/dist/EnemClassroomStartModal/index.d.ts.map +1 -1
- package/dist/EnemClassroomStartModal/index.js +7 -7
- package/dist/EnemClassroomStartModal/index.mjs +6 -6
- package/dist/EnemMoment/index.js +57 -57
- package/dist/EnemMoment/index.mjs +51 -51
- package/dist/EnemMomentStudentModal/index.js +50 -50
- package/dist/EnemMomentStudentModal/index.mjs +48 -48
- package/dist/ExamDetailsLayout/ExamDetailsPage/index.js +49 -49
- package/dist/ExamDetailsLayout/ExamDetailsPage/index.mjs +48 -48
- package/dist/ExamPageLayout/index.js +49 -49
- package/dist/ExamPageLayout/index.mjs +48 -48
- package/dist/Forum/index.js +5 -5
- package/dist/Forum/index.mjs +4 -4
- package/dist/KnowledgePerformanceCard/index.js +51 -51
- package/dist/KnowledgePerformanceCard/index.mjs +50 -50
- package/dist/LessonCardPreview/index.js +49 -49
- package/dist/LessonCardPreview/index.mjs +48 -48
- package/dist/LessonMediaSections/index.js +49 -49
- package/dist/LessonMediaSections/index.mjs +48 -48
- package/dist/LessonsCatalog/index.js +49 -49
- package/dist/LessonsCatalog/index.mjs +48 -48
- package/dist/Modal/index.d.ts.map +1 -1
- package/dist/Modal/index.js +2 -2
- package/dist/Modal/index.mjs +1 -1
- package/dist/MultiCheckSelect/index.js +5 -5
- package/dist/MultiCheckSelect/index.mjs +4 -4
- package/dist/MultiSearchSelect/index.js +3 -3
- package/dist/MultiSearchSelect/index.mjs +2 -2
- package/dist/MultipleChoice/index.js +3 -3
- package/dist/MultipleChoice/index.mjs +2 -2
- package/dist/NotificationCard/index.js +4 -4
- package/dist/NotificationCard/index.mjs +3 -3
- package/dist/PerformanceReportModal/index.js +3 -3
- package/dist/PerformanceReportModal/index.mjs +2 -2
- package/dist/PrintableUsersTable/index.js +49 -49
- package/dist/PrintableUsersTable/index.mjs +48 -48
- package/dist/ProgressModal/index.js +3 -3
- package/dist/ProgressModal/index.mjs +2 -2
- package/dist/QuestionsPerformanceCard/index.js +50 -50
- package/dist/QuestionsPerformanceCard/index.mjs +49 -49
- package/dist/Quiz/index.js +6 -6
- package/dist/Quiz/index.mjs +5 -5
- package/dist/RecommendedLessonCreate/index.js +49 -49
- package/dist/RecommendedLessonCreate/index.mjs +48 -48
- package/dist/RecommendedLessonDetails/index.js +7 -7
- package/dist/RecommendedLessonDetails/index.mjs +6 -6
- package/dist/RecommendedLessonsHistory/index.js +49 -49
- package/dist/RecommendedLessonsHistory/index.mjs +48 -48
- package/dist/ReportDetailModal/index.js +4 -4
- package/dist/ReportDetailModal/index.mjs +3 -3
- package/dist/RichEditor/index.js +5 -5
- package/dist/RichEditor/index.mjs +4 -4
- package/dist/{RichEditorCore-6MTYSSKS.mjs → RichEditorCore-2S5YUAOI.mjs} +5 -5
- package/dist/{RichEditorCore-KDARBEK4.js → RichEditorCore-N7A2445B.js} +16 -16
- package/dist/{RichEditorCore-KDARBEK4.js.map → RichEditorCore-N7A2445B.js.map} +1 -1
- package/dist/Search/index.js +4 -4
- package/dist/Search/index.mjs +3 -3
- package/dist/SendActivityModal/SendActivityModal.js +49 -49
- package/dist/SendActivityModal/SendActivityModal.mjs +48 -48
- package/dist/SendActivityModal/index.js +49 -49
- package/dist/SendActivityModal/index.mjs +48 -48
- package/dist/SendLessonModal/index.js +49 -49
- package/dist/SendLessonModal/index.mjs +48 -48
- package/dist/SimulatedPerformance/SimulatedPerformanceView/index.js +49 -49
- package/dist/SimulatedPerformance/SimulatedPerformanceView/index.mjs +48 -48
- package/dist/SimulatedPerformance/useSimulatedPerformance/index.js +5 -5
- package/dist/SimulatedPerformance/useSimulatedPerformance/index.mjs +4 -4
- package/dist/SimulatedStudentSimulationsModal/index.js +6 -6
- package/dist/SimulatedStudentSimulationsModal/index.mjs +5 -5
- package/dist/SimulationsPage/index.js +49 -49
- package/dist/SimulationsPage/index.mjs +48 -48
- package/dist/StudentLessonProgressModal/index.js +3 -3
- package/dist/StudentLessonProgressModal/index.mjs +2 -2
- package/dist/StudentPerformanceDetailsModal/index.js +5 -5
- package/dist/StudentPerformanceDetailsModal/index.mjs +4 -4
- package/dist/Support/TicketModal/index.js +3 -3
- package/dist/Support/TicketModal/index.mjs +2 -2
- package/dist/Support/index.js +4 -4
- package/dist/Support/index.mjs +3 -3
- package/dist/SupportChatComposer/index.js +3 -3
- package/dist/SupportChatComposer/index.mjs +2 -2
- package/dist/SupportChatEvent/index.js +3 -3
- package/dist/SupportChatEvent/index.mjs +2 -2
- package/dist/SupportChatMessage/index.js +3 -3
- package/dist/SupportChatMessage/index.mjs +2 -2
- package/dist/SupportChatWidget/index.js +3 -3
- package/dist/SupportChatWidget/index.mjs +2 -2
- package/dist/SupportConcludeModal/index.js +4 -4
- package/dist/SupportConcludeModal/index.mjs +3 -3
- package/dist/SupportConversationHeader/index.js +4 -4
- package/dist/SupportConversationHeader/index.mjs +3 -3
- package/dist/SupportConversationMessages/index.js +5 -5
- package/dist/SupportConversationMessages/index.mjs +4 -4
- package/dist/SupportTicketConversation/index.js +12 -12
- package/dist/SupportTicketConversation/index.mjs +11 -11
- package/dist/TableProvider/index.js +49 -49
- package/dist/TableProvider/index.mjs +48 -48
- package/dist/UnifiedDraftModelPage/index.js +49 -49
- package/dist/UnifiedDraftModelPage/index.mjs +48 -48
- package/dist/UnifiedHistoryPage/index.js +49 -49
- package/dist/UnifiedHistoryPage/index.mjs +48 -48
- package/dist/VideoPlayer/index.js +5 -5
- package/dist/VideoPlayer/index.mjs +4 -4
- package/dist/Whiteboard/index.js +3 -3
- package/dist/Whiteboard/index.mjs +2 -2
- package/dist/{chunk-B6QOBHZM.mjs → chunk-24BEZK43.mjs} +2 -2
- package/dist/{chunk-CYSODZGX.js → chunk-27Y6V3GM.js} +169 -66
- package/dist/chunk-27Y6V3GM.js.map +1 -0
- package/dist/{chunk-OQTXTDSK.mjs → chunk-2OJ3QSLU.mjs} +3 -3
- package/dist/{chunk-VDUO65VK.mjs → chunk-3EGJJDIM.mjs} +2 -2
- package/dist/{chunk-VDUO65VK.mjs.map → chunk-3EGJJDIM.mjs.map} +1 -1
- package/dist/{chunk-4OUBPEVS.mjs → chunk-3HKLGNUE.mjs} +3 -3
- package/dist/{chunk-YA7XLH7D.js → chunk-3MFACEZ2.js} +3 -3
- package/dist/{chunk-YA7XLH7D.js.map → chunk-3MFACEZ2.js.map} +1 -1
- package/dist/{chunk-IHBWJM4C.mjs → chunk-3Q7RJKUP.mjs} +2 -2
- package/dist/{chunk-PY6WD6SB.mjs → chunk-3WRB4WCD.mjs} +4 -4
- package/dist/{chunk-4RHCBMMP.js → chunk-46VHAL4S.js} +3 -3
- package/dist/{chunk-4RHCBMMP.js.map → chunk-46VHAL4S.js.map} +1 -1
- package/dist/{chunk-POT3ALY3.js → chunk-5DINXU6W.js} +1 -1
- package/dist/{chunk-POT3ALY3.js.map → chunk-5DINXU6W.js.map} +1 -1
- package/dist/{chunk-L52NFNFK.mjs → chunk-5VC4BK65.mjs} +3 -3
- package/dist/{chunk-B67RLIUM.js → chunk-72D7G6YQ.js} +3 -3
- package/dist/{chunk-B67RLIUM.js.map → chunk-72D7G6YQ.js.map} +1 -1
- package/dist/{chunk-C65DG2RI.mjs → chunk-7FSBWUOM.mjs} +2 -2
- package/dist/{chunk-QWMFWYII.js → chunk-7IZNLARX.js} +3 -3
- package/dist/{chunk-QWMFWYII.js.map → chunk-7IZNLARX.js.map} +1 -1
- package/dist/{chunk-ZCBT2BUZ.js → chunk-7RFHXFFV.js} +13 -13
- package/dist/{chunk-ZCBT2BUZ.js.map → chunk-7RFHXFFV.js.map} +1 -1
- package/dist/{chunk-EMWX6IKP.js → chunk-7VJP5D26.js} +13 -13
- package/dist/{chunk-EMWX6IKP.js.map → chunk-7VJP5D26.js.map} +1 -1
- package/dist/{chunk-HCQJR5CE.mjs → chunk-7ZWEBNFB.mjs} +2 -2
- package/dist/{chunk-3RHNJUXU.mjs → chunk-ACSEJOW6.mjs} +2 -2
- package/dist/{chunk-WQZHUW7C.mjs → chunk-ANFHLSDB.mjs} +3 -3
- package/dist/{chunk-H6NQOLQY.mjs → chunk-ANFLWCVD.mjs} +2 -2
- package/dist/{chunk-BKBDQXZX.js → chunk-AOYPUZRO.js} +3 -3
- package/dist/{chunk-BKBDQXZX.js.map → chunk-AOYPUZRO.js.map} +1 -1
- package/dist/{chunk-5WS6IENQ.mjs → chunk-AU2HLQDJ.mjs} +2 -2
- package/dist/{chunk-P6FX3NT6.mjs → chunk-AZGDHAP5.mjs} +2 -2
- package/dist/{chunk-NCGO7FPB.js → chunk-AZKNYREH.js} +8 -8
- package/dist/{chunk-NCGO7FPB.js.map → chunk-AZKNYREH.js.map} +1 -1
- package/dist/{chunk-R6MTPYYW.js → chunk-AZTBNERQ.js} +3 -3
- package/dist/{chunk-R6MTPYYW.js.map → chunk-AZTBNERQ.js.map} +1 -1
- package/dist/{chunk-NPB2JTYC.mjs → chunk-BO3QVQ37.mjs} +4 -4
- package/dist/{chunk-2WI4Q2I2.mjs → chunk-BPMWRFSI.mjs} +4 -4
- package/dist/{chunk-TEW33GLR.mjs → chunk-BXN3VO6I.mjs} +2 -2
- package/dist/{chunk-7ZB3YHF2.mjs → chunk-C6LC5LD5.mjs} +1 -1
- package/dist/{chunk-7ZB3YHF2.mjs.map → chunk-C6LC5LD5.mjs.map} +1 -1
- package/dist/{chunk-5HONWYQA.js → chunk-C7ILTLT5.js} +3 -3
- package/dist/{chunk-5HONWYQA.js.map → chunk-C7ILTLT5.js.map} +1 -1
- package/dist/{chunk-OES54VU6.mjs → chunk-CFV2OZ3P.mjs} +116 -13
- package/dist/chunk-CFV2OZ3P.mjs.map +1 -0
- package/dist/{chunk-4OVIHZ3I.js → chunk-CGXWWPEA.js} +6 -6
- package/dist/{chunk-4OVIHZ3I.js.map → chunk-CGXWWPEA.js.map} +1 -1
- package/dist/{chunk-MNU45FM3.js → chunk-COT4KW3H.js} +2 -2
- package/dist/{chunk-MNU45FM3.js.map → chunk-COT4KW3H.js.map} +1 -1
- package/dist/{chunk-JDQUXG75.js → chunk-CVDNCU6L.js} +3 -3
- package/dist/{chunk-JDQUXG75.js.map → chunk-CVDNCU6L.js.map} +1 -1
- package/dist/{chunk-HBZHWSMZ.mjs → chunk-D46PJSQ7.mjs} +13 -13
- package/dist/{chunk-ARYIJ4RU.js → chunk-DDORKDQO.js} +3 -3
- package/dist/{chunk-ARYIJ4RU.js.map → chunk-DDORKDQO.js.map} +1 -1
- package/dist/{chunk-7YQTDJFA.js → chunk-DKXSR2V3.js} +3 -3
- package/dist/{chunk-7YQTDJFA.js.map → chunk-DKXSR2V3.js.map} +1 -1
- package/dist/{chunk-I6EAPPBL.mjs → chunk-DZMYIOKZ.mjs} +5 -5
- package/dist/{chunk-SKM7NFPY.js → chunk-EPXAFJVW.js} +6 -6
- package/dist/{chunk-SKM7NFPY.js.map → chunk-EPXAFJVW.js.map} +1 -1
- package/dist/{chunk-AYYMS23L.js → chunk-ESIQ2GB4.js} +7 -7
- package/dist/{chunk-AYYMS23L.js.map → chunk-ESIQ2GB4.js.map} +1 -1
- package/dist/{chunk-NU2WNYOD.js → chunk-EW7HGUFV.js} +87 -87
- package/dist/{chunk-NU2WNYOD.js.map → chunk-EW7HGUFV.js.map} +1 -1
- package/dist/{chunk-I4QGQMXI.mjs → chunk-EXYXZFNK.mjs} +2 -2
- package/dist/{chunk-RBJAO57A.js → chunk-FAAR4XC5.js} +3 -3
- package/dist/{chunk-RBJAO57A.js.map → chunk-FAAR4XC5.js.map} +1 -1
- package/dist/{chunk-N2VNE3ON.js → chunk-FDTDJDZI.js} +13 -13
- package/dist/{chunk-N2VNE3ON.js.map → chunk-FDTDJDZI.js.map} +1 -1
- package/dist/{chunk-SU5Q64TW.mjs → chunk-FVCTKS33.mjs} +6 -5
- package/dist/chunk-FVCTKS33.mjs.map +1 -0
- package/dist/{chunk-AVHZHWI3.js → chunk-GBIFSFWJ.js} +15 -15
- package/dist/{chunk-AVHZHWI3.js.map → chunk-GBIFSFWJ.js.map} +1 -1
- package/dist/{chunk-KSUPPAKH.js → chunk-GIQSHTB5.js} +9 -9
- package/dist/{chunk-KSUPPAKH.js.map → chunk-GIQSHTB5.js.map} +1 -1
- package/dist/{chunk-4QBKD4JN.js → chunk-GUUJYGXF.js} +3 -3
- package/dist/{chunk-4QBKD4JN.js.map → chunk-GUUJYGXF.js.map} +1 -1
- package/dist/{chunk-M4O46VDY.mjs → chunk-GX2PKLN2.mjs} +2 -2
- package/dist/{chunk-5BUTINIM.mjs → chunk-HLVLSWKD.mjs} +3 -3
- package/dist/{chunk-FU23WOTY.js → chunk-HSV5VZ4Z.js} +6 -6
- package/dist/{chunk-FU23WOTY.js.map → chunk-HSV5VZ4Z.js.map} +1 -1
- package/dist/{chunk-MMS2ELDK.mjs → chunk-IIQCS3A2.mjs} +2 -2
- package/dist/{chunk-NKWLCZUZ.mjs → chunk-JGGUMHM6.mjs} +2 -2
- package/dist/{chunk-HT2GREFH.js → chunk-JLD46DL7.js} +11 -11
- package/dist/{chunk-HT2GREFH.js.map → chunk-JLD46DL7.js.map} +1 -1
- package/dist/{chunk-NVL5TKWD.mjs → chunk-JYFLOQYT.mjs} +2 -2
- package/dist/{chunk-DWGDEUEW.mjs → chunk-KG563P7F.mjs} +5 -5
- package/dist/{chunk-TCIC5556.mjs → chunk-KH4TEDGD.mjs} +2 -2
- package/dist/{chunk-23PAYKWB.js → chunk-KM27ZDHZ.js} +2 -2
- package/dist/{chunk-23PAYKWB.js.map → chunk-KM27ZDHZ.js.map} +1 -1
- package/dist/{chunk-XZUMD3CB.mjs → chunk-LODDYDTP.mjs} +2 -2
- package/dist/{chunk-XZUMD3CB.mjs.map → chunk-LODDYDTP.mjs.map} +1 -1
- package/dist/{chunk-E6CGJBID.mjs → chunk-LUHKMY56.mjs} +2 -2
- package/dist/{chunk-TVLEUNKE.mjs → chunk-LZP5VZCO.mjs} +2 -2
- package/dist/{chunk-EHNEBXS7.mjs → chunk-M6DXZ2FM.mjs} +2 -2
- package/dist/{chunk-B4C5DWAP.mjs → chunk-MMMJDO75.mjs} +2 -2
- package/dist/{chunk-AFCWMJQO.js → chunk-MOUPNP53.js} +3 -3
- package/dist/{chunk-AFCWMJQO.js.map → chunk-MOUPNP53.js.map} +1 -1
- package/dist/{chunk-36KFIZ43.js → chunk-MYPUZ6EG.js} +3 -3
- package/dist/{chunk-36KFIZ43.js.map → chunk-MYPUZ6EG.js.map} +1 -1
- package/dist/{chunk-6JHIKIPA.js → chunk-NUGLTWEA.js} +23 -23
- package/dist/{chunk-6JHIKIPA.js.map → chunk-NUGLTWEA.js.map} +1 -1
- package/dist/{chunk-KWXOYHYK.mjs → chunk-P7MORXSE.mjs} +2 -2
- package/dist/{chunk-YG4IZ5Q3.mjs → chunk-Q2OSEROT.mjs} +2 -2
- package/dist/{chunk-L5SXG6T6.mjs → chunk-QQKOHKO2.mjs} +3 -3
- package/dist/{chunk-SADSJ2MV.js → chunk-QVGUYPXO.js} +3 -3
- package/dist/{chunk-SADSJ2MV.js.map → chunk-QVGUYPXO.js.map} +1 -1
- package/dist/{chunk-K45PTE7F.mjs → chunk-RBKQXT73.mjs} +4 -4
- package/dist/{chunk-JZFISWTP.js → chunk-RIN2IN7E.js} +2 -2
- package/dist/{chunk-JZFISWTP.js.map → chunk-RIN2IN7E.js.map} +1 -1
- package/dist/{chunk-BXLAOYAG.mjs → chunk-RIUVPTG7.mjs} +2 -2
- package/dist/{chunk-QVB54LBS.mjs → chunk-RNZGX6MX.mjs} +3 -3
- package/dist/{chunk-VSVLIRO2.js → chunk-RXEU77VN.js} +15 -15
- package/dist/{chunk-VSVLIRO2.js.map → chunk-RXEU77VN.js.map} +1 -1
- package/dist/{chunk-ZRIYCU2V.js → chunk-S2BZPPAI.js} +5 -5
- package/dist/{chunk-ZRIYCU2V.js.map → chunk-S2BZPPAI.js.map} +1 -1
- package/dist/{chunk-MES2BP4T.mjs → chunk-SGL5IUCV.mjs} +2 -2
- package/dist/{chunk-7LS5UQCQ.mjs → chunk-STKUPDHG.mjs} +2 -2
- package/dist/{chunk-PPYCCT2K.js → chunk-SWDVSCQJ.js} +9 -8
- package/dist/chunk-SWDVSCQJ.js.map +1 -0
- package/dist/{chunk-JCA76TCX.mjs → chunk-SZKLJOTN.mjs} +2 -2
- package/dist/{chunk-F5WR6XZZ.js → chunk-T73HPYF2.js} +3 -3
- package/dist/{chunk-F5WR6XZZ.js.map → chunk-T73HPYF2.js.map} +1 -1
- package/dist/{chunk-ZB6HDDE4.js → chunk-TQO37PVW.js} +14 -14
- package/dist/{chunk-ZB6HDDE4.js.map → chunk-TQO37PVW.js.map} +1 -1
- package/dist/{chunk-VKQ5AJDT.mjs → chunk-UENZQF2Y.mjs} +2 -2
- package/dist/{chunk-DCG7EUBC.js → chunk-UWLW5YVB.js} +6 -6
- package/dist/{chunk-DCG7EUBC.js.map → chunk-UWLW5YVB.js.map} +1 -1
- package/dist/{chunk-37AJ2GVO.mjs → chunk-UXJIZDXQ.mjs} +2 -2
- package/dist/{chunk-PLLPFCN3.js → chunk-UXMDPRAI.js} +14 -14
- package/dist/{chunk-PLLPFCN3.js.map → chunk-UXMDPRAI.js.map} +1 -1
- package/dist/{chunk-7BOTKRY6.mjs → chunk-V55DG5B4.mjs} +2 -2
- package/dist/{chunk-YRTIDZTN.js → chunk-VHEDMUAW.js} +4 -4
- package/dist/{chunk-YRTIDZTN.js.map → chunk-VHEDMUAW.js.map} +1 -1
- package/dist/{chunk-4RD55E4R.js → chunk-VWLWSO4J.js} +3 -3
- package/dist/{chunk-4RD55E4R.js.map → chunk-VWLWSO4J.js.map} +1 -1
- package/dist/{chunk-VJQIKFMG.mjs → chunk-W3CWEZDB.mjs} +3 -3
- package/dist/{chunk-JM6SLU7D.js → chunk-W7NNH4VY.js} +6 -6
- package/dist/{chunk-JM6SLU7D.js.map → chunk-W7NNH4VY.js.map} +1 -1
- package/dist/{chunk-VQ7MSYIH.js → chunk-WUN5NHZE.js} +4 -4
- package/dist/{chunk-VQ7MSYIH.js.map → chunk-WUN5NHZE.js.map} +1 -1
- package/dist/{chunk-AAU7ABJN.js → chunk-WWTRSQ3X.js} +3 -3
- package/dist/{chunk-AAU7ABJN.js.map → chunk-WWTRSQ3X.js.map} +1 -1
- package/dist/{chunk-BR6D2TGF.js → chunk-WZOA2PVJ.js} +3 -3
- package/dist/{chunk-BR6D2TGF.js.map → chunk-WZOA2PVJ.js.map} +1 -1
- package/dist/{chunk-TZKR5Z4J.mjs → chunk-XJF2F36N.mjs} +2 -2
- package/dist/{chunk-P6ACVNQK.mjs → chunk-YD2XBEKO.mjs} +2 -2
- package/dist/{chunk-LMXWIPGR.js → chunk-YWPLHFAK.js} +3 -3
- package/dist/{chunk-LMXWIPGR.js.map → chunk-YWPLHFAK.js.map} +1 -1
- package/dist/{chunk-DCEWWHV5.mjs → chunk-Z3HGYPH7.mjs} +2 -2
- package/dist/{chunk-Q3PKW6TH.js → chunk-ZCV3HWI2.js} +6 -6
- package/dist/{chunk-Q3PKW6TH.js.map → chunk-ZCV3HWI2.js.map} +1 -1
- package/dist/{chunk-OSA3YVDZ.mjs → chunk-ZF46DCAC.mjs} +2 -2
- package/dist/{chunk-5T6IDXSB.js → chunk-ZO2GA23I.js} +3 -3
- package/dist/{chunk-5T6IDXSB.js.map → chunk-ZO2GA23I.js.map} +1 -1
- package/dist/{chunk-ZN3763VZ.js → chunk-ZQLW4EJB.js} +9 -9
- package/dist/{chunk-ZN3763VZ.js.map → chunk-ZQLW4EJB.js.map} +1 -1
- package/dist/{chunk-CM5R4SPF.js → chunk-ZQQJTO6T.js} +3 -3
- package/dist/{chunk-CM5R4SPF.js.map → chunk-ZQQJTO6T.js.map} +1 -1
- package/dist/{chunk-SEL4QDCO.js → chunk-ZSXSAE3D.js} +3 -3
- package/dist/{chunk-SEL4QDCO.js.map → chunk-ZSXSAE3D.js.map} +1 -1
- package/dist/components/SupportChatWidget/guestSupportChatAdapter.d.ts +6 -1
- package/dist/components/SupportChatWidget/guestSupportChatAdapter.d.ts.map +1 -1
- package/dist/hooks/useAppContent/index.js +49 -49
- package/dist/hooks/useAppContent/index.mjs +48 -48
- package/dist/hooks/useSupportChat.d.ts +2 -0
- package/dist/hooks/useSupportChat.d.ts.map +1 -1
- package/dist/index.js +49 -49
- package/dist/index.mjs +48 -48
- package/dist/types/supportChat/index.d.ts +14 -0
- package/dist/types/supportChat/index.d.ts.map +1 -1
- package/dist/types/supportChat/index.js +2 -2
- package/dist/types/supportChat/index.mjs +1 -1
- package/dist/types/supportChat.d.ts +14 -0
- package/dist/types/supportChat.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-CYSODZGX.js.map +0 -1
- package/dist/chunk-OES54VU6.mjs.map +0 -1
- package/dist/chunk-PPYCCT2K.js.map +0 -1
- package/dist/chunk-SU5Q64TW.mjs.map +0 -1
- /package/dist/{RichEditorCore-6MTYSSKS.mjs.map → RichEditorCore-2S5YUAOI.mjs.map} +0 -0
- /package/dist/{chunk-B6QOBHZM.mjs.map → chunk-24BEZK43.mjs.map} +0 -0
- /package/dist/{chunk-OQTXTDSK.mjs.map → chunk-2OJ3QSLU.mjs.map} +0 -0
- /package/dist/{chunk-4OUBPEVS.mjs.map → chunk-3HKLGNUE.mjs.map} +0 -0
- /package/dist/{chunk-IHBWJM4C.mjs.map → chunk-3Q7RJKUP.mjs.map} +0 -0
- /package/dist/{chunk-PY6WD6SB.mjs.map → chunk-3WRB4WCD.mjs.map} +0 -0
- /package/dist/{chunk-L52NFNFK.mjs.map → chunk-5VC4BK65.mjs.map} +0 -0
- /package/dist/{chunk-C65DG2RI.mjs.map → chunk-7FSBWUOM.mjs.map} +0 -0
- /package/dist/{chunk-HCQJR5CE.mjs.map → chunk-7ZWEBNFB.mjs.map} +0 -0
- /package/dist/{chunk-3RHNJUXU.mjs.map → chunk-ACSEJOW6.mjs.map} +0 -0
- /package/dist/{chunk-WQZHUW7C.mjs.map → chunk-ANFHLSDB.mjs.map} +0 -0
- /package/dist/{chunk-H6NQOLQY.mjs.map → chunk-ANFLWCVD.mjs.map} +0 -0
- /package/dist/{chunk-5WS6IENQ.mjs.map → chunk-AU2HLQDJ.mjs.map} +0 -0
- /package/dist/{chunk-P6FX3NT6.mjs.map → chunk-AZGDHAP5.mjs.map} +0 -0
- /package/dist/{chunk-NPB2JTYC.mjs.map → chunk-BO3QVQ37.mjs.map} +0 -0
- /package/dist/{chunk-2WI4Q2I2.mjs.map → chunk-BPMWRFSI.mjs.map} +0 -0
- /package/dist/{chunk-TEW33GLR.mjs.map → chunk-BXN3VO6I.mjs.map} +0 -0
- /package/dist/{chunk-HBZHWSMZ.mjs.map → chunk-D46PJSQ7.mjs.map} +0 -0
- /package/dist/{chunk-I6EAPPBL.mjs.map → chunk-DZMYIOKZ.mjs.map} +0 -0
- /package/dist/{chunk-I4QGQMXI.mjs.map → chunk-EXYXZFNK.mjs.map} +0 -0
- /package/dist/{chunk-M4O46VDY.mjs.map → chunk-GX2PKLN2.mjs.map} +0 -0
- /package/dist/{chunk-5BUTINIM.mjs.map → chunk-HLVLSWKD.mjs.map} +0 -0
- /package/dist/{chunk-MMS2ELDK.mjs.map → chunk-IIQCS3A2.mjs.map} +0 -0
- /package/dist/{chunk-NKWLCZUZ.mjs.map → chunk-JGGUMHM6.mjs.map} +0 -0
- /package/dist/{chunk-NVL5TKWD.mjs.map → chunk-JYFLOQYT.mjs.map} +0 -0
- /package/dist/{chunk-DWGDEUEW.mjs.map → chunk-KG563P7F.mjs.map} +0 -0
- /package/dist/{chunk-TCIC5556.mjs.map → chunk-KH4TEDGD.mjs.map} +0 -0
- /package/dist/{chunk-E6CGJBID.mjs.map → chunk-LUHKMY56.mjs.map} +0 -0
- /package/dist/{chunk-TVLEUNKE.mjs.map → chunk-LZP5VZCO.mjs.map} +0 -0
- /package/dist/{chunk-EHNEBXS7.mjs.map → chunk-M6DXZ2FM.mjs.map} +0 -0
- /package/dist/{chunk-B4C5DWAP.mjs.map → chunk-MMMJDO75.mjs.map} +0 -0
- /package/dist/{chunk-KWXOYHYK.mjs.map → chunk-P7MORXSE.mjs.map} +0 -0
- /package/dist/{chunk-YG4IZ5Q3.mjs.map → chunk-Q2OSEROT.mjs.map} +0 -0
- /package/dist/{chunk-L5SXG6T6.mjs.map → chunk-QQKOHKO2.mjs.map} +0 -0
- /package/dist/{chunk-K45PTE7F.mjs.map → chunk-RBKQXT73.mjs.map} +0 -0
- /package/dist/{chunk-BXLAOYAG.mjs.map → chunk-RIUVPTG7.mjs.map} +0 -0
- /package/dist/{chunk-QVB54LBS.mjs.map → chunk-RNZGX6MX.mjs.map} +0 -0
- /package/dist/{chunk-MES2BP4T.mjs.map → chunk-SGL5IUCV.mjs.map} +0 -0
- /package/dist/{chunk-7LS5UQCQ.mjs.map → chunk-STKUPDHG.mjs.map} +0 -0
- /package/dist/{chunk-JCA76TCX.mjs.map → chunk-SZKLJOTN.mjs.map} +0 -0
- /package/dist/{chunk-VKQ5AJDT.mjs.map → chunk-UENZQF2Y.mjs.map} +0 -0
- /package/dist/{chunk-37AJ2GVO.mjs.map → chunk-UXJIZDXQ.mjs.map} +0 -0
- /package/dist/{chunk-7BOTKRY6.mjs.map → chunk-V55DG5B4.mjs.map} +0 -0
- /package/dist/{chunk-VJQIKFMG.mjs.map → chunk-W3CWEZDB.mjs.map} +0 -0
- /package/dist/{chunk-TZKR5Z4J.mjs.map → chunk-XJF2F36N.mjs.map} +0 -0
- /package/dist/{chunk-P6ACVNQK.mjs.map → chunk-YD2XBEKO.mjs.map} +0 -0
- /package/dist/{chunk-DCEWWHV5.mjs.map → chunk-Z3HGYPH7.mjs.map} +0 -0
- /package/dist/{chunk-OSA3YVDZ.mjs.map → chunk-ZF46DCAC.mjs.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-SADSJ2MV.js","../src/components/VideoPlayer/VideoPlayer.tsx","../src/components/VideoPlayer/useCaptionPosition.ts"],"names":["useEffect"],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-SADSJ2MV.js","sourcesContent":[null,"import {\n useRef,\n useState,\n useEffect,\n useCallback,\n MouseEvent,\n KeyboardEvent,\n FocusEvent,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { PlayIcon } from '@phosphor-icons/react/dist/csr/Play';\nimport { PauseIcon } from '@phosphor-icons/react/dist/csr/Pause';\nimport { SpeakerHighIcon } from '@phosphor-icons/react/dist/csr/SpeakerHigh';\nimport { SpeakerSlashIcon } from '@phosphor-icons/react/dist/csr/SpeakerSlash';\nimport { ArrowsOutSimpleIcon } from '@phosphor-icons/react/dist/csr/ArrowsOutSimple';\nimport { ArrowsInSimpleIcon } from '@phosphor-icons/react/dist/csr/ArrowsInSimple';\nimport { ClosedCaptioningIcon } from '@phosphor-icons/react/dist/csr/ClosedCaptioning';\nimport { DotsThreeVerticalIcon } from '@phosphor-icons/react/dist/csr/DotsThreeVertical';\nimport { cn } from '../../utils/utils';\nimport IconButton from '../IconButton/IconButton';\nimport Text from '../Text/Text';\nimport { useMobile } from '../../hooks/useMobile';\nimport {\n UNMUTE_FALLBACK_VOLUME,\n useMediaVolumePreference,\n} from '../../store/mediaPreferencesStore';\nimport DownloadButton, {\n DownloadContent,\n} from '../DownloadButton/DownloadButton';\nimport { useCaptionPosition } from './useCaptionPosition';\n\n// Constants for timeout durations\nconst CONTROLS_HIDE_TIMEOUT = 3000; // 3 seconds for normal control hiding\nconst LEAVE_HIDE_TIMEOUT = 1000; // 1 second when mouse leaves the video area\nconst INIT_DELAY = 100; // ms delay to initialize controls on mount\n\n/**\n * VideoPlayer component props interface\n */\ninterface VideoPlayerProps {\n /** Video source URL */\n src: string;\n /** Video poster/thumbnail URL */\n poster?: string;\n /** Subtitles URL */\n subtitles?: string;\n /** Video title */\n title?: string;\n /** Video subtitle/description */\n subtitle?: string;\n /**\n * Hides the header bar that carries the title and subtitle, for designs\n * where the video sits under a heading of its own. The title is still used\n * to label the player for screen readers.\n */\n hideHeader?: boolean;\n /** Initial playback time in seconds */\n initialTime?: number;\n /**\n * Callback fired when video time updates. Receives the current second and\n * the media's duration, which is only known once metadata has loaded (0\n * before that), so consumers that store it must treat it as optional.\n */\n onTimeUpdate?: (seconds: number, duration?: number) => void;\n /** Callback fired with progress percentage (0-100) */\n onProgress?: (progress: number) => void;\n /** Callback fired when video completes (>95% watched) */\n onVideoComplete?: () => void;\n /** Additional CSS classes */\n className?: string;\n /** Auto-save progress to localStorage */\n autoSave?: boolean;\n /** localStorage key for saving progress */\n storageKey?: string;\n /** User ID used to scope localStorage keys — prevents progress leaking between users */\n userId?: string;\n /** Download content URLs for lesson materials */\n downloadContent?: DownloadContent;\n /** Show download button in header */\n showDownloadButton?: boolean;\n /** Callback fired when download starts */\n onDownloadStart?: (contentType: string) => void;\n /** Callback fired when download completes */\n onDownloadComplete?: (contentType: string) => void;\n /** Callback fired when download fails */\n onDownloadError?: (contentType: string, error: Error) => void;\n}\n\n/**\n * Format seconds to MM:SS display format\n * @param seconds - Time in seconds\n * @returns Formatted time string\n */\nconst formatTime = (seconds: number): string => {\n if (!seconds || Number.isNaN(seconds)) return '0:00';\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n};\n\n/**\n * Progress bar component props\n */\ninterface ProgressBarProps {\n currentTime: number;\n duration: number;\n progressPercentage: number;\n onSeek: (time: number) => void;\n}\n\n/**\n * Progress bar subcomponent\n */\nconst ProgressBar = ({\n currentTime,\n duration,\n progressPercentage,\n onSeek,\n className = 'px-4 pb-2',\n}: ProgressBarProps & { className?: string }) => (\n <div className={className}>\n <input\n type=\"range\"\n min={0}\n max={duration || 100}\n value={currentTime}\n onChange={(e) => onSeek(Number.parseFloat(e.target.value))}\n className=\"w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Progresso do vídeo\"\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${progressPercentage}%, var(--color-secondary-300) ${progressPercentage}%)`,\n }}\n />\n </div>\n);\n\n/**\n * Volume controls component props\n */\ninterface VolumeControlsProps {\n volume: number;\n isMuted: boolean;\n onVolumeChange: (volume: number) => void;\n onToggleMute: () => void;\n iconSize?: number;\n showSlider?: boolean;\n}\n\n/**\n * Volume controls subcomponent\n */\nconst VolumeControls = ({\n volume,\n isMuted,\n onVolumeChange,\n onToggleMute,\n iconSize = 24,\n showSlider = true,\n}: VolumeControlsProps) => (\n <div className=\"flex items-center gap-2\">\n <IconButton\n icon={\n isMuted ? (\n <SpeakerSlashIcon size={iconSize} />\n ) : (\n <SpeakerHighIcon size={iconSize} />\n )\n }\n onClick={onToggleMute}\n // O rótulo diz a AÇÃO, o `aria-pressed` diz o ESTADO — e é ele que faz o\n // leitor anunciar \"botão alternar\". Sem o `aria-pressed` não dá para\n // saber se está mudo sem acionar o botão e ouvir o rótulo mudar.\n aria-label={isMuted ? 'Ativar áudio' : 'Desativar áudio'}\n aria-pressed={isMuted}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n {showSlider && (\n <input\n type=\"range\"\n min={0}\n max={100}\n value={Math.round(volume * 100)}\n onChange={(e) => onVolumeChange(Number.parseInt(e.target.value))}\n className=\"w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Volume\"\n // O papel de \"controle deslizante\" já vem do `type=\"range\"`. O que\n // falta é a unidade: sem `aria-valuetext` o leitor fala o número cru\n // (\"100\") em vez de \"100 por cento\".\n aria-valuetext={`${Math.round(volume * 100)} por cento`}\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${volume * 100}%, var(--color-secondary-300) ${volume * 100}%)`,\n }}\n />\n )}\n </div>\n);\n\n/**\n * Speed menu component props\n */\ninterface SpeedMenuProps {\n showSpeedMenu: boolean;\n playbackRate: number;\n onToggleMenu: () => void;\n onSpeedChange: (speed: number) => void;\n isFullscreen: boolean;\n iconSize?: number;\n isTinyMobile?: boolean;\n}\n\n/**\n * Speed menu subcomponent\n */\nconst SpeedMenu = ({\n showSpeedMenu,\n playbackRate,\n onToggleMenu,\n onSpeedChange,\n isFullscreen,\n iconSize = 24,\n isTinyMobile = false,\n}: SpeedMenuProps) => {\n const buttonRef = useRef<HTMLButtonElement>(null);\n const speedMenuContainerRef = useRef<HTMLDivElement>(null);\n const speedMenuRef = useRef<HTMLDivElement>(null);\n\n const getMenuPosition = () => {\n if (!buttonRef.current) return { top: 0, left: 0 };\n const rect = buttonRef.current.getBoundingClientRect();\n\n // Adjust positioning for tiny mobile screens\n const menuHeight = isTinyMobile ? 150 : 180;\n const menuWidth = isTinyMobile ? 60 : 80;\n const padding = isTinyMobile ? 4 : 8;\n\n return {\n // Fixed coords are viewport-based — no scroll offsets.\n top: Math.max(padding, rect.top - menuHeight),\n left: Math.max(padding, rect.right - menuWidth),\n };\n };\n\n // `getBoundingClientRect` obriga o browser a recalcular o layout na hora.\n // Medir só quando o menu vai abrir evita esse reflow síncrono em todo render\n // do player — e o player re-renderiza acompanhando o tempo do vídeo, no\n // mesmo thread que decodifica a mídia.\n const position = showSpeedMenu ? getMenuPosition() : { top: 0, left: 0 };\n\n useEffect(() => {\n const handleClickOutside = (event: Event) => {\n const target = event.target as Node;\n\n // Check if click is outside both the container and the menu\n const isOutsideContainer =\n speedMenuContainerRef.current &&\n !speedMenuContainerRef.current.contains(target);\n const isOutsideMenu =\n speedMenuRef.current && !speedMenuRef.current.contains(target);\n\n // Only close if click is outside both refs (null-safe checks)\n if (isOutsideContainer && isOutsideMenu) {\n onToggleMenu();\n }\n };\n\n if (showSpeedMenu) {\n document.addEventListener('mousedown', handleClickOutside);\n }\n\n return () => {\n document.removeEventListener('mousedown', handleClickOutside);\n };\n }, [showSpeedMenu, onToggleMenu]);\n\n // On open, focus goes to the checked speed so the screen reader announces\n // the current choice. The timeout waits for the (possibly portaled) menu;\n // not a rAF, which the player cannot rely on (see the init fallback below).\n useEffect(() => {\n if (!showSpeedMenu) return;\n const timer = setTimeout(() => {\n speedMenuRef.current\n ?.querySelector<HTMLElement>('[aria-checked=\"true\"]')\n ?.focus();\n }, 0);\n return () => clearTimeout(timer);\n }, [showSpeedMenu]);\n\n /** Closes the menu and hands the focus back to the button that opened it. */\n const closeAndReturnFocus = () => {\n onToggleMenu();\n buttonRef.current?.focus();\n };\n\n /**\n * Menu keyboard: arrows/Home/End move between speeds (roving focus),\n * Escape closes, Tab leaves. Handled keys stop here so they don't reach the\n * player's own shortcuts; preventDefault on Escape keeps a Modal open.\n */\n const handleMenuKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const items = Array.from(\n speedMenuRef.current?.querySelectorAll<HTMLElement>(\n '[role=\"menuitemradio\"]'\n ) ?? []\n );\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n const nextIndexByKey: Record<string, number> = {\n ArrowDown: (currentIndex + 1) % items.length,\n ArrowUp: (currentIndex - 1 + items.length) % items.length,\n Home: 0,\n End: items.length - 1,\n };\n\n if (event.key in nextIndexByKey) {\n event.preventDefault();\n event.stopPropagation();\n items[nextIndexByKey[event.key]]?.focus();\n } else if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n closeAndReturnFocus();\n } else if (event.key === 'Tab') {\n // Move focus to the button first, without preventDefault: the native\n // Tab/Shift+Tab then continues from it instead of from the portaled\n // menu at the end of <body>, which unmounts right after\n buttonRef.current?.focus();\n onToggleMenu();\n }\n };\n\n const menuContent = (\n <div\n ref={speedMenuRef}\n role=\"menu\"\n aria-label=\"Velocidade de reprodução\"\n // Focusable by script only: focus lives on the speeds (roving tabindex)\n tabIndex={-1}\n onKeyDown={handleMenuKeyDown}\n className={\n isFullscreen\n ? 'absolute bottom-12 right-0 bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]'\n : 'fixed bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]'\n }\n style={\n isFullscreen\n ? undefined\n : {\n top: `${position.top}px`,\n left: `${position.left}px`,\n }\n }\n >\n {[0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => (\n <button\n key={speed}\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={playbackRate === speed}\n // Roving tabindex: arrows move between speeds, Tab leaves the menu\n tabIndex={-1}\n onClick={() => {\n onSpeedChange(speed);\n buttonRef.current?.focus();\n }}\n className={`block w-full text-left px-3 py-1 text-sm rounded hover:bg-border-50 transition-colors ${\n playbackRate === speed\n ? 'bg-primary-950 text-secondary-100 font-medium'\n : 'text-text-950'\n }`}\n >\n {speed}x\n </button>\n ))}\n </div>\n );\n\n // SSR-safe portal content\n const portalContent =\n showSpeedMenu &&\n globalThis.window !== undefined &&\n globalThis.document !== undefined &&\n !!globalThis.document?.body\n ? createPortal(menuContent, globalThis.document.body)\n : null;\n\n return (\n <div className=\"relative\" ref={speedMenuContainerRef}>\n <IconButton\n ref={buttonRef}\n icon={<DotsThreeVerticalIcon size={iconSize} />}\n onClick={onToggleMenu}\n aria-label=\"Velocidade de reprodução\"\n aria-haspopup=\"menu\"\n aria-expanded={showSpeedMenu}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n {showSpeedMenu && (isFullscreen ? menuContent : portalContent)}\n </div>\n );\n};\n\n/**\n * Video player component with controls and progress tracking\n * Integrates with backend lesson progress system\n *\n * @param props - VideoPlayer component props\n * @returns Video player element with controls\n */\nconst VideoPlayer = ({\n src,\n poster,\n subtitles,\n title,\n subtitle: subtitleText,\n hideHeader = false,\n initialTime = 0,\n onTimeUpdate,\n onProgress,\n onVideoComplete,\n className,\n autoSave = true,\n storageKey = 'video-progress',\n userId,\n downloadContent,\n showDownloadButton = false,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n}: VideoPlayerProps) => {\n const videoRef = useRef<HTMLVideoElement>(null);\n const { isUltraSmallMobile, isTinyMobile } = useMobile();\n const [isPlaying, setIsPlaying] = useState(false);\n const [hasStarted, setHasStarted] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n // Volume e mudo vivem em um store persistido (não em estado local): o valor\n // escolhido pelo usuário precisa sobreviver à troca de vídeo/aula e valer\n // para qualquer player montado depois — inclusive o podcast —, em vez de\n // voltar a 100% a cada mídia.\n const { volume, isMuted, setVolume, setIsMuted } = useMediaVolumePreference();\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [showControls, setShowControls] = useState(true);\n const [hasCompleted, setHasCompleted] = useState(false);\n const [showCaptions, setShowCaptions] = useState(false);\n const [subtitlesValidation, setSubtitlesValidation] = useState<\n 'idle' | 'validating' | 'valid' | 'invalid'\n >('idle');\n\n /**\n * Estado da retomada desta mídia: `pending` enquanto a posição salva ainda\n * precisa ser aplicada, `done` depois que o `<video>` já a aceitou.\n *\n * É um ref, e não estado, porque é exatamente o que impede o restore de\n * rodar de novo no meio da reprodução (ver `applyResumeTime`).\n */\n const resumeStateRef = useRef<'pending' | 'done'>('pending');\n /** Espelho de `hasStarted` legível de dentro dos callbacks de mídia. */\n const hasStartedRef = useRef(false);\n\n // Reset completion flag when changing videos.\n //\n // `storageKey` is part of the identity here because two different items can\n // legitimately share the same `src` (for example, two lessons built from the\n // same recording). Keying the reset on `src` alone left `hasCompleted` set\n // when the consumer swapped to such an item, and `onVideoComplete` never\n // fired again for it.\n useEffect(() => {\n setHasCompleted(false);\n setHasStarted(false);\n hasStartedRef.current = false;\n // Mídia nova, retomada nova: é a troca de mídia — e só ela — que libera\n // um novo restore de posição.\n resumeStateRef.current = 'pending';\n }, [src, storageKey]);\n const [playbackRate, setPlaybackRate] = useState(1);\n const [showSpeedMenu, setShowSpeedMenu] = useState(false);\n const lastSaveTimeRef = useRef(0);\n const trackRef = useRef<HTMLTrackElement>(null);\n const controlsTimeoutRef = useRef<number | null>(null);\n const lastMousePositionRef = useRef({ x: 0, y: 0 });\n\n /**\n * Check if user is currently interacting with controls\n */\n const isUserInteracting = useCallback(() => {\n // Check if speed menu is open\n if (showSpeedMenu) {\n return true;\n }\n\n // Check if any control element has focus\n const activeElement = document.activeElement;\n const videoContainer = videoRef.current?.parentElement;\n\n if (activeElement && videoContainer?.contains(activeElement)) {\n // Ignore the video element itself - it should not prevent control hiding\n if (activeElement === videoRef.current) {\n return false;\n }\n\n // Check if focused element is a control (button, input, etc.)\n const isControl = activeElement.matches('button, input, [tabindex]');\n if (isControl) {\n return true;\n }\n }\n\n return false;\n }, [showSpeedMenu]);\n\n /**\n * Clear controls timeout\n */\n const clearControlsTimeout = useCallback(() => {\n if (controlsTimeoutRef.current) {\n clearTimeout(controlsTimeoutRef.current);\n controlsTimeoutRef.current = null;\n }\n }, []);\n\n /**\n * Show controls and set auto-hide timer\n */\n const showControlsWithTimer = useCallback(() => {\n setShowControls(true);\n clearControlsTimeout();\n\n // In fullscreen mode, only hide if video is playing\n if (isFullscreen) {\n if (isPlaying) {\n controlsTimeoutRef.current = globalThis.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT) as unknown as number;\n }\n } else {\n // In normal mode, always set a timer to hide controls\n controlsTimeoutRef.current = globalThis.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT) as unknown as number;\n }\n }, [isFullscreen, isPlaying, clearControlsTimeout]);\n\n /**\n * Handle mouse move with position detection\n */\n const handleMouseMove = useCallback(\n (event: MouseEvent) => {\n const currentX = event.clientX;\n const currentY = event.clientY;\n const lastPos = lastMousePositionRef.current;\n\n // Check if mouse actually moved (minimum 5px threshold)\n const hasMoved =\n Math.abs(currentX - lastPos.x) > 5 ||\n Math.abs(currentY - lastPos.y) > 5;\n\n if (hasMoved) {\n lastMousePositionRef.current = { x: currentX, y: currentY };\n showControlsWithTimer();\n }\n },\n [showControlsWithTimer]\n );\n\n /**\n * Handle mouse enter to show controls with appropriate timer logic\n */\n const handleMouseEnter = useCallback(() => {\n showControlsWithTimer();\n }, [showControlsWithTimer]);\n\n /**\n * Handle mouse leave to hide controls faster\n */\n const handleMouseLeave = useCallback(() => {\n const userInteracting = isUserInteracting();\n clearControlsTimeout();\n\n // Hide controls when mouse leaves, except when in fullscreen or user is interacting\n if (!isFullscreen && !userInteracting) {\n // Use shorter timeout when mouse leaves\n controlsTimeoutRef.current = globalThis.setTimeout(() => {\n setShowControls(false);\n }, LEAVE_HIDE_TIMEOUT) as unknown as number;\n }\n }, [isFullscreen, clearControlsTimeout, isUserInteracting]);\n\n /**\n * Apply the saved volume/mute preference to the video element.\n *\n * `src` is a dependency so a newly loaded video also starts at the volume the\n * user last chose, including when the consumer swaps the source in place\n * instead of remounting the player.\n */\n useEffect(() => {\n if (videoRef.current) {\n videoRef.current.volume = volume;\n videoRef.current.muted = isMuted;\n }\n }, [volume, isMuted, src]);\n\n /**\n * Keep the preference in sync with volume changes we did not originate\n * (native fullscreen controls on iOS, media keys, browser-level volume).\n *\n * Registered after the effect above so the browser's own restore, which\n * happens while the element is created, cannot overwrite the stored value.\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const onVolumeChange = () => {\n setVolume(video.volume);\n setIsMuted(video.muted || video.volume === 0);\n };\n\n video.addEventListener('volumechange', onVolumeChange);\n\n return () => {\n video.removeEventListener('volumechange', onVolumeChange);\n };\n }, [setVolume, setIsMuted]);\n\n /**\n * Synchronize isPlaying state with media events\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const onPlay = () => {\n setIsPlaying(true);\n setHasStarted(true);\n hasStartedRef.current = true;\n };\n const onPause = () => setIsPlaying(false);\n const onEnded = () => setIsPlaying(false);\n\n video.addEventListener('play', onPlay);\n video.addEventListener('pause', onPause);\n video.addEventListener('ended', onEnded);\n\n return () => {\n video.removeEventListener('play', onPlay);\n video.removeEventListener('pause', onPause);\n video.removeEventListener('ended', onEnded);\n };\n }, []);\n\n /**\n * Set iOS/Safari inline playback attributes imperatively\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n // Ensure inline playback on iOS/Safari\n video.setAttribute('playsinline', '');\n video.setAttribute('webkit-playsinline', '');\n }, []);\n\n /**\n * Handle controls auto-hide when play state changes\n */\n useEffect(() => {\n if (isPlaying) {\n // Start timer when video starts playing\n showControlsWithTimer();\n } else {\n // Keep controls visible when paused only in fullscreen\n clearControlsTimeout();\n if (isFullscreen) {\n setShowControls(true);\n } else {\n // In normal mode (not fullscreen), initialize timer even when paused\n // This ensures controls will hide properly from the start\n showControlsWithTimer();\n }\n }\n }, [isPlaying, isFullscreen, showControlsWithTimer, clearControlsTimeout]);\n\n /**\n * Handle fullscreen state changes from browser events\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const handleFullscreenChange = () => {\n const isCurrentlyFullscreen = !!document.fullscreenElement;\n setIsFullscreen(isCurrentlyFullscreen);\n\n // Show controls when entering fullscreen, hide after timeout if playing\n if (isCurrentlyFullscreen) {\n showControlsWithTimer();\n }\n };\n\n // Safari iOS-specific fullscreen event handlers\n const handleWebkitBeginFullscreen = () => {\n setIsFullscreen(true);\n showControlsWithTimer();\n };\n\n const handleWebkitEndFullscreen = () => {\n setIsFullscreen(false);\n };\n\n // Standard fullscreen events\n document.addEventListener('fullscreenchange', handleFullscreenChange);\n\n // Safari iOS fullscreen events\n video.addEventListener(\n 'webkitbeginfullscreen',\n handleWebkitBeginFullscreen\n );\n video.addEventListener('webkitendfullscreen', handleWebkitEndFullscreen);\n\n return () => {\n document.removeEventListener('fullscreenchange', handleFullscreenChange);\n video.removeEventListener(\n 'webkitbeginfullscreen',\n handleWebkitBeginFullscreen\n );\n video.removeEventListener(\n 'webkitendfullscreen',\n handleWebkitEndFullscreen\n );\n };\n }, [showControlsWithTimer]);\n\n /**\n * Initialize controls behavior on component mount\n * This ensures controls work correctly from the first load\n */\n useEffect(() => {\n const init = () => {\n if (!isFullscreen) {\n showControlsWithTimer();\n }\n };\n // Prefer rAF to avoid arbitrary timing if available; fall back to INIT_DELAY.\n let raf1 = 0,\n raf2 = 0,\n tid: number | undefined;\n if (globalThis.requestAnimationFrame === undefined) {\n tid = globalThis.setTimeout(init, INIT_DELAY) as unknown as number;\n return () => {\n if (tid) clearTimeout(tid);\n };\n } else {\n raf1 = requestAnimationFrame(() => {\n raf2 = requestAnimationFrame(init);\n });\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n }\n }, []); // Run only once on mount\n\n /**\n * Get initial time from props or localStorage\n */\n const getInitialTime = useCallback((): number | undefined => {\n if (!autoSave || !storageKey) {\n return Number.isFinite(initialTime) && initialTime >= 0\n ? initialTime\n : undefined;\n }\n\n const scopedKey = userId ? `${storageKey}-${userId}` : storageKey;\n const saved = Number(\n localStorage.getItem(`${scopedKey}-${src}`) || Number.NaN\n );\n const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;\n const hasValidSaved = Number.isFinite(saved) && saved >= 0;\n\n if (hasValidInitial) return initialTime;\n if (hasValidSaved) return saved;\n return undefined;\n }, [autoSave, storageKey, userId, src, initialTime]);\n\n /**\n * Última versão de `getInitialTime`, lida sem amarrar a retomada à\n * identidade do callback.\n */\n const getInitialTimeRef = useRef(getInitialTime);\n useEffect(() => {\n getInitialTimeRef.current = getInitialTime;\n }, [getInitialTime]);\n\n /**\n * Aplica a posição de retomada — no máximo uma vez por mídia, sempre antes\n * de a reprodução começar.\n *\n * O que estava acontecendo: `initialTime` é realimentado pelo consumidor com\n * o valor que ele recebe de `onTimeUpdate` (a página da aula guarda o\n * `lastWatchedTimestamp` e devolve a cada 2s). Com o restore num efeito\n * dependente de `getInitialTime` — que muda junto com `initialTime` —, cada\n * atualização dessas virava uma escrita em `video.currentTime` no meio da\n * reprodução. Toda escrita em `currentTime` é um seek: o browser descarta o\n * buffer decodificado e volta a bufferizar daquele ponto. Resultado, um\n * engasgo a cada 2 segundos num arquivo que não tem defeito nenhum.\n *\n * Por isso a retomada agora é um evento da mídia, não uma reação a props:\n * `resumeStateRef` só volta a `pending` quando `src`/`storageKey` mudam.\n */\n const applyResumeTime = useCallback(() => {\n const video = videoRef.current;\n if (!video || resumeStateRef.current === 'done') return;\n\n // Depois que o aluno deu play, a posição dele é a que vale — não a salva.\n if (hasStartedRef.current) {\n resumeStateRef.current = 'done';\n return;\n }\n\n const start = getInitialTimeRef.current();\n const isUsable =\n start !== undefined &&\n Number.isFinite(start) &&\n start > 0 &&\n // Retomar no fim deixaria o vídeo parado no último frame, com cara de\n // travado; nesse caso é melhor começar do zero.\n (!Number.isFinite(video.duration) || start < video.duration - 0.5);\n\n if (!isUsable) {\n resumeStateRef.current = 'done';\n return;\n }\n\n video.currentTime = start;\n setCurrentTime(start);\n\n // Sem metadados o browser trata a atribuição como \"posição inicial padrão\"\n // e alguns a descartam no load; só damos a retomada por concluída quando\n // existe uma linha de tempo de verdade. Até lá, `loadedmetadata` repete.\n if (video.readyState >= 1) {\n resumeStateRef.current = 'done';\n }\n }, []);\n\n // Tentativa na montagem/troca de mídia: cobre o caso em que os metadados já\n // chegaram (vídeo em cache, player remontado) e o evento não vai mais vir.\n useEffect(() => {\n applyResumeTime();\n }, [applyResumeTime, src, storageKey]);\n\n /**\n * Save progress to localStorage periodically\n */\n const saveProgress = useCallback(\n (time: number) => {\n if (!autoSave || !storageKey) return;\n\n const now = Date.now();\n if (now - lastSaveTimeRef.current > 5000) {\n const scopedKey = userId ? `${storageKey}-${userId}` : storageKey;\n localStorage.setItem(`${scopedKey}-${src}`, time.toString());\n lastSaveTimeRef.current = now;\n }\n },\n [autoSave, storageKey, userId, src]\n );\n\n /**\n * Handle play/pause toggle\n */\n const togglePlayPause = useCallback(async () => {\n const video = videoRef.current;\n if (!video) return;\n\n if (!video.paused) {\n video.pause();\n return;\n }\n\n try {\n await video.play();\n } catch {\n // Playback prevented (e.g., autoplay policy); keep state unchanged.\n }\n }, []);\n\n /**\n * Handle volume change\n */\n const handleVolumeChange = useCallback(\n (newVolume: number) => {\n const video = videoRef.current;\n if (!video) return;\n\n const volumeValue = newVolume / 100; // Convert 0-100 to 0-1\n video.volume = volumeValue;\n setVolume(volumeValue);\n\n // Auto mute/unmute based on volume\n const shouldMute = volumeValue === 0;\n const shouldUnmute = volumeValue > 0 && isMuted;\n\n if (shouldMute) {\n video.muted = true;\n setIsMuted(true);\n } else if (shouldUnmute) {\n video.muted = false;\n setIsMuted(false);\n }\n },\n [isMuted, setVolume, setIsMuted]\n );\n\n /**\n * Handle mute toggle\n */\n const toggleMute = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n if (isMuted) {\n // Unmute: restore volume or set to 50% if it was 0\n const restoreVolume = volume > 0 ? volume : UNMUTE_FALLBACK_VOLUME;\n video.volume = restoreVolume;\n video.muted = false;\n setVolume(restoreVolume);\n setIsMuted(false);\n } else {\n // Mute: set volume to 0 and mute\n video.muted = true;\n setIsMuted(true);\n }\n }, [isMuted, volume, setVolume, setIsMuted]);\n\n /**\n * Handle video seek\n */\n const handleSeek = useCallback((newTime: number) => {\n const video = videoRef.current;\n if (video && Number.isFinite(newTime)) {\n video.currentTime = newTime;\n // A barra é um input controlado: sem o valor exato aqui, o `timeupdate`\n // (que só reporta o segundo cheio) devolveria o thumb para o passo\n // anterior durante o arraste.\n setCurrentTime(newTime);\n }\n }, []);\n\n /**\n * Detect if running on Safari iOS\n */\n const isSafariIOS = useCallback((): boolean => {\n const ua = navigator.userAgent;\n const isIOS = /iPad|iPhone|iPod/.test(ua);\n const isWebKit = /WebKit/.test(ua);\n const isNotChrome = !/CriOS|Chrome/.test(ua);\n return isIOS && isWebKit && isNotChrome;\n }, []);\n\n /**\n * Handle fullscreen toggle\n */\n const toggleFullscreen = useCallback(() => {\n const video = videoRef.current;\n const container = video?.parentElement;\n if (!video || !container) return;\n\n // Safari iOS requires using webkitEnterFullscreen on video element\n if (isSafariIOS()) {\n // Type assertion for Safari-specific API\n const videoElement = video as HTMLVideoElement & {\n webkitEnterFullscreen?: () => void;\n webkitExitFullscreen?: () => void;\n webkitDisplayingFullscreen?: boolean;\n };\n\n if (!isFullscreen && videoElement.webkitEnterFullscreen) {\n videoElement.webkitEnterFullscreen();\n } else if (isFullscreen && videoElement.webkitExitFullscreen) {\n videoElement.webkitExitFullscreen();\n }\n } else if (!isFullscreen && container.requestFullscreen) {\n // Standard fullscreen API for other browsers. The returned promise can\n // reject (e.g. document not active after a tab switch); swallow it.\n container.requestFullscreen().catch(() => {});\n } else if (isFullscreen && document.exitFullscreen) {\n document.exitFullscreen().catch(() => {});\n }\n }, [isFullscreen, isSafariIOS]);\n\n /**\n * Handle playback speed change\n */\n const handleSpeedChange = useCallback((speed: number) => {\n if (videoRef.current) {\n videoRef.current.playbackRate = speed;\n setPlaybackRate(speed);\n setShowSpeedMenu(false);\n }\n }, []);\n\n /**\n * Toggle speed menu visibility\n */\n const toggleSpeedMenu = useCallback(() => {\n setShowSpeedMenu(!showSpeedMenu);\n }, [showSpeedMenu]);\n\n /**\n * Toggle captions visibility\n */\n const toggleCaptions = useCallback(() => {\n if (\n !trackRef.current?.track ||\n !subtitles ||\n subtitlesValidation !== 'valid'\n )\n return;\n\n const newShowCaptions = !showCaptions;\n setShowCaptions(newShowCaptions);\n\n // Control track mode programmatically - we already validated subtitles above\n trackRef.current.track.mode = newShowCaptions ? 'showing' : 'hidden';\n }, [showCaptions, subtitles, subtitlesValidation]);\n\n /**\n * Check video completion and fire callback\n */\n const checkVideoCompletion = useCallback(\n (progressPercent: number) => {\n if (progressPercent >= 95 && !hasCompleted) {\n setHasCompleted(true);\n onVideoComplete?.();\n }\n },\n [hasCompleted, onVideoComplete]\n );\n\n /**\n * Handle time update\n */\n const handleTimeUpdate = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const current = video.currentTime;\n\n // `timeupdate` dispara ~4x por segundo, e a UI só mostra segundos inteiros\n // (`formatTime` trunca). Re-renderizar o player a cada evento gastaria\n // três renders inúteis por segundo no mesmo thread que decodifica o vídeo\n // — custo que aparece como queda de frames. Sai apenas quando o segundo\n // exibido muda de fato; `handleSeek` cuida da precisão ao arrastar.\n setCurrentTime((previous) =>\n Math.floor(previous) === Math.floor(current) ? previous : current\n );\n\n // Save progress periodically\n saveProgress(current);\n\n // Fire callbacks\n onTimeUpdate?.(current, duration);\n\n if (duration > 0) {\n const progressPercent = (current / duration) * 100;\n onProgress?.(progressPercent);\n checkVideoCompletion(progressPercent);\n }\n }, [duration, saveProgress, onTimeUpdate, onProgress, checkVideoCompletion]);\n\n /**\n * Handle loaded metadata\n */\n const handleLoadedMetadata = useCallback(() => {\n if (!videoRef.current) return;\n\n setDuration(videoRef.current.duration);\n // Agora existe linha de tempo: é aqui que o seek de retomada realmente\n // pega, e é a última vez que ele acontece nesta mídia.\n applyResumeTime();\n }, [applyResumeTime]);\n\n /**\n * Validate subtitles URL before showing the button\n */\n useEffect(() => {\n const controller = new AbortController();\n\n const validateSubtitles = async () => {\n // If no subtitles, mark as idle\n if (!subtitles) {\n setSubtitlesValidation('idle');\n return;\n }\n\n // Start validation\n setSubtitlesValidation('validating');\n\n try {\n // Check if it's a data URL (inline VTT)\n if (subtitles.startsWith('data:')) {\n setSubtitlesValidation('valid');\n return;\n }\n\n // Fetch the subtitles file to validate it\n const response = await fetch(subtitles, {\n method: 'HEAD',\n signal: controller.signal,\n });\n\n if (response.ok) {\n // Optionally check content type\n const contentType = response.headers.get('content-type');\n const isValidType =\n !contentType ||\n contentType.includes('text/vtt') ||\n contentType.includes('text/plain') ||\n contentType.includes('application/octet-stream');\n\n if (isValidType) {\n setSubtitlesValidation('valid');\n } else {\n setSubtitlesValidation('invalid');\n console.warn(\n `Subtitles URL has invalid content type: ${contentType}`\n );\n }\n } else {\n setSubtitlesValidation('invalid');\n console.warn(\n `Subtitles URL returned status: ${response.status} ${response.statusText}`\n );\n }\n } catch (error) {\n // Ignore AbortError - it's expected when cleaning up\n if (error instanceof Error && error.name === 'AbortError') {\n return;\n }\n console.warn('Subtitles URL validation failed:', error);\n setSubtitlesValidation('invalid');\n }\n };\n\n validateSubtitles();\n\n // Cleanup: abort ongoing fetch to prevent stale updates\n return () => {\n controller.abort();\n };\n }, [subtitles]);\n\n /**\n * Initialize track mode when track is available\n */\n useEffect(() => {\n if (trackRef.current?.track) {\n // Set initial mode based on showCaptions state and subtitle availability\n trackRef.current.track.mode =\n showCaptions && subtitles && subtitlesValidation === 'valid'\n ? 'showing'\n : 'hidden';\n }\n }, [subtitles, showCaptions, subtitlesValidation]);\n\n /**\n * Handle visibility change and blur to pause video when losing focus\n */\n useEffect(() => {\n const handleVisibilityChange = () => {\n if (document.hidden && isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n const handleBlur = () => {\n if (isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n document.addEventListener('visibilitychange', handleVisibilityChange);\n globalThis.addEventListener('blur', handleBlur);\n\n return () => {\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n globalThis.removeEventListener('blur', handleBlur);\n // Clean up timers on unmount\n clearControlsTimeout();\n };\n }, [isPlaying, clearControlsTimeout]);\n\n const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0;\n\n /**\n * Get responsive icon size based on screen size\n */\n const getIconSize = useCallback(() => {\n if (isTinyMobile) return 18;\n if (isUltraSmallMobile) return 20;\n return 24;\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive padding classes for controls\n */\n const getControlsPadding = useCallback(() => {\n if (isTinyMobile) return 'px-2 pb-2 pt-1';\n if (isUltraSmallMobile) return 'px-3 pb-3 pt-1';\n return 'px-4 pb-4';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive gap classes for controls\n */\n const getControlsGap = useCallback(() => {\n if (isTinyMobile) return 'gap-1';\n if (isUltraSmallMobile) return 'gap-2';\n return 'gap-4';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive padding for progress bar\n */\n const getProgressBarPadding = useCallback(() => {\n if (isTinyMobile) return 'px-2 pb-1';\n if (isUltraSmallMobile) return 'px-3 pb-1';\n return 'px-4 pb-2';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive positioning classes for center play button\n */\n const getCenterPlayButtonPosition = useCallback(() => {\n if (isTinyMobile) return 'items-center justify-center -translate-y-12';\n if (isUltraSmallMobile) return 'items-center justify-center -translate-y-8';\n return 'items-center justify-center';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Barra de controles visível.\n *\n * A barra some por `opacity-0`, mas continua no DOM e focável — dá para\n * chegar nela por Tab enquanto está invisível. Aí as teclas até funcionam\n * (as setas mudam o volume, por exemplo), só que nada aparece na tela e a\n * impressão é de que o controle está quebrado.\n *\n * Manter a barra acesa enquanto o foco estiver dentro dela resolve sem\n * torná-la inalcançável: escondê-la de verdade (`invisible`) impediria o Tab\n * de entrar, e aí não haveria foco para revelá-la de volta.\n */\n const [controlsHaveFocus, setControlsHaveFocus] = useState(false);\n const controlsVisible = showControls || controlsHaveFocus;\n\n /** Handlers de foco da barra; `onBlur` só apaga ao sair do subtree. */\n const controlsFocusProps = {\n onFocus: () => setControlsHaveFocus(true),\n onBlur: (event: FocusEvent<HTMLDivElement>) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n setControlsHaveFocus(false);\n }\n },\n };\n\n /**\n * Calculate top controls opacity based on state\n */\n const getTopControlsOpacity = useCallback(() => {\n return controlsVisible ? 'opacity-100' : 'opacity-0';\n }, [controlsVisible]);\n\n /**\n * Calculate bottom controls opacity based on state\n */\n const getBottomControlsOpacity = useCallback(() => {\n return controlsVisible ? 'opacity-100' : 'opacity-0';\n }, [controlsVisible]);\n\n /**\n * Seek video backward\n */\n const seekBackward = useCallback(() => {\n if (videoRef.current) {\n videoRef.current.currentTime -= 10;\n }\n }, []);\n\n /**\n * Seek video forward\n */\n const seekForward = useCallback(() => {\n if (videoRef.current) {\n videoRef.current.currentTime += 10;\n }\n }, []);\n\n /**\n * Increase volume\n */\n const increaseVolume = useCallback(() => {\n handleVolumeChange(Math.min(100, volume * 100 + 10));\n }, [handleVolumeChange, volume]);\n\n /**\n * Decrease volume\n */\n const decreaseVolume = useCallback(() => {\n handleVolumeChange(Math.max(0, volume * 100 - 10));\n }, [handleVolumeChange, volume]);\n\n /**\n * Handle video element keyboard events\n */\n const handleVideoKeyDown = useCallback(\n (e: KeyboardEvent) => {\n if (!e.key) return;\n\n // Prevent bubbling to parent handlers to avoid double toggles\n e.stopPropagation();\n showControlsWithTimer();\n\n // Map of key handlers for better maintainability and reduced complexity\n const keyHandlers: Record<string, () => void | Promise<void>> = {\n ' ': togglePlayPause,\n Enter: togglePlayPause,\n ArrowLeft: seekBackward,\n ArrowRight: seekForward,\n ArrowUp: increaseVolume,\n ArrowDown: decreaseVolume,\n m: toggleMute,\n M: toggleMute,\n f: toggleFullscreen,\n F: toggleFullscreen,\n };\n\n const handler = keyHandlers[e.key];\n if (handler) {\n e.preventDefault();\n handler();\n }\n },\n [\n showControlsWithTimer,\n togglePlayPause,\n seekBackward,\n seekForward,\n increaseVolume,\n decreaseVolume,\n toggleMute,\n toggleFullscreen,\n ]\n );\n\n const groupedSubTitleValid = subtitles && subtitlesValidation === 'valid';\n const showPoster = Boolean(poster) && !hasStarted;\n\n useCaptionPosition({\n trackRef,\n controlsVisible,\n enabled: Boolean(groupedSubTitleValid) && showCaptions,\n isUltraSmallMobile,\n isTinyMobile,\n });\n\n return (\n <div className={cn('flex flex-col', className)}>\n {/* Integrated Header */}\n {!hideHeader && (title || subtitleText) && (\n <div className=\"bg-subject-1 px-8 py-4 flex items-end justify-between min-h-20\">\n <div className=\"flex flex-col gap-1\">\n {title && (\n <Text\n as=\"h2\"\n size=\"lg\"\n weight=\"bold\"\n color=\"text-text-900\"\n className=\"leading-5 tracking-wide\"\n >\n {title}\n </Text>\n )}\n {subtitleText && (\n <Text\n as=\"p\"\n size=\"sm\"\n weight=\"normal\"\n color=\"text-text-600\"\n className=\"leading-5\"\n >\n {subtitleText}\n </Text>\n )}\n </div>\n\n {/* Download Button */}\n {showDownloadButton && downloadContent && (\n <DownloadButton\n content={downloadContent}\n lessonTitle={title}\n onDownloadStart={onDownloadStart}\n onDownloadComplete={onDownloadComplete}\n onDownloadError={onDownloadError}\n className=\"flex-shrink-0\"\n />\n )}\n </div>\n )}\n\n {/* Video Container */}\n <section\n className={cn(\n 'relative w-full bg-background overflow-hidden group',\n 'rounded-b-xl',\n // Hide cursor when controls are hidden and video is playing\n isPlaying && !showControls\n ? 'cursor-none group-hover:cursor-default'\n : 'cursor-default'\n )}\n aria-label={\n title ? `Reprodutor de vídeo: ${title}` : 'Reprodutor de vídeo'\n }\n onMouseMove={handleMouseMove}\n onMouseEnter={handleMouseEnter}\n onTouchStart={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n >\n {/* Video Element */}\n <video\n ref={videoRef}\n src={src}\n crossOrigin=\"anonymous\"\n className=\"w-full h-full object-contain analytica-video\"\n controlsList=\"nodownload\"\n playsInline\n onTimeUpdate={handleTimeUpdate}\n onLoadedMetadata={handleLoadedMetadata}\n onClick={togglePlayPause}\n onKeyDown={handleVideoKeyDown}\n tabIndex={0}\n aria-label={title ? `Vídeo: ${title}` : 'Vídeo'}\n >\n <track\n ref={trackRef}\n kind=\"captions\"\n src={\n groupedSubTitleValid\n ? subtitles\n : 'data:text/vtt;charset=utf-8,WEBVTT'\n }\n srcLang=\"pt-br\"\n label={\n groupedSubTitleValid\n ? 'Legendas em Português'\n : 'Sem legendas disponíveis'\n }\n default={false}\n />\n </video>\n\n {showPoster && (\n <img\n src={poster}\n alt={title ? `Capa: ${title}` : 'Capa do vídeo'}\n className=\"absolute inset-0 w-full h-full object-cover\"\n aria-hidden=\"true\"\n />\n )}\n\n {/* Center Play Button */}\n {!isPlaying && (\n <div\n className={cn(\n 'absolute inset-0 flex bg-black/30 transition-opacity',\n getCenterPlayButtonPosition()\n )}\n >\n <IconButton\n icon={<PlayIcon size={32} weight=\"regular\" className=\"ml-1\" />}\n onClick={togglePlayPause}\n aria-label=\"Reproduzir vídeo\"\n className=\"!bg-transparent !text-white !w-auto !h-auto hover:!bg-transparent hover:!text-gray-200\"\n />\n </div>\n )}\n\n {/* Top Controls */}\n <div\n className={cn(\n 'absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/70 to-transparent transition-opacity',\n getTopControlsOpacity()\n )}\n {...controlsFocusProps}\n >\n <div className=\"flex justify-start\">\n <IconButton\n icon={\n isFullscreen ? (\n <ArrowsInSimpleIcon size={24} />\n ) : (\n <ArrowsOutSimpleIcon size={24} />\n )\n }\n onClick={toggleFullscreen}\n aria-label={\n isFullscreen ? 'Desativar tela cheia' : 'Ativar tela cheia'\n }\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n </div>\n </div>\n\n {/* Bottom Controls */}\n <div\n className={cn(\n 'absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent transition-opacity',\n getBottomControlsOpacity()\n )}\n {...controlsFocusProps}\n >\n {/* Progress Bar */}\n <ProgressBar\n currentTime={currentTime}\n duration={duration}\n progressPercentage={progressPercentage}\n onSeek={handleSeek}\n className={getProgressBarPadding()}\n />\n\n {/* Control Buttons */}\n <div\n className={cn(\n 'flex items-center justify-between',\n getControlsPadding()\n )}\n >\n {/* Left Controls */}\n <div className={cn('flex items-center', getControlsGap())}>\n {/* Play/Pause */}\n <IconButton\n icon={\n isPlaying ? (\n <PauseIcon size={getIconSize()} />\n ) : (\n <PlayIcon size={getIconSize()} />\n )\n }\n onClick={togglePlayPause}\n aria-label={isPlaying ? 'Pausar' : 'Reproduzir'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n {/* Volume */}\n <VolumeControls\n volume={volume}\n isMuted={isMuted}\n onVolumeChange={handleVolumeChange}\n onToggleMute={toggleMute}\n iconSize={getIconSize()}\n showSlider={!isUltraSmallMobile}\n />\n\n {/* Captions - Only show after validation is complete and valid */}\n {groupedSubTitleValid && (\n <IconButton\n icon={<ClosedCaptioningIcon size={getIconSize()} />}\n onClick={toggleCaptions}\n // Mesma natureza do mudo: ação no rótulo, estado no\n // `aria-pressed` — hoje o único sinal de que as legendas\n // estão ligadas é a cor do ícone.\n aria-label={\n showCaptions ? 'Ocultar legendas' : 'Exibir legendas'\n }\n aria-pressed={showCaptions}\n className={cn(\n '!bg-transparent hover:!bg-white/20',\n showCaptions ? '!text-primary-400' : '!text-white'\n )}\n />\n )}\n\n {/* Time Display */}\n <Text size=\"sm\" weight=\"medium\" color=\"text-white\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </Text>\n </div>\n\n {/* Right Controls */}\n <div className=\"flex items-center gap-4\">\n {/* Speed Control */}\n <SpeedMenu\n showSpeedMenu={showSpeedMenu}\n playbackRate={playbackRate}\n onToggleMenu={toggleSpeedMenu}\n onSpeedChange={handleSpeedChange}\n iconSize={getIconSize()}\n isTinyMobile={isTinyMobile}\n isFullscreen={isFullscreen}\n />\n </div>\n </div>\n </div>\n </section>\n </div>\n );\n};\n\nexport default VideoPlayer;\n","import { useEffect, type RefObject } from 'react';\n\n/**\n * Vertical position of the caption box, as a percentage of the video display\n * area, for each screen range. `visible` applies while the controls bar is on\n * screen; `hidden` while it is faded out.\n *\n * The values differ per range because `line` is relative to the video height\n * while the controls bar height is nearly fixed in pixels — the same bar eats\n * ~18% of a 450px tall video and ~30% of a 170px one.\n */\nexport const CAPTION_LINE = {\n default: { visible: 78, hidden: 90 },\n ultraSmall: { visible: 68, hidden: 88 },\n tiny: { visible: 62, hidden: 86 },\n} as const;\n\ntype CaptionLineInput = {\n controlsVisible: boolean;\n isUltraSmallMobile: boolean;\n isTinyMobile: boolean;\n};\n\n/**\n * Picks the caption line for the current screen range and controls state.\n */\nexport const getCaptionLine = ({\n controlsVisible,\n isUltraSmallMobile,\n isTinyMobile,\n}: CaptionLineInput): number => {\n const range = (() => {\n if (isTinyMobile) return CAPTION_LINE.tiny;\n if (isUltraSmallMobile) return CAPTION_LINE.ultraSmall;\n return CAPTION_LINE.default;\n })();\n\n return controlsVisible ? range.visible : range.hidden;\n};\n\n/**\n * Moves every cue of the track to `line`, expressed as a percentage of the\n * video display area.\n *\n * `snapToLines = false` is what switches `line` from \"text line count\" to that\n * percentage. The cue's own `line` from the VTT file is overwritten on purpose:\n * honouring it would reinstate the bug precisely for the cues that ask to sit\n * at the bottom, which is the case being fixed.\n *\n * A missing or empty cue list is a no-op — `track.cues` is null until the VTT\n * file loads.\n */\nexport const applyCaptionLine = (\n cues: TextTrackCueList | null | undefined,\n line: number\n): void => {\n if (!cues) return;\n\n for (const cue of cues) {\n const vttCue = cue as VTTCue;\n vttCue.snapToLines = false;\n vttCue.line = line;\n }\n};\n\ntype UseCaptionPositionInput = {\n trackRef: RefObject<HTMLTrackElement | null>;\n controlsVisible: boolean;\n enabled: boolean;\n isUltraSmallMobile: boolean;\n isTinyMobile: boolean;\n};\n\n/**\n * Keeps the native caption clear of the controls bar, lifting it while the bar\n * is on screen and dropping it back when the bar fades out.\n *\n * Positioning the native cue — instead of rendering the caption ourselves —\n * is what keeps captions working inside Safari iOS native fullscreen, where\n * `webkitEnterFullscreen` hands the video over to the system player and no\n * custom overlay is drawn.\n *\n * Repositioning runs on the track's `load` event, because `track.cues` is null\n * until the VTT file arrives, and again whenever the controls state or the\n * screen range changes.\n */\nexport const useCaptionPosition = ({\n trackRef,\n controlsVisible,\n enabled,\n isUltraSmallMobile,\n isTinyMobile,\n}: UseCaptionPositionInput): void => {\n useEffect(() => {\n const trackElement = trackRef.current;\n if (!trackElement || !enabled) return;\n\n const line = getCaptionLine({\n controlsVisible,\n isUltraSmallMobile,\n isTinyMobile,\n });\n const reposition = () => applyCaptionLine(trackElement.track?.cues, line);\n\n reposition();\n trackElement.addEventListener('load', reposition);\n\n return () => trackElement.removeEventListener('load', reposition);\n }, [trackRef, controlsVisible, enabled, isUltraSmallMobile, isTinyMobile]);\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-QVGUYPXO.js","../src/components/VideoPlayer/VideoPlayer.tsx","../src/components/VideoPlayer/useCaptionPosition.ts"],"names":["useEffect"],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-QVGUYPXO.js","sourcesContent":[null,"import {\n useRef,\n useState,\n useEffect,\n useCallback,\n MouseEvent,\n KeyboardEvent,\n FocusEvent,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { PlayIcon } from '@phosphor-icons/react/dist/csr/Play';\nimport { PauseIcon } from '@phosphor-icons/react/dist/csr/Pause';\nimport { SpeakerHighIcon } from '@phosphor-icons/react/dist/csr/SpeakerHigh';\nimport { SpeakerSlashIcon } from '@phosphor-icons/react/dist/csr/SpeakerSlash';\nimport { ArrowsOutSimpleIcon } from '@phosphor-icons/react/dist/csr/ArrowsOutSimple';\nimport { ArrowsInSimpleIcon } from '@phosphor-icons/react/dist/csr/ArrowsInSimple';\nimport { ClosedCaptioningIcon } from '@phosphor-icons/react/dist/csr/ClosedCaptioning';\nimport { DotsThreeVerticalIcon } from '@phosphor-icons/react/dist/csr/DotsThreeVertical';\nimport { cn } from '../../utils/utils';\nimport IconButton from '../IconButton/IconButton';\nimport Text from '../Text/Text';\nimport { useMobile } from '../../hooks/useMobile';\nimport {\n UNMUTE_FALLBACK_VOLUME,\n useMediaVolumePreference,\n} from '../../store/mediaPreferencesStore';\nimport DownloadButton, {\n DownloadContent,\n} from '../DownloadButton/DownloadButton';\nimport { useCaptionPosition } from './useCaptionPosition';\n\n// Constants for timeout durations\nconst CONTROLS_HIDE_TIMEOUT = 3000; // 3 seconds for normal control hiding\nconst LEAVE_HIDE_TIMEOUT = 1000; // 1 second when mouse leaves the video area\nconst INIT_DELAY = 100; // ms delay to initialize controls on mount\n\n/**\n * VideoPlayer component props interface\n */\ninterface VideoPlayerProps {\n /** Video source URL */\n src: string;\n /** Video poster/thumbnail URL */\n poster?: string;\n /** Subtitles URL */\n subtitles?: string;\n /** Video title */\n title?: string;\n /** Video subtitle/description */\n subtitle?: string;\n /**\n * Hides the header bar that carries the title and subtitle, for designs\n * where the video sits under a heading of its own. The title is still used\n * to label the player for screen readers.\n */\n hideHeader?: boolean;\n /** Initial playback time in seconds */\n initialTime?: number;\n /**\n * Callback fired when video time updates. Receives the current second and\n * the media's duration, which is only known once metadata has loaded (0\n * before that), so consumers that store it must treat it as optional.\n */\n onTimeUpdate?: (seconds: number, duration?: number) => void;\n /** Callback fired with progress percentage (0-100) */\n onProgress?: (progress: number) => void;\n /** Callback fired when video completes (>95% watched) */\n onVideoComplete?: () => void;\n /** Additional CSS classes */\n className?: string;\n /** Auto-save progress to localStorage */\n autoSave?: boolean;\n /** localStorage key for saving progress */\n storageKey?: string;\n /** User ID used to scope localStorage keys — prevents progress leaking between users */\n userId?: string;\n /** Download content URLs for lesson materials */\n downloadContent?: DownloadContent;\n /** Show download button in header */\n showDownloadButton?: boolean;\n /** Callback fired when download starts */\n onDownloadStart?: (contentType: string) => void;\n /** Callback fired when download completes */\n onDownloadComplete?: (contentType: string) => void;\n /** Callback fired when download fails */\n onDownloadError?: (contentType: string, error: Error) => void;\n}\n\n/**\n * Format seconds to MM:SS display format\n * @param seconds - Time in seconds\n * @returns Formatted time string\n */\nconst formatTime = (seconds: number): string => {\n if (!seconds || Number.isNaN(seconds)) return '0:00';\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n};\n\n/**\n * Progress bar component props\n */\ninterface ProgressBarProps {\n currentTime: number;\n duration: number;\n progressPercentage: number;\n onSeek: (time: number) => void;\n}\n\n/**\n * Progress bar subcomponent\n */\nconst ProgressBar = ({\n currentTime,\n duration,\n progressPercentage,\n onSeek,\n className = 'px-4 pb-2',\n}: ProgressBarProps & { className?: string }) => (\n <div className={className}>\n <input\n type=\"range\"\n min={0}\n max={duration || 100}\n value={currentTime}\n onChange={(e) => onSeek(Number.parseFloat(e.target.value))}\n className=\"w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Progresso do vídeo\"\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${progressPercentage}%, var(--color-secondary-300) ${progressPercentage}%)`,\n }}\n />\n </div>\n);\n\n/**\n * Volume controls component props\n */\ninterface VolumeControlsProps {\n volume: number;\n isMuted: boolean;\n onVolumeChange: (volume: number) => void;\n onToggleMute: () => void;\n iconSize?: number;\n showSlider?: boolean;\n}\n\n/**\n * Volume controls subcomponent\n */\nconst VolumeControls = ({\n volume,\n isMuted,\n onVolumeChange,\n onToggleMute,\n iconSize = 24,\n showSlider = true,\n}: VolumeControlsProps) => (\n <div className=\"flex items-center gap-2\">\n <IconButton\n icon={\n isMuted ? (\n <SpeakerSlashIcon size={iconSize} />\n ) : (\n <SpeakerHighIcon size={iconSize} />\n )\n }\n onClick={onToggleMute}\n // O rótulo diz a AÇÃO, o `aria-pressed` diz o ESTADO — e é ele que faz o\n // leitor anunciar \"botão alternar\". Sem o `aria-pressed` não dá para\n // saber se está mudo sem acionar o botão e ouvir o rótulo mudar.\n aria-label={isMuted ? 'Ativar áudio' : 'Desativar áudio'}\n aria-pressed={isMuted}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n {showSlider && (\n <input\n type=\"range\"\n min={0}\n max={100}\n value={Math.round(volume * 100)}\n onChange={(e) => onVolumeChange(Number.parseInt(e.target.value))}\n className=\"w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Volume\"\n // O papel de \"controle deslizante\" já vem do `type=\"range\"`. O que\n // falta é a unidade: sem `aria-valuetext` o leitor fala o número cru\n // (\"100\") em vez de \"100 por cento\".\n aria-valuetext={`${Math.round(volume * 100)} por cento`}\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${volume * 100}%, var(--color-secondary-300) ${volume * 100}%)`,\n }}\n />\n )}\n </div>\n);\n\n/**\n * Speed menu component props\n */\ninterface SpeedMenuProps {\n showSpeedMenu: boolean;\n playbackRate: number;\n onToggleMenu: () => void;\n onSpeedChange: (speed: number) => void;\n isFullscreen: boolean;\n iconSize?: number;\n isTinyMobile?: boolean;\n}\n\n/**\n * Speed menu subcomponent\n */\nconst SpeedMenu = ({\n showSpeedMenu,\n playbackRate,\n onToggleMenu,\n onSpeedChange,\n isFullscreen,\n iconSize = 24,\n isTinyMobile = false,\n}: SpeedMenuProps) => {\n const buttonRef = useRef<HTMLButtonElement>(null);\n const speedMenuContainerRef = useRef<HTMLDivElement>(null);\n const speedMenuRef = useRef<HTMLDivElement>(null);\n\n const getMenuPosition = () => {\n if (!buttonRef.current) return { top: 0, left: 0 };\n const rect = buttonRef.current.getBoundingClientRect();\n\n // Adjust positioning for tiny mobile screens\n const menuHeight = isTinyMobile ? 150 : 180;\n const menuWidth = isTinyMobile ? 60 : 80;\n const padding = isTinyMobile ? 4 : 8;\n\n return {\n // Fixed coords are viewport-based — no scroll offsets.\n top: Math.max(padding, rect.top - menuHeight),\n left: Math.max(padding, rect.right - menuWidth),\n };\n };\n\n // `getBoundingClientRect` obriga o browser a recalcular o layout na hora.\n // Medir só quando o menu vai abrir evita esse reflow síncrono em todo render\n // do player — e o player re-renderiza acompanhando o tempo do vídeo, no\n // mesmo thread que decodifica a mídia.\n const position = showSpeedMenu ? getMenuPosition() : { top: 0, left: 0 };\n\n useEffect(() => {\n const handleClickOutside = (event: Event) => {\n const target = event.target as Node;\n\n // Check if click is outside both the container and the menu\n const isOutsideContainer =\n speedMenuContainerRef.current &&\n !speedMenuContainerRef.current.contains(target);\n const isOutsideMenu =\n speedMenuRef.current && !speedMenuRef.current.contains(target);\n\n // Only close if click is outside both refs (null-safe checks)\n if (isOutsideContainer && isOutsideMenu) {\n onToggleMenu();\n }\n };\n\n if (showSpeedMenu) {\n document.addEventListener('mousedown', handleClickOutside);\n }\n\n return () => {\n document.removeEventListener('mousedown', handleClickOutside);\n };\n }, [showSpeedMenu, onToggleMenu]);\n\n // On open, focus goes to the checked speed so the screen reader announces\n // the current choice. The timeout waits for the (possibly portaled) menu;\n // not a rAF, which the player cannot rely on (see the init fallback below).\n useEffect(() => {\n if (!showSpeedMenu) return;\n const timer = setTimeout(() => {\n speedMenuRef.current\n ?.querySelector<HTMLElement>('[aria-checked=\"true\"]')\n ?.focus();\n }, 0);\n return () => clearTimeout(timer);\n }, [showSpeedMenu]);\n\n /** Closes the menu and hands the focus back to the button that opened it. */\n const closeAndReturnFocus = () => {\n onToggleMenu();\n buttonRef.current?.focus();\n };\n\n /**\n * Menu keyboard: arrows/Home/End move between speeds (roving focus),\n * Escape closes, Tab leaves. Handled keys stop here so they don't reach the\n * player's own shortcuts; preventDefault on Escape keeps a Modal open.\n */\n const handleMenuKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const items = Array.from(\n speedMenuRef.current?.querySelectorAll<HTMLElement>(\n '[role=\"menuitemradio\"]'\n ) ?? []\n );\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n const nextIndexByKey: Record<string, number> = {\n ArrowDown: (currentIndex + 1) % items.length,\n ArrowUp: (currentIndex - 1 + items.length) % items.length,\n Home: 0,\n End: items.length - 1,\n };\n\n if (event.key in nextIndexByKey) {\n event.preventDefault();\n event.stopPropagation();\n items[nextIndexByKey[event.key]]?.focus();\n } else if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n closeAndReturnFocus();\n } else if (event.key === 'Tab') {\n // Move focus to the button first, without preventDefault: the native\n // Tab/Shift+Tab then continues from it instead of from the portaled\n // menu at the end of <body>, which unmounts right after\n buttonRef.current?.focus();\n onToggleMenu();\n }\n };\n\n const menuContent = (\n <div\n ref={speedMenuRef}\n role=\"menu\"\n aria-label=\"Velocidade de reprodução\"\n // Focusable by script only: focus lives on the speeds (roving tabindex)\n tabIndex={-1}\n onKeyDown={handleMenuKeyDown}\n className={\n isFullscreen\n ? 'absolute bottom-12 right-0 bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]'\n : 'fixed bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]'\n }\n style={\n isFullscreen\n ? undefined\n : {\n top: `${position.top}px`,\n left: `${position.left}px`,\n }\n }\n >\n {[0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => (\n <button\n key={speed}\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={playbackRate === speed}\n // Roving tabindex: arrows move between speeds, Tab leaves the menu\n tabIndex={-1}\n onClick={() => {\n onSpeedChange(speed);\n buttonRef.current?.focus();\n }}\n className={`block w-full text-left px-3 py-1 text-sm rounded hover:bg-border-50 transition-colors ${\n playbackRate === speed\n ? 'bg-primary-950 text-secondary-100 font-medium'\n : 'text-text-950'\n }`}\n >\n {speed}x\n </button>\n ))}\n </div>\n );\n\n // SSR-safe portal content\n const portalContent =\n showSpeedMenu &&\n globalThis.window !== undefined &&\n globalThis.document !== undefined &&\n !!globalThis.document?.body\n ? createPortal(menuContent, globalThis.document.body)\n : null;\n\n return (\n <div className=\"relative\" ref={speedMenuContainerRef}>\n <IconButton\n ref={buttonRef}\n icon={<DotsThreeVerticalIcon size={iconSize} />}\n onClick={onToggleMenu}\n aria-label=\"Velocidade de reprodução\"\n aria-haspopup=\"menu\"\n aria-expanded={showSpeedMenu}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n {showSpeedMenu && (isFullscreen ? menuContent : portalContent)}\n </div>\n );\n};\n\n/**\n * Video player component with controls and progress tracking\n * Integrates with backend lesson progress system\n *\n * @param props - VideoPlayer component props\n * @returns Video player element with controls\n */\nconst VideoPlayer = ({\n src,\n poster,\n subtitles,\n title,\n subtitle: subtitleText,\n hideHeader = false,\n initialTime = 0,\n onTimeUpdate,\n onProgress,\n onVideoComplete,\n className,\n autoSave = true,\n storageKey = 'video-progress',\n userId,\n downloadContent,\n showDownloadButton = false,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n}: VideoPlayerProps) => {\n const videoRef = useRef<HTMLVideoElement>(null);\n const { isUltraSmallMobile, isTinyMobile } = useMobile();\n const [isPlaying, setIsPlaying] = useState(false);\n const [hasStarted, setHasStarted] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n // Volume e mudo vivem em um store persistido (não em estado local): o valor\n // escolhido pelo usuário precisa sobreviver à troca de vídeo/aula e valer\n // para qualquer player montado depois — inclusive o podcast —, em vez de\n // voltar a 100% a cada mídia.\n const { volume, isMuted, setVolume, setIsMuted } = useMediaVolumePreference();\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [showControls, setShowControls] = useState(true);\n const [hasCompleted, setHasCompleted] = useState(false);\n const [showCaptions, setShowCaptions] = useState(false);\n const [subtitlesValidation, setSubtitlesValidation] = useState<\n 'idle' | 'validating' | 'valid' | 'invalid'\n >('idle');\n\n /**\n * Estado da retomada desta mídia: `pending` enquanto a posição salva ainda\n * precisa ser aplicada, `done` depois que o `<video>` já a aceitou.\n *\n * É um ref, e não estado, porque é exatamente o que impede o restore de\n * rodar de novo no meio da reprodução (ver `applyResumeTime`).\n */\n const resumeStateRef = useRef<'pending' | 'done'>('pending');\n /** Espelho de `hasStarted` legível de dentro dos callbacks de mídia. */\n const hasStartedRef = useRef(false);\n\n // Reset completion flag when changing videos.\n //\n // `storageKey` is part of the identity here because two different items can\n // legitimately share the same `src` (for example, two lessons built from the\n // same recording). Keying the reset on `src` alone left `hasCompleted` set\n // when the consumer swapped to such an item, and `onVideoComplete` never\n // fired again for it.\n useEffect(() => {\n setHasCompleted(false);\n setHasStarted(false);\n hasStartedRef.current = false;\n // Mídia nova, retomada nova: é a troca de mídia — e só ela — que libera\n // um novo restore de posição.\n resumeStateRef.current = 'pending';\n }, [src, storageKey]);\n const [playbackRate, setPlaybackRate] = useState(1);\n const [showSpeedMenu, setShowSpeedMenu] = useState(false);\n const lastSaveTimeRef = useRef(0);\n const trackRef = useRef<HTMLTrackElement>(null);\n const controlsTimeoutRef = useRef<number | null>(null);\n const lastMousePositionRef = useRef({ x: 0, y: 0 });\n\n /**\n * Check if user is currently interacting with controls\n */\n const isUserInteracting = useCallback(() => {\n // Check if speed menu is open\n if (showSpeedMenu) {\n return true;\n }\n\n // Check if any control element has focus\n const activeElement = document.activeElement;\n const videoContainer = videoRef.current?.parentElement;\n\n if (activeElement && videoContainer?.contains(activeElement)) {\n // Ignore the video element itself - it should not prevent control hiding\n if (activeElement === videoRef.current) {\n return false;\n }\n\n // Check if focused element is a control (button, input, etc.)\n const isControl = activeElement.matches('button, input, [tabindex]');\n if (isControl) {\n return true;\n }\n }\n\n return false;\n }, [showSpeedMenu]);\n\n /**\n * Clear controls timeout\n */\n const clearControlsTimeout = useCallback(() => {\n if (controlsTimeoutRef.current) {\n clearTimeout(controlsTimeoutRef.current);\n controlsTimeoutRef.current = null;\n }\n }, []);\n\n /**\n * Show controls and set auto-hide timer\n */\n const showControlsWithTimer = useCallback(() => {\n setShowControls(true);\n clearControlsTimeout();\n\n // In fullscreen mode, only hide if video is playing\n if (isFullscreen) {\n if (isPlaying) {\n controlsTimeoutRef.current = globalThis.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT) as unknown as number;\n }\n } else {\n // In normal mode, always set a timer to hide controls\n controlsTimeoutRef.current = globalThis.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT) as unknown as number;\n }\n }, [isFullscreen, isPlaying, clearControlsTimeout]);\n\n /**\n * Handle mouse move with position detection\n */\n const handleMouseMove = useCallback(\n (event: MouseEvent) => {\n const currentX = event.clientX;\n const currentY = event.clientY;\n const lastPos = lastMousePositionRef.current;\n\n // Check if mouse actually moved (minimum 5px threshold)\n const hasMoved =\n Math.abs(currentX - lastPos.x) > 5 ||\n Math.abs(currentY - lastPos.y) > 5;\n\n if (hasMoved) {\n lastMousePositionRef.current = { x: currentX, y: currentY };\n showControlsWithTimer();\n }\n },\n [showControlsWithTimer]\n );\n\n /**\n * Handle mouse enter to show controls with appropriate timer logic\n */\n const handleMouseEnter = useCallback(() => {\n showControlsWithTimer();\n }, [showControlsWithTimer]);\n\n /**\n * Handle mouse leave to hide controls faster\n */\n const handleMouseLeave = useCallback(() => {\n const userInteracting = isUserInteracting();\n clearControlsTimeout();\n\n // Hide controls when mouse leaves, except when in fullscreen or user is interacting\n if (!isFullscreen && !userInteracting) {\n // Use shorter timeout when mouse leaves\n controlsTimeoutRef.current = globalThis.setTimeout(() => {\n setShowControls(false);\n }, LEAVE_HIDE_TIMEOUT) as unknown as number;\n }\n }, [isFullscreen, clearControlsTimeout, isUserInteracting]);\n\n /**\n * Apply the saved volume/mute preference to the video element.\n *\n * `src` is a dependency so a newly loaded video also starts at the volume the\n * user last chose, including when the consumer swaps the source in place\n * instead of remounting the player.\n */\n useEffect(() => {\n if (videoRef.current) {\n videoRef.current.volume = volume;\n videoRef.current.muted = isMuted;\n }\n }, [volume, isMuted, src]);\n\n /**\n * Keep the preference in sync with volume changes we did not originate\n * (native fullscreen controls on iOS, media keys, browser-level volume).\n *\n * Registered after the effect above so the browser's own restore, which\n * happens while the element is created, cannot overwrite the stored value.\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const onVolumeChange = () => {\n setVolume(video.volume);\n setIsMuted(video.muted || video.volume === 0);\n };\n\n video.addEventListener('volumechange', onVolumeChange);\n\n return () => {\n video.removeEventListener('volumechange', onVolumeChange);\n };\n }, [setVolume, setIsMuted]);\n\n /**\n * Synchronize isPlaying state with media events\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const onPlay = () => {\n setIsPlaying(true);\n setHasStarted(true);\n hasStartedRef.current = true;\n };\n const onPause = () => setIsPlaying(false);\n const onEnded = () => setIsPlaying(false);\n\n video.addEventListener('play', onPlay);\n video.addEventListener('pause', onPause);\n video.addEventListener('ended', onEnded);\n\n return () => {\n video.removeEventListener('play', onPlay);\n video.removeEventListener('pause', onPause);\n video.removeEventListener('ended', onEnded);\n };\n }, []);\n\n /**\n * Set iOS/Safari inline playback attributes imperatively\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n // Ensure inline playback on iOS/Safari\n video.setAttribute('playsinline', '');\n video.setAttribute('webkit-playsinline', '');\n }, []);\n\n /**\n * Handle controls auto-hide when play state changes\n */\n useEffect(() => {\n if (isPlaying) {\n // Start timer when video starts playing\n showControlsWithTimer();\n } else {\n // Keep controls visible when paused only in fullscreen\n clearControlsTimeout();\n if (isFullscreen) {\n setShowControls(true);\n } else {\n // In normal mode (not fullscreen), initialize timer even when paused\n // This ensures controls will hide properly from the start\n showControlsWithTimer();\n }\n }\n }, [isPlaying, isFullscreen, showControlsWithTimer, clearControlsTimeout]);\n\n /**\n * Handle fullscreen state changes from browser events\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const handleFullscreenChange = () => {\n const isCurrentlyFullscreen = !!document.fullscreenElement;\n setIsFullscreen(isCurrentlyFullscreen);\n\n // Show controls when entering fullscreen, hide after timeout if playing\n if (isCurrentlyFullscreen) {\n showControlsWithTimer();\n }\n };\n\n // Safari iOS-specific fullscreen event handlers\n const handleWebkitBeginFullscreen = () => {\n setIsFullscreen(true);\n showControlsWithTimer();\n };\n\n const handleWebkitEndFullscreen = () => {\n setIsFullscreen(false);\n };\n\n // Standard fullscreen events\n document.addEventListener('fullscreenchange', handleFullscreenChange);\n\n // Safari iOS fullscreen events\n video.addEventListener(\n 'webkitbeginfullscreen',\n handleWebkitBeginFullscreen\n );\n video.addEventListener('webkitendfullscreen', handleWebkitEndFullscreen);\n\n return () => {\n document.removeEventListener('fullscreenchange', handleFullscreenChange);\n video.removeEventListener(\n 'webkitbeginfullscreen',\n handleWebkitBeginFullscreen\n );\n video.removeEventListener(\n 'webkitendfullscreen',\n handleWebkitEndFullscreen\n );\n };\n }, [showControlsWithTimer]);\n\n /**\n * Initialize controls behavior on component mount\n * This ensures controls work correctly from the first load\n */\n useEffect(() => {\n const init = () => {\n if (!isFullscreen) {\n showControlsWithTimer();\n }\n };\n // Prefer rAF to avoid arbitrary timing if available; fall back to INIT_DELAY.\n let raf1 = 0,\n raf2 = 0,\n tid: number | undefined;\n if (globalThis.requestAnimationFrame === undefined) {\n tid = globalThis.setTimeout(init, INIT_DELAY) as unknown as number;\n return () => {\n if (tid) clearTimeout(tid);\n };\n } else {\n raf1 = requestAnimationFrame(() => {\n raf2 = requestAnimationFrame(init);\n });\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n }\n }, []); // Run only once on mount\n\n /**\n * Get initial time from props or localStorage\n */\n const getInitialTime = useCallback((): number | undefined => {\n if (!autoSave || !storageKey) {\n return Number.isFinite(initialTime) && initialTime >= 0\n ? initialTime\n : undefined;\n }\n\n const scopedKey = userId ? `${storageKey}-${userId}` : storageKey;\n const saved = Number(\n localStorage.getItem(`${scopedKey}-${src}`) || Number.NaN\n );\n const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;\n const hasValidSaved = Number.isFinite(saved) && saved >= 0;\n\n if (hasValidInitial) return initialTime;\n if (hasValidSaved) return saved;\n return undefined;\n }, [autoSave, storageKey, userId, src, initialTime]);\n\n /**\n * Última versão de `getInitialTime`, lida sem amarrar a retomada à\n * identidade do callback.\n */\n const getInitialTimeRef = useRef(getInitialTime);\n useEffect(() => {\n getInitialTimeRef.current = getInitialTime;\n }, [getInitialTime]);\n\n /**\n * Aplica a posição de retomada — no máximo uma vez por mídia, sempre antes\n * de a reprodução começar.\n *\n * O que estava acontecendo: `initialTime` é realimentado pelo consumidor com\n * o valor que ele recebe de `onTimeUpdate` (a página da aula guarda o\n * `lastWatchedTimestamp` e devolve a cada 2s). Com o restore num efeito\n * dependente de `getInitialTime` — que muda junto com `initialTime` —, cada\n * atualização dessas virava uma escrita em `video.currentTime` no meio da\n * reprodução. Toda escrita em `currentTime` é um seek: o browser descarta o\n * buffer decodificado e volta a bufferizar daquele ponto. Resultado, um\n * engasgo a cada 2 segundos num arquivo que não tem defeito nenhum.\n *\n * Por isso a retomada agora é um evento da mídia, não uma reação a props:\n * `resumeStateRef` só volta a `pending` quando `src`/`storageKey` mudam.\n */\n const applyResumeTime = useCallback(() => {\n const video = videoRef.current;\n if (!video || resumeStateRef.current === 'done') return;\n\n // Depois que o aluno deu play, a posição dele é a que vale — não a salva.\n if (hasStartedRef.current) {\n resumeStateRef.current = 'done';\n return;\n }\n\n const start = getInitialTimeRef.current();\n const isUsable =\n start !== undefined &&\n Number.isFinite(start) &&\n start > 0 &&\n // Retomar no fim deixaria o vídeo parado no último frame, com cara de\n // travado; nesse caso é melhor começar do zero.\n (!Number.isFinite(video.duration) || start < video.duration - 0.5);\n\n if (!isUsable) {\n resumeStateRef.current = 'done';\n return;\n }\n\n video.currentTime = start;\n setCurrentTime(start);\n\n // Sem metadados o browser trata a atribuição como \"posição inicial padrão\"\n // e alguns a descartam no load; só damos a retomada por concluída quando\n // existe uma linha de tempo de verdade. Até lá, `loadedmetadata` repete.\n if (video.readyState >= 1) {\n resumeStateRef.current = 'done';\n }\n }, []);\n\n // Tentativa na montagem/troca de mídia: cobre o caso em que os metadados já\n // chegaram (vídeo em cache, player remontado) e o evento não vai mais vir.\n useEffect(() => {\n applyResumeTime();\n }, [applyResumeTime, src, storageKey]);\n\n /**\n * Save progress to localStorage periodically\n */\n const saveProgress = useCallback(\n (time: number) => {\n if (!autoSave || !storageKey) return;\n\n const now = Date.now();\n if (now - lastSaveTimeRef.current > 5000) {\n const scopedKey = userId ? `${storageKey}-${userId}` : storageKey;\n localStorage.setItem(`${scopedKey}-${src}`, time.toString());\n lastSaveTimeRef.current = now;\n }\n },\n [autoSave, storageKey, userId, src]\n );\n\n /**\n * Handle play/pause toggle\n */\n const togglePlayPause = useCallback(async () => {\n const video = videoRef.current;\n if (!video) return;\n\n if (!video.paused) {\n video.pause();\n return;\n }\n\n try {\n await video.play();\n } catch {\n // Playback prevented (e.g., autoplay policy); keep state unchanged.\n }\n }, []);\n\n /**\n * Handle volume change\n */\n const handleVolumeChange = useCallback(\n (newVolume: number) => {\n const video = videoRef.current;\n if (!video) return;\n\n const volumeValue = newVolume / 100; // Convert 0-100 to 0-1\n video.volume = volumeValue;\n setVolume(volumeValue);\n\n // Auto mute/unmute based on volume\n const shouldMute = volumeValue === 0;\n const shouldUnmute = volumeValue > 0 && isMuted;\n\n if (shouldMute) {\n video.muted = true;\n setIsMuted(true);\n } else if (shouldUnmute) {\n video.muted = false;\n setIsMuted(false);\n }\n },\n [isMuted, setVolume, setIsMuted]\n );\n\n /**\n * Handle mute toggle\n */\n const toggleMute = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n if (isMuted) {\n // Unmute: restore volume or set to 50% if it was 0\n const restoreVolume = volume > 0 ? volume : UNMUTE_FALLBACK_VOLUME;\n video.volume = restoreVolume;\n video.muted = false;\n setVolume(restoreVolume);\n setIsMuted(false);\n } else {\n // Mute: set volume to 0 and mute\n video.muted = true;\n setIsMuted(true);\n }\n }, [isMuted, volume, setVolume, setIsMuted]);\n\n /**\n * Handle video seek\n */\n const handleSeek = useCallback((newTime: number) => {\n const video = videoRef.current;\n if (video && Number.isFinite(newTime)) {\n video.currentTime = newTime;\n // A barra é um input controlado: sem o valor exato aqui, o `timeupdate`\n // (que só reporta o segundo cheio) devolveria o thumb para o passo\n // anterior durante o arraste.\n setCurrentTime(newTime);\n }\n }, []);\n\n /**\n * Detect if running on Safari iOS\n */\n const isSafariIOS = useCallback((): boolean => {\n const ua = navigator.userAgent;\n const isIOS = /iPad|iPhone|iPod/.test(ua);\n const isWebKit = /WebKit/.test(ua);\n const isNotChrome = !/CriOS|Chrome/.test(ua);\n return isIOS && isWebKit && isNotChrome;\n }, []);\n\n /**\n * Handle fullscreen toggle\n */\n const toggleFullscreen = useCallback(() => {\n const video = videoRef.current;\n const container = video?.parentElement;\n if (!video || !container) return;\n\n // Safari iOS requires using webkitEnterFullscreen on video element\n if (isSafariIOS()) {\n // Type assertion for Safari-specific API\n const videoElement = video as HTMLVideoElement & {\n webkitEnterFullscreen?: () => void;\n webkitExitFullscreen?: () => void;\n webkitDisplayingFullscreen?: boolean;\n };\n\n if (!isFullscreen && videoElement.webkitEnterFullscreen) {\n videoElement.webkitEnterFullscreen();\n } else if (isFullscreen && videoElement.webkitExitFullscreen) {\n videoElement.webkitExitFullscreen();\n }\n } else if (!isFullscreen && container.requestFullscreen) {\n // Standard fullscreen API for other browsers. The returned promise can\n // reject (e.g. document not active after a tab switch); swallow it.\n container.requestFullscreen().catch(() => {});\n } else if (isFullscreen && document.exitFullscreen) {\n document.exitFullscreen().catch(() => {});\n }\n }, [isFullscreen, isSafariIOS]);\n\n /**\n * Handle playback speed change\n */\n const handleSpeedChange = useCallback((speed: number) => {\n if (videoRef.current) {\n videoRef.current.playbackRate = speed;\n setPlaybackRate(speed);\n setShowSpeedMenu(false);\n }\n }, []);\n\n /**\n * Toggle speed menu visibility\n */\n const toggleSpeedMenu = useCallback(() => {\n setShowSpeedMenu(!showSpeedMenu);\n }, [showSpeedMenu]);\n\n /**\n * Toggle captions visibility\n */\n const toggleCaptions = useCallback(() => {\n if (\n !trackRef.current?.track ||\n !subtitles ||\n subtitlesValidation !== 'valid'\n )\n return;\n\n const newShowCaptions = !showCaptions;\n setShowCaptions(newShowCaptions);\n\n // Control track mode programmatically - we already validated subtitles above\n trackRef.current.track.mode = newShowCaptions ? 'showing' : 'hidden';\n }, [showCaptions, subtitles, subtitlesValidation]);\n\n /**\n * Check video completion and fire callback\n */\n const checkVideoCompletion = useCallback(\n (progressPercent: number) => {\n if (progressPercent >= 95 && !hasCompleted) {\n setHasCompleted(true);\n onVideoComplete?.();\n }\n },\n [hasCompleted, onVideoComplete]\n );\n\n /**\n * Handle time update\n */\n const handleTimeUpdate = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const current = video.currentTime;\n\n // `timeupdate` dispara ~4x por segundo, e a UI só mostra segundos inteiros\n // (`formatTime` trunca). Re-renderizar o player a cada evento gastaria\n // três renders inúteis por segundo no mesmo thread que decodifica o vídeo\n // — custo que aparece como queda de frames. Sai apenas quando o segundo\n // exibido muda de fato; `handleSeek` cuida da precisão ao arrastar.\n setCurrentTime((previous) =>\n Math.floor(previous) === Math.floor(current) ? previous : current\n );\n\n // Save progress periodically\n saveProgress(current);\n\n // Fire callbacks\n onTimeUpdate?.(current, duration);\n\n if (duration > 0) {\n const progressPercent = (current / duration) * 100;\n onProgress?.(progressPercent);\n checkVideoCompletion(progressPercent);\n }\n }, [duration, saveProgress, onTimeUpdate, onProgress, checkVideoCompletion]);\n\n /**\n * Handle loaded metadata\n */\n const handleLoadedMetadata = useCallback(() => {\n if (!videoRef.current) return;\n\n setDuration(videoRef.current.duration);\n // Agora existe linha de tempo: é aqui que o seek de retomada realmente\n // pega, e é a última vez que ele acontece nesta mídia.\n applyResumeTime();\n }, [applyResumeTime]);\n\n /**\n * Validate subtitles URL before showing the button\n */\n useEffect(() => {\n const controller = new AbortController();\n\n const validateSubtitles = async () => {\n // If no subtitles, mark as idle\n if (!subtitles) {\n setSubtitlesValidation('idle');\n return;\n }\n\n // Start validation\n setSubtitlesValidation('validating');\n\n try {\n // Check if it's a data URL (inline VTT)\n if (subtitles.startsWith('data:')) {\n setSubtitlesValidation('valid');\n return;\n }\n\n // Fetch the subtitles file to validate it\n const response = await fetch(subtitles, {\n method: 'HEAD',\n signal: controller.signal,\n });\n\n if (response.ok) {\n // Optionally check content type\n const contentType = response.headers.get('content-type');\n const isValidType =\n !contentType ||\n contentType.includes('text/vtt') ||\n contentType.includes('text/plain') ||\n contentType.includes('application/octet-stream');\n\n if (isValidType) {\n setSubtitlesValidation('valid');\n } else {\n setSubtitlesValidation('invalid');\n console.warn(\n `Subtitles URL has invalid content type: ${contentType}`\n );\n }\n } else {\n setSubtitlesValidation('invalid');\n console.warn(\n `Subtitles URL returned status: ${response.status} ${response.statusText}`\n );\n }\n } catch (error) {\n // Ignore AbortError - it's expected when cleaning up\n if (error instanceof Error && error.name === 'AbortError') {\n return;\n }\n console.warn('Subtitles URL validation failed:', error);\n setSubtitlesValidation('invalid');\n }\n };\n\n validateSubtitles();\n\n // Cleanup: abort ongoing fetch to prevent stale updates\n return () => {\n controller.abort();\n };\n }, [subtitles]);\n\n /**\n * Initialize track mode when track is available\n */\n useEffect(() => {\n if (trackRef.current?.track) {\n // Set initial mode based on showCaptions state and subtitle availability\n trackRef.current.track.mode =\n showCaptions && subtitles && subtitlesValidation === 'valid'\n ? 'showing'\n : 'hidden';\n }\n }, [subtitles, showCaptions, subtitlesValidation]);\n\n /**\n * Handle visibility change and blur to pause video when losing focus\n */\n useEffect(() => {\n const handleVisibilityChange = () => {\n if (document.hidden && isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n const handleBlur = () => {\n if (isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n document.addEventListener('visibilitychange', handleVisibilityChange);\n globalThis.addEventListener('blur', handleBlur);\n\n return () => {\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n globalThis.removeEventListener('blur', handleBlur);\n // Clean up timers on unmount\n clearControlsTimeout();\n };\n }, [isPlaying, clearControlsTimeout]);\n\n const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0;\n\n /**\n * Get responsive icon size based on screen size\n */\n const getIconSize = useCallback(() => {\n if (isTinyMobile) return 18;\n if (isUltraSmallMobile) return 20;\n return 24;\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive padding classes for controls\n */\n const getControlsPadding = useCallback(() => {\n if (isTinyMobile) return 'px-2 pb-2 pt-1';\n if (isUltraSmallMobile) return 'px-3 pb-3 pt-1';\n return 'px-4 pb-4';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive gap classes for controls\n */\n const getControlsGap = useCallback(() => {\n if (isTinyMobile) return 'gap-1';\n if (isUltraSmallMobile) return 'gap-2';\n return 'gap-4';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive padding for progress bar\n */\n const getProgressBarPadding = useCallback(() => {\n if (isTinyMobile) return 'px-2 pb-1';\n if (isUltraSmallMobile) return 'px-3 pb-1';\n return 'px-4 pb-2';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Get responsive positioning classes for center play button\n */\n const getCenterPlayButtonPosition = useCallback(() => {\n if (isTinyMobile) return 'items-center justify-center -translate-y-12';\n if (isUltraSmallMobile) return 'items-center justify-center -translate-y-8';\n return 'items-center justify-center';\n }, [isTinyMobile, isUltraSmallMobile]);\n\n /**\n * Barra de controles visível.\n *\n * A barra some por `opacity-0`, mas continua no DOM e focável — dá para\n * chegar nela por Tab enquanto está invisível. Aí as teclas até funcionam\n * (as setas mudam o volume, por exemplo), só que nada aparece na tela e a\n * impressão é de que o controle está quebrado.\n *\n * Manter a barra acesa enquanto o foco estiver dentro dela resolve sem\n * torná-la inalcançável: escondê-la de verdade (`invisible`) impediria o Tab\n * de entrar, e aí não haveria foco para revelá-la de volta.\n */\n const [controlsHaveFocus, setControlsHaveFocus] = useState(false);\n const controlsVisible = showControls || controlsHaveFocus;\n\n /** Handlers de foco da barra; `onBlur` só apaga ao sair do subtree. */\n const controlsFocusProps = {\n onFocus: () => setControlsHaveFocus(true),\n onBlur: (event: FocusEvent<HTMLDivElement>) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n setControlsHaveFocus(false);\n }\n },\n };\n\n /**\n * Calculate top controls opacity based on state\n */\n const getTopControlsOpacity = useCallback(() => {\n return controlsVisible ? 'opacity-100' : 'opacity-0';\n }, [controlsVisible]);\n\n /**\n * Calculate bottom controls opacity based on state\n */\n const getBottomControlsOpacity = useCallback(() => {\n return controlsVisible ? 'opacity-100' : 'opacity-0';\n }, [controlsVisible]);\n\n /**\n * Seek video backward\n */\n const seekBackward = useCallback(() => {\n if (videoRef.current) {\n videoRef.current.currentTime -= 10;\n }\n }, []);\n\n /**\n * Seek video forward\n */\n const seekForward = useCallback(() => {\n if (videoRef.current) {\n videoRef.current.currentTime += 10;\n }\n }, []);\n\n /**\n * Increase volume\n */\n const increaseVolume = useCallback(() => {\n handleVolumeChange(Math.min(100, volume * 100 + 10));\n }, [handleVolumeChange, volume]);\n\n /**\n * Decrease volume\n */\n const decreaseVolume = useCallback(() => {\n handleVolumeChange(Math.max(0, volume * 100 - 10));\n }, [handleVolumeChange, volume]);\n\n /**\n * Handle video element keyboard events\n */\n const handleVideoKeyDown = useCallback(\n (e: KeyboardEvent) => {\n if (!e.key) return;\n\n // Prevent bubbling to parent handlers to avoid double toggles\n e.stopPropagation();\n showControlsWithTimer();\n\n // Map of key handlers for better maintainability and reduced complexity\n const keyHandlers: Record<string, () => void | Promise<void>> = {\n ' ': togglePlayPause,\n Enter: togglePlayPause,\n ArrowLeft: seekBackward,\n ArrowRight: seekForward,\n ArrowUp: increaseVolume,\n ArrowDown: decreaseVolume,\n m: toggleMute,\n M: toggleMute,\n f: toggleFullscreen,\n F: toggleFullscreen,\n };\n\n const handler = keyHandlers[e.key];\n if (handler) {\n e.preventDefault();\n handler();\n }\n },\n [\n showControlsWithTimer,\n togglePlayPause,\n seekBackward,\n seekForward,\n increaseVolume,\n decreaseVolume,\n toggleMute,\n toggleFullscreen,\n ]\n );\n\n const groupedSubTitleValid = subtitles && subtitlesValidation === 'valid';\n const showPoster = Boolean(poster) && !hasStarted;\n\n useCaptionPosition({\n trackRef,\n controlsVisible,\n enabled: Boolean(groupedSubTitleValid) && showCaptions,\n isUltraSmallMobile,\n isTinyMobile,\n });\n\n return (\n <div className={cn('flex flex-col', className)}>\n {/* Integrated Header */}\n {!hideHeader && (title || subtitleText) && (\n <div className=\"bg-subject-1 px-8 py-4 flex items-end justify-between min-h-20\">\n <div className=\"flex flex-col gap-1\">\n {title && (\n <Text\n as=\"h2\"\n size=\"lg\"\n weight=\"bold\"\n color=\"text-text-900\"\n className=\"leading-5 tracking-wide\"\n >\n {title}\n </Text>\n )}\n {subtitleText && (\n <Text\n as=\"p\"\n size=\"sm\"\n weight=\"normal\"\n color=\"text-text-600\"\n className=\"leading-5\"\n >\n {subtitleText}\n </Text>\n )}\n </div>\n\n {/* Download Button */}\n {showDownloadButton && downloadContent && (\n <DownloadButton\n content={downloadContent}\n lessonTitle={title}\n onDownloadStart={onDownloadStart}\n onDownloadComplete={onDownloadComplete}\n onDownloadError={onDownloadError}\n className=\"flex-shrink-0\"\n />\n )}\n </div>\n )}\n\n {/* Video Container */}\n <section\n className={cn(\n 'relative w-full bg-background overflow-hidden group',\n 'rounded-b-xl',\n // Hide cursor when controls are hidden and video is playing\n isPlaying && !showControls\n ? 'cursor-none group-hover:cursor-default'\n : 'cursor-default'\n )}\n aria-label={\n title ? `Reprodutor de vídeo: ${title}` : 'Reprodutor de vídeo'\n }\n onMouseMove={handleMouseMove}\n onMouseEnter={handleMouseEnter}\n onTouchStart={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n >\n {/* Video Element */}\n <video\n ref={videoRef}\n src={src}\n crossOrigin=\"anonymous\"\n className=\"w-full h-full object-contain analytica-video\"\n controlsList=\"nodownload\"\n playsInline\n onTimeUpdate={handleTimeUpdate}\n onLoadedMetadata={handleLoadedMetadata}\n onClick={togglePlayPause}\n onKeyDown={handleVideoKeyDown}\n tabIndex={0}\n aria-label={title ? `Vídeo: ${title}` : 'Vídeo'}\n >\n <track\n ref={trackRef}\n kind=\"captions\"\n src={\n groupedSubTitleValid\n ? subtitles\n : 'data:text/vtt;charset=utf-8,WEBVTT'\n }\n srcLang=\"pt-br\"\n label={\n groupedSubTitleValid\n ? 'Legendas em Português'\n : 'Sem legendas disponíveis'\n }\n default={false}\n />\n </video>\n\n {showPoster && (\n <img\n src={poster}\n alt={title ? `Capa: ${title}` : 'Capa do vídeo'}\n className=\"absolute inset-0 w-full h-full object-cover\"\n aria-hidden=\"true\"\n />\n )}\n\n {/* Center Play Button */}\n {!isPlaying && (\n <div\n className={cn(\n 'absolute inset-0 flex bg-black/30 transition-opacity',\n getCenterPlayButtonPosition()\n )}\n >\n <IconButton\n icon={<PlayIcon size={32} weight=\"regular\" className=\"ml-1\" />}\n onClick={togglePlayPause}\n aria-label=\"Reproduzir vídeo\"\n className=\"!bg-transparent !text-white !w-auto !h-auto hover:!bg-transparent hover:!text-gray-200\"\n />\n </div>\n )}\n\n {/* Top Controls */}\n <div\n className={cn(\n 'absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/70 to-transparent transition-opacity',\n getTopControlsOpacity()\n )}\n {...controlsFocusProps}\n >\n <div className=\"flex justify-start\">\n <IconButton\n icon={\n isFullscreen ? (\n <ArrowsInSimpleIcon size={24} />\n ) : (\n <ArrowsOutSimpleIcon size={24} />\n )\n }\n onClick={toggleFullscreen}\n aria-label={\n isFullscreen ? 'Desativar tela cheia' : 'Ativar tela cheia'\n }\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n </div>\n </div>\n\n {/* Bottom Controls */}\n <div\n className={cn(\n 'absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent transition-opacity',\n getBottomControlsOpacity()\n )}\n {...controlsFocusProps}\n >\n {/* Progress Bar */}\n <ProgressBar\n currentTime={currentTime}\n duration={duration}\n progressPercentage={progressPercentage}\n onSeek={handleSeek}\n className={getProgressBarPadding()}\n />\n\n {/* Control Buttons */}\n <div\n className={cn(\n 'flex items-center justify-between',\n getControlsPadding()\n )}\n >\n {/* Left Controls */}\n <div className={cn('flex items-center', getControlsGap())}>\n {/* Play/Pause */}\n <IconButton\n icon={\n isPlaying ? (\n <PauseIcon size={getIconSize()} />\n ) : (\n <PlayIcon size={getIconSize()} />\n )\n }\n onClick={togglePlayPause}\n aria-label={isPlaying ? 'Pausar' : 'Reproduzir'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n {/* Volume */}\n <VolumeControls\n volume={volume}\n isMuted={isMuted}\n onVolumeChange={handleVolumeChange}\n onToggleMute={toggleMute}\n iconSize={getIconSize()}\n showSlider={!isUltraSmallMobile}\n />\n\n {/* Captions - Only show after validation is complete and valid */}\n {groupedSubTitleValid && (\n <IconButton\n icon={<ClosedCaptioningIcon size={getIconSize()} />}\n onClick={toggleCaptions}\n // Mesma natureza do mudo: ação no rótulo, estado no\n // `aria-pressed` — hoje o único sinal de que as legendas\n // estão ligadas é a cor do ícone.\n aria-label={\n showCaptions ? 'Ocultar legendas' : 'Exibir legendas'\n }\n aria-pressed={showCaptions}\n className={cn(\n '!bg-transparent hover:!bg-white/20',\n showCaptions ? '!text-primary-400' : '!text-white'\n )}\n />\n )}\n\n {/* Time Display */}\n <Text size=\"sm\" weight=\"medium\" color=\"text-white\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </Text>\n </div>\n\n {/* Right Controls */}\n <div className=\"flex items-center gap-4\">\n {/* Speed Control */}\n <SpeedMenu\n showSpeedMenu={showSpeedMenu}\n playbackRate={playbackRate}\n onToggleMenu={toggleSpeedMenu}\n onSpeedChange={handleSpeedChange}\n iconSize={getIconSize()}\n isTinyMobile={isTinyMobile}\n isFullscreen={isFullscreen}\n />\n </div>\n </div>\n </div>\n </section>\n </div>\n );\n};\n\nexport default VideoPlayer;\n","import { useEffect, type RefObject } from 'react';\n\n/**\n * Vertical position of the caption box, as a percentage of the video display\n * area, for each screen range. `visible` applies while the controls bar is on\n * screen; `hidden` while it is faded out.\n *\n * The values differ per range because `line` is relative to the video height\n * while the controls bar height is nearly fixed in pixels — the same bar eats\n * ~18% of a 450px tall video and ~30% of a 170px one.\n */\nexport const CAPTION_LINE = {\n default: { visible: 78, hidden: 90 },\n ultraSmall: { visible: 68, hidden: 88 },\n tiny: { visible: 62, hidden: 86 },\n} as const;\n\ntype CaptionLineInput = {\n controlsVisible: boolean;\n isUltraSmallMobile: boolean;\n isTinyMobile: boolean;\n};\n\n/**\n * Picks the caption line for the current screen range and controls state.\n */\nexport const getCaptionLine = ({\n controlsVisible,\n isUltraSmallMobile,\n isTinyMobile,\n}: CaptionLineInput): number => {\n const range = (() => {\n if (isTinyMobile) return CAPTION_LINE.tiny;\n if (isUltraSmallMobile) return CAPTION_LINE.ultraSmall;\n return CAPTION_LINE.default;\n })();\n\n return controlsVisible ? range.visible : range.hidden;\n};\n\n/**\n * Moves every cue of the track to `line`, expressed as a percentage of the\n * video display area.\n *\n * `snapToLines = false` is what switches `line` from \"text line count\" to that\n * percentage. The cue's own `line` from the VTT file is overwritten on purpose:\n * honouring it would reinstate the bug precisely for the cues that ask to sit\n * at the bottom, which is the case being fixed.\n *\n * A missing or empty cue list is a no-op — `track.cues` is null until the VTT\n * file loads.\n */\nexport const applyCaptionLine = (\n cues: TextTrackCueList | null | undefined,\n line: number\n): void => {\n if (!cues) return;\n\n for (const cue of cues) {\n const vttCue = cue as VTTCue;\n vttCue.snapToLines = false;\n vttCue.line = line;\n }\n};\n\ntype UseCaptionPositionInput = {\n trackRef: RefObject<HTMLTrackElement | null>;\n controlsVisible: boolean;\n enabled: boolean;\n isUltraSmallMobile: boolean;\n isTinyMobile: boolean;\n};\n\n/**\n * Keeps the native caption clear of the controls bar, lifting it while the bar\n * is on screen and dropping it back when the bar fades out.\n *\n * Positioning the native cue — instead of rendering the caption ourselves —\n * is what keeps captions working inside Safari iOS native fullscreen, where\n * `webkitEnterFullscreen` hands the video over to the system player and no\n * custom overlay is drawn.\n *\n * Repositioning runs on the track's `load` event, because `track.cues` is null\n * until the VTT file arrives, and again whenever the controls state or the\n * screen range changes.\n */\nexport const useCaptionPosition = ({\n trackRef,\n controlsVisible,\n enabled,\n isUltraSmallMobile,\n isTinyMobile,\n}: UseCaptionPositionInput): void => {\n useEffect(() => {\n const trackElement = trackRef.current;\n if (!trackElement || !enabled) return;\n\n const line = getCaptionLine({\n controlsVisible,\n isUltraSmallMobile,\n isTinyMobile,\n });\n const reposition = () => applyCaptionLine(trackElement.track?.cues, line);\n\n reposition();\n trackElement.addEventListener('load', reposition);\n\n return () => trackElement.removeEventListener('load', reposition);\n }, [trackRef, controlsVisible, enabled, isUltraSmallMobile, isTinyMobile]);\n};\n"]}
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
} from "./chunk-Q3WC5SG6.mjs";
|
|
4
4
|
import {
|
|
5
5
|
SupportChatMessage
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-P7MORXSE.mjs";
|
|
7
7
|
import {
|
|
8
8
|
SupportChatEvent
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-Q2OSEROT.mjs";
|
|
10
10
|
import {
|
|
11
11
|
SUPPORT_CHAT_EVENT_TYPE
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-C6LC5LD5.mjs";
|
|
13
13
|
import {
|
|
14
14
|
Text_default
|
|
15
15
|
} from "./chunk-IMCIR6TJ.mjs";
|
|
@@ -117,4 +117,4 @@ function SupportConversationMessages({
|
|
|
117
117
|
export {
|
|
118
118
|
SupportConversationMessages
|
|
119
119
|
};
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
120
|
+
//# sourceMappingURL=chunk-RBKQXT73.mjs.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
var _react = require('react');
|
|
3
3
|
var _jsxruntime = require('react/jsx-runtime');
|
|
4
4
|
var RichEditorCore = _react.lazy.call(void 0,
|
|
5
|
-
() => Promise.resolve().then(() => _interopRequireWildcard(require("./RichEditorCore-
|
|
5
|
+
() => Promise.resolve().then(() => _interopRequireWildcard(require("./RichEditorCore-N7A2445B.js"))).then((module) => ({
|
|
6
6
|
default: module.RichEditor
|
|
7
7
|
}))
|
|
8
8
|
);
|
|
@@ -90,4 +90,4 @@ function MissingDependenciesErrorFallback(error) {
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
exports.RichEditor = RichEditor;
|
|
93
|
-
//# sourceMappingURL=chunk-
|
|
93
|
+
//# sourceMappingURL=chunk-RIN2IN7E.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-RIN2IN7E.js","../src/components/RichEditor/RichEditor.tsx"],"names":[],"mappings":"AAAA;ACAA,8BAAqD;AAiHzC,+CAAA;AA9GZ,IAAM,eAAA,EAAiB,yBAAA;AAAA,EAAK,CAAA,EAAA,GAC1B,4DAAA,CAAO,8BAAkB,GAAA,CAAE,IAAA,CAAK,CAAC,MAAA,EAAA,GAAA,CAAY;AAAA,IAC3C,OAAA,EAAS,MAAA,CAAO;AAAA,EAClB,CAAA,CAAE;AACJ,CAAA;AAqDA,IAAM,wBAAA,EAAN,MAAA,QAAsC,iBAGpC;AAAA,EACA,WAAA,CAAY,KAAA,EAA2B;AACrC,IAAA,KAAA,CAAM,KAAK,CAAA;AACX,IAAA,IAAA,CAAK,MAAA,EAAQ,EAAE,QAAA,EAAU,KAAA,EAAO,KAAA,EAAO,KAAK,CAAA;AAAA,EAC9C;AAAA,EAEA,OAAO,wBAAA,CAAyB,KAAA,EAAkC;AAChE,IAAA,OAAO,EAAE,QAAA,EAAU,IAAA,EAAM,MAAM,CAAA;AAAA,EACjC;AAAA,EAEA,MAAA,CAAA,EAAS;AACP,IAAA,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,SAAA,GAAY,IAAA,CAAK,KAAA,CAAM,KAAA,EAAO;AAC3C,MAAA,OAAO,IAAA,CAAK,KAAA,CAAM,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAAA,IAC7C;AACA,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,QAAA;AAAA,EACpB;AACF,CAAA;AAEA,IAAM,oBAAA,EAAsB;AAAA,EAC1B,eAAA;AAAA,EACA,cAAA;AAAA,EACA,qBAAA;AAAA,EACA,6BAAA;AAAA,EACA,8BAAA;AAAA,EACA,6BAAA;AAAA,EACA,+BAAA;AAAA,EACA,wBAAA;AAAA,EACA,+BAAA;AAAA,EACA,yBAAA;AAAA,EACA;AACF,CAAA;AAEA,SAAS,wBAAA,CAAyB,EAAE,MAAM,CAAA,EAA8B;AACtE,EAAA,MAAM,cAAA,EACJ,KAAA,CAAM,OAAA,CAAQ,QAAA,CAAS,SAAS,EAAA,GAChC,KAAA,CAAM,OAAA,CAAQ,QAAA,CAAS,6BAA6B,CAAA;AAEtD,EAAA,MAAM,eAAA,EAAiB,CAAA,SAAA,EAAY,mBAAA,CAAoB,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA;AACD,EAAA;AAG9C,EAAA;AAEI,oBAAA;AACZ,MAAA;AAAA,MAAA;AACW,QAAA;AACL,QAAA;AACG,QAAA;AACD,QAAA;AAEP,QAAA;AAAC,UAAA;AAAA,UAAA;AACe,YAAA;AACC,YAAA;AACF,YAAA;AACX,YAAA;AAAA,UAAA;AACJ,QAAA;AAAA,MAAA;AAEJ,IAAA;AAEE,oBAAA;AAAc,sBAAA;AAKD,sBAAA;AAMI,MAAA;AAEX,wBAAA;AAAa,0BAAA;AAGG,0BAAA;AAGlB,QAAA;AAEE,wBAAA;AAAa,0BAAA;AAGG,0BAAA;AAGlB,QAAA;AACF,MAAA;AAEJ,IAAA;AAEJ,EAAA;AAEJ;AAE2B;AAER,EAAA;AACE,oBAAA;AACA,oBAAA;AACA,oBAAA;AACjB,EAAA;AAEJ;AAEmD;AAEZ,EAAA;AAMvC;AAEkE;AACjB,EAAA;AACjD;AD5FmE;AACA;AACA;AACA","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-RIN2IN7E.js","sourcesContent":[null,"import { lazy, Suspense, Component, ReactNode } from 'react';\n\n// Lazy load the editor core to catch missing dependencies\nconst RichEditorCore = lazy(() =>\n import('./RichEditorCore').then((module) => ({\n default: module.RichEditor,\n }))\n);\n\ninterface RichEditorProps {\n readonly content?: string;\n /**\n * Notificado a cada edição do autor e, na carga, quando o `content` recebido\n * trazia cor: o editor descarta a cor ao parsear e devolve aqui o HTML limpo,\n * para que salvar sem editar nada não regrave a cor. Carregar conteúdo sem cor\n * não dispara nada.\n */\n readonly onChange?: (data: { json: object; html: string }) => void;\n readonly placeholder?: string;\n /**\n * Optional callback to generate LaTeX using AI\n * If provided, the AI generation feature will be enabled in the formula dialog\n * @param description - Natural language description of the formula\n * @returns Promise resolving to the LaTeX string\n */\n readonly onGenerateLatexWithAI?: (description: string) => Promise<string>;\n /**\n * Optional callback to upload an image file and get back its public URL.\n * If provided, the file upload tab is enabled in the image dialog; otherwise\n * images can still be inserted by URL.\n * @param file - The image file selected by the user\n * @returns Promise resolving to the public URL of the uploaded image\n */\n readonly onUploadImage?: (file: File) => Promise<string>;\n /**\n * Enables pasting an image straight from the clipboard (Ctrl+V), uploading it\n * through `onUploadImage` and inserting the resulting URL. Off by default and\n * ignored without `onUploadImage`, since there would be nowhere to put the\n * file — the editor schema rejects base64 sources.\n */\n readonly allowImagePaste?: boolean;\n /**\n * Adds a \"Vídeo\" toolbar button that embeds YouTube/Vimeo players. Off by\n * default: without it, iframes in loaded or pasted HTML are dropped as before.\n */\n readonly enableVideoEmbed?: boolean;\n /** Adds an emoji picker to the toolbar. Off by default. */\n readonly enableEmoji?: boolean;\n}\n\ninterface ErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\ninterface ErrorBoundaryProps {\n children: ReactNode;\n fallback: (error: Error) => ReactNode;\n}\n\nclass RichEditorErrorBoundary extends Component<\n ErrorBoundaryProps,\n ErrorBoundaryState\n> {\n constructor(props: ErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { hasError: true, error };\n }\n\n render() {\n if (this.state.hasError && this.state.error) {\n return this.props.fallback(this.state.error);\n }\n return this.props.children;\n }\n}\n\nconst TIPTAP_DEPENDENCIES = [\n '@tiptap/react',\n '@tiptap/core',\n '@tiptap/starter-kit',\n '@tiptap/extension-underline',\n '@tiptap/extension-text-align',\n '@tiptap/extension-subscript',\n '@tiptap/extension-superscript',\n '@tiptap/extension-link',\n '@tiptap/extension-placeholder',\n '@tiptap/extension-image',\n '@tiptap/extension-table',\n];\n\nfunction MissingDependenciesError({ error }: { readonly error: Error }) {\n const isTiptapError =\n error.message.includes('@tiptap') ||\n error.message.includes(\"Cannot find module '@tiptap\");\n\n const installCommand = `yarn add ${TIPTAP_DEPENDENCIES.join(' ')}`;\n const npmCommand = `npm install ${TIPTAP_DEPENDENCIES.join(' ')}`;\n\n return (\n <div className=\"border border-red-200 rounded-xl overflow-hidden bg-red-50 p-6\">\n <div className=\"flex items-start gap-3\">\n <div className=\"shrink-0 w-10 h-10 bg-red-100 rounded-full flex items-center justify-center\">\n <svg\n className=\"w-5 h-5 text-red-600\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <h3 className=\"text-lg font-semibold text-red-800 mb-2\">\n {isTiptapError\n ? 'Dependências do TipTap não encontradas'\n : 'Erro ao carregar o RichEditor'}\n </h3>\n <p className=\"text-red-700 text-sm mb-4\">\n {isTiptapError\n ? 'O componente RichEditor requer as dependências do TipTap para funcionar. Instale-as executando um dos comandos abaixo:'\n : error.message}\n </p>\n {isTiptapError && (\n <div className=\"space-y-3\">\n <div>\n <p className=\"text-xs text-red-600 font-medium mb-1\">\n Usando Yarn:\n </p>\n <code className=\"block bg-red-100 text-red-800 px-3 py-2 rounded text-xs font-mono overflow-x-auto\">\n {installCommand}\n </code>\n </div>\n <div>\n <p className=\"text-xs text-red-600 font-medium mb-1\">\n Usando NPM:\n </p>\n <code className=\"block bg-red-100 text-red-800 px-3 py-2 rounded text-xs font-mono overflow-x-auto\">\n {npmCommand}\n </code>\n </div>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n\nfunction LoadingFallback() {\n return (\n <div className=\"border border-border-200 rounded-xl overflow-hidden bg-background animate-pulse\">\n <div className=\"h-10 bg-background-100 border-b border-border-200\" />\n <div className=\"h-32 bg-background-50\" />\n <div className=\"h-8 bg-background-100 border-t border-border-200\" />\n </div>\n );\n}\n\nexport function RichEditor(props: RichEditorProps) {\n return (\n <RichEditorErrorBoundary fallback={MissingDependenciesErrorFallback}>\n <Suspense fallback={<LoadingFallback />}>\n <RichEditorCore {...props} />\n </Suspense>\n </RichEditorErrorBoundary>\n );\n}\n\nfunction MissingDependenciesErrorFallback(error: Readonly<Error>) {\n return <MissingDependenciesError error={error} />;\n}\n"]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { lazy, Suspense, Component } from "react";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
var RichEditorCore = lazy(
|
|
5
|
-
() => import("./RichEditorCore-
|
|
5
|
+
() => import("./RichEditorCore-2S5YUAOI.mjs").then((module) => ({
|
|
6
6
|
default: module.RichEditor
|
|
7
7
|
}))
|
|
8
8
|
);
|
|
@@ -90,4 +90,4 @@ function MissingDependenciesErrorFallback(error) {
|
|
|
90
90
|
export {
|
|
91
91
|
RichEditor
|
|
92
92
|
};
|
|
93
|
-
//# sourceMappingURL=chunk-
|
|
93
|
+
//# sourceMappingURL=chunk-RIUVPTG7.mjs.map
|
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
} from "./chunk-4D3QCMQO.mjs";
|
|
4
4
|
import {
|
|
5
5
|
DownloadModal_default
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LUHKMY56.mjs";
|
|
7
7
|
import {
|
|
8
8
|
Modal_default
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LODDYDTP.mjs";
|
|
10
10
|
import {
|
|
11
11
|
Button_default
|
|
12
12
|
} from "./chunk-ZQZBOTZJ.mjs";
|
|
@@ -90,4 +90,4 @@ export {
|
|
|
90
90
|
ReportDetailModal,
|
|
91
91
|
ReportDetailModal_default
|
|
92
92
|
};
|
|
93
|
-
//# sourceMappingURL=chunk-
|
|
93
|
+
//# sourceMappingURL=chunk-RNZGX6MX.mjs.map
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
var _chunkHPJSXD6Gjs = require('./chunk-HPJSXD6G.js');
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunk7VJP5D26js = require('./chunk-7VJP5D26.js');
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
var
|
|
9
|
+
var _chunkTQO37PVWjs = require('./chunk-TQO37PVW.js');
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
var
|
|
12
|
+
var _chunk5DINXU6Wjs = require('./chunk-5DINXU6W.js');
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
var _chunkANT66KVKjs = require('./chunk-ANT66KVK.js');
|
|
@@ -24,51 +24,51 @@ var ConversationItem = ({ item }) => {
|
|
|
24
24
|
switch (item.kind) {
|
|
25
25
|
case _chunkHPJSXD6Gjs.SUPPORT_CONVERSATION_ITEM.DATE:
|
|
26
26
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
27
|
-
|
|
27
|
+
_chunkTQO37PVWjs.SupportChatEvent,
|
|
28
28
|
{
|
|
29
|
-
type:
|
|
29
|
+
type: _chunk5DINXU6Wjs.SUPPORT_CHAT_EVENT_TYPE.DATE,
|
|
30
30
|
text: item.label
|
|
31
31
|
}
|
|
32
32
|
);
|
|
33
33
|
case _chunkHPJSXD6Gjs.SUPPORT_CONVERSATION_ITEM.SYSTEM:
|
|
34
34
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
35
|
-
|
|
35
|
+
_chunkTQO37PVWjs.SupportChatEvent,
|
|
36
36
|
{
|
|
37
|
-
type:
|
|
37
|
+
type: _chunk5DINXU6Wjs.SUPPORT_CHAT_EVENT_TYPE.NEUTRAL,
|
|
38
38
|
text: item.text,
|
|
39
39
|
time: item.time
|
|
40
40
|
}
|
|
41
41
|
);
|
|
42
42
|
case _chunkHPJSXD6Gjs.SUPPORT_CONVERSATION_ITEM.TRANSFER:
|
|
43
43
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
44
|
-
|
|
44
|
+
_chunkTQO37PVWjs.SupportChatEvent,
|
|
45
45
|
{
|
|
46
|
-
type:
|
|
46
|
+
type: _chunk5DINXU6Wjs.SUPPORT_CHAT_EVENT_TYPE.TRANSFER,
|
|
47
47
|
text: item.text,
|
|
48
48
|
time: item.time
|
|
49
49
|
}
|
|
50
50
|
);
|
|
51
51
|
case _chunkHPJSXD6Gjs.SUPPORT_CONVERSATION_ITEM.AI:
|
|
52
52
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
53
|
-
|
|
53
|
+
_chunkTQO37PVWjs.SupportChatEvent,
|
|
54
54
|
{
|
|
55
|
-
type:
|
|
55
|
+
type: _chunk5DINXU6Wjs.SUPPORT_CHAT_EVENT_TYPE.AI,
|
|
56
56
|
text: item.text,
|
|
57
57
|
time: item.time
|
|
58
58
|
}
|
|
59
59
|
);
|
|
60
60
|
case _chunkHPJSXD6Gjs.SUPPORT_CONVERSATION_ITEM.DONE:
|
|
61
61
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
62
|
-
|
|
62
|
+
_chunkTQO37PVWjs.SupportChatEvent,
|
|
63
63
|
{
|
|
64
|
-
type:
|
|
64
|
+
type: _chunk5DINXU6Wjs.SUPPORT_CHAT_EVENT_TYPE.SUCCESS,
|
|
65
65
|
text: item.text,
|
|
66
66
|
time: item.time
|
|
67
67
|
}
|
|
68
68
|
);
|
|
69
69
|
default:
|
|
70
70
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
71
|
-
|
|
71
|
+
_chunk7VJP5D26js.SupportChatMessage,
|
|
72
72
|
{
|
|
73
73
|
type: item.type,
|
|
74
74
|
text: item.text,
|
|
@@ -117,4 +117,4 @@ function SupportConversationMessages({
|
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
exports.SupportConversationMessages = SupportConversationMessages;
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
120
|
+
//# sourceMappingURL=chunk-RXEU77VN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-RXEU77VN.js","../src/components/SupportConversation/SupportConversationMessages.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-RXEU77VN.js","sourcesContent":[null,"import { useEffect, useRef } from 'react';\nimport Text from '../Text/Text';\nimport SupportChatMessage from '../SupportChat/SupportChatMessage';\nimport SupportChatEvent from '../SupportChat/SupportChatEvent';\nimport { cn } from '../../utils/utils';\nimport { SUPPORT_CHAT_EVENT_TYPE } from '../../types/supportChat';\nimport {\n SUPPORT_CONVERSATION_ITEM,\n type SupportConversationItem,\n} from '../../types/supportConversation';\n\n/**\n * Props of the SupportConversationMessages component\n */\nexport interface SupportConversationMessagesProps {\n /** Timeline in display order (oldest first) */\n items: SupportConversationItem[];\n /** First load: shows the loading text instead of the timeline */\n loading?: boolean;\n /** Text when the ticket has no item yet */\n emptyText?: string;\n className?: string;\n}\n\n/** Renders one item of the timeline with the matching chat piece */\nconst ConversationItem = ({ item }: { item: SupportConversationItem }) => {\n switch (item.kind) {\n case SUPPORT_CONVERSATION_ITEM.DATE:\n return (\n <SupportChatEvent\n type={SUPPORT_CHAT_EVENT_TYPE.DATE}\n text={item.label}\n />\n );\n case SUPPORT_CONVERSATION_ITEM.SYSTEM:\n return (\n <SupportChatEvent\n type={SUPPORT_CHAT_EVENT_TYPE.NEUTRAL}\n text={item.text}\n time={item.time}\n />\n );\n case SUPPORT_CONVERSATION_ITEM.TRANSFER:\n return (\n <SupportChatEvent\n type={SUPPORT_CHAT_EVENT_TYPE.TRANSFER}\n text={item.text}\n time={item.time}\n />\n );\n case SUPPORT_CONVERSATION_ITEM.AI:\n return (\n <SupportChatEvent\n type={SUPPORT_CHAT_EVENT_TYPE.AI}\n text={item.text}\n time={item.time}\n />\n );\n case SUPPORT_CONVERSATION_ITEM.DONE:\n return (\n <SupportChatEvent\n type={SUPPORT_CHAT_EVENT_TYPE.SUCCESS}\n text={item.text}\n time={item.time}\n />\n );\n default:\n return (\n <SupportChatMessage\n type={item.type}\n text={item.text}\n time={item.time}\n author={item.author}\n faq={item.faq}\n attachments={item.attachments}\n status={item.status}\n onRetry={item.onRetry}\n />\n );\n }\n};\n\n/**\n * Scrollable timeline of the support conversation: date separators, system\n * and handoff events and the messages of the user, the AI, the agents and\n * the internal notes.\n *\n * It is the only part of the conversation that scrolls, and it starts at the\n * latest message: it scrolls to the bottom on mount and whenever the\n * timeline grows.\n */\nexport default function SupportConversationMessages({\n items,\n loading = false,\n emptyText = 'Nenhuma mensagem neste atendimento ainda.',\n className,\n}: Readonly<SupportConversationMessagesProps>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const scroll = scrollRef.current;\n scroll?.scrollTo?.({ top: scroll.scrollHeight });\n }, [items.length, loading]);\n\n return (\n <div\n ref={scrollRef}\n role=\"log\"\n aria-label=\"Mensagens do atendimento\"\n aria-busy={loading}\n data-testid=\"support-conversation-messages\"\n className={cn(\n 'flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-6 py-4',\n className\n )}\n >\n {loading && (\n <Text size=\"sm\" color=\"text-text-500\" className=\"text-center\">\n Carregando conversa…\n </Text>\n )}\n {!loading && items.length === 0 && (\n <Text size=\"sm\" color=\"text-text-500\" className=\"text-center\">\n {emptyText}\n </Text>\n )}\n {!loading &&\n items.map((item) => <ConversationItem key={item.id} item={item} />)}\n </div>\n );\n}\n"]}
|