srcdev-nuxt-components 9.1.0 → 9.1.1

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.
Files changed (20) hide show
  1. package/.claude/settings.json +2 -1
  2. package/.claude/skills/components/page-hero-highlights.md +60 -0
  3. package/.claude/skills/components/site-navigation.md +120 -0
  4. package/.claude/skills/index.md +1 -1
  5. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +780 -0
  6. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +335 -0
  7. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +328 -0
  8. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +30 -0
  9. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +36 -21
  10. package/app/components/04.templates/page-hero-highlights/PageHeroHighlightsHeader.vue +66 -0
  11. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +50 -3
  12. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +77 -0
  13. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +15 -7
  14. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +51 -0
  15. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +1 -1
  16. package/app/layouts/default.vue +1 -0
  17. package/app/pages/page-hero-highlights.vue +15 -11
  18. package/nuxt.config.ts +7 -0
  19. package/package.json +6 -6
  20. package/.claude/skills/components/treatment-consultant.md +0 -128
@@ -0,0 +1,780 @@
1
+ <template>
2
+ <nav
3
+ ref="navRef"
4
+ class="site-navigation"
5
+ :class="[
6
+ elementClasses,
7
+ `site-navigation--${navAlign}`,
8
+ { 'is-collapsed': isCollapsed, 'is-loaded': isLoaded, 'menu-open': isMenuOpen },
9
+ ]"
10
+ aria-label="Site navigation"
11
+ >
12
+ <ul
13
+ v-if="!isCollapsed || !isLoaded"
14
+ ref="navListRef"
15
+ class="site-nav-list"
16
+ @click="handleNavLinkClick"
17
+ @mouseleave="resetHoverNavToActive"
18
+ @mouseover="handleNavHover"
19
+ >
20
+ <li v-for="item in navItemData.main" :key="item.href" :class="item.cssName">
21
+ <NuxtLink :href="item.href" :external="item.isExternal || undefined" class="site-nav-link" data-nav-item>
22
+ <Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
23
+ {{ item.text }}
24
+ </NuxtLink>
25
+ </li>
26
+ </ul>
27
+
28
+ <InputButtonCore
29
+ v-if="isCollapsed && isLoaded"
30
+ class="site-nav-burger"
31
+ :class="{ 'is-open': isMenuOpen }"
32
+ variant="tertiary"
33
+ :button-text="isMenuOpen ? 'Close navigation menu' : 'Open navigation menu'"
34
+ :aria-expanded="String(isMenuOpen)"
35
+ aria-controls="site-nav-panel"
36
+ @click="toggleMenu"
37
+ >
38
+ <template #iconOnly>
39
+ <span class="burger-bar" aria-hidden="true"></span>
40
+ <span class="burger-bar" aria-hidden="true"></span>
41
+ <span class="burger-bar" aria-hidden="true"></span>
42
+ </template>
43
+ </InputButtonCore>
44
+
45
+ <Teleport to="body">
46
+ <div
47
+ v-if="isCollapsed && isLoaded"
48
+ class="site-nav-backdrop"
49
+ :class="{ 'is-open': isMenuOpen }"
50
+ aria-hidden="true"
51
+ @click="closeMenu"
52
+ ></div>
53
+ </Teleport>
54
+
55
+ <div
56
+ v-if="isCollapsed && isLoaded"
57
+ id="site-nav-panel"
58
+ class="site-nav-panel"
59
+ :class="{ 'is-open': isMenuOpen }"
60
+ :inert="!isMenuOpen ? true : undefined"
61
+ >
62
+ <div class="site-nav-panel-inner">
63
+ <ul
64
+ ref="panelListRef"
65
+ class="site-nav-panel-list"
66
+ @click="handlePanelLinkClick"
67
+ @mouseover="handlePanelHover"
68
+ @mouseleave="resetHoverPanelToActive"
69
+ >
70
+ <li v-for="item in navItemData.main" :key="item.href" :class="item.cssName">
71
+ <NuxtLink
72
+ :href="item.href"
73
+ :external="item.isExternal || undefined"
74
+ class="site-nav-panel-link"
75
+ data-panel-nav-item
76
+ @click="closeMenu"
77
+ >
78
+ <Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
79
+ {{ item.text }}
80
+ </NuxtLink>
81
+ </li>
82
+ </ul>
83
+ </div>
84
+ </div>
85
+ </nav>
86
+ </template>
87
+
88
+ <script setup lang="ts">
89
+ import { useResizeObserver, onClickOutside } from "@vueuse/core";
90
+ import type { NavItemData } from "~/types/components";
91
+
92
+ interface Props {
93
+ navItemData: NavItemData;
94
+ navAlign?: "left" | "center" | "right";
95
+ styleClassPassthrough?: string | string[];
96
+ }
97
+
98
+ const props = withDefaults(defineProps<Props>(), {
99
+ navAlign: "left",
100
+ styleClassPassthrough: () => [],
101
+ });
102
+
103
+ const navRef = ref<HTMLElement | null>(null);
104
+ const navListRef = ref<HTMLUListElement | null>(null);
105
+
106
+ const isCollapsed = ref(false);
107
+ const isLoaded = useState("site-nav-loaded", () => false);
108
+ const isMenuOpen = ref(false);
109
+
110
+ // Stored natural width of the list — used when the list is not in the DOM
111
+ const navListNaturalWidth = ref(0);
112
+
113
+ const checkOverflow = () => {
114
+ if (!navRef.value) return;
115
+
116
+ // Measure and store the list width whenever it's in the DOM
117
+ if (navListRef.value) {
118
+ navListNaturalWidth.value = navListRef.value.scrollWidth;
119
+ }
120
+
121
+ isCollapsed.value = navListNaturalWidth.value > navRef.value.clientWidth;
122
+ };
123
+
124
+ const toggleMenu = () => {
125
+ isMenuOpen.value = !isMenuOpen.value;
126
+ };
127
+
128
+ const closeMenu = () => {
129
+ isMenuOpen.value = false;
130
+ };
131
+
132
+ // ─── Nav decorators (active / hover indicators) ─────────────────────────────
133
+
134
+ const NAV_DECORATOR_DURATION = 200;
135
+
136
+ // Single pending snap timer — cancel the previous one when a new move starts
137
+ let navSnapTimer: ReturnType<typeof setTimeout> | null = null;
138
+ let panelSnapTimer: ReturnType<typeof setTimeout> | null = null;
139
+
140
+ const currentActiveNavLink = ref<HTMLElement | null>(null);
141
+ const currentHoveredNavLink = ref<HTMLElement | null>(null);
142
+ const previousHoveredNavLink = ref<HTMLElement | null>(null);
143
+
144
+ const getNavLinks = () =>
145
+ navListRef.value ? Array.from(navListRef.value.querySelectorAll<HTMLElement>("[data-nav-item]")) : [];
146
+
147
+ const setFinalNavActivePositions = (instant = false) => {
148
+ if (!navListRef.value || !currentActiveNavLink.value) return;
149
+ const list = navListRef.value;
150
+ const el = currentActiveNavLink.value;
151
+ list.style.setProperty("--_transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
152
+ list.style.setProperty("--_x-active", el.offsetLeft + "px");
153
+ list.style.setProperty("--_width-active", String(el.offsetWidth / list.offsetWidth));
154
+ };
155
+
156
+ const setFinalNavHoveredPositions = (instant = false) => {
157
+ if (!navListRef.value || !currentHoveredNavLink.value) return;
158
+ const list = navListRef.value;
159
+ const el = currentHoveredNavLink.value;
160
+ list.style.setProperty("--_transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
161
+ list.style.setProperty("--_x-hovered", el.offsetLeft + "px");
162
+ list.style.setProperty("--_width-hovered", String(el.offsetWidth / list.offsetWidth));
163
+ };
164
+
165
+ const moveNavHoveredIndicator = () => {
166
+ if (!navListRef.value || !currentHoveredNavLink.value || !previousHoveredNavLink.value) return;
167
+ const list = navListRef.value;
168
+ const curr = currentHoveredNavLink.value;
169
+ const prev = previousHoveredNavLink.value;
170
+ list.style.setProperty("--_transition-duration", NAV_DECORATOR_DURATION + "ms");
171
+ const isMovingRight = prev.compareDocumentPosition(curr) === 4;
172
+ let transitionWidth: number;
173
+ if (isMovingRight) {
174
+ transitionWidth = curr.offsetLeft + curr.offsetWidth - prev.offsetLeft;
175
+ } else {
176
+ transitionWidth = prev.offsetLeft + prev.offsetWidth - curr.offsetLeft;
177
+ list.style.setProperty("--_x-hovered", curr.offsetLeft + "px");
178
+ }
179
+ list.style.setProperty("--_width-hovered", String(transitionWidth / list.offsetWidth));
180
+ if (navSnapTimer !== null) clearTimeout(navSnapTimer);
181
+ navSnapTimer = setTimeout(() => {
182
+ navSnapTimer = null;
183
+ setFinalNavHoveredPositions();
184
+ }, NAV_DECORATOR_DURATION + 20);
185
+ };
186
+
187
+ const handleNavLinkClick = (event: MouseEvent) => {
188
+ const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
189
+ if (!target) return;
190
+ currentActiveNavLink.value = target;
191
+ currentHoveredNavLink.value = target;
192
+ previousHoveredNavLink.value = target;
193
+ };
194
+
195
+ const handleNavHover = (event: MouseEvent) => {
196
+ 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;
200
+ moveNavHoveredIndicator();
201
+ };
202
+
203
+ const resetHoverNavToActive = () => {
204
+ if (!currentActiveNavLink.value || currentHoveredNavLink.value === currentActiveNavLink.value) return;
205
+ previousHoveredNavLink.value = currentHoveredNavLink.value;
206
+ currentHoveredNavLink.value = currentActiveNavLink.value;
207
+ moveNavHoveredIndicator();
208
+ };
209
+
210
+ const initNavDecorators = () => {
211
+ if (!navListRef.value) return;
212
+ const links = getNavLinks();
213
+ if (!links.length) return;
214
+
215
+ // Cancel any in-flight snap timer before resetting positions
216
+ if (navSnapTimer !== null) {
217
+ clearTimeout(navSnapTimer);
218
+ navSnapTimer = null;
219
+ }
220
+
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];
224
+ if (!activeLink) return;
225
+
226
+ currentActiveNavLink.value = activeLink;
227
+ currentHoveredNavLink.value = activeLink;
228
+ previousHoveredNavLink.value = activeLink;
229
+
230
+ setFinalNavActivePositions(true);
231
+ 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
+
246
+ // ─── Panel decorators (y-axis active / hover indicators) ─────────────────────
247
+
248
+ const panelListRef = ref<HTMLUListElement | null>(null);
249
+
250
+ const currentActivePanelLink = ref<HTMLElement | null>(null);
251
+ const currentHoveredPanelLink = ref<HTMLElement | null>(null);
252
+ const previousHoveredPanelLink = ref<HTMLElement | null>(null);
253
+
254
+ const getPanelLinks = () =>
255
+ panelListRef.value ? Array.from(panelListRef.value.querySelectorAll<HTMLElement>("[data-panel-nav-item]")) : [];
256
+
257
+ const setFinalPanelActivePositions = (instant = false) => {
258
+ if (!panelListRef.value || !currentActivePanelLink.value) return;
259
+ const list = panelListRef.value;
260
+ const el = currentActivePanelLink.value;
261
+ list.style.setProperty("--_panel-transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
262
+ list.style.setProperty("--_panel-y-active", el.offsetTop + "px");
263
+ list.style.setProperty("--_panel-height-active", String(el.offsetHeight / list.offsetHeight));
264
+ };
265
+
266
+ const setFinalPanelHoveredPositions = (instant = false) => {
267
+ if (!panelListRef.value || !currentHoveredPanelLink.value) return;
268
+ const list = panelListRef.value;
269
+ const el = currentHoveredPanelLink.value;
270
+ list.style.setProperty("--_panel-transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
271
+ list.style.setProperty("--_panel-y-hovered", el.offsetTop + "px");
272
+ list.style.setProperty("--_panel-height-hovered", String(el.offsetHeight / list.offsetHeight));
273
+ };
274
+
275
+ const movePanelHoveredIndicator = () => {
276
+ if (!panelListRef.value || !currentHoveredPanelLink.value || !previousHoveredPanelLink.value) return;
277
+ const list = panelListRef.value;
278
+ const curr = currentHoveredPanelLink.value;
279
+ const prev = previousHoveredPanelLink.value;
280
+ list.style.setProperty("--_panel-transition-duration", NAV_DECORATOR_DURATION + "ms");
281
+ const isMovingDown = prev.compareDocumentPosition(curr) === 4;
282
+ let transitionHeight: number;
283
+ if (isMovingDown) {
284
+ transitionHeight = curr.offsetTop + curr.offsetHeight - prev.offsetTop;
285
+ } else {
286
+ transitionHeight = prev.offsetTop + prev.offsetHeight - curr.offsetTop;
287
+ list.style.setProperty("--_panel-y-hovered", curr.offsetTop + "px");
288
+ }
289
+ list.style.setProperty("--_panel-height-hovered", String(transitionHeight / list.offsetHeight));
290
+ if (panelSnapTimer !== null) clearTimeout(panelSnapTimer);
291
+ panelSnapTimer = setTimeout(() => {
292
+ panelSnapTimer = null;
293
+ setFinalPanelHoveredPositions();
294
+ }, NAV_DECORATOR_DURATION + 20);
295
+ };
296
+
297
+ const handlePanelLinkClick = (event: MouseEvent) => {
298
+ const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
299
+ if (!target) return;
300
+ currentActivePanelLink.value = target;
301
+ currentHoveredPanelLink.value = target;
302
+ previousHoveredPanelLink.value = target;
303
+ };
304
+
305
+ const handlePanelHover = (event: MouseEvent) => {
306
+ 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;
310
+ movePanelHoveredIndicator();
311
+ };
312
+
313
+ const resetHoverPanelToActive = () => {
314
+ if (!currentActivePanelLink.value || currentHoveredPanelLink.value === currentActivePanelLink.value) return;
315
+ previousHoveredPanelLink.value = currentHoveredPanelLink.value;
316
+ currentHoveredPanelLink.value = currentActivePanelLink.value;
317
+ movePanelHoveredIndicator();
318
+ };
319
+
320
+ const initPanelDecorators = () => {
321
+ if (!panelListRef.value) return;
322
+ const links = getPanelLinks();
323
+ if (!links.length) return;
324
+
325
+ if (panelSnapTimer !== null) {
326
+ clearTimeout(panelSnapTimer);
327
+ panelSnapTimer = null;
328
+ }
329
+
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];
333
+ if (!activeLink) return;
334
+
335
+ currentActivePanelLink.value = activeLink;
336
+ currentHoveredPanelLink.value = activeLink;
337
+ previousHoveredPanelLink.value = activeLink;
338
+
339
+ setFinalPanelActivePositions(true);
340
+ 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
+ };
352
+
353
+ // ─────────────────────────────────────────────────────────────────────────────
354
+
355
+ // Close panel on navigation and re-init decorators.
356
+ // flush: 'post' ensures router-link-active classes are applied.
357
+ // requestAnimationFrame defers the measurement until after browser layout,
358
+ // preventing stale offsetLeft reads and racing with hover setTimeouts.
359
+ const route = useRoute();
360
+ watch(
361
+ () => route.path,
362
+ () => {
363
+ closeMenu();
364
+ requestAnimationFrame(() => {
365
+ 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
+ onMounted(async () => {
383
+ await nextTick();
384
+ checkOverflow();
385
+ isLoaded.value = true;
386
+ await nextTick();
387
+ initNavDecorators();
388
+ });
389
+
390
+ watch(isCollapsed, async (collapsed) => {
391
+ if (!collapsed) {
392
+ await nextTick();
393
+ initNavDecorators();
394
+ }
395
+ });
396
+
397
+ watch(isMenuOpen, async (open) => {
398
+ if (open) {
399
+ await nextTick();
400
+ initPanelDecorators();
401
+ }
402
+ });
403
+
404
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
405
+
406
+ watch(
407
+ () => props.styleClassPassthrough,
408
+ () => resetElementClasses(props.styleClassPassthrough)
409
+ );
410
+ </script>
411
+
412
+ <style lang="css">
413
+ @layer components {
414
+ .site-nav-backdrop {
415
+ --_backdrop-bg: var(--site-nav-backdrop-bg, oklch(0% 0 0 / 55%));
416
+ --_backdrop-blur: var(--site-nav-backdrop-blur, 3px);
417
+ --_backdrop-duration: var(--site-nav-backdrop-duration, 350ms);
418
+
419
+ position: fixed;
420
+ inset: 0;
421
+ z-index: 10;
422
+ background: var(--_backdrop-bg);
423
+ backdrop-filter: blur(var(--_backdrop-blur));
424
+ opacity: 0;
425
+ pointer-events: none;
426
+ transition: opacity var(--_backdrop-duration) ease;
427
+
428
+ &.is-open {
429
+ opacity: 1;
430
+ pointer-events: auto;
431
+ }
432
+ }
433
+
434
+ .site-navigation {
435
+ /* ─── Public token API ────────────────────────────────────────────── */
436
+
437
+ /* Decorators — horizontal nav */
438
+ --_decorator-hovered-bg: transparent;
439
+ --_decorator-active-bg: transparent;
440
+ --_decorator-indicator-color: var(--site-nav-decorator-indicator-color, var(--rose-05, currentColor));
441
+
442
+ /* Decorators — panel */
443
+ --_panel-decorator-hovered-bg: var(--site-nav-panel-decorator-hovered-bg, transparent);
444
+ --_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-indicator-left: var(--site-nav-panel-indicator-left, 0);
447
+ --_panel-indicator-right: var(--site-nav-panel-indicator-right, auto);
448
+
449
+ /* 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)));
453
+ --_link-size: var(--site-nav-link-size, 1.6rem);
454
+ --_link-tracking: var(--site-nav-link-tracking, 0.06em);
455
+ --_link-weight: var(--site-nav-link-weight, 400);
456
+ --_link-accent: var(--site-nav-link-accent, var(--rose-05, currentColor));
457
+ --_nav-gap: var(--site-nav-gap, 2.2rem);
458
+ --_nav-transition: var(--site-nav-transition, 250ms ease);
459
+
460
+ /* Panel */
461
+ --_panel-bg: var(--site-nav-panel-bg, var(--page-bg, #1a1614));
462
+ --_panel-border-color: var(
463
+ --site-nav-panel-border-color,
464
+ color-mix(in oklch, var(--rose-05, #c0847a) 35%, transparent)
465
+ );
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)));
470
+ --_panel-padding-block: var(--site-nav-panel-padding-block, 1.4rem);
471
+ --_panel-padding-inline: var(--site-nav-panel-padding-inline, 1.5rem);
472
+ --_panel-slide-duration: var(--site-nav-panel-slide-duration, 350ms);
473
+ --_panel-slide-easing: var(--site-nav-panel-slide-easing, cubic-bezier(0.4, 0, 0.2, 1));
474
+
475
+ /* Burger */
476
+ --_burger-bar-width: var(--site-nav-burger-width, 22px);
477
+ --_burger-bar-height: var(--site-nav-burger-height, 1.5px);
478
+ --_burger-bar-gap: var(--site-nav-burger-gap, 5px);
479
+ --_burger-color: var(--site-nav-burger-color, var(--warm-01, currentColor));
480
+ --_burger-transition: var(--site-nav-burger-transition, 300ms ease);
481
+
482
+ /* ─────────────────────────────────────────────────────────────────── */
483
+
484
+ display: flex;
485
+ align-items: center;
486
+ min-width: 0;
487
+
488
+ /* Hide everything until first measurement to prevent wrong-state flash */
489
+ &:not(.is-loaded) {
490
+ opacity: 0;
491
+ }
492
+
493
+ /* ─── Horizontal list ───────────────────────────────────────────── */
494
+
495
+ .site-nav-list {
496
+ list-style: none;
497
+ margin: 0;
498
+ padding: 0;
499
+ display: flex;
500
+ gap: var(--_nav-gap);
501
+ align-items: center;
502
+ position: relative;
503
+
504
+ .nav-indicator-li {
505
+ position: absolute;
506
+ inset: 0;
507
+ pointer-events: none;
508
+ list-style: none;
509
+ }
510
+
511
+ .nav__hovered,
512
+ .nav__active,
513
+ .nav__active-indicator {
514
+ position: absolute;
515
+ left: 0;
516
+ top: 0;
517
+ bottom: 0;
518
+ right: 0;
519
+ scale: var(--_width-hovered, 0.001) 1;
520
+ translate: var(--_x-hovered, 0) 0;
521
+ transform-origin: left;
522
+ transition:
523
+ scale var(--_transition-duration, 200ms),
524
+ translate var(--_transition-duration, 200ms);
525
+ pointer-events: none;
526
+ }
527
+
528
+ .nav__active {
529
+ scale: var(--_width-active, 0.001) 1;
530
+ translate: var(--_x-active, 0) 0;
531
+ }
532
+
533
+ .nav__active-indicator {
534
+ scale: var(--_width-hovered, 0.001) 1;
535
+ translate: var(--_x-hovered, 0) 0;
536
+ }
537
+
538
+ .nav__hovered {
539
+ background: var(--_decorator-hovered-bg);
540
+ border-radius: 4px;
541
+ z-index: 1;
542
+ }
543
+
544
+ .nav__active {
545
+ background: var(--_decorator-active-bg);
546
+ border-radius: 4px;
547
+ z-index: 2;
548
+ }
549
+
550
+ .nav__active-indicator {
551
+ top: auto;
552
+ height: 2px;
553
+ background-color: var(--_decorator-indicator-color);
554
+ z-index: 3;
555
+ }
556
+
557
+ .site-nav-link {
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 0.4em;
561
+ color: var(--_link-color);
562
+ font-size: var(--_link-size);
563
+ font-weight: var(--_link-weight);
564
+ letter-spacing: var(--_link-tracking);
565
+ text-decoration: none;
566
+ text-wrap: nowrap;
567
+ padding-block: 0.8rem;
568
+ padding-inline: 0.4rem;
569
+ position: relative;
570
+ z-index: 4;
571
+ transition: color var(--_nav-transition);
572
+
573
+ &:hover,
574
+ &:focus-visible {
575
+ color: var(--_link-hover-color);
576
+ outline: none;
577
+ }
578
+
579
+ &.router-link-active {
580
+ color: var(--_link-active-color);
581
+ }
582
+ }
583
+ }
584
+
585
+ /* ─── Alignment variants ────────────────────────────────────────── */
586
+
587
+ &.site-navigation--center .site-nav-list {
588
+ margin-inline: auto;
589
+ }
590
+
591
+ &.site-navigation--right .site-nav-list {
592
+ margin-inline-start: auto;
593
+ }
594
+
595
+ &.is-collapsed {
596
+ justify-content: end;
597
+ }
598
+
599
+ /* ─── Burger button (InputButtonCore) ──────────────────────────── */
600
+
601
+ .site-nav-burger.input-button-core.icon-only {
602
+ margin-inline-start: auto;
603
+ color: var(--_burger-color);
604
+
605
+ /* Strip all InputButtonCore visual styling */
606
+ background: none;
607
+ border: none;
608
+ outline: none;
609
+ text-decoration: none;
610
+ padding: 8px;
611
+ border-radius: 4px;
612
+ transition: outline-color var(--_nav-transition);
613
+
614
+ /* Override icon-only aspect-ratio and inner icon margin */
615
+ &.icon-only {
616
+ aspect-ratio: unset;
617
+ border-radius: 4px;
618
+
619
+ .btn-icon {
620
+ margin: 0;
621
+ display: flex;
622
+ flex-direction: column;
623
+ gap: var(--_burger-bar-gap);
624
+ }
625
+ }
626
+
627
+ &:focus-visible {
628
+ outline: 2px solid var(--_burger-color);
629
+ outline-offset: 4px;
630
+ }
631
+ }
632
+
633
+ .burger-bar {
634
+ display: block;
635
+ width: var(--_burger-bar-width);
636
+ height: var(--_burger-bar-height);
637
+ background: currentColor;
638
+ border-radius: 1px;
639
+ transform-origin: center;
640
+ transition:
641
+ transform var(--_burger-transition),
642
+ opacity var(--_burger-transition);
643
+ }
644
+
645
+ .site-nav-burger.is-open {
646
+ .burger-bar:nth-child(1) {
647
+ transform: translateY(calc(var(--_burger-bar-height) + var(--_burger-bar-gap))) rotate(45deg);
648
+ }
649
+
650
+ .burger-bar:nth-child(2) {
651
+ opacity: 0;
652
+ transform: scaleX(0);
653
+ }
654
+
655
+ .burger-bar:nth-child(3) {
656
+ transform: translateY(calc(-1 * (var(--_burger-bar-height) + var(--_burger-bar-gap)))) rotate(-45deg);
657
+ }
658
+ }
659
+
660
+ /* ─── Mobile drop panel ─────────────────────────────────────────── */
661
+
662
+ .site-nav-panel {
663
+ position: absolute;
664
+ left: 0;
665
+ right: 0;
666
+ top: 100%;
667
+
668
+ display: grid;
669
+ grid-template-rows: 0fr;
670
+ border-block-start: 1px solid transparent;
671
+ transition:
672
+ grid-template-rows var(--_panel-slide-duration) var(--_panel-slide-easing),
673
+ border-color var(--_panel-slide-duration) var(--_panel-slide-easing);
674
+
675
+ z-index: 1;
676
+
677
+ &.is-open {
678
+ grid-template-rows: 1fr;
679
+ border-block-start-color: var(--_panel-border-color);
680
+ }
681
+
682
+ .site-nav-panel-inner {
683
+ overflow: hidden;
684
+ background-color: var(--_panel-bg);
685
+ }
686
+
687
+ .site-nav-panel-list {
688
+ list-style: none;
689
+ margin: 0;
690
+ padding: 0;
691
+ position: relative;
692
+
693
+ .nav-indicator-li {
694
+ position: absolute;
695
+ inset: 0;
696
+ pointer-events: none;
697
+ list-style: none;
698
+ }
699
+
700
+ .nav__hovered,
701
+ .nav__active,
702
+ .nav__active-indicator {
703
+ position: absolute;
704
+ left: 0;
705
+ top: 0;
706
+ bottom: 0;
707
+ right: 0;
708
+ scale: 1 var(--_panel-height-hovered, 0.001);
709
+ translate: 0 var(--_panel-y-hovered, 0);
710
+ transform-origin: top;
711
+ transition:
712
+ scale var(--_panel-transition-duration, 200ms),
713
+ translate var(--_panel-transition-duration, 200ms);
714
+ pointer-events: none;
715
+ }
716
+
717
+ .nav__active {
718
+ scale: 1 var(--_panel-height-active, 0.001);
719
+ translate: 0 var(--_panel-y-active, 0);
720
+ }
721
+
722
+ .nav__active-indicator {
723
+ left: var(--_panel-indicator-left, 0);
724
+ right: var(--_panel-indicator-right, auto);
725
+ width: 2px;
726
+ }
727
+
728
+ .nav__hovered {
729
+ background: var(--_panel-decorator-hovered-bg);
730
+ z-index: 1;
731
+ }
732
+
733
+ .nav__active {
734
+ background: var(--_panel-decorator-active-bg);
735
+ z-index: 2;
736
+ }
737
+
738
+ .nav__active-indicator {
739
+ background: var(--_panel-decorator-indicator-color);
740
+ z-index: 3;
741
+ }
742
+
743
+ li {
744
+ border-block-end: 1px solid var(--_panel-item-border);
745
+
746
+ &:last-child {
747
+ border-block-end: none;
748
+ }
749
+ }
750
+
751
+ .site-nav-panel-link {
752
+ display: flex;
753
+ align-items: center;
754
+ gap: 0.5em;
755
+ color: var(--_panel-link-color);
756
+ font-size: var(--_link-size);
757
+ font-weight: var(--_link-weight);
758
+ letter-spacing: var(--_link-tracking);
759
+ text-decoration: none;
760
+ padding-block: var(--_panel-padding-block);
761
+ padding-inline: var(--_panel-padding-inline);
762
+ position: relative;
763
+ z-index: 4;
764
+ transition: color var(--_nav-transition);
765
+
766
+ &:hover,
767
+ &:focus-visible {
768
+ color: var(--_panel-link-hover-color);
769
+ outline: none;
770
+ }
771
+
772
+ &.router-link-active {
773
+ color: var(--_panel-link-active-color);
774
+ }
775
+ }
776
+ }
777
+ }
778
+ }
779
+ }
780
+ </style>