srcdev-nuxt-components 9.5.6 → 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/component-compliance-checklist.md +6 -2
- 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/navigation-items.md +1 -0
- package/.claude/skills/components/opening-hours.md +12 -0
- package/.claude/skills/components/price-list.md +10 -1
- package/.claude/skills/components/responsive-header.md +74 -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/site-header.md +2 -0
- 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/.claude/skills/storybook-add-story.md +14 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +13 -1
- 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-site-header.code-snippets +3 -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/02.molecules/opening-hours/CONSUMER-STYLING.md +11 -1
- package/app/components/02.molecules/opening-hours/OpeningHours.vue +71 -43
- package/app/components/02.molecules/opening-hours/stories/OpeningHours.stories.ts +138 -6
- package/app/components/02.molecules/opening-hours/tests/OpeningHours.spec.ts +53 -0
- package/app/components/02.molecules/opening-hours/tests/__snapshots__/OpeningHours.spec.ts.snap +1 -1
- package/app/components/02.molecules/opening-hours/tests/buildOpeningHoursSpecification.spec.ts +37 -1
- package/app/components/02.molecules/price-list/CONSUMER-STYLING.md +17 -5
- package/app/components/02.molecules/price-list/PriceList.vue +32 -9
- package/app/components/02.molecules/price-list/stories/PriceList.stories.ts +125 -29
- package/app/components/02.molecules/price-list/tests/PriceList.spec.ts +20 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +9 -4
- 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 +18 -0
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +56 -18
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +106 -31
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +201 -18
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +21 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +66 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +30 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +35 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +23 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +71 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +386 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +11 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +79 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +17 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +64 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +29 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +32 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +10 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +36 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +131 -6
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +9 -9
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +1 -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/03.organisms/site-header/SiteHeader.vue +8 -0
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- 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/app/utils/opening-hours.ts +23 -2
- 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
|
@@ -27,15 +27,23 @@
|
|
|
27
27
|
'is-active': isActiveNavItem(link),
|
|
28
28
|
}"
|
|
29
29
|
:style="{
|
|
30
|
-
'--_main-navigation-item-width':
|
|
31
|
-
mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.width + 'px',
|
|
30
|
+
'--_main-navigation-item-width': itemWidth(mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]),
|
|
32
31
|
}"
|
|
33
32
|
:data-group-key="groupKey"
|
|
34
33
|
:data-local-index="localIndex"
|
|
35
34
|
role="none"
|
|
36
35
|
@mouseenter="hoveredItemKey = `${String(groupKey)}-${localIndex}`"
|
|
37
36
|
>
|
|
38
|
-
<
|
|
37
|
+
<a
|
|
38
|
+
v-if="isAnchorPath(link.path)"
|
|
39
|
+
class="overflow-navigation-link"
|
|
40
|
+
:href="link.path"
|
|
41
|
+
role="menuitem"
|
|
42
|
+
@click="emit('anchorClick', $event, link.path)"
|
|
43
|
+
>
|
|
44
|
+
<span class="overflow-navigation-text">{{ link.name }}</span>
|
|
45
|
+
</a>
|
|
46
|
+
<NuxtLink v-else class="overflow-navigation-link" :to="link.path" role="menuitem">
|
|
39
47
|
<span class="overflow-navigation-text">{{ link.name }}</span>
|
|
40
48
|
</NuxtLink>
|
|
41
49
|
</li>
|
|
@@ -48,8 +56,7 @@
|
|
|
48
56
|
'is-active': isActiveNavItem(link),
|
|
49
57
|
}"
|
|
50
58
|
:style="{
|
|
51
|
-
'--_main-navigation-item-width':
|
|
52
|
-
mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.width + 'px',
|
|
59
|
+
'--_main-navigation-item-width': itemWidth(mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]),
|
|
53
60
|
}"
|
|
54
61
|
:data-group-key="groupKey"
|
|
55
62
|
:data-local-index="localIndex"
|
|
@@ -66,11 +73,11 @@
|
|
|
66
73
|
<template #summary>
|
|
67
74
|
<span
|
|
68
75
|
class="overflow-navigation-text"
|
|
69
|
-
:aria-label="
|
|
76
|
+
:aria-label="submenuLabel(link)"
|
|
70
77
|
role="menuitem"
|
|
71
78
|
:aria-haspopup="true"
|
|
72
79
|
>
|
|
73
|
-
{{ link.childLinksTitle }}
|
|
80
|
+
{{ link.childLinksTitle ?? link.name }}
|
|
74
81
|
</span>
|
|
75
82
|
</template>
|
|
76
83
|
<template #icon>
|
|
@@ -81,7 +88,7 @@
|
|
|
81
88
|
<ul class="overflow-navigation-sub-nav-list" @mouseleave="hoveredChildKey = null">
|
|
82
89
|
<li
|
|
83
90
|
v-for="(childLink, childIndex) in link.childLinks"
|
|
84
|
-
:key="
|
|
91
|
+
:key="childIndex"
|
|
85
92
|
class="overflow-navigation-sub-nav-item"
|
|
86
93
|
:class="{
|
|
87
94
|
'is-hovered': hoveredChildKey === `${String(groupKey)}-${localIndex}-${childIndex}`,
|
|
@@ -89,7 +96,16 @@
|
|
|
89
96
|
}"
|
|
90
97
|
@mouseenter="hoveredChildKey = `${String(groupKey)}-${localIndex}-${childIndex}`"
|
|
91
98
|
>
|
|
92
|
-
<
|
|
99
|
+
<a
|
|
100
|
+
v-if="childLink.path && isAnchorPath(childLink.path)"
|
|
101
|
+
:href="childLink.path"
|
|
102
|
+
class="overflow-navigation-sub-nav-link"
|
|
103
|
+
role="menuitem"
|
|
104
|
+
@click="emit('anchorClick', $event, childLink.path)"
|
|
105
|
+
>
|
|
106
|
+
<span class="overflow-navigation-sub-nav-text">{{ childLink.name }}</span>
|
|
107
|
+
</a>
|
|
108
|
+
<NuxtLink v-else :to="childLink.path" class="overflow-navigation-sub-nav-link" role="menuitem">
|
|
93
109
|
<span class="overflow-navigation-sub-nav-text">{{ childLink.name }}</span>
|
|
94
110
|
</NuxtLink>
|
|
95
111
|
</li>
|
|
@@ -117,6 +133,10 @@ interface Props {
|
|
|
117
133
|
styleClassPassthrough?: string | string[];
|
|
118
134
|
/** aria-label on the overflow menu — override for localisation. */
|
|
119
135
|
ariaLabel?: string;
|
|
136
|
+
/** aria-label on each submenu summary; {title} is replaced with the item's title. */
|
|
137
|
+
submenuAriaLabel?: string;
|
|
138
|
+
/** The active "#anchor" path, owned by ResponsiveHeader so both menus agree. */
|
|
139
|
+
activeHash?: string;
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -127,8 +147,17 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
127
147
|
panelVariant: "classic",
|
|
128
148
|
styleClassPassthrough: () => [],
|
|
129
149
|
ariaLabel: "Overflow navigation menu",
|
|
150
|
+
submenuAriaLabel: "{title} submenu",
|
|
151
|
+
activeHash: "",
|
|
130
152
|
});
|
|
131
153
|
|
|
154
|
+
const emit = defineEmits<{
|
|
155
|
+
anchorClick: [event: MouseEvent, href: string];
|
|
156
|
+
}>();
|
|
157
|
+
|
|
158
|
+
const submenuLabel = (link: ResponsiveHeaderNavItem) =>
|
|
159
|
+
props.submenuAriaLabel.replace("{title}", link.childLinksTitle ?? link.name);
|
|
160
|
+
|
|
132
161
|
const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
|
|
133
162
|
|
|
134
163
|
const hoveredItemKey = ref<string | null>(null);
|
|
@@ -158,9 +187,13 @@ onUnmounted(() => {
|
|
|
158
187
|
|
|
159
188
|
const route = useRoute();
|
|
160
189
|
|
|
190
|
+
const isAnchorPath = (path?: string) => Boolean(path?.startsWith("#"));
|
|
191
|
+
const isCurrentPath = (path?: string) =>
|
|
192
|
+
Boolean(path) && (isAnchorPath(path) ? path === props.activeHash : path === route.path);
|
|
193
|
+
|
|
161
194
|
const isActiveNavItem = (link: ResponsiveHeaderNavItem): boolean => {
|
|
162
|
-
if (link.path) return
|
|
163
|
-
if (link.childLinks) return link.childLinks.some((child) => child.path
|
|
195
|
+
if (link.path) return isCurrentPath(link.path);
|
|
196
|
+
if (link.childLinks) return link.childLinks.some((child) => isCurrentPath(child.path));
|
|
164
197
|
return false;
|
|
165
198
|
};
|
|
166
199
|
// const DETAILS_ANIMATION_DURATION_STRING = `${DETAILS_ANIMATION_DURATION}ms` as const;
|
|
@@ -178,6 +211,9 @@ const widestNavLinkWidthInMainNavigationState = computed(() => {
|
|
|
178
211
|
}, 0);
|
|
179
212
|
});
|
|
180
213
|
|
|
214
|
+
const itemWidth = (link?: ResponsiveHeaderNavItem) =>
|
|
215
|
+
link?.config?.width === undefined ? undefined : `${link.config.width}px`;
|
|
216
|
+
|
|
181
217
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
182
218
|
|
|
183
219
|
// Performance: Use immediate flag and more efficient watching
|
|
@@ -199,6 +235,7 @@ watch(
|
|
|
199
235
|
on this element to avoid cascade conflicts with ancestor overrides.
|
|
200
236
|
|
|
201
237
|
--overflow-nav-padding-inline (default: 0.8rem)
|
|
238
|
+
--overflow-nav-max-inline-size (default: calc(100vw - 3.2rem))
|
|
202
239
|
--overflow-nav-items-gap (default: 0px)
|
|
203
240
|
--overflow-nav-items-padding-block (default: 0.8rem)
|
|
204
241
|
|
|
@@ -212,10 +249,15 @@ watch(
|
|
|
212
249
|
--overflow-nav-decorator-hovered-bg (default: oklch(100% 0 0 / 6%))
|
|
213
250
|
──────────────────────────────────────────────────────────────────────── */
|
|
214
251
|
|
|
252
|
+
--_max-inline-size: var(--overflow-nav-max-inline-size, calc(100vw - 3.2rem));
|
|
253
|
+
|
|
215
254
|
display: flex;
|
|
216
255
|
flex-direction: column;
|
|
217
256
|
gap: var(--overflow-nav-items-gap, 0px);
|
|
218
257
|
position: relative;
|
|
258
|
+
inline-size: max-content;
|
|
259
|
+
max-inline-size: var(--_max-inline-size);
|
|
260
|
+
overflow-wrap: anywhere;
|
|
219
261
|
|
|
220
262
|
.overflow-navigation-list {
|
|
221
263
|
display: none;
|
|
@@ -224,7 +266,7 @@ watch(
|
|
|
224
266
|
display: flex;
|
|
225
267
|
flex-direction: column;
|
|
226
268
|
gap: var(--overflow-nav-items-gap, 0px);
|
|
227
|
-
min-width: var(--_overflow-navigation-list-min-width,
|
|
269
|
+
min-width: min(var(--_overflow-navigation-list-min-width, 0px), var(--_max-inline-size));
|
|
228
270
|
}
|
|
229
271
|
|
|
230
272
|
.overflow-navigation-item {
|
|
@@ -262,9 +304,7 @@ watch(
|
|
|
262
304
|
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
|
|
263
305
|
|
|
264
306
|
.label-wrapper {
|
|
265
|
-
|
|
266
|
-
white-space: nowrap;
|
|
267
|
-
}
|
|
307
|
+
min-inline-size: 0;
|
|
268
308
|
}
|
|
269
309
|
.icon-wrapper {
|
|
270
310
|
padding: 0;
|
|
@@ -325,9 +365,7 @@ watch(
|
|
|
325
365
|
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
|
|
326
366
|
|
|
327
367
|
.label-wrapper {
|
|
328
|
-
|
|
329
|
-
white-space: nowrap;
|
|
330
|
-
}
|
|
368
|
+
min-inline-size: 0;
|
|
331
369
|
}
|
|
332
370
|
.icon-wrapper {
|
|
333
371
|
padding: 0;
|
|
@@ -32,16 +32,25 @@
|
|
|
32
32
|
'is-active': isActiveNavItem(link),
|
|
33
33
|
}"
|
|
34
34
|
:style="{
|
|
35
|
-
'--_main-navigation-item-width':
|
|
36
|
-
mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.width + 'px',
|
|
35
|
+
'--_main-navigation-item-width': itemWidth(mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]),
|
|
37
36
|
}"
|
|
38
37
|
:data-group-key="groupKey"
|
|
39
38
|
:data-local-index="localIndex"
|
|
40
39
|
@mouseenter="handleNavigationItemHover(`${String(groupKey)}-${localIndex}`)"
|
|
41
40
|
@focusin="handleNavigationItemHover(`${String(groupKey)}-${localIndex}`)"
|
|
42
41
|
>
|
|
42
|
+
<a
|
|
43
|
+
v-if="link.path && isAnchorPath(link.path)"
|
|
44
|
+
class="main-navigation-link"
|
|
45
|
+
:class="{ 'has-icon': link.iconName }"
|
|
46
|
+
:href="link.path"
|
|
47
|
+
@click="handleAnchorClick($event, link.path)"
|
|
48
|
+
>
|
|
49
|
+
<Icon v-if="link.iconName" :name="link.iconName" class="decorator-icon" aria-hidden="true" />
|
|
50
|
+
{{ link.name }}
|
|
51
|
+
</a>
|
|
43
52
|
<NuxtLink
|
|
44
|
-
v-if="link.path"
|
|
53
|
+
v-else-if="link.path"
|
|
45
54
|
class="main-navigation-link"
|
|
46
55
|
:class="{ 'has-icon': link.iconName }"
|
|
47
56
|
:to="link.path"
|
|
@@ -52,7 +61,7 @@
|
|
|
52
61
|
<details v-else ref="navigationDetails" class="main-navigation-details" name="navigation-group">
|
|
53
62
|
<summary
|
|
54
63
|
class="main-navigation-details-summary has-toggle-icon"
|
|
55
|
-
:aria-label="
|
|
64
|
+
:aria-label="submenuLabel(link)"
|
|
56
65
|
@mouseenter="handleSummaryHover($event)"
|
|
57
66
|
@focusin="handleSummaryHover($event)"
|
|
58
67
|
@click.prevent="handleSummaryAction($event)"
|
|
@@ -61,12 +70,25 @@
|
|
|
61
70
|
<Icon name="mdi:chevron-down" class="icon" :aria-hidden="true" />
|
|
62
71
|
<Icon v-if="link.iconName" :name="link.iconName" class="decorator-icon" aria-hidden="true" />
|
|
63
72
|
|
|
64
|
-
{{ link.childLinksTitle }}
|
|
73
|
+
{{ link.childLinksTitle ?? link.name }}
|
|
65
74
|
</summary>
|
|
66
75
|
<div class="main-navigation-sub-nav" role="menu" @mouseenter="handleSubNavHover">
|
|
67
76
|
<ul class="main-navigation-sub-nav-list">
|
|
68
|
-
<li
|
|
69
|
-
|
|
77
|
+
<li
|
|
78
|
+
v-for="(childLink, childIndex) in link.childLinks"
|
|
79
|
+
:key="childIndex"
|
|
80
|
+
class="main-navigation-sub-nav-item"
|
|
81
|
+
>
|
|
82
|
+
<a
|
|
83
|
+
v-if="childLink.path && isAnchorPath(childLink.path)"
|
|
84
|
+
:href="childLink.path"
|
|
85
|
+
class="main-navigation-sub-nav-link"
|
|
86
|
+
role="menuitem"
|
|
87
|
+
@click="handleAnchorClick($event, childLink.path)"
|
|
88
|
+
>
|
|
89
|
+
{{ childLink.name }}
|
|
90
|
+
</a>
|
|
91
|
+
<NuxtLink v-else :to="childLink.path" class="main-navigation-sub-nav-link" role="menuitem">
|
|
70
92
|
{{ childLink.name }}
|
|
71
93
|
</NuxtLink>
|
|
72
94
|
</li>
|
|
@@ -85,7 +107,7 @@
|
|
|
85
107
|
:class="overflowDetailsClass ? [overflowDetailsClass] : []"
|
|
86
108
|
name="overflow-group"
|
|
87
109
|
>
|
|
88
|
-
<summary class="overflow-details-summary has-toggle-icon">
|
|
110
|
+
<summary class="overflow-details-summary has-toggle-icon" :aria-label="overflowButtonLabel">
|
|
89
111
|
<Icon
|
|
90
112
|
:name="overflowDetailsSummaryIcons.more ?? 'gravity-ui:ellipsis'"
|
|
91
113
|
class="icon"
|
|
@@ -104,6 +126,9 @@
|
|
|
104
126
|
:main-navigation-state="mainNavigationState"
|
|
105
127
|
:panel-variant="panelVariant"
|
|
106
128
|
:aria-label="overflowMenuAriaLabel"
|
|
129
|
+
:submenu-aria-label="submenuAriaLabel"
|
|
130
|
+
:active-hash="activeHash"
|
|
131
|
+
@anchor-click="handleAnchorClick"
|
|
107
132
|
/>
|
|
108
133
|
</div>
|
|
109
134
|
</details>
|
|
@@ -136,6 +161,15 @@ interface Props {
|
|
|
136
161
|
secondaryNavAriaLabel?: string;
|
|
137
162
|
/** aria-label on the overflow menu itself (forwarded to NavigationItems) — override for localisation. */
|
|
138
163
|
overflowMenuAriaLabel?: string;
|
|
164
|
+
/** Accessible name of the overflow/burger button, which shows only icons. */
|
|
165
|
+
overflowButtonLabel?: string;
|
|
166
|
+
/** aria-label on each dropdown summary; {title} is replaced with the item's title. Forwarded to NavigationItems. */
|
|
167
|
+
submenuAriaLabel?: string;
|
|
168
|
+
/**
|
|
169
|
+
* Pixels to leave above a section when a same-page "#anchor" link scrolls to it (e.g. a sticky
|
|
170
|
+
* header's height). Pass a getter to read it at click time.
|
|
171
|
+
*/
|
|
172
|
+
anchorScrollOffset?: number | (() => number);
|
|
139
173
|
}
|
|
140
174
|
|
|
141
175
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -155,8 +189,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
155
189
|
mainNavAriaLabel: "Main navigation",
|
|
156
190
|
secondaryNavAriaLabel: "Secondary navigation",
|
|
157
191
|
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
192
|
+
overflowButtonLabel: "More navigation",
|
|
193
|
+
submenuAriaLabel: "{title} submenu",
|
|
194
|
+
anchorScrollOffset: undefined,
|
|
158
195
|
});
|
|
159
196
|
|
|
197
|
+
const submenuLabel = (link: ResponsiveHeaderNavItem) =>
|
|
198
|
+
props.submenuAriaLabel.replace("{title}", link.childLinksTitle ?? link.name);
|
|
199
|
+
|
|
160
200
|
const collapseNavigationBelowWidth = computed(
|
|
161
201
|
() => props.collapseBreakpoint !== null || props.collapseAtMainNavIntersection
|
|
162
202
|
);
|
|
@@ -173,6 +213,7 @@ const {
|
|
|
173
213
|
const slots = useSlots();
|
|
174
214
|
|
|
175
215
|
const navigationWrapperRef = useTemplateRef<HTMLDivElement>("navigationWrapper");
|
|
216
|
+
const mainNavRef = useTemplateRef<HTMLElement>("mainNav");
|
|
176
217
|
|
|
177
218
|
const closeAllNavigationDetails = () => {
|
|
178
219
|
navigationDetailsRefs.value?.forEach((element) => {
|
|
@@ -301,12 +342,32 @@ const hoveredItemKey = ref<string | null>(null);
|
|
|
301
342
|
|
|
302
343
|
const route = useRoute();
|
|
303
344
|
|
|
345
|
+
// "#anchor" paths are same-page section links: plain <a>, smooth-scrolled, active by hash.
|
|
346
|
+
const { handleNavClick, activeHash } = useAnchorScroll({ offset: props.anchorScrollOffset });
|
|
347
|
+
const isAnchorPath = (path?: string) => Boolean(path?.startsWith("#"));
|
|
348
|
+
const isCurrentPath = (path?: string) =>
|
|
349
|
+
Boolean(path) && (isAnchorPath(path) ? path === activeHash.value : path === route.path);
|
|
350
|
+
|
|
304
351
|
const isActiveNavItem = (link: ResponsiveHeaderNavItem): boolean => {
|
|
305
|
-
if (link.path) return
|
|
306
|
-
if (link.childLinks) return link.childLinks.some((child) => child.path
|
|
352
|
+
if (link.path) return isCurrentPath(link.path);
|
|
353
|
+
if (link.childLinks) return link.childLinks.some((child) => isCurrentPath(child.path));
|
|
307
354
|
return false;
|
|
308
355
|
};
|
|
309
356
|
|
|
357
|
+
const handleAnchorClick = (event: MouseEvent, href: string) => {
|
|
358
|
+
handleNavClick(event, href);
|
|
359
|
+
closeAllNavigationDetails();
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
onMounted(() => {
|
|
363
|
+
if (activeHash.value) return;
|
|
364
|
+
const firstAnchor = Object.values(props.responsiveNavLinks)
|
|
365
|
+
.flat()
|
|
366
|
+
.flatMap((link) => [link, ...(link.childLinks ?? [])])
|
|
367
|
+
.find((link) => isAnchorPath(link.path));
|
|
368
|
+
if (firstAnchor?.path) activeHash.value = firstAnchor.path;
|
|
369
|
+
});
|
|
370
|
+
|
|
310
371
|
const isAnimated = ref(true);
|
|
311
372
|
|
|
312
373
|
// Local (non-useState) flag — resets to false on every mount.
|
|
@@ -361,7 +422,10 @@ const mainNavigationState = ref<ResponsiveHeaderState>({
|
|
|
361
422
|
firstNav: false,
|
|
362
423
|
secondNav: false,
|
|
363
424
|
},
|
|
364
|
-
|
|
425
|
+
// Copied so the geometry pass's config writes never land in the consumer's own nav data
|
|
426
|
+
clonedNavLinks: Object.fromEntries(
|
|
427
|
+
Object.entries(props.responsiveNavLinks).map(([groupKey, group]) => [groupKey, group.map((link) => ({ ...link }))])
|
|
428
|
+
),
|
|
365
429
|
hasSecondNav: Object.keys(props.responsiveNavLinks).length > 1,
|
|
366
430
|
});
|
|
367
431
|
|
|
@@ -398,12 +462,13 @@ const navigationDetailsRefs = useTemplateRef<HTMLElement[]>("navigationDetails")
|
|
|
398
462
|
|
|
399
463
|
const overflowDetailsRef = useTemplateRef<HTMLDetailsElement>("overflowDetails");
|
|
400
464
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
465
|
+
// Item-based rather than reading navListVisibility.firstNav/secondNav, so any group key works and
|
|
466
|
+
// empty nav data never shows a burger with nothing in it
|
|
467
|
+
const showOverflowDetails = computed(() =>
|
|
468
|
+
Object.values(mainNavigationState.value.clonedNavLinks ?? {}).some((group) =>
|
|
469
|
+
group.some((link) => link.config?.visible === false)
|
|
470
|
+
)
|
|
471
|
+
);
|
|
407
472
|
|
|
408
473
|
const mainNavigationMarginBlockEnd = computed(() => {
|
|
409
474
|
return secondaryNavRects.value ? secondaryNavRects.value.width + props.gapBetweenFirstAndSecondNav : 0;
|
|
@@ -462,20 +527,18 @@ const determineNavigationItemVisibility = (rect: DOMRect) => {
|
|
|
462
527
|
return false;
|
|
463
528
|
}
|
|
464
529
|
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Default to visible
|
|
474
|
-
return true;
|
|
530
|
+
// An item fits if it ends within the main nav, whose margin-inline-end already reserves the
|
|
531
|
+
// secondary nav plus the gap. Measured live with the item so both read the same layout.
|
|
532
|
+
const mainNavRect = mainNavRef.value?.getBoundingClientRect();
|
|
533
|
+
if (!mainNavRect) return true;
|
|
534
|
+
return Math.floor(rect.right) <= Math.floor(mainNavRect.right);
|
|
475
535
|
};
|
|
476
536
|
|
|
477
537
|
const initMainNavigationState = () => {
|
|
478
|
-
if (!mainNavigationItemsRefs.value)
|
|
538
|
+
if (!mainNavigationItemsRefs.value?.length) {
|
|
539
|
+
isGeometryReady.value = true;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
479
542
|
|
|
480
543
|
mainNavigationItemsRefs.value.forEach(async (item) => {
|
|
481
544
|
// await nextTick()
|
|
@@ -642,6 +705,9 @@ onMounted(async () => {
|
|
|
642
705
|
|
|
643
706
|
useResizeObserver(navigationWrapperRef, runGeometryPass);
|
|
644
707
|
|
|
708
|
+
const itemWidth = (link?: ResponsiveHeaderNavItem) =>
|
|
709
|
+
link?.config?.width === undefined ? undefined : `${link.config.width}px`;
|
|
710
|
+
|
|
645
711
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
646
712
|
|
|
647
713
|
watch(
|
|
@@ -690,6 +756,8 @@ watch(
|
|
|
690
756
|
--responsive-header-sub-nav-border (default: 1px solid #efefef75)
|
|
691
757
|
--responsive-header-sub-nav-border-radius (default: 8px)
|
|
692
758
|
--responsive-header-sub-nav-padding (default: 12px)
|
|
759
|
+
--responsive-header-sub-nav-max-inline-size (default: min(48rem, calc(100vw - 3.2rem)))
|
|
760
|
+
--responsive-header-sub-nav-max-block-size (default: 70vh)
|
|
693
761
|
|
|
694
762
|
--responsive-header-overflow-btn-bg (default: Canvas)
|
|
695
763
|
--responsive-header-overflow-btn-size (default: 20px)
|
|
@@ -702,12 +770,14 @@ watch(
|
|
|
702
770
|
--responsive-header-overflow-nav-border (default: 1px solid #ffffff90)
|
|
703
771
|
--responsive-header-overflow-nav-border-radius (default: 8px)
|
|
704
772
|
--responsive-header-overflow-nav-padding-block (default: 12px)
|
|
773
|
+
--responsive-header-overflow-nav-max-block-size (default: 70vh)
|
|
705
774
|
|
|
706
775
|
--responsive-nav-decorator-indicator-color (default: currentColor)
|
|
707
776
|
--responsive-nav-decorator-hovered-indicator-color (default: inherits --responsive-nav-decorator-indicator-color)
|
|
708
777
|
--responsive-nav-decorator-hovered-bg (default: oklch(100% 0 0 / 8%))
|
|
709
778
|
|
|
710
779
|
--responsive-header-link-font-size (default: inherit)
|
|
780
|
+
--responsive-header-main-nav-justify-content (default: space-between)
|
|
711
781
|
──────────────────────────────────────────────────────────────────────── */
|
|
712
782
|
|
|
713
783
|
/* `inherit` means a viewport-relative (vw/clamp) ancestor font-size silently
|
|
@@ -749,7 +819,7 @@ watch(
|
|
|
749
819
|
display: flex;
|
|
750
820
|
flex-wrap: nowrap;
|
|
751
821
|
flex-grow: 1;
|
|
752
|
-
justify-content: space-between;
|
|
822
|
+
justify-content: var(--responsive-header-main-nav-justify-content, space-between);
|
|
753
823
|
gap: 60px;
|
|
754
824
|
overflow-x: hidden;
|
|
755
825
|
margin-inline-end: v-bind(mainNavigationMarginBlockEndStr);
|
|
@@ -868,6 +938,9 @@ watch(
|
|
|
868
938
|
translate: 0 12px;
|
|
869
939
|
|
|
870
940
|
min-width: var(--_main-navigation-item-width);
|
|
941
|
+
max-inline-size: var(--responsive-header-sub-nav-max-inline-size, min(48rem, calc(100vw - 3.2rem)));
|
|
942
|
+
max-block-size: var(--responsive-header-sub-nav-max-block-size, 70vh);
|
|
943
|
+
overflow-y: auto;
|
|
871
944
|
|
|
872
945
|
.main-navigation-sub-nav-list {
|
|
873
946
|
display: grid;
|
|
@@ -883,7 +956,7 @@ watch(
|
|
|
883
956
|
|
|
884
957
|
.main-navigation-sub-nav-link {
|
|
885
958
|
display: block;
|
|
886
|
-
|
|
959
|
+
overflow-wrap: anywhere;
|
|
887
960
|
text-decoration: none;
|
|
888
961
|
color: var(--responsive-header-link-color, inherit);
|
|
889
962
|
}
|
|
@@ -1048,7 +1121,9 @@ watch(
|
|
|
1048
1121
|
padding-block: var(--responsive-header-overflow-nav-padding-block, 12px);
|
|
1049
1122
|
margin: 0;
|
|
1050
1123
|
z-index: 999;
|
|
1051
|
-
min-width:
|
|
1124
|
+
min-width: fit-content;
|
|
1125
|
+
max-block-size: var(--responsive-header-overflow-nav-max-block-size, 70vh);
|
|
1126
|
+
overflow-y: auto;
|
|
1052
1127
|
|
|
1053
1128
|
display: grid;
|
|
1054
1129
|
grid-auto-flow: row;
|