srcdev-nuxt-components 9.1.10 → 9.1.12

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.
@@ -5,7 +5,7 @@
5
5
  :class="[
6
6
  elementClasses,
7
7
  `site-navigation--${navAlign}`,
8
- { 'is-collapsed': isCollapsed, 'is-loaded': isLoaded, 'menu-open': isMenuOpen },
8
+ { 'is-collapsed': isCollapsed, 'is-loaded': isLoaded, 'menu-open': isMenuOpen, 'is-animated': isAnimated },
9
9
  ]"
10
10
  aria-label="Site navigation"
11
11
  >
@@ -113,6 +113,10 @@ const props = withDefaults(defineProps<Props>(), {
113
113
  styleClassPassthrough: () => [],
114
114
  });
115
115
 
116
+ // ─── Animation gate — prevents indicator from transitioning on first paint ───
117
+
118
+ const isAnimated = ref(false);
119
+
116
120
  // ─── Nav decorators (active / hover indicators) ─────────────────────────────
117
121
 
118
122
  const NAV_DECORATOR_DURATION = 200;
@@ -178,13 +182,6 @@ const moveNavHoveredIndicator = () => {
178
182
  const handleNavLinkClick = (event: MouseEvent) => {
179
183
  const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
180
184
  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
-
188
185
  currentActiveNavLink = target;
189
186
  currentHoveredNavLink = target;
190
187
  previousHoveredNavLink = target;
@@ -206,6 +203,7 @@ const resetHoverNavToActive = () => {
206
203
  };
207
204
 
208
205
  const initNavDecorators = () => {
206
+ cachedNavLinks = []; // invalidate — list may have been re-rendered (collapse toggle)
209
207
  if (!navListRef.value) return;
210
208
  const links = getNavLinks();
211
209
  if (!links.length) return;
@@ -287,13 +285,6 @@ const movePanelHoveredIndicator = () => {
287
285
  const handlePanelLinkClick = (event: MouseEvent) => {
288
286
  const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
289
287
  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
-
297
288
  currentActivePanelLink = target;
298
289
  currentHoveredPanelLink = target;
299
290
  previousHoveredPanelLink = target;
@@ -338,21 +329,30 @@ const initPanelDecorators = () => {
338
329
 
339
330
  // ─────────────────────────────────────────────────────────────────────────────
340
331
 
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);
332
+ const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, activeHref, isActiveItem, toggleMenu, closeMenu } =
333
+ useNavCollapse("site-nav-loaded", {
334
+ onResize: async () => {
335
+ await nextTick(); // wait for Vue to re-render after any isCollapsed change
336
+ initNavDecorators();
337
+ if (isMenuOpen.value) {
338
+ setFinalPanelActivePositions(true);
339
+ setFinalPanelHoveredPositions(true);
340
+ }
348
341
  },
349
342
  onRouteChange: () => {
343
+ isAnimated.value = false;
350
344
  requestAnimationFrame(() => {
351
345
  initNavDecorators();
346
+ nextTick(() => {
347
+ isAnimated.value = true;
348
+ });
352
349
  });
353
350
  },
354
351
  onMounted: () => {
355
352
  initNavDecorators();
353
+ nextTick(() => {
354
+ isAnimated.value = true;
355
+ });
356
356
  },
357
357
  });
358
358
 
@@ -484,10 +484,15 @@ watch(
484
484
  scale: var(--_width-hovered, 0.001) 1;
485
485
  translate: var(--_x-hovered, 0) 0;
486
486
  transform-origin: left;
487
+ pointer-events: none;
488
+ }
489
+
490
+ .site-navigation.is-animated & .nav__hovered,
491
+ .site-navigation.is-animated & .nav__active,
492
+ .site-navigation.is-animated & .nav__active-indicator {
487
493
  transition:
488
494
  scale var(--_transition-duration, 200ms),
489
495
  translate var(--_transition-duration, 200ms);
490
- pointer-events: none;
491
496
  }
492
497
 
493
498
  .nav__active {
@@ -672,10 +677,15 @@ watch(
672
677
  scale: 1 var(--_panel-height-hovered, 0.001);
673
678
  translate: 0 var(--_panel-y-hovered, 0);
674
679
  transform-origin: top;
680
+ pointer-events: none;
681
+ }
682
+
683
+ .site-navigation.is-animated & .nav__hovered,
684
+ .site-navigation.is-animated & .nav__active,
685
+ .site-navigation.is-animated & .nav__active-indicator {
675
686
  transition:
676
687
  scale var(--_panel-transition-duration, 200ms),
677
688
  translate var(--_panel-transition-duration, 200ms);
678
- pointer-events: none;
679
689
  }
680
690
 
681
691
  .nav__active {
@@ -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,7 +13,6 @@
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
@@ -21,13 +20,13 @@
21
20
  :key="item.href"
22
21
  :data-href="item.href"
23
22
  :class="[item.cssName, { 'is-active': isActiveItem(item.href), 'is-hovered': hoveredItemHref === item.href }]"
23
+ @mouseenter="hoveredItemHref = item.href ?? null"
24
24
  >
25
25
  <NuxtLink
26
26
  :href="item.href"
27
27
  :external="item.isExternal || undefined"
28
28
  class="tab-nav-link"
29
29
  data-nav-item
30
- @click="handleNavLinkClick"
31
30
  >
32
31
  <Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
33
32
  {{ item.text }}
@@ -38,7 +37,7 @@
38
37
  </ul>
39
38
 
40
39
  <InputButtonCore
41
- v-if="isCollapsed && isLoaded"
40
+ v-if="showCollapsed"
42
41
  class="tab-nav-burger"
43
42
  :class="{ 'is-open': isMenuOpen }"
44
43
  variant="tertiary"
@@ -56,7 +55,7 @@
56
55
 
57
56
  <Teleport to="body">
58
57
  <div
59
- v-if="isCollapsed && isLoaded"
58
+ v-if="showCollapsed"
60
59
  class="tab-nav-backdrop"
61
60
  :class="{ 'is-open': isMenuOpen }"
62
61
  aria-hidden="true"
@@ -65,7 +64,7 @@
65
64
  </Teleport>
66
65
 
67
66
  <div
68
- v-if="isCollapsed && isLoaded"
67
+ v-if="showCollapsed"
69
68
  id="tab-nav-panel"
70
69
  class="tab-nav-panel"
71
70
  :class="{ 'is-open': isMenuOpen }"
@@ -78,7 +77,7 @@
78
77
  :href="item.href"
79
78
  :external="item.isExternal || undefined"
80
79
  class="tab-nav-panel-link"
81
- @click="handlePanelLinkClick"
80
+ @click="closeMenu"
82
81
  >
83
82
  <Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
84
83
  {{ item.text }}
@@ -104,33 +103,11 @@ const props = withDefaults(defineProps<Props>(), {
104
103
  styleClassPassthrough: () => [],
105
104
  });
106
105
 
107
- const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, isActiveItem, toggleMenu, closeMenu, navigationStore } =
108
- useNavCollapse(props.navItemData, "tab-nav-loaded");
109
-
110
- // Handle navigation link clicks to update store
111
- const handleNavLinkClick = (event: MouseEvent) => {
112
- const target = (event.target as HTMLElement).closest<HTMLElement>("[href]");
113
- if (!target) return;
114
- const href = target.getAttribute("href");
115
- if (href) navigationStore.handleNavLinkClick(href);
116
- };
117
-
118
- // Handle panel link clicks to update store and close menu
119
- const handlePanelLinkClick = (event: MouseEvent) => {
120
- const target = (event.target as HTMLElement).closest<HTMLElement>("[href]");
121
- if (!target) return;
122
- const href = target.getAttribute("href");
123
- if (href) navigationStore.handleNavLinkClick(href);
124
- closeMenu();
125
- };
106
+ const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, isActiveItem, toggleMenu, closeMenu } =
107
+ useNavCollapse("tab-nav-loaded");
126
108
 
127
109
  const hoveredItemHref = ref<string | null>(null);
128
-
129
- const handleNavMousemove = (event: MouseEvent) => {
130
- const li = (event.target as HTMLElement).closest<HTMLElement>("li[data-href]");
131
- if (!li) return;
132
- hoveredItemHref.value = li.dataset.href ?? null;
133
- };
110
+ const showCollapsed = computed(() => isCollapsed.value && isLoaded.value);
134
111
 
135
112
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
136
113
 
@@ -167,8 +144,7 @@ watch(
167
144
 
168
145
  /* Decorators — horizontal nav */
169
146
  --_decorator-hovered-bg: var(--tab-nav-decorator-hovered-bg, transparent);
170
- /* --_decorator-indicator-color: var(--tab-nav-decorator-indicator-color, var(--slate-01, currentColor)); */
171
- --_decorator-indicator-color: red;
147
+ --_decorator-indicator-color: var(--tab-nav-decorator-indicator-color, var(--slate-01, currentColor));
172
148
 
173
149
  /* Horizontal nav */
174
150
  --_link-color: var(--tab-nav-link-color, var(--slate-01, currentColor));
@@ -3,7 +3,7 @@
3
3
  exports[`TabNavigation > renders correct HTML structure 1`] = `
4
4
  "<nav class="tab-navigation tab-navigation--left is-loaded" aria-label="Site navigation">
5
5
  <ul class="tab-nav-list">
6
- <li data-href="/" class=""><a href="/" class="tab-nav-link" data-nav-item="">
6
+ <li data-href="/" class="is-active"><a href="/" class="tab-nav-link" data-nav-item="">
7
7
  <!--v-if--> Home
8
8
  </a></li>
9
9
  <li data-href="/about" class=""><a href="/about" class="tab-nav-link" data-nav-item="">
@@ -1,5 +1,4 @@
1
1
  import { useResizeObserver, onClickOutside } from "@vueuse/core";
2
- import type { NavItemData } from "~/types/components";
3
2
 
4
3
  interface NavCollapseOptions {
5
4
  onResize?: () => void;
@@ -7,14 +6,13 @@ interface NavCollapseOptions {
7
6
  onMounted?: () => void;
8
7
  }
9
8
 
10
- export const useNavCollapse = (navItemData: NavItemData, stateKey: string, options: NavCollapseOptions = {}) => {
9
+ export const useNavCollapse = (stateKey: string, options: NavCollapseOptions = {}) => {
11
10
  const navRef = ref<HTMLElement | null>(null);
12
11
  const navListRef = ref<HTMLUListElement | null>(null);
13
12
 
14
13
  const isCollapsed = ref(false);
15
14
  const isLoaded = useState(stateKey, () => false);
16
15
  const isMenuOpen = ref(false);
17
- const isMounted = ref(false);
18
16
 
19
17
  // Stored natural width of the list — used when the list is not in the DOM
20
18
  let navListNaturalWidth = 0;
@@ -35,19 +33,17 @@ export const useNavCollapse = (navItemData: NavItemData, stateKey: string, optio
35
33
  isMenuOpen.value = false;
36
34
  };
37
35
 
38
- const navigationStore = useNavigationStore();
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.
39
38
  const route = useRoute();
40
39
 
41
- const activeHref = computed(() => navigationStore.currentActiveHref);
42
-
43
- // Client-side only active state check to prevent hydration mismatch
44
- const isActiveItem = (href?: string) => isMounted.value && activeHref.value === href;
40
+ const activeHref = computed(() => route.path);
41
+ const isActiveItem = (href?: string) => href === route.path;
45
42
 
46
43
  watch(
47
44
  () => route.path,
48
- (newPath) => {
45
+ () => {
49
46
  closeMenu();
50
- navigationStore.syncWithRoute(navItemData.main ?? [], newPath);
51
47
  options.onRouteChange?.();
52
48
  },
53
49
  { flush: "post" }
@@ -68,8 +64,6 @@ export const useNavCollapse = (navItemData: NavItemData, stateKey: string, optio
68
64
  checkOverflow();
69
65
  isLoaded.value = true;
70
66
  await router.isReady();
71
- navigationStore.initializeFromRoute(navItemData.main ?? [], route.path);
72
- isMounted.value = true;
73
67
  options.onMounted?.();
74
68
  });
75
69
 
@@ -79,12 +73,10 @@ export const useNavCollapse = (navItemData: NavItemData, stateKey: string, optio
79
73
  isCollapsed,
80
74
  isLoaded,
81
75
  isMenuOpen,
82
- isMounted,
83
76
  activeHref,
84
77
  checkOverflow,
85
78
  toggleMenu,
86
79
  closeMenu,
87
80
  isActiveItem,
88
- navigationStore,
89
81
  };
90
82
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.1.10",
4
+ "version": "9.1.12",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
@@ -1,113 +0,0 @@
1
- export const useNavigationStore = defineStore(
2
- "useNavigationStore",
3
- () => {
4
- // State
5
- const activeHref = ref<string | null>(null);
6
- const isInitialized = ref(false);
7
-
8
- // Getters
9
- const currentActiveHref = computed(() => activeHref.value);
10
-
11
- // Actions
12
- const setActiveHref = (href: string | null) => {
13
- activeHref.value = href;
14
- };
15
-
16
- /**
17
- * Initialize active href based on current route and navigation items
18
- * Used on component mount to handle deep links and initial page loads
19
- */
20
- const initializeFromRoute = (navItems: Array<{ href?: string }>, routePath: string) => {
21
- if (isInitialized.value) return;
22
-
23
- const items = navItems.filter((item): item is { href: string } => Boolean(item.href));
24
-
25
- // Try exact match first
26
- const exact = items.find((item) => routePath === item.href);
27
- if (exact) {
28
- activeHref.value = exact.href;
29
- isInitialized.value = true;
30
- return;
31
- }
32
-
33
- // Fall back to longest prefix match
34
- const prefixMatches = items
35
- .filter((item) => routePath.startsWith(item.href + "/"))
36
- .sort((a, b) => b.href.length - a.href.length);
37
-
38
- if (prefixMatches.length > 0) {
39
- activeHref.value = prefixMatches[0]!.href;
40
- } else {
41
- // Default to null if no matches (let component handle fallback)
42
- activeHref.value = null;
43
- }
44
-
45
- isInitialized.value = true;
46
- };
47
-
48
- /**
49
- * Update active href when user clicks a navigation link
50
- * Provides explicit control over active state
51
- */
52
- const handleNavLinkClick = (href: string) => {
53
- activeHref.value = href;
54
- };
55
-
56
- /**
57
- * Sync with route changes (for programmatic navigation)
58
- * Re-runs the route matching logic when route changes
59
- */
60
- const syncWithRoute = (navItems: Array<{ href?: string }>, routePath: string) => {
61
- const items = navItems.filter((item): item is { href: string } => Boolean(item.href));
62
-
63
- // Try exact match first
64
- const exact = items.find((item) => routePath === item.href);
65
- if (exact) {
66
- activeHref.value = exact.href;
67
- return;
68
- }
69
-
70
- // Fall back to longest prefix match
71
- const prefixMatches = items
72
- .filter((item) => routePath.startsWith(item.href + "/"))
73
- .sort((a, b) => b.href.length - a.href.length);
74
-
75
- if (prefixMatches.length > 0) {
76
- activeHref.value = prefixMatches[0]!.href;
77
- }
78
- // Don't reset to null if no matches - preserve existing active state
79
- };
80
-
81
- /**
82
- * Reset store state (useful for testing or when navigation data changes)
83
- */
84
- const reset = () => {
85
- activeHref.value = null;
86
- isInitialized.value = false;
87
- };
88
-
89
- return {
90
- // State
91
- activeHref,
92
- isInitialized,
93
-
94
- // Getters
95
- currentActiveHref,
96
-
97
- // Actions
98
- setActiveHref,
99
- initializeFromRoute,
100
- handleNavLinkClick,
101
- syncWithRoute,
102
- reset,
103
- };
104
- },
105
- {
106
- persist: {
107
- storage: piniaPluginPersistedstate.sessionStorage(),
108
- // Only persist the active href, not initialization state
109
- // (we want to re-initialize on page refresh to handle route changes)
110
- pick: ["activeHref"],
111
- },
112
- }
113
- );