88import { XMarkIcon } from '@heroicons/react/24/outline' ;
99import { AlgoliaSearch } from '@nx/nx-dev/feature-search' ;
1010import { Menu , MenuItem , MenuSection } from '@nx/nx-dev/models-menu' ;
11+ import { iconsMap } from '@nx/nx-dev/ui-references' ;
1112import cx from 'classnames' ;
1213import Link from 'next/link' ;
1314import { useRouter } from 'next/router' ;
@@ -31,16 +32,30 @@ export function Sidebar({ menu }: SidebarProps): JSX.Element {
3132 data-testid = "navigation"
3233 className = "pb-4 text-base lg:text-sm"
3334 >
34- { menu . sections . map ( ( section , index ) => (
35- < SidebarSection key = { section . id + '-' + index } section = { section } />
36- ) ) }
35+ { menu . sections . map ( ( section , index ) => {
36+ return (
37+ < SidebarSection
38+ key = { section . id + '-' + index }
39+ section = { section }
40+ isInTechnologiesPath = { false }
41+ />
42+ ) ;
43+ } ) }
3744 </ nav >
3845 </ div >
3946 ) ;
4047}
4148
42- function SidebarSection ( { section } : { section : MenuSection } ) : JSX . Element {
49+ function SidebarSection ( {
50+ section,
51+ isInTechnologiesPath,
52+ } : {
53+ section : MenuSection ;
54+ isInTechnologiesPath : boolean ;
55+ } ) : JSX . Element {
4356 const router = useRouter ( ) ;
57+
58+ // Get all items with refs
4459 const itemList = section . itemList . map ( ( i ) => ( {
4560 ...i ,
4661 ref : createRef < HTMLDivElement > ( ) ,
@@ -56,6 +71,7 @@ function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
5671 } , 0 ) ;
5772 } ) ;
5873 } , [ currentItem ] ) ;
74+
5975 return (
6076 < >
6177 { section . hideSectionHeader ? null : (
@@ -70,11 +86,21 @@ function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
7086 < li className = "mt-2" >
7187 { itemList
7288 . filter ( ( i ) => ! ! i . children ?. length )
73- . map ( ( item , index ) => (
74- < div key = { item . id + '-' + index } ref = { item . ref } >
75- < SidebarSectionItems key = { item . id + '-' + index } item = { item } />
76- </ div >
77- ) ) }
89+ . map ( ( item , index ) => {
90+ // Check if this specific item is the Technologies item
91+ const isTechnologiesItem = item . id === 'technologies' ;
92+
93+ return (
94+ < div key = { item . id + '-' + index } ref = { item . ref } >
95+ < SidebarSectionItems
96+ key = { item . id + '-' + index }
97+ item = { item }
98+ isNested = { false }
99+ isInTechnologiesPath = { isTechnologiesItem }
100+ />
101+ </ div >
102+ ) ;
103+ } ) }
78104 </ li >
79105 </ ul >
80106 </ >
@@ -84,51 +110,88 @@ function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
84110function SidebarSectionItems ( {
85111 item,
86112 isNested = false ,
113+ isInTechnologiesPath = false ,
87114} : {
88115 item : MenuItem ;
89116 isNested ?: boolean ;
117+ isInTechnologiesPath ?: boolean ;
90118} ) : JSX . Element {
91119 const router = useRouter ( ) ;
92120 const [ collapsed , setCollapsed ] = useState ( ! item . disableCollapsible ) ;
93121
122+ // Check if this is the Technologies main item
123+ const isTechnologiesItem = item . id === 'technologies' ;
124+
125+ // If this is direct child of the Technologies item, show an icon
126+ const isDirectTechnologyChild = isInTechnologiesPath ;
127+
128+ // Get the icon key for this technology
129+ let iconKey = null ;
130+ if ( isDirectTechnologyChild ) {
131+ iconKey = getIconKeyForTechnology ( item . id ) ;
132+ }
133+
94134 const handleCollapseToggle = useCallback ( ( ) => {
95135 if ( ! item . disableCollapsible ) {
96136 setCollapsed ( ! collapsed ) ;
97137 }
98138 } , [ collapsed , setCollapsed , item ] ) ;
139+
99140 function withoutAnchors ( linkText : string ) : string {
100141 return linkText ?. includes ( '#' )
101142 ? linkText . substring ( 0 , linkText . indexOf ( '#' ) )
102143 : linkText ;
103144 }
104145
146+ // Update the children mapping to safely handle cases where item.children might be undefined
147+ const children = item . children || [ ] ;
148+
105149 return (
106150 < >
107151 < h5
108152 data-testid = { `section-h5:${ item . id } ` }
109153 className = { cx (
110- 'flex py-2' ,
154+ 'group flex items-center py-2' ,
155+ isDirectTechnologyChild
156+ ? '-ml-1 rounded-md px-1 hover:bg-slate-50 dark:hover:bg-slate-800/60'
157+ : '' ,
111158 'text-sm font-semibold text-slate-800 lg:text-sm dark:text-slate-200' ,
112159 item . disableCollapsible ? 'cursor-text' : 'cursor-pointer'
113160 ) }
114161 onClick = { handleCollapseToggle }
115162 >
116- { item . disableCollapsible ? (
117- < Link
118- href = { item . path as string }
119- className = "hover:underline"
120- prefetch = { false }
121- >
122- { item . name }
123- </ Link >
124- ) : (
125- < >
126- { item . name } < CollapsibleIcon isCollapsed = { collapsed } />
127- </ >
163+ { isDirectTechnologyChild && (
164+ < div className = "mr-2 flex h-6 w-6 flex-shrink-0 items-center justify-center" >
165+ < img
166+ className = "h-5 w-5 object-cover opacity-100 dark:invert"
167+ loading = "lazy"
168+ src = { iconsMap [ iconKey || 'nx' ] }
169+ alt = { item . name + ' illustration' }
170+ aria-hidden = "true"
171+ />
172+ </ div >
128173 ) }
174+ < div className = { cx ( 'flex flex-grow items-center justify-between' ) } >
175+ { item . disableCollapsible ? (
176+ < Link
177+ href = { item . path as string }
178+ className = "hover:underline"
179+ prefetch = { false }
180+ >
181+ { item . name }
182+ </ Link >
183+ ) : (
184+ < >
185+ < span className = { isDirectTechnologyChild ? 'flex-grow' : '' } >
186+ { item . name }
187+ </ span >
188+ < CollapsibleIcon isCollapsed = { collapsed } />
189+ </ >
190+ ) }
191+ </ div >
129192 </ h5 >
130193 < ul className = { cx ( 'mb-6' , collapsed ? 'hidden' : '' ) } >
131- { ( item . children as MenuItem [ ] ) . map ( ( subItem , index ) => {
194+ { children . map ( ( subItem , index ) => {
132195 const isActiveLink = withoutAnchors ( router . asPath ) . startsWith (
133196 subItem . path
134197 ) ;
@@ -150,8 +213,12 @@ function SidebarSectionItems({
150213 : 'border-l-transparent hover:border-blue-300 dark:border-l-transparent dark:hover:border-sky-400' )
151214 ) }
152215 >
153- { subItem . children . length ? (
154- < SidebarSectionItems item = { subItem } isNested = { true } />
216+ { ( subItem . children || [ ] ) . length ? (
217+ < SidebarSectionItems
218+ item = { subItem }
219+ isNested = { true }
220+ isInTechnologiesPath = { isTechnologiesItem }
221+ />
155222 ) : (
156223 < Link
157224 href = { subItem . path }
@@ -369,6 +436,7 @@ export function SidebarMobile({
369436 < SidebarSection
370437 key = { section . id + '-' + index }
371438 section = { section }
439+ isInTechnologiesPath = { false }
372440 />
373441 ) ) }
374442 </ nav >
@@ -381,3 +449,68 @@ export function SidebarMobile({
381449 </ Transition >
382450 ) ;
383451}
452+
453+ function getIconKeyForTechnology ( idOrName : string ) : string {
454+ // Normalize the input to lowercase for more reliable matching
455+ const normalized = idOrName . toLowerCase ( ) ;
456+
457+ // Technology icon mapping
458+ const technologyIconMap : Record < string , string > = {
459+ // JavaScript/TypeScript
460+ typescript : 'js' ,
461+ js : 'js' ,
462+
463+ // Angular
464+ angular : 'angular' ,
465+ 'angular-rspack' : 'angular-rspack' ,
466+ 'angular-rsbuild' : 'angular-rsbuild' ,
467+
468+ // React
469+ react : 'react' ,
470+ 'react-native' : 'react-native' ,
471+ remix : 'remix' ,
472+ next : 'next' ,
473+ expo : 'expo' ,
474+
475+ // Vue
476+ vue : 'vue' ,
477+ nuxt : 'nuxt' ,
478+
479+ // Node
480+ nodejs : 'node' ,
481+ 'node.js' : 'node' ,
482+ node : 'node' ,
483+
484+ // Java
485+ java : 'gradle' ,
486+ gradle : 'gradle' ,
487+
488+ // Module Federation
489+ 'module-federation' : 'module-federation' ,
490+
491+ // Linting
492+ eslint : 'eslint' ,
493+ 'eslint-technology' : 'eslint' ,
494+
495+ // Testing
496+ 'testing-tools' : 'jest' ,
497+ cypress : 'cypress' ,
498+ jest : 'jest' ,
499+ playwright : 'playwright' ,
500+ storybook : 'storybook' ,
501+ detox : 'detox' ,
502+
503+ // Build tools
504+ 'build-tools' : 'webpack' ,
505+ 'build tools' : 'webpack' ,
506+ webpack : 'webpack' ,
507+ vite : 'vite' ,
508+ rollup : 'rollup' ,
509+ esbuild : 'esbuild' ,
510+ rspack : 'rspack' ,
511+ rsbuild : 'rsbuild' ,
512+ } ;
513+
514+ // Return the mapped icon or 'nx' as default
515+ return technologyIconMap [ normalized ] || 'nx' ;
516+ }
0 commit comments