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.
Files changed (113) hide show
  1. package/.claude/skills/component-compliance-checklist.md +6 -2
  2. package/.claude/skills/components/breadcrumb.md +5 -0
  3. package/.claude/skills/components/display-pill.md +2 -0
  4. package/.claude/skills/components/input-button.md +3 -0
  5. package/.claude/skills/components/navigation-items.md +1 -0
  6. package/.claude/skills/components/opening-hours.md +12 -0
  7. package/.claude/skills/components/price-list.md +10 -1
  8. package/.claude/skills/components/responsive-header.md +74 -0
  9. package/.claude/skills/components/service-detail.md +32 -0
  10. package/.claude/skills/components/service-summary.md +8 -0
  11. package/.claude/skills/components/services-card-grid.md +5 -3
  12. package/.claude/skills/components/services-card.md +22 -12
  13. package/.claude/skills/components/site-header.md +2 -0
  14. package/.claude/skills/components/slider-gallery.md +10 -4
  15. package/.claude/skills/components/tab-navigation.md +25 -9
  16. package/.claude/skills/index.md +1 -2
  17. package/.claude/skills/storybook-add-story.md +14 -0
  18. package/.vscode/srcdev-component-responsive-header.code-snippets +13 -1
  19. package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
  20. package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
  21. package/.vscode/srcdev-component-services-card.code-snippets +38 -36
  22. package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
  23. package/.vscode/srcdev-component-site-header.code-snippets +3 -1
  24. package/.vscode/srcdev-component-tab-navigation.code-snippets +1 -1
  25. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +3 -0
  26. package/app/components/01.atoms/display-pill/DisplayPill.vue +5 -2
  27. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
  28. package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
  29. package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
  30. package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
  31. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +52 -11
  32. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +55 -23
  33. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +176 -257
  34. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +42 -2
  35. package/app/components/02.molecules/opening-hours/CONSUMER-STYLING.md +11 -1
  36. package/app/components/02.molecules/opening-hours/OpeningHours.vue +71 -43
  37. package/app/components/02.molecules/opening-hours/stories/OpeningHours.stories.ts +138 -6
  38. package/app/components/02.molecules/opening-hours/tests/OpeningHours.spec.ts +53 -0
  39. package/app/components/02.molecules/opening-hours/tests/__snapshots__/OpeningHours.spec.ts.snap +1 -1
  40. package/app/components/02.molecules/opening-hours/tests/buildOpeningHoursSpecification.spec.ts +37 -1
  41. package/app/components/02.molecules/price-list/CONSUMER-STYLING.md +17 -5
  42. package/app/components/02.molecules/price-list/PriceList.vue +32 -9
  43. package/app/components/02.molecules/price-list/stories/PriceList.stories.ts +125 -29
  44. package/app/components/02.molecules/price-list/tests/PriceList.spec.ts +20 -0
  45. package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +9 -4
  46. package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
  47. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
  48. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
  49. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
  50. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
  51. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +18 -0
  52. package/app/components/03.organisms/responsive-header/NavigationItems.vue +56 -18
  53. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +106 -31
  54. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +201 -18
  55. package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +21 -0
  56. package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +66 -0
  57. package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +30 -0
  58. package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +1 -0
  59. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +35 -0
  60. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +23 -0
  61. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +1 -0
  62. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +71 -0
  63. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +386 -0
  64. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +11 -0
  65. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +79 -0
  66. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +17 -0
  67. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +64 -0
  68. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +29 -0
  69. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +32 -0
  70. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +10 -0
  71. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
  72. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +36 -0
  73. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +131 -6
  74. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +9 -9
  75. package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +1 -0
  76. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
  77. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
  78. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
  79. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
  80. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -3
  81. package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
  82. package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
  83. package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
  84. package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
  85. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
  86. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
  87. package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
  88. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
  89. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
  90. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
  91. package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
  92. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
  93. package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
  94. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
  95. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
  96. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
  97. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
  98. package/app/components/03.organisms/site-header/SiteHeader.vue +8 -0
  99. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  100. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
  101. package/app/components/05.forms/input-button/InputButton.vue +3 -1
  102. package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
  103. package/app/composables/useAnchorScroll.ts +1 -1
  104. package/app/utils/opening-hours.ts +23 -2
  105. package/nuxt.config.ts +1 -1
  106. package/package.json +1 -1
  107. package/.claude/skills/components/site-navigation.md +0 -145
  108. package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
  109. package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
  110. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
  111. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
  112. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
  113. 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
- <NuxtLink class="overflow-navigation-link" :to="link.path" role="menuitem">
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="`${link.childLinksTitle} submenu`"
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="childLink.name"
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
- <NuxtLink :to="childLink.path" class="overflow-navigation-sub-nav-link" role="menuitem">
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 route.path === link.path;
163
- if (link.childLinks) return link.childLinks.some((child) => child.path && route.path === 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, auto);
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
- .overflow-navigation-text {
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
- .overflow-navigation-text {
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="`${link.childLinksTitle} submenu`"
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 v-for="childLink in link.childLinks" :key="childLink.name" class="main-navigation-sub-nav-item">
69
- <NuxtLink :to="childLink.path" class="main-navigation-sub-nav-link" role="menuitem">
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 route.path === link.path;
306
- if (link.childLinks) return link.childLinks.some((child) => child.path && route.path === 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
- clonedNavLinks: props.responsiveNavLinks,
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
- const showOverflowDetails = computed(() => {
402
- const hasHiddenNav =
403
- !mainNavigationState.value.navListVisibility["firstNav"] ||
404
- (!mainNavigationState.value.navListVisibility["secondNav"] && mainNavigationState.value.hasSecondNav);
405
- return hasHiddenNav;
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
- // Use default responsive visibility logic if wrapper exists
466
- if (navigationWrapperRects.value) {
467
- return (
468
- Math.floor(rect.right + mainNavigationMarginBlockEnd.value + props.gapBetweenFirstAndSecondNav) <
469
- navigationWrapperRects.value.right
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) return;
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
- white-space: nowrap;
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: var(--_overflow-drop-down-width, fit-content);
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;