@@ -5,6 +5,7 @@ export interface NavLink {
55 label : string ;
66 href : string ;
77 isActive ?: boolean ;
8+ children ?: NavLink [ ] ;
89}
910
1011interface NavigationProps {
@@ -14,6 +15,7 @@ interface NavigationProps {
1415
1516const Navigation : Component < NavigationProps > = ( props ) => {
1617 const [ isOpen , setIsOpen ] = createSignal ( false ) ;
18+ const [ openDropdown , setOpenDropdown ] = createSignal < string | null > ( null ) ;
1719
1820 const isActive = ( href : string ) : boolean => {
1921 if ( href === "/" ) {
@@ -22,6 +24,11 @@ const Navigation: Component<NavigationProps> = (props) => {
2224 return props . currentPath . startsWith ( href ) ;
2325 } ;
2426
27+ const hasActiveChild = ( link : NavLink ) : boolean => {
28+ if ( ! link . children ) return false ;
29+ return link . children . some ( ( child ) => isActive ( child . href ) ) ;
30+ } ;
31+
2532 onMount ( ( ) => {
2633 if ( typeof window === "undefined" ) return ;
2734
@@ -60,15 +67,76 @@ const Navigation: Component<NavigationProps> = (props) => {
6067 < ul class = "hidden md:flex items-center gap-1 rounded-xl border border-[var(--border-strong)] bg-[color-mix(in_srgb,var(--surface-panel)_84%,transparent)] px-1.5 py-1 shadow-[0_10px_24px_rgba(15,23,42,0.08)]" >
6168 < For each = { props . links } >
6269 { ( link ) => (
63- < li >
64- < a
65- class = { `${ baseClass } ${ isActive ( link . href ) ? activeClass : "" } ` }
66- href = { link . href }
67- data-track = { `nav_desktop_${ toTrackToken ( link . label ) } ` }
68- aria-current = { isActive ( link . href ) ? "page" : undefined }
70+ < li class = "relative" >
71+ < Show
72+ when = { link . children && link . children . length > 0 }
73+ fallback = {
74+ < a
75+ class = { `${ baseClass } ${ isActive ( link . href ) ? activeClass : "" } ` }
76+ href = { link . href }
77+ data-track = { `nav_desktop_${ toTrackToken ( link . label ) } ` }
78+ aria-current = { isActive ( link . href ) ? "page" : undefined }
79+ >
80+ { link . label }
81+ </ a >
82+ }
6983 >
70- { link . label }
71- </ a >
84+ < button
85+ type = "button"
86+ class = { `${ baseClass } ${ isActive ( link . href ) || hasActiveChild ( link ) ? activeClass : "" } gap-1` }
87+ onClick = { ( ) =>
88+ setOpenDropdown (
89+ openDropdown ( ) === link . label ? null : link . label ,
90+ )
91+ }
92+ onMouseEnter = { ( ) => setOpenDropdown ( link . label ) }
93+ aria-expanded = { openDropdown ( ) === link . label }
94+ aria-haspopup = "true"
95+ >
96+ { link . label }
97+ < svg
98+ class = { `h-3.5 w-3.5 transition-transform ${ openDropdown ( ) === link . label ? "rotate-180" : "" } ` }
99+ viewBox = "0 0 24 24"
100+ fill = "none"
101+ stroke = "currentColor"
102+ stroke-width = "2.5"
103+ stroke-linecap = "round"
104+ stroke-linejoin = "round"
105+ aria-hidden = "true"
106+ >
107+ < polyline points = "6 9 12 15 18 9" />
108+ </ svg >
109+ </ button >
110+ < Show when = { openDropdown ( ) === link . label } >
111+ < div
112+ class = "absolute left-0 top-full z-50 mt-1 min-w-[220px] rounded-xl border border-[var(--border-strong)] bg-[var(--surface-panel)] p-1.5 shadow-lg"
113+ onMouseLeave = { ( ) => setOpenDropdown ( null ) }
114+ role = "menu"
115+ >
116+ < For each = { link . children } >
117+ { ( child ) => (
118+ < a
119+ class = "block rounded-lg px-4 py-2.5 text-sm font-medium text-[var(--text-muted)] transition-colors hover:bg-[var(--surface-elevated)] hover:text-[var(--text-strong)]"
120+ href = { child . href }
121+ data-track = { `nav_desktop_${ toTrackToken ( link . label ) } _${ toTrackToken ( child . label ) } ` }
122+ onClick = { ( ) => setOpenDropdown ( null ) }
123+ >
124+ { child . label }
125+ </ a >
126+ ) }
127+ </ For >
128+ < div class = "my-1.5 border-t border-[var(--border-strong)]" />
129+ < a
130+ class = "block rounded-lg px-4 py-2.5 text-sm font-semibold text-[var(--accent-strong)] transition-colors hover:bg-[var(--surface-elevated)]"
131+ href = { link . href }
132+ data-track = { `nav_desktop_${ toTrackToken ( link . label ) } _all` }
133+ onClick = { ( ) => setOpenDropdown ( null ) }
134+ >
135+ View All Services →
136+ </ a >
137+ </ div >
138+ </ Show >
139+ </ Show >
72140 </ li >
73141 ) }
74142 </ For >
@@ -162,19 +230,76 @@ const Navigation: Component<NavigationProps> = (props) => {
162230 </ svg >
163231 </ button >
164232 </ div >
165- < ul class = "py-2" >
233+ < ul class = "py-2 overflow-y-auto max-h-[calc(100vh-180px)] " >
166234 < For each = { props . links } >
167235 { ( link ) => (
168236 < li >
169- < a
170- class = { `${ mobileBaseClass } ${ isActive ( link . href ) ? mobileActiveClass : "" } ` }
171- href = { link . href }
172- data-track = { `nav_mobile_${ toTrackToken ( link . label ) } ` }
173- onClick = { ( ) => setIsOpen ( false ) }
174- aria-current = { isActive ( link . href ) ? "page" : undefined }
237+ < Show
238+ when = { link . children && link . children . length > 0 }
239+ fallback = {
240+ < a
241+ class = { `${ mobileBaseClass } ${ isActive ( link . href ) ? mobileActiveClass : "" } ` }
242+ href = { link . href }
243+ data-track = { `nav_mobile_${ toTrackToken ( link . label ) } ` }
244+ onClick = { ( ) => setIsOpen ( false ) }
245+ aria-current = { isActive ( link . href ) ? "page" : undefined }
246+ >
247+ { link . label }
248+ </ a >
249+ }
175250 >
176- { link . label }
177- </ a >
251+ < button
252+ type = "button"
253+ class = { `${ mobileBaseClass } flex items-center justify-between ${ isActive ( link . href ) || hasActiveChild ( link ) ? mobileActiveClass : "" } ` }
254+ onClick = { ( ) =>
255+ setOpenDropdown (
256+ openDropdown ( ) === link . label ? null : link . label ,
257+ )
258+ }
259+ >
260+ { link . label }
261+ < svg
262+ class = { `h-4 w-4 transition-transform ${ openDropdown ( ) === link . label ? "rotate-180" : "" } ` }
263+ viewBox = "0 0 24 24"
264+ fill = "none"
265+ stroke = "currentColor"
266+ stroke-width = "2"
267+ stroke-linecap = "round"
268+ stroke-linejoin = "round"
269+ aria-hidden = "true"
270+ >
271+ < polyline points = "6 9 12 15 18 9" />
272+ </ svg >
273+ </ button >
274+ < Show when = { openDropdown ( ) === link . label } >
275+ < ul class = "bg-[var(--surface-panel)] border-t border-[var(--border-strong)]" >
276+ < For each = { link . children } >
277+ { ( child ) => (
278+ < li >
279+ < a
280+ class = "block px-8 py-2.5 text-sm text-[var(--text-muted)] hover:bg-[var(--surface-elevated)] hover:text-[var(--text-strong)]"
281+ href = { child . href }
282+ data-track = { `nav_mobile_${ toTrackToken ( link . label ) } _${ toTrackToken ( child . label ) } ` }
283+ onClick = { ( ) => setIsOpen ( false ) }
284+ >
285+ { child . label }
286+ </ a >
287+ </ li >
288+ ) }
289+ </ For >
290+ < li >
291+ < a
292+ class = "block px-8 py-2.5 text-sm font-semibold text-[var(--accent-strong)] hover:bg-[var(--surface-elevated)]"
293+ href = { link . href }
294+ data-track = { `nav_mobile_${ toTrackToken ( link . label ) } _all` }
295+ onClick = { ( ) => setIsOpen ( false ) }
296+ >
297+ View All Services →
298+ </ a >
299+ </ li >
300+ </ ul >
301+ </ Show >
302+ </ Show >
178303 </ li >
179304 ) }
180305 </ For >
0 commit comments