srcdev-nuxt-components 9.5.7 → 9.6.0
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/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/display-pill.md +2 -0
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/responsive-header.md +49 -0
- package/.claude/skills/components/service-detail.md +32 -0
- package/.claude/skills/components/service-summary.md +8 -0
- package/.claude/skills/components/services-card-grid.md +5 -3
- package/.claude/skills/components/services-card.md +22 -12
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tab-navigation.md +25 -9
- package/.claude/skills/index.md +1 -2
- package/.vscode/srcdev-component-responsive-header.code-snippets +10 -0
- package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
- package/.vscode/srcdev-component-services-card.code-snippets +38 -36
- package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
- package/.vscode/srcdev-component-tab-navigation.code-snippets +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/display-pill/DisplayPill.vue +5 -2
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +52 -11
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +55 -23
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +176 -257
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +42 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +4 -0
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +33 -4
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +51 -4
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -3
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
- package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
- package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-button/InputButton.vue +3 -1
- package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
- package/app/composables/useAnchorScroll.ts +1 -1
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
|
@@ -1,751 +0,0 @@
|
|
|
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, 'is-animated': isAnimated },
|
|
9
|
-
]"
|
|
10
|
-
:aria-label="ariaLabel"
|
|
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
|
|
21
|
-
v-for="item in navItemData.main"
|
|
22
|
-
:key="item.href"
|
|
23
|
-
:class="[item.cssName, { 'is-active': isActiveItem(item.href) }]"
|
|
24
|
-
>
|
|
25
|
-
<NuxtLink :href="item.href" :external="item.isExternal || undefined" class="site-nav-link" data-nav-item>
|
|
26
|
-
<Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
|
|
27
|
-
{{ item.text }}
|
|
28
|
-
</NuxtLink>
|
|
29
|
-
</li>
|
|
30
|
-
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__hovered"></div></li>
|
|
31
|
-
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active"></div></li>
|
|
32
|
-
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active-indicator"></div></li>
|
|
33
|
-
</ul>
|
|
34
|
-
|
|
35
|
-
<InputButton
|
|
36
|
-
v-if="isCollapsed && isLoaded"
|
|
37
|
-
class="site-nav-burger"
|
|
38
|
-
:class="{ 'is-open': isMenuOpen }"
|
|
39
|
-
variant="tertiary"
|
|
40
|
-
:button-text="isMenuOpen ? closeMenuLabel : openMenuLabel"
|
|
41
|
-
:aria-expanded="String(isMenuOpen)"
|
|
42
|
-
:aria-controls="panelId"
|
|
43
|
-
@click="toggleMenu"
|
|
44
|
-
>
|
|
45
|
-
<template #iconOnly>
|
|
46
|
-
<span class="burger-bar" aria-hidden="true"></span>
|
|
47
|
-
<span class="burger-bar" aria-hidden="true"></span>
|
|
48
|
-
<span class="burger-bar" aria-hidden="true"></span>
|
|
49
|
-
</template>
|
|
50
|
-
</InputButton>
|
|
51
|
-
|
|
52
|
-
<Teleport to="body">
|
|
53
|
-
<div
|
|
54
|
-
v-if="isCollapsed && isLoaded"
|
|
55
|
-
class="site-nav-backdrop"
|
|
56
|
-
:class="{ 'is-open': isMenuOpen }"
|
|
57
|
-
aria-hidden="true"
|
|
58
|
-
@click="closeMenu"
|
|
59
|
-
></div>
|
|
60
|
-
</Teleport>
|
|
61
|
-
|
|
62
|
-
<div
|
|
63
|
-
v-if="isCollapsed && isLoaded"
|
|
64
|
-
:id="panelId"
|
|
65
|
-
class="site-nav-panel"
|
|
66
|
-
:class="{ 'is-open': isMenuOpen }"
|
|
67
|
-
:inert="!isMenuOpen ? true : undefined"
|
|
68
|
-
>
|
|
69
|
-
<div class="site-nav-panel-inner">
|
|
70
|
-
<ul
|
|
71
|
-
ref="panelListRef"
|
|
72
|
-
class="site-nav-panel-list"
|
|
73
|
-
@click="handlePanelLinkClick"
|
|
74
|
-
@mouseover="handlePanelHover"
|
|
75
|
-
@mouseleave="resetHoverPanelToActive"
|
|
76
|
-
>
|
|
77
|
-
<li
|
|
78
|
-
v-for="item in navItemData.main"
|
|
79
|
-
:key="item.href"
|
|
80
|
-
:class="[item.cssName, { 'is-active': isActiveItem(item.href) }]"
|
|
81
|
-
>
|
|
82
|
-
<NuxtLink
|
|
83
|
-
:href="item.href"
|
|
84
|
-
:external="item.isExternal || undefined"
|
|
85
|
-
class="site-nav-panel-link"
|
|
86
|
-
data-panel-nav-item
|
|
87
|
-
@click="closeMenu"
|
|
88
|
-
>
|
|
89
|
-
<Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
|
|
90
|
-
{{ item.text }}
|
|
91
|
-
</NuxtLink>
|
|
92
|
-
</li>
|
|
93
|
-
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__hovered"></div></li>
|
|
94
|
-
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active"></div></li>
|
|
95
|
-
<li aria-hidden="true" role="none" class="nav-indicator-li"><div class="nav__active-indicator"></div></li>
|
|
96
|
-
</ul>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
</nav>
|
|
100
|
-
</template>
|
|
101
|
-
|
|
102
|
-
<script setup lang="ts">
|
|
103
|
-
import type { NavItemData } from "~/types/components";
|
|
104
|
-
|
|
105
|
-
interface Props {
|
|
106
|
-
navItemData: NavItemData;
|
|
107
|
-
navAlign?: "left" | "center" | "right";
|
|
108
|
-
styleClassPassthrough?: string | string[];
|
|
109
|
-
/** aria-label on the nav landmark — override for localisation. */
|
|
110
|
-
ariaLabel?: string;
|
|
111
|
-
/** Burger button label while the menu is closed — override for localisation. */
|
|
112
|
-
openMenuLabel?: string;
|
|
113
|
-
/** Burger button label while the menu is open — override for localisation. */
|
|
114
|
-
closeMenuLabel?: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
118
|
-
navAlign: "left",
|
|
119
|
-
styleClassPassthrough: () => [],
|
|
120
|
-
ariaLabel: "Site navigation",
|
|
121
|
-
openMenuLabel: "Open navigation menu",
|
|
122
|
-
closeMenuLabel: "Close navigation menu",
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const panelId = `site-nav-panel-${useId()}`;
|
|
126
|
-
|
|
127
|
-
// ─── Animation gate — prevents indicator from transitioning on first paint ───
|
|
128
|
-
|
|
129
|
-
const isAnimated = ref(false);
|
|
130
|
-
// Stays false until ResizeObserver confirms the nav has been laid out (stronger
|
|
131
|
-
// guarantee than nextTick / RAF which don't ensure layout is complete).
|
|
132
|
-
let hasFirstLayout = false;
|
|
133
|
-
|
|
134
|
-
// ─── Nav decorators (active / hover indicators) ─────────────────────────────
|
|
135
|
-
|
|
136
|
-
const NAV_DECORATOR_DURATION = 200;
|
|
137
|
-
|
|
138
|
-
// Single pending snap timer — cancel the previous one when a new move starts
|
|
139
|
-
let navSnapTimer: ReturnType<typeof setTimeout> | null = null;
|
|
140
|
-
let panelSnapTimer: ReturnType<typeof setTimeout> | null = null;
|
|
141
|
-
|
|
142
|
-
let currentActiveNavLink: HTMLElement | null = null;
|
|
143
|
-
let currentHoveredNavLink: HTMLElement | null = null;
|
|
144
|
-
let previousHoveredNavLink: HTMLElement | null = null;
|
|
145
|
-
|
|
146
|
-
// Cached after first init — invalidated when nav list is re-rendered (collapse toggle)
|
|
147
|
-
let cachedNavLinks: HTMLElement[] = [];
|
|
148
|
-
|
|
149
|
-
const getNavLinks = (): HTMLElement[] => {
|
|
150
|
-
if (cachedNavLinks.length) return cachedNavLinks;
|
|
151
|
-
if (!navListRef.value) return [];
|
|
152
|
-
cachedNavLinks = Array.from(navListRef.value.querySelectorAll<HTMLElement>("[data-nav-item]"));
|
|
153
|
-
return cachedNavLinks;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const setFinalNavActivePositions = (instant = false) => {
|
|
157
|
-
if (!navListRef.value || !currentActiveNavLink) return;
|
|
158
|
-
const list = navListRef.value;
|
|
159
|
-
const el = currentActiveNavLink;
|
|
160
|
-
list.style.setProperty("--_transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
|
|
161
|
-
list.style.setProperty("--_x-active", el.offsetLeft + "px");
|
|
162
|
-
list.style.setProperty("--_width-active", String(el.offsetWidth / list.offsetWidth));
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const setFinalNavHoveredPositions = (instant = false) => {
|
|
166
|
-
if (!navListRef.value || !currentHoveredNavLink) return;
|
|
167
|
-
const list = navListRef.value;
|
|
168
|
-
const el = currentHoveredNavLink;
|
|
169
|
-
list.style.setProperty("--_transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
|
|
170
|
-
list.style.setProperty("--_x-hovered", el.offsetLeft + "px");
|
|
171
|
-
list.style.setProperty("--_width-hovered", String(el.offsetWidth / list.offsetWidth));
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const moveNavHoveredIndicator = () => {
|
|
175
|
-
if (!navListRef.value || !currentHoveredNavLink || !previousHoveredNavLink) return;
|
|
176
|
-
const list = navListRef.value;
|
|
177
|
-
const curr = currentHoveredNavLink;
|
|
178
|
-
const prev = previousHoveredNavLink;
|
|
179
|
-
list.style.setProperty("--_transition-duration", NAV_DECORATOR_DURATION + "ms");
|
|
180
|
-
const isMovingRight = prev.compareDocumentPosition(curr) === 4;
|
|
181
|
-
let transitionWidth: number;
|
|
182
|
-
if (isMovingRight) {
|
|
183
|
-
transitionWidth = curr.offsetLeft + curr.offsetWidth - prev.offsetLeft;
|
|
184
|
-
} else {
|
|
185
|
-
transitionWidth = prev.offsetLeft + prev.offsetWidth - curr.offsetLeft;
|
|
186
|
-
list.style.setProperty("--_x-hovered", curr.offsetLeft + "px");
|
|
187
|
-
}
|
|
188
|
-
list.style.setProperty("--_width-hovered", String(transitionWidth / list.offsetWidth));
|
|
189
|
-
if (navSnapTimer !== null) clearTimeout(navSnapTimer);
|
|
190
|
-
navSnapTimer = setTimeout(() => {
|
|
191
|
-
navSnapTimer = null;
|
|
192
|
-
setFinalNavHoveredPositions();
|
|
193
|
-
}, NAV_DECORATOR_DURATION + 20);
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const handleNavLinkClick = (event: MouseEvent) => {
|
|
197
|
-
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
|
|
198
|
-
if (!target) return;
|
|
199
|
-
currentActiveNavLink = target;
|
|
200
|
-
currentHoveredNavLink = target;
|
|
201
|
-
previousHoveredNavLink = target;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const handleNavHover = (event: MouseEvent) => {
|
|
205
|
-
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
|
|
206
|
-
if (!target || target === currentHoveredNavLink) return;
|
|
207
|
-
previousHoveredNavLink = currentHoveredNavLink;
|
|
208
|
-
currentHoveredNavLink = target;
|
|
209
|
-
moveNavHoveredIndicator();
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const resetHoverNavToActive = () => {
|
|
213
|
-
if (!currentActiveNavLink || currentHoveredNavLink === currentActiveNavLink) return;
|
|
214
|
-
previousHoveredNavLink = currentHoveredNavLink;
|
|
215
|
-
currentHoveredNavLink = currentActiveNavLink;
|
|
216
|
-
moveNavHoveredIndicator();
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const initNavDecorators = () => {
|
|
220
|
-
cachedNavLinks = []; // invalidate — list may have been re-rendered (collapse toggle)
|
|
221
|
-
if (!navListRef.value) return;
|
|
222
|
-
const links = getNavLinks();
|
|
223
|
-
if (!links.length) return;
|
|
224
|
-
|
|
225
|
-
if (navSnapTimer !== null) {
|
|
226
|
-
clearTimeout(navSnapTimer);
|
|
227
|
-
navSnapTimer = null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const activeLink = links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
|
|
231
|
-
if (!activeLink) return;
|
|
232
|
-
|
|
233
|
-
currentActiveNavLink = activeLink;
|
|
234
|
-
currentHoveredNavLink = activeLink;
|
|
235
|
-
previousHoveredNavLink = activeLink;
|
|
236
|
-
|
|
237
|
-
setFinalNavActivePositions(true);
|
|
238
|
-
setFinalNavHoveredPositions(true);
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
// ─── Panel decorators (y-axis active / hover indicators) ─────────────────────
|
|
242
|
-
|
|
243
|
-
const panelListRef = ref<HTMLUListElement | null>(null);
|
|
244
|
-
|
|
245
|
-
let currentActivePanelLink: HTMLElement | null = null;
|
|
246
|
-
let currentHoveredPanelLink: HTMLElement | null = null;
|
|
247
|
-
let previousHoveredPanelLink: HTMLElement | null = null;
|
|
248
|
-
|
|
249
|
-
// Cached after first init — invalidated when panel list is re-rendered (menu open/close)
|
|
250
|
-
let cachedPanelLinks: HTMLElement[] = [];
|
|
251
|
-
|
|
252
|
-
const getPanelLinks = (): HTMLElement[] => {
|
|
253
|
-
if (cachedPanelLinks.length) return cachedPanelLinks;
|
|
254
|
-
if (!panelListRef.value) return [];
|
|
255
|
-
cachedPanelLinks = Array.from(panelListRef.value.querySelectorAll<HTMLElement>("[data-panel-nav-item]"));
|
|
256
|
-
return cachedPanelLinks;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const setFinalPanelActivePositions = (instant = false) => {
|
|
260
|
-
if (!panelListRef.value || !currentActivePanelLink) return;
|
|
261
|
-
const list = panelListRef.value;
|
|
262
|
-
const el = currentActivePanelLink;
|
|
263
|
-
list.style.setProperty("--_panel-transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
|
|
264
|
-
list.style.setProperty("--_panel-y-active", el.offsetTop + "px");
|
|
265
|
-
list.style.setProperty("--_panel-height-active", String(el.offsetHeight / list.offsetHeight));
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const setFinalPanelHoveredPositions = (instant = false) => {
|
|
269
|
-
if (!panelListRef.value || !currentHoveredPanelLink) return;
|
|
270
|
-
const list = panelListRef.value;
|
|
271
|
-
const el = currentHoveredPanelLink;
|
|
272
|
-
list.style.setProperty("--_panel-transition-duration", instant ? "0ms" : NAV_DECORATOR_DURATION + "ms");
|
|
273
|
-
list.style.setProperty("--_panel-y-hovered", el.offsetTop + "px");
|
|
274
|
-
list.style.setProperty("--_panel-height-hovered", String(el.offsetHeight / list.offsetHeight));
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const movePanelHoveredIndicator = () => {
|
|
278
|
-
if (!panelListRef.value || !currentHoveredPanelLink || !previousHoveredPanelLink) return;
|
|
279
|
-
const list = panelListRef.value;
|
|
280
|
-
const curr = currentHoveredPanelLink;
|
|
281
|
-
const prev = previousHoveredPanelLink;
|
|
282
|
-
list.style.setProperty("--_panel-transition-duration", NAV_DECORATOR_DURATION + "ms");
|
|
283
|
-
const isMovingDown = prev.compareDocumentPosition(curr) === 4;
|
|
284
|
-
let transitionHeight: number;
|
|
285
|
-
if (isMovingDown) {
|
|
286
|
-
transitionHeight = curr.offsetTop + curr.offsetHeight - prev.offsetTop;
|
|
287
|
-
} else {
|
|
288
|
-
transitionHeight = prev.offsetTop + prev.offsetHeight - curr.offsetTop;
|
|
289
|
-
list.style.setProperty("--_panel-y-hovered", curr.offsetTop + "px");
|
|
290
|
-
}
|
|
291
|
-
list.style.setProperty("--_panel-height-hovered", String(transitionHeight / list.offsetHeight));
|
|
292
|
-
if (panelSnapTimer !== null) clearTimeout(panelSnapTimer);
|
|
293
|
-
panelSnapTimer = setTimeout(() => {
|
|
294
|
-
panelSnapTimer = null;
|
|
295
|
-
setFinalPanelHoveredPositions();
|
|
296
|
-
}, NAV_DECORATOR_DURATION + 20);
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const handlePanelLinkClick = (event: MouseEvent) => {
|
|
300
|
-
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
|
|
301
|
-
if (!target) return;
|
|
302
|
-
currentActivePanelLink = target;
|
|
303
|
-
currentHoveredPanelLink = target;
|
|
304
|
-
previousHoveredPanelLink = target;
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const handlePanelHover = (event: MouseEvent) => {
|
|
308
|
-
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
|
|
309
|
-
if (!target || target === currentHoveredPanelLink) return;
|
|
310
|
-
previousHoveredPanelLink = currentHoveredPanelLink;
|
|
311
|
-
currentHoveredPanelLink = target;
|
|
312
|
-
movePanelHoveredIndicator();
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
const resetHoverPanelToActive = () => {
|
|
316
|
-
if (!currentActivePanelLink || currentHoveredPanelLink === currentActivePanelLink) return;
|
|
317
|
-
previousHoveredPanelLink = currentHoveredPanelLink;
|
|
318
|
-
currentHoveredPanelLink = currentActivePanelLink;
|
|
319
|
-
movePanelHoveredIndicator();
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const initPanelDecorators = () => {
|
|
323
|
-
if (!panelListRef.value) return;
|
|
324
|
-
cachedPanelLinks = []; // panel DOM is re-rendered each open — invalidate cache
|
|
325
|
-
const links = getPanelLinks();
|
|
326
|
-
if (!links.length) return;
|
|
327
|
-
|
|
328
|
-
if (panelSnapTimer !== null) {
|
|
329
|
-
clearTimeout(panelSnapTimer);
|
|
330
|
-
panelSnapTimer = null;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const activeLink = links.find((el) => el.getAttribute("href") === activeHref.value) ?? links[0];
|
|
334
|
-
if (!activeLink) return;
|
|
335
|
-
|
|
336
|
-
currentActivePanelLink = activeLink;
|
|
337
|
-
currentHoveredPanelLink = activeLink;
|
|
338
|
-
previousHoveredPanelLink = activeLink;
|
|
339
|
-
|
|
340
|
-
setFinalPanelActivePositions(true);
|
|
341
|
-
setFinalPanelHoveredPositions(true);
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
345
|
-
|
|
346
|
-
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, activeHref, isActiveItem, toggleMenu, closeMenu } =
|
|
347
|
-
useNavCollapse("site-nav-loaded", {
|
|
348
|
-
onResize: async () => {
|
|
349
|
-
await nextTick(); // wait for Vue to re-render after any isCollapsed change
|
|
350
|
-
initNavDecorators();
|
|
351
|
-
if (!hasFirstLayout) {
|
|
352
|
-
hasFirstLayout = true;
|
|
353
|
-
// RAF here (not nextTick) ensures the browser paints the snapped position
|
|
354
|
-
// before transitions are enabled — avoids animating from x=0 on first layout.
|
|
355
|
-
requestAnimationFrame(() => {
|
|
356
|
-
isAnimated.value = true;
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
if (isMenuOpen.value) {
|
|
360
|
-
setFinalPanelActivePositions(true);
|
|
361
|
-
setFinalPanelHoveredPositions(true);
|
|
362
|
-
}
|
|
363
|
-
},
|
|
364
|
-
onRouteChange: () => {
|
|
365
|
-
isAnimated.value = false;
|
|
366
|
-
// Double RAF: RAF1 sets the new position and lets the browser paint it
|
|
367
|
-
// without transitions. RAF2 re-enables transitions only after that frame
|
|
368
|
-
// has committed — so there is no position delta left to animate.
|
|
369
|
-
requestAnimationFrame(() => {
|
|
370
|
-
initNavDecorators();
|
|
371
|
-
requestAnimationFrame(() => {
|
|
372
|
-
isAnimated.value = true;
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
},
|
|
376
|
-
onMounted: () => {
|
|
377
|
-
initNavDecorators();
|
|
378
|
-
// isAnimated is set in onResize — ResizeObserver guarantees layout is
|
|
379
|
-
// complete before we enable transitions, unlike nextTick / RAF.
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
watch(isMenuOpen, async (open) => {
|
|
384
|
-
if (open) {
|
|
385
|
-
await nextTick();
|
|
386
|
-
initPanelDecorators();
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
391
|
-
|
|
392
|
-
watch(
|
|
393
|
-
() => props.styleClassPassthrough,
|
|
394
|
-
() => resetElementClasses(props.styleClassPassthrough)
|
|
395
|
-
);
|
|
396
|
-
</script>
|
|
397
|
-
|
|
398
|
-
<style lang="css">
|
|
399
|
-
@layer components {
|
|
400
|
-
.site-nav-backdrop {
|
|
401
|
-
position: fixed;
|
|
402
|
-
inset: 0;
|
|
403
|
-
z-index: var(--site-nav-backdrop-z-index, 10);
|
|
404
|
-
background: var(--site-nav-backdrop-bg, oklch(0% 0 0 / 55%));
|
|
405
|
-
backdrop-filter: blur(var(--site-nav-backdrop-blur, 3px));
|
|
406
|
-
opacity: 0;
|
|
407
|
-
pointer-events: none;
|
|
408
|
-
transition: opacity var(--site-nav-backdrop-duration, 350ms) ease;
|
|
409
|
-
|
|
410
|
-
&.is-open {
|
|
411
|
-
opacity: 1;
|
|
412
|
-
pointer-events: auto;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
.site-navigation {
|
|
417
|
-
/* ─── Public token API ────────────────────────────────────────────── */
|
|
418
|
-
|
|
419
|
-
/* Horizontal nav */
|
|
420
|
-
--_link-color: var(--site-nav-link-color, var(--slate-01, currentColor));
|
|
421
|
-
--_link-size: var(--site-nav-link-size, 1.6rem);
|
|
422
|
-
--_link-tracking: var(--site-nav-link-tracking, 0.06em);
|
|
423
|
-
--_link-weight: var(--site-nav-link-weight, 400);
|
|
424
|
-
--_nav-transition: var(--site-nav-transition, 250ms ease);
|
|
425
|
-
|
|
426
|
-
/* Panel */
|
|
427
|
-
--_panel-border-color: var(
|
|
428
|
-
--site-nav-panel-border-color,
|
|
429
|
-
color-mix(in oklch, var(--slate-01, #c0847a) 35%, transparent)
|
|
430
|
-
);
|
|
431
|
-
--_panel-item-border: var(
|
|
432
|
-
--site-nav-panel-item-border,
|
|
433
|
-
color-mix(in oklch, var(--slate-01, white) 8%, transparent)
|
|
434
|
-
);
|
|
435
|
-
--_panel-link-color: var(--site-nav-panel-link-color, var(--slate-01, currentColor));
|
|
436
|
-
--_panel-slide-duration: var(--site-nav-panel-slide-duration, 350ms);
|
|
437
|
-
--_panel-slide-easing: var(--site-nav-panel-slide-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
|
438
|
-
|
|
439
|
-
/* Burger */
|
|
440
|
-
--_burger-bar-height: var(--site-nav-burger-height, 1.5px);
|
|
441
|
-
--_burger-bar-gap: var(--site-nav-burger-gap, 5px);
|
|
442
|
-
--_burger-color: var(--site-nav-burger-color, var(--slate-01, currentColor));
|
|
443
|
-
--_burger-transition: var(--site-nav-burger-transition, 300ms ease);
|
|
444
|
-
|
|
445
|
-
/* ─────────────────────────────────────────────────────────────────── */
|
|
446
|
-
|
|
447
|
-
display: flex;
|
|
448
|
-
align-items: center;
|
|
449
|
-
min-width: 0;
|
|
450
|
-
|
|
451
|
-
/* Hide everything until first measurement to prevent wrong-state flash */
|
|
452
|
-
&:not(.is-loaded) {
|
|
453
|
-
opacity: 0;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/* ─── Horizontal list ───────────────────────────────────────────── */
|
|
457
|
-
|
|
458
|
-
.site-nav-list {
|
|
459
|
-
list-style: none;
|
|
460
|
-
margin: 0;
|
|
461
|
-
padding: 0;
|
|
462
|
-
display: flex;
|
|
463
|
-
gap: var(--site-nav-gap, 2.2rem);
|
|
464
|
-
align-items: center;
|
|
465
|
-
position: relative;
|
|
466
|
-
|
|
467
|
-
.nav-indicator-li {
|
|
468
|
-
position: absolute;
|
|
469
|
-
inset: 0;
|
|
470
|
-
pointer-events: none;
|
|
471
|
-
list-style: none;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
.nav__hovered,
|
|
475
|
-
.nav__active,
|
|
476
|
-
.nav__active-indicator {
|
|
477
|
-
position: absolute;
|
|
478
|
-
left: 0;
|
|
479
|
-
top: 0;
|
|
480
|
-
bottom: 0;
|
|
481
|
-
right: 0;
|
|
482
|
-
scale: var(--_width-hovered, 0.001) 1;
|
|
483
|
-
translate: var(--_x-hovered, 0) 0;
|
|
484
|
-
transform-origin: left;
|
|
485
|
-
pointer-events: none;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
.site-navigation.is-animated & .nav__hovered,
|
|
489
|
-
.site-navigation.is-animated & .nav__active,
|
|
490
|
-
.site-navigation.is-animated & .nav__active-indicator {
|
|
491
|
-
transition:
|
|
492
|
-
scale var(--_transition-duration, 200ms),
|
|
493
|
-
translate var(--_transition-duration, 200ms);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
.nav__active {
|
|
497
|
-
scale: var(--_width-active, 0.001) 1;
|
|
498
|
-
translate: var(--_x-active, 0) 0;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
.nav__active-indicator {
|
|
502
|
-
scale: var(--_width-hovered, 0.001) 1;
|
|
503
|
-
translate: var(--_x-hovered, 0) 0;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
.nav__hovered {
|
|
507
|
-
background: var(--site-nav-decorator-hovered-bg, transparent);
|
|
508
|
-
border-radius: 4px;
|
|
509
|
-
z-index: 1;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
.nav__active {
|
|
513
|
-
background: var(--site-nav-decorator-active-bg, transparent);
|
|
514
|
-
border-radius: 4px;
|
|
515
|
-
z-index: 2;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
.nav__active-indicator {
|
|
519
|
-
top: auto;
|
|
520
|
-
height: 2px;
|
|
521
|
-
background-color: var(--site-nav-decorator-indicator-color, var(--slate-01, currentColor));
|
|
522
|
-
z-index: 3;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
.site-nav-link {
|
|
526
|
-
display: flex;
|
|
527
|
-
align-items: center;
|
|
528
|
-
gap: 0.4em;
|
|
529
|
-
color: var(--_link-color);
|
|
530
|
-
font-size: var(--_link-size);
|
|
531
|
-
font-weight: var(--_link-weight);
|
|
532
|
-
letter-spacing: var(--_link-tracking);
|
|
533
|
-
text-decoration: none;
|
|
534
|
-
white-space: nowrap;
|
|
535
|
-
padding-block: 0.8rem;
|
|
536
|
-
padding-inline: 0.4rem;
|
|
537
|
-
position: relative;
|
|
538
|
-
z-index: 4;
|
|
539
|
-
transition: color var(--_nav-transition);
|
|
540
|
-
|
|
541
|
-
&:hover,
|
|
542
|
-
&:focus-visible {
|
|
543
|
-
color: var(--site-nav-link-hover-color, var(--slate-04, var(--_link-color)));
|
|
544
|
-
outline: none;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
li.is-active .site-nav-link {
|
|
548
|
-
color: var(--site-nav-link-active-color, var(--slate-01, var(--_link-color)));
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/* ─── Alignment variants ────────────────────────────────────────── */
|
|
553
|
-
|
|
554
|
-
&.site-navigation--center .site-nav-list {
|
|
555
|
-
margin-inline: auto;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
&.site-navigation--right .site-nav-list {
|
|
559
|
-
margin-inline-start: auto;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
&.is-collapsed {
|
|
563
|
-
justify-content: end;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/* ─── Burger button (InputButton) ──────────────────────────── */
|
|
567
|
-
|
|
568
|
-
.site-nav-burger.input-button.icon-only {
|
|
569
|
-
margin-inline-start: auto;
|
|
570
|
-
color: var(--_burger-color);
|
|
571
|
-
|
|
572
|
-
/* Strip all InputButton visual styling */
|
|
573
|
-
background: none;
|
|
574
|
-
border: none;
|
|
575
|
-
outline: none;
|
|
576
|
-
text-decoration: none;
|
|
577
|
-
padding: 8px;
|
|
578
|
-
border-radius: 4px;
|
|
579
|
-
transition: outline-color var(--_nav-transition);
|
|
580
|
-
|
|
581
|
-
/* Override icon-only aspect-ratio and inner icon margin */
|
|
582
|
-
&.icon-only {
|
|
583
|
-
aspect-ratio: unset;
|
|
584
|
-
border-radius: 4px;
|
|
585
|
-
|
|
586
|
-
.btn-icon {
|
|
587
|
-
margin: 0;
|
|
588
|
-
display: flex;
|
|
589
|
-
flex-direction: column;
|
|
590
|
-
gap: var(--_burger-bar-gap);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
&:focus-visible {
|
|
595
|
-
outline: 2px solid var(--_burger-color);
|
|
596
|
-
outline-offset: 4px;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
.burger-bar {
|
|
601
|
-
display: block;
|
|
602
|
-
width: var(--site-nav-burger-width, 22px);
|
|
603
|
-
height: var(--_burger-bar-height);
|
|
604
|
-
background: currentColor;
|
|
605
|
-
border-radius: 1px;
|
|
606
|
-
transform-origin: center;
|
|
607
|
-
transition:
|
|
608
|
-
transform var(--_burger-transition),
|
|
609
|
-
opacity var(--_burger-transition);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
.site-nav-burger.is-open {
|
|
613
|
-
.burger-bar:nth-child(1) {
|
|
614
|
-
transform: translateY(calc(var(--_burger-bar-height) + var(--_burger-bar-gap))) rotate(45deg);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
.burger-bar:nth-child(2) {
|
|
618
|
-
opacity: 0;
|
|
619
|
-
transform: scaleX(0);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
.burger-bar:nth-child(3) {
|
|
623
|
-
transform: translateY(calc(-1 * (var(--_burger-bar-height) + var(--_burger-bar-gap)))) rotate(-45deg);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/* ─── Mobile drop panel ─────────────────────────────────────────── */
|
|
628
|
-
|
|
629
|
-
.site-nav-panel {
|
|
630
|
-
position: absolute;
|
|
631
|
-
left: 0;
|
|
632
|
-
right: 0;
|
|
633
|
-
top: 100%;
|
|
634
|
-
|
|
635
|
-
display: grid;
|
|
636
|
-
grid-template-rows: 0fr;
|
|
637
|
-
border-block-start: 1px solid transparent;
|
|
638
|
-
transition:
|
|
639
|
-
grid-template-rows var(--_panel-slide-duration) var(--_panel-slide-easing),
|
|
640
|
-
border-color var(--_panel-slide-duration) var(--_panel-slide-easing);
|
|
641
|
-
|
|
642
|
-
z-index: 1;
|
|
643
|
-
|
|
644
|
-
&.is-open {
|
|
645
|
-
grid-template-rows: 1fr;
|
|
646
|
-
border-block-start-color: var(--_panel-border-color);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
.site-nav-panel-inner {
|
|
650
|
-
overflow: hidden;
|
|
651
|
-
background-color: var(--site-nav-panel-bg, var(--page-bg, #1a1614));
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
.site-nav-panel-list {
|
|
655
|
-
list-style: none;
|
|
656
|
-
margin: 0;
|
|
657
|
-
padding: 0;
|
|
658
|
-
position: relative;
|
|
659
|
-
|
|
660
|
-
.nav-indicator-li {
|
|
661
|
-
position: absolute;
|
|
662
|
-
inset: 0;
|
|
663
|
-
pointer-events: none;
|
|
664
|
-
list-style: none;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
.nav__hovered,
|
|
668
|
-
.nav__active,
|
|
669
|
-
.nav__active-indicator {
|
|
670
|
-
position: absolute;
|
|
671
|
-
left: 0;
|
|
672
|
-
top: 0;
|
|
673
|
-
bottom: 0;
|
|
674
|
-
right: 0;
|
|
675
|
-
scale: 1 var(--_panel-height-hovered, 0.001);
|
|
676
|
-
translate: 0 var(--_panel-y-hovered, 0);
|
|
677
|
-
transform-origin: top;
|
|
678
|
-
pointer-events: none;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
.site-navigation.is-animated & .nav__hovered,
|
|
682
|
-
.site-navigation.is-animated & .nav__active,
|
|
683
|
-
.site-navigation.is-animated & .nav__active-indicator {
|
|
684
|
-
transition:
|
|
685
|
-
scale var(--_panel-transition-duration, 200ms),
|
|
686
|
-
translate var(--_panel-transition-duration, 200ms);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
.nav__active {
|
|
690
|
-
scale: 1 var(--_panel-height-active, 0.001);
|
|
691
|
-
translate: 0 var(--_panel-y-active, 0);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
.nav__active-indicator {
|
|
695
|
-
left: var(--site-nav-panel-indicator-left, 0);
|
|
696
|
-
right: var(--site-nav-panel-indicator-right, auto);
|
|
697
|
-
width: 2px;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
.nav__hovered {
|
|
701
|
-
background: var(--site-nav-panel-decorator-hovered-bg, transparent);
|
|
702
|
-
z-index: 1;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
.nav__active {
|
|
706
|
-
background: var(--site-nav-panel-decorator-active-bg, transparent);
|
|
707
|
-
z-index: 2;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
.nav__active-indicator {
|
|
711
|
-
background: var(--site-nav-panel-decorator-indicator-color, var(--slate-01, currentColor));
|
|
712
|
-
z-index: 3;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
li {
|
|
716
|
-
border-block-end: 1px solid var(--_panel-item-border);
|
|
717
|
-
|
|
718
|
-
&:last-child {
|
|
719
|
-
border-block-end: none;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
.site-nav-panel-link {
|
|
724
|
-
display: flex;
|
|
725
|
-
align-items: center;
|
|
726
|
-
gap: 0.5em;
|
|
727
|
-
color: var(--_panel-link-color);
|
|
728
|
-
font-size: var(--_link-size);
|
|
729
|
-
font-weight: var(--_link-weight);
|
|
730
|
-
letter-spacing: var(--_link-tracking);
|
|
731
|
-
text-decoration: none;
|
|
732
|
-
padding-block: var(--site-nav-panel-padding-block, 1.4rem);
|
|
733
|
-
padding-inline: var(--site-nav-panel-padding-inline, 1.5rem);
|
|
734
|
-
position: relative;
|
|
735
|
-
z-index: 4;
|
|
736
|
-
transition: color var(--_nav-transition);
|
|
737
|
-
|
|
738
|
-
&:hover,
|
|
739
|
-
&:focus-visible {
|
|
740
|
-
color: var(--site-nav-panel-link-hover-color, var(--slate-04, var(--_panel-link-color)));
|
|
741
|
-
outline: none;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
li.is-active .site-nav-panel-link {
|
|
745
|
-
color: var(--site-nav-panel-link-active-color, var(--slate-01, var(--_panel-link-color)));
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
</style>
|