@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, type ComponentType } from 'react';
22import { useSelector , useDispatch } from 'react-redux' ;
33import { useTranslation } from 'react-i18next' ;
44import { motion , AnimatePresence } from 'motion/react' ;
5+ import { useTheme } from 'next-themes' ;
56import {
67 AlertCircle ,
78 BadgeCheck ,
@@ -27,6 +28,7 @@ import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
2728import { ScrollArea } from './ui/scroll-area' ;
2829import type { DistributionModeState } from '../../types/distribution-mode' ;
2930import type { DesktopVersionInfoPayload } from '../../types/version-info' ;
31+ import { isDesktopTheme } from '../lib/desktop-theme' ;
3032import {
3133 createLoadingSidebarAboutFetchState ,
3234 hasSidebarAboutEntries ,
@@ -38,48 +40,68 @@ import {
3840 type SidebarAboutModel ,
3941 type SidebarAboutSectionId ,
4042} from '../lib/about-sidebar' ;
43+ import { cn } from '../lib/utils' ;
44+
45+ type NavigationEmphasis = 'default' | 'sponsor' | 'turboengine' ;
46+
4147interface NavigationItem {
4248 id : ViewType | 'official-website' | 'cost-calculator' ;
4349 labelKey : string ;
4450 descriptionKey ?: string ;
4551 icon : ComponentType < { className ?: string } > ;
4652 url ?: string ;
53+ emphasis : NavigationEmphasis ;
4754}
4855
49- const navigationItems : NavigationItem [ ] = [
50- { id : 'system' , labelKey : 'sidebar.dashboard' , icon : Settings } ,
51- { id : 'version' , labelKey : 'sidebar.versionManagement' , icon : FileText } ,
52- { id : 'diagnostic' , labelKey : 'sidebar.diagnostic' , icon : Stethoscope } ,
53- { id : 'dependency-management' , labelKey : 'sidebar.dependencyManagement' , icon : PackageOpen } ,
54- { id : 'settings' , labelKey : 'sidebar.settings' , icon : Settings } ,
56+ const primaryNavigationItems : NavigationItem [ ] = [
57+ { id : 'system' , labelKey : 'sidebar.dashboard' , icon : Settings , emphasis : 'default' } ,
58+ { id : 'version' , labelKey : 'sidebar.versionManagement' , icon : FileText , emphasis : 'default' } ,
59+ { id : 'diagnostic' , labelKey : 'sidebar.diagnostic' , icon : Stethoscope , emphasis : 'default' } ,
60+ { id : 'dependency-management' , labelKey : 'sidebar.dependencyManagement' , icon : PackageOpen , emphasis : 'default' } ,
5561] ;
5662
63+ const settingsNavigationItem : NavigationItem = {
64+ id : 'settings' ,
65+ labelKey : 'sidebar.settings' ,
66+ icon : Settings ,
67+ emphasis : 'default' ,
68+ } ;
69+
5770const officialWebsiteItem : NavigationItem = {
5871 id : 'official-website' ,
5972 labelKey : 'navigation.officialWebsite' ,
6073 descriptionKey : 'navigation.officialWebsiteDesc' ,
6174 icon : GlobeIcon ,
6275 url : 'https://hagicode.com/' ,
76+ emphasis : 'default' ,
6377} ;
6478
6579const subscriptionNavigationItem : NavigationItem = {
6680 id : 'subscription' ,
6781 labelKey : 'sidebar.subscription' ,
6882 icon : BadgeCheck ,
83+ emphasis : 'sponsor' ,
6984} ;
7085
7186const turboEngineNavigationItem : NavigationItem = {
7287 id : 'turboengine' ,
7388 labelKey : 'sidebar.turboEngine' ,
7489 icon : Cpu ,
90+ emphasis : 'turboengine' ,
7591} ;
7692
93+ const featuredNavigationItems : NavigationItem [ ] = [
94+ subscriptionNavigationItem ,
95+ turboEngineNavigationItem ,
96+ ] ;
97+
7798const remainingExternalLinkItems : NavigationItem [ ] = [
7899 {
79100 id : 'cost-calculator' ,
80101 labelKey : 'navigation.costCalculator' ,
81102 url : 'https://cost.hagicode.com' ,
82103 icon : Calculator ,
104+ emphasis : 'default' ,
83105 } ,
84106] ;
85107
@@ -176,17 +198,61 @@ function AboutBrandLogo({ entry }: { entry: SidebarAboutEntry }) {
176198 ) ;
177199}
178200
201+ function getFeaturedNavigationPalette (
202+ emphasis : Exclude < NavigationEmphasis , 'default' > ,
203+ isDarkTheme : boolean ,
204+ ) {
205+ if ( emphasis === 'sponsor' ) {
206+ return {
207+ button : 'border-transparent bg-transparent text-foreground shadow-none hover:bg-transparent' ,
208+ activeButton : 'border-transparent bg-transparent shadow-none' ,
209+ iconWrap : 'border-transparent bg-transparent' ,
210+ iconClass : isDarkTheme
211+ ? 'text-[#f1cb71] drop-shadow-[0_0_14px_rgba(241,203,113,0.38)] transition-all duration-300 group-hover:scale-110 group-hover:text-[#ffe09a]'
212+ : 'text-[#6e4304] transition-all duration-300 group-hover:scale-110 group-hover:text-[#4d2d00]' ,
213+ activeIconClass : isDarkTheme ? 'text-[#ffe8b8]' : 'text-[#4d2d00]' ,
214+ labelClass : isDarkTheme
215+ ? 'bg-[linear-gradient(90deg,#f7d882,#fff1c8,#e0a93a)] bg-clip-text text-transparent'
216+ : 'bg-[linear-gradient(90deg,#a06b18_0%,#171717_48%,#171717_52%,#a06b18_100%)] bg-clip-text text-transparent' ,
217+ activeLabelClass : isDarkTheme
218+ ? 'bg-[linear-gradient(90deg,#ffe5a2,#fff6da,#f0bb52)] bg-clip-text text-transparent'
219+ : 'bg-[linear-gradient(90deg,#7d4f0d_0%,#050505_48%,#050505_52%,#7d4f0d_100%)] bg-clip-text text-transparent' ,
220+ focus : 'focus-visible:ring-[#f0cf80]/45' ,
221+ } ;
222+ }
223+
224+ return {
225+ button : 'border-transparent bg-transparent text-foreground shadow-none hover:bg-transparent' ,
226+ activeButton : 'border-transparent bg-transparent shadow-none' ,
227+ iconWrap : 'border-transparent bg-transparent' ,
228+ iconClass : isDarkTheme
229+ ? 'text-[#ffbe57] drop-shadow-[0_0_14px_rgba(255,190,87,0.38)] transition-all duration-300 group-hover:scale-110 group-hover:text-[#ffd48c]'
230+ : 'text-[#8a4300] transition-all duration-300 group-hover:scale-110 group-hover:text-[#682700]' ,
231+ activeIconClass : isDarkTheme ? 'text-[#ffe0a2]' : 'text-[#682700]' ,
232+ labelClass : isDarkTheme
233+ ? 'bg-[linear-gradient(90deg,#ffca6e,#ffe6a8,#e89b2f)] bg-clip-text text-transparent'
234+ : 'bg-[linear-gradient(90deg,#b46112_0%,#171717_48%,#171717_52%,#b46112_100%)] bg-clip-text text-transparent' ,
235+ activeLabelClass : isDarkTheme
236+ ? 'bg-[linear-gradient(90deg,#ffe0a2,#fff0c8,#f5b246)] bg-clip-text text-transparent'
237+ : 'bg-[linear-gradient(90deg,#8b4308_0%,#050505_48%,#050505_52%,#8b4308_100%)] bg-clip-text text-transparent' ,
238+ focus : 'focus-visible:ring-[#ffc86b]/45' ,
239+ } ;
240+ }
241+
179242export default function SidebarNavigation ( { distributionState } : SidebarNavigationProps ) {
180243 const { t, i18n } = useTranslation ( 'common' ) ;
244+ const { resolvedTheme } = useTheme ( ) ;
181245 const dispatch = useDispatch ( ) ;
182246 const currentView = useSelector ( ( state : RootState ) => state . view . currentView ) ;
247+ const activeTheme = isDesktopTheme ( resolvedTheme ) ? resolvedTheme : 'light' ;
248+ const isDarkTheme = activeTheme === 'dark' ;
183249 const isFusionMode = distributionState . fusionMode ;
184250 const visibleNavigationItems = useMemo ( ( ) => {
185251 const baseItems = isFusionMode
186- ? navigationItems . filter ( ( item ) => item . id !== 'version' )
187- : navigationItems ;
252+ ? primaryNavigationItems . filter ( ( item ) => item . id !== 'version' )
253+ : primaryNavigationItems ;
188254
189- return [ ...baseItems , subscriptionNavigationItem , turboEngineNavigationItem ] ;
255+ return [ ...baseItems , ... featuredNavigationItems , settingsNavigationItem ] ;
190256 } , [ isFusionMode ] ) ;
191257 const aboutLocale = useMemo (
192258 ( ) => normalizeSidebarAboutLocale ( i18n . resolvedLanguage ?? i18n . language ) ,
@@ -489,6 +555,10 @@ export default function SidebarNavigation({ distributionState }: SidebarNavigati
489555 { visibleNavigationItems . map ( ( item , index ) => {
490556 const Icon = item . icon ;
491557 const isActive = isNavActive ( item ) ;
558+ const featuredPalette = item . emphasis === 'default'
559+ ? null
560+ : getFeaturedNavigationPalette ( item . emphasis , isDarkTheme ) ;
561+ const isFeatured = featuredPalette !== null ;
492562
493563 return (
494564 < motion . button
@@ -497,18 +567,22 @@ export default function SidebarNavigation({ distributionState }: SidebarNavigati
497567 initial = { { opacity : 0 , x : - 20 } }
498568 animate = { { opacity : 1 , x : 0 } }
499569 transition = { { delay : index * 0.05 , duration : 0.3 } }
500- whileHover = { { x : 0 } }
570+ whileHover = { isFeatured ? { x : 0 , y : - 2 } : { x : 0 } }
501571 whileTap = { { scale : 0.98 } }
502- className = { `
503- relative flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left
504- overflow-hidden group
505- ${ isActive
506- ? 'border border-border/80 bg-accent text-foreground shadow-sm'
507- : 'border border-transparent text-muted-foreground hover:border-border/70 hover:bg-muted/55 hover:text-foreground'
508- }
509- ` }
572+ className = { cn (
573+ 'group relative flex w-full items-center gap-3 overflow-hidden rounded-xl border px-3 py-2.5 text-left transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2' ,
574+ isFeatured
575+ ? cn (
576+ featuredPalette ?. button ,
577+ featuredPalette ?. focus ,
578+ isActive && featuredPalette ?. activeButton ,
579+ )
580+ : isActive
581+ ? 'border-border/80 bg-accent text-foreground shadow-sm focus-visible:ring-primary/35'
582+ : 'border-transparent text-muted-foreground hover:border-border/70 hover:bg-muted/55 hover:text-foreground focus-visible:ring-primary/25' ,
583+ ) }
510584 >
511- { ! isActive && (
585+ { ! isFeatured && ! isActive && (
512586 < motion . div
513587 initial = { { opacity : 0 } }
514588 whileHover = { { opacity : 1 } }
@@ -517,7 +591,28 @@ export default function SidebarNavigation({ distributionState }: SidebarNavigati
517591 />
518592 ) }
519593
520- < Icon className = { `relative z-10 h-5 w-5 flex-shrink-0 ${ isActive ? 'text-primary' : '' } ` } />
594+ < div
595+ className = { cn (
596+ 'relative z-10 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg border transition-all duration-300' ,
597+ isFeatured
598+ ? featuredPalette ?. iconWrap
599+ : isActive
600+ ? 'border-primary/25 bg-primary/10 text-primary'
601+ : 'border-border/60 bg-background/75 text-muted-foreground group-hover:border-border/80 group-hover:bg-background' ,
602+ ) }
603+ >
604+ < Icon
605+ className = { cn (
606+ 'h-4 w-4' ,
607+ isFeatured
608+ ? cn (
609+ featuredPalette ?. iconClass ,
610+ isActive && featuredPalette ?. activeIconClass ,
611+ )
612+ : '' ,
613+ ) }
614+ />
615+ </ div >
521616
522617 < AnimatePresence mode = "wait" >
523618 { ! collapsed && (
@@ -526,9 +621,23 @@ export default function SidebarNavigation({ distributionState }: SidebarNavigati
526621 animate = { { opacity : 1 , width : 'auto' } }
527622 exit = { { opacity : 0 , width : 0 } }
528623 transition = { { duration : 0.2 } }
529- className = "relative z-10 flex min-w-0 items-center gap-2"
624+ className = { cn (
625+ 'relative z-10 flex min-w-0 items-center gap-2' ,
626+ ! isFeatured && 'pr-5' ,
627+ ) }
530628 >
531- < span className = "truncate font-medium text-sm whitespace-nowrap" >
629+ < span
630+ className = { cn (
631+ 'truncate whitespace-nowrap text-sm' ,
632+ isFeatured
633+ ? cn (
634+ 'font-semibold tracking-[0.01em]' ,
635+ featuredPalette ?. labelClass ,
636+ isActive && featuredPalette ?. activeLabelClass ,
637+ )
638+ : 'font-medium' ,
639+ ) }
640+ >
532641 { t ( item . labelKey ) }
533642 </ span >
534643 </ motion . div >
@@ -554,7 +663,12 @@ export default function SidebarNavigation({ distributionState }: SidebarNavigati
554663 transition = { { delay : 0.2 , duration : 0.3 } }
555664 className = "min-h-0 flex-1 overflow-hidden"
556665 >
557- < ScrollArea className = "h-full" type = "always" >
666+ < ScrollArea
667+ className = "h-full"
668+ type = "scroll"
669+ scrollBarClassName = "w-1.5 p-0.5"
670+ scrollThumbClassName = "bg-border/65"
671+ >
558672 < div className = "space-y-2 pb-6" >
559673 < motion . button
560674 type = "button"
0 commit comments