srcdev-nuxt-components 9.5.7 → 9.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -8
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/responsive-header.md +59 -0
- package/.claude/skills/components/select-menu.md +54 -5
- 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 -2
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -5
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -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-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- 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/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- 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 +42 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
- 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 +5 -4
- 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 +2 -2
- 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 +6 -6
- 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/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.claude/skills/components/tab-navigation.md +0 -272
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- 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
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- package/app/types/components/nav-item.d.ts +0 -11
|
@@ -20,25 +20,51 @@ are documented alongside it since that's where they're consumed.
|
|
|
20
20
|
| `--responsive-header-padding-block` / `--responsive-header-padding-inline` | `0` | Root element padding. |
|
|
21
21
|
| `--responsive-header-max-height` | `none` | Root element max-height. |
|
|
22
22
|
| `--responsive-header-inline-size` | `100%` | Root element inline-size. |
|
|
23
|
-
| `--responsive-header-
|
|
23
|
+
| `--responsive-header-link-color-hover` | `--responsive-header-link-color` | Top-bar link/summary colour on the hovered item. Unset, an active item keeps its active colour. |
|
|
24
|
+
| `--responsive-header-link-color-active` | `--responsive-header-link-color` | Top-bar link/summary colour on the active item (current route or hash). |
|
|
25
|
+
| `--responsive-header-sub-nav-bg` / `-border` / `-border-radius` / `-padding` | `var(--page-bg, var(--theme-surface-subtle))` / `1px solid color-mix(in oklch, var(--theme-text) 20%, transparent)` / `8px` / `12px` | Top-bar dropdown panel (`.main-navigation-sub-nav`). |
|
|
24
26
|
| `--responsive-header-sub-nav-max-inline-size` | `min(48rem, calc(100vw - 3.2rem))` | Widest the top-bar dropdown panel gets; long child names wrap inside it. |
|
|
25
27
|
| `--responsive-header-sub-nav-max-block-size` | `70vh` | Tallest the top-bar dropdown panel gets before it scrolls, so a long child list stays reachable under a sticky header. |
|
|
26
|
-
| `--responsive-header-overflow-btn-bg` / `-size` / `-border` / `-outline` / `-icon-color` / `-hover-outline` | `
|
|
27
|
-
| `--responsive-header-overflow-nav-bg` / `-border` / `-border-radius` / `-padding-block` | `
|
|
28
|
+
| `--responsive-header-overflow-btn-bg` / `-size` / `-border` / `-outline` / `-icon-color` / `-hover-outline` | `transparent` / `20px` / `1px solid color-mix(in oklch, var(--theme-text) 20%, transparent)` / `1px solid transparent` / `inherit` / `1px solid var(--theme-border-focus)` | Overflow burger button. |
|
|
29
|
+
| `--responsive-header-overflow-nav-bg` / `-border` / `-border-radius` / `-padding-block` | `var(--page-bg, var(--theme-surface-subtle))` / `1px solid color-mix(in oklch, var(--theme-text) 20%, transparent)` / `8px` / `12px` | Overflow panel container. |
|
|
28
30
|
| `--responsive-header-overflow-nav-max-block-size` | `70vh` | Tallest the overflow panel gets before it scrolls. |
|
|
29
|
-
| `--
|
|
31
|
+
| `--responsive-nav-decorator-indicator-color` | `currentColor` | Active-item indicator bar/underline. |
|
|
32
|
+
| `--responsive-nav-decorator-hovered-indicator-color` | inherits the above | Indicator colour while hovering. |
|
|
33
|
+
| `--responsive-nav-decorator-hovered-bg` | `color-mix(in oklch, var(--theme-text) 8%, transparent)` | Hover highlight background behind the hovered item. |
|
|
34
|
+
|
|
35
|
+
### Overflow panel content (`NavigationItems`)
|
|
36
|
+
|
|
37
|
+
| Token | Default | Controls |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `--overflow-nav-link-color` | `inherit` | Link/summary colour in the overflow panel. |
|
|
40
|
+
| `--overflow-nav-link-color-hover` | `--overflow-nav-link-color` | Colour on the hovered item. Unset, an active item keeps its active colour. |
|
|
41
|
+
| `--overflow-nav-link-color-active` | `--overflow-nav-link-color` | Colour on the active item. |
|
|
42
|
+
| `--overflow-nav-link-border-color` | `color-mix(in oklch, var(--theme-text) 12%, transparent)` | Divider under each row. |
|
|
43
|
+
| `--overflow-nav-sub-item-color` / `--overflow-nav-sub-item-font-size` | `inherit` / `inherit` | Child links inside a group. |
|
|
44
|
+
| `--overflow-nav-decorator-indicator-color` | `currentColor` | Active-row indicator bar. |
|
|
45
|
+
| `--overflow-nav-decorator-hovered-indicator-color` | inherits the above | Indicator colour while hovering. |
|
|
46
|
+
| `--overflow-nav-decorator-hovered-bg` | `color-mix(in oklch, var(--theme-text) 6%, transparent)` | Hover highlight behind a row. |
|
|
47
|
+
| `--overflow-nav-padding-inline` / `--overflow-nav-items-padding-block` / `--overflow-nav-items-gap` | `0.8rem` / `0.8rem` / `0px` | Row spacing. |
|
|
48
|
+
| `--overflow-nav-max-inline-size` | `calc(100vw - 3.2rem)` | Widest the overflow panel's content gets; long names wrap inside it. |
|
|
49
|
+
|
|
50
|
+
Private: `--_link-color` / `--_overflow-link-color` carry the per-state (hover/active) link colour
|
|
51
|
+
from the item to its link and summary. Not public API.
|
|
30
52
|
|
|
31
53
|
Link and dropdown text is never line-clamped: it names where the link goes. Long unbroken names
|
|
32
54
|
wrap inside the dropdown and overflow panels; in the top bar, an item too wide to fit moves to the
|
|
33
55
|
overflow menu.
|
|
34
56
|
|
|
57
|
+
> **Changed 2026-10-07**: the defaults assumed a dark bar: `Canvas` panel and button backgrounds,
|
|
58
|
+
> translucent white borders (`#ffffff90`, `#efefef75`), a white button outline/hover outline and
|
|
59
|
+
> white-tint hover highlights, all near-invisible on a light header. They now come from the
|
|
60
|
+
> `--theme-*` slots and `--page-bg` (see the tables). A dark header sets these tokens. Also added
|
|
61
|
+
> the `-link-color-hover` / `-link-color-active` tokens (top bar and overflow panel), which
|
|
62
|
+
> TabNavigation had and ResponsiveHeader lacked.
|
|
63
|
+
|
|
35
64
|
> **Changed 2026-10-05**: the dropdown and overflow panels had no maximum size, so a long child
|
|
36
|
-
> name or a long child list ran off-screen. They are now capped by the
|
|
65
|
+
> name or a long child list ran off-screen. They are now capped by the max-size tokens above.
|
|
37
66
|
> `--_overflow-drop-down-width`, a private token nothing ever set, is gone (`min-width: fit-content`
|
|
38
67
|
> directly).
|
|
39
|
-
| `--responsive-nav-decorator-indicator-color` | `currentColor` | Active-item indicator bar/underline. |
|
|
40
|
-
| `--responsive-nav-decorator-hovered-indicator-color` | inherits the above | Indicator colour while hovering. |
|
|
41
|
-
| `--responsive-nav-decorator-hovered-bg` | `oklch(100% 0 0 / 8%)` | Hover highlight background behind the hovered item. |
|
|
42
68
|
|
|
43
69
|
---
|
|
44
70
|
|
|
@@ -50,8 +76,9 @@ tokens on a scope class matching the one passed via `styleClassPassthrough`:
|
|
|
50
76
|
```css
|
|
51
77
|
/* assets/styles/setup/07.components/responsive-header.css */
|
|
52
78
|
.site-header-nav {
|
|
53
|
-
--responsive-header-link-color: var(--
|
|
54
|
-
--responsive-header-
|
|
79
|
+
--responsive-header-link-color: var(--theme-text);
|
|
80
|
+
--responsive-header-link-color-hover: var(--theme-accent);
|
|
81
|
+
--responsive-header-link-color-active: var(--theme-accent);
|
|
55
82
|
--responsive-header-padding-inline: 1.2rem;
|
|
56
83
|
|
|
57
84
|
/* Fixed, NOT a fluid var(--step-*) token — see the token table above. */
|
|
@@ -80,12 +107,16 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
80
107
|
|
|
81
108
|
## Notes
|
|
82
109
|
|
|
110
|
+
- Same-page `#anchor` items (since 2026-10-06) render as plain `<a>` with the same classes as route
|
|
111
|
+
links (`.main-navigation-link`, `.overflow-navigation-link`, the sub-nav link classes), so every token
|
|
112
|
+
and selector here applies to them. Their `li.is-active` follows the current hash rather than the route.
|
|
113
|
+
|
|
83
114
|
- Anything with async-loaded content (an `iconName` icon on a nav item, a new decorator inside
|
|
84
115
|
a nav link) must reserve its own `width`/`height` in CSS — an unsized element measures at
|
|
85
116
|
`0` during the initial geometry pass and then pops in afterward with nothing to detect it.
|
|
86
117
|
See `.decorator-icon`/the chevron `.icon` in the component's own `<style>` block for the
|
|
87
118
|
pattern (`1.35em` square, `flex-shrink: 0`).
|
|
88
|
-
- `NavigationItems`' own tokens (`--overflow-nav-*`)
|
|
119
|
+
- `NavigationItems`' own tokens (`--overflow-nav-*`) have their own table above since
|
|
89
120
|
they style the overflow-panel's *content*, distinct from `ResponsiveHeader`'s own tokens
|
|
90
121
|
above which style the top bar and the overflow *button*/*container*.
|
|
91
122
|
|
|
@@ -34,7 +34,16 @@
|
|
|
34
34
|
role="none"
|
|
35
35
|
@mouseenter="hoveredItemKey = `${String(groupKey)}-${localIndex}`"
|
|
36
36
|
>
|
|
37
|
-
<
|
|
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">
|
|
38
47
|
<span class="overflow-navigation-text">{{ link.name }}</span>
|
|
39
48
|
</NuxtLink>
|
|
40
49
|
</li>
|
|
@@ -87,7 +96,16 @@
|
|
|
87
96
|
}"
|
|
88
97
|
@mouseenter="hoveredChildKey = `${String(groupKey)}-${localIndex}-${childIndex}`"
|
|
89
98
|
>
|
|
90
|
-
<
|
|
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">
|
|
91
109
|
<span class="overflow-navigation-sub-nav-text">{{ childLink.name }}</span>
|
|
92
110
|
</NuxtLink>
|
|
93
111
|
</li>
|
|
@@ -117,6 +135,8 @@ interface Props {
|
|
|
117
135
|
ariaLabel?: string;
|
|
118
136
|
/** aria-label on each submenu summary; {title} is replaced with the item's title. */
|
|
119
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>(), {
|
|
@@ -128,8 +148,13 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
128
148
|
styleClassPassthrough: () => [],
|
|
129
149
|
ariaLabel: "Overflow navigation menu",
|
|
130
150
|
submenuAriaLabel: "{title} submenu",
|
|
151
|
+
activeHash: "",
|
|
131
152
|
});
|
|
132
153
|
|
|
154
|
+
const emit = defineEmits<{
|
|
155
|
+
anchorClick: [event: MouseEvent, href: string];
|
|
156
|
+
}>();
|
|
157
|
+
|
|
133
158
|
const submenuLabel = (link: ResponsiveHeaderNavItem) =>
|
|
134
159
|
props.submenuAriaLabel.replace("{title}", link.childLinksTitle ?? link.name);
|
|
135
160
|
|
|
@@ -162,9 +187,13 @@ onUnmounted(() => {
|
|
|
162
187
|
|
|
163
188
|
const route = useRoute();
|
|
164
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
|
+
|
|
165
194
|
const isActiveNavItem = (link: ResponsiveHeaderNavItem): boolean => {
|
|
166
|
-
if (link.path) return
|
|
167
|
-
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));
|
|
168
197
|
return false;
|
|
169
198
|
};
|
|
170
199
|
// const DETAILS_ANIMATION_DURATION_STRING = `${DETAILS_ANIMATION_DURATION}ms` as const;
|
|
@@ -211,13 +240,15 @@ watch(
|
|
|
211
240
|
--overflow-nav-items-padding-block (default: 0.8rem)
|
|
212
241
|
|
|
213
242
|
--overflow-nav-link-color (default: inherit)
|
|
214
|
-
--overflow-nav-link-
|
|
243
|
+
--overflow-nav-link-color-hover (default: --overflow-nav-link-color)
|
|
244
|
+
--overflow-nav-link-color-active (default: --overflow-nav-link-color)
|
|
245
|
+
--overflow-nav-link-border-color (default: color-mix(in oklch, var(--theme-text) 12%, transparent))
|
|
215
246
|
--overflow-nav-sub-item-color (default: inherit)
|
|
216
247
|
--overflow-nav-sub-item-font-size (default: inherit)
|
|
217
248
|
|
|
218
249
|
--overflow-nav-decorator-indicator-color (default: currentColor)
|
|
219
250
|
--overflow-nav-decorator-hovered-indicator-color (default: inherits --overflow-nav-decorator-indicator-color)
|
|
220
|
-
--overflow-nav-decorator-hovered-bg (default:
|
|
251
|
+
--overflow-nav-decorator-hovered-bg (default: color-mix(in oklch, var(--theme-text) 6%, transparent))
|
|
221
252
|
──────────────────────────────────────────────────────────────────────── */
|
|
222
253
|
|
|
223
254
|
--_max-inline-size: var(--overflow-nav-max-inline-size, calc(100vw - 3.2rem));
|
|
@@ -247,13 +278,26 @@ watch(
|
|
|
247
278
|
display: block;
|
|
248
279
|
}
|
|
249
280
|
|
|
281
|
+
/* Per-state link colour; hover falls back to the item's current state. Unset means the resting colour. */
|
|
282
|
+
&.is-active {
|
|
283
|
+
--_overflow-link-color: var(--overflow-nav-link-color-active);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
&.is-hovered {
|
|
287
|
+
--_overflow-link-color: var(--overflow-nav-link-color-hover, var(--overflow-nav-link-color-active));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
&.is-hovered:not(.is-active) {
|
|
291
|
+
--_overflow-link-color: var(--overflow-nav-link-color-hover);
|
|
292
|
+
}
|
|
293
|
+
|
|
250
294
|
.overflow-navigation-link {
|
|
251
295
|
text-decoration: none;
|
|
252
|
-
color: var(--overflow-nav-link-color, inherit);
|
|
296
|
+
color: var(--_overflow-link-color, var(--overflow-nav-link-color, inherit));
|
|
253
297
|
padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
|
|
254
298
|
padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
|
|
255
299
|
display: flex;
|
|
256
|
-
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color,
|
|
300
|
+
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, color-mix(in oklch, var(--theme-text) 12%, transparent));
|
|
257
301
|
}
|
|
258
302
|
|
|
259
303
|
.overflow-navigation-details {
|
|
@@ -271,8 +315,8 @@ watch(
|
|
|
271
315
|
padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
|
|
272
316
|
padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
|
|
273
317
|
gap: 1rem;
|
|
274
|
-
color: var(--overflow-nav-link-color, inherit);
|
|
275
|
-
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color,
|
|
318
|
+
color: var(--_overflow-link-color, var(--overflow-nav-link-color, inherit));
|
|
319
|
+
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, color-mix(in oklch, var(--theme-text) 12%, transparent));
|
|
276
320
|
|
|
277
321
|
.label-wrapper {
|
|
278
322
|
min-inline-size: 0;
|
|
@@ -287,7 +331,7 @@ watch(
|
|
|
287
331
|
border-bottom: 0.1rem solid transparent;
|
|
288
332
|
}
|
|
289
333
|
+ .expanding-panel-content {
|
|
290
|
-
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color,
|
|
334
|
+
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, color-mix(in oklch, var(--theme-text) 12%, transparent));
|
|
291
335
|
.overflow-navigation-sub-nav-inner {
|
|
292
336
|
margin-top: var(--overflow-nav-items-gap, 0px);
|
|
293
337
|
}
|
|
@@ -332,8 +376,8 @@ watch(
|
|
|
332
376
|
padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
|
|
333
377
|
padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
|
|
334
378
|
gap: 1rem;
|
|
335
|
-
color: var(--overflow-nav-link-color, inherit);
|
|
336
|
-
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color,
|
|
379
|
+
color: var(--_overflow-link-color, var(--overflow-nav-link-color, inherit));
|
|
380
|
+
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, color-mix(in oklch, var(--theme-text) 12%, transparent));
|
|
337
381
|
|
|
338
382
|
.label-wrapper {
|
|
339
383
|
min-inline-size: 0;
|
|
@@ -348,7 +392,7 @@ watch(
|
|
|
348
392
|
border-bottom: 0.1rem solid transparent;
|
|
349
393
|
}
|
|
350
394
|
+ .expanding-panel-classic-content {
|
|
351
|
-
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color,
|
|
395
|
+
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, color-mix(in oklch, var(--theme-text) 12%, transparent));
|
|
352
396
|
.overflow-navigation-sub-nav-inner {
|
|
353
397
|
margin-top: var(--overflow-nav-items-gap, 0px);
|
|
354
398
|
}
|
|
@@ -457,7 +501,7 @@ watch(
|
|
|
457
501
|
right: 0;
|
|
458
502
|
top: anchor(top);
|
|
459
503
|
bottom: anchor(bottom);
|
|
460
|
-
background: var(--overflow-nav-decorator-hovered-bg,
|
|
504
|
+
background: var(--overflow-nav-decorator-hovered-bg, color-mix(in oklch, var(--theme-text) 6%, transparent));
|
|
461
505
|
z-index: 1;
|
|
462
506
|
opacity: 0;
|
|
463
507
|
transition:
|
|
@@ -523,7 +567,7 @@ watch(
|
|
|
523
567
|
right: 0;
|
|
524
568
|
top: anchor(top);
|
|
525
569
|
bottom: anchor(bottom);
|
|
526
|
-
background: var(--overflow-nav-decorator-hovered-bg,
|
|
570
|
+
background: var(--overflow-nav-decorator-hovered-bg, color-mix(in oklch, var(--theme-text) 6%, transparent));
|
|
527
571
|
border-inline-start: 2px solid var(
|
|
528
572
|
--overflow-nav-decorator-hovered-indicator-color,
|
|
529
573
|
var(--overflow-nav-decorator-indicator-color, currentColor)
|
|
@@ -39,8 +39,18 @@
|
|
|
39
39
|
@mouseenter="handleNavigationItemHover(`${String(groupKey)}-${localIndex}`)"
|
|
40
40
|
@focusin="handleNavigationItemHover(`${String(groupKey)}-${localIndex}`)"
|
|
41
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>
|
|
42
52
|
<NuxtLink
|
|
43
|
-
v-if="link.path"
|
|
53
|
+
v-else-if="link.path"
|
|
44
54
|
class="main-navigation-link"
|
|
45
55
|
:class="{ 'has-icon': link.iconName }"
|
|
46
56
|
:to="link.path"
|
|
@@ -69,7 +79,16 @@
|
|
|
69
79
|
:key="childIndex"
|
|
70
80
|
class="main-navigation-sub-nav-item"
|
|
71
81
|
>
|
|
72
|
-
<
|
|
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">
|
|
73
92
|
{{ childLink.name }}
|
|
74
93
|
</NuxtLink>
|
|
75
94
|
</li>
|
|
@@ -108,6 +127,8 @@
|
|
|
108
127
|
:panel-variant="panelVariant"
|
|
109
128
|
:aria-label="overflowMenuAriaLabel"
|
|
110
129
|
:submenu-aria-label="submenuAriaLabel"
|
|
130
|
+
:active-hash="activeHash"
|
|
131
|
+
@anchor-click="handleAnchorClick"
|
|
111
132
|
/>
|
|
112
133
|
</div>
|
|
113
134
|
</details>
|
|
@@ -144,6 +165,11 @@ interface Props {
|
|
|
144
165
|
overflowButtonLabel?: string;
|
|
145
166
|
/** aria-label on each dropdown summary; {title} is replaced with the item's title. Forwarded to NavigationItems. */
|
|
146
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);
|
|
147
173
|
}
|
|
148
174
|
|
|
149
175
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -165,6 +191,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
165
191
|
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
166
192
|
overflowButtonLabel: "More navigation",
|
|
167
193
|
submenuAriaLabel: "{title} submenu",
|
|
194
|
+
anchorScrollOffset: undefined,
|
|
168
195
|
});
|
|
169
196
|
|
|
170
197
|
const submenuLabel = (link: ResponsiveHeaderNavItem) =>
|
|
@@ -315,12 +342,32 @@ const hoveredItemKey = ref<string | null>(null);
|
|
|
315
342
|
|
|
316
343
|
const route = useRoute();
|
|
317
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
|
+
|
|
318
351
|
const isActiveNavItem = (link: ResponsiveHeaderNavItem): boolean => {
|
|
319
|
-
if (link.path) return
|
|
320
|
-
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));
|
|
321
354
|
return false;
|
|
322
355
|
};
|
|
323
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
|
+
|
|
324
371
|
const isAnimated = ref(true);
|
|
325
372
|
|
|
326
373
|
// Local (non-useState) flag — resets to false on every mount.
|
|
@@ -704,30 +751,32 @@ watch(
|
|
|
704
751
|
|
|
705
752
|
--responsive-header-color (default: inherit)
|
|
706
753
|
--responsive-header-link-color (default: inherit)
|
|
754
|
+
--responsive-header-link-color-hover (default: --responsive-header-link-color)
|
|
755
|
+
--responsive-header-link-color-active (default: --responsive-header-link-color)
|
|
707
756
|
|
|
708
|
-
--responsive-header-sub-nav-bg (default:
|
|
709
|
-
--responsive-header-sub-nav-border (default: 1px solid
|
|
757
|
+
--responsive-header-sub-nav-bg (default: var(--page-bg, var(--theme-surface-subtle)))
|
|
758
|
+
--responsive-header-sub-nav-border (default: 1px solid color-mix(in oklch, var(--theme-text) 20%, transparent))
|
|
710
759
|
--responsive-header-sub-nav-border-radius (default: 8px)
|
|
711
760
|
--responsive-header-sub-nav-padding (default: 12px)
|
|
712
761
|
--responsive-header-sub-nav-max-inline-size (default: min(48rem, calc(100vw - 3.2rem)))
|
|
713
762
|
--responsive-header-sub-nav-max-block-size (default: 70vh)
|
|
714
763
|
|
|
715
|
-
--responsive-header-overflow-btn-bg (default:
|
|
764
|
+
--responsive-header-overflow-btn-bg (default: transparent)
|
|
716
765
|
--responsive-header-overflow-btn-size (default: 20px)
|
|
717
|
-
--responsive-header-overflow-btn-border (default: 1px solid
|
|
718
|
-
--responsive-header-overflow-btn-outline (default: 1px solid
|
|
766
|
+
--responsive-header-overflow-btn-border (default: 1px solid color-mix(in oklch, var(--theme-text) 20%, transparent))
|
|
767
|
+
--responsive-header-overflow-btn-outline (default: 1px solid transparent)
|
|
719
768
|
--responsive-header-overflow-btn-icon-color (default: inherit)
|
|
720
|
-
--responsive-header-overflow-btn-hover-outline (default: 1px solid
|
|
769
|
+
--responsive-header-overflow-btn-hover-outline (default: 1px solid var(--theme-border-focus))
|
|
721
770
|
|
|
722
|
-
--responsive-header-overflow-nav-bg (default:
|
|
723
|
-
--responsive-header-overflow-nav-border (default: 1px solid
|
|
771
|
+
--responsive-header-overflow-nav-bg (default: var(--page-bg, var(--theme-surface-subtle)))
|
|
772
|
+
--responsive-header-overflow-nav-border (default: 1px solid color-mix(in oklch, var(--theme-text) 20%, transparent))
|
|
724
773
|
--responsive-header-overflow-nav-border-radius (default: 8px)
|
|
725
774
|
--responsive-header-overflow-nav-padding-block (default: 12px)
|
|
726
775
|
--responsive-header-overflow-nav-max-block-size (default: 70vh)
|
|
727
776
|
|
|
728
777
|
--responsive-nav-decorator-indicator-color (default: currentColor)
|
|
729
778
|
--responsive-nav-decorator-hovered-indicator-color (default: inherits --responsive-nav-decorator-indicator-color)
|
|
730
|
-
--responsive-nav-decorator-hovered-bg (default:
|
|
779
|
+
--responsive-nav-decorator-hovered-bg (default: color-mix(in oklch, var(--theme-text) 8%, transparent))
|
|
731
780
|
|
|
732
781
|
--responsive-header-link-font-size (default: inherit)
|
|
733
782
|
--responsive-header-main-nav-justify-content (default: space-between)
|
|
@@ -807,7 +856,7 @@ watch(
|
|
|
807
856
|
gap: 6px;
|
|
808
857
|
white-space: nowrap;
|
|
809
858
|
font-size: var(--responsive-header-link-font-size, inherit);
|
|
810
|
-
color: var(--responsive-header-link-color, inherit);
|
|
859
|
+
color: var(--_link-color, var(--responsive-header-link-color, inherit));
|
|
811
860
|
text-decoration: none;
|
|
812
861
|
cursor: pointer;
|
|
813
862
|
margin-inline-start: 0;
|
|
@@ -869,7 +918,7 @@ watch(
|
|
|
869
918
|
position: relative;
|
|
870
919
|
z-index: 4;
|
|
871
920
|
font-size: var(--responsive-header-link-font-size, inherit);
|
|
872
|
-
color: var(--responsive-header-link-color, inherit);
|
|
921
|
+
color: var(--_link-color, var(--responsive-header-link-color, inherit));
|
|
873
922
|
|
|
874
923
|
&::-webkit-details-marker,
|
|
875
924
|
&::marker {
|
|
@@ -885,9 +934,9 @@ watch(
|
|
|
885
934
|
position: absolute;
|
|
886
935
|
|
|
887
936
|
padding: var(--responsive-header-sub-nav-padding, 12px);
|
|
888
|
-
border: var(--responsive-header-sub-nav-border, 1px solid
|
|
937
|
+
border: var(--responsive-header-sub-nav-border, 1px solid color-mix(in oklch, var(--theme-text) 20%, transparent));
|
|
889
938
|
border-radius: var(--responsive-header-sub-nav-border-radius, 8px);
|
|
890
|
-
background-color: var(--responsive-header-sub-nav-bg,
|
|
939
|
+
background-color: var(--responsive-header-sub-nav-bg, var(--page-bg, var(--theme-surface-subtle)));
|
|
891
940
|
translate: 0 12px;
|
|
892
941
|
|
|
893
942
|
min-width: var(--_main-navigation-item-width);
|
|
@@ -933,6 +982,19 @@ watch(
|
|
|
933
982
|
margin-inline-start: var(--_main-navigation-item-width);
|
|
934
983
|
}
|
|
935
984
|
}
|
|
985
|
+
|
|
986
|
+
/* Per-state link colour; hover falls back to the item's current state. Unset means the resting colour. */
|
|
987
|
+
&.is-active {
|
|
988
|
+
--_link-color: var(--responsive-header-link-color-active);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
&.is-hovered {
|
|
992
|
+
--_link-color: var(--responsive-header-link-color-hover, var(--responsive-header-link-color-active));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
&.is-hovered:not(.is-active) {
|
|
996
|
+
--_link-color: var(--responsive-header-link-color-hover);
|
|
997
|
+
}
|
|
936
998
|
}
|
|
937
999
|
|
|
938
1000
|
&:last-child {
|
|
@@ -1024,9 +1086,9 @@ watch(
|
|
|
1024
1086
|
|
|
1025
1087
|
aspect-ratio: 1;
|
|
1026
1088
|
border-radius: 4px;
|
|
1027
|
-
border: var(--responsive-header-overflow-btn-border, 1px solid
|
|
1028
|
-
outline: var(--responsive-header-overflow-btn-outline, 1px solid
|
|
1029
|
-
background-color: var(--responsive-header-overflow-btn-bg,
|
|
1089
|
+
border: var(--responsive-header-overflow-btn-border, 1px solid color-mix(in oklch, var(--theme-text) 20%, transparent));
|
|
1090
|
+
outline: var(--responsive-header-overflow-btn-outline, 1px solid transparent);
|
|
1091
|
+
background-color: var(--responsive-header-overflow-btn-bg, transparent);
|
|
1030
1092
|
|
|
1031
1093
|
width: var(--_icon-size);
|
|
1032
1094
|
overflow: hidden;
|
|
@@ -1042,7 +1104,7 @@ watch(
|
|
|
1042
1104
|
&:hover,
|
|
1043
1105
|
&:focus-visible {
|
|
1044
1106
|
--_icon-zoom: 1.2;
|
|
1045
|
-
outline: var(--responsive-header-overflow-btn-hover-outline, 1px solid
|
|
1107
|
+
outline: var(--responsive-header-overflow-btn-hover-outline, 1px solid var(--theme-border-focus));
|
|
1046
1108
|
}
|
|
1047
1109
|
|
|
1048
1110
|
.icon {
|
|
@@ -1068,8 +1130,8 @@ watch(
|
|
|
1068
1130
|
position: absolute;
|
|
1069
1131
|
top: 135%;
|
|
1070
1132
|
right: 0;
|
|
1071
|
-
background-color: var(--responsive-header-overflow-nav-bg,
|
|
1072
|
-
border: var(--responsive-header-overflow-nav-border, 1px solid
|
|
1133
|
+
background-color: var(--responsive-header-overflow-nav-bg, var(--page-bg, var(--theme-surface-subtle)));
|
|
1134
|
+
border: var(--responsive-header-overflow-nav-border, 1px solid color-mix(in oklch, var(--theme-text) 20%, transparent));
|
|
1073
1135
|
border-radius: var(--responsive-header-overflow-nav-border-radius, 8px);
|
|
1074
1136
|
padding-block: var(--responsive-header-overflow-nav-padding-block, 12px);
|
|
1075
1137
|
margin: 0;
|
|
@@ -1125,7 +1187,7 @@ watch(
|
|
|
1125
1187
|
right: anchor(right);
|
|
1126
1188
|
top: anchor(top);
|
|
1127
1189
|
bottom: anchor(bottom);
|
|
1128
|
-
background: var(--responsive-nav-decorator-hovered-bg,
|
|
1190
|
+
background: var(--responsive-nav-decorator-hovered-bg, color-mix(in oklch, var(--theme-text) 8%, transparent));
|
|
1129
1191
|
border-radius: 4px;
|
|
1130
1192
|
z-index: 1;
|
|
1131
1193
|
opacity: 0;
|
|
@@ -82,6 +82,11 @@ const meta: Meta<StoryArgs> = {
|
|
|
82
82
|
description: "aria-label on each dropdown summary; {title} is replaced with the item's title",
|
|
83
83
|
table: { category: "Labels", defaultValue: { summary: "{title} submenu" } },
|
|
84
84
|
},
|
|
85
|
+
anchorScrollOffset: {
|
|
86
|
+
control: { type: "number", min: 0, step: 10 },
|
|
87
|
+
description: "Pixels left above a section when a #anchor link scrolls to it (e.g. a sticky header's height)",
|
|
88
|
+
table: { category: "Behaviour", defaultValue: { summary: "0" } },
|
|
89
|
+
},
|
|
85
90
|
styleClassPassthrough: {
|
|
86
91
|
control: "object",
|
|
87
92
|
description: "Extra CSS classes applied to the root element",
|
|
@@ -200,6 +205,53 @@ export const MainNavAlignment: Story = {
|
|
|
200
205
|
},
|
|
201
206
|
};
|
|
202
207
|
|
|
208
|
+
export const SamePageAnchorLinks: Story = {
|
|
209
|
+
name: "Same-Page Anchor Links",
|
|
210
|
+
args: {
|
|
211
|
+
responsiveNavLinks: {
|
|
212
|
+
main: [
|
|
213
|
+
{ name: "Home", path: "#rh-home" },
|
|
214
|
+
{ name: "About", path: "#rh-about" },
|
|
215
|
+
{ name: "Testimonials", path: "#rh-testimonials" },
|
|
216
|
+
{
|
|
217
|
+
name: "More",
|
|
218
|
+
childLinks: [
|
|
219
|
+
{ name: "Prices", path: "#rh-prices" },
|
|
220
|
+
{ name: "Blog (a route)", path: "/blog" },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
{ name: "Contact", path: "#rh-contact" },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
anchorScrollOffset: 80,
|
|
227
|
+
},
|
|
228
|
+
render: renderWith(`
|
|
229
|
+
<div style="position: sticky; top: 0; z-index: 2; background: var(--page-bg, white); border-block-end: 1px solid currentColor;">
|
|
230
|
+
<ResponsiveHeader v-bind="componentArgs" />
|
|
231
|
+
</div>
|
|
232
|
+
<section
|
|
233
|
+
v-for="id in ['rh-home', 'rh-about', 'rh-testimonials', 'rh-prices', 'rh-contact']"
|
|
234
|
+
:id="id"
|
|
235
|
+
:key="id"
|
|
236
|
+
style="min-block-size: 70vh; padding: 2.4rem; border-block-end: 1px dashed currentColor;"
|
|
237
|
+
>
|
|
238
|
+
<h2 style="margin: 0;">#{{ id }}</h2>
|
|
239
|
+
</section>
|
|
240
|
+
`),
|
|
241
|
+
parameters: {
|
|
242
|
+
docs: {
|
|
243
|
+
description: {
|
|
244
|
+
story:
|
|
245
|
+
"Items whose `path` starts with `#` are same-page section links: they render as plain `<a>` (not NuxtLink), " +
|
|
246
|
+
"smooth-scroll to the section (an instant jump under reduced motion), and are active by hash rather than by " +
|
|
247
|
+
"route; the first anchor is active on load. `anchorScrollOffset` (80 here) leaves room for the sticky header. " +
|
|
248
|
+
"Route links like Blog mix in as normal. Narrow the canvas so items move into the overflow menu: anchor links " +
|
|
249
|
+
"there scroll the same way and close the menu.",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
203
255
|
// ─── Stress test ──────────────────────────────────────────────────────────────
|
|
204
256
|
|
|
205
257
|
const stressNavLinks: ResponsiveHeaderProp = {
|
|
@@ -168,4 +168,24 @@ describe("NavigationItems", () => {
|
|
|
168
168
|
});
|
|
169
169
|
expect(wrapper.html()).toMatchSnapshot();
|
|
170
170
|
});
|
|
171
|
+
|
|
172
|
+
// ─── Same-page anchor links ─────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
it("renders #anchor items as plain links, emits anchorClick and follows activeHash", async () => {
|
|
175
|
+
const wrapper = await mountSuspended(NavigationItems, {
|
|
176
|
+
props: {
|
|
177
|
+
activeHash: "#about",
|
|
178
|
+
mainNavigationState: {
|
|
179
|
+
clonedNavLinks: { firstNav: [{ name: "Home", path: "#home" }, { name: "About", path: "#about" }] },
|
|
180
|
+
navListVisibility: { firstNav: false },
|
|
181
|
+
hasSecondNav: false,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const links = wrapper.findAll(".overflow-navigation-link");
|
|
186
|
+
expect(links[0]!.attributes("href")).toBe("#home");
|
|
187
|
+
expect(wrapper.findAll(".overflow-navigation-item")[1]!.classes()).toContain("is-active");
|
|
188
|
+
await links[0]!.trigger("click");
|
|
189
|
+
expect(wrapper.emitted("anchorClick")?.[0]?.[1]).toBe("#home");
|
|
190
|
+
});
|
|
171
191
|
});
|
|
@@ -401,4 +401,44 @@ describe("ResponsiveHeader", () => {
|
|
|
401
401
|
});
|
|
402
402
|
expect(wrapper.html()).toMatchSnapshot();
|
|
403
403
|
});
|
|
404
|
+
|
|
405
|
+
// ─── Same-page anchor links ─────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
describe("anchor links", () => {
|
|
408
|
+
const anchorLinks: ResponsiveHeaderProp = {
|
|
409
|
+
firstNav: [
|
|
410
|
+
{ name: "Home", path: "#home" },
|
|
411
|
+
{ name: "About", path: "#about" },
|
|
412
|
+
{ name: "Blog", path: "/blog" },
|
|
413
|
+
],
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
it("renders #anchor paths as plain links and route paths as NuxtLinks", async () => {
|
|
417
|
+
const wrapper = await mountSuspended(ResponsiveHeader, { props: { responsiveNavLinks: anchorLinks } });
|
|
418
|
+
const links = wrapper.findAll(".main-navigation-link");
|
|
419
|
+
expect(links[0]!.attributes("href")).toBe("#home");
|
|
420
|
+
expect(links[2]!.attributes("href")).toBe("/blog");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("marks the first anchor item active on load", async () => {
|
|
424
|
+
const wrapper = await mountSuspended(ResponsiveHeader, { props: { responsiveNavLinks: anchorLinks } });
|
|
425
|
+
await nextTick();
|
|
426
|
+
const items = wrapper.findAll(".main-navigation-item");
|
|
427
|
+
expect(items[0]!.classes()).toContain("is-active");
|
|
428
|
+
expect(items[1]!.classes()).not.toContain("is-active");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("moves the active item to a clicked anchor and scrolls instead of jumping", async () => {
|
|
432
|
+
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({ matches: false }));
|
|
433
|
+
const section = document.createElement("section");
|
|
434
|
+
section.id = "about";
|
|
435
|
+
section.scrollIntoView = vi.fn();
|
|
436
|
+
document.body.appendChild(section);
|
|
437
|
+
const wrapper = await mountSuspended(ResponsiveHeader, { props: { responsiveNavLinks: anchorLinks } });
|
|
438
|
+
await wrapper.findAll(".main-navigation-link")[1]!.trigger("click");
|
|
439
|
+
expect(section.scrollIntoView).toHaveBeenCalled();
|
|
440
|
+
expect(wrapper.findAll(".main-navigation-item")[1]!.classes()).toContain("is-active");
|
|
441
|
+
section.remove();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
404
444
|
});
|