srcdev-nuxt-components 9.1.8 → 9.1.10
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 +47 -92
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +484 -0
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +340 -0
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +335 -0
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +27 -0
- package/app/composables/useNavCollapse.ts +90 -0
- package/app/stores/useNavigationStore.ts +113 -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;
|
|
@@ -200,6 +178,13 @@ const moveNavHoveredIndicator = () => {
|
|
|
200
178
|
const handleNavLinkClick = (event: MouseEvent) => {
|
|
201
179
|
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
|
|
202
180
|
if (!target) return;
|
|
181
|
+
|
|
182
|
+
// Update store with clicked href for reliable active state tracking
|
|
183
|
+
const href = target.getAttribute("href");
|
|
184
|
+
if (href) {
|
|
185
|
+
navigationStore.handleNavLinkClick(href);
|
|
186
|
+
}
|
|
187
|
+
|
|
203
188
|
currentActiveNavLink = target;
|
|
204
189
|
currentHoveredNavLink = target;
|
|
205
190
|
previousHoveredNavLink = target;
|
|
@@ -230,10 +215,7 @@ const initNavDecorators = () => {
|
|
|
230
215
|
navSnapTimer = null;
|
|
231
216
|
}
|
|
232
217
|
|
|
233
|
-
const activeLink =
|
|
234
|
-
links.find((el) => el.classList.contains("router-link-exact-active")) ??
|
|
235
|
-
links.find((el) => el.classList.contains("router-link-active")) ??
|
|
236
|
-
links[0];
|
|
218
|
+
const activeLink = links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
|
|
237
219
|
if (!activeLink) return;
|
|
238
220
|
|
|
239
221
|
currentActiveNavLink = activeLink;
|
|
@@ -305,6 +287,13 @@ const movePanelHoveredIndicator = () => {
|
|
|
305
287
|
const handlePanelLinkClick = (event: MouseEvent) => {
|
|
306
288
|
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
|
|
307
289
|
if (!target) return;
|
|
290
|
+
|
|
291
|
+
// Update store with clicked href for reliable active state tracking
|
|
292
|
+
const href = target.getAttribute("href");
|
|
293
|
+
if (href) {
|
|
294
|
+
navigationStore.handleNavLinkClick(href);
|
|
295
|
+
}
|
|
296
|
+
|
|
308
297
|
currentActivePanelLink = target;
|
|
309
298
|
currentHoveredPanelLink = target;
|
|
310
299
|
previousHoveredPanelLink = target;
|
|
@@ -336,10 +325,7 @@ const initPanelDecorators = () => {
|
|
|
336
325
|
panelSnapTimer = null;
|
|
337
326
|
}
|
|
338
327
|
|
|
339
|
-
const activeLink =
|
|
340
|
-
links.find((el) => el.classList.contains("router-link-exact-active")) ??
|
|
341
|
-
links.find((el) => el.classList.contains("router-link-active")) ??
|
|
342
|
-
links[0];
|
|
328
|
+
const activeLink = links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
|
|
343
329
|
if (!activeLink) return;
|
|
344
330
|
|
|
345
331
|
currentActivePanelLink = activeLink;
|
|
@@ -352,52 +338,23 @@ const initPanelDecorators = () => {
|
|
|
352
338
|
|
|
353
339
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
354
340
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
341
|
+
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, activeHref, isActiveItem, toggleMenu, closeMenu, navigationStore } =
|
|
342
|
+
useNavCollapse(props.navItemData, "site-nav-loaded", {
|
|
343
|
+
onResize: () => {
|
|
344
|
+
setFinalNavActivePositions(true);
|
|
345
|
+
setFinalNavHoveredPositions(true);
|
|
346
|
+
setFinalPanelActivePositions(true);
|
|
347
|
+
setFinalPanelHoveredPositions(true);
|
|
348
|
+
},
|
|
349
|
+
onRouteChange: () => {
|
|
350
|
+
requestAnimationFrame(() => {
|
|
351
|
+
initNavDecorators();
|
|
352
|
+
});
|
|
353
|
+
},
|
|
354
|
+
onMounted: () => {
|
|
365
355
|
initNavDecorators();
|
|
366
|
-
}
|
|
367
|
-
},
|
|
368
|
-
{ flush: "post" }
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
useResizeObserver(navRef, () => {
|
|
372
|
-
checkOverflow();
|
|
373
|
-
if (!isCollapsed.value) closeMenu();
|
|
374
|
-
setFinalNavActivePositions(true);
|
|
375
|
-
setFinalNavHoveredPositions(true);
|
|
376
|
-
setFinalPanelActivePositions(true);
|
|
377
|
-
setFinalPanelHoveredPositions(true);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
onClickOutside(navRef, closeMenu);
|
|
381
|
-
|
|
382
|
-
const router = useRouter();
|
|
383
|
-
|
|
384
|
-
onMounted(async () => {
|
|
385
|
-
await nextTick();
|
|
386
|
-
checkOverflow();
|
|
387
|
-
isLoaded.value = true;
|
|
388
|
-
await router.isReady();
|
|
389
|
-
requestAnimationFrame(() => {
|
|
390
|
-
initNavDecorators();
|
|
356
|
+
},
|
|
391
357
|
});
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
watch(isCollapsed, async (collapsed) => {
|
|
395
|
-
if (!collapsed) {
|
|
396
|
-
cachedNavLinks = []; // nav <ul> was re-rendered — invalidate cache
|
|
397
|
-
await nextTick();
|
|
398
|
-
initNavDecorators();
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
358
|
|
|
402
359
|
watch(isMenuOpen, async (open) => {
|
|
403
360
|
if (open) {
|
|
@@ -583,10 +540,9 @@ watch(
|
|
|
583
540
|
color: var(--_link-hover-color);
|
|
584
541
|
outline: none;
|
|
585
542
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
543
|
+
}
|
|
544
|
+
li.is-active .site-nav-link {
|
|
545
|
+
color: var(--_link-active-color);
|
|
590
546
|
}
|
|
591
547
|
}
|
|
592
548
|
|
|
@@ -776,10 +732,9 @@ watch(
|
|
|
776
732
|
color: var(--_panel-link-hover-color);
|
|
777
733
|
outline: none;
|
|
778
734
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
735
|
+
}
|
|
736
|
+
li.is-active .site-nav-panel-link {
|
|
737
|
+
color: var(--_panel-link-active-color);
|
|
783
738
|
}
|
|
784
739
|
}
|
|
785
740
|
}
|