8
8
import { XMarkIcon } from '@heroicons/react/24/outline' ;
9
9
import { AlgoliaSearch } from '@nx/nx-dev/feature-search' ;
10
10
import { Menu , MenuItem , MenuSection } from '@nx/nx-dev/models-menu' ;
11
+ import { iconsMap } from '@nx/nx-dev/ui-references' ;
11
12
import cx from 'classnames' ;
12
13
import Link from 'next/link' ;
13
14
import { useRouter } from 'next/router' ;
@@ -31,16 +32,30 @@ export function Sidebar({ menu }: SidebarProps): JSX.Element {
31
32
data-testid = "navigation"
32
33
className = "pb-4 text-base lg:text-sm"
33
34
>
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
+ } ) }
37
44
</ nav >
38
45
</ div >
39
46
) ;
40
47
}
41
48
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 {
43
56
const router = useRouter ( ) ;
57
+
58
+ // Get all items with refs
44
59
const itemList = section . itemList . map ( ( i ) => ( {
45
60
...i ,
46
61
ref : createRef < HTMLDivElement > ( ) ,
@@ -56,6 +71,7 @@ function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
56
71
} , 0 ) ;
57
72
} ) ;
58
73
} , [ currentItem ] ) ;
74
+
59
75
return (
60
76
< >
61
77
{ section . hideSectionHeader ? null : (
@@ -70,11 +86,21 @@ function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
70
86
< li className = "mt-2" >
71
87
{ itemList
72
88
. 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
+ } ) }
78
104
</ li >
79
105
</ ul >
80
106
</ >
@@ -84,51 +110,88 @@ function SidebarSection({ section }: { section: MenuSection }): JSX.Element {
84
110
function SidebarSectionItems ( {
85
111
item,
86
112
isNested = false ,
113
+ isInTechnologiesPath = false ,
87
114
} : {
88
115
item : MenuItem ;
89
116
isNested ?: boolean ;
117
+ isInTechnologiesPath ?: boolean ;
90
118
} ) : JSX . Element {
91
119
const router = useRouter ( ) ;
92
120
const [ collapsed , setCollapsed ] = useState ( ! item . disableCollapsible ) ;
93
121
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
+
94
134
const handleCollapseToggle = useCallback ( ( ) => {
95
135
if ( ! item . disableCollapsible ) {
96
136
setCollapsed ( ! collapsed ) ;
97
137
}
98
138
} , [ collapsed , setCollapsed , item ] ) ;
139
+
99
140
function withoutAnchors ( linkText : string ) : string {
100
141
return linkText ?. includes ( '#' )
101
142
? linkText . substring ( 0 , linkText . indexOf ( '#' ) )
102
143
: linkText ;
103
144
}
104
145
146
+ // Update the children mapping to safely handle cases where item.children might be undefined
147
+ const children = item . children || [ ] ;
148
+
105
149
return (
106
150
< >
107
151
< h5
108
152
data-testid = { `section-h5:${ item . id } ` }
109
153
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
+ : '' ,
111
158
'text-sm font-semibold text-slate-800 lg:text-sm dark:text-slate-200' ,
112
159
item . disableCollapsible ? 'cursor-text' : 'cursor-pointer'
113
160
) }
114
161
onClick = { handleCollapseToggle }
115
162
>
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 >
128
173
) }
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 >
129
192
</ h5 >
130
193
< ul className = { cx ( 'mb-6' , collapsed ? 'hidden' : '' ) } >
131
- { ( item . children as MenuItem [ ] ) . map ( ( subItem , index ) => {
194
+ { children . map ( ( subItem , index ) => {
132
195
const isActiveLink = withoutAnchors ( router . asPath ) . startsWith (
133
196
subItem . path
134
197
) ;
@@ -150,8 +213,12 @@ function SidebarSectionItems({
150
213
: 'border-l-transparent hover:border-blue-300 dark:border-l-transparent dark:hover:border-sky-400' )
151
214
) }
152
215
>
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
+ />
155
222
) : (
156
223
< Link
157
224
href = { subItem . path }
@@ -369,6 +436,7 @@ export function SidebarMobile({
369
436
< SidebarSection
370
437
key = { section . id + '-' + index }
371
438
section = { section }
439
+ isInTechnologiesPath = { false }
372
440
/>
373
441
) ) }
374
442
</ nav >
@@ -381,3 +449,68 @@ export function SidebarMobile({
381
449
</ Transition >
382
450
) ;
383
451
}
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