1- <!-- src/components/SiteHeader.vue -->
21<template >
32 <header class =" hdr" >
43 <div class =" wrap" >
98 <!-- Desktop nav -->
109 <nav class =" nav" aria-label =" Primary" >
1110 <template v-for =" item in navItems " :key =" itemKey (item )" >
12- <!-- Dropdown -->
11+ <!-- Dropdown: only when item has children (About) -->
1312 <div
14- v-if =" item.kind === 'dropdown' "
13+ v-if =" Array.isArray( item.items) && item.items.length "
1514 class =" dd"
1615 @mouseenter =" openDropdown(item.label)"
1716 @mouseleave =" closeDropdown(item.label)"
1817 >
1918 <button
2019 type =" button"
2120 class =" link dd-btn"
22- :class =" { 'is-open': isOpen(item.label) }"
21+ :class =" {
22+ 'is-open': isOpen(item.label),
23+ 'is-active': isActive(item) || item.items.some(isChildActive),
24+ }"
2325 @click =" toggleDropdown(item.label)"
2426 aria-haspopup =" menu"
2527 :aria-expanded =" String(isOpen(item.label))"
2931 </button >
3032
3133 <div class =" dd-menu" :class =" { open: isOpen(item.label) }" role =" menu" >
32- <!-- Dropdown children: support href (docs) OR to (vue routes) -->
3334 <template v-for =" child in item .items " :key =" childKey (child )" >
34- <!-- External / href (same-domain docs or external) -->
35+ <!-- External child -->
3536 <a
3637 v-if =" isExternal(child)"
3738 class =" dd-item"
39+ :class =" { 'is-active': isChildActive(child) }"
3840 :href =" child.href"
3941 :target =" child.target || (isSameOrigin(child.href) ? '_self' : '_blank')"
4042 rel =" noreferrer"
4345 {{ child.label }}
4446 </a >
4547
46- <!-- Internal vue-router link -->
48+ <!-- Internal child -->
4749 <RouterLink
4850 v-else
4951 class =" dd-item"
52+ :class =" { 'is-active': isChildActive(child) }"
5053 :to =" child.to"
5154 @click =" closeAll()"
5255 >
5659 </div >
5760 </div >
5861
59- <!-- External top-level (GitHub etc. ) -->
62+ <!-- External top-level: Docs (href ) -->
6063 <a
6164 v-else-if =" isExternal(item)"
6265 class =" link"
66+ :class =" { 'is-active': isActive(item) }"
6367 :href =" item.href"
6468 :target =" item.target || (isSameOrigin(item.href) ? '_self' : '_blank')"
6569 rel =" noreferrer"
70+ @click =" closeAll()"
6671 >
6772 {{ item.label }}
6873 </a >
6974
70- <!-- Internal -->
75+ <!-- Internal top-level: Install, Registry -->
7176 <RouterLink
7277 v-else
7378 class =" link"
79+ :class =" { 'is-active': isActive(item) }"
7480 :to =" item.to"
75- active-class =" is-active"
7681 @click =" closeAll()"
7782 >
7883 {{ item.label }}
124129 </form >
125130
126131 <template v-for =" item in navItems " :key =" ' m-' + itemKey (item )" >
127- <!-- Dropdown -->
128- <div v-if =" item.kind === 'dropdown' " class =" mdd" >
132+ <!-- Dropdown: only when item has children (About) -->
133+ <div v-if =" Array.isArray( item.items) && item.items.length " class =" mdd" >
129134 <button
130135 type =" button"
131136 class =" mlink mdd-btn"
137+ :class =" { 'is-active': isActive(item) || item.items.some(isChildActive) }"
132138 @click =" toggleMobileGroup(item.label)"
133139 :aria-expanded =" String(isMobileGroupOpen(item.label))"
134140 >
138144
139145 <div v-if =" isMobileGroupOpen(item.label)" class =" mdd-menu" >
140146 <template v-for =" child in item .items " :key =" ' m-' + childKey (child )" >
141- <!-- External / href -->
147+ <!-- External child -->
142148 <a
143149 v-if =" isExternal(child)"
144150 class =" mdd-item"
151+ :class =" { 'is-active': isChildActive(child) }"
145152 :href =" child.href"
146153 :target =" child.target || (isSameOrigin(child.href) ? '_self' : '_blank')"
147154 rel =" noreferrer"
150157 {{ child.label }}
151158 </a >
152159
153- <!-- Internal vue-router link -->
160+ <!-- Internal child -->
154161 <RouterLink
155162 v-else
156163 class =" mdd-item"
164+ :class =" { 'is-active': isChildActive(child) }"
157165 :to =" child.to"
158166 @click =" closeMobile()"
159167 >
163171 </div >
164172 </div >
165173
166- <!-- External -->
174+ <!-- External top-level: Docs -->
167175 <a
168176 v-else-if =" isExternal(item)"
169177 class =" mlink"
178+ :class =" { 'is-active': isActive(item) }"
170179 :href =" item.href"
171180 :target =" item.target || (isSameOrigin(item.href) ? '_self' : '_blank')"
172181 rel =" noreferrer"
175184 {{ item.label }}
176185 </a >
177186
178- <!-- Internal -->
187+ <!-- Internal top-level -->
179188 <RouterLink
180189 v-else
181190 class =" mlink"
191+ :class =" { 'is-active': isActive(item) }"
182192 :to =" item.to"
183193 @click =" closeMobile()"
184194 >
189199 </header >
190200</template >
191201
202+
192203<script setup>
193204import { computed , onBeforeUnmount , onMounted , ref } from " vue" ;
205+ import { useRoute } from " vue-router" ;
194206import { NAV } from " @/data/nav" ;
195207
208+ const route = useRoute ();
196209const navItems = computed (() => NAV );
197210
198211function isExternal (item ) {
199212 return !! item? .external || !! item? .href ;
200213}
201214
215+ function normalizePath (p ) {
216+ if (! p) return " " ;
217+ // keep only pathname (ignore query/hash)
218+ const q = p .indexOf (" ?" );
219+ const h = p .indexOf (" #" );
220+ const cut = Math .min (q === - 1 ? p .length : q, h === - 1 ? p .length : h);
221+ return p .slice (0 , cut);
222+ }
223+
224+ function isActive (item ) {
225+ const match = item? .match || " " ;
226+ if (! match) return false ;
227+
228+ const current = normalizePath (route .path );
229+ // exact or prefix match
230+ return current === match || current .startsWith (match + " /" );
231+ }
232+
233+ function isChildActive (child ) {
234+ // internal vue route
235+ if (child? .to ) return normalizePath (route .path ).startsWith (normalizePath (child .to ));
236+ // same origin href: docs is external:true but same site, so compare path
237+ if (child? .href && isSameOrigin (child .href )) {
238+ const path = normalizePath (new URL (child .href , window .location .origin ).pathname );
239+ const current = normalizePath (route .path );
240+ return current === path || current .startsWith (path + " /" );
241+ }
242+ return false ;
243+ }
244+
202245// Same-origin helper: docs are same-domain (/docs/...)
203246function isSameOrigin (href ) {
204247 if (! href) return false ;
205- // relative paths like "/docs/..." are same-origin
206248 if (href .startsWith (" /" )) return true ;
207249 try {
208250 const u = new URL (href, window .location .origin );
@@ -213,7 +255,7 @@ function isSameOrigin(href) {
213255}
214256
215257function itemKey (item ) {
216- if (item .kind === " dropdown " ) return ` dd:${ item .label } ` ;
258+ if (item .items ? . length ) return ` dd:${ item .label } ` ;
217259 if (isExternal (item)) return ` href:${ item .href } ` ;
218260 return ` to:${ item .to } ` ;
219261}
@@ -274,7 +316,7 @@ onBeforeUnmount(() => {
274316 document .documentElement .classList .remove (" nav-open" );
275317});
276318
277- /* Search (docs via VitePress) */
319+ /* Search */
278320const qDesktop = ref (" " );
279321const qMobile = ref (" " );
280322
@@ -289,14 +331,11 @@ function submitSearch(which) {
289331
290332 closeAll ();
291333 closeMobile ();
292-
293- // VitePress docs:
294- // - simplest: go to /docs/ and let VitePress search handle it
295- // - we also pass query param so you can read it later if you want
296334 window .location .href = ` /docs/?q=${ encodeURIComponent (q)} ` ;
297335}
298336< / script>
299337
338+
300339< style scoped>
301340/* === Header shell === */
302341.hdr {
0 commit comments