srcdev-nuxt-components 9.1.3 → 9.1.5

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.
@@ -23,6 +23,9 @@
23
23
  {{ item.text }}
24
24
  </NuxtLink>
25
25
  </li>
26
+ <li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__hovered"></div></li>
27
+ <li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active"></div></li>
28
+ <li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active-indicator"></div></li>
26
29
  </ul>
27
30
 
28
31
  <InputButtonCore
@@ -79,6 +82,9 @@
79
82
  {{ item.text }}
80
83
  </NuxtLink>
81
84
  </li>
85
+ <li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__hovered"></div></li>
86
+ <li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active"></div></li>
87
+ <li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active-indicator"></div></li>
82
88
  </ul>
83
89
  </div>
84
90
  </div>
@@ -108,17 +114,17 @@ const isLoaded = useState("site-nav-loaded", () => false);
108
114
  const isMenuOpen = ref(false);
109
115
 
110
116
  // Stored natural width of the list — used when the list is not in the DOM
111
- const navListNaturalWidth = ref(0);
117
+ let navListNaturalWidth = 0;
112
118
 
113
119
  const checkOverflow = () => {
114
120
  if (!navRef.value) return;
115
121
 
116
122
  // Measure and store the list width whenever it's in the DOM
117
123
  if (navListRef.value) {
118
- navListNaturalWidth.value = navListRef.value.scrollWidth;
124
+ navListNaturalWidth = navListRef.value.scrollWidth;
119
125
  }
120
126
 
121
- isCollapsed.value = navListNaturalWidth.value > navRef.value.clientWidth;
127
+ isCollapsed.value = navListNaturalWidth > navRef.value.clientWidth;
122
128
  };
123
129
 
124
130
  const toggleMenu = () => {
@@ -137,36 +143,43 @@ const NAV_DECORATOR_DURATION = 200;
137
143
  let navSnapTimer: ReturnType<typeof setTimeout> | null = null;
138
144
  let panelSnapTimer: ReturnType<typeof setTimeout> | null = null;
139
145
 
140
- const currentActiveNavLink = ref<HTMLElement | null>(null);
141
- const currentHoveredNavLink = ref<HTMLElement | null>(null);
142
- const previousHoveredNavLink = ref<HTMLElement | null>(null);
146
+ let currentActiveNavLink: HTMLElement | null = null;
147
+ let currentHoveredNavLink: HTMLElement | null = null;
148
+ let previousHoveredNavLink: HTMLElement | null = null;
143
149
 
144
- const getNavLinks = () =>
145
- navListRef.value ? Array.from(navListRef.value.querySelectorAll<HTMLElement>("[data-nav-item]")) : [];
150
+ // Cached after first init — invalidated when nav list is re-rendered (collapse toggle)
151
+ let cachedNavLinks: HTMLElement[] = [];
152
+
153
+ const getNavLinks = (): HTMLElement[] => {
154
+ if (cachedNavLinks.length) return cachedNavLinks;
155
+ if (!navListRef.value) return [];
156
+ cachedNavLinks = Array.from(navListRef.value.querySelectorAll<HTMLElement>("[data-nav-item]"));
157
+ return cachedNavLinks;
158
+ };
146
159
 
147
160
  const setFinalNavActivePositions = (instant = false) => {
148
- if (!navListRef.value || !currentActiveNavLink.value) return;
161
+ if (!navListRef.value || !currentActiveNavLink) return;
149
162
  const list = navListRef.value;
150
- const el = currentActiveNavLink.value;
163
+ const el = currentActiveNavLink;
151
164
  list.style.setProperty("--_transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
152
165
  list.style.setProperty("--_x-active", el.offsetLeft + "px");
153
166
  list.style.setProperty("--_width-active", String(el.offsetWidth / list.offsetWidth));
154
167
  };
155
168
 
156
169
  const setFinalNavHoveredPositions = (instant = false) => {
157
- if (!navListRef.value || !currentHoveredNavLink.value) return;
170
+ if (!navListRef.value || !currentHoveredNavLink) return;
158
171
  const list = navListRef.value;
159
- const el = currentHoveredNavLink.value;
172
+ const el = currentHoveredNavLink;
160
173
  list.style.setProperty("--_transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
161
174
  list.style.setProperty("--_x-hovered", el.offsetLeft + "px");
162
175
  list.style.setProperty("--_width-hovered", String(el.offsetWidth / list.offsetWidth));
163
176
  };
164
177
 
165
178
  const moveNavHoveredIndicator = () => {
166
- if (!navListRef.value || !currentHoveredNavLink.value || !previousHoveredNavLink.value) return;
179
+ if (!navListRef.value || !currentHoveredNavLink || !previousHoveredNavLink) return;
167
180
  const list = navListRef.value;
168
- const curr = currentHoveredNavLink.value;
169
- const prev = previousHoveredNavLink.value;
181
+ const curr = currentHoveredNavLink;
182
+ const prev = previousHoveredNavLink;
170
183
  list.style.setProperty("--_transition-duration", NAV_DECORATOR_DURATION + "ms");
171
184
  const isMovingRight = prev.compareDocumentPosition(curr) === 4;
172
185
  let transitionWidth: number;
@@ -187,23 +200,23 @@ const moveNavHoveredIndicator = () => {
187
200
  const handleNavLinkClick = (event: MouseEvent) => {
188
201
  const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
189
202
  if (!target) return;
190
- currentActiveNavLink.value = target;
191
- currentHoveredNavLink.value = target;
192
- previousHoveredNavLink.value = target;
203
+ currentActiveNavLink = target;
204
+ currentHoveredNavLink = target;
205
+ previousHoveredNavLink = target;
193
206
  };
194
207
 
195
208
  const handleNavHover = (event: MouseEvent) => {
196
209
  const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
197
- if (!target || target === currentHoveredNavLink.value) return;
198
- previousHoveredNavLink.value = currentHoveredNavLink.value;
199
- currentHoveredNavLink.value = target;
210
+ if (!target || target === currentHoveredNavLink) return;
211
+ previousHoveredNavLink = currentHoveredNavLink;
212
+ currentHoveredNavLink = target;
200
213
  moveNavHoveredIndicator();
201
214
  };
202
215
 
203
216
  const resetHoverNavToActive = () => {
204
- if (!currentActiveNavLink.value || currentHoveredNavLink.value === currentActiveNavLink.value) return;
205
- previousHoveredNavLink.value = currentHoveredNavLink.value;
206
- currentHoveredNavLink.value = currentActiveNavLink.value;
217
+ if (!currentActiveNavLink || currentHoveredNavLink === currentActiveNavLink) return;
218
+ previousHoveredNavLink = currentHoveredNavLink;
219
+ currentHoveredNavLink = currentActiveNavLink;
207
220
  moveNavHoveredIndicator();
208
221
  };
209
222
 
@@ -212,71 +225,66 @@ const initNavDecorators = () => {
212
225
  const links = getNavLinks();
213
226
  if (!links.length) return;
214
227
 
215
- // Cancel any in-flight snap timer before resetting positions
216
228
  if (navSnapTimer !== null) {
217
229
  clearTimeout(navSnapTimer);
218
230
  navSnapTimer = null;
219
231
  }
220
232
 
221
- navListRef.value.querySelectorAll(".nav-indicator-li").forEach((el) => el.remove());
222
-
223
- const activeLink = links.find((el) => el.classList.contains("router-link-active")) ?? links[0];
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];
224
237
  if (!activeLink) return;
225
238
 
226
- currentActiveNavLink.value = activeLink;
227
- currentHoveredNavLink.value = activeLink;
228
- previousHoveredNavLink.value = activeLink;
239
+ currentActiveNavLink = activeLink;
240
+ currentHoveredNavLink = activeLink;
241
+ previousHoveredNavLink = activeLink;
229
242
 
230
243
  setFinalNavActivePositions(true);
231
244
  setFinalNavHoveredPositions(true);
232
-
233
- // Wrap each indicator in a <li> so the <ul> contains only valid children
234
- ["nav__active-indicator", "nav__active", "nav__hovered"].forEach((cls) => {
235
- const li = document.createElement("li");
236
- li.classList.add("nav-indicator-li");
237
- li.setAttribute("aria-hidden", "true");
238
- li.setAttribute("role", "none");
239
- const div = document.createElement("div");
240
- div.classList.add(cls);
241
- li.appendChild(div);
242
- navListRef.value!.appendChild(li);
243
- });
244
245
  };
245
246
 
246
247
  // ─── Panel decorators (y-axis active / hover indicators) ─────────────────────
247
248
 
248
249
  const panelListRef = ref<HTMLUListElement | null>(null);
249
250
 
250
- const currentActivePanelLink = ref<HTMLElement | null>(null);
251
- const currentHoveredPanelLink = ref<HTMLElement | null>(null);
252
- const previousHoveredPanelLink = ref<HTMLElement | null>(null);
251
+ let currentActivePanelLink: HTMLElement | null = null;
252
+ let currentHoveredPanelLink: HTMLElement | null = null;
253
+ let previousHoveredPanelLink: HTMLElement | null = null;
253
254
 
254
- const getPanelLinks = () =>
255
- panelListRef.value ? Array.from(panelListRef.value.querySelectorAll<HTMLElement>("[data-panel-nav-item]")) : [];
255
+ // Cached after first init — invalidated when panel list is re-rendered (menu open/close)
256
+ let cachedPanelLinks: HTMLElement[] = [];
257
+
258
+ const getPanelLinks = (): HTMLElement[] => {
259
+ if (cachedPanelLinks.length) return cachedPanelLinks;
260
+ if (!panelListRef.value) return [];
261
+ cachedPanelLinks = Array.from(panelListRef.value.querySelectorAll<HTMLElement>("[data-panel-nav-item]"));
262
+ return cachedPanelLinks;
263
+ };
256
264
 
257
265
  const setFinalPanelActivePositions = (instant = false) => {
258
- if (!panelListRef.value || !currentActivePanelLink.value) return;
266
+ if (!panelListRef.value || !currentActivePanelLink) return;
259
267
  const list = panelListRef.value;
260
- const el = currentActivePanelLink.value;
268
+ const el = currentActivePanelLink;
261
269
  list.style.setProperty("--_panel-transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
262
270
  list.style.setProperty("--_panel-y-active", el.offsetTop + "px");
263
271
  list.style.setProperty("--_panel-height-active", String(el.offsetHeight / list.offsetHeight));
264
272
  };
265
273
 
266
274
  const setFinalPanelHoveredPositions = (instant = false) => {
267
- if (!panelListRef.value || !currentHoveredPanelLink.value) return;
275
+ if (!panelListRef.value || !currentHoveredPanelLink) return;
268
276
  const list = panelListRef.value;
269
- const el = currentHoveredPanelLink.value;
277
+ const el = currentHoveredPanelLink;
270
278
  list.style.setProperty("--_panel-transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
271
279
  list.style.setProperty("--_panel-y-hovered", el.offsetTop + "px");
272
280
  list.style.setProperty("--_panel-height-hovered", String(el.offsetHeight / list.offsetHeight));
273
281
  };
274
282
 
275
283
  const movePanelHoveredIndicator = () => {
276
- if (!panelListRef.value || !currentHoveredPanelLink.value || !previousHoveredPanelLink.value) return;
284
+ if (!panelListRef.value || !currentHoveredPanelLink || !previousHoveredPanelLink) return;
277
285
  const list = panelListRef.value;
278
- const curr = currentHoveredPanelLink.value;
279
- const prev = previousHoveredPanelLink.value;
286
+ const curr = currentHoveredPanelLink;
287
+ const prev = previousHoveredPanelLink;
280
288
  list.style.setProperty("--_panel-transition-duration", NAV_DECORATOR_DURATION + "ms");
281
289
  const isMovingDown = prev.compareDocumentPosition(curr) === 4;
282
290
  let transitionHeight: number;
@@ -297,28 +305,29 @@ const movePanelHoveredIndicator = () => {
297
305
  const handlePanelLinkClick = (event: MouseEvent) => {
298
306
  const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
299
307
  if (!target) return;
300
- currentActivePanelLink.value = target;
301
- currentHoveredPanelLink.value = target;
302
- previousHoveredPanelLink.value = target;
308
+ currentActivePanelLink = target;
309
+ currentHoveredPanelLink = target;
310
+ previousHoveredPanelLink = target;
303
311
  };
304
312
 
305
313
  const handlePanelHover = (event: MouseEvent) => {
306
314
  const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
307
- if (!target || target === currentHoveredPanelLink.value) return;
308
- previousHoveredPanelLink.value = currentHoveredPanelLink.value;
309
- currentHoveredPanelLink.value = target;
315
+ if (!target || target === currentHoveredPanelLink) return;
316
+ previousHoveredPanelLink = currentHoveredPanelLink;
317
+ currentHoveredPanelLink = target;
310
318
  movePanelHoveredIndicator();
311
319
  };
312
320
 
313
321
  const resetHoverPanelToActive = () => {
314
- if (!currentActivePanelLink.value || currentHoveredPanelLink.value === currentActivePanelLink.value) return;
315
- previousHoveredPanelLink.value = currentHoveredPanelLink.value;
316
- currentHoveredPanelLink.value = currentActivePanelLink.value;
322
+ if (!currentActivePanelLink || currentHoveredPanelLink === currentActivePanelLink) return;
323
+ previousHoveredPanelLink = currentHoveredPanelLink;
324
+ currentHoveredPanelLink = currentActivePanelLink;
317
325
  movePanelHoveredIndicator();
318
326
  };
319
327
 
320
328
  const initPanelDecorators = () => {
321
329
  if (!panelListRef.value) return;
330
+ cachedPanelLinks = []; // panel DOM is re-rendered each open — invalidate cache
322
331
  const links = getPanelLinks();
323
332
  if (!links.length) return;
324
333
 
@@ -327,27 +336,18 @@ const initPanelDecorators = () => {
327
336
  panelSnapTimer = null;
328
337
  }
329
338
 
330
- panelListRef.value.querySelectorAll(".nav-indicator-li").forEach((el) => el.remove());
331
-
332
- const activeLink = links.find((el) => el.classList.contains("router-link-active")) ?? links[0];
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];
333
343
  if (!activeLink) return;
334
344
 
335
- currentActivePanelLink.value = activeLink;
336
- currentHoveredPanelLink.value = activeLink;
337
- previousHoveredPanelLink.value = activeLink;
345
+ currentActivePanelLink = activeLink;
346
+ currentHoveredPanelLink = activeLink;
347
+ previousHoveredPanelLink = activeLink;
338
348
 
339
349
  setFinalPanelActivePositions(true);
340
350
  setFinalPanelHoveredPositions(true);
341
- ["nav__active-indicator", "nav__active", "nav__hovered"].forEach((cls) => {
342
- const li = document.createElement("li");
343
- li.classList.add("nav-indicator-li");
344
- li.setAttribute("aria-hidden", "true");
345
- li.setAttribute("role", "none");
346
- const div = document.createElement("div");
347
- div.classList.add(cls);
348
- li.appendChild(div);
349
- panelListRef.value!.appendChild(li);
350
- });
351
351
  };
352
352
 
353
353
  // ─────────────────────────────────────────────────────────────────────────────
@@ -389,6 +389,7 @@ onMounted(async () => {
389
389
 
390
390
  watch(isCollapsed, async (collapsed) => {
391
391
  if (!collapsed) {
392
+ cachedNavLinks = []; // nav <ul> was re-rendered — invalidate cache
392
393
  await nextTick();
393
394
  initNavDecorators();
394
395
  }
@@ -437,23 +438,23 @@ watch(
437
438
  /* Decorators — horizontal nav */
438
439
  --_decorator-hovered-bg: transparent;
439
440
  --_decorator-active-bg: transparent;
440
- --_decorator-indicator-color: var(--site-nav-decorator-indicator-color, var(--rose-05, currentColor));
441
+ --_decorator-indicator-color: var(--site-nav-decorator-indicator-color, var(--slate-01, currentColor));
441
442
 
442
443
  /* Decorators — panel */
443
444
  --_panel-decorator-hovered-bg: var(--site-nav-panel-decorator-hovered-bg, transparent);
444
445
  --_panel-decorator-active-bg: var(--site-nav-panel-decorator-active-bg, transparent);
445
- --_panel-decorator-indicator-color: var(--site-nav-panel-decorator-indicator-color, var(--rose-05, currentColor));
446
+ --_panel-decorator-indicator-color: var(--site-nav-panel-decorator-indicator-color, var(--slate-01, currentColor));
446
447
  --_panel-indicator-left: var(--site-nav-panel-indicator-left, 0);
447
448
  --_panel-indicator-right: var(--site-nav-panel-indicator-right, auto);
448
449
 
449
450
  /* Horizontal nav */
450
- --_link-color: var(--site-nav-link-color, var(--warm-01, currentColor));
451
- --_link-hover-color: var(--site-nav-link-hover-color, var(--rose-04, var(--_link-color)));
452
- --_link-active-color: var(--site-nav-link-active-color, var(--rose-05, var(--_link-color)));
451
+ --_link-color: var(--site-nav-link-color, var(--slate-01, currentColor));
452
+ --_link-hover-color: var(--site-nav-link-hover-color, var(--slate-04, var(--_link-color)));
453
+ --_link-active-color: var(--site-nav-link-active-color, var(--slate-01, var(--_link-color)));
453
454
  --_link-size: var(--site-nav-link-size, 1.6rem);
454
455
  --_link-tracking: var(--site-nav-link-tracking, 0.06em);
455
456
  --_link-weight: var(--site-nav-link-weight, 400);
456
- --_link-accent: var(--site-nav-link-accent, var(--rose-05, currentColor));
457
+ --_link-accent: var(--site-nav-link-accent, var(--slate-01, currentColor));
457
458
  --_nav-gap: var(--site-nav-gap, 2.2rem);
458
459
  --_nav-transition: var(--site-nav-transition, 250ms ease);
459
460
 
@@ -461,12 +462,15 @@ watch(
461
462
  --_panel-bg: var(--site-nav-panel-bg, var(--page-bg, #1a1614));
462
463
  --_panel-border-color: var(
463
464
  --site-nav-panel-border-color,
464
- color-mix(in oklch, var(--rose-05, #c0847a) 35%, transparent)
465
+ color-mix(in oklch, var(--slate-01, #c0847a) 35%, transparent)
466
+ );
467
+ --_panel-item-border: var(
468
+ --site-nav-panel-item-border,
469
+ color-mix(in oklch, var(--slate-01, white) 8%, transparent)
465
470
  );
466
- --_panel-item-border: var(--site-nav-panel-item-border, color-mix(in oklch, var(--warm-01, white) 8%, transparent));
467
- --_panel-link-color: var(--site-nav-panel-link-color, var(--warm-01, currentColor));
468
- --_panel-link-hover-color: var(--site-nav-panel-link-hover-color, var(--rose-04, var(--_panel-link-color)));
469
- --_panel-link-active-color: var(--site-nav-panel-link-active-color, var(--rose-05, var(--_panel-link-color)));
471
+ --_panel-link-color: var(--site-nav-panel-link-color, var(--slate-01, currentColor));
472
+ --_panel-link-hover-color: var(--site-nav-panel-link-hover-color, var(--slate-04, var(--_panel-link-color)));
473
+ --_panel-link-active-color: var(--site-nav-panel-link-active-color, var(--slate-01, var(--_panel-link-color)));
470
474
  --_panel-padding-block: var(--site-nav-panel-padding-block, 1.4rem);
471
475
  --_panel-padding-inline: var(--site-nav-panel-padding-inline, 1.5rem);
472
476
  --_panel-slide-duration: var(--site-nav-panel-slide-duration, 350ms);
@@ -476,7 +480,7 @@ watch(
476
480
  --_burger-bar-width: var(--site-nav-burger-width, 22px);
477
481
  --_burger-bar-height: var(--site-nav-burger-height, 1.5px);
478
482
  --_burger-bar-gap: var(--site-nav-burger-gap, 5px);
479
- --_burger-color: var(--site-nav-burger-color, var(--warm-01, currentColor));
483
+ --_burger-color: var(--site-nav-burger-color, var(--slate-01, currentColor));
480
484
  --_burger-transition: var(--site-nav-burger-transition, 300ms ease);
481
485
 
482
486
  /* ─────────────────────────────────────────────────────────────────── */
@@ -576,7 +580,7 @@ watch(
576
580
  outline: none;
577
581
  }
578
582
 
579
- &.router-link-active {
583
+ &.router-link-exact-active {
580
584
  color: var(--_link-active-color);
581
585
  }
582
586
  }
@@ -769,7 +773,7 @@ watch(
769
773
  outline: none;
770
774
  }
771
775
 
772
- &.router-link-active {
776
+ &.router-link-exact-active {
773
777
  color: var(--_panel-link-active-color);
774
778
  }
775
779
  }
@@ -12,14 +12,14 @@ exports[`SiteNavigation > renders correct HTML structure 1`] = `
12
12
  <li class=""><a href="/contact" class="site-nav-link" data-nav-item="">
13
13
  <!--v-if--> Contact
14
14
  </a></li>
15
- <li class="nav-indicator-li" aria-hidden="true" role="none">
16
- <div class="nav__active-indicator"></div>
15
+ <li aria-hidden="true" role="none" class="nav-indicator-li">
16
+ <div class="nav__hovered"></div>
17
17
  </li>
18
- <li class="nav-indicator-li" aria-hidden="true" role="none">
18
+ <li aria-hidden="true" role="none" class="nav-indicator-li">
19
19
  <div class="nav__active"></div>
20
20
  </li>
21
- <li class="nav-indicator-li" aria-hidden="true" role="none">
22
- <div class="nav__hovered"></div>
21
+ <li aria-hidden="true" role="none" class="nav-indicator-li">
22
+ <div class="nav__active-indicator"></div>
23
23
  </li>
24
24
  </ul>
25
25
  <!--v-if-->
@@ -1,10 +1,5 @@
1
1
  <template>
2
- <component
3
- :is="tag"
4
- class="services-section"
5
- :class="[elementClasses]"
6
- :aria-labelledby="ariaLabelledby"
7
- >
2
+ <component :is="tag" class="services-section" :class="[elementClasses]" :aria-labelledby="ariaLabelledby">
8
3
  <div class="services-section__grid" :class="{ 'services-section__grid--reverse': reverse }">
9
4
  <div class="image-wrapper">
10
5
  <NuxtImg :src="serviceData.image" :alt="serviceData.title" :loading="imageLoading" class="image" />
@@ -67,8 +62,7 @@
67
62
  :show-connectors="true"
68
63
  :item-count="serviceData.process.length"
69
64
  >
70
- <template v-for="(item, index) in serviceData.process" :key="index" #[`item-${index}`]>
71
- <p class="page-body-normal">{{ item }}</p>
65
+ <template v-for="(item, i) in serviceData.process" :key="i" #[`item-${i}`]>
72
66
  <p class="page-body-normal">{{ item }}</p>
73
67
  </template>
74
68
  </StepperList>
@@ -83,11 +77,10 @@
83
77
  />
84
78
 
85
79
  <StepperList v-if="!isSummary" :connected="false" :item-count="serviceData.idealFor.length">
86
- <template v-for="(_, index) in serviceData.idealFor" :key="index" #[`indicator-${index}`]>
80
+ <template v-for="(_, i) in serviceData.idealFor" :key="i" #[`indicator-${i}`]>
87
81
  <Icon name="mdi:checkbox-marked-circle-outline" class="indicator-icon" />
88
82
  </template>
89
- <template v-for="(item, index) in serviceData.idealFor" :key="index" #[`item-${index}`]>
90
- <p class="page-body-normal">{{ item }}</p>
83
+ <template v-for="(item, i) in serviceData.idealFor" :key="i" #[`item-${i}`]>
91
84
  <p class="page-body-normal">{{ item }}</p>
92
85
  </template>
93
86
  </StepperList>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="site-nav-demo-layout">
3
3
  <header class="site-nav-demo-header">
4
- <NuxtLink to="/ui/navigation/site-navigation" class="site-nav-demo-logo">SiteNav Demo</NuxtLink>
4
+ <NuxtLink to="/" class="site-nav-demo-logo">SiteNav Demo</NuxtLink>
5
5
  <SiteNavigation :nav-item-data="navItemData" nav-align="right" />
6
6
  </header>
7
7
  <main class="site-nav-demo-main">
@@ -66,4 +66,121 @@ const navItemData: NavItemData = {
66
66
  width: 100%;
67
67
  }
68
68
  }
69
+
70
+ /* Shared demo page styles */
71
+ .snav-demo-page {
72
+ h1,
73
+ h2,
74
+ h3 {
75
+ margin-block-end: 1.6rem;
76
+ }
77
+ h2 {
78
+ margin-block-start: 4rem;
79
+ }
80
+ > p {
81
+ margin-block-end: 2.4rem;
82
+ line-height: 1.6;
83
+ }
84
+ ul {
85
+ padding-inline-start: 2rem;
86
+ margin-block-end: 2.4rem;
87
+ line-height: 2;
88
+ }
89
+ }
90
+
91
+ .snav-demo-hero {
92
+ width: 100%;
93
+ height: auto;
94
+ aspect-ratio: 2 / 1;
95
+ object-fit: cover;
96
+ border-radius: 0.8rem;
97
+ display: block;
98
+ margin-block-end: 2.4rem;
99
+ }
100
+
101
+ .snav-demo-grid {
102
+ display: grid;
103
+ grid-template-columns: repeat(3, 1fr);
104
+ gap: 1.6rem;
105
+
106
+ &--wide {
107
+ grid-template-columns: repeat(2, 1fr);
108
+ }
109
+
110
+ @media (max-width: 600px) {
111
+ grid-template-columns: repeat(2, 1fr);
112
+ &--wide {
113
+ grid-template-columns: 1fr;
114
+ }
115
+ }
116
+ }
117
+
118
+ .snav-demo-img {
119
+ width: 100%;
120
+ height: auto;
121
+ aspect-ratio: 3 / 2;
122
+ object-fit: cover;
123
+ border-radius: 0.4rem;
124
+ display: block;
125
+ }
126
+
127
+ .snav-demo-split {
128
+ display: grid;
129
+ grid-template-columns: 1fr 1fr;
130
+ gap: 3.2rem;
131
+ align-items: start;
132
+ margin-block-end: 4rem;
133
+
134
+ &__text {
135
+ h2,
136
+ h3 {
137
+ margin-block-start: 2.4rem;
138
+ }
139
+ p {
140
+ line-height: 1.6;
141
+ margin-block-end: 1.6rem;
142
+ }
143
+ }
144
+
145
+ &__img {
146
+ width: 100%;
147
+ height: auto;
148
+ aspect-ratio: 3 / 4;
149
+ object-fit: cover;
150
+ border-radius: 0.4rem;
151
+ display: block;
152
+ }
153
+
154
+ @media (max-width: 650px) {
155
+ grid-template-columns: 1fr;
156
+ }
157
+ }
158
+
159
+ .snav-demo-services {
160
+ display: grid;
161
+ grid-template-columns: repeat(3, 1fr);
162
+ gap: 2.4rem;
163
+
164
+ @media (max-width: 650px) {
165
+ grid-template-columns: 1fr;
166
+ }
167
+ }
168
+
169
+ .snav-demo-service-card {
170
+ border-radius: 0.8rem;
171
+ overflow: hidden;
172
+ background: oklch(100% 0 0 / 5%);
173
+
174
+ &__body {
175
+ padding: 1.6rem;
176
+ h3 {
177
+ margin-block-end: 0.8rem;
178
+ }
179
+ p {
180
+ font-size: 1.4rem;
181
+ line-height: 1.6;
182
+ opacity: 0.8;
183
+ }
184
+ }
185
+ }
69
186
  </style>
@@ -1,7 +1,32 @@
1
1
  <template>
2
- <div>
2
+ <div class="snav-demo-page">
3
3
  <h1>About</h1>
4
- <p>This is the About page. The active indicator in the navigation above should be sitting under "About".</p>
4
+ <p>This is the About page. The active indicator in the navigation above should be sitting under "About". Lots of images below to simulate a content-heavy page.</p>
5
+
6
+ <div class="snav-demo-split">
7
+ <div class="snav-demo-split__text">
8
+ <h2>Our Story</h2>
9
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
10
+ <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
11
+ <h2>Our Team</h2>
12
+ <p>We are a team of dedicated professionals committed to delivering the best experience. Our combined expertise spans design, development, and user experience research.</p>
13
+ <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis.</p>
14
+ </div>
15
+ <NuxtImg src="/images/rotating-carousel/image-1.webp" alt="About hero" width="600" height="800" loading="eager" class="snav-demo-split__img" />
16
+ </div>
17
+
18
+ <h2>Gallery</h2>
19
+ <div class="snav-demo-grid">
20
+ <NuxtImg v-for="n in 9" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`About gallery ${n}`" width="600" height="400" loading="lazy" class="snav-demo-img" />
21
+ </div>
22
+
23
+ <h2>Behind the scenes</h2>
24
+ <div class="snav-demo-grid snav-demo-grid--wide">
25
+ <NuxtImg src="/images/banners/banner-ginger.jpeg" alt="Behind the scenes 1" width="800" height="500" loading="lazy" class="snav-demo-img" />
26
+ <NuxtImg src="/images/banners/banner-mid-brown.webp" alt="Behind the scenes 2" width="800" height="500" loading="lazy" class="snav-demo-img" />
27
+ <NuxtImg src="/images/page/hero/hero-dark.jpg" alt="Behind the scenes 3" width="800" height="500" loading="lazy" class="snav-demo-img" />
28
+ <NuxtImg src="/images/page/hero/hero-red.jpg" alt="Behind the scenes 4" width="800" height="500" loading="lazy" class="snav-demo-img" />
29
+ </div>
5
30
  </div>
6
31
  </template>
7
32
 
@@ -1,7 +1,35 @@
1
1
  <template>
2
- <div>
2
+ <div class="snav-demo-page">
3
3
  <h1>Contact</h1>
4
4
  <p>This is the Contact page. The active indicator in the navigation above should be sitting under "Contact".</p>
5
+
6
+ <div class="snav-demo-split">
7
+ <div class="snav-demo-split__text">
8
+ <h2>Get in Touch</h2>
9
+ <p>We'd love to hear from you. Whether you have a question about our services, want to book an appointment, or just want to say hello — our door is always open.</p>
10
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
11
+ <h3>Opening Hours</h3>
12
+ <ul>
13
+ <li>Monday – Friday: 9am – 7pm</li>
14
+ <li>Saturday: 9am – 5pm</li>
15
+ <li>Sunday: Closed</li>
16
+ </ul>
17
+ <h3>Address</h3>
18
+ <p>123 Example Street<br />London, UK<br />EC1A 1BB</p>
19
+ </div>
20
+ <NuxtImg src="/images/rotating-carousel/55-woman-garden-backdrop.webp" alt="Contact hero" width="600" height="800" loading="eager" class="snav-demo-split__img" />
21
+ </div>
22
+
23
+ <h2>Find Us</h2>
24
+ <div class="snav-demo-grid snav-demo-grid--wide">
25
+ <NuxtImg src="/images/rotating-carousel/image-5.webp" alt="Location 1" width="800" height="500" loading="lazy" class="snav-demo-img" />
26
+ <NuxtImg src="/images/rotating-carousel/image-6.webp" alt="Location 2" width="800" height="500" loading="lazy" class="snav-demo-img" />
27
+ </div>
28
+
29
+ <h2>Instagram</h2>
30
+ <div class="snav-demo-grid">
31
+ <NuxtImg v-for="n in [1, 2, 3, 7, 8, 9]" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`Instagram post ${n}`" width="600" height="600" loading="lazy" class="snav-demo-img snav-demo-img--square" />
32
+ </div>
5
33
  </div>
6
34
  </template>
7
35
 
@@ -1,7 +1,34 @@
1
1
  <template>
2
- <div>
2
+ <div class="snav-demo-page">
3
3
  <h1>Home</h1>
4
- <p>Welcome to the SiteNavigation component demo. Resize the window to see the burger menu collapse, and navigate between pages to see the active indicator update.</p>
4
+ <p>Welcome to the SiteNavigation component demo. Resize the window to see the burger menu collapse, and navigate between pages to see the active indicator update. This page is intentionally heavy with images to stress-test nav behaviour during loading.</p>
5
+
6
+ <NuxtImg
7
+ src="/images/banners/banner-ginger.jpeg"
8
+ alt="Hero banner"
9
+ width="1200"
10
+ height="600"
11
+ loading="eager"
12
+ class="snav-demo-hero"
13
+ />
14
+
15
+ <h2>Featured Gallery</h2>
16
+ <div class="snav-demo-grid">
17
+ <NuxtImg v-for="n in 9" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`Gallery image ${n}`" width="600" height="400" loading="lazy" class="snav-demo-img" />
18
+ </div>
19
+
20
+ <h2>More Images</h2>
21
+ <div class="snav-demo-grid snav-demo-grid--wide">
22
+ <NuxtImg src="/images/banners/banner-light-brunette.webp" alt="Banner light brunette" width="800" height="500" loading="lazy" class="snav-demo-img" />
23
+ <NuxtImg src="/images/banners/banner-mid-brown.webp" alt="Banner mid brown" width="800" height="500" loading="lazy" class="snav-demo-img" />
24
+ <NuxtImg src="/images/rotating-carousel/35-light-brown-highlights.webp" alt="Light brown highlights" width="800" height="500" loading="lazy" class="snav-demo-img" />
25
+ <NuxtImg src="/images/rotating-carousel/55-woman-garden-backdrop.webp" alt="Woman garden backdrop" width="800" height="500" loading="lazy" class="snav-demo-img" />
26
+ </div>
27
+
28
+ <h2>Hero Images</h2>
29
+ <div class="snav-demo-grid">
30
+ <NuxtImg v-for="hero in ['hero-blonde', 'hero-dark', 'hero-hair', 'hero-red']" :key="hero" :src="`/images/page/hero/${hero}.jpg`" :alt="hero" width="600" height="400" loading="lazy" class="snav-demo-img" />
31
+ </div>
5
32
  </div>
6
33
  </template>
7
34
 
@@ -1,7 +1,28 @@
1
1
  <template>
2
- <div>
2
+ <div class="snav-demo-page">
3
3
  <h1>Portfolio</h1>
4
- <p>This is the Portfolio page. The active indicator in the navigation above should be sitting under "Portfolio".</p>
4
+ <p>This is the Portfolio page. The active indicator in the navigation above should be sitting under "Portfolio". Maximum image load to stress-test navigation behaviour.</p>
5
+
6
+ <h2>Spotlight Work</h2>
7
+ <div class="snav-demo-grid snav-demo-grid--dense">
8
+ <NuxtImg v-for="n in 9" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`Portfolio piece ${n}`" width="600" height="600" loading="lazy" class="snav-demo-img snav-demo-img--square" />
9
+ </div>
10
+
11
+ <h2>Featured Looks</h2>
12
+ <div class="snav-demo-grid snav-demo-grid--wide">
13
+ <NuxtImg src="/images/banners/banner-ginger.jpeg" alt="Ginger look" width="800" height="500" loading="lazy" class="snav-demo-img" />
14
+ <NuxtImg src="/images/banners/banner-light-brunette.webp" alt="Light brunette look" width="800" height="500" loading="lazy" class="snav-demo-img" />
15
+ <NuxtImg src="/images/banners/banner-mid-brown.webp" alt="Mid brown look" width="800" height="500" loading="lazy" class="snav-demo-img" />
16
+ <NuxtImg src="/images/rotating-carousel/35-light-brown-highlights.webp" alt="Light brown highlights" width="800" height="500" loading="lazy" class="snav-demo-img" />
17
+ <NuxtImg src="/images/rotating-carousel/55-woman-garden-backdrop.webp" alt="Garden backdrop" width="800" height="500" loading="lazy" class="snav-demo-img" />
18
+ <NuxtImg src="/images/rotating-carousel/indian-pink-stripe.webp" alt="Pink stripe" width="800" height="500" loading="lazy" class="snav-demo-img" />
19
+ <NuxtImg src="/images/rotating-carousel/spanish-green-stripe.webp" alt="Green stripe" width="800" height="500" loading="lazy" class="snav-demo-img" />
20
+ </div>
21
+
22
+ <h2>Hero Series</h2>
23
+ <div class="snav-demo-grid">
24
+ <NuxtImg v-for="hero in ['hero-blonde', 'hero-dark', 'hero-hair', 'hero-red']" :key="hero" :src="`/images/page/hero/${hero}.jpg`" :alt="hero" width="600" height="400" loading="lazy" class="snav-demo-img" />
25
+ </div>
5
26
  </div>
6
27
  </template>
7
28
 
@@ -9,3 +30,14 @@
9
30
  definePageMeta({ layout: "site-navigation-demo" });
10
31
  useHead({ title: "SiteNavigation Demo — Portfolio" });
11
32
  </script>
33
+
34
+ <style lang="css">
35
+ .snav-demo-img--square {
36
+ aspect-ratio: 1 / 1;
37
+ }
38
+
39
+ .snav-demo-grid--dense {
40
+ grid-template-columns: repeat(3, 1fr);
41
+ gap: 0.8rem;
42
+ }
43
+ </style>
@@ -1,11 +1,42 @@
1
1
  <template>
2
- <div>
2
+ <div class="snav-demo-page">
3
3
  <h1>Services</h1>
4
4
  <p>This is the Services page. The active indicator in the navigation above should be sitting under "Services".</p>
5
+
6
+ <NuxtImg
7
+ src="/images/banners/banner-light-brunette.webp"
8
+ alt="Services banner"
9
+ width="1200"
10
+ height="500"
11
+ loading="eager"
12
+ class="snav-demo-hero"
13
+ />
14
+
15
+ <h2>What We Offer</h2>
16
+ <div class="snav-demo-services">
17
+ <div v-for="(service, i) in services" :key="i" class="snav-demo-service-card">
18
+ <NuxtImg :src="service.img" :alt="service.title" width="600" height="400" loading="lazy" class="snav-demo-img" />
19
+ <div class="snav-demo-service-card__body">
20
+ <h3>{{ service.title }}</h3>
21
+ <p>{{ service.description }}</p>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <h2>Our Work</h2>
27
+ <div class="snav-demo-grid">
28
+ <NuxtImg v-for="n in [4, 5, 6, 7, 8, 9]" :key="n" :src="`/images/rotating-carousel/image-${n}.webp`" :alt="`Work sample ${n}`" width="600" height="400" loading="lazy" class="snav-demo-img" />
29
+ </div>
5
30
  </div>
6
31
  </template>
7
32
 
8
33
  <script setup lang="ts">
9
34
  definePageMeta({ layout: "site-navigation-demo" });
10
35
  useHead({ title: "SiteNavigation Demo — Services" });
36
+
37
+ const services = [
38
+ { title: "Colour", img: "/images/rotating-carousel/image-1.webp", description: "From subtle highlights to bold transformations. Our colour specialists use only the finest products for vibrant, lasting results." },
39
+ { title: "Cut & Style", img: "/images/rotating-carousel/image-2.webp", description: "Precision cuts tailored to your face shape and lifestyle. Walk out feeling confident every single time." },
40
+ { title: "Treatment", img: "/images/rotating-carousel/image-3.webp", description: "Restore shine and health with our premium treatment range. Ideal for damaged, dry, or colour-treated hair." },
41
+ ];
11
42
  </script>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.1.3",
4
+ "version": "9.1.5",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",