srcdev-nuxt-components 9.1.9 → 9.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +3 -1
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +30 -91
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +1 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +15 -78
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +27 -0
- package/app/composables/useNavCollapse.ts +82 -0
- package/package.json +1 -1
package/.claude/settings.json
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"Bash(npx tsc:*)",
|
|
17
17
|
"Bash(npx vitest:*)",
|
|
18
18
|
"Bash(git show:*)",
|
|
19
|
-
"Edit(/.claude/skills/components/**)"
|
|
19
|
+
"Edit(/.claude/skills/components/**)",
|
|
20
|
+
"Bash(npx nuxi:*)",
|
|
21
|
+
"Bash(node -e \"const t = require\\('/Users/simoncornforth/websites/nuxt-components/node_modules/pinia-plugin-persistedstate'\\); console.log\\(Object.keys\\(t\\)\\)\")"
|
|
20
22
|
],
|
|
21
23
|
"additionalDirectories": [
|
|
22
24
|
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/content-wrappers/content-width",
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
@mouseleave="resetHoverNavToActive"
|
|
18
18
|
@mouseover="handleNavHover"
|
|
19
19
|
>
|
|
20
|
-
<li
|
|
20
|
+
<li
|
|
21
|
+
v-for="item in navItemData.main"
|
|
22
|
+
:key="item.href"
|
|
23
|
+
:class="[item.cssName, { 'is-active': isActiveItem(item.href) }]"
|
|
24
|
+
>
|
|
21
25
|
<NuxtLink :href="item.href" :external="item.isExternal || undefined" class="site-nav-link" data-nav-item>
|
|
22
26
|
<Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
|
|
23
27
|
{{ item.text }}
|
|
@@ -70,7 +74,11 @@
|
|
|
70
74
|
@mouseover="handlePanelHover"
|
|
71
75
|
@mouseleave="resetHoverPanelToActive"
|
|
72
76
|
>
|
|
73
|
-
<li
|
|
77
|
+
<li
|
|
78
|
+
v-for="item in navItemData.main"
|
|
79
|
+
:key="item.href"
|
|
80
|
+
:class="[item.cssName, { 'is-active': isActiveItem(item.href) }]"
|
|
81
|
+
>
|
|
74
82
|
<NuxtLink
|
|
75
83
|
:href="item.href"
|
|
76
84
|
:external="item.isExternal || undefined"
|
|
@@ -92,7 +100,6 @@
|
|
|
92
100
|
</template>
|
|
93
101
|
|
|
94
102
|
<script setup lang="ts">
|
|
95
|
-
import { useResizeObserver, onClickOutside } from "@vueuse/core";
|
|
96
103
|
import type { NavItemData } from "~/types/components";
|
|
97
104
|
|
|
98
105
|
interface Props {
|
|
@@ -106,35 +113,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
106
113
|
styleClassPassthrough: () => [],
|
|
107
114
|
});
|
|
108
115
|
|
|
109
|
-
const navRef = ref<HTMLElement | null>(null);
|
|
110
|
-
const navListRef = ref<HTMLUListElement | null>(null);
|
|
111
|
-
|
|
112
|
-
const isCollapsed = ref(false);
|
|
113
|
-
const isLoaded = useState("site-nav-loaded", () => false);
|
|
114
|
-
const isMenuOpen = ref(false);
|
|
115
|
-
|
|
116
|
-
// Stored natural width of the list — used when the list is not in the DOM
|
|
117
|
-
let navListNaturalWidth = 0;
|
|
118
|
-
|
|
119
|
-
const checkOverflow = () => {
|
|
120
|
-
if (!navRef.value) return;
|
|
121
|
-
|
|
122
|
-
// Measure and store the list width whenever it's in the DOM
|
|
123
|
-
if (navListRef.value) {
|
|
124
|
-
navListNaturalWidth = navListRef.value.scrollWidth;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
isCollapsed.value = navListNaturalWidth > navRef.value.clientWidth;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const toggleMenu = () => {
|
|
131
|
-
isMenuOpen.value = !isMenuOpen.value;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const closeMenu = () => {
|
|
135
|
-
isMenuOpen.value = false;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
116
|
// ─── Nav decorators (active / hover indicators) ─────────────────────────────
|
|
139
117
|
|
|
140
118
|
const NAV_DECORATOR_DURATION = 200;
|
|
@@ -221,6 +199,7 @@ const resetHoverNavToActive = () => {
|
|
|
221
199
|
};
|
|
222
200
|
|
|
223
201
|
const initNavDecorators = () => {
|
|
202
|
+
cachedNavLinks = []; // invalidate — list may have been re-rendered (collapse toggle)
|
|
224
203
|
if (!navListRef.value) return;
|
|
225
204
|
const links = getNavLinks();
|
|
226
205
|
if (!links.length) return;
|
|
@@ -230,8 +209,7 @@ const initNavDecorators = () => {
|
|
|
230
209
|
navSnapTimer = null;
|
|
231
210
|
}
|
|
232
211
|
|
|
233
|
-
const activeLink =
|
|
234
|
-
links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
|
|
212
|
+
const activeLink = links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
|
|
235
213
|
if (!activeLink) return;
|
|
236
214
|
|
|
237
215
|
currentActiveNavLink = activeLink;
|
|
@@ -334,8 +312,7 @@ const initPanelDecorators = () => {
|
|
|
334
312
|
panelSnapTimer = null;
|
|
335
313
|
}
|
|
336
314
|
|
|
337
|
-
const activeLink =
|
|
338
|
-
links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
|
|
315
|
+
const activeLink = links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
|
|
339
316
|
if (!activeLink) return;
|
|
340
317
|
|
|
341
318
|
currentActivePanelLink = activeLink;
|
|
@@ -348,61 +325,25 @@ const initPanelDecorators = () => {
|
|
|
348
325
|
|
|
349
326
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
350
327
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const activeHref = computed(() => {
|
|
356
|
-
const items = props.navItemData.main ?? [];
|
|
357
|
-
const exact = items.find((item) => item.href && route.path === item.href);
|
|
358
|
-
if (exact) return exact.href ?? null;
|
|
359
|
-
return (
|
|
360
|
-
items
|
|
361
|
-
.filter((item) => item.href && route.path.startsWith(item.href + "/"))
|
|
362
|
-
.sort((a, b) => (b.href?.length ?? 0) - (a.href?.length ?? 0))[0]?.href ?? null
|
|
363
|
-
);
|
|
364
|
-
});
|
|
365
|
-
watch(
|
|
366
|
-
() => route.path,
|
|
367
|
-
() => {
|
|
368
|
-
closeMenu();
|
|
369
|
-
requestAnimationFrame(() => {
|
|
328
|
+
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, activeHref, isActiveItem, toggleMenu, closeMenu } =
|
|
329
|
+
useNavCollapse("site-nav-loaded", {
|
|
330
|
+
onResize: async () => {
|
|
331
|
+
await nextTick(); // wait for Vue to re-render after any isCollapsed change
|
|
370
332
|
initNavDecorators();
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
onClickOutside(navRef, closeMenu);
|
|
386
|
-
|
|
387
|
-
const router = useRouter();
|
|
388
|
-
|
|
389
|
-
onMounted(async () => {
|
|
390
|
-
await nextTick();
|
|
391
|
-
checkOverflow();
|
|
392
|
-
isLoaded.value = true;
|
|
393
|
-
await router.isReady();
|
|
394
|
-
requestAnimationFrame(() => {
|
|
395
|
-
initNavDecorators();
|
|
333
|
+
if (isMenuOpen.value) {
|
|
334
|
+
setFinalPanelActivePositions(true);
|
|
335
|
+
setFinalPanelHoveredPositions(true);
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
onRouteChange: () => {
|
|
339
|
+
requestAnimationFrame(() => {
|
|
340
|
+
initNavDecorators();
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
onMounted: () => {
|
|
344
|
+
initNavDecorators();
|
|
345
|
+
},
|
|
396
346
|
});
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
watch(isCollapsed, async (collapsed) => {
|
|
400
|
-
if (!collapsed) {
|
|
401
|
-
cachedNavLinks = []; // nav <ul> was re-rendered — invalidate cache
|
|
402
|
-
await nextTick();
|
|
403
|
-
initNavDecorators();
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
347
|
|
|
407
348
|
watch(isMenuOpen, async (open) => {
|
|
408
349
|
if (open) {
|
|
@@ -588,7 +529,6 @@ watch(
|
|
|
588
529
|
color: var(--_link-hover-color);
|
|
589
530
|
outline: none;
|
|
590
531
|
}
|
|
591
|
-
|
|
592
532
|
}
|
|
593
533
|
li.is-active .site-nav-link {
|
|
594
534
|
color: var(--_link-active-color);
|
|
@@ -781,7 +721,6 @@ watch(
|
|
|
781
721
|
color: var(--_panel-link-hover-color);
|
|
782
722
|
outline: none;
|
|
783
723
|
}
|
|
784
|
-
|
|
785
724
|
}
|
|
786
725
|
li.is-active .site-nav-panel-link {
|
|
787
726
|
color: var(--_panel-link-active-color);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports[`SiteNavigation > renders correct HTML structure 1`] = `
|
|
4
4
|
"<nav class="site-navigation site-navigation--left is-loaded" aria-label="Site navigation">
|
|
5
5
|
<ul class="site-nav-list" style="--_transition-duration: 0ms; --_x-active: 0px; --_width-active: NaN; --_x-hovered: 0px; --_width-hovered: NaN;">
|
|
6
|
-
<li class=""><a href="/" class="site-nav-link" data-nav-item="">
|
|
6
|
+
<li class="is-active"><a href="/" class="site-nav-link" data-nav-item="">
|
|
7
7
|
<!--v-if--> Home
|
|
8
8
|
</a></li>
|
|
9
9
|
<li class=""><a href="/about" class="site-nav-link" data-nav-item="">
|
|
@@ -13,16 +13,21 @@
|
|
|
13
13
|
v-if="!isCollapsed || !isLoaded"
|
|
14
14
|
ref="navListRef"
|
|
15
15
|
class="tab-nav-list"
|
|
16
|
-
@mousemove="handleNavMousemove"
|
|
17
16
|
@mouseleave="hoveredItemHref = null"
|
|
18
17
|
>
|
|
19
18
|
<li
|
|
20
19
|
v-for="item in navItemData.main"
|
|
21
20
|
:key="item.href"
|
|
22
21
|
:data-href="item.href"
|
|
23
|
-
:class="[item.cssName, { 'is-active':
|
|
22
|
+
:class="[item.cssName, { 'is-active': isActiveItem(item.href), 'is-hovered': hoveredItemHref === item.href }]"
|
|
23
|
+
@mouseenter="hoveredItemHref = item.href ?? null"
|
|
24
24
|
>
|
|
25
|
-
<NuxtLink
|
|
25
|
+
<NuxtLink
|
|
26
|
+
:href="item.href"
|
|
27
|
+
:external="item.isExternal || undefined"
|
|
28
|
+
class="tab-nav-link"
|
|
29
|
+
data-nav-item
|
|
30
|
+
>
|
|
26
31
|
<Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
|
|
27
32
|
{{ item.text }}
|
|
28
33
|
</NuxtLink>
|
|
@@ -32,7 +37,7 @@
|
|
|
32
37
|
</ul>
|
|
33
38
|
|
|
34
39
|
<InputButtonCore
|
|
35
|
-
v-if="
|
|
40
|
+
v-if="showCollapsed"
|
|
36
41
|
class="tab-nav-burger"
|
|
37
42
|
:class="{ 'is-open': isMenuOpen }"
|
|
38
43
|
variant="tertiary"
|
|
@@ -50,7 +55,7 @@
|
|
|
50
55
|
|
|
51
56
|
<Teleport to="body">
|
|
52
57
|
<div
|
|
53
|
-
v-if="
|
|
58
|
+
v-if="showCollapsed"
|
|
54
59
|
class="tab-nav-backdrop"
|
|
55
60
|
:class="{ 'is-open': isMenuOpen }"
|
|
56
61
|
aria-hidden="true"
|
|
@@ -59,7 +64,7 @@
|
|
|
59
64
|
</Teleport>
|
|
60
65
|
|
|
61
66
|
<div
|
|
62
|
-
v-if="
|
|
67
|
+
v-if="showCollapsed"
|
|
63
68
|
id="tab-nav-panel"
|
|
64
69
|
class="tab-nav-panel"
|
|
65
70
|
:class="{ 'is-open': isMenuOpen }"
|
|
@@ -85,7 +90,6 @@
|
|
|
85
90
|
</template>
|
|
86
91
|
|
|
87
92
|
<script setup lang="ts">
|
|
88
|
-
import { useResizeObserver, onClickOutside } from "@vueuse/core";
|
|
89
93
|
import type { NavItemData } from "~/types/components";
|
|
90
94
|
|
|
91
95
|
interface Props {
|
|
@@ -99,77 +103,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
99
103
|
styleClassPassthrough: () => [],
|
|
100
104
|
});
|
|
101
105
|
|
|
102
|
-
const navRef
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const isCollapsed = ref(false);
|
|
106
|
-
const isLoaded = useState("tab-nav-loaded", () => false);
|
|
107
|
-
const isMenuOpen = ref(false);
|
|
108
|
-
|
|
109
|
-
// Stored natural width of the list — used when the list is not in the DOM
|
|
110
|
-
let navListNaturalWidth = 0;
|
|
111
|
-
|
|
112
|
-
const checkOverflow = () => {
|
|
113
|
-
if (!navRef.value) return;
|
|
114
|
-
|
|
115
|
-
if (navListRef.value) {
|
|
116
|
-
navListNaturalWidth = navListRef.value.scrollWidth;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
isCollapsed.value = navListNaturalWidth > navRef.value.clientWidth;
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const toggleMenu = () => {
|
|
123
|
-
isMenuOpen.value = !isMenuOpen.value;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const closeMenu = () => {
|
|
127
|
-
isMenuOpen.value = false;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const route = useRoute();
|
|
131
|
-
|
|
132
|
-
// Compute the active nav item href from the current route.
|
|
133
|
-
// Prefers an exact match; falls back to the longest prefix match.
|
|
134
|
-
const activeHref = computed(() => {
|
|
135
|
-
const items = props.navItemData.main ?? [];
|
|
136
|
-
const exact = items.find((item) => item.href && route.path === item.href);
|
|
137
|
-
if (exact) return exact.href ?? null;
|
|
138
|
-
return (
|
|
139
|
-
items
|
|
140
|
-
.filter((item) => item.href && route.path.startsWith(item.href + "/"))
|
|
141
|
-
.sort((a, b) => (b.href?.length ?? 0) - (a.href?.length ?? 0))[0]?.href ?? null
|
|
142
|
-
);
|
|
143
|
-
});
|
|
106
|
+
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, isActiveItem, toggleMenu, closeMenu } =
|
|
107
|
+
useNavCollapse("tab-nav-loaded");
|
|
144
108
|
|
|
145
109
|
const hoveredItemHref = ref<string | null>(null);
|
|
146
|
-
|
|
147
|
-
const handleNavMousemove = (event: MouseEvent) => {
|
|
148
|
-
const li = (event.target as HTMLElement).closest<HTMLElement>("li[data-href]");
|
|
149
|
-
if (!li) return;
|
|
150
|
-
hoveredItemHref.value = li.dataset.href ?? null;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
watch(
|
|
154
|
-
() => route.path,
|
|
155
|
-
() => {
|
|
156
|
-
closeMenu();
|
|
157
|
-
},
|
|
158
|
-
{ flush: "post" }
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
useResizeObserver(navRef, () => {
|
|
162
|
-
checkOverflow();
|
|
163
|
-
if (!isCollapsed.value) closeMenu();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
onClickOutside(navRef, closeMenu);
|
|
167
|
-
|
|
168
|
-
onMounted(async () => {
|
|
169
|
-
await nextTick();
|
|
170
|
-
checkOverflow();
|
|
171
|
-
isLoaded.value = true;
|
|
172
|
-
});
|
|
110
|
+
const showCollapsed = computed(() => isCollapsed.value && isLoaded.value);
|
|
173
111
|
|
|
174
112
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
175
113
|
|
|
@@ -206,8 +144,7 @@ watch(
|
|
|
206
144
|
|
|
207
145
|
/* Decorators — horizontal nav */
|
|
208
146
|
--_decorator-hovered-bg: var(--tab-nav-decorator-hovered-bg, transparent);
|
|
209
|
-
|
|
210
|
-
--_decorator-indicator-color: red;
|
|
147
|
+
--_decorator-indicator-color: var(--tab-nav-decorator-indicator-color, var(--slate-01, currentColor));
|
|
211
148
|
|
|
212
149
|
/* Horizontal nav */
|
|
213
150
|
--_link-color: var(--tab-nav-link-color, var(--slate-01, currentColor));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`TabNavigation > renders correct HTML structure 1`] = `
|
|
4
|
+
"<nav class="tab-navigation tab-navigation--left is-loaded" aria-label="Site navigation">
|
|
5
|
+
<ul class="tab-nav-list">
|
|
6
|
+
<li data-href="/" class="is-active"><a href="/" class="tab-nav-link" data-nav-item="">
|
|
7
|
+
<!--v-if--> Home
|
|
8
|
+
</a></li>
|
|
9
|
+
<li data-href="/about" class=""><a href="/about" class="tab-nav-link" data-nav-item="">
|
|
10
|
+
<!--v-if--> About
|
|
11
|
+
</a></li>
|
|
12
|
+
<li data-href="/contact" class=""><a href="/contact" class="tab-nav-link" data-nav-item="">
|
|
13
|
+
<!--v-if--> Contact
|
|
14
|
+
</a></li>
|
|
15
|
+
<li aria-hidden="true" role="none" class="nav-indicator-li">
|
|
16
|
+
<div class="nav__hovered"></div>
|
|
17
|
+
</li>
|
|
18
|
+
<li aria-hidden="true" role="none" class="nav-indicator-li">
|
|
19
|
+
<div class="nav__active-indicator"></div>
|
|
20
|
+
</li>
|
|
21
|
+
</ul>
|
|
22
|
+
<!--v-if-->
|
|
23
|
+
<!--teleport start-->
|
|
24
|
+
<!--teleport end-->
|
|
25
|
+
<!--v-if-->
|
|
26
|
+
</nav>"
|
|
27
|
+
`;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useResizeObserver, onClickOutside } from "@vueuse/core";
|
|
2
|
+
|
|
3
|
+
interface NavCollapseOptions {
|
|
4
|
+
onResize?: () => void;
|
|
5
|
+
onRouteChange?: () => void;
|
|
6
|
+
onMounted?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useNavCollapse = (stateKey: string, options: NavCollapseOptions = {}) => {
|
|
10
|
+
const navRef = ref<HTMLElement | null>(null);
|
|
11
|
+
const navListRef = ref<HTMLUListElement | null>(null);
|
|
12
|
+
|
|
13
|
+
const isCollapsed = ref(false);
|
|
14
|
+
const isLoaded = useState(stateKey, () => false);
|
|
15
|
+
const isMenuOpen = ref(false);
|
|
16
|
+
|
|
17
|
+
// Stored natural width of the list — used when the list is not in the DOM
|
|
18
|
+
let navListNaturalWidth = 0;
|
|
19
|
+
|
|
20
|
+
const checkOverflow = () => {
|
|
21
|
+
if (!navRef.value) return;
|
|
22
|
+
if (navListRef.value) {
|
|
23
|
+
navListNaturalWidth = navListRef.value.scrollWidth;
|
|
24
|
+
}
|
|
25
|
+
isCollapsed.value = navListNaturalWidth > navRef.value.clientWidth;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const toggleMenu = () => {
|
|
29
|
+
isMenuOpen.value = !isMenuOpen.value;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const closeMenu = () => {
|
|
33
|
+
isMenuOpen.value = false;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// useRoute() in Nuxt is SSR-aware — route.path is consistent on server and client,
|
|
37
|
+
// so isActiveItem can be used directly without a isMounted guard.
|
|
38
|
+
const route = useRoute();
|
|
39
|
+
|
|
40
|
+
const activeHref = computed(() => route.path);
|
|
41
|
+
const isActiveItem = (href?: string) => href === route.path;
|
|
42
|
+
|
|
43
|
+
watch(
|
|
44
|
+
() => route.path,
|
|
45
|
+
() => {
|
|
46
|
+
closeMenu();
|
|
47
|
+
options.onRouteChange?.();
|
|
48
|
+
},
|
|
49
|
+
{ flush: "post" }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
useResizeObserver(navRef, () => {
|
|
53
|
+
checkOverflow();
|
|
54
|
+
if (!isCollapsed.value) closeMenu();
|
|
55
|
+
options.onResize?.();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
onClickOutside(navRef, closeMenu);
|
|
59
|
+
|
|
60
|
+
const router = useRouter();
|
|
61
|
+
|
|
62
|
+
onMounted(async () => {
|
|
63
|
+
await nextTick();
|
|
64
|
+
checkOverflow();
|
|
65
|
+
isLoaded.value = true;
|
|
66
|
+
await router.isReady();
|
|
67
|
+
options.onMounted?.();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
navRef,
|
|
72
|
+
navListRef,
|
|
73
|
+
isCollapsed,
|
|
74
|
+
isLoaded,
|
|
75
|
+
isMenuOpen,
|
|
76
|
+
activeHref,
|
|
77
|
+
checkOverflow,
|
|
78
|
+
toggleMenu,
|
|
79
|
+
closeMenu,
|
|
80
|
+
isActiveItem,
|
|
81
|
+
};
|
|
82
|
+
};
|