xertica-ui 2.5.0 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +56 -17
- package/assets/xertica-logo.svg +37 -37
- package/assets/xertica-x-logo.svg +20 -20
- package/bin/cli.ts +14 -2
- package/bin/generate-tokens.ts +262 -262
- package/bin/language-config.ts +359 -358
- package/components/assistant/code-block/CodeBlock.tsx +268 -268
- package/components/assistant/formatted-document/FormattedDocument.tsx +147 -147
- package/components/assistant/modern-chat-input/ModernChatInput.tsx +564 -564
- package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +99 -99
- package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -104
- package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -81
- package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +88 -88
- package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +75 -75
- package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +564 -564
- package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +67 -67
- package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +103 -103
- package/components/assistant/xertica-assistant/use-assistant.ts +615 -615
- package/components/assistant/xertica-assistant/xertica-assistant.tsx +611 -611
- package/components/blocks/card-patterns/ActivityCard.tsx +100 -100
- package/components/blocks/card-patterns/ActivityCardSkeleton.tsx +56 -56
- package/components/blocks/card-patterns/FeatureCardSkeleton.tsx +58 -58
- package/components/blocks/card-patterns/NotificationCard.tsx +140 -140
- package/components/blocks/card-patterns/NotificationCardSkeleton.tsx +81 -81
- package/components/blocks/card-patterns/ProfileCard.tsx +112 -112
- package/components/blocks/card-patterns/ProfileCardSkeleton.tsx +69 -69
- package/components/blocks/card-patterns/ProjectCard.tsx +123 -123
- package/components/blocks/card-patterns/ProjectCardSkeleton.tsx +67 -67
- package/components/blocks/card-patterns/QuickActionCardSkeleton.tsx +44 -44
- package/components/blocks/card-patterns/card-patterns.stories.tsx +594 -594
- package/components/blocks/card-patterns/index.ts +29 -29
- package/components/brand/language-selector/LanguageSelector.tsx +102 -102
- package/components/brand/language-selector/language-selector.stories.tsx +111 -111
- package/components/brand/language-selector/language-selector.test.tsx +101 -101
- package/components/brand/theme-toggle/ThemeToggle.tsx +74 -74
- package/components/brand/xertica-provider/xertica-provider.mdx +61 -61
- package/components/index.ts +86 -86
- package/components/layout/sidebar/sidebar.mdx +1 -1
- package/components/layout/sidebar/sidebar.stories.tsx +1033 -787
- package/components/layout/sidebar/sidebar.tsx +338 -1
- package/components/media/FloatingMediaWrapper.tsx +371 -371
- package/components/media/audio-player/AudioPlayer.tsx +768 -768
- package/components/media/video-player/VideoPlayer.tsx +310 -310
- package/components/pages/home-content/HomeContent.tsx +120 -120
- package/components/pages/home-content/home-content.mdx +62 -62
- package/components/pages/home-page/HomePage.tsx +78 -78
- package/components/pages/home-page/home-page.mdx +53 -53
- package/components/pages/template-content/TemplateContent.tsx +1354 -1354
- package/components/pages/template-content/template-content.mdx +61 -61
- package/components/pages/template-page/TemplatePage.stories.tsx +32 -32
- package/components/pages/template-page/template-page.mdx +53 -53
- package/components/shared/error-boundary.stories.tsx +114 -114
- package/components/shared/error-boundary.tsx +150 -150
- package/components/shared/error-fallbacks.tsx +222 -222
- package/components/ui/accordion/accordion.mdx +8 -8
- package/components/ui/alert/alert.mdx +8 -8
- package/components/ui/alert-dialog/alert-dialog.mdx +8 -8
- package/components/ui/aspect-ratio/aspect-ratio.mdx +8 -8
- package/components/ui/assistant-chart/assistant-chart.mdx +8 -8
- package/components/ui/avatar/avatar.mdx +8 -8
- package/components/ui/badge/badge.mdx +8 -8
- package/components/ui/breadcrumb/breadcrumb.mdx +8 -8
- package/components/ui/button/button.mdx +8 -8
- package/components/ui/calendar/calendar.mdx +8 -8
- package/components/ui/card/card.mdx +8 -8
- package/components/ui/carousel/carousel.mdx +8 -8
- package/components/ui/chart/chart.mdx +8 -8
- package/components/ui/chart/chart.test.tsx +178 -178
- package/components/ui/chart/chart.tsx +2245 -2239
- package/components/ui/checkbox/checkbox.mdx +8 -8
- package/components/ui/collapsible/collapsible.mdx +8 -8
- package/components/ui/command/command.mdx +8 -8
- package/components/ui/context-menu/context-menu.mdx +8 -8
- package/components/ui/dialog/dialog.mdx +8 -8
- package/components/ui/drawer/drawer.mdx +8 -8
- package/components/ui/dropdown-menu/dropdown-menu.mdx +8 -8
- package/components/ui/empty/empty.mdx +8 -8
- package/components/ui/file-upload/file-upload.mdx +8 -8
- package/components/ui/hover-card/hover-card.mdx +8 -8
- package/components/ui/input/input.mdx +8 -8
- package/components/ui/input-otp/input-otp.mdx +8 -8
- package/components/ui/label/label.mdx +8 -8
- package/components/ui/map/map.mdx +8 -8
- package/components/ui/menubar/menubar.mdx +8 -8
- package/components/ui/navigation-menu/navigation-menu.mdx +8 -8
- package/components/ui/notification-badge/notification-badge.mdx +8 -8
- package/components/ui/pagination/pagination.mdx +8 -8
- package/components/ui/popover/popover.mdx +8 -8
- package/components/ui/progress/progress.mdx +8 -8
- package/components/ui/radio-group/radio-group.mdx +8 -8
- package/components/ui/rating/rating.mdx +8 -8
- package/components/ui/resizable/resizable.mdx +8 -8
- package/components/ui/route-map/route-map.mdx +8 -8
- package/components/ui/scroll-area/scroll-area.mdx +8 -8
- package/components/ui/search/search.mdx +8 -8
- package/components/ui/select/select.mdx +8 -8
- package/components/ui/separator/separator.mdx +8 -8
- package/components/ui/sheet/sheet.mdx +8 -8
- package/components/ui/simple-map/simple-map.mdx +8 -8
- package/components/ui/skeleton/skeleton.mdx +8 -8
- package/components/ui/slider/slider.mdx +8 -8
- package/components/ui/sonner/sonner.mdx +8 -8
- package/components/ui/stats-card/index.ts +2 -2
- package/components/ui/stats-card/stats-card-skeleton.tsx +60 -60
- package/components/ui/stats-card/stats-card.mdx +8 -8
- package/components/ui/stats-card/stats-card.tsx +109 -109
- package/components/ui/stepper/stepper.mdx +8 -8
- package/components/ui/switch/switch.mdx +8 -8
- package/components/ui/table/table.mdx +8 -8
- package/components/ui/tabs/tabs.mdx +8 -8
- package/components/ui/textarea/textarea.mdx +8 -8
- package/components/ui/timeline/timeline.mdx +8 -8
- package/components/ui/toggle/toggle.mdx +8 -8
- package/components/ui/toggle-group/toggle-group.mdx +8 -8
- package/components/ui/tooltip/tooltip.mdx +8 -8
- package/components/ui/tree-view/tree-view.mdx +8 -8
- package/components.json +511 -511
- package/contexts/AuthContext.tsx +121 -121
- package/contexts/BrandColorsContext.tsx +282 -282
- package/contexts/LanguageContext.test.tsx +121 -121
- package/contexts/LanguageContext.tsx +250 -250
- package/contexts/theme-data.ts +391 -391
- package/dist/{AssistantChart-DoZCyS5r.cjs → AssistantChart-9w31gdAb.cjs} +4 -4
- package/dist/{AssistantChart-CldVCVDe.cjs → AssistantChart-BAudAfne.cjs} +5 -5
- package/dist/{AssistantChart-Bdd44uBn.cjs → AssistantChart-BAx9VQvb.cjs} +127 -388
- package/dist/{AssistantChart-Cu3m7RBo.js → AssistantChart-BP8upjMk.js} +5 -5
- package/dist/{AssistantChart-CFhDdGyU.js → AssistantChart-CVko2A1W.js} +130 -391
- package/dist/{AssistantChart-C_hwFRRr.js → AssistantChart-CVzmmhx4.js} +4 -4
- package/dist/{AudioPlayer-IAU5q5T1.cjs → AudioPlayer-1ypwE2Wh.cjs} +1 -1
- package/dist/{AudioPlayer-CGRUtUdN.js → AudioPlayer-DuKXrCfy.js} +1 -1
- package/dist/{LanguageContext-CS14yCpi.js → LanguageContext-BwhwC3G2.js} +2 -2
- package/dist/{LanguageContext-B_KFTCzT.cjs → LanguageContext-DvUt5jBg.cjs} +2 -2
- package/dist/{ThemeContext-C2EwAPDt.js → ThemeContext-BbBNoFTG.js} +2 -2
- package/dist/{ThemeContext-Bmod0Cg2.cjs → ThemeContext-BblcjQup.cjs} +13 -8
- package/dist/{ThemeContext-BWq9ACPo.js → ThemeContext-Bo-W2WZH.js} +13 -8
- package/dist/{ThemeContext-j5aGtPky.cjs → ThemeContext-CP3a0jxy.cjs} +193 -262
- package/dist/{ThemeContext-vTjumZeM.cjs → ThemeContext-Cmr8Ex8H.cjs} +2 -2
- package/dist/ThemeContext-CpqYShLq.cjs +324 -0
- package/dist/{ThemeContext-CQSo4Iwc.js → ThemeContext-D3LzacmG.js} +8 -1
- package/dist/ThemeContext-Du2nE1PL.js +325 -0
- package/dist/ThemeContext-GeEBTJ3q.cjs +1621 -0
- package/dist/ThemeContext-JyLK9B1o.js +1622 -0
- package/dist/{ThemeContext-CGk3KK0k.cjs → ThemeContext-U4dEYc6C.cjs} +8 -1
- package/dist/{ThemeContext-BXjrgUjW.js → ThemeContext-ept8jhXI.js} +200 -261
- package/dist/{VerifyEmailPage-CGIwmWrm.js → VerifyEmailPage-B31mCrMc.js} +1 -1
- package/dist/{VerifyEmailPage-C0c2e5n0.js → VerifyEmailPage-BE-L9mB7.js} +7 -7
- package/dist/{VerifyEmailPage-DSBMRHtl.js → VerifyEmailPage-BIBOKV7Z.js} +41 -36
- package/dist/{VerifyEmailPage-DgIid028.js → VerifyEmailPage-BJjAMUTW.js} +4 -4
- package/dist/{VerifyEmailPage--1Vurewl.cjs → VerifyEmailPage-BRSP-Pwt.cjs} +3 -3
- package/dist/{VerifyEmailPage-Cwi3kbol.cjs → VerifyEmailPage-Bae2cBXT.cjs} +7 -7
- package/dist/{VerifyEmailPage-De6bQjrz.cjs → VerifyEmailPage-BiRm7Nh4.cjs} +41 -36
- package/dist/{VerifyEmailPage-ByerOcm4.cjs → VerifyEmailPage-Bv8Ah_TK.cjs} +23 -20
- package/dist/VerifyEmailPage-Bvfv8HVQ.js +3214 -0
- package/dist/{VerifyEmailPage-BComraR7.cjs → VerifyEmailPage-CR7kb5df.cjs} +22 -12
- package/dist/{VerifyEmailPage-CpqqpLpo.cjs → VerifyEmailPage-C_Zk6Gen.cjs} +1 -1
- package/dist/{VerifyEmailPage-MTD7AG1Z.js → VerifyEmailPage-C_ihbcth.js} +4 -4
- package/dist/{VerifyEmailPage-1WwWczAn.js → VerifyEmailPage-CbgjOF0v.js} +22 -12
- package/dist/{VerifyEmailPage-DvMLZgFt.js → VerifyEmailPage-CdYPSJoO.js} +1 -1
- package/dist/{VerifyEmailPage-By3Jf__L.cjs → VerifyEmailPage-CkBYfsNy.cjs} +4 -4
- package/dist/{VerifyEmailPage-CJLz3jrn.js → VerifyEmailPage-Cyl55sJb.js} +23 -20
- package/dist/VerifyEmailPage-D-FRj5TU.cjs +3213 -0
- package/dist/{VerifyEmailPage-B4peJjAT.cjs → VerifyEmailPage-DF2ilhum.cjs} +334 -356
- package/dist/{VerifyEmailPage-CYXtbKi3.cjs → VerifyEmailPage-DMBh4NM9.cjs} +1 -1
- package/dist/{VerifyEmailPage-CgMxRb4z.js → VerifyEmailPage-DTtFfC-J.js} +3 -3
- package/dist/{VerifyEmailPage-CFLMls1p.cjs → VerifyEmailPage-Dt7zgA4w.cjs} +4 -4
- package/dist/{VerifyEmailPage-C5TNQTBa.js → VerifyEmailPage-EhudUdqF.js} +343 -355
- package/dist/{VerifyEmailPage-DGhuIqkb.js → VerifyEmailPage-X14vhdyl.js} +4 -4
- package/dist/VerifyEmailPage-hdB8JQGv.cjs +3213 -0
- package/dist/{VerifyEmailPage-Bp1XXl3H.cjs → VerifyEmailPage-u_Dn7t1U.cjs} +4 -4
- package/dist/VerifyEmailPage-vYHbYK3q.js +3214 -0
- package/dist/{XerticaProvider-CBGc4EMA.cjs → XerticaProvider-AChwphCO.cjs} +4 -4
- package/dist/{XerticaProvider-BIrqfZ-i.cjs → XerticaProvider-AbWlr7Af.cjs} +8 -11
- package/dist/{XerticaProvider-D-yNhF94.cjs → XerticaProvider-B8CaV7xu.cjs} +1 -1
- package/dist/{XerticaProvider-CEoWMTxu.js → XerticaProvider-BITjgC5p.js} +2 -2
- package/dist/{XerticaProvider-CllrbMEJ.cjs → XerticaProvider-By8q3Roe.cjs} +2 -2
- package/dist/{XerticaProvider-C1DKnvLh.js → XerticaProvider-CUYJZc32.js} +4 -4
- package/dist/{XerticaProvider-ET0ihewn.cjs → XerticaProvider-CW9hpCdF.cjs} +2 -2
- package/dist/{XerticaProvider-Dt5HEzbQ.js → XerticaProvider-CWgby5mY.js} +10 -10
- package/dist/XerticaProvider-CWs6EwNa.js +49 -0
- package/dist/XerticaProvider-CjQAQPcn.cjs +48 -0
- package/dist/XerticaProvider-D5lLumH-.js +49 -0
- package/dist/{XerticaProvider-DYq4JWtg.js → XerticaProvider-DQtvJU7m.js} +1 -1
- package/dist/XerticaProvider-qQUDop71.cjs +48 -0
- package/dist/{XerticaProvider-B7EVH-NF.js → XerticaProvider-siSt9uG2.js} +2 -2
- package/dist/{XerticaXLogo-Zw2B276b.cjs → XerticaXLogo-8TTzBjHw.cjs} +1 -1
- package/dist/{XerticaXLogo-B7xQ5dhi.js → XerticaXLogo-BWaag64t.js} +1 -1
- package/dist/{XerticaXLogo-DZbo4vOE.js → XerticaXLogo-CFuIlYFH.js} +12 -12
- package/dist/{XerticaXLogo-bvZSgwGF.cjs → XerticaXLogo-CU-U-GP4.cjs} +7 -13
- package/dist/XerticaXLogo-ChryA6xj.js +252 -0
- package/dist/{XerticaXLogo-CQUUjXoH.cjs → XerticaXLogo-CziKMQil.cjs} +8 -8
- package/dist/XerticaXLogo-DHz5SugF.js +252 -0
- package/dist/XerticaXLogo-DTee_y8X.cjs +251 -0
- package/dist/{XerticaXLogo-Cmsp-Eey.js → XerticaXLogo-DfUvz-lD.js} +9 -9
- package/dist/XerticaXLogo-kslQ8Tk_.cjs +251 -0
- package/dist/{alert-dialog-s-vmNkJ_.js → alert-dialog-iDe5VE5o.js} +3 -3
- package/dist/{alert-dialog-DSKByiKZ.cjs → alert-dialog-yckpaOpy.cjs} +3 -3
- package/dist/cli.js +16 -6
- package/dist/components/ui/chart/chart.d.ts +7 -5
- package/dist/{google-maps-loader-Y-QkD-Li.cjs → google-maps-loader-BqsYL48U.cjs} +0 -5
- package/dist/{google-maps-loader-CTYySAun.js → google-maps-loader-t2IlYBzw.js} +0 -4
- package/dist/index-CkTUgOwX.js +8 -0
- package/dist/{index-COtD8bRW.cjs → index-D3RLKRAs.cjs} +1 -1
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +2 -2
- package/dist/index.umd.js +454 -1027
- package/dist/layout.cjs.js +1 -1
- package/dist/layout.es.js +1 -1
- package/dist/pages.cjs.js +1 -1
- package/dist/pages.es.js +1 -1
- package/dist/{sidebar-DAaY8bRU.cjs → sidebar-B3EYhli0.cjs} +33 -24
- package/dist/{sidebar-nzPoVHBQ.cjs → sidebar-B9NR0lCe.cjs} +46 -41
- package/dist/{sidebar-CeTMuzOx.cjs → sidebar-BvF5I2Ue.cjs} +47 -128
- package/dist/{sidebar-q7P2Godd.cjs → sidebar-C5B_LHek.cjs} +1 -1
- package/dist/{sidebar-CrQDDdcz.js → sidebar-CA6_ek3f.js} +33 -24
- package/dist/sidebar-CLmIjgNd.cjs +1136 -0
- package/dist/{sidebar-BxGXsDAd.cjs → sidebar-CVUGHOS_.cjs} +8 -16
- package/dist/{sidebar-BViy8Eeu.js → sidebar-CmvwjnVb.js} +9 -17
- package/dist/{sidebar-B6SlKZYN.js → sidebar-CplprZpM.js} +49 -40
- package/dist/sidebar-Duermn32.js +1133 -0
- package/dist/{sidebar-BbVIQvlP.js → sidebar-Dz7bd3zP.js} +1 -1
- package/dist/{sidebar-0ocFLSks.js → sidebar-KIS0C2JH.js} +50 -127
- package/dist/sidebar-OTO_up7Z.js +801 -0
- package/dist/sidebar-zowjejT2.cjs +800 -0
- package/dist/{use-audio-player-nv8ZSGa1.js → use-audio-player-Bkh23vQ3.js} +3 -7
- package/dist/{use-audio-player-NKsWyjWu.cjs → use-audio-player-Dn1NR9xN.cjs} +3 -7
- package/dist/{xertica-assistant-dyP7KHM5.cjs → xertica-assistant-B1IaHXnB.cjs} +388 -529
- package/dist/{xertica-assistant-ciJaWqm1.js → xertica-assistant-BMqdyRVi.js} +10 -28
- package/dist/{xertica-assistant-V_IdW4WF.cjs → xertica-assistant-Bj3vBCq_.cjs} +9 -27
- package/dist/{xertica-assistant-yX1CFBBo.js → xertica-assistant-DPsESB6t.js} +390 -531
- package/dist/{CodeBlock-7TTgmdGG.cjs → xertica-assistant-Qp3ydksa.cjs} +51 -263
- package/dist/{CodeBlock-BeSt1h5P.js → xertica-assistant-gnCJdcZY.js} +7 -219
- package/dist/xertica-ui.css +2 -2
- package/docs/architecture-improvements.md +456 -456
- package/docs/architecture.md +312 -312
- package/docs/components/assistant.md +428 -428
- package/docs/components/branding.md +252 -252
- package/docs/components/card-patterns.md +447 -447
- package/docs/components/error-boundary.md +201 -201
- package/docs/components/hooks.md +432 -432
- package/docs/components/language-selector.md +176 -176
- package/docs/components/pages.md +323 -323
- package/docs/components/sidebar.md +331 -331
- package/docs/components/stats-card.md +138 -138
- package/docs/doc-audit.md +244 -244
- package/docs/getting-started.md +616 -616
- package/docs/guidelines.md +330 -330
- package/docs/i18n.md +480 -480
- package/docs/installation.md +268 -268
- package/docs/llms.md +295 -295
- package/docs/state-management.md +289 -289
- package/guidelines/Guidelines.md +409 -409
- package/llms-compact.txt +1 -1
- package/llms-full.txt +10688 -10688
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/styles/xertica/base.css +90 -90
- package/styles/xertica/tokens.css +240 -240
- package/templates/.prettierignore +4 -4
- package/templates/.prettierrc +10 -10
- package/templates/CLAUDE.md +180 -180
- package/templates/package.json +2 -2
- package/templates/src/app/App.tsx +46 -46
- package/templates/src/app/components/AuthGuard.tsx +131 -131
- package/templates/src/features/assistant/data/mock.ts +75 -75
- package/templates/src/features/assistant/hooks/useAssistantConfig.ts +20 -20
- package/templates/src/features/assistant/index.ts +5 -5
- package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +70 -70
- package/templates/src/features/auth/ui/LoginContent.tsx +92 -92
- package/templates/src/features/auth/ui/ResetPasswordContent.tsx +183 -183
- package/templates/src/features/auth/ui/SocialLoginButtons.tsx +78 -78
- package/templates/src/features/auth/ui/VerifyEmailContent.tsx +80 -80
- package/templates/src/features/home/data/mock.ts +41 -41
- package/templates/src/features/home/hooks/useFeatureCards.ts +20 -20
- package/templates/src/features/home/index.ts +11 -11
- package/templates/src/features/home/ui/HomeContent.tsx +117 -117
- package/templates/src/features/template/ui/CrudTemplate.tsx +112 -112
- package/templates/src/features/template/ui/DashboardTemplate.tsx +110 -110
- package/templates/src/features/template/ui/FormTemplate.tsx +117 -117
- package/templates/src/features/template/ui/LoginTemplate.tsx +59 -59
- package/templates/src/features/template/ui/TemplateContent.tsx +1322 -1322
- package/templates/src/i18n.ts +124 -124
- package/templates/src/locales/en/common.json +21 -21
- package/templates/src/locales/en/components/activityCard.json +10 -10
- package/templates/src/locales/en/components/assistant.json +119 -119
- package/templates/src/locales/en/components/media.json +29 -29
- package/templates/src/locales/en/components/notificationCard.json +5 -5
- package/templates/src/locales/en/components/profileCard.json +8 -8
- package/templates/src/locales/en/components/projectCard.json +10 -10
- package/templates/src/locales/en/components/sidebar.json +14 -14
- package/templates/src/locales/en/components/stats.json +8 -8
- package/templates/src/locales/en/components/team.json +14 -14
- package/templates/src/locales/en/errors.json +9 -9
- package/templates/src/locales/en/languageSelector.json +7 -7
- package/templates/src/locales/en/nav.json +6 -6
- package/templates/src/locales/en/pages/crudTemplate.json +25 -25
- package/templates/src/locales/en/pages/dashboardTemplate.json +20 -20
- package/templates/src/locales/en/pages/forgotPassword.json +10 -10
- package/templates/src/locales/en/pages/formTemplate.json +16 -16
- package/templates/src/locales/en/pages/home.json +7 -7
- package/templates/src/locales/en/pages/login.json +15 -15
- package/templates/src/locales/en/pages/loginTemplate.json +9 -9
- package/templates/src/locales/en/pages/resetPassword.json +18 -18
- package/templates/src/locales/en/pages/templates.json +317 -317
- package/templates/src/locales/en/pages/verifyEmail.json +12 -12
- package/templates/src/locales/en/themeToggle.json +6 -6
- package/templates/src/locales/es/common.json +21 -21
- package/templates/src/locales/es/components/activityCard.json +10 -10
- package/templates/src/locales/es/components/assistant.json +119 -119
- package/templates/src/locales/es/components/media.json +29 -29
- package/templates/src/locales/es/components/notificationCard.json +5 -5
- package/templates/src/locales/es/components/profileCard.json +8 -8
- package/templates/src/locales/es/components/projectCard.json +10 -10
- package/templates/src/locales/es/components/sidebar.json +14 -14
- package/templates/src/locales/es/components/stats.json +8 -8
- package/templates/src/locales/es/components/team.json +14 -14
- package/templates/src/locales/es/errors.json +9 -9
- package/templates/src/locales/es/languageSelector.json +7 -7
- package/templates/src/locales/es/nav.json +6 -6
- package/templates/src/locales/es/pages/crudTemplate.json +25 -25
- package/templates/src/locales/es/pages/dashboardTemplate.json +20 -20
- package/templates/src/locales/es/pages/forgotPassword.json +10 -10
- package/templates/src/locales/es/pages/formTemplate.json +16 -16
- package/templates/src/locales/es/pages/home.json +7 -7
- package/templates/src/locales/es/pages/login.json +15 -15
- package/templates/src/locales/es/pages/loginTemplate.json +9 -9
- package/templates/src/locales/es/pages/resetPassword.json +18 -18
- package/templates/src/locales/es/pages/templates.json +317 -317
- package/templates/src/locales/es/pages/verifyEmail.json +12 -12
- package/templates/src/locales/es/themeToggle.json +6 -6
- package/templates/src/locales/pt-BR/common.json +21 -21
- package/templates/src/locales/pt-BR/components/activityCard.json +10 -10
- package/templates/src/locales/pt-BR/components/assistant.json +119 -119
- package/templates/src/locales/pt-BR/components/media.json +29 -29
- package/templates/src/locales/pt-BR/components/notificationCard.json +5 -5
- package/templates/src/locales/pt-BR/components/profileCard.json +8 -8
- package/templates/src/locales/pt-BR/components/projectCard.json +10 -10
- package/templates/src/locales/pt-BR/components/sidebar.json +14 -14
- package/templates/src/locales/pt-BR/components/stats.json +8 -8
- package/templates/src/locales/pt-BR/components/team.json +14 -14
- package/templates/src/locales/pt-BR/errors.json +9 -9
- package/templates/src/locales/pt-BR/languageSelector.json +7 -7
- package/templates/src/locales/pt-BR/nav.json +6 -6
- package/templates/src/locales/pt-BR/pages/crudTemplate.json +25 -25
- package/templates/src/locales/pt-BR/pages/dashboardTemplate.json +20 -20
- package/templates/src/locales/pt-BR/pages/forgotPassword.json +10 -10
- package/templates/src/locales/pt-BR/pages/formTemplate.json +16 -16
- package/templates/src/locales/pt-BR/pages/home.json +7 -7
- package/templates/src/locales/pt-BR/pages/login.json +15 -15
- package/templates/src/locales/pt-BR/pages/loginTemplate.json +9 -9
- package/templates/src/locales/pt-BR/pages/resetPassword.json +18 -18
- package/templates/src/locales/pt-BR/pages/templates.json +317 -317
- package/templates/src/locales/pt-BR/pages/verifyEmail.json +12 -12
- package/templates/src/locales/pt-BR/themeToggle.json +6 -6
- package/templates/src/pages/AssistantPage.tsx +470 -470
- package/templates/src/pages/HomePage.tsx +53 -53
- package/templates/src/shared/error-boundary.tsx +150 -150
- package/templates/src/shared/error-fallbacks.tsx +222 -222
- package/templates/src/styles/xertica/tokens.css +240 -240
- package/templates/vite.config.js +20 -20
- package/templates/vite.config.ts +55 -55
- package/dist/AssistantChart-BKVtGUKF.js +0 -3383
- package/dist/AssistantChart-CxGjH7Qk.js +0 -3477
- package/dist/AssistantChart-DIpshm3i.js +0 -4784
- package/dist/AssistantChart-D_PTeu8P.cjs +0 -3503
- package/dist/AssistantChart-WeycT5Pd.cjs +0 -3551
- package/dist/AssistantChart-zjsy2GaZ.cjs +0 -4810
- package/dist/AudioPlayer-B1lt5cPl.cjs +0 -989
- package/dist/AudioPlayer-BZ7bibzU.cjs +0 -982
- package/dist/AudioPlayer-BpRPS4-1.cjs +0 -1277
- package/dist/AudioPlayer-C12BjQBV.cjs +0 -997
- package/dist/AudioPlayer-CFeV8t-5.cjs +0 -936
- package/dist/AudioPlayer-Coly3q5R.js +0 -1278
- package/dist/AudioPlayer-CySJIyvL.js +0 -937
- package/dist/AudioPlayer-DMcG_c7L.js +0 -990
- package/dist/AudioPlayer-DcFKRJE_.js +0 -998
- package/dist/AudioPlayer-e8LfNoqO.js +0 -983
- package/dist/BrandColorsContext-565dDHd5.js +0 -660
- package/dist/BrandColorsContext-BcJbtkqn.cjs +0 -659
- package/dist/CodeBlock-BgfYL_rD.cjs +0 -2094
- package/dist/CodeBlock-BlcqlA9M.cjs +0 -2094
- package/dist/CodeBlock-Bnmeu5ez.cjs +0 -2094
- package/dist/CodeBlock-BtfPlbAI.js +0 -2078
- package/dist/CodeBlock-CIySIuYr.js +0 -2078
- package/dist/CodeBlock-CuPtUM-7.cjs +0 -2094
- package/dist/CodeBlock-D6ffWXgc.js +0 -2078
- package/dist/CodeBlock-D8dcwbit.cjs +0 -2094
- package/dist/CodeBlock-DMZrFnlw.cjs +0 -2094
- package/dist/CodeBlock-DlBehYN8.js +0 -2078
- package/dist/CodeBlock-DnYNI8rQ.js +0 -2078
- package/dist/CodeBlock-DvKWbSnE.cjs +0 -2094
- package/dist/CodeBlock-DwMCfkFY.js +0 -2078
- package/dist/CodeBlock-Dy6CNYyj.js +0 -2078
- package/dist/CodeBlock-U1pPOQI7.cjs +0 -2094
- package/dist/CodeBlock-f_GpNhEB.js +0 -2078
- package/dist/CodeBlock-oB6u8nI1.js +0 -2078
- package/dist/CodeBlock-tZC31B73.cjs +0 -2094
- package/dist/FeatureCard-CxC-7C-C.cjs +0 -300
- package/dist/FeatureCard-DbHWCb4E.js +0 -301
- package/dist/ImageWithFallback-CGtidP6B.cjs +0 -4542
- package/dist/ImageWithFallback-lsg3pdFg.js +0 -4508
- package/dist/LanguageSelector-B5YfbHra.js +0 -231
- package/dist/LanguageSelector-D6uacAIM.cjs +0 -230
- package/dist/LayoutContext-B45-e9DI.cjs +0 -93
- package/dist/LayoutContext-BAql6ZRY.js +0 -97
- package/dist/LayoutContext-Bav3UMEA.js +0 -94
- package/dist/LayoutContext-BvK-ggDa.cjs +0 -96
- package/dist/ThemeContext-BoH4NLfN.js +0 -734
- package/dist/ThemeContext-r69W20Xg.cjs +0 -733
- package/dist/VerifyEmailPage-COiyNl1y.js +0 -2825
- package/dist/VerifyEmailPage-CqKsR2v8.js +0 -2827
- package/dist/VerifyEmailPage-DjQKRlUS.cjs +0 -2824
- package/dist/VerifyEmailPage-s-1X3LDJ.cjs +0 -2826
- package/dist/XerticaOrbe-KL1RBHzw.cjs +0 -1354
- package/dist/XerticaOrbe-zwS1p2a8.js +0 -1355
- package/dist/XerticaProvider-6btlAlzc.js +0 -17
- package/dist/XerticaProvider-BNoNOxQ5.cjs +0 -16
- package/dist/XerticaProvider-BlY2limY.cjs +0 -38
- package/dist/XerticaProvider-DDuiIcKo.js +0 -39
- package/dist/XerticaProvider-cI9hSs27.cjs +0 -38
- package/dist/XerticaProvider-hSwhNQex.js +0 -39
- package/dist/alert-dialog-BOje--vD.js +0 -847
- package/dist/alert-dialog-BtEuQqrg.cjs +0 -870
- package/dist/breadcrumb-CqJ7bHY5.js +0 -161
- package/dist/breadcrumb-m9Hb2_XN.cjs +0 -177
- package/dist/components/assistant/xertica-assistant/hooks/index.d.ts +0 -6
- package/dist/components/assistant/xertica-assistant/hooks/use-assistant-conversations.d.ts +0 -21
- package/dist/components/assistant/xertica-assistant/hooks/use-assistant-messages.d.ts +0 -49
- package/dist/components/assistant/xertica-assistant/hooks/use-assistant-suggestions.d.ts +0 -16
- package/dist/components/blocks/audio-player/AudioPlayer.d.ts +0 -35
- package/dist/components/blocks/audio-player/index.d.ts +0 -1
- package/dist/components/blocks/document-editor/DocumentEditor.d.ts +0 -26
- package/dist/components/blocks/document-editor/index.d.ts +0 -1
- package/dist/components/blocks/podcast-player/PodcastPlayer.d.ts +0 -41
- package/dist/components/blocks/podcast-player/index.d.ts +0 -1
- package/dist/components/ui/chart/parts/chart-dashboard.d.ts +0 -113
- package/dist/components/ui/chart/parts/chart-metric.d.ts +0 -118
- package/dist/components/ui/chart/parts/chart-primitives.d.ts +0 -101
- package/dist/components/ui/chart/parts/chart-shared.d.ts +0 -20
- package/dist/components/ui/chart/parts/chart-utils.d.ts +0 -12
- package/dist/components/ui/chart/parts/index.d.ts +0 -5
- package/dist/dropdown-menu-BDB5CmQs.cjs +0 -247
- package/dist/dropdown-menu-DQidbKBD.js +0 -231
- package/dist/google-maps-loader-BFWp6VPd.js +0 -287
- package/dist/google-maps-loader-BKcdgFbu.cjs +0 -312
- package/dist/google-maps-loader-CumCNXeG.js +0 -312
- package/dist/google-maps-loader-eS3uQ5TA.cjs +0 -287
- package/dist/header-Cgy6vYPk.cjs +0 -731
- package/dist/header-DRlT4jgI.js +0 -715
- package/dist/header-Dux00SI4.cjs +0 -731
- package/dist/header-EkGKXPsD.js +0 -715
- package/dist/header-WfEywpyc.cjs +0 -731
- package/dist/header-tifNQn2U.js +0 -715
- package/dist/index-BhapVLVj.js +0 -8
- package/dist/index-D6fxYEY8.cjs +0 -7
- package/dist/index-DAIp0_HK.js +0 -8
- package/dist/index-DW5tYe26.js +0 -8
- package/dist/index-GA__GvnG.cjs +0 -7
- package/dist/input-2R4loU86.js +0 -127
- package/dist/input-DWANSKGb.cjs +0 -145
- package/dist/progress-DPtzoVV8.js +0 -175
- package/dist/progress-EeaoqqUs.cjs +0 -191
- package/dist/rich-text-editor-0mraWT5y.cjs +0 -2376
- package/dist/rich-text-editor-B-IkcPD0.js +0 -2874
- package/dist/rich-text-editor-B6jMRLzk.cjs +0 -1939
- package/dist/rich-text-editor-B8_oYcIR.js +0 -1730
- package/dist/rich-text-editor-B9UbSXNb.js +0 -1203
- package/dist/rich-text-editor-BYuRBNBU.js +0 -2373
- package/dist/rich-text-editor-Bb9pySTs.cjs +0 -2374
- package/dist/rich-text-editor-BcL6L3cm.cjs +0 -2374
- package/dist/rich-text-editor-BoVZYtTs.cjs +0 -2391
- package/dist/rich-text-editor-Bp3zQqMC.js +0 -2954
- package/dist/rich-text-editor-CMgSN_w2.js +0 -1189
- package/dist/rich-text-editor-CPV1lEPH.cjs +0 -1748
- package/dist/rich-text-editor-CeucBdIv.cjs +0 -2971
- package/dist/rich-text-editor-CoKqbCtu.cjs +0 -1799
- package/dist/rich-text-editor-Cw56T_mB.js +0 -2356
- package/dist/rich-text-editor-Cyt8qs2b.js +0 -1921
- package/dist/rich-text-editor-D6H84OcX.cjs +0 -1220
- package/dist/rich-text-editor-D76gD-QI.js +0 -2328
- package/dist/rich-text-editor-DKkokOnA.js +0 -1781
- package/dist/rich-text-editor-DNsdpN64.cjs +0 -2359
- package/dist/rich-text-editor-DfG8bCyY.js +0 -2358
- package/dist/rich-text-editor-Dxjw31Z4.js +0 -2341
- package/dist/rich-text-editor-DzP0Epmb.js +0 -2356
- package/dist/rich-text-editor-bRkNoeZY.cjs +0 -2891
- package/dist/rich-text-editor-lyYE2ZG5.cjs +0 -1207
- package/dist/rich-text-editor-skplNlBM.cjs +0 -2345
- package/dist/select-Bkbr0f-Z.cjs +0 -162
- package/dist/select-CvIVdX2n.js +0 -145
- package/dist/sidebar-CK_0ZQHj.cjs +0 -803
- package/dist/sidebar-CUuOvYhK.js +0 -787
- package/dist/sidebar-DQj1z3jG.cjs +0 -758
- package/dist/sidebar-Djn5syhi.cjs +0 -786
- package/dist/sidebar-LluMXfam.js +0 -759
- package/dist/sidebar-_rT7rBMk.js +0 -787
- package/dist/slider-Bc5Hd0y1.js +0 -56
- package/dist/slider-N7hFFj6X.cjs +0 -73
- package/dist/tooltip-Ded96neP.cjs +0 -137
- package/dist/tooltip-HDOoD2-0.js +0 -120
- package/dist/use-audio-player-B31J-aqh.cjs +0 -187
- package/dist/use-audio-player-BkmEmj8Q.js +0 -185
- package/dist/use-audio-player-CLFTWFW1.cjs +0 -184
- package/dist/use-audio-player-CLLn00I6.js +0 -188
- package/dist/use-file-upload-BcjEo2S5.js +0 -404
- package/dist/use-file-upload-CRJR68Tj.cjs +0 -403
- package/dist/use-mobile-B0hNy_Y6.cjs +0 -4303
- package/dist/use-mobile-BXuYROXM.js +0 -4202
- package/dist/use-mobile-Bbd51ASU.cjs +0 -4392
- package/dist/use-mobile-Bk6CX-TC.js +0 -4359
- package/dist/use-mobile-BvYdisLP.js +0 -4202
- package/dist/use-mobile-BzuxjzNX.cjs +0 -4392
- package/dist/use-mobile-CG2-SdXV.cjs +0 -4235
- package/dist/use-mobile-CKb5pqTs.js +0 -4269
- package/dist/use-mobile-CYuAuGDl.js +0 -4202
- package/dist/use-mobile-CaENcqm-.js +0 -4508
- package/dist/use-mobile-CbrYgJGJ.js +0 -4203
- package/dist/use-mobile-Cd4xPrKq.cjs +0 -46
- package/dist/use-mobile-DMOvImGQ.cjs +0 -4542
- package/dist/use-mobile-DRB3BQgD.cjs +0 -4235
- package/dist/use-mobile-DZvv7QMR.js +0 -4359
- package/dist/use-mobile-DdI_TXam.cjs +0 -4235
- package/dist/use-mobile-DlceKf8a.js +0 -4359
- package/dist/use-mobile-DsOnow1o.cjs +0 -4236
- package/dist/use-mobile-Kcj6jSnK.cjs +0 -4392
- package/dist/use-mobile-bnKcua_i.js +0 -4202
- package/dist/use-mobile-j4w2Jrf1.js +0 -30
- package/dist/use-mobile-ncXBeE2z.cjs +0 -4235
- package/dist/use-rich-text-editor-DjiddBGv.js +0 -282
- package/dist/use-rich-text-editor-lpeswbCs.cjs +0 -281
- package/dist/xertica-assistant-BdiZag0h.js +0 -2187
- package/dist/xertica-assistant-CrgTb6Hs.cjs +0 -2155
- package/dist/xertica-assistant-DCsnQyi5.js +0 -2156
- package/dist/xertica-assistant-DUBpmEgo.cjs +0 -2186
- package/dist/{rich-text-editor-DgF8s7xW.js → rich-text-editor-BmsjY03B.js} +26 -26
- package/dist/{rich-text-editor-mWoaSCE4.cjs → rich-text-editor-GS2kpTAK.cjs} +26 -26
|
@@ -1,768 +1,768 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { createPortal } from 'react-dom';
|
|
4
|
-
import {
|
|
5
|
-
Play,
|
|
6
|
-
Pause,
|
|
7
|
-
Volume2,
|
|
8
|
-
VolumeX,
|
|
9
|
-
Maximize2,
|
|
10
|
-
PlayCircle,
|
|
11
|
-
PauseCircle,
|
|
12
|
-
RotateCcw,
|
|
13
|
-
Gauge,
|
|
14
|
-
Info,
|
|
15
|
-
Download,
|
|
16
|
-
X,
|
|
17
|
-
ExternalLink,
|
|
18
|
-
Radio,
|
|
19
|
-
MoreVertical,
|
|
20
|
-
MoreHorizontal,
|
|
21
|
-
} from 'lucide-react';
|
|
22
|
-
import { Slider } from '../../ui/slider';
|
|
23
|
-
import { Button } from '../../ui/button';
|
|
24
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip';
|
|
25
|
-
import {
|
|
26
|
-
DropdownMenu,
|
|
27
|
-
DropdownMenuContent,
|
|
28
|
-
DropdownMenuItem,
|
|
29
|
-
DropdownMenuTrigger,
|
|
30
|
-
DropdownMenuSeparator,
|
|
31
|
-
} from '../../ui/dropdown-menu';
|
|
32
|
-
import { FloatingMediaWrapper } from '../FloatingMediaWrapper';
|
|
33
|
-
import { cn } from '../../shared/utils';
|
|
34
|
-
import { useAudioPlayer } from './use-audio-player';
|
|
35
|
-
|
|
36
|
-
export interface AudioPlayerProps {
|
|
37
|
-
/** Source URL of the audio file */
|
|
38
|
-
src?: string;
|
|
39
|
-
/** Title of the audio track/podcast */
|
|
40
|
-
title?: string;
|
|
41
|
-
/** Artist or subtitle information */
|
|
42
|
-
artist?: string;
|
|
43
|
-
/** Subtitle or source information (specific to 'bar' variant) */
|
|
44
|
-
subtitle?: string;
|
|
45
|
-
/** URL for cover art (specific to 'card' variant) */
|
|
46
|
-
coverArt?: string;
|
|
47
|
-
/** Whether to play automatically */
|
|
48
|
-
autoPlay?: boolean;
|
|
49
|
-
/** CSS class name for the container */
|
|
50
|
-
className?: string;
|
|
51
|
-
/** Display variant: 'card' (standard floating) or 'bar' (global fixed bar) */
|
|
52
|
-
variant?: 'card' | 'bar';
|
|
53
|
-
/** Whether the player is currently visible and open (only for 'bar' variant) */
|
|
54
|
-
isOpen?: boolean;
|
|
55
|
-
/** Callback to close the player bar (only for 'bar' variant) */
|
|
56
|
-
onClose?: () => void;
|
|
57
|
-
/** Total duration in seconds (optional, will be loaded from metadata if not provided) */
|
|
58
|
-
duration?: number;
|
|
59
|
-
/** Initial playback time in seconds */
|
|
60
|
-
currentTime?: number;
|
|
61
|
-
/** Color theme variant for the bar layout */
|
|
62
|
-
colorVariant?: 'default' | 'primary';
|
|
63
|
-
/** Whether the player should automatically float when scrolled out of view */
|
|
64
|
-
enableAutoFloat?: boolean;
|
|
65
|
-
/** Callback fired when user manually closes the floating player */
|
|
66
|
-
onCloseFloating?: () => void;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Unified Audio Player component.
|
|
71
|
-
*
|
|
72
|
-
* All state and logic is managed by the `useAudioPlayer` headless hook.
|
|
73
|
-
* This component is responsible only for rendering.
|
|
74
|
-
*/
|
|
75
|
-
export function AudioPlayer({
|
|
76
|
-
src,
|
|
77
|
-
title,
|
|
78
|
-
artist,
|
|
79
|
-
subtitle,
|
|
80
|
-
autoPlay = false,
|
|
81
|
-
className,
|
|
82
|
-
variant = 'card',
|
|
83
|
-
isOpen = true,
|
|
84
|
-
onClose,
|
|
85
|
-
duration: initialDuration,
|
|
86
|
-
currentTime: initialTime = 0,
|
|
87
|
-
colorVariant = 'default',
|
|
88
|
-
enableAutoFloat = true,
|
|
89
|
-
onCloseFloating,
|
|
90
|
-
}: AudioPlayerProps) {
|
|
91
|
-
const { t } = useTranslation();
|
|
92
|
-
|
|
93
|
-
const {
|
|
94
|
-
audioRef,
|
|
95
|
-
containerRef,
|
|
96
|
-
isPlaying,
|
|
97
|
-
currentTime,
|
|
98
|
-
duration,
|
|
99
|
-
volume,
|
|
100
|
-
isMuted,
|
|
101
|
-
playbackSpeed,
|
|
102
|
-
isFloating,
|
|
103
|
-
isManualFloating,
|
|
104
|
-
isVisible,
|
|
105
|
-
isMobile,
|
|
106
|
-
enableAutoFloatLocal,
|
|
107
|
-
sidebarExpanded,
|
|
108
|
-
sidebarWidth,
|
|
109
|
-
assistenteExpanded,
|
|
110
|
-
togglePlay,
|
|
111
|
-
toggleMute,
|
|
112
|
-
handleSeek,
|
|
113
|
-
handleVolumeChange,
|
|
114
|
-
handleSetFloating,
|
|
115
|
-
handleEnableManualFloat,
|
|
116
|
-
setPlaybackSpeed,
|
|
117
|
-
resetAudio,
|
|
118
|
-
formatTime,
|
|
119
|
-
onPlay,
|
|
120
|
-
onPause,
|
|
121
|
-
onEnded,
|
|
122
|
-
onTimeUpdate,
|
|
123
|
-
onLoadedMetadata,
|
|
124
|
-
} = useAudioPlayer({
|
|
125
|
-
src,
|
|
126
|
-
autoPlay,
|
|
127
|
-
initialTime,
|
|
128
|
-
initialDuration,
|
|
129
|
-
variant,
|
|
130
|
-
isOpen,
|
|
131
|
-
enableAutoFloat,
|
|
132
|
-
onCloseFloating,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// ── Audio element (hidden) ──────────────────────────────────────────────────
|
|
136
|
-
const audioElement = (
|
|
137
|
-
<audio
|
|
138
|
-
ref={audioRef}
|
|
139
|
-
src={src}
|
|
140
|
-
autoPlay={autoPlay}
|
|
141
|
-
className="hidden"
|
|
142
|
-
onTimeUpdate={onTimeUpdate}
|
|
143
|
-
onLoadedMetadata={onLoadedMetadata}
|
|
144
|
-
onPlay={onPlay}
|
|
145
|
-
onPause={onPause}
|
|
146
|
-
onEnded={onEnded}
|
|
147
|
-
/>
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// ── Bar variant — desktop layout ────────────────────────────────────────────
|
|
151
|
-
const desktopLayout = (
|
|
152
|
-
<div
|
|
153
|
-
className={cn(
|
|
154
|
-
'px-6 flex items-center justify-between gap-4',
|
|
155
|
-
colorVariant === 'primary' ? 'h-[80px] gap-6' : 'h-[80px]'
|
|
156
|
-
)}
|
|
157
|
-
>
|
|
158
|
-
{/* Left: Info */}
|
|
159
|
-
<div
|
|
160
|
-
className={cn(
|
|
161
|
-
'flex flex-col min-w-0 shrink-0 relative z-10',
|
|
162
|
-
colorVariant === 'primary' ? 'w-[280px]' : 'w-[280px]'
|
|
163
|
-
)}
|
|
164
|
-
>
|
|
165
|
-
<div className="flex items-center gap-2 mb-0.5">
|
|
166
|
-
{colorVariant === 'primary' ? (
|
|
167
|
-
<div className="w-2 h-2 rounded-full bg-[var(--chart-4)] animate-pulse shrink-0" />
|
|
168
|
-
) : (
|
|
169
|
-
<Radio className="w-3 h-3 text-[var(--chart-4)] animate-pulse shrink-0" />
|
|
170
|
-
)}
|
|
171
|
-
<h4
|
|
172
|
-
className={cn(
|
|
173
|
-
'truncate',
|
|
174
|
-
colorVariant === 'primary'
|
|
175
|
-
? 'font-semibold text-base tracking-tight'
|
|
176
|
-
: 'font-medium text-base'
|
|
177
|
-
)}
|
|
178
|
-
title={title || t('media.audioTitle')}
|
|
179
|
-
>
|
|
180
|
-
{title || t('media.audioTitle')}
|
|
181
|
-
</h4>
|
|
182
|
-
</div>
|
|
183
|
-
<div
|
|
184
|
-
className={cn(
|
|
185
|
-
'flex items-center gap-1 text-xs text-muted-foreground',
|
|
186
|
-
colorVariant === 'primary' && 'opacity-80'
|
|
187
|
-
)}
|
|
188
|
-
>
|
|
189
|
-
<span className="truncate">{subtitle || artist}</span>
|
|
190
|
-
<ExternalLink
|
|
191
|
-
className={cn(
|
|
192
|
-
'w-3 h-3 cursor-pointer hover:text-foreground ml-1 transition-opacity',
|
|
193
|
-
colorVariant === 'primary'
|
|
194
|
-
? 'opacity-60 hover:opacity-100'
|
|
195
|
-
: 'opacity-70 hover:opacity-100'
|
|
196
|
-
)}
|
|
197
|
-
/>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
{/* Center: Controls */}
|
|
202
|
-
<div
|
|
203
|
-
className={cn(
|
|
204
|
-
'flex-1 flex items-center justify-center gap-4 min-w-0 max-w-xl relative z-10',
|
|
205
|
-
colorVariant === 'primary' && 'gap-4'
|
|
206
|
-
)}
|
|
207
|
-
>
|
|
208
|
-
<button
|
|
209
|
-
onClick={togglePlay}
|
|
210
|
-
aria-label={isPlaying ? t('media.pause') : t('media.play')}
|
|
211
|
-
className={cn(
|
|
212
|
-
'transition-all focus:outline-none shrink-0',
|
|
213
|
-
colorVariant === 'primary'
|
|
214
|
-
? 'hover:scale-110'
|
|
215
|
-
: 'text-muted-foreground hover:text-foreground hover:scale-110 active:scale-95 transform duration-100'
|
|
216
|
-
)}
|
|
217
|
-
>
|
|
218
|
-
{isPlaying ? (
|
|
219
|
-
<PauseCircle
|
|
220
|
-
className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')}
|
|
221
|
-
strokeWidth={colorVariant === 'primary' ? 1 : 1.5}
|
|
222
|
-
/>
|
|
223
|
-
) : (
|
|
224
|
-
<PlayCircle
|
|
225
|
-
className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')}
|
|
226
|
-
strokeWidth={colorVariant === 'primary' ? 1 : 1.5}
|
|
227
|
-
/>
|
|
228
|
-
)}
|
|
229
|
-
</button>
|
|
230
|
-
|
|
231
|
-
<div className="flex-1 flex items-center gap-3 max-w-lg">
|
|
232
|
-
<span
|
|
233
|
-
className={cn(
|
|
234
|
-
'text-xs font-mono shrink-0 w-10 text-right',
|
|
235
|
-
colorVariant === 'primary' ? 'text-[11px] w-12 opacity-80' : 'text-muted-foreground'
|
|
236
|
-
)}
|
|
237
|
-
>
|
|
238
|
-
{formatTime(currentTime)}
|
|
239
|
-
</span>
|
|
240
|
-
<Slider
|
|
241
|
-
value={[currentTime]}
|
|
242
|
-
max={duration || 100}
|
|
243
|
-
onValueChange={handleSeek}
|
|
244
|
-
className="cursor-pointer"
|
|
245
|
-
/>
|
|
246
|
-
<span
|
|
247
|
-
className={cn(
|
|
248
|
-
'text-xs font-mono shrink-0 w-10',
|
|
249
|
-
colorVariant === 'primary' ? 'text-[11px] w-12 opacity-80' : 'text-muted-foreground'
|
|
250
|
-
)}
|
|
251
|
-
>
|
|
252
|
-
{formatTime(duration)}
|
|
253
|
-
</span>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
|
|
257
|
-
{/* Right: Actions */}
|
|
258
|
-
<div className="flex items-center gap-2 shrink-0 relative z-10">
|
|
259
|
-
<div className="hidden lg:flex items-center gap-2 w-28 mr-2 group">
|
|
260
|
-
<button
|
|
261
|
-
onClick={toggleMute}
|
|
262
|
-
className="text-muted-foreground hover:text-foreground"
|
|
263
|
-
aria-label={isMuted || volume === 0 ? t('media.unmute') : t('media.mute')}
|
|
264
|
-
>
|
|
265
|
-
{isMuted || volume === 0 ? (
|
|
266
|
-
<VolumeX className="w-5 h-5" />
|
|
267
|
-
) : (
|
|
268
|
-
<Volume2 className="w-5 h-5" />
|
|
269
|
-
)}
|
|
270
|
-
</button>
|
|
271
|
-
<Slider
|
|
272
|
-
value={[isMuted ? 0 : volume * 100]}
|
|
273
|
-
max={100}
|
|
274
|
-
onValueChange={val => handleVolumeChange([val[0] / 100])}
|
|
275
|
-
className="w-full opacity-60 group-hover:opacity-100 transition-opacity"
|
|
276
|
-
/>
|
|
277
|
-
</div>
|
|
278
|
-
|
|
279
|
-
<div className="flex items-center gap-1">
|
|
280
|
-
<div className="hidden md:flex">
|
|
281
|
-
<TooltipProvider>
|
|
282
|
-
<Tooltip>
|
|
283
|
-
<TooltipTrigger asChild>
|
|
284
|
-
<Button
|
|
285
|
-
variant="ghost"
|
|
286
|
-
size="icon"
|
|
287
|
-
className="rounded-full h-9 w-9"
|
|
288
|
-
onClick={resetAudio}
|
|
289
|
-
aria-label={t('media.restart')}
|
|
290
|
-
>
|
|
291
|
-
<RotateCcw className="w-4 h-4" />
|
|
292
|
-
</Button>
|
|
293
|
-
</TooltipTrigger>
|
|
294
|
-
<TooltipContent className="z-[1001]">{t('media.restart')}</TooltipContent>
|
|
295
|
-
</Tooltip>
|
|
296
|
-
</TooltipProvider>
|
|
297
|
-
|
|
298
|
-
<TooltipProvider>
|
|
299
|
-
<Tooltip>
|
|
300
|
-
<TooltipTrigger asChild>
|
|
301
|
-
<Button
|
|
302
|
-
variant="ghost"
|
|
303
|
-
size="icon"
|
|
304
|
-
className="text-[var(--chart-3)] hover:bg-accent rounded-full h-9 w-9"
|
|
305
|
-
aria-label={t('media.audioInfo')}
|
|
306
|
-
>
|
|
307
|
-
<Info className="w-4 h-4 fill-current" />
|
|
308
|
-
</Button>
|
|
309
|
-
</TooltipTrigger>
|
|
310
|
-
<TooltipContent className="z-[1001]">{t('media.info')}</TooltipContent>
|
|
311
|
-
</Tooltip>
|
|
312
|
-
</TooltipProvider>
|
|
313
|
-
</div>
|
|
314
|
-
|
|
315
|
-
<div className="hidden xl:flex items-center gap-1">
|
|
316
|
-
<DropdownMenu>
|
|
317
|
-
<DropdownMenuTrigger asChild>
|
|
318
|
-
<Button
|
|
319
|
-
variant="ghost"
|
|
320
|
-
size="icon"
|
|
321
|
-
className="rounded-full h-9 w-9"
|
|
322
|
-
aria-label={t('media.selectSpeed')}
|
|
323
|
-
>
|
|
324
|
-
<Gauge className="w-4 h-4" />
|
|
325
|
-
</Button>
|
|
326
|
-
</DropdownMenuTrigger>
|
|
327
|
-
<DropdownMenuContent align="center" className="w-32 z-[1001]">
|
|
328
|
-
{[0.5, 1, 1.5, 2].map(speed => (
|
|
329
|
-
<DropdownMenuItem
|
|
330
|
-
key={speed}
|
|
331
|
-
onClick={() => setPlaybackSpeed(speed)}
|
|
332
|
-
className={cn(
|
|
333
|
-
'cursor-pointer',
|
|
334
|
-
playbackSpeed === speed && 'bg-accent font-bold'
|
|
335
|
-
)}
|
|
336
|
-
>
|
|
337
|
-
{speed}x
|
|
338
|
-
</DropdownMenuItem>
|
|
339
|
-
))}
|
|
340
|
-
</DropdownMenuContent>
|
|
341
|
-
</DropdownMenu>
|
|
342
|
-
|
|
343
|
-
<TooltipProvider>
|
|
344
|
-
<Tooltip>
|
|
345
|
-
<TooltipTrigger asChild>
|
|
346
|
-
<Button
|
|
347
|
-
variant="ghost"
|
|
348
|
-
size="icon"
|
|
349
|
-
className="rounded-full h-9 w-9"
|
|
350
|
-
aria-label={t('media.downloadAudio')}
|
|
351
|
-
>
|
|
352
|
-
<Download className="w-4 h-4" />
|
|
353
|
-
</Button>
|
|
354
|
-
</TooltipTrigger>
|
|
355
|
-
<TooltipContent className="z-[1001]">{t('media.downloadAudio')}</TooltipContent>
|
|
356
|
-
</Tooltip>
|
|
357
|
-
</TooltipProvider>
|
|
358
|
-
</div>
|
|
359
|
-
|
|
360
|
-
<div className="xl:hidden">
|
|
361
|
-
<DropdownMenu>
|
|
362
|
-
<DropdownMenuTrigger asChild>
|
|
363
|
-
<Button
|
|
364
|
-
variant="ghost"
|
|
365
|
-
size="icon"
|
|
366
|
-
className="rounded-full h-9 w-9"
|
|
367
|
-
aria-label={t('media.moreOptions')}
|
|
368
|
-
>
|
|
369
|
-
<MoreHorizontal className="w-5 h-5" />
|
|
370
|
-
</Button>
|
|
371
|
-
</DropdownMenuTrigger>
|
|
372
|
-
<DropdownMenuContent align="end" className="w-56 p-2 z-[1001]">
|
|
373
|
-
<div className="lg:hidden">
|
|
374
|
-
<div className="px-2 py-3">
|
|
375
|
-
<div className="flex items-center justify-between mb-3 px-1">
|
|
376
|
-
<div className="flex items-center gap-2">
|
|
377
|
-
{isMuted || volume === 0 ? (
|
|
378
|
-
<VolumeX className="w-4 h-4 text-muted-foreground" />
|
|
379
|
-
) : (
|
|
380
|
-
<Volume2 className="w-4 h-4 text-muted-foreground" />
|
|
381
|
-
)}
|
|
382
|
-
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
|
|
383
|
-
Volume
|
|
384
|
-
</span>
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
387
|
-
<Slider
|
|
388
|
-
value={[isMuted ? 0 : volume * 100]}
|
|
389
|
-
max={100}
|
|
390
|
-
onValueChange={val => handleVolumeChange([val[0] / 100])}
|
|
391
|
-
/>
|
|
392
|
-
</div>
|
|
393
|
-
<DropdownMenuSeparator className="my-1" />
|
|
394
|
-
</div>
|
|
395
|
-
|
|
396
|
-
<div className="md:hidden">
|
|
397
|
-
<DropdownMenuItem onClick={resetAudio} className="cursor-pointer py-2.5">
|
|
398
|
-
<RotateCcw className="w-4 h-4 mr-3 text-muted-foreground" />{' '}
|
|
399
|
-
{t('media.restart')}
|
|
400
|
-
</DropdownMenuItem>
|
|
401
|
-
<DropdownMenuItem className="cursor-pointer py-2.5">
|
|
402
|
-
<Info className="w-4 h-4 mr-3 text-muted-foreground" /> {t('media.info')}
|
|
403
|
-
</DropdownMenuItem>
|
|
404
|
-
<DropdownMenuSeparator className="my-1" />
|
|
405
|
-
</div>
|
|
406
|
-
|
|
407
|
-
<div className="xl:hidden">
|
|
408
|
-
<div className="px-3 py-2 flex flex-col gap-2">
|
|
409
|
-
<div className="flex items-center gap-2">
|
|
410
|
-
<Gauge className="w-4 h-4 text-muted-foreground" />
|
|
411
|
-
<span className="text-sm font-medium">{t('media.speed')}</span>
|
|
412
|
-
</div>
|
|
413
|
-
<div className="flex flex-wrap gap-1">
|
|
414
|
-
{[0.5, 1, 1.5, 2].map(speed => (
|
|
415
|
-
<button
|
|
416
|
-
key={speed}
|
|
417
|
-
onClick={() => setPlaybackSpeed(speed)}
|
|
418
|
-
className={cn(
|
|
419
|
-
'px-2 py-0.5 text-[10px] rounded border transition-colors',
|
|
420
|
-
playbackSpeed === speed
|
|
421
|
-
? 'bg-primary text-primary-foreground border-primary'
|
|
422
|
-
: 'bg-transparent border-border hover:bg-accent'
|
|
423
|
-
)}
|
|
424
|
-
>
|
|
425
|
-
{speed}x
|
|
426
|
-
</button>
|
|
427
|
-
))}
|
|
428
|
-
</div>
|
|
429
|
-
</div>
|
|
430
|
-
<DropdownMenuSeparator className="my-1" />
|
|
431
|
-
<DropdownMenuItem className="cursor-pointer py-2.5">
|
|
432
|
-
<Download className="w-4 h-4 mr-3 text-muted-foreground" />{' '}
|
|
433
|
-
{t('media.downloadAudio')}
|
|
434
|
-
</DropdownMenuItem>
|
|
435
|
-
</div>
|
|
436
|
-
</DropdownMenuContent>
|
|
437
|
-
</DropdownMenu>
|
|
438
|
-
</div>
|
|
439
|
-
|
|
440
|
-
<div className="w-px h-8 bg-border mx-1" />
|
|
441
|
-
|
|
442
|
-
<Button
|
|
443
|
-
onClick={onClose}
|
|
444
|
-
variant="ghost"
|
|
445
|
-
size="icon"
|
|
446
|
-
className="rounded-full h-9 w-9"
|
|
447
|
-
aria-label={t('media.closePlayer')}
|
|
448
|
-
>
|
|
449
|
-
<X className="w-5 h-5" />
|
|
450
|
-
</Button>
|
|
451
|
-
</div>
|
|
452
|
-
</div>
|
|
453
|
-
</div>
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
// ── Bar variant — mobile layout ─────────────────────────────────────────────
|
|
457
|
-
const mobileLayout = (
|
|
458
|
-
<div
|
|
459
|
-
className={cn(
|
|
460
|
-
'flex flex-col py-3 px-4 gap-3',
|
|
461
|
-
colorVariant === 'primary' && 'py-4 px-5 gap-4'
|
|
462
|
-
)}
|
|
463
|
-
>
|
|
464
|
-
<div className="flex items-start justify-between gap-3">
|
|
465
|
-
<div className="flex flex-col min-w-0 flex-1">
|
|
466
|
-
<div className="flex items-center gap-2 mb-1">
|
|
467
|
-
{colorVariant === 'primary' ? (
|
|
468
|
-
<div className="w-2 h-2 rounded-full bg-[var(--chart-4)] animate-pulse shrink-0" />
|
|
469
|
-
) : (
|
|
470
|
-
<Radio className="w-3 h-3 text-[var(--chart-4)] animate-pulse shrink-0" />
|
|
471
|
-
)}
|
|
472
|
-
<h4
|
|
473
|
-
className={cn(
|
|
474
|
-
'truncate',
|
|
475
|
-
colorVariant === 'primary'
|
|
476
|
-
? 'font-semibold text-sm tracking-tight'
|
|
477
|
-
: 'font-medium text-sm'
|
|
478
|
-
)}
|
|
479
|
-
>
|
|
480
|
-
{title}
|
|
481
|
-
</h4>
|
|
482
|
-
</div>
|
|
483
|
-
<div
|
|
484
|
-
className={cn(
|
|
485
|
-
'flex items-center gap-1.5 text-xs text-muted-foreground',
|
|
486
|
-
colorVariant === 'primary' && 'opacity-70'
|
|
487
|
-
)}
|
|
488
|
-
>
|
|
489
|
-
<span className="truncate">{subtitle || artist}</span>
|
|
490
|
-
</div>
|
|
491
|
-
</div>
|
|
492
|
-
<Button
|
|
493
|
-
onClick={onClose}
|
|
494
|
-
variant="ghost"
|
|
495
|
-
size="icon"
|
|
496
|
-
className="rounded-full h-8 w-8 shrink-0"
|
|
497
|
-
aria-label={t('media.closePlayer')}
|
|
498
|
-
>
|
|
499
|
-
<X className="w-4 h-4" />
|
|
500
|
-
</Button>
|
|
501
|
-
</div>
|
|
502
|
-
|
|
503
|
-
<div className="flex items-center gap-3">
|
|
504
|
-
<button
|
|
505
|
-
onClick={togglePlay}
|
|
506
|
-
aria-label={isPlaying ? t('media.pause') : t('media.play')}
|
|
507
|
-
className="shrink-0 transition-transform active:scale-95"
|
|
508
|
-
>
|
|
509
|
-
{isPlaying ? (
|
|
510
|
-
<PauseCircle
|
|
511
|
-
className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')}
|
|
512
|
-
strokeWidth={colorVariant === 'primary' ? 1 : 1.5}
|
|
513
|
-
/>
|
|
514
|
-
) : (
|
|
515
|
-
<PlayCircle
|
|
516
|
-
className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')}
|
|
517
|
-
strokeWidth={colorVariant === 'primary' ? 1 : 1.5}
|
|
518
|
-
/>
|
|
519
|
-
)}
|
|
520
|
-
</button>
|
|
521
|
-
|
|
522
|
-
<div className="flex-1 flex items-center gap-2">
|
|
523
|
-
<Slider
|
|
524
|
-
value={[currentTime]}
|
|
525
|
-
max={duration || 100}
|
|
526
|
-
onValueChange={handleSeek}
|
|
527
|
-
className="cursor-pointer"
|
|
528
|
-
aria-label={t('media.playbackProgress')}
|
|
529
|
-
/>
|
|
530
|
-
</div>
|
|
531
|
-
|
|
532
|
-
<DropdownMenu>
|
|
533
|
-
<DropdownMenuTrigger asChild>
|
|
534
|
-
<Button
|
|
535
|
-
variant="ghost"
|
|
536
|
-
size="icon"
|
|
537
|
-
className="h-9 w-9 rounded-full"
|
|
538
|
-
aria-label={t('media.moreOptions')}
|
|
539
|
-
>
|
|
540
|
-
<MoreVertical className="w-5 h-5" />
|
|
541
|
-
</Button>
|
|
542
|
-
</DropdownMenuTrigger>
|
|
543
|
-
<DropdownMenuContent align="end" className="w-56 p-2 z-[1001]">
|
|
544
|
-
<DropdownMenuItem onClick={resetAudio}>{t('media.restart')}</DropdownMenuItem>
|
|
545
|
-
<DropdownMenuItem>{t('media.speedLabel', { speed: playbackSpeed })}</DropdownMenuItem>
|
|
546
|
-
</DropdownMenuContent>
|
|
547
|
-
</DropdownMenu>
|
|
548
|
-
</div>
|
|
549
|
-
</div>
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
// ── Card variant — compact layout ───────────────────────────────────────────
|
|
553
|
-
const desktopLayoutCompact = (
|
|
554
|
-
<div
|
|
555
|
-
className={cn(
|
|
556
|
-
'flex items-center gap-3 w-full',
|
|
557
|
-
isFloating ? 'px-3 py-1 text-foreground' : 'px-4 py-3'
|
|
558
|
-
)}
|
|
559
|
-
>
|
|
560
|
-
<Button
|
|
561
|
-
onClick={togglePlay}
|
|
562
|
-
size="icon"
|
|
563
|
-
variant="outline"
|
|
564
|
-
className={cn(
|
|
565
|
-
'shrink-0 rounded-full border-border text-foreground hover:bg-accent transition-colors',
|
|
566
|
-
colorVariant === 'primary' &&
|
|
567
|
-
'border-primary-foreground/20 hover:bg-primary-foreground/10 text-primary-foreground',
|
|
568
|
-
isFloating ? 'h-8 w-8' : 'h-10 w-10'
|
|
569
|
-
)}
|
|
570
|
-
aria-label={isPlaying ? t('media.pause') : t('media.play')}
|
|
571
|
-
>
|
|
572
|
-
{isPlaying ? (
|
|
573
|
-
<Pause className={cn(isFloating ? 'w-3 h-3' : 'w-4 h-4', 'fill-current')} />
|
|
574
|
-
) : (
|
|
575
|
-
<Play className={cn(isFloating ? 'w-3 h-3' : 'w-4 h-4', 'fill-current ml-0.5')} />
|
|
576
|
-
)}
|
|
577
|
-
</Button>
|
|
578
|
-
<div className="flex-1 min-w-0 flex flex-col justify-center gap-1.5">
|
|
579
|
-
<div className="flex items-center justify-between text-xs leading-none">
|
|
580
|
-
<span
|
|
581
|
-
className={cn(
|
|
582
|
-
'font-medium truncate',
|
|
583
|
-
colorVariant === 'primary' ? 'text-primary-foreground' : 'text-foreground'
|
|
584
|
-
)}
|
|
585
|
-
>
|
|
586
|
-
{title}
|
|
587
|
-
</span>
|
|
588
|
-
<span
|
|
589
|
-
className={cn(
|
|
590
|
-
'font-medium tabular-nums text-[10px] sm:text-xs',
|
|
591
|
-
colorVariant === 'primary' ? 'text-primary-foreground/70' : 'text-muted-foreground'
|
|
592
|
-
)}
|
|
593
|
-
>
|
|
594
|
-
{formatTime(currentTime)} / {formatTime(duration)}
|
|
595
|
-
</span>
|
|
596
|
-
</div>
|
|
597
|
-
<Slider
|
|
598
|
-
value={[currentTime]}
|
|
599
|
-
max={duration || 100}
|
|
600
|
-
step={1}
|
|
601
|
-
onValueChange={handleSeek}
|
|
602
|
-
className="w-full"
|
|
603
|
-
aria-label={t('media.playbackProgress')}
|
|
604
|
-
/>
|
|
605
|
-
</div>
|
|
606
|
-
|
|
607
|
-
<div className="flex items-center gap-1 shrink-0">
|
|
608
|
-
<DropdownMenu>
|
|
609
|
-
<DropdownMenuTrigger asChild>
|
|
610
|
-
<Button
|
|
611
|
-
variant="ghost"
|
|
612
|
-
size="icon"
|
|
613
|
-
className={cn(
|
|
614
|
-
'rounded-full',
|
|
615
|
-
colorVariant === 'primary'
|
|
616
|
-
? 'text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/10'
|
|
617
|
-
: 'text-muted-foreground hover:text-foreground',
|
|
618
|
-
isFloating ? 'h-7 w-7' : 'h-8 w-8'
|
|
619
|
-
)}
|
|
620
|
-
aria-label="Mais opções"
|
|
621
|
-
>
|
|
622
|
-
<MoreVertical className={isFloating ? 'w-4 h-4' : 'w-5 h-5'} />
|
|
623
|
-
</Button>
|
|
624
|
-
</DropdownMenuTrigger>
|
|
625
|
-
<DropdownMenuContent align="end" className="w-56 p-2 z-[1001]">
|
|
626
|
-
<div className="px-2 py-3">
|
|
627
|
-
<div className="flex items-center justify-between mb-3 px-1">
|
|
628
|
-
<div className="flex items-center gap-2">
|
|
629
|
-
{isMuted || volume === 0 ? (
|
|
630
|
-
<VolumeX className="w-4 h-4 text-muted-foreground" />
|
|
631
|
-
) : (
|
|
632
|
-
<Volume2 className="w-4 h-4 text-muted-foreground" />
|
|
633
|
-
)}
|
|
634
|
-
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
|
|
635
|
-
Volume
|
|
636
|
-
</span>
|
|
637
|
-
</div>
|
|
638
|
-
</div>
|
|
639
|
-
<Slider
|
|
640
|
-
value={[isMuted ? 0 : volume * 100]}
|
|
641
|
-
max={100}
|
|
642
|
-
onValueChange={val => handleVolumeChange([val[0] / 100])}
|
|
643
|
-
aria-label="Volume"
|
|
644
|
-
/>
|
|
645
|
-
</div>
|
|
646
|
-
|
|
647
|
-
<DropdownMenuSeparator className="my-2" />
|
|
648
|
-
|
|
649
|
-
<div className="px-2 py-1 flex flex-col gap-2">
|
|
650
|
-
<div className="flex items-center gap-2">
|
|
651
|
-
<Gauge className="w-4 h-4 text-muted-foreground" />
|
|
652
|
-
<span className="text-sm font-medium">{t('media.speed')}</span>
|
|
653
|
-
</div>
|
|
654
|
-
<div className="flex flex-wrap gap-1">
|
|
655
|
-
{[0.5, 1, 1.5, 2].map(speed => (
|
|
656
|
-
<button
|
|
657
|
-
key={speed}
|
|
658
|
-
onClick={() => setPlaybackSpeed(speed)}
|
|
659
|
-
className={cn(
|
|
660
|
-
'px-2 py-1 text-[10px] rounded border transition-colors',
|
|
661
|
-
playbackSpeed === speed
|
|
662
|
-
? 'bg-primary text-primary-foreground border-primary'
|
|
663
|
-
: 'bg-transparent border-border hover:bg-accent'
|
|
664
|
-
)}
|
|
665
|
-
>
|
|
666
|
-
{speed}x
|
|
667
|
-
</button>
|
|
668
|
-
))}
|
|
669
|
-
</div>
|
|
670
|
-
</div>
|
|
671
|
-
|
|
672
|
-
<DropdownMenuSeparator className="my-2" />
|
|
673
|
-
|
|
674
|
-
<DropdownMenuItem onClick={resetAudio} className="cursor-pointer py-2.5">
|
|
675
|
-
<RotateCcw className="w-4 h-4 mr-3 text-muted-foreground" />
|
|
676
|
-
<span className="text-sm">{t('media.restart')}</span>
|
|
677
|
-
</DropdownMenuItem>
|
|
678
|
-
|
|
679
|
-
<DropdownMenuItem className="cursor-pointer py-2.5">
|
|
680
|
-
<Download className="w-4 h-4 mr-3 text-muted-foreground" />
|
|
681
|
-
<span className="text-sm">{t('media.downloadFile')}</span>
|
|
682
|
-
</DropdownMenuItem>
|
|
683
|
-
</DropdownMenuContent>
|
|
684
|
-
</DropdownMenu>
|
|
685
|
-
|
|
686
|
-
{!isFloating && (
|
|
687
|
-
<Button
|
|
688
|
-
variant="ghost"
|
|
689
|
-
size="icon"
|
|
690
|
-
className={cn(
|
|
691
|
-
'h-8 w-8',
|
|
692
|
-
colorVariant === 'primary'
|
|
693
|
-
? 'text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/10'
|
|
694
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
695
|
-
)}
|
|
696
|
-
onClick={handleEnableManualFloat}
|
|
697
|
-
title={t('media.floatingMode')}
|
|
698
|
-
aria-label={t('media.openFloatingMode')}
|
|
699
|
-
>
|
|
700
|
-
<Maximize2 className="w-4 h-4" />
|
|
701
|
-
</Button>
|
|
702
|
-
)}
|
|
703
|
-
</div>
|
|
704
|
-
</div>
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
// ── Early return ────────────────────────────────────────────────────────────
|
|
708
|
-
if (!isVisible) return null;
|
|
709
|
-
|
|
710
|
-
// ── Bar variant — portal render ─────────────────────────────────────────────
|
|
711
|
-
if (variant === 'bar' && isVisible && typeof document !== 'undefined') {
|
|
712
|
-
return createPortal(
|
|
713
|
-
<div
|
|
714
|
-
className={cn(
|
|
715
|
-
'fixed bottom-0 z-[100] border-t transition-all duration-300 ease-in-out',
|
|
716
|
-
colorVariant === 'primary'
|
|
717
|
-
? 'bg-primary/95 text-primary-foreground border-primary/20 backdrop-blur-sm [&_.text-muted-foreground]:text-primary-foreground/70 [&_.text-foreground]:text-primary-foreground [&_button]:text-primary-foreground [&_[data-slot=slider-track]]:bg-primary-foreground/30 [&_[data-slot=slider-range]]:bg-primary-foreground [&_[data-slot=slider-thumb]]:border-primary-foreground [&_[data-slot=slider-thumb]]:bg-primary-foreground'
|
|
718
|
-
: 'bg-card/95 text-card-foreground border-border backdrop-blur-md shadow-[var(--elevation-sm)]',
|
|
719
|
-
isOpen ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0',
|
|
720
|
-
className
|
|
721
|
-
)}
|
|
722
|
-
style={{
|
|
723
|
-
left: isMobile ? 0 : sidebarExpanded ? sidebarWidth : 80,
|
|
724
|
-
right: isMobile ? 0 : assistenteExpanded ? 420 : 80,
|
|
725
|
-
}}
|
|
726
|
-
>
|
|
727
|
-
{audioElement}
|
|
728
|
-
{!isMobile ? desktopLayout : mobileLayout}
|
|
729
|
-
</div>,
|
|
730
|
-
document.body
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// ── Card variant — floating wrapper render ──────────────────────────────────
|
|
735
|
-
return (
|
|
736
|
-
<div ref={containerRef} className={className}>
|
|
737
|
-
{audioElement}
|
|
738
|
-
<FloatingMediaWrapper
|
|
739
|
-
isFloating={isFloating}
|
|
740
|
-
setIsFloating={handleSetFloating}
|
|
741
|
-
onClose={() => handleSetFloating(false)}
|
|
742
|
-
onCloseMedia={() => {
|
|
743
|
-
handleSetFloating(false);
|
|
744
|
-
}}
|
|
745
|
-
title={t('media.playingMedia')}
|
|
746
|
-
aspectRatio={320 / 110}
|
|
747
|
-
minHeight={110}
|
|
748
|
-
minWidth={320}
|
|
749
|
-
colorVariant={colorVariant}
|
|
750
|
-
className="w-full"
|
|
751
|
-
playerId="audio-player"
|
|
752
|
-
enablePadding
|
|
753
|
-
>
|
|
754
|
-
<div
|
|
755
|
-
className={cn(
|
|
756
|
-
'w-full overflow-hidden flex flex-col justify-center',
|
|
757
|
-
colorVariant === 'primary'
|
|
758
|
-
? 'bg-primary text-primary-foreground [&_[data-slot=slider-track]]:bg-white/20 [&_[data-slot=slider-range]]:bg-white [&_[data-slot=slider-thumb]]:border-white [&_[data-slot=slider-thumb]]:bg-white'
|
|
759
|
-
: 'bg-card text-card-foreground',
|
|
760
|
-
isFloating ? 'h-full bg-transparent' : 'border rounded-md shadow-sm'
|
|
761
|
-
)}
|
|
762
|
-
>
|
|
763
|
-
{desktopLayoutCompact}
|
|
764
|
-
</div>
|
|
765
|
-
</FloatingMediaWrapper>
|
|
766
|
-
</div>
|
|
767
|
-
);
|
|
768
|
-
}
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import {
|
|
5
|
+
Play,
|
|
6
|
+
Pause,
|
|
7
|
+
Volume2,
|
|
8
|
+
VolumeX,
|
|
9
|
+
Maximize2,
|
|
10
|
+
PlayCircle,
|
|
11
|
+
PauseCircle,
|
|
12
|
+
RotateCcw,
|
|
13
|
+
Gauge,
|
|
14
|
+
Info,
|
|
15
|
+
Download,
|
|
16
|
+
X,
|
|
17
|
+
ExternalLink,
|
|
18
|
+
Radio,
|
|
19
|
+
MoreVertical,
|
|
20
|
+
MoreHorizontal,
|
|
21
|
+
} from 'lucide-react';
|
|
22
|
+
import { Slider } from '../../ui/slider';
|
|
23
|
+
import { Button } from '../../ui/button';
|
|
24
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip';
|
|
25
|
+
import {
|
|
26
|
+
DropdownMenu,
|
|
27
|
+
DropdownMenuContent,
|
|
28
|
+
DropdownMenuItem,
|
|
29
|
+
DropdownMenuTrigger,
|
|
30
|
+
DropdownMenuSeparator,
|
|
31
|
+
} from '../../ui/dropdown-menu';
|
|
32
|
+
import { FloatingMediaWrapper } from '../FloatingMediaWrapper';
|
|
33
|
+
import { cn } from '../../shared/utils';
|
|
34
|
+
import { useAudioPlayer } from './use-audio-player';
|
|
35
|
+
|
|
36
|
+
export interface AudioPlayerProps {
|
|
37
|
+
/** Source URL of the audio file */
|
|
38
|
+
src?: string;
|
|
39
|
+
/** Title of the audio track/podcast */
|
|
40
|
+
title?: string;
|
|
41
|
+
/** Artist or subtitle information */
|
|
42
|
+
artist?: string;
|
|
43
|
+
/** Subtitle or source information (specific to 'bar' variant) */
|
|
44
|
+
subtitle?: string;
|
|
45
|
+
/** URL for cover art (specific to 'card' variant) */
|
|
46
|
+
coverArt?: string;
|
|
47
|
+
/** Whether to play automatically */
|
|
48
|
+
autoPlay?: boolean;
|
|
49
|
+
/** CSS class name for the container */
|
|
50
|
+
className?: string;
|
|
51
|
+
/** Display variant: 'card' (standard floating) or 'bar' (global fixed bar) */
|
|
52
|
+
variant?: 'card' | 'bar';
|
|
53
|
+
/** Whether the player is currently visible and open (only for 'bar' variant) */
|
|
54
|
+
isOpen?: boolean;
|
|
55
|
+
/** Callback to close the player bar (only for 'bar' variant) */
|
|
56
|
+
onClose?: () => void;
|
|
57
|
+
/** Total duration in seconds (optional, will be loaded from metadata if not provided) */
|
|
58
|
+
duration?: number;
|
|
59
|
+
/** Initial playback time in seconds */
|
|
60
|
+
currentTime?: number;
|
|
61
|
+
/** Color theme variant for the bar layout */
|
|
62
|
+
colorVariant?: 'default' | 'primary';
|
|
63
|
+
/** Whether the player should automatically float when scrolled out of view */
|
|
64
|
+
enableAutoFloat?: boolean;
|
|
65
|
+
/** Callback fired when user manually closes the floating player */
|
|
66
|
+
onCloseFloating?: () => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Unified Audio Player component.
|
|
71
|
+
*
|
|
72
|
+
* All state and logic is managed by the `useAudioPlayer` headless hook.
|
|
73
|
+
* This component is responsible only for rendering.
|
|
74
|
+
*/
|
|
75
|
+
export function AudioPlayer({
|
|
76
|
+
src,
|
|
77
|
+
title,
|
|
78
|
+
artist,
|
|
79
|
+
subtitle,
|
|
80
|
+
autoPlay = false,
|
|
81
|
+
className,
|
|
82
|
+
variant = 'card',
|
|
83
|
+
isOpen = true,
|
|
84
|
+
onClose,
|
|
85
|
+
duration: initialDuration,
|
|
86
|
+
currentTime: initialTime = 0,
|
|
87
|
+
colorVariant = 'default',
|
|
88
|
+
enableAutoFloat = true,
|
|
89
|
+
onCloseFloating,
|
|
90
|
+
}: AudioPlayerProps) {
|
|
91
|
+
const { t } = useTranslation();
|
|
92
|
+
|
|
93
|
+
const {
|
|
94
|
+
audioRef,
|
|
95
|
+
containerRef,
|
|
96
|
+
isPlaying,
|
|
97
|
+
currentTime,
|
|
98
|
+
duration,
|
|
99
|
+
volume,
|
|
100
|
+
isMuted,
|
|
101
|
+
playbackSpeed,
|
|
102
|
+
isFloating,
|
|
103
|
+
isManualFloating,
|
|
104
|
+
isVisible,
|
|
105
|
+
isMobile,
|
|
106
|
+
enableAutoFloatLocal,
|
|
107
|
+
sidebarExpanded,
|
|
108
|
+
sidebarWidth,
|
|
109
|
+
assistenteExpanded,
|
|
110
|
+
togglePlay,
|
|
111
|
+
toggleMute,
|
|
112
|
+
handleSeek,
|
|
113
|
+
handleVolumeChange,
|
|
114
|
+
handleSetFloating,
|
|
115
|
+
handleEnableManualFloat,
|
|
116
|
+
setPlaybackSpeed,
|
|
117
|
+
resetAudio,
|
|
118
|
+
formatTime,
|
|
119
|
+
onPlay,
|
|
120
|
+
onPause,
|
|
121
|
+
onEnded,
|
|
122
|
+
onTimeUpdate,
|
|
123
|
+
onLoadedMetadata,
|
|
124
|
+
} = useAudioPlayer({
|
|
125
|
+
src,
|
|
126
|
+
autoPlay,
|
|
127
|
+
initialTime,
|
|
128
|
+
initialDuration,
|
|
129
|
+
variant,
|
|
130
|
+
isOpen,
|
|
131
|
+
enableAutoFloat,
|
|
132
|
+
onCloseFloating,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ── Audio element (hidden) ──────────────────────────────────────────────────
|
|
136
|
+
const audioElement = (
|
|
137
|
+
<audio
|
|
138
|
+
ref={audioRef}
|
|
139
|
+
src={src}
|
|
140
|
+
autoPlay={autoPlay}
|
|
141
|
+
className="hidden"
|
|
142
|
+
onTimeUpdate={onTimeUpdate}
|
|
143
|
+
onLoadedMetadata={onLoadedMetadata}
|
|
144
|
+
onPlay={onPlay}
|
|
145
|
+
onPause={onPause}
|
|
146
|
+
onEnded={onEnded}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// ── Bar variant — desktop layout ────────────────────────────────────────────
|
|
151
|
+
const desktopLayout = (
|
|
152
|
+
<div
|
|
153
|
+
className={cn(
|
|
154
|
+
'px-6 flex items-center justify-between gap-4',
|
|
155
|
+
colorVariant === 'primary' ? 'h-[80px] gap-6' : 'h-[80px]'
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{/* Left: Info */}
|
|
159
|
+
<div
|
|
160
|
+
className={cn(
|
|
161
|
+
'flex flex-col min-w-0 shrink-0 relative z-10',
|
|
162
|
+
colorVariant === 'primary' ? 'w-[280px]' : 'w-[280px]'
|
|
163
|
+
)}
|
|
164
|
+
>
|
|
165
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
166
|
+
{colorVariant === 'primary' ? (
|
|
167
|
+
<div className="w-2 h-2 rounded-full bg-[var(--chart-4)] animate-pulse shrink-0" />
|
|
168
|
+
) : (
|
|
169
|
+
<Radio className="w-3 h-3 text-[var(--chart-4)] animate-pulse shrink-0" />
|
|
170
|
+
)}
|
|
171
|
+
<h4
|
|
172
|
+
className={cn(
|
|
173
|
+
'truncate',
|
|
174
|
+
colorVariant === 'primary'
|
|
175
|
+
? 'font-semibold text-base tracking-tight'
|
|
176
|
+
: 'font-medium text-base'
|
|
177
|
+
)}
|
|
178
|
+
title={title || t('media.audioTitle')}
|
|
179
|
+
>
|
|
180
|
+
{title || t('media.audioTitle')}
|
|
181
|
+
</h4>
|
|
182
|
+
</div>
|
|
183
|
+
<div
|
|
184
|
+
className={cn(
|
|
185
|
+
'flex items-center gap-1 text-xs text-muted-foreground',
|
|
186
|
+
colorVariant === 'primary' && 'opacity-80'
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
<span className="truncate">{subtitle || artist}</span>
|
|
190
|
+
<ExternalLink
|
|
191
|
+
className={cn(
|
|
192
|
+
'w-3 h-3 cursor-pointer hover:text-foreground ml-1 transition-opacity',
|
|
193
|
+
colorVariant === 'primary'
|
|
194
|
+
? 'opacity-60 hover:opacity-100'
|
|
195
|
+
: 'opacity-70 hover:opacity-100'
|
|
196
|
+
)}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Center: Controls */}
|
|
202
|
+
<div
|
|
203
|
+
className={cn(
|
|
204
|
+
'flex-1 flex items-center justify-center gap-4 min-w-0 max-w-xl relative z-10',
|
|
205
|
+
colorVariant === 'primary' && 'gap-4'
|
|
206
|
+
)}
|
|
207
|
+
>
|
|
208
|
+
<button
|
|
209
|
+
onClick={togglePlay}
|
|
210
|
+
aria-label={isPlaying ? t('media.pause') : t('media.play')}
|
|
211
|
+
className={cn(
|
|
212
|
+
'transition-all focus:outline-none shrink-0',
|
|
213
|
+
colorVariant === 'primary'
|
|
214
|
+
? 'hover:scale-110'
|
|
215
|
+
: 'text-muted-foreground hover:text-foreground hover:scale-110 active:scale-95 transform duration-100'
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
{isPlaying ? (
|
|
219
|
+
<PauseCircle
|
|
220
|
+
className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')}
|
|
221
|
+
strokeWidth={colorVariant === 'primary' ? 1 : 1.5}
|
|
222
|
+
/>
|
|
223
|
+
) : (
|
|
224
|
+
<PlayCircle
|
|
225
|
+
className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')}
|
|
226
|
+
strokeWidth={colorVariant === 'primary' ? 1 : 1.5}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
</button>
|
|
230
|
+
|
|
231
|
+
<div className="flex-1 flex items-center gap-3 max-w-lg">
|
|
232
|
+
<span
|
|
233
|
+
className={cn(
|
|
234
|
+
'text-xs font-mono shrink-0 w-10 text-right',
|
|
235
|
+
colorVariant === 'primary' ? 'text-[11px] w-12 opacity-80' : 'text-muted-foreground'
|
|
236
|
+
)}
|
|
237
|
+
>
|
|
238
|
+
{formatTime(currentTime)}
|
|
239
|
+
</span>
|
|
240
|
+
<Slider
|
|
241
|
+
value={[currentTime]}
|
|
242
|
+
max={duration || 100}
|
|
243
|
+
onValueChange={handleSeek}
|
|
244
|
+
className="cursor-pointer"
|
|
245
|
+
/>
|
|
246
|
+
<span
|
|
247
|
+
className={cn(
|
|
248
|
+
'text-xs font-mono shrink-0 w-10',
|
|
249
|
+
colorVariant === 'primary' ? 'text-[11px] w-12 opacity-80' : 'text-muted-foreground'
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
{formatTime(duration)}
|
|
253
|
+
</span>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Right: Actions */}
|
|
258
|
+
<div className="flex items-center gap-2 shrink-0 relative z-10">
|
|
259
|
+
<div className="hidden lg:flex items-center gap-2 w-28 mr-2 group">
|
|
260
|
+
<button
|
|
261
|
+
onClick={toggleMute}
|
|
262
|
+
className="text-muted-foreground hover:text-foreground"
|
|
263
|
+
aria-label={isMuted || volume === 0 ? t('media.unmute') : t('media.mute')}
|
|
264
|
+
>
|
|
265
|
+
{isMuted || volume === 0 ? (
|
|
266
|
+
<VolumeX className="w-5 h-5" />
|
|
267
|
+
) : (
|
|
268
|
+
<Volume2 className="w-5 h-5" />
|
|
269
|
+
)}
|
|
270
|
+
</button>
|
|
271
|
+
<Slider
|
|
272
|
+
value={[isMuted ? 0 : volume * 100]}
|
|
273
|
+
max={100}
|
|
274
|
+
onValueChange={val => handleVolumeChange([val[0] / 100])}
|
|
275
|
+
className="w-full opacity-60 group-hover:opacity-100 transition-opacity"
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="flex items-center gap-1">
|
|
280
|
+
<div className="hidden md:flex">
|
|
281
|
+
<TooltipProvider>
|
|
282
|
+
<Tooltip>
|
|
283
|
+
<TooltipTrigger asChild>
|
|
284
|
+
<Button
|
|
285
|
+
variant="ghost"
|
|
286
|
+
size="icon"
|
|
287
|
+
className="rounded-full h-9 w-9"
|
|
288
|
+
onClick={resetAudio}
|
|
289
|
+
aria-label={t('media.restart')}
|
|
290
|
+
>
|
|
291
|
+
<RotateCcw className="w-4 h-4" />
|
|
292
|
+
</Button>
|
|
293
|
+
</TooltipTrigger>
|
|
294
|
+
<TooltipContent className="z-[1001]">{t('media.restart')}</TooltipContent>
|
|
295
|
+
</Tooltip>
|
|
296
|
+
</TooltipProvider>
|
|
297
|
+
|
|
298
|
+
<TooltipProvider>
|
|
299
|
+
<Tooltip>
|
|
300
|
+
<TooltipTrigger asChild>
|
|
301
|
+
<Button
|
|
302
|
+
variant="ghost"
|
|
303
|
+
size="icon"
|
|
304
|
+
className="text-[var(--chart-3)] hover:bg-accent rounded-full h-9 w-9"
|
|
305
|
+
aria-label={t('media.audioInfo')}
|
|
306
|
+
>
|
|
307
|
+
<Info className="w-4 h-4 fill-current" />
|
|
308
|
+
</Button>
|
|
309
|
+
</TooltipTrigger>
|
|
310
|
+
<TooltipContent className="z-[1001]">{t('media.info')}</TooltipContent>
|
|
311
|
+
</Tooltip>
|
|
312
|
+
</TooltipProvider>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<div className="hidden xl:flex items-center gap-1">
|
|
316
|
+
<DropdownMenu>
|
|
317
|
+
<DropdownMenuTrigger asChild>
|
|
318
|
+
<Button
|
|
319
|
+
variant="ghost"
|
|
320
|
+
size="icon"
|
|
321
|
+
className="rounded-full h-9 w-9"
|
|
322
|
+
aria-label={t('media.selectSpeed')}
|
|
323
|
+
>
|
|
324
|
+
<Gauge className="w-4 h-4" />
|
|
325
|
+
</Button>
|
|
326
|
+
</DropdownMenuTrigger>
|
|
327
|
+
<DropdownMenuContent align="center" className="w-32 z-[1001]">
|
|
328
|
+
{[0.5, 1, 1.5, 2].map(speed => (
|
|
329
|
+
<DropdownMenuItem
|
|
330
|
+
key={speed}
|
|
331
|
+
onClick={() => setPlaybackSpeed(speed)}
|
|
332
|
+
className={cn(
|
|
333
|
+
'cursor-pointer',
|
|
334
|
+
playbackSpeed === speed && 'bg-accent font-bold'
|
|
335
|
+
)}
|
|
336
|
+
>
|
|
337
|
+
{speed}x
|
|
338
|
+
</DropdownMenuItem>
|
|
339
|
+
))}
|
|
340
|
+
</DropdownMenuContent>
|
|
341
|
+
</DropdownMenu>
|
|
342
|
+
|
|
343
|
+
<TooltipProvider>
|
|
344
|
+
<Tooltip>
|
|
345
|
+
<TooltipTrigger asChild>
|
|
346
|
+
<Button
|
|
347
|
+
variant="ghost"
|
|
348
|
+
size="icon"
|
|
349
|
+
className="rounded-full h-9 w-9"
|
|
350
|
+
aria-label={t('media.downloadAudio')}
|
|
351
|
+
>
|
|
352
|
+
<Download className="w-4 h-4" />
|
|
353
|
+
</Button>
|
|
354
|
+
</TooltipTrigger>
|
|
355
|
+
<TooltipContent className="z-[1001]">{t('media.downloadAudio')}</TooltipContent>
|
|
356
|
+
</Tooltip>
|
|
357
|
+
</TooltipProvider>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<div className="xl:hidden">
|
|
361
|
+
<DropdownMenu>
|
|
362
|
+
<DropdownMenuTrigger asChild>
|
|
363
|
+
<Button
|
|
364
|
+
variant="ghost"
|
|
365
|
+
size="icon"
|
|
366
|
+
className="rounded-full h-9 w-9"
|
|
367
|
+
aria-label={t('media.moreOptions')}
|
|
368
|
+
>
|
|
369
|
+
<MoreHorizontal className="w-5 h-5" />
|
|
370
|
+
</Button>
|
|
371
|
+
</DropdownMenuTrigger>
|
|
372
|
+
<DropdownMenuContent align="end" className="w-56 p-2 z-[1001]">
|
|
373
|
+
<div className="lg:hidden">
|
|
374
|
+
<div className="px-2 py-3">
|
|
375
|
+
<div className="flex items-center justify-between mb-3 px-1">
|
|
376
|
+
<div className="flex items-center gap-2">
|
|
377
|
+
{isMuted || volume === 0 ? (
|
|
378
|
+
<VolumeX className="w-4 h-4 text-muted-foreground" />
|
|
379
|
+
) : (
|
|
380
|
+
<Volume2 className="w-4 h-4 text-muted-foreground" />
|
|
381
|
+
)}
|
|
382
|
+
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
|
|
383
|
+
Volume
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<Slider
|
|
388
|
+
value={[isMuted ? 0 : volume * 100]}
|
|
389
|
+
max={100}
|
|
390
|
+
onValueChange={val => handleVolumeChange([val[0] / 100])}
|
|
391
|
+
/>
|
|
392
|
+
</div>
|
|
393
|
+
<DropdownMenuSeparator className="my-1" />
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<div className="md:hidden">
|
|
397
|
+
<DropdownMenuItem onClick={resetAudio} className="cursor-pointer py-2.5">
|
|
398
|
+
<RotateCcw className="w-4 h-4 mr-3 text-muted-foreground" />{' '}
|
|
399
|
+
{t('media.restart')}
|
|
400
|
+
</DropdownMenuItem>
|
|
401
|
+
<DropdownMenuItem className="cursor-pointer py-2.5">
|
|
402
|
+
<Info className="w-4 h-4 mr-3 text-muted-foreground" /> {t('media.info')}
|
|
403
|
+
</DropdownMenuItem>
|
|
404
|
+
<DropdownMenuSeparator className="my-1" />
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<div className="xl:hidden">
|
|
408
|
+
<div className="px-3 py-2 flex flex-col gap-2">
|
|
409
|
+
<div className="flex items-center gap-2">
|
|
410
|
+
<Gauge className="w-4 h-4 text-muted-foreground" />
|
|
411
|
+
<span className="text-sm font-medium">{t('media.speed')}</span>
|
|
412
|
+
</div>
|
|
413
|
+
<div className="flex flex-wrap gap-1">
|
|
414
|
+
{[0.5, 1, 1.5, 2].map(speed => (
|
|
415
|
+
<button
|
|
416
|
+
key={speed}
|
|
417
|
+
onClick={() => setPlaybackSpeed(speed)}
|
|
418
|
+
className={cn(
|
|
419
|
+
'px-2 py-0.5 text-[10px] rounded border transition-colors',
|
|
420
|
+
playbackSpeed === speed
|
|
421
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
422
|
+
: 'bg-transparent border-border hover:bg-accent'
|
|
423
|
+
)}
|
|
424
|
+
>
|
|
425
|
+
{speed}x
|
|
426
|
+
</button>
|
|
427
|
+
))}
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
<DropdownMenuSeparator className="my-1" />
|
|
431
|
+
<DropdownMenuItem className="cursor-pointer py-2.5">
|
|
432
|
+
<Download className="w-4 h-4 mr-3 text-muted-foreground" />{' '}
|
|
433
|
+
{t('media.downloadAudio')}
|
|
434
|
+
</DropdownMenuItem>
|
|
435
|
+
</div>
|
|
436
|
+
</DropdownMenuContent>
|
|
437
|
+
</DropdownMenu>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div className="w-px h-8 bg-border mx-1" />
|
|
441
|
+
|
|
442
|
+
<Button
|
|
443
|
+
onClick={onClose}
|
|
444
|
+
variant="ghost"
|
|
445
|
+
size="icon"
|
|
446
|
+
className="rounded-full h-9 w-9"
|
|
447
|
+
aria-label={t('media.closePlayer')}
|
|
448
|
+
>
|
|
449
|
+
<X className="w-5 h-5" />
|
|
450
|
+
</Button>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
// ── Bar variant — mobile layout ─────────────────────────────────────────────
|
|
457
|
+
const mobileLayout = (
|
|
458
|
+
<div
|
|
459
|
+
className={cn(
|
|
460
|
+
'flex flex-col py-3 px-4 gap-3',
|
|
461
|
+
colorVariant === 'primary' && 'py-4 px-5 gap-4'
|
|
462
|
+
)}
|
|
463
|
+
>
|
|
464
|
+
<div className="flex items-start justify-between gap-3">
|
|
465
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
466
|
+
<div className="flex items-center gap-2 mb-1">
|
|
467
|
+
{colorVariant === 'primary' ? (
|
|
468
|
+
<div className="w-2 h-2 rounded-full bg-[var(--chart-4)] animate-pulse shrink-0" />
|
|
469
|
+
) : (
|
|
470
|
+
<Radio className="w-3 h-3 text-[var(--chart-4)] animate-pulse shrink-0" />
|
|
471
|
+
)}
|
|
472
|
+
<h4
|
|
473
|
+
className={cn(
|
|
474
|
+
'truncate',
|
|
475
|
+
colorVariant === 'primary'
|
|
476
|
+
? 'font-semibold text-sm tracking-tight'
|
|
477
|
+
: 'font-medium text-sm'
|
|
478
|
+
)}
|
|
479
|
+
>
|
|
480
|
+
{title}
|
|
481
|
+
</h4>
|
|
482
|
+
</div>
|
|
483
|
+
<div
|
|
484
|
+
className={cn(
|
|
485
|
+
'flex items-center gap-1.5 text-xs text-muted-foreground',
|
|
486
|
+
colorVariant === 'primary' && 'opacity-70'
|
|
487
|
+
)}
|
|
488
|
+
>
|
|
489
|
+
<span className="truncate">{subtitle || artist}</span>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
<Button
|
|
493
|
+
onClick={onClose}
|
|
494
|
+
variant="ghost"
|
|
495
|
+
size="icon"
|
|
496
|
+
className="rounded-full h-8 w-8 shrink-0"
|
|
497
|
+
aria-label={t('media.closePlayer')}
|
|
498
|
+
>
|
|
499
|
+
<X className="w-4 h-4" />
|
|
500
|
+
</Button>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<div className="flex items-center gap-3">
|
|
504
|
+
<button
|
|
505
|
+
onClick={togglePlay}
|
|
506
|
+
aria-label={isPlaying ? t('media.pause') : t('media.play')}
|
|
507
|
+
className="shrink-0 transition-transform active:scale-95"
|
|
508
|
+
>
|
|
509
|
+
{isPlaying ? (
|
|
510
|
+
<PauseCircle
|
|
511
|
+
className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')}
|
|
512
|
+
strokeWidth={colorVariant === 'primary' ? 1 : 1.5}
|
|
513
|
+
/>
|
|
514
|
+
) : (
|
|
515
|
+
<PlayCircle
|
|
516
|
+
className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')}
|
|
517
|
+
strokeWidth={colorVariant === 'primary' ? 1 : 1.5}
|
|
518
|
+
/>
|
|
519
|
+
)}
|
|
520
|
+
</button>
|
|
521
|
+
|
|
522
|
+
<div className="flex-1 flex items-center gap-2">
|
|
523
|
+
<Slider
|
|
524
|
+
value={[currentTime]}
|
|
525
|
+
max={duration || 100}
|
|
526
|
+
onValueChange={handleSeek}
|
|
527
|
+
className="cursor-pointer"
|
|
528
|
+
aria-label={t('media.playbackProgress')}
|
|
529
|
+
/>
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
<DropdownMenu>
|
|
533
|
+
<DropdownMenuTrigger asChild>
|
|
534
|
+
<Button
|
|
535
|
+
variant="ghost"
|
|
536
|
+
size="icon"
|
|
537
|
+
className="h-9 w-9 rounded-full"
|
|
538
|
+
aria-label={t('media.moreOptions')}
|
|
539
|
+
>
|
|
540
|
+
<MoreVertical className="w-5 h-5" />
|
|
541
|
+
</Button>
|
|
542
|
+
</DropdownMenuTrigger>
|
|
543
|
+
<DropdownMenuContent align="end" className="w-56 p-2 z-[1001]">
|
|
544
|
+
<DropdownMenuItem onClick={resetAudio}>{t('media.restart')}</DropdownMenuItem>
|
|
545
|
+
<DropdownMenuItem>{t('media.speedLabel', { speed: playbackSpeed })}</DropdownMenuItem>
|
|
546
|
+
</DropdownMenuContent>
|
|
547
|
+
</DropdownMenu>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
// ── Card variant — compact layout ───────────────────────────────────────────
|
|
553
|
+
const desktopLayoutCompact = (
|
|
554
|
+
<div
|
|
555
|
+
className={cn(
|
|
556
|
+
'flex items-center gap-3 w-full',
|
|
557
|
+
isFloating ? 'px-3 py-1 text-foreground' : 'px-4 py-3'
|
|
558
|
+
)}
|
|
559
|
+
>
|
|
560
|
+
<Button
|
|
561
|
+
onClick={togglePlay}
|
|
562
|
+
size="icon"
|
|
563
|
+
variant="outline"
|
|
564
|
+
className={cn(
|
|
565
|
+
'shrink-0 rounded-full border-border text-foreground hover:bg-accent transition-colors',
|
|
566
|
+
colorVariant === 'primary' &&
|
|
567
|
+
'border-primary-foreground/20 hover:bg-primary-foreground/10 text-primary-foreground',
|
|
568
|
+
isFloating ? 'h-8 w-8' : 'h-10 w-10'
|
|
569
|
+
)}
|
|
570
|
+
aria-label={isPlaying ? t('media.pause') : t('media.play')}
|
|
571
|
+
>
|
|
572
|
+
{isPlaying ? (
|
|
573
|
+
<Pause className={cn(isFloating ? 'w-3 h-3' : 'w-4 h-4', 'fill-current')} />
|
|
574
|
+
) : (
|
|
575
|
+
<Play className={cn(isFloating ? 'w-3 h-3' : 'w-4 h-4', 'fill-current ml-0.5')} />
|
|
576
|
+
)}
|
|
577
|
+
</Button>
|
|
578
|
+
<div className="flex-1 min-w-0 flex flex-col justify-center gap-1.5">
|
|
579
|
+
<div className="flex items-center justify-between text-xs leading-none">
|
|
580
|
+
<span
|
|
581
|
+
className={cn(
|
|
582
|
+
'font-medium truncate',
|
|
583
|
+
colorVariant === 'primary' ? 'text-primary-foreground' : 'text-foreground'
|
|
584
|
+
)}
|
|
585
|
+
>
|
|
586
|
+
{title}
|
|
587
|
+
</span>
|
|
588
|
+
<span
|
|
589
|
+
className={cn(
|
|
590
|
+
'font-medium tabular-nums text-[10px] sm:text-xs',
|
|
591
|
+
colorVariant === 'primary' ? 'text-primary-foreground/70' : 'text-muted-foreground'
|
|
592
|
+
)}
|
|
593
|
+
>
|
|
594
|
+
{formatTime(currentTime)} / {formatTime(duration)}
|
|
595
|
+
</span>
|
|
596
|
+
</div>
|
|
597
|
+
<Slider
|
|
598
|
+
value={[currentTime]}
|
|
599
|
+
max={duration || 100}
|
|
600
|
+
step={1}
|
|
601
|
+
onValueChange={handleSeek}
|
|
602
|
+
className="w-full"
|
|
603
|
+
aria-label={t('media.playbackProgress')}
|
|
604
|
+
/>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
608
|
+
<DropdownMenu>
|
|
609
|
+
<DropdownMenuTrigger asChild>
|
|
610
|
+
<Button
|
|
611
|
+
variant="ghost"
|
|
612
|
+
size="icon"
|
|
613
|
+
className={cn(
|
|
614
|
+
'rounded-full',
|
|
615
|
+
colorVariant === 'primary'
|
|
616
|
+
? 'text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/10'
|
|
617
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
618
|
+
isFloating ? 'h-7 w-7' : 'h-8 w-8'
|
|
619
|
+
)}
|
|
620
|
+
aria-label="Mais opções"
|
|
621
|
+
>
|
|
622
|
+
<MoreVertical className={isFloating ? 'w-4 h-4' : 'w-5 h-5'} />
|
|
623
|
+
</Button>
|
|
624
|
+
</DropdownMenuTrigger>
|
|
625
|
+
<DropdownMenuContent align="end" className="w-56 p-2 z-[1001]">
|
|
626
|
+
<div className="px-2 py-3">
|
|
627
|
+
<div className="flex items-center justify-between mb-3 px-1">
|
|
628
|
+
<div className="flex items-center gap-2">
|
|
629
|
+
{isMuted || volume === 0 ? (
|
|
630
|
+
<VolumeX className="w-4 h-4 text-muted-foreground" />
|
|
631
|
+
) : (
|
|
632
|
+
<Volume2 className="w-4 h-4 text-muted-foreground" />
|
|
633
|
+
)}
|
|
634
|
+
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
|
|
635
|
+
Volume
|
|
636
|
+
</span>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
<Slider
|
|
640
|
+
value={[isMuted ? 0 : volume * 100]}
|
|
641
|
+
max={100}
|
|
642
|
+
onValueChange={val => handleVolumeChange([val[0] / 100])}
|
|
643
|
+
aria-label="Volume"
|
|
644
|
+
/>
|
|
645
|
+
</div>
|
|
646
|
+
|
|
647
|
+
<DropdownMenuSeparator className="my-2" />
|
|
648
|
+
|
|
649
|
+
<div className="px-2 py-1 flex flex-col gap-2">
|
|
650
|
+
<div className="flex items-center gap-2">
|
|
651
|
+
<Gauge className="w-4 h-4 text-muted-foreground" />
|
|
652
|
+
<span className="text-sm font-medium">{t('media.speed')}</span>
|
|
653
|
+
</div>
|
|
654
|
+
<div className="flex flex-wrap gap-1">
|
|
655
|
+
{[0.5, 1, 1.5, 2].map(speed => (
|
|
656
|
+
<button
|
|
657
|
+
key={speed}
|
|
658
|
+
onClick={() => setPlaybackSpeed(speed)}
|
|
659
|
+
className={cn(
|
|
660
|
+
'px-2 py-1 text-[10px] rounded border transition-colors',
|
|
661
|
+
playbackSpeed === speed
|
|
662
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
663
|
+
: 'bg-transparent border-border hover:bg-accent'
|
|
664
|
+
)}
|
|
665
|
+
>
|
|
666
|
+
{speed}x
|
|
667
|
+
</button>
|
|
668
|
+
))}
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
<DropdownMenuSeparator className="my-2" />
|
|
673
|
+
|
|
674
|
+
<DropdownMenuItem onClick={resetAudio} className="cursor-pointer py-2.5">
|
|
675
|
+
<RotateCcw className="w-4 h-4 mr-3 text-muted-foreground" />
|
|
676
|
+
<span className="text-sm">{t('media.restart')}</span>
|
|
677
|
+
</DropdownMenuItem>
|
|
678
|
+
|
|
679
|
+
<DropdownMenuItem className="cursor-pointer py-2.5">
|
|
680
|
+
<Download className="w-4 h-4 mr-3 text-muted-foreground" />
|
|
681
|
+
<span className="text-sm">{t('media.downloadFile')}</span>
|
|
682
|
+
</DropdownMenuItem>
|
|
683
|
+
</DropdownMenuContent>
|
|
684
|
+
</DropdownMenu>
|
|
685
|
+
|
|
686
|
+
{!isFloating && (
|
|
687
|
+
<Button
|
|
688
|
+
variant="ghost"
|
|
689
|
+
size="icon"
|
|
690
|
+
className={cn(
|
|
691
|
+
'h-8 w-8',
|
|
692
|
+
colorVariant === 'primary'
|
|
693
|
+
? 'text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/10'
|
|
694
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
695
|
+
)}
|
|
696
|
+
onClick={handleEnableManualFloat}
|
|
697
|
+
title={t('media.floatingMode')}
|
|
698
|
+
aria-label={t('media.openFloatingMode')}
|
|
699
|
+
>
|
|
700
|
+
<Maximize2 className="w-4 h-4" />
|
|
701
|
+
</Button>
|
|
702
|
+
)}
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// ── Early return ────────────────────────────────────────────────────────────
|
|
708
|
+
if (!isVisible) return null;
|
|
709
|
+
|
|
710
|
+
// ── Bar variant — portal render ─────────────────────────────────────────────
|
|
711
|
+
if (variant === 'bar' && isVisible && typeof document !== 'undefined') {
|
|
712
|
+
return createPortal(
|
|
713
|
+
<div
|
|
714
|
+
className={cn(
|
|
715
|
+
'fixed bottom-0 z-[100] border-t transition-all duration-300 ease-in-out',
|
|
716
|
+
colorVariant === 'primary'
|
|
717
|
+
? 'bg-primary/95 text-primary-foreground border-primary/20 backdrop-blur-sm [&_.text-muted-foreground]:text-primary-foreground/70 [&_.text-foreground]:text-primary-foreground [&_button]:text-primary-foreground [&_[data-slot=slider-track]]:bg-primary-foreground/30 [&_[data-slot=slider-range]]:bg-primary-foreground [&_[data-slot=slider-thumb]]:border-primary-foreground [&_[data-slot=slider-thumb]]:bg-primary-foreground'
|
|
718
|
+
: 'bg-card/95 text-card-foreground border-border backdrop-blur-md shadow-[var(--elevation-sm)]',
|
|
719
|
+
isOpen ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0',
|
|
720
|
+
className
|
|
721
|
+
)}
|
|
722
|
+
style={{
|
|
723
|
+
left: isMobile ? 0 : sidebarExpanded ? sidebarWidth : 80,
|
|
724
|
+
right: isMobile ? 0 : assistenteExpanded ? 420 : 80,
|
|
725
|
+
}}
|
|
726
|
+
>
|
|
727
|
+
{audioElement}
|
|
728
|
+
{!isMobile ? desktopLayout : mobileLayout}
|
|
729
|
+
</div>,
|
|
730
|
+
document.body
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ── Card variant — floating wrapper render ──────────────────────────────────
|
|
735
|
+
return (
|
|
736
|
+
<div ref={containerRef} className={className}>
|
|
737
|
+
{audioElement}
|
|
738
|
+
<FloatingMediaWrapper
|
|
739
|
+
isFloating={isFloating}
|
|
740
|
+
setIsFloating={handleSetFloating}
|
|
741
|
+
onClose={() => handleSetFloating(false)}
|
|
742
|
+
onCloseMedia={() => {
|
|
743
|
+
handleSetFloating(false);
|
|
744
|
+
}}
|
|
745
|
+
title={t('media.playingMedia')}
|
|
746
|
+
aspectRatio={320 / 110}
|
|
747
|
+
minHeight={110}
|
|
748
|
+
minWidth={320}
|
|
749
|
+
colorVariant={colorVariant}
|
|
750
|
+
className="w-full"
|
|
751
|
+
playerId="audio-player"
|
|
752
|
+
enablePadding
|
|
753
|
+
>
|
|
754
|
+
<div
|
|
755
|
+
className={cn(
|
|
756
|
+
'w-full overflow-hidden flex flex-col justify-center',
|
|
757
|
+
colorVariant === 'primary'
|
|
758
|
+
? 'bg-primary text-primary-foreground [&_[data-slot=slider-track]]:bg-white/20 [&_[data-slot=slider-range]]:bg-white [&_[data-slot=slider-thumb]]:border-white [&_[data-slot=slider-thumb]]:bg-white'
|
|
759
|
+
: 'bg-card text-card-foreground',
|
|
760
|
+
isFloating ? 'h-full bg-transparent' : 'border rounded-md shadow-sm'
|
|
761
|
+
)}
|
|
762
|
+
>
|
|
763
|
+
{desktopLayoutCompact}
|
|
764
|
+
</div>
|
|
765
|
+
</FloatingMediaWrapper>
|
|
766
|
+
</div>
|
|
767
|
+
);
|
|
768
|
+
}
|