srcdev-nuxt-components 9.1.0 → 9.1.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.
Files changed (20) hide show
  1. package/.claude/settings.json +2 -1
  2. package/.claude/skills/components/page-hero-highlights.md +60 -0
  3. package/.claude/skills/components/site-navigation.md +120 -0
  4. package/.claude/skills/index.md +1 -1
  5. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +780 -0
  6. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +335 -0
  7. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +328 -0
  8. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +30 -0
  9. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +36 -21
  10. package/app/components/04.templates/page-hero-highlights/PageHeroHighlightsHeader.vue +66 -0
  11. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +50 -3
  12. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +77 -0
  13. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +15 -7
  14. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +51 -0
  15. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +1 -1
  16. package/app/layouts/default.vue +1 -0
  17. package/app/pages/page-hero-highlights.vue +15 -11
  18. package/nuxt.config.ts +7 -0
  19. package/package.json +6 -6
  20. package/.claude/skills/components/treatment-consultant.md +0 -128
@@ -0,0 +1,335 @@
1
+ import { ref, reactive, computed } from "vue";
2
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
3
+ import SiteNavigationComponent from "../SiteNavigation.vue";
4
+ import type { NavItemData } from "~/types/components/navigation-horizontal.d";
5
+
6
+ const meta: Meta<typeof SiteNavigationComponent> = {
7
+ title: "Molecules/SiteNavigation",
8
+ component: SiteNavigationComponent,
9
+ argTypes: {
10
+ navAlign: {
11
+ control: { type: "select" },
12
+ options: ["left", "center", "right"],
13
+ description: "Horizontal alignment of the nav list",
14
+ },
15
+ navItemData: { table: { disable: true } },
16
+ styleClassPassthrough: { table: { disable: true } },
17
+ },
18
+ args: {
19
+ navAlign: "left",
20
+ },
21
+ };
22
+
23
+ export default meta;
24
+
25
+ const navItemData: NavItemData = {
26
+ main: [
27
+ { text: "Home", href: "#" },
28
+ { text: "About", href: "#" },
29
+ { text: "Services", href: "#" },
30
+ { text: "Work", href: "#" },
31
+ { text: "Contact", href: "#" },
32
+ ],
33
+ };
34
+
35
+ type Theme = "light" | "dark";
36
+
37
+ const themeDefaults: Record<
38
+ Theme,
39
+ {
40
+ indicatorColor: string;
41
+ linkColor: string;
42
+ linkHoverColor: string;
43
+ linkActiveColor: string;
44
+ panelBg: string;
45
+ panelLinkColor: string;
46
+ burgerColor: string;
47
+ pageBg: string;
48
+ }
49
+ > = {
50
+ dark: {
51
+ indicatorColor: "#c0847a",
52
+ linkColor: "#f0ece8",
53
+ linkHoverColor: "#e09890",
54
+ linkActiveColor: "#c0847a",
55
+ panelBg: "#1a1614",
56
+ panelLinkColor: "#f0ece8",
57
+ burgerColor: "#f0ece8",
58
+ pageBg: "#1a1614",
59
+ },
60
+ light: {
61
+ indicatorColor: "#b05a50",
62
+ linkColor: "#1a1614",
63
+ linkHoverColor: "#b05a50",
64
+ linkActiveColor: "#8c3a30",
65
+ panelBg: "#f5f2f0",
66
+ panelLinkColor: "#1a1614",
67
+ burgerColor: "#1a1614",
68
+ pageBg: "#f5f2f0",
69
+ },
70
+ };
71
+
72
+ const DefaultTemplate: StoryFn<typeof SiteNavigationComponent> = (args) => ({
73
+ components: { SiteNavigationComponent },
74
+ setup() {
75
+ const theme = ref<Theme>("dark");
76
+
77
+ const controls = reactive({ ...themeDefaults.dark });
78
+
79
+ const setTheme = (newTheme: Theme) => {
80
+ theme.value = newTheme;
81
+ Object.assign(controls, themeDefaults[newTheme]);
82
+ };
83
+
84
+ const navStyle = computed(() => ({
85
+ "--site-nav-decorator-indicator-color": controls.indicatorColor,
86
+ "--site-nav-link-color": controls.linkColor,
87
+ "--site-nav-link-hover-color": controls.linkHoverColor,
88
+ "--site-nav-link-active-color": controls.linkActiveColor,
89
+ "--site-nav-panel-bg": controls.panelBg,
90
+ "--site-nav-panel-link-color": controls.panelLinkColor,
91
+ "--site-nav-panel-decorator-indicator-color": controls.indicatorColor,
92
+ "--site-nav-burger-color": controls.burgerColor,
93
+ "--page-bg": controls.pageBg,
94
+ "margin-block": "3.6rem",
95
+ }));
96
+
97
+ const cssSnippet = computed(
98
+ () => `.your-selector {
99
+ /* Indicator */
100
+ --site-nav-decorator-indicator-color: ${controls.indicatorColor};
101
+
102
+ /* Horizontal nav links */
103
+ --site-nav-link-color: ${controls.linkColor};
104
+ --site-nav-link-hover-color: ${controls.linkHoverColor};
105
+ --site-nav-link-active-color: ${controls.linkActiveColor};
106
+
107
+ /* Mobile panel */
108
+ --site-nav-panel-bg: ${controls.panelBg};
109
+ --site-nav-panel-link-color: ${controls.panelLinkColor};
110
+ --site-nav-panel-decorator-indicator-color: ${controls.indicatorColor};
111
+
112
+ /* Burger */
113
+ --site-nav-burger-color: ${controls.burgerColor};
114
+ }`
115
+ );
116
+
117
+ const copied = ref(false);
118
+ const copySnippet = async () => {
119
+ await navigator.clipboard.writeText(cssSnippet.value);
120
+ copied.value = true;
121
+ setTimeout(() => {
122
+ copied.value = false;
123
+ }, 2000);
124
+ };
125
+
126
+ return { args, navItemData, theme, controls, setTheme, navStyle, cssSnippet, copied, copySnippet };
127
+ },
128
+ template: `
129
+ <div class="sb-sitenav-story">
130
+
131
+ <!-- Preview: simulated header bar -->
132
+ <div class="sb-sitenav-note">
133
+ Resize the browser window to see the navigation collapse into a burger menu.
134
+ </div>
135
+ <div class="sb-sitenav-header" :class="'theme-' + theme" :style="navStyle">
136
+ <div class="sb-sitenav-logo">LOGO</div>
137
+ <SiteNavigationComponent
138
+ :nav-item-data="navItemData"
139
+ :nav-align="args.navAlign"
140
+ />
141
+ </div>
142
+
143
+ <!-- Controls -->
144
+ <div class="sb-sitenav-playground">
145
+
146
+ <fieldset>
147
+ <legend>Theme</legend>
148
+ <div class="sb-control-row">
149
+ <label>Light / Dark</label>
150
+ <div class="sb-theme-toggle">
151
+ <button :class="{ active: theme === 'light' }" @click="setTheme('light')">Light</button>
152
+ <button :class="{ active: theme === 'dark' }" @click="setTheme('dark')">Dark</button>
153
+ </div>
154
+ </div>
155
+ </fieldset>
156
+
157
+ <fieldset>
158
+ <legend>Indicator</legend>
159
+ <div class="sb-control-row">
160
+ <label for="sb-indicator-color">Indicator colour</label>
161
+ <input id="sb-indicator-color" v-model="controls.indicatorColor" type="color" />
162
+ </div>
163
+ </fieldset>
164
+
165
+ <fieldset>
166
+ <legend>Horizontal nav links</legend>
167
+ <div class="sb-control-row">
168
+ <label for="sb-link-color">Link colour</label>
169
+ <input id="sb-link-color" v-model="controls.linkColor" type="color" />
170
+ </div>
171
+ <div class="sb-control-row">
172
+ <label for="sb-link-hover-color">Hover colour</label>
173
+ <input id="sb-link-hover-color" v-model="controls.linkHoverColor" type="color" />
174
+ </div>
175
+ <div class="sb-control-row">
176
+ <label for="sb-link-active-color">Active colour</label>
177
+ <input id="sb-link-active-color" v-model="controls.linkActiveColor" type="color" />
178
+ </div>
179
+ </fieldset>
180
+
181
+ <fieldset>
182
+ <legend>Mobile panel</legend>
183
+ <div class="sb-control-row">
184
+ <label for="sb-panel-bg">Panel background</label>
185
+ <input id="sb-panel-bg" v-model="controls.panelBg" type="color" />
186
+ </div>
187
+ <div class="sb-control-row">
188
+ <label for="sb-panel-link-color">Panel link colour</label>
189
+ <input id="sb-panel-link-color" v-model="controls.panelLinkColor" type="color" />
190
+ </div>
191
+ </fieldset>
192
+
193
+ <fieldset>
194
+ <legend>Burger</legend>
195
+ <div class="sb-control-row">
196
+ <label for="sb-burger-color">Burger colour</label>
197
+ <input id="sb-burger-color" v-model="controls.burgerColor" type="color" />
198
+ </div>
199
+ </fieldset>
200
+
201
+ </div>
202
+
203
+ <!-- CSS snippet -->
204
+ <div class="sb-css-snippet">
205
+ <div class="sb-css-snippet-header">
206
+ <strong>CSS Token Snippet</strong>
207
+ <button class="sb-copy-btn" @click="copySnippet">{{ copied ? 'Copied!' : 'Copy' }}</button>
208
+ </div>
209
+ <pre class="sb-css-snippet-code">{{ cssSnippet }}</pre>
210
+ </div>
211
+
212
+ </div>
213
+
214
+ <style>
215
+ .sb-sitenav-story {
216
+ font-size: 1.4rem;
217
+ display: grid;
218
+ gap: 2.4rem;
219
+ padding: 2.4rem;
220
+ }
221
+ .sb-sitenav-note {
222
+ font-size: 1.3rem;
223
+ color: #888;
224
+ font-style: italic;
225
+ }
226
+ .sb-sitenav-header {
227
+ display: flex;
228
+ align-items: center;
229
+ gap: 3.2rem;
230
+ padding: 1.2rem 2.4rem;
231
+ transition: background 300ms;
232
+ position: relative;
233
+ min-height: 6rem;
234
+ border-radius: 0.4rem;
235
+ }
236
+ .sb-sitenav-header.theme-dark {
237
+ background: var(--page-bg, #1a1614);
238
+ color-scheme: dark;
239
+ }
240
+ .sb-sitenav-header.theme-light {
241
+ background: var(--page-bg, #f5f2f0);
242
+ color-scheme: light;
243
+ }
244
+ .sb-sitenav-logo {
245
+ font-size: 1.8rem;
246
+ font-weight: 700;
247
+ letter-spacing: 0.1em;
248
+ flex-shrink: 0;
249
+ opacity: 0.5;
250
+ }
251
+ .sb-sitenav-header .site-navigation {
252
+ flex: 1;
253
+ min-width: 0;
254
+ }
255
+ .sb-sitenav-playground {
256
+ display: grid;
257
+ grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
258
+ gap: 1.6rem;
259
+ }
260
+ .sb-sitenav-playground fieldset {
261
+ border: 1px solid #ccc;
262
+ border-radius: 0.4rem;
263
+ padding: 1.6rem;
264
+ }
265
+ .sb-sitenav-playground legend {
266
+ font-weight: bold;
267
+ padding: 0 0.8rem;
268
+ font-size: 1.4rem;
269
+ }
270
+ .sb-control-row {
271
+ display: grid;
272
+ grid-template-columns: 1fr 14rem;
273
+ gap: 1rem;
274
+ align-items: center;
275
+ padding: 0.5rem 0;
276
+ font-size: 1.3rem;
277
+ }
278
+ .sb-control-row input[type="color"] {
279
+ height: 3.2rem;
280
+ width: 100%;
281
+ padding: 0.2rem;
282
+ border: 1px solid #ccc;
283
+ border-radius: 0.2rem;
284
+ cursor: pointer;
285
+ background: transparent;
286
+ }
287
+ .sb-theme-toggle { display: flex; gap: 0.4rem; }
288
+ .sb-theme-toggle button {
289
+ padding: 0.4rem 1.6rem;
290
+ border: 1px solid #ccc;
291
+ border-radius: 0.2rem;
292
+ cursor: pointer;
293
+ background: transparent;
294
+ font-size: 1.3rem;
295
+ color: inherit;
296
+ }
297
+ .sb-theme-toggle button.active { background: #e0e0e0; font-weight: bold; }
298
+ .sb-css-snippet {
299
+ border: 1px solid #ccc;
300
+ border-radius: 0.4rem;
301
+ overflow: hidden;
302
+ }
303
+ .sb-css-snippet-header {
304
+ display: flex;
305
+ justify-content: space-between;
306
+ align-items: center;
307
+ padding: 1rem 1.6rem;
308
+ border-bottom: 1px solid #ccc;
309
+ font-size: 1.4rem;
310
+ }
311
+ .sb-css-snippet-code {
312
+ margin: 0;
313
+ padding: 1.6rem;
314
+ font-family: monospace;
315
+ font-size: 1.3rem;
316
+ line-height: 1.6;
317
+ overflow-x: auto;
318
+ white-space: pre;
319
+ }
320
+ .sb-copy-btn {
321
+ padding: 0.4rem 1.2rem;
322
+ border: 1px solid #ccc;
323
+ border-radius: 0.2rem;
324
+ cursor: pointer;
325
+ background: transparent;
326
+ font-size: 1.3rem;
327
+ color: inherit;
328
+ }
329
+ .sb-copy-btn:hover { background: #e0e0e0; }
330
+ </style>
331
+ `,
332
+ });
333
+
334
+ export const Default = DefaultTemplate.bind({});
335
+ Default.args = { navAlign: "left" };
@@ -0,0 +1,328 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import { nextTick } from "vue";
4
+ import SiteNavigation from "../SiteNavigation.vue";
5
+ import type { NavItemData } from "~/types/components/navigation-horizontal.d";
6
+
7
+ // useResizeObserver (from @vueuse/core) requires ResizeObserver
8
+ const mockResizeObserver = vi.fn(() => ({
9
+ observe: vi.fn(),
10
+ unobserve: vi.fn(),
11
+ disconnect: vi.fn(),
12
+ }));
13
+
14
+ beforeEach(() => {
15
+ vi.stubGlobal("ResizeObserver", mockResizeObserver);
16
+ // ⚠️ Do NOT call vi.unstubAllGlobals() in afterEach —
17
+ // it removes global stubs from vitest.setup.ts ($fetch, etc.)
18
+ });
19
+
20
+ const defaultNavItemData: NavItemData = {
21
+ main: [
22
+ { text: "Home", href: "/" },
23
+ { text: "About", href: "/about" },
24
+ { text: "Contact", href: "/contact" },
25
+ ],
26
+ };
27
+
28
+ describe("SiteNavigation", () => {
29
+ it("mounts without error", async () => {
30
+ const wrapper = await mountSuspended(SiteNavigation, {
31
+ props: { navItemData: defaultNavItemData },
32
+ });
33
+ expect(wrapper.vm).toBeTruthy();
34
+ });
35
+
36
+ it("renders a nav element as root", async () => {
37
+ const wrapper = await mountSuspended(SiteNavigation, {
38
+ props: { navItemData: defaultNavItemData },
39
+ });
40
+ expect(wrapper.element.tagName.toLowerCase()).toBe("nav");
41
+ });
42
+
43
+ it("has aria-label on the nav", async () => {
44
+ const wrapper = await mountSuspended(SiteNavigation, {
45
+ props: { navItemData: defaultNavItemData },
46
+ });
47
+ expect(wrapper.attributes("aria-label")).toBe("Site navigation");
48
+ });
49
+
50
+ it("renders correct HTML structure", async () => {
51
+ const wrapper = await mountSuspended(SiteNavigation, {
52
+ props: { navItemData: defaultNavItemData },
53
+ });
54
+ expect(wrapper.html()).toMatchSnapshot();
55
+ });
56
+
57
+ // ─── isLoaded ──────────────────────────────────────────────────────────────
58
+
59
+ it("applies is-loaded class after mounting", async () => {
60
+ const wrapper = await mountSuspended(SiteNavigation, {
61
+ props: { navItemData: defaultNavItemData },
62
+ });
63
+ await nextTick();
64
+ expect(wrapper.classes()).toContain("is-loaded");
65
+ });
66
+
67
+ // ─── Nav list ──────────────────────────────────────────────────────────────
68
+
69
+ it("renders the nav list when not collapsed", async () => {
70
+ const wrapper = await mountSuspended(SiteNavigation, {
71
+ props: { navItemData: defaultNavItemData },
72
+ });
73
+ expect(wrapper.find(".site-nav-list").exists()).toBe(true);
74
+ });
75
+
76
+ it("renders the correct number of nav items", async () => {
77
+ const wrapper = await mountSuspended(SiteNavigation, {
78
+ props: { navItemData: defaultNavItemData },
79
+ });
80
+ // Filter out .nav-indicator-li elements injected by initNavDecorators
81
+ const items = wrapper.findAll(".site-nav-list li:not(.nav-indicator-li)");
82
+ expect(items.length).toBe(3);
83
+ });
84
+
85
+ it("renders item text correctly", async () => {
86
+ const wrapper = await mountSuspended(SiteNavigation, {
87
+ props: { navItemData: defaultNavItemData },
88
+ });
89
+ const links = wrapper.findAll(".site-nav-link");
90
+ expect(links[0]!.text()).toContain("Home");
91
+ expect(links[1]!.text()).toContain("About");
92
+ expect(links[2]!.text()).toContain("Contact");
93
+ });
94
+
95
+ it("renders item href correctly", async () => {
96
+ const wrapper = await mountSuspended(SiteNavigation, {
97
+ props: { navItemData: defaultNavItemData },
98
+ });
99
+ const links = wrapper.findAll(".site-nav-link");
100
+ expect(links[0]!.attributes("href")).toBe("/");
101
+ expect(links[1]!.attributes("href")).toBe("/about");
102
+ expect(links[2]!.attributes("href")).toBe("/contact");
103
+ });
104
+
105
+ it("applies cssName as class on the li element", async () => {
106
+ const navItemData: NavItemData = {
107
+ main: [{ text: "Home", href: "/", cssName: "is-active" }],
108
+ };
109
+ const wrapper = await mountSuspended(SiteNavigation, {
110
+ props: { navItemData },
111
+ });
112
+ expect(wrapper.find(".site-nav-list li").classes()).toContain("is-active");
113
+ });
114
+
115
+ it("renders an icon when iconName is provided", async () => {
116
+ const navItemData: NavItemData = {
117
+ main: [{ text: "Home", href: "/", iconName: "heroicons:home" }],
118
+ };
119
+ const wrapper = await mountSuspended(SiteNavigation, {
120
+ props: { navItemData },
121
+ });
122
+ // Icon renders with aria-hidden="true"
123
+ const firstItem = wrapper.find(".site-nav-list li:not(.nav-indicator-li)");
124
+ expect(firstItem.find("[aria-hidden='true']").exists()).toBe(true);
125
+ });
126
+
127
+ it("does not render an icon when iconName is not provided", async () => {
128
+ const wrapper = await mountSuspended(SiteNavigation, {
129
+ props: { navItemData: defaultNavItemData },
130
+ });
131
+ const firstLink = wrapper.find(".site-nav-link");
132
+ expect(firstLink.find("[aria-hidden='true']").exists()).toBe(false);
133
+ });
134
+
135
+ it("renders external links with the correct href", async () => {
136
+ const navItemData: NavItemData = {
137
+ main: [{ text: "External", href: "https://example.com", isExternal: true }],
138
+ };
139
+ const wrapper = await mountSuspended(SiteNavigation, {
140
+ props: { navItemData },
141
+ });
142
+ expect(wrapper.find(".site-nav-link").attributes("href")).toBe("https://example.com");
143
+ });
144
+
145
+ // ─── Alignment variants ────────────────────────────────────────────────────
146
+
147
+ it("applies site-navigation--left class by default", async () => {
148
+ const wrapper = await mountSuspended(SiteNavigation, {
149
+ props: { navItemData: defaultNavItemData },
150
+ });
151
+ expect(wrapper.classes()).toContain("site-navigation--left");
152
+ });
153
+
154
+ it("applies site-navigation--center class when navAlign is center", async () => {
155
+ const wrapper = await mountSuspended(SiteNavigation, {
156
+ props: { navItemData: defaultNavItemData, navAlign: "center" },
157
+ });
158
+ expect(wrapper.classes()).toContain("site-navigation--center");
159
+ });
160
+
161
+ it("applies site-navigation--right class when navAlign is right", async () => {
162
+ const wrapper = await mountSuspended(SiteNavigation, {
163
+ props: { navItemData: defaultNavItemData, navAlign: "right" },
164
+ });
165
+ expect(wrapper.classes()).toContain("site-navigation--right");
166
+ });
167
+
168
+ // ─── styleClassPassthrough ─────────────────────────────────────────────────
169
+
170
+ it("applies styleClassPassthrough classes to the nav element", async () => {
171
+ const wrapper = await mountSuspended(SiteNavigation, {
172
+ props: {
173
+ navItemData: defaultNavItemData,
174
+ styleClassPassthrough: ["custom-nav", "theme-dark"],
175
+ },
176
+ });
177
+ expect(wrapper.classes()).toContain("custom-nav");
178
+ expect(wrapper.classes()).toContain("theme-dark");
179
+ });
180
+
181
+ it("updates classes when styleClassPassthrough prop changes", async () => {
182
+ const wrapper = await mountSuspended(SiteNavigation, {
183
+ props: {
184
+ navItemData: defaultNavItemData,
185
+ styleClassPassthrough: ["initial-class"],
186
+ },
187
+ });
188
+ expect(wrapper.classes()).toContain("initial-class");
189
+
190
+ await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
191
+ await nextTick();
192
+
193
+ expect(wrapper.classes()).toContain("updated-class");
194
+ expect(wrapper.classes()).not.toContain("initial-class");
195
+ });
196
+
197
+ // ─── Non-collapsed state (default in JSDOM — dimensions are all 0) ─────────
198
+
199
+ it("does not show the burger button when not collapsed", async () => {
200
+ const wrapper = await mountSuspended(SiteNavigation, {
201
+ props: { navItemData: defaultNavItemData },
202
+ });
203
+ expect(wrapper.find(".site-nav-burger").exists()).toBe(false);
204
+ });
205
+
206
+ it("does not show the nav panel when not collapsed", async () => {
207
+ const wrapper = await mountSuspended(SiteNavigation, {
208
+ props: { navItemData: defaultNavItemData },
209
+ });
210
+ expect(wrapper.find(".site-nav-panel").exists()).toBe(false);
211
+ });
212
+
213
+ it("does not have is-collapsed class when not collapsed", async () => {
214
+ const wrapper = await mountSuspended(SiteNavigation, {
215
+ props: { navItemData: defaultNavItemData },
216
+ });
217
+ expect(wrapper.classes()).not.toContain("is-collapsed");
218
+ });
219
+
220
+ // ─── Collapsed state ───────────────────────────────────────────────────────
221
+ // isCollapsed is driven by DOM layout measurements (scrollWidth > clientWidth).
222
+ // In JSDOM both values are 0 so we cannot trigger collapse via resize.
223
+ // Instead we force the state directly through the component's setup context.
224
+
225
+ describe("when collapsed", () => {
226
+ // setupState auto-unwraps refs — set the plain value directly
227
+ interface SiteNavSetup {
228
+ isCollapsed: boolean;
229
+ isMenuOpen: boolean;
230
+ }
231
+
232
+ async function mountCollapsed() {
233
+ const wrapper = await mountSuspended(SiteNavigation, {
234
+ props: { navItemData: defaultNavItemData },
235
+ });
236
+ const setup = (wrapper.vm as unknown as { $: { setupState: SiteNavSetup } }).$.setupState;
237
+ setup.isCollapsed = true;
238
+ await nextTick();
239
+ return { wrapper, setup };
240
+ }
241
+
242
+ it("applies is-collapsed class", async () => {
243
+ const { wrapper } = await mountCollapsed();
244
+ expect(wrapper.classes()).toContain("is-collapsed");
245
+ });
246
+
247
+ it("hides the horizontal nav list", async () => {
248
+ const { wrapper } = await mountCollapsed();
249
+ expect(wrapper.find(".site-nav-list").exists()).toBe(false);
250
+ });
251
+
252
+ it("shows the burger button", async () => {
253
+ const { wrapper } = await mountCollapsed();
254
+ expect(wrapper.find(".site-nav-burger").exists()).toBe(true);
255
+ });
256
+
257
+ it("shows the nav panel", async () => {
258
+ const { wrapper } = await mountCollapsed();
259
+ expect(wrapper.find(".site-nav-panel").exists()).toBe(true);
260
+ });
261
+
262
+ it("burger button has aria-expanded false when menu is closed", async () => {
263
+ const { wrapper } = await mountCollapsed();
264
+ expect(wrapper.find(".site-nav-burger").attributes("aria-expanded")).toBe("false");
265
+ });
266
+
267
+ it("panel does not have is-open class when menu is closed", async () => {
268
+ const { wrapper } = await mountCollapsed();
269
+ expect(wrapper.find(".site-nav-panel").classes()).not.toContain("is-open");
270
+ });
271
+
272
+ it("renders panel links matching main nav items", async () => {
273
+ const { wrapper } = await mountCollapsed();
274
+ const panelLinks = wrapper.findAll(".site-nav-panel-link");
275
+ expect(panelLinks.length).toBe(3);
276
+ expect(panelLinks[0]!.text()).toContain("Home");
277
+ expect(panelLinks[1]!.text()).toContain("About");
278
+ expect(panelLinks[2]!.text()).toContain("Contact");
279
+ });
280
+
281
+ it("toggles isMenuOpen on burger click", async () => {
282
+ const { wrapper, setup } = await mountCollapsed();
283
+ expect(setup.isMenuOpen).toBe(false);
284
+ await wrapper.find(".site-nav-burger").trigger("click");
285
+ expect(setup.isMenuOpen).toBe(true);
286
+ await wrapper.find(".site-nav-burger").trigger("click");
287
+ expect(setup.isMenuOpen).toBe(false);
288
+ });
289
+
290
+ it("applies menu-open class when menu is open", async () => {
291
+ const { wrapper } = await mountCollapsed();
292
+ await wrapper.find(".site-nav-burger").trigger("click");
293
+ await nextTick();
294
+ expect(wrapper.classes()).toContain("menu-open");
295
+ });
296
+
297
+ it("burger has aria-expanded true when menu is open", async () => {
298
+ const { wrapper } = await mountCollapsed();
299
+ await wrapper.find(".site-nav-burger").trigger("click");
300
+ await nextTick();
301
+ expect(wrapper.find(".site-nav-burger").attributes("aria-expanded")).toBe("true");
302
+ });
303
+
304
+ it("panel has is-open class when menu is open", async () => {
305
+ const { wrapper } = await mountCollapsed();
306
+ await wrapper.find(".site-nav-burger").trigger("click");
307
+ await nextTick();
308
+ expect(wrapper.find(".site-nav-panel").classes()).toContain("is-open");
309
+ });
310
+
311
+ it("panel is inert when menu is closed", async () => {
312
+ const { wrapper } = await mountCollapsed();
313
+ expect(wrapper.find(".site-nav-panel").attributes("inert")).toBeDefined();
314
+ });
315
+
316
+ it("panel is not inert when menu is open", async () => {
317
+ const { wrapper } = await mountCollapsed();
318
+ await wrapper.find(".site-nav-burger").trigger("click");
319
+ await nextTick();
320
+ expect(wrapper.find(".site-nav-panel").attributes("inert")).toBeUndefined();
321
+ });
322
+
323
+ it("panel has aria-controls pointing to site-nav-panel", async () => {
324
+ const { wrapper } = await mountCollapsed();
325
+ expect(wrapper.find(".site-nav-burger").attributes("aria-controls")).toBe("site-nav-panel");
326
+ });
327
+ });
328
+ });
@@ -0,0 +1,30 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`SiteNavigation > renders correct HTML structure 1`] = `
4
+ "<nav class="site-navigation site-navigation--left is-loaded" aria-label="Site navigation">
5
+ <ul class="site-nav-list" style="--_transition-duration: 0ms; --_x-active: 0px; --_width-active: NaN; --_x-hovered: 0px; --_width-hovered: NaN;">
6
+ <li class=""><a href="/" class="site-nav-link" data-nav-item="">
7
+ <!--v-if--> Home
8
+ </a></li>
9
+ <li class=""><a href="/about" class="site-nav-link" data-nav-item="">
10
+ <!--v-if--> About
11
+ </a></li>
12
+ <li class=""><a href="/contact" class="site-nav-link" data-nav-item="">
13
+ <!--v-if--> Contact
14
+ </a></li>
15
+ <li class="nav-indicator-li" aria-hidden="true" role="none">
16
+ <div class="nav__active-indicator"></div>
17
+ </li>
18
+ <li class="nav-indicator-li" aria-hidden="true" role="none">
19
+ <div class="nav__active"></div>
20
+ </li>
21
+ <li class="nav-indicator-li" aria-hidden="true" role="none">
22
+ <div class="nav__hovered"></div>
23
+ </li>
24
+ </ul>
25
+ <!--v-if-->
26
+ <!--teleport start-->
27
+ <!--teleport end-->
28
+ <!--v-if-->
29
+ </nav>"
30
+ `;