@ -1,7 +1,6 @@
import type { MouseEvent as ReactMouseEvent , ReactNode } from "react" ;
import { useCallback , useEffect , useRef , useState } from "react" ;
import { createPortal } from "react-dom" ;
import { Dropdown , Message , Tooltip } from "@arco-design/web-react" ;
import { Dropdown , Tooltip } from "@arco-design/web-react" ;
import { Link , useLocation , useNavigate } from "react-router-dom" ;
import { useTranslation } from "react-i18next" ;
import { useShallow } from "zustand/react/shallow" ;
@ -12,14 +11,12 @@ import { hasPendingExploreTemplateTabIntent } from "@/store/useExploreFeedTabSto
import { isLayoutContentScrollLocked , isScrollEventFromLayoutModalMount , isScrollEventInLayoutContentScroll , setLayoutContentModalMount , setLayoutContentScrollRoot } from "@/layouts/layoutContentModalMount" ;
import { changeLanguage , type AppLanguage , SUPPORTED_LANGUAGES } from "@/i18n" ;
import { closeAssetDetailsModal , closeCharacterDetailModal , openGatewayApikeyDialog , openLoginDialog , openPointsDetailDialog , openPointsRechargeDialog } from "@/dialog" ;
import { AliceLiveClassDialogOverlay } from "@/dialog/alice-live-class" ;
import { AppMacDownloadDialogOverlay } from "@/dialog/app-download-mac" ;
import { canAccessRoute , guardRouteAccess } from "@/router/routeGuard" ;
import { useComposerStore } from "@/store/useComposerStore" ;
import { useUserStore } from "@/store/useUserStore" ;
import { getToken , removeToken } from "@/utils/auth" ;
import { openLatestPrivacyPolicyLink , PRIVACY_POLICY_KEY_AI_USE_NOTICE , PRIVACY_POLICY_KEY_PRIVACY_POLICY , PRIVACY_POLICY_KEY_USAGE_TUTORIAL , PRIVACY_POLICY_KEY_USER_TERMS } from "@/utils/openLatestPrivacyPolicyLink" ;
import { getAppLatest , get ParamConfig , logout } from "@/api/api" ;
import { getParamConfig , logout } from "@/api/api" ;
import { useCopyText } from "@/hooks/useCopyText" ;
import { useLayoutViewportScale } from "@/hooks/useViewportScale" ;
import { memberLevelMap } from "@/constants" ;
@ -34,44 +31,78 @@ interface LayoutSideNavItem {
id : string ;
labelKey : string ;
iconSrc : string ;
activeIconSrc? : string ;
to : string ;
badge? : string ;
badgeKey? : string ;
}
interface LayoutSideNavIconProps {
item : LayoutSideNavItem ;
}
const CANVAS_BASE_URL = import . meta . env . VITE_CANVAS_URL as string | undefined ;
// 侧栏图标(顺序:探索 → 角色 → 声音 → 创造 → Popi TV)
const LAYOUT_NAV_ASSETS = {
logo : new URL ( "@/assets/images/layout/nav/popi-logo.png" , import . meta . url ) . href ,
collapse : new URL ( "@/assets/images/layout/nav/collapse.png" , import . meta . url ) . href ,
explore : new URL ( "@/assets/images/layout/nav/explore.png" , import . meta . url ) . href ,
exploreActive : new URL ( "@/assets/images/layout/nav/explore_active.png" , import . meta . url ) . href ,
characters : new URL ( "@/assets/images/layout/nav/characters.png" , import . meta . url ) . href ,
charactersActive : new URL ( "@/assets/images/layout/nav/characters_active.png" , import . meta . url ) . href ,
create : new URL ( "@/assets/images/layout/nav/create.png" , import . meta . url ) . href ,
createActive : new URL ( "@/assets/images/layout/nav/create_active.png" , import . meta . url ) . href ,
canvas : new URL ( "@/assets/images/layout/nav/popi-tv.png" , import . meta . url ) . href ,
canvasActive : new URL ( "@/assets/images/layout/nav/popi-tv_active.png" , import . meta . url ) . href ,
voice : new URL ( "@/assets/images/layout/nav/voice.png" , import . meta . url ) . href ,
voiceActive : new URL ( "@/assets/images/layout/nav/voice_active.png" , import . meta . url ) . href ,
subscribe : new URL ( "@/assets/images/layout/nav/subscribe.png" , import . meta . url ) . href ,
subscribeActive : new URL ( "@/assets/images/layout/nav/subscribe_active.png" , import . meta . url ) . href ,
profile : new URL ( "@/assets/images/layout/nav/profile.png" , import . meta . url ) . href ,
profileActive : new URL ( "@/assets/images/layout/nav/profile_active.png" , import . meta . url ) . href ,
more : new URL ( "@/assets/images/layout/nav/more.png" , import . meta . url ) . href ,
topHelp : new URL ( "@/assets/images/layout/nav/top-language.png" , import . meta . url ) . href ,
topLanguage : new URL ( "@/assets/images/layout/nav/top-help.png" , import . meta . url ) . href ,
topService : new URL ( "@/assets/images/layout/nav/top-weixin.png" , import . meta . url ) . href ,
topSkill : new URL ( "@/assets/images/layout/nav/top-skill.png" , import . meta . url ) . href ,
} as const ;
// 侧栏图标顺序与 2026 7月官网 Figma 设计保持一致。
const LAYOUT_SIDE_NAV_ITEMS : readonly LayoutSideNavItem [ ] = [
{
id : "explore" ,
labelKey : "layout.nav.explore" ,
iconSrc : new URL ( "@/assets/images/menu/explore.png" , import . meta . url ) . href ,
iconSrc : LAYOUT_NAV_ASSETS.explore ,
activeIconSrc : LAYOUT_NAV_ASSETS.exploreActive ,
to : ROUTES.EXPLORE ,
} ,
{
id : "characters" ,
labelKey : "layout.nav.characters" ,
iconSrc : new URL ( "@/assets/images/menu/characters.png" , import . meta . url ) . href ,
iconSrc : LAYOUT_NAV_ASSETS.characters ,
activeIconSrc : LAYOUT_NAV_ASSETS.charactersActive ,
to : ROUTES.CHARACTERS ,
} ,
{
id : "voice" ,
labelKey : "layout.nav.voice" ,
iconSrc : new URL ( "@/assets/images/menu/voice.png" , import . meta . url ) . href ,
to : ROUTES.VOICE ,
} ,
{
id : "create" ,
labelKey : "layout.nav.create" ,
iconSrc : new URL ( "@/assets/images/menu/create.png" , import . meta . url ) . href ,
iconSrc : LAYOUT_NAV_ASSETS.create ,
activeIconSrc : LAYOUT_NAV_ASSETS.createActive ,
to : ROUTES.CREATE ,
} ,
{
id : "canvas" ,
labelKey : "layout.nav.canvas" ,
iconSrc : new URL ( "@/assets/images/menu/canvas.png" , import . meta . url ) . href ,
iconSrc : LAYOUT_NAV_ASSETS.canvas ,
activeIconSrc : LAYOUT_NAV_ASSETS.canvasActive ,
to : CANVAS_BASE_URL ? ? "" ,
badge : "Beta" ,
badgeKey : "layout.nav.canvasBadge" ,
} ,
{
id : "voice" ,
labelKey : "layout.nav.voice" ,
iconSrc : LAYOUT_NAV_ASSETS.voice ,
activeIconSrc : LAYOUT_NAV_ASSETS.voiceActive ,
to : ROUTES.VOICE ,
} ,
] ;
@ -82,7 +113,6 @@ const LANGUAGE_LABEL_KEYS: Record<AppLanguage, string> = {
} ;
const LAYOUT_PROMO_BAR_DISMISSED_KEY = "popiart.layout.promoBar.dismissed" ;
const LAYOUT_APP_DOWNLOAD_TITLE_ICON = new URL ( "@/assets/images/layout/app-download/ic-popi-logo.png" , import . meta . url ) . href ;
function isLayoutPromoBarDismissed ( ) : boolean {
try {
@ -109,21 +139,43 @@ function buildCanvasUrl(): string | null {
return url . toString ( ) ;
}
function readServiceQrCode ( value : unknown ) : string {
if ( typeof value !== "object" || value == null || ! ( "systemConfig" in value ) ) return "" ;
const systemConfig = value . systemConfig ;
if ( typeof systemConfig !== "object" || systemConfig == null || ! ( "serviceQrCode" in systemConfig ) ) return "" ;
return typeof systemConfig . serviceQrCode === "string" ? systemConfig . serviceQrCode : "" ;
}
const LAYOUT_FOOTER_NAV_ITEMS : readonly LayoutSideNavItem [ ] = [
{
id : "subscribe" ,
labelKey : "layout.footer.subscribe" ,
iconSrc : new URL ( "@/assets/images/menu/subscribe.png" , import . meta . url ) . href ,
iconSrc : LAYOUT_NAV_ASSETS.subscribe ,
activeIconSrc : LAYOUT_NAV_ASSETS.subscribeActive ,
to : ROUTES.SUBSCRIBE ,
} ,
{
id : "profile" ,
labelKey : "layout.footer.profile" ,
iconSrc : new URL ( "@/assets/images/menu/profile.png" , import . meta . url ) . href ,
iconSrc : LAYOUT_NAV_ASSETS.profile ,
activeIconSrc : LAYOUT_NAV_ASSETS.profileActive ,
to : ROUTES.PROFILE ,
} ,
] ;
function LayoutSideNavIcon ( { item } : LayoutSideNavIconProps ) {
if ( ! item . activeIconSrc ) {
return < img className = "layoutShell__sideIcon" src = { item . iconSrc } alt = "" / > ;
}
return (
< span className = "layoutShell__sideIcon" aria - hidden = "true" >
< img className = "layoutShell__sideIconImage layoutShell__sideIconImage_default" src = { item . iconSrc } alt = "" / >
< img className = "layoutShell__sideIconImage layoutShell__sideIconImage_active" src = { item . activeIconSrc } alt = "" / >
< / span >
) ;
}
/ * *
* 主 布 局 ( 与 路 由 结 合 ) : 顶 部 栏 + 左 侧 栏 + 右 侧 内 容 区
*
@ -147,10 +199,8 @@ export function Layout({ children }: LayoutProps) {
const [ topbarServiceQrOpen , setTopbarServiceQrOpen ] = useState ( false ) ;
const [ sidebarCollapsed , setSidebarCollapsed ] = useState ( false ) ;
const [ promoBarVisible , setPromoBarVisible ] = useState ( ( ) = > ! isLayoutPromoBarDismissed ( ) ) ;
const [ aliceLiveClassOpen , setAliceLiveClassOpen ] = useState ( false ) ;
const activePromptPanelHost = useComposerStore ( ( s ) = > s . activePromptPanelHost ) ;
const footerRef = useRef < HTMLDivElement > ( null ) ;
const footerPopoverRef = useRef < HTMLDivElement > ( null ) ;
const userMenuRef = useRef < HTMLDivElement > ( null ) ;
const languageToolRef = useRef < HTMLDivElement > ( null ) ;
@ -231,35 +281,17 @@ export function Layout({ children }: LayoutProps) {
} , [ ] ) ;
const [ serviceQrCode , setServiceQrCode ] = useState ( "" ) ;
const [ macIntelDownloadUrl , setMacIntelDownloadUrl ] = useState ( "" ) ;
const [ macArmDownloadUrl , setMacArmDownloadUrl ] = useState ( "" ) ;
const [ windowsDownloadUrl , setWindowsDownloadUrl ] = useState ( "" ) ;
const [ appMacDownloadOpen , setAppMacDownloadOpen ] = useState ( false ) ;
useEffect ( ( ) = > {
void getParamConfig ( )
. then ( ( res ) = > {
setServiceQrCode ( res . systemConfig ? . serviceQrCode ? ? "" ) ;
setServiceQrCode ( readServiceQrCode ( res ) ) ;
} )
. catch ( ( ) = > {
setServiceQrCode ( "" ) ;
} ) ;
} , [ ] ) ;
useEffect ( ( ) = > {
void getAppLatest ( )
. then ( ( data ) = > {
setMacIntelDownloadUrl ( data . macIntel ? . url ? ? "" ) ;
setMacArmDownloadUrl ( data . macArm ? . url ? ? "" ) ;
setWindowsDownloadUrl ( data . windowsX64 ? . url ? ? "" ) ;
} )
. catch ( ( ) = > {
setMacIntelDownloadUrl ( "" ) ;
setMacArmDownloadUrl ( "" ) ;
setWindowsDownloadUrl ( "" ) ;
} ) ;
} , [ ] ) ;
useEffect ( ( ) = > {
if ( pathname === ROUTES . EXPLORE && hasPendingExploreTemplateTabIntent ( ) ) {
return ;
@ -413,12 +445,12 @@ export function Layout({ children }: LayoutProps) {
< span > { t ( "layout.promo.seedanceSuffix" ) } < / span >
< / p >
< div className = "layoutShell__promoSegment" >
< span className = "layoutShell__promoBadge" > { t ( "layout.promo.badgeHot" ) } < / span >
< p className = "layoutShell__promoText" >
< span > { t ( "layout.promo.discountPrefix" ) } < / span >
< span className = "layoutShell__promoText_heavy" > { t ( "layout.promo.discountValue" ) } < / span >
< span > { t ( "layout.promo.discountSuffix" ) } < / span >
< / p >
< span className = "layoutShell__promoBadge" > { t ( "layout.promo.badgeHot" ) } < / span >
< / div >
< p className = "layoutShell__promoText" >
< span > { t ( "layout.promo.pointsPrefix" ) } < / span >
@ -430,82 +462,45 @@ export function Layout({ children }: LayoutProps) {
< / button >
< / div >
) : null }
< button
type = "button"
className = "layoutShell__sidebarCollapse"
aria - disabled = { isCharactersCreateRoute }
onClick = { ( ) = > {
if ( ! isCharactersCreateRoute ) setSidebarCollapsed ( ( v ) = > ! v ) ;
} }
aria - label = { effectiveSidebarCollapsed ? t ( "layout.sidebar.expand" ) : t ( "layout.sidebar.collapse" ) }
aria - expanded = { ! effectiveSidebarCollapsed }
>
< span className = "layoutShell__sidebarCollapseText" > { t ( "layout.sidebar.menuLabel" ) } < / span >
< img src = { LAYOUT_NAV_ASSETS . collapse } alt = "" / >
< / button >
< header className = "layoutShell__topbar" aria - label = { t ( "layout.aria.topbar" ) } onClick = { dismissMainDetailModals } >
< div className = "layoutShell__topbarLeft flex items-center justify-between gap-2" >
{ ! effectiveSidebarCollapsed ? < img className = "layoutShell__logo" onClick = { ( ) = > navigate ( ROUTES . INDEX ) } src = { new URL ( "@/assets/images/popiart.png" , import . meta . url ) . href } / > : null }
< button
type = "button"
className = "layoutShell__collapse"
aria - disabled = { isCharactersCreateRoute }
onClick = { ( ) = > {
if ( ! isCharactersCreateRoute ) setSidebarCollapsed ( ( v ) = > ! v ) ;
} }
aria - label = { effectiveSidebarCollapsed ? t ( "layout.sidebar.expand" ) : t ( "layout.sidebar.collapse" ) }
aria - expanded = { ! effectiveSidebarCollapsed }
>
< img src = { new URL ( "@/assets/images/ic_collapse.png" , import . meta . url ) . href } / >
< / button >
< / div >
< div className = "layoutShell__topbarRight" aria - label = { t ( "layout.aria.userArea" ) } >
< div className = "layoutShell__aliceMeetupTool" >
< button type = "button" className = "layoutShell__aliceMeetup" aria - label = { t ( "layout.topbar.aliceMeetup" ) } >
< img className = "layoutShell__aliceMeetupIcon" src = { new URL ( "@/assets/images/popi_avatar.png" , import . meta . url ) . href } alt = "" / >
< span className = "layoutShell__aliceMeetupText" > { t ( "layout.topbar.aliceMeetup" ) } < / span >
< / button >
< div className = "layoutShell__aliceMeetupFloat" role = "dialog" aria - label = { t ( "layout.topbar.aliceMeetupBook" ) } onClick = { ( e ) = > e . stopPropagation ( ) } >
< img className = "layoutShell__aliceMeetupQr" src = { serviceQrCode } alt = { t ( "layout.topbar.aliceMeetupBook" ) } / >
< p className = "layoutShell__aliceMeetupFloatText" > { t ( "layout.topbar.aliceMeetupBook" ) } < / p >
< / div >
< / div >
< div className = "layoutShell__topbarTools" >
< div
< button
type = "button"
className = "goToSkillHub"
onClick = { ( ) = > {
const url = buildSkillHubUrl ( ) ;
if ( url ) window . open ( url , "_blank" ) ;
} }
>
< img src = { new URL ( "@/assets/images/dot.png" , import . meta . url ) . href } / >
SKILL
< / div >
{ /* 联系客服:顶栏定位浮层展示二维码 */ }
{ serviceQrCode ? (
< div className = "layoutShell__topbarTool" ref = { serviceToolRef } >
< button
type = "button"
className = { [ "layoutShell__topbarIconBtn" , "flex" , "items-center" , "justify-center" , topbarServiceQrOpen ? "layoutShell__topbarIconBtn_active" : "" ] . filter ( Boolean ) . join ( " " ) }
aria - label = { t ( "layout.topbar.contactServiceAria" ) }
aria - expanded = { topbarServiceQrOpen }
onClick = { ( e ) = > {
e . stopPropagation ( ) ;
setTopbarServiceQrOpen ( ( v ) = > ! v ) ;
setLanguageMenuOpen ( false ) ;
setUserMenuOpen ( false ) ;
setFooterPopoverOpen ( false ) ;
} }
>
< img src = { new URL ( "@/assets/images/ic_service_wx.png" , import . meta . url ) . href } alt = "" / >
< / button >
{ topbarServiceQrOpen ? (
< div className = "layoutShell__topbarServiceQrFloat" role = "dialog" aria - label = { t ( "layout.topbar.contactServiceAria" ) } onClick = { ( e ) = > e . stopPropagation ( ) } >
< img className = "layoutShell__topbarServiceQrImage" src = { serviceQrCode } alt = { t ( "contactService.qrAlt" ) } / >
< p className = "layoutShell__topbarServiceQrTip" > { t ( "layout.topbar.scan_contact_service" ) } < / p >
< / div >
) : null }
< / div >
) : null }
< img className = "layoutShell__topbarSkillIcon" src = { LAYOUT_NAV_ASSETS . topSkill } alt = "" / >
< span className = "layoutShell__topbarSkillText" > { t ( "layout.topbar.skill" ) } < / span >
< / button >
{ /* 使用说明 */ }
< div className = "layoutShell__topbarTool" >
< Tooltip content = { t ( "layout.usageTutorial" ) } >
< button
type = "button"
className = "layoutShell__topbarIconBtn flex items-center justify-center "
className = "layoutShell__topbarIconBtn"
onClick = { ( ) = > {
void openLatestPrivacyPolicyLink ( PRIVACY_POLICY_KEY_USAGE_TUTORIAL ) ;
} }
aria - label = { t ( "layout.usageTutorial" ) }
>
< img src = { new URL ( "@/assets/images/ic_help.png" , import . meta . url ) . href } / >
< img src = { LAYOUT_NAV_ASSETS . topHelp } alt = "" / >
< / button >
< / Tooltip >
< / div >
@ -550,16 +545,40 @@ export function Layout({ children }: LayoutProps) {
}
>
< Tooltip content = { t ( "layout.language.tooltip" ) } disabled = { languageMenuOpen } { ... ( languageMenuOpen ? { popupVisible : false } : {} ) } style = { { zIndex : 1000 } } >
< button type = "button" className = "layoutShell__topbarIconBtn flex items-center justify-center " aria - label = { t ( "layout.language.menuAria" ) } aria - expanded = { languageMenuOpen } aria - haspopup = "menu" >
< img src = { new URL ( "@/assets/images/ic_language.png" , import . meta . url ) . href } / >
< button type = "button" className = "layoutShell__topbarIconBtn" aria - label = { t ( "layout.language.menuAria" ) } aria - expanded = { languageMenuOpen } aria - haspopup = "menu" >
< img src = { LAYOUT_NAV_ASSETS . topLanguage } alt = "" / >
< / button >
< / Tooltip >
< / Dropdown >
< / div >
{ /* 联系客服:顶栏定位浮层展示二维码 */ }
< div className = "layoutShell__topbarTool" ref = { serviceToolRef } >
< button
type = "button"
className = { [ "layoutShell__topbarIconBtn" , topbarServiceQrOpen ? "layoutShell__topbarIconBtn_active" : "" ] . filter ( Boolean ) . join ( " " ) }
aria - label = { t ( "layout.topbar.contactServiceAria" ) }
aria - expanded = { topbarServiceQrOpen }
onClick = { ( e ) = > {
e . stopPropagation ( ) ;
if ( ! serviceQrCode ) return ;
setTopbarServiceQrOpen ( ( v ) = > ! v ) ;
setLanguageMenuOpen ( false ) ;
setUserMenuOpen ( false ) ;
setFooterPopoverOpen ( false ) ;
} }
>
< img src = { LAYOUT_NAV_ASSETS . topService } alt = "" / >
< / button >
{ topbarServiceQrOpen ? (
< div className = "layoutShell__topbarServiceQrFloat" role = "dialog" aria - label = { t ( "layout.topbar.contactServiceAria" ) } onClick = { ( e ) = > e . stopPropagation ( ) } >
< img className = "layoutShell__topbarServiceQrImage" src = { serviceQrCode } alt = { t ( "contactService.qrAlt" ) } / >
< p className = "layoutShell__topbarServiceQrTip" > { t ( "layout.topbar.scan_contact_service" ) } < / p >
< / div >
) : null }
< / div >
< / div >
{ /* 用户积分 */ }
< div className = "layoutShell__userPoints" onClick = { ( ) = > openPointsDetailDialog ( ) } >
< img className = "layoutShell__userPointsIcon" src = { new URL ( "@/assets/images/ic_stars_active.png" , import . meta . url ) . href } / >
< div className = "layoutShell__userPointsValue" > { String ( user ? . allCoins ? ? 0 ) } < / div >
< div className = "layoutShell__userPointsLabel" > { t ( "layout.nav.points" ) } < / div >
< / div >
@ -660,82 +679,40 @@ export function Layout({ children }: LayoutProps) {
) : null }
< / div >
) : (
< button type = "button" className = "layoutShell__loginBtn " aria - label = { t ( "auth.login_or_register" ) } onClick = { ( ) = > void openLoginDialog ( ) } >
{ t ( "auth.login_or_register" ) }
< button type = "button" className = "layoutShell__avatar layoutShell__avatar_guest " aria - label = { t ( "auth.login_or_register" ) } onClick = { ( ) = > void openLoginDialog ( ) } >
< img className = "layoutShell__avatarBg" src = { new URL ( "@/assets/images/avatar.png" , import . meta . url ) . href } alt = "" / >
< / button >
) }
< / div >
< / header >
{ aliceLiveClassOpen
? createPortal (
< AliceLiveClassDialogOverlay onClose = { ( ) = > setAliceLiveClassOpen ( false ) } / > ,
document . body ,
)
: null }
{ appMacDownloadOpen
? createPortal (
< AppMacDownloadDialogOverlay
macIntelUrl = { macIntelDownloadUrl }
macArmUrl = { macArmDownloadUrl }
windowsUrl = { windowsDownloadUrl }
onClose = { ( ) = > setAppMacDownloadOpen ( false ) }
/ > ,
document . body ,
)
: null }
< div className = "layoutShell__body" >
{ /* 左侧栏菜单 */ }
< aside className = { [ "layoutShell__sidebar" , footerPopoverOpen ? "layoutShell__sidebar_footerPopoverOpen" : "" ] . filter ( Boolean ) . join ( " " ) } aria - label = { t ( "layout.aria.sidebar" ) } onClick = { dismissMainDetailModals } >
< button type = "button" className = "layoutShell__brand" aria - label = { t ( "layout.brand.aria" ) } onClick = { ( ) = > navigate ( ROUTES . INDEX ) } >
< img className = "layoutShell__brandLogo" src = { LAYOUT_NAV_ASSETS . logo } alt = "" / >
< / button >
< nav className = "layoutShell__sideNav" aria - label = { t ( "layout.aria.sideNav" ) } >
{ LAYOUT_SIDE_NAV_ITEMS . map ( ( item ) = > {
const isActive = pathname === item . to || ( item . to !== "/" && pathname . startsWith ( ` ${ item . to } / ` ) ) ;
const className = [ "layoutShell__sideItem" , isActive ? "layoutShell__sideItem_active" : "" ] . filter ( Boolean ) . join ( " " ) ;
return (
< Link key = { item . id } to = { item . to } className = { className } title = { effectiveSidebarCollapsed ? t ( item . labelKey ) : undefined } onClick = { ( evt ) = > handleLayoutNavClick ( evt , item . to ) } >
< img className = "layoutShell__sideIcon" src = { item . iconSrc } / >
< LayoutSideNavIcon item = { item } / >
< span className = "layoutShell__sideText" > { t ( item . labelKey ) } < / span >
{ item . badge ? < span className = "layoutShell__sideBadge" > { item . badge } < / span > : null }
{ item . badgeKey ? < span className = "layoutShell__sideBadge" > { t ( item . badgeKey ) } < / span > : null }
< / Link >
) ;
} ) }
< / nav >
< div className = "layoutShell__aliceLiveClassTool" >
< button
type = "button"
className = "layoutShell__aliceLiveClass"
aria - label = { t ( "layout.sidebar.aliceLiveClass" ) }
onClick = { ( ) = > {
setAliceLiveClassOpen ( true ) ;
} }
>
< img className = "layoutShell__aliceLiveClassIcon" src = { new URL ( "@/assets/images/layout/alice-live-class/meetup-avatar.png" , import . meta . url ) . href } alt = "" / >
< span className = "layoutShell__aliceLiveClassText" > { t ( "layout.sidebar.aliceLiveClass" ) } < / span >
< img className = "layoutShell__aliceLiveClassArrow" src = { new URL ( "@/assets/images/layout/alice-live-class/meetup-arrow.png" , import . meta . url ) . href } alt = "" / >
< / button >
< / div >
< div className = "layoutShell__footer" ref = { footerRef } >
< div className = "layoutShell__appDownload" >
< button
type = "button"
className = "layoutShell__appDownloadTitle"
aria - label = { t ( "layout.sidebar.appDownloadTitle" ) }
onClick = { ( ) = > setAppMacDownloadOpen ( true ) }
>
< img className = "layoutShell__appDownloadTitleIcon" src = { LAYOUT_APP_DOWNLOAD_TITLE_ICON } alt = "" / >
< span className = "layoutShell__appDownloadTitleText" > { t ( "layout.sidebar.appDownloadTitle" ) } < / span >
< / button >
< / div >
< div className = "layoutShell__footer" >
< nav className = "layoutShell__footerNav" aria - label = { t ( "layout.footer.navAria" ) } >
{ LAYOUT_FOOTER_NAV_ITEMS . map ( ( item ) = > {
const isActive = pathname === item . to || ( item . to !== "/" && pathname . startsWith ( ` ${ item . to } / ` ) ) ;
const className = [ "layoutShell__sideItem" , isActive ? "layoutShell__sideItem_active" : "" ] . filter ( Boolean ) . join ( " " ) ;
return (
< Link key = { item . id } to = { item . to } className = { className } title = { effectiveSidebarCollapsed ? t ( item . labelKey ) : undefined } onClick = { ( evt ) = > handleLayoutNavClick ( evt , item . to ) } >
< img className = "layoutShell__sideIcon" src = { item . iconSrc } / >
< LayoutSideNavIcon item = { item } / >
< span className = "layoutShell__sideText" > { t ( item . labelKey ) } < / span >
< / Link >
) ;
@ -756,7 +733,7 @@ export function Layout({ children }: LayoutProps) {
aria - expanded = { footerPopoverOpen }
aria - haspopup = "dialog"
>
< img src = { new URL ( "@/assets/images/ic_menu.png" , import . meta . url ) . href } / >
< img src = { LAYOUT_NAV_ASSETS . more } alt = "" / >
< / button >
{ footerPopoverOpen ? (
< div className = "layoutShell__footerPopoverPanel" role = "dialog" aria - label = { t ( "layout.footerPopover.menuAria" ) } onClick = { ( e ) = > e . stopPropagation ( ) } >