srcdev-nuxt-components 9.4.4 → 9.4.6
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/commands/migrate-component.md +106 -0
- package/.claude/component-ledger/artifact-url.txt +1 -0
- package/.claude/component-ledger/audit.json +1 -0
- package/.claude/component-ledger/build.mjs +111 -0
- package/.claude/component-ledger/output.html +559 -0
- package/.claude/component-ledger/template.html +559 -0
- package/.claude/hooks/refresh-component-ledger.sh +32 -0
- package/.claude/settings.json +10 -0
- package/.claude/skills/components/animated-svg-text.md +78 -0
- package/.claude/skills/components/breadcrumb.md +1 -0
- package/.claude/skills/components/canvas-switcher.md +61 -0
- package/.claude/skills/components/clip-element.md +67 -0
- package/.claude/skills/components/clipped-panel.md +64 -0
- package/.claude/skills/components/content-docs.md +2 -0
- package/.claude/skills/components/deep-expanding-menu-classic.md +94 -0
- package/.claude/skills/components/deep-expanding-menu.md +92 -0
- package/.claude/skills/components/display-banner.md +60 -0
- package/.claude/skills/components/display-tooltip-defined.md +139 -0
- package/.claude/skills/components/display-tooltip.md +85 -0
- package/.claude/skills/components/marquee-scroller.md +130 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/responsive-header.md +3 -0
- package/.claude/skills/components/site-header.md +4 -0
- package/.claude/skills/components/site-navigation.md +1 -0
- package/.claude/skills/components/tab-navigation.md +1 -0
- package/.claude/skills/composable-tooltips-guide.md +10 -0
- package/.claude/skills/index.md +8 -1
- package/.claude/skills/storybook-add-story.md +30 -0
- package/.vscode/srcdev-component-animated-svg-text.code-snippets +15 -0
- package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
- package/.vscode/srcdev-component-canvas-switcher.code-snippets +9 -0
- package/.vscode/srcdev-component-clip-element.code-snippets +20 -0
- package/.vscode/srcdev-component-clipped-panel.code-snippets +23 -0
- package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
- package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +9 -0
- package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +9 -0
- package/.vscode/srcdev-component-display-banner.code-snippets +30 -0
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +29 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +38 -0
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
- package/.vscode/srcdev-component-site-header.code-snippets +17 -0
- package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
- package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
- package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
- package/app/components/01.atoms/animations/animated-svg-text/AnimatedSvgText.vue +58 -0
- package/app/components/01.atoms/animations/animated-svg-text/CONSUMER-STYLING.md +34 -0
- package/app/components/01.atoms/animations/animated-svg-text/stories/AnimatedSvgText.stories.ts +69 -0
- package/app/components/01.atoms/animations/animated-svg-text/tests/AnimatedSvgText.spec.ts +63 -0
- package/app/components/01.atoms/animations/animated-svg-text/tests/__snapshots__/AnimatedSvgText.spec.ts.snap +7 -0
- package/app/components/01.atoms/animations/clip-element/CONSUMER-STYLING.md +27 -0
- package/app/components/{clip-element → 01.atoms/animations/clip-element}/ClipElement.vue +16 -11
- package/app/components/01.atoms/animations/clip-element/stories/ClipElement.stories.ts +133 -0
- package/app/components/01.atoms/animations/clip-element/tests/ClipElement.spec.ts +70 -0
- package/app/components/01.atoms/animations/clip-element/tests/__snapshots__/ClipElement.spec.ts.snap +7 -0
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
- package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +27 -0
- package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +111 -0
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +75 -0
- package/app/components/01.atoms/canvas-switcher/tests/CanvasSwitcher.spec.ts +67 -0
- package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +39 -0
- package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +68 -0
- package/app/components/01.atoms/clipped-panel/stories/ClippedPanel.stories.ts +105 -0
- package/app/components/01.atoms/clipped-panel/tests/ClippedPanel.spec.ts +67 -0
- package/app/components/01.atoms/clipped-panel/tests/__snapshots__/ClippedPanel.spec.ts.snap +3 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
- package/app/components/01.atoms/display-banner/CONSUMER-STYLING.md +35 -0
- package/app/components/{display-banner → 01.atoms/display-banner}/DisplayBanner.vue +13 -35
- package/app/components/01.atoms/display-banner/stories/DisplayBanner.stories.ts +100 -0
- package/app/components/01.atoms/display-banner/tests/DisplayBanner.spec.ts +78 -0
- package/app/components/01.atoms/display-banner/tests/__snapshots__/DisplayBanner.spec.ts.snap +8 -0
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +62 -0
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +188 -0
- package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +94 -0
- package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +75 -0
- package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +12 -0
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
- package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +42 -0
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +116 -0
- package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +126 -0
- package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +66 -0
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +17 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +44 -0
- package/app/components/{deep-expanding-menu → 02.molecules/navigation/deep-expanding-menu}/DeepExpandingMenu.vue +53 -64
- package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +79 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +92 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +17 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +40 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +200 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +79 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +82 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +18 -0
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/types/components/display-tooltip-defined.d.ts +10 -0
- package/app/types/components/index.ts +2 -0
- package/app/types/components/marquee-scroller.d.ts +10 -0
- package/package.json +1 -1
- package/app/components/animated-svg-text/AnimatedSvgText.vue +0 -89
- package/app/components/canvas-switcher/CanvasSwitcher.vue +0 -104
- package/app/components/clipped-panels/ClippedPanel.vue +0 -87
- package/app/components/deep-expanding-menu/DeepExpandingMenuOld.vue +0 -218
- package/app/components/display-details/DisplayDetailsCore.vue +0 -122
- package/app/components/display-tooltip/DisplayTooltip.vue +0 -170
- package/app/components/display-tooltip/DisplayTooltipDefined.vue +0 -103
- package/app/components/magnetic-navigation/MagneticNavigation.vue +0 -162
- package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="tag" class="deep-expanding-menu-classic" :class="[elementClasses]">
|
|
3
|
+
<div class="inner">
|
|
4
|
+
<template v-for="(link, key) in navLinks" :key="key">
|
|
5
|
+
<NuxtLink v-if="link.path" :to="link.path" class="navigation-link">{{ link.name }}</NuxtLink>
|
|
6
|
+
<details v-else ref="navigationGroupRef" class="navigation-group" name="navigation-group">
|
|
7
|
+
<summary class="navigation-group-toggle">
|
|
8
|
+
<span>{{ link.name }}</span>
|
|
9
|
+
<Icon name="bi:caret-down-fill" class="icon" />
|
|
10
|
+
</summary>
|
|
11
|
+
<div class="navigation-group-panel">
|
|
12
|
+
<h4 class="page-heading-4 mb-6">{{ link.childLinksTitle }}</h4>
|
|
13
|
+
<ul class="navigation-group-list">
|
|
14
|
+
<li v-for="childLink in link.childLinks" :key="childLink.name" class="navigation-group-item">
|
|
15
|
+
<NuxtLink :to="childLink.path" class="navigation-group-link">{{ childLink.name }}</NuxtLink>
|
|
16
|
+
</li>
|
|
17
|
+
</ul>
|
|
18
|
+
</div>
|
|
19
|
+
</details>
|
|
20
|
+
</template>
|
|
21
|
+
</div>
|
|
22
|
+
</component>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { onClickOutside } from "@vueuse/core";
|
|
27
|
+
import type { ResponsiveHeaderNavItem } from "~/types/components";
|
|
28
|
+
|
|
29
|
+
interface Props {
|
|
30
|
+
tag?: "div" | "section" | "nav" | "ul" | "ol";
|
|
31
|
+
navLinks?: ResponsiveHeaderNavItem[];
|
|
32
|
+
styleClassPassthrough?: string | string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
36
|
+
tag: "nav",
|
|
37
|
+
navLinks: () => [],
|
|
38
|
+
styleClassPassthrough: () => [],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
42
|
+
|
|
43
|
+
const navigationGroupRef = useTemplateRef<HTMLElement[]>("navigationGroupRef");
|
|
44
|
+
|
|
45
|
+
watch(
|
|
46
|
+
() => props.styleClassPassthrough,
|
|
47
|
+
() => {
|
|
48
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
onMounted(() => {
|
|
53
|
+
navigationGroupRef.value?.forEach((element, index) => {
|
|
54
|
+
onClickOutside(element, () => {
|
|
55
|
+
navigationGroupRef.value?.[index]?.removeAttribute("open");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<style lang="css">
|
|
62
|
+
@layer components {
|
|
63
|
+
@layer popover-setup {
|
|
64
|
+
.deep-expanding-menu-classic {
|
|
65
|
+
container-type: inline-size;
|
|
66
|
+
display: grid;
|
|
67
|
+
grid-template-areas: "element-stack";
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: var(--deep-expanding-menu-classic-gap, 1.2rem);
|
|
70
|
+
|
|
71
|
+
.inner {
|
|
72
|
+
grid-area: element-stack;
|
|
73
|
+
display: flex;
|
|
74
|
+
gap: var(--deep-expanding-menu-classic-item-gap, 2.4rem);
|
|
75
|
+
align-items: center;
|
|
76
|
+
z-index: 1;
|
|
77
|
+
|
|
78
|
+
.navigation-link,
|
|
79
|
+
.navigation-group-toggle {
|
|
80
|
+
all: unset;
|
|
81
|
+
border-bottom: var(--deep-expanding-menu-classic-link-border-width, 0.2rem) solid transparent;
|
|
82
|
+
padding-block: var(--deep-expanding-menu-classic-link-padding-block, 0.8rem);
|
|
83
|
+
|
|
84
|
+
transition: border-color 200ms;
|
|
85
|
+
|
|
86
|
+
&:hover,
|
|
87
|
+
&:focus,
|
|
88
|
+
&:focus-visible {
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
border-color: var(
|
|
91
|
+
--deep-expanding-menu-classic-link-border-colour-hover,
|
|
92
|
+
light-dark(var(--blue-10), var(--slate-00))
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.navigation-group {
|
|
98
|
+
--_icon-transform: scaleY(1);
|
|
99
|
+
|
|
100
|
+
display: grid;
|
|
101
|
+
grid-template-areas: "details-stack";
|
|
102
|
+
z-index: 1;
|
|
103
|
+
position: relative;
|
|
104
|
+
|
|
105
|
+
summary::-webkit-details-marker,
|
|
106
|
+
summary::marker {
|
|
107
|
+
display: none;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&[open] {
|
|
111
|
+
--_icon-transform: scaleY(-1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.navigation-group-toggle {
|
|
115
|
+
grid-area: details-stack;
|
|
116
|
+
|
|
117
|
+
display: flex !important;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 1.2rem;
|
|
120
|
+
list-style: none;
|
|
121
|
+
|
|
122
|
+
.icon {
|
|
123
|
+
display: block;
|
|
124
|
+
font-size: var(--deep-expanding-menu-classic-icon-size, 1.2rem);
|
|
125
|
+
|
|
126
|
+
transform: var(--_icon-transform);
|
|
127
|
+
transition: transform 200ms;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.navigation-group-panel {
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-area: details-stack;
|
|
134
|
+
z-index: 2;
|
|
135
|
+
position: absolute;
|
|
136
|
+
inset: auto;
|
|
137
|
+
top: var(--deep-expanding-menu-classic-panel-offset-top, 4rem);
|
|
138
|
+
left: 0rem;
|
|
139
|
+
gap: var(--deep-expanding-menu-classic-panel-list-gap, 1.2rem);
|
|
140
|
+
|
|
141
|
+
width: var(--deep-expanding-menu-classic-panel-width, 20rem);
|
|
142
|
+
|
|
143
|
+
@media screen and (min-width: 768px) {
|
|
144
|
+
width: var(--deep-expanding-menu-classic-panel-width-tablet, 40rem);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@media screen and (min-width: 1024px) {
|
|
148
|
+
width: var(--deep-expanding-menu-classic-panel-width-desktop, 60rem);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
background-color: var(--deep-expanding-menu-classic-panel-background-colour, white);
|
|
152
|
+
border: var(--deep-expanding-menu-classic-panel-border-width, 0.1rem) solid
|
|
153
|
+
var(--deep-expanding-menu-classic-panel-border-colour, black);
|
|
154
|
+
border-radius: var(--deep-expanding-menu-classic-panel-border-radius, 1.2rem);
|
|
155
|
+
box-shadow: var(--deep-expanding-menu-classic-panel-shadow, 0 0 1rem rgba(0, 0, 0, 0.1));
|
|
156
|
+
padding: var(--deep-expanding-menu-classic-panel-padding, 1.2rem);
|
|
157
|
+
overflow: clip;
|
|
158
|
+
|
|
159
|
+
h4 {
|
|
160
|
+
color: var(--deep-expanding-menu-classic-panel-heading-colour, var(--slate-10));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.navigation-group-list {
|
|
164
|
+
display: grid;
|
|
165
|
+
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
|
166
|
+
gap: var(--deep-expanding-menu-classic-panel-list-gap, 1.2rem);
|
|
167
|
+
padding-inline-start: 0;
|
|
168
|
+
margin-block-end: 0.8rem;
|
|
169
|
+
|
|
170
|
+
.navigation-group-item {
|
|
171
|
+
display: block;
|
|
172
|
+
|
|
173
|
+
a.navigation-group-link {
|
|
174
|
+
display: inline-block;
|
|
175
|
+
color: var(--deep-expanding-menu-classic-group-link-colour, var(--slate-10));
|
|
176
|
+
text-decoration: none;
|
|
177
|
+
padding-block: 0.8rem;
|
|
178
|
+
|
|
179
|
+
border-bottom: var(--deep-expanding-menu-classic-link-border-width, 0.2rem) solid transparent;
|
|
180
|
+
|
|
181
|
+
transition: border-color 200ms;
|
|
182
|
+
|
|
183
|
+
&:hover,
|
|
184
|
+
&:focus-visible {
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
border-color: var(
|
|
187
|
+
--deep-expanding-menu-classic-group-link-border-colour-hover,
|
|
188
|
+
var(--slate-10)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
</style>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import DeepExpandingMenuClassic from "../DeepExpandingMenuClassic.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import type { ResponsiveHeaderNavItem } from "~/types/components";
|
|
4
|
+
|
|
5
|
+
const navLinks: ResponsiveHeaderNavItem[] = [
|
|
6
|
+
{ name: "Home", path: "/" },
|
|
7
|
+
{
|
|
8
|
+
name: "Services",
|
|
9
|
+
childLinksTitle: "Our services",
|
|
10
|
+
childLinks: [
|
|
11
|
+
{ name: "Haircuts", path: "/services/haircuts" },
|
|
12
|
+
{ name: "Colouring", path: "/services/colouring" },
|
|
13
|
+
{ name: "Styling", path: "/services/styling" },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{ name: "Contact", path: "/contact" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const meta: Meta<typeof DeepExpandingMenuClassic> = {
|
|
20
|
+
title: "Molecules/DeepExpandingMenuClassic",
|
|
21
|
+
component: DeepExpandingMenuClassic,
|
|
22
|
+
argTypes: {
|
|
23
|
+
tag: {
|
|
24
|
+
control: { type: "select" },
|
|
25
|
+
options: ["div", "section", "nav", "ul", "ol"],
|
|
26
|
+
description: "Root element tag",
|
|
27
|
+
table: { category: "Basic" },
|
|
28
|
+
},
|
|
29
|
+
navLinks: { table: { disable: true } },
|
|
30
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
tag: "nav",
|
|
34
|
+
navLinks,
|
|
35
|
+
},
|
|
36
|
+
parameters: {
|
|
37
|
+
docs: {
|
|
38
|
+
description: {
|
|
39
|
+
component:
|
|
40
|
+
"`<details>`/`<summary>`-based fallback implementation of `DeepExpandingMenu`, using `onClickOutside` to close an open group instead of CSS anchor-positioning/Popover API — use this when the modern implementation's browser support (see its own docs) isn't acceptable.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default meta;
|
|
47
|
+
type Story = StoryObj<typeof DeepExpandingMenuClassic>;
|
|
48
|
+
|
|
49
|
+
export const Default: Story = {
|
|
50
|
+
render: (args) => ({
|
|
51
|
+
components: { DeepExpandingMenuClassic },
|
|
52
|
+
setup() {
|
|
53
|
+
return { args };
|
|
54
|
+
},
|
|
55
|
+
template: `<DeepExpandingMenuClassic v-bind="args" style="padding: 4rem;" />`,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const CustomColours: Story = {
|
|
60
|
+
render: (args) => ({
|
|
61
|
+
components: { DeepExpandingMenuClassic },
|
|
62
|
+
setup() {
|
|
63
|
+
return { args };
|
|
64
|
+
},
|
|
65
|
+
template: `
|
|
66
|
+
<DeepExpandingMenuClassic
|
|
67
|
+
v-bind="args"
|
|
68
|
+
style="padding: 4rem; --deep-expanding-menu-classic-panel-background-colour: #1a1a1a; --deep-expanding-menu-classic-panel-border-colour: transparent; --deep-expanding-menu-classic-group-link-colour: white; --deep-expanding-menu-classic-panel-heading-colour: #aaaaaa;"
|
|
69
|
+
/>
|
|
70
|
+
`,
|
|
71
|
+
}),
|
|
72
|
+
parameters: {
|
|
73
|
+
docs: {
|
|
74
|
+
description: {
|
|
75
|
+
story: "Overriding the public tokens via inline `--deep-expanding-menu-classic-*` custom properties.",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import DeepExpandingMenuClassic from "../DeepExpandingMenuClassic.vue";
|
|
4
|
+
import type { ResponsiveHeaderNavItem } from "~/types/components";
|
|
5
|
+
|
|
6
|
+
const defaultNavLinks: ResponsiveHeaderNavItem[] = [
|
|
7
|
+
{ name: "Home", path: "/" },
|
|
8
|
+
{
|
|
9
|
+
name: "Services",
|
|
10
|
+
childLinksTitle: "Our services",
|
|
11
|
+
childLinks: [
|
|
12
|
+
{ name: "Haircuts", path: "/services/haircuts" },
|
|
13
|
+
{ name: "Colouring", path: "/services/colouring" },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{ name: "Contact", path: "/contact" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
describe("DeepExpandingMenuClassic", () => {
|
|
20
|
+
it("mounts without error", async () => {
|
|
21
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, { props: { navLinks: defaultNavLinks } });
|
|
22
|
+
expect(wrapper.vm).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders correct HTML structure", async () => {
|
|
26
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, { props: { navLinks: defaultNavLinks } });
|
|
27
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders a <nav> by default", async () => {
|
|
31
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, { props: { navLinks: defaultNavLinks } });
|
|
32
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe("nav");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renders the given tag", async () => {
|
|
36
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, {
|
|
37
|
+
props: { navLinks: defaultNavLinks, tag: "div" },
|
|
38
|
+
});
|
|
39
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe("div");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders a direct link for items with a path", async () => {
|
|
43
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, { props: { navLinks: defaultNavLinks } });
|
|
44
|
+
const links = wrapper.findAll(".navigation-link");
|
|
45
|
+
expect(links.map((l) => l.text())).toEqual(["Home", "Contact"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders a <details> group for items with childLinks", async () => {
|
|
49
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, { props: { navLinks: defaultNavLinks } });
|
|
50
|
+
expect(wrapper.find("details.navigation-group").exists()).toBe(true);
|
|
51
|
+
expect(wrapper.find(".navigation-group-toggle").text()).toContain("Services");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders each child link inside the group panel", async () => {
|
|
55
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, { props: { navLinks: defaultNavLinks } });
|
|
56
|
+
const childLinks = wrapper.findAll(".navigation-group-link");
|
|
57
|
+
expect(childLinks.map((l) => l.text())).toEqual(["Haircuts", "Colouring"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("renders the childLinksTitle heading", async () => {
|
|
61
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, { props: { navLinks: defaultNavLinks } });
|
|
62
|
+
expect(wrapper.find(".navigation-group-panel h4").text()).toBe("Our services");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("applies styleClassPassthrough classes", async () => {
|
|
66
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, {
|
|
67
|
+
props: { navLinks: defaultNavLinks, styleClassPassthrough: ["custom-class"] },
|
|
68
|
+
});
|
|
69
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("resets classes when styleClassPassthrough prop changes", async () => {
|
|
73
|
+
const wrapper = await mountSuspended(DeepExpandingMenuClassic, {
|
|
74
|
+
props: { navLinks: defaultNavLinks, styleClassPassthrough: ["initial-class"] },
|
|
75
|
+
});
|
|
76
|
+
expect(wrapper.classes()).toContain("initial-class");
|
|
77
|
+
|
|
78
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
|
|
79
|
+
expect(wrapper.classes()).not.toContain("initial-class");
|
|
80
|
+
expect(wrapper.classes()).toContain("updated-class");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`DeepExpandingMenuClassic > renders correct HTML structure 1`] = `
|
|
4
|
+
"<nav class="deep-expanding-menu-classic">
|
|
5
|
+
<div class="inner"><a href="/" class="navigation-link">Home</a>
|
|
6
|
+
<details class="navigation-group" name="navigation-group">
|
|
7
|
+
<summary class="navigation-group-toggle"><span>Services</span><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></summary>
|
|
8
|
+
<div class="navigation-group-panel">
|
|
9
|
+
<h4 class="page-heading-4 mb-6">Our services</h4>
|
|
10
|
+
<ul class="navigation-group-list">
|
|
11
|
+
<li class="navigation-group-item"><a href="/services/haircuts" class="navigation-group-link">Haircuts</a></li>
|
|
12
|
+
<li class="navigation-group-item"><a href="/services/colouring" class="navigation-group-link">Colouring</a></li>
|
|
13
|
+
</ul>
|
|
14
|
+
</div>
|
|
15
|
+
</details><a href="/contact" class="navigation-link">Contact</a>
|
|
16
|
+
</div>
|
|
17
|
+
</nav>"
|
|
18
|
+
`;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
`site-navigation--${navAlign}`,
|
|
8
8
|
{ 'is-collapsed': isCollapsed, 'is-loaded': isLoaded, 'menu-open': isMenuOpen, 'is-animated': isAnimated },
|
|
9
9
|
]"
|
|
10
|
-
aria-label="
|
|
10
|
+
:aria-label="ariaLabel"
|
|
11
11
|
>
|
|
12
12
|
<ul
|
|
13
13
|
v-if="!isCollapsed || !isLoaded"
|
|
@@ -106,11 +106,14 @@ interface Props {
|
|
|
106
106
|
navItemData: NavItemData;
|
|
107
107
|
navAlign?: "left" | "center" | "right";
|
|
108
108
|
styleClassPassthrough?: string | string[];
|
|
109
|
+
/** aria-label on the nav landmark — override for localisation. */
|
|
110
|
+
ariaLabel?: string;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
const props = withDefaults(defineProps<Props>(), {
|
|
112
114
|
navAlign: "left",
|
|
113
115
|
styleClassPassthrough: () => [],
|
|
116
|
+
ariaLabel: "Site navigation",
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
// ─── Animation gate — prevents indicator from transitioning on first paint ───
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
`tab-navigation--${navAlign}`,
|
|
8
8
|
{ 'is-collapsed': isCollapsed, 'is-loaded': isLoaded, 'menu-open': isMenuOpen, 'is-animated': isAnimated },
|
|
9
9
|
]"
|
|
10
|
-
aria-label="
|
|
10
|
+
:aria-label="ariaLabel"
|
|
11
11
|
>
|
|
12
12
|
<ul v-if="!isCollapsed || !isLoaded" ref="navListRef" class="tab-nav-list" @mouseleave="hoveredItemHref = null">
|
|
13
13
|
<li
|
|
@@ -131,12 +131,15 @@ interface Props {
|
|
|
131
131
|
navAlign?: "left" | "center" | "right";
|
|
132
132
|
styleClassPassthrough?: string | string[];
|
|
133
133
|
anchorScrollOffset?: number | (() => number);
|
|
134
|
+
/** aria-label on the nav landmark — override for localisation. */
|
|
135
|
+
ariaLabel?: string;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
const props = withDefaults(defineProps<Props>(), {
|
|
137
139
|
navAlign: "left",
|
|
138
140
|
styleClassPassthrough: () => [],
|
|
139
141
|
anchorScrollOffset: undefined,
|
|
142
|
+
ariaLabel: "Site navigation",
|
|
140
143
|
});
|
|
141
144
|
|
|
142
145
|
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, isActiveItem, toggleMenu, closeMenu } =
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div ref="sliderGalleryWrapper" class="slider-gallery" :class="[elementClasses]">
|
|
3
3
|
<div class="loading-state" :class="[{ galleryLoaded: !galleryLoaded }]">
|
|
4
4
|
<div class="loading-spinner"></div>
|
|
5
|
-
<p>
|
|
5
|
+
<p>{{ loadingText }}</p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
<div v-if="showGallery" class="gallery-content" :class="[{ galleryLoaded: !galleryLoaded }]">
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{{ item.description }}
|
|
18
18
|
</div>
|
|
19
19
|
<div class="buttons" :class="item.textBrightness">
|
|
20
|
-
<button>
|
|
20
|
+
<button>{{ seeMoreText }}</button>
|
|
21
21
|
</div>
|
|
22
22
|
</div>
|
|
23
23
|
</div>
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
</div>
|
|
41
41
|
|
|
42
42
|
<div class="arrows">
|
|
43
|
-
<button id="prev" ref="prevDom" aria-label="
|
|
43
|
+
<button id="prev" ref="prevDom" :aria-label="prevAriaLabel" @click.prevent="doPrevious()">
|
|
44
44
|
<Icon name="ic:outline-keyboard-arrow-left" class="arrows-icon" />
|
|
45
45
|
</button>
|
|
46
|
-
<button id="next" ref="nextDom" aria-label="
|
|
46
|
+
<button id="next" ref="nextDom" :aria-label="nextAriaLabel" @click.prevent="doNext()">
|
|
47
47
|
<Icon name="ic:outline-keyboard-arrow-right" class="arrows-icon" />
|
|
48
48
|
</button>
|
|
49
49
|
</div>
|
|
@@ -60,6 +60,14 @@ interface Props {
|
|
|
60
60
|
autoRun?: boolean;
|
|
61
61
|
autoRunInterval?: number;
|
|
62
62
|
animationDuration?: number;
|
|
63
|
+
/** Loading-state copy — override for localisation. */
|
|
64
|
+
loadingText?: string;
|
|
65
|
+
/** Per-slide call-to-action button copy — override for localisation. */
|
|
66
|
+
seeMoreText?: string;
|
|
67
|
+
/** aria-label on the previous-image button — override for localisation. */
|
|
68
|
+
prevAriaLabel?: string;
|
|
69
|
+
/** aria-label on the next-image button — override for localisation. */
|
|
70
|
+
nextAriaLabel?: string;
|
|
63
71
|
styleClassPassthrough?: string | string[];
|
|
64
72
|
}
|
|
65
73
|
|
|
@@ -67,6 +75,10 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
67
75
|
autoRun: true,
|
|
68
76
|
autoRunInterval: 7000,
|
|
69
77
|
animationDuration: 3000,
|
|
78
|
+
loadingText: "Loading gallery...",
|
|
79
|
+
seeMoreText: "SEE MORE",
|
|
80
|
+
prevAriaLabel: "Previous image",
|
|
81
|
+
nextAriaLabel: "Next image",
|
|
70
82
|
styleClassPassthrough: () => [],
|
|
71
83
|
});
|
|
72
84
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
class="overflow-navigation-wrapper"
|
|
4
4
|
:class="[elementClasses, { 'is-panel-animating': isPanelAnimating }]"
|
|
5
5
|
role="menu"
|
|
6
|
-
aria-label="
|
|
6
|
+
:aria-label="ariaLabel"
|
|
7
7
|
@mouseleave="
|
|
8
8
|
hoveredItemKey = null;
|
|
9
9
|
hoveredChildKey = null;
|
|
@@ -115,6 +115,8 @@ interface Props {
|
|
|
115
115
|
mainNavigationState?: ResponsiveHeaderState;
|
|
116
116
|
panelVariant?: "modern" | "classic";
|
|
117
117
|
styleClassPassthrough?: string | string[];
|
|
118
|
+
/** aria-label on the overflow menu — override for localisation. */
|
|
119
|
+
ariaLabel?: string;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -124,6 +126,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
124
126
|
// "modern" is opt-in.
|
|
125
127
|
panelVariant: "classic",
|
|
126
128
|
styleClassPassthrough: () => [],
|
|
129
|
+
ariaLabel: "Overflow navigation menu",
|
|
127
130
|
});
|
|
128
131
|
|
|
129
132
|
const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
ref="mainNav"
|
|
10
10
|
class="main-navigation"
|
|
11
11
|
:class="{ 'is-animated': isAnimated }"
|
|
12
|
-
aria-label="
|
|
12
|
+
:aria-label="mainNavAriaLabel"
|
|
13
13
|
@mouseleave="hoveredItemKey = null"
|
|
14
14
|
@focusout="handleNavFocusout"
|
|
15
15
|
>
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
<div aria-hidden="true" class="nav-indicator-hovered"></div>
|
|
79
79
|
<div aria-hidden="true" class="nav-indicator-active"></div>
|
|
80
80
|
</nav>
|
|
81
|
-
<nav ref="secondaryNav" class="secondary-navigation" aria-label="
|
|
81
|
+
<nav ref="secondaryNav" class="secondary-navigation" :aria-label="secondaryNavAriaLabel">
|
|
82
82
|
<details
|
|
83
83
|
ref="overflowDetails"
|
|
84
84
|
class="overflow-details"
|
|
@@ -100,7 +100,11 @@
|
|
|
100
100
|
/>
|
|
101
101
|
</summary>
|
|
102
102
|
<div class="overflow-details-nav" role="menu">
|
|
103
|
-
<NavigationItems
|
|
103
|
+
<NavigationItems
|
|
104
|
+
:main-navigation-state="mainNavigationState"
|
|
105
|
+
:panel-variant="panelVariant"
|
|
106
|
+
:aria-label="overflowMenuAriaLabel"
|
|
107
|
+
/>
|
|
104
108
|
</div>
|
|
105
109
|
</details>
|
|
106
110
|
<slot v-if="slots.secondaryNavigation" name="secondaryNavigation"></slot>
|
|
@@ -126,6 +130,12 @@ interface Props {
|
|
|
126
130
|
styleClassPassthrough?: string | string[];
|
|
127
131
|
allowExpandOnGesture?: boolean;
|
|
128
132
|
panelVariant?: "modern" | "classic";
|
|
133
|
+
/** aria-label on the primary nav landmark — override for localisation. */
|
|
134
|
+
mainNavAriaLabel?: string;
|
|
135
|
+
/** aria-label on the secondary (overflow) nav landmark — override for localisation. */
|
|
136
|
+
secondaryNavAriaLabel?: string;
|
|
137
|
+
/** aria-label on the overflow menu itself (forwarded to NavigationItems) — override for localisation. */
|
|
138
|
+
overflowMenuAriaLabel?: string;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -142,6 +152,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
142
152
|
// Forwarded to NavigationItems' overflow submenu panels — see its own default for why
|
|
143
153
|
// "classic" is the safe default (CLAUDE.md pitfall #19).
|
|
144
154
|
panelVariant: "classic",
|
|
155
|
+
mainNavAriaLabel: "Main navigation",
|
|
156
|
+
secondaryNavAriaLabel: "Secondary navigation",
|
|
157
|
+
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
145
158
|
});
|
|
146
159
|
|
|
147
160
|
const collapseNavigationBelowWidth = computed(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<PageRow tag="div" :variant="pageRowVariant" :style-class-passthrough="styleClassPassthrough">
|
|
3
3
|
<template #default>
|
|
4
4
|
<header class="site-header">
|
|
5
|
-
<nav class="home-navigation" aria-label="
|
|
5
|
+
<nav class="home-navigation" :aria-label="homeNavAriaLabel">
|
|
6
6
|
<SkipLinks>
|
|
7
7
|
<template #homeLink>
|
|
8
8
|
<slot name="branding"></slot>
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
:collapse-at-main-nav-intersection="collapseAtMainNavIntersection"
|
|
18
18
|
:allow-expand-on-gesture="allowExpandOnGesture"
|
|
19
19
|
:style-class-passthrough="navStyleClassPassthrough"
|
|
20
|
+
:main-nav-aria-label="mainNavAriaLabel"
|
|
21
|
+
:secondary-nav-aria-label="secondaryNavAriaLabel"
|
|
22
|
+
:overflow-menu-aria-label="overflowMenuAriaLabel"
|
|
20
23
|
>
|
|
21
24
|
<template v-if="slots.secondaryNavigation" #secondaryNavigation>
|
|
22
25
|
<slot name="secondaryNavigation"></slot>
|
|
@@ -43,6 +46,14 @@ interface Props {
|
|
|
43
46
|
pageRowVariant?: "full" | "popout" | "content" | "inset-content";
|
|
44
47
|
styleClassPassthrough?: string | string[];
|
|
45
48
|
navStyleClassPassthrough?: string | string[];
|
|
49
|
+
/** aria-label on the home/branding nav landmark — override for localisation. */
|
|
50
|
+
homeNavAriaLabel?: string;
|
|
51
|
+
/** Forwarded to ResponsiveHeader's mainNavAriaLabel — override for localisation. */
|
|
52
|
+
mainNavAriaLabel?: string;
|
|
53
|
+
/** Forwarded to ResponsiveHeader's secondaryNavAriaLabel — override for localisation. */
|
|
54
|
+
secondaryNavAriaLabel?: string;
|
|
55
|
+
/** Forwarded to ResponsiveHeader's overflowMenuAriaLabel — override for localisation. */
|
|
56
|
+
overflowMenuAriaLabel?: string;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
withDefaults(defineProps<Props>(), {
|
|
@@ -58,6 +69,10 @@ withDefaults(defineProps<Props>(), {
|
|
|
58
69
|
pageRowVariant: "content",
|
|
59
70
|
styleClassPassthrough: () => [],
|
|
60
71
|
navStyleClassPassthrough: () => [],
|
|
72
|
+
homeNavAriaLabel: "Home Navigation",
|
|
73
|
+
mainNavAriaLabel: "Main navigation",
|
|
74
|
+
secondaryNavAriaLabel: "Secondary navigation",
|
|
75
|
+
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
61
76
|
});
|
|
62
77
|
|
|
63
78
|
const slots = useSlots();
|
|
@@ -9,7 +9,7 @@ exports[`SiteHeader > renders correct HTML structure with default props 1`] = `
|
|
|
9
9
|
<nav class="skip-links-nav" aria-label="Skip navigation"><a href="#main-content" class="skip-link">Skip to main content</a><a href="#footer-content" class="skip-link">Skip to footer</a></nav>
|
|
10
10
|
</div>
|
|
11
11
|
</nav>
|
|
12
|
-
<div class="responsive-header-stub"></div>
|
|
12
|
+
<div class="responsive-header-stub" main-nav-aria-label="Main navigation" secondary-nav-aria-label="Secondary navigation" overflow-menu-aria-label="Overflow navigation menu"></div>
|
|
13
13
|
</header>
|
|
14
14
|
</div>"
|
|
15
15
|
`;
|