srcdev-nuxt-components 9.5.7 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.claude/skills/components/breadcrumb.md +5 -0
  2. package/.claude/skills/components/display-pill.md +2 -0
  3. package/.claude/skills/components/input-button.md +3 -0
  4. package/.claude/skills/components/responsive-header.md +49 -0
  5. package/.claude/skills/components/service-detail.md +32 -0
  6. package/.claude/skills/components/service-summary.md +8 -0
  7. package/.claude/skills/components/services-card-grid.md +5 -3
  8. package/.claude/skills/components/services-card.md +22 -12
  9. package/.claude/skills/components/slider-gallery.md +10 -4
  10. package/.claude/skills/components/tab-navigation.md +25 -9
  11. package/.claude/skills/index.md +1 -2
  12. package/.vscode/srcdev-component-responsive-header.code-snippets +10 -0
  13. package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
  14. package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
  15. package/.vscode/srcdev-component-services-card.code-snippets +38 -36
  16. package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
  17. package/.vscode/srcdev-component-tab-navigation.code-snippets +1 -1
  18. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +3 -0
  19. package/app/components/01.atoms/display-pill/DisplayPill.vue +5 -2
  20. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
  21. package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
  22. package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
  23. package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
  24. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +52 -11
  25. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +55 -23
  26. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +176 -257
  27. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +42 -2
  28. package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
  29. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
  30. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
  31. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
  32. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
  33. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +4 -0
  34. package/app/components/03.organisms/responsive-header/NavigationItems.vue +33 -4
  35. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +51 -4
  36. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
  37. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
  38. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
  39. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
  40. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
  41. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
  42. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
  43. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -3
  44. package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
  45. package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
  46. package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
  47. package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
  48. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
  49. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
  50. package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
  51. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
  52. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
  53. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
  54. package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
  55. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
  56. package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
  57. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
  58. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
  59. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
  60. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
  61. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
  62. package/app/components/05.forms/input-button/InputButton.vue +3 -1
  63. package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
  64. package/app/composables/useAnchorScroll.ts +1 -1
  65. package/nuxt.config.ts +1 -1
  66. package/package.json +1 -1
  67. package/.claude/skills/components/site-navigation.md +0 -145
  68. package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
  69. package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
  70. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
  71. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
  72. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
  73. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
@@ -1,367 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { mountSuspended } from "@nuxt/test-utils/runtime";
3
- import { defineComponent, nextTick } from "vue";
4
- import SiteNavigation from "../SiteNavigation.vue";
5
- import type { NavItemData } from "~/types/components/nav-item.d";
6
-
7
- // useResizeObserver (from @vueuse/core) requires ResizeObserver
8
- // Vitest 4: a mock's implementation must be a regular function (not an
9
- // arrow function) to remain constructible via `new`.
10
- const mockResizeObserver = vi.fn(function () {
11
- return {
12
- observe: vi.fn(),
13
- unobserve: vi.fn(),
14
- disconnect: vi.fn(),
15
- };
16
- });
17
-
18
- beforeEach(() => {
19
- vi.stubGlobal("ResizeObserver", mockResizeObserver);
20
- // ⚠️ Do NOT call vi.unstubAllGlobals() in afterEach —
21
- // it removes global stubs from vitest.setup.ts ($fetch, etc.)
22
- });
23
-
24
- const defaultNavItemData: NavItemData = {
25
- main: [
26
- { text: "Home", href: "/" },
27
- { text: "About", href: "/about" },
28
- { text: "Contact", href: "/contact" },
29
- ],
30
- };
31
-
32
- describe("SiteNavigation", () => {
33
- it("mounts without error", async () => {
34
- const wrapper = await mountSuspended(SiteNavigation, {
35
- props: { navItemData: defaultNavItemData },
36
- });
37
- expect(wrapper.vm).toBeTruthy();
38
- });
39
-
40
- it("renders a nav element as root", async () => {
41
- const wrapper = await mountSuspended(SiteNavigation, {
42
- props: { navItemData: defaultNavItemData },
43
- });
44
- expect(wrapper.element.tagName.toLowerCase()).toBe("nav");
45
- });
46
-
47
- it("has aria-label on the nav", async () => {
48
- const wrapper = await mountSuspended(SiteNavigation, {
49
- props: { navItemData: defaultNavItemData },
50
- });
51
- expect(wrapper.attributes("aria-label")).toBe("Site navigation");
52
- });
53
-
54
- it("renders correct HTML structure", async () => {
55
- const wrapper = await mountSuspended(SiteNavigation, {
56
- props: { navItemData: defaultNavItemData },
57
- });
58
- expect(wrapper.html()).toMatchSnapshot();
59
- });
60
-
61
- // ─── isLoaded ──────────────────────────────────────────────────────────────
62
-
63
- it("applies is-loaded class after mounting", async () => {
64
- const wrapper = await mountSuspended(SiteNavigation, {
65
- props: { navItemData: defaultNavItemData },
66
- });
67
- await nextTick();
68
- expect(wrapper.classes()).toContain("is-loaded");
69
- });
70
-
71
- // ─── Nav list ──────────────────────────────────────────────────────────────
72
-
73
- it("renders the nav list when not collapsed", async () => {
74
- const wrapper = await mountSuspended(SiteNavigation, {
75
- props: { navItemData: defaultNavItemData },
76
- });
77
- expect(wrapper.find(".site-nav-list").exists()).toBe(true);
78
- });
79
-
80
- it("renders the correct number of nav items", async () => {
81
- const wrapper = await mountSuspended(SiteNavigation, {
82
- props: { navItemData: defaultNavItemData },
83
- });
84
- // Filter out .nav-indicator-li elements injected by initNavDecorators
85
- const items = wrapper.findAll(".site-nav-list li:not(.nav-indicator-li)");
86
- expect(items.length).toBe(3);
87
- });
88
-
89
- it("renders item text correctly", async () => {
90
- const wrapper = await mountSuspended(SiteNavigation, {
91
- props: { navItemData: defaultNavItemData },
92
- });
93
- const links = wrapper.findAll(".site-nav-link");
94
- expect(links[0]!.text()).toContain("Home");
95
- expect(links[1]!.text()).toContain("About");
96
- expect(links[2]!.text()).toContain("Contact");
97
- });
98
-
99
- it("renders item href correctly", async () => {
100
- const wrapper = await mountSuspended(SiteNavigation, {
101
- props: { navItemData: defaultNavItemData },
102
- });
103
- const links = wrapper.findAll(".site-nav-link");
104
- expect(links[0]!.attributes("href")).toBe("/");
105
- expect(links[1]!.attributes("href")).toBe("/about");
106
- expect(links[2]!.attributes("href")).toBe("/contact");
107
- });
108
-
109
- it("applies cssName as class on the li element", async () => {
110
- const navItemData: NavItemData = {
111
- main: [{ text: "Home", href: "/", cssName: "is-active" }],
112
- };
113
- const wrapper = await mountSuspended(SiteNavigation, {
114
- props: { navItemData },
115
- });
116
- expect(wrapper.find(".site-nav-list li").classes()).toContain("is-active");
117
- });
118
-
119
- it("renders an icon when iconName is provided", async () => {
120
- const navItemData: NavItemData = {
121
- main: [{ text: "Home", href: "/", iconName: "heroicons:home" }],
122
- };
123
- const wrapper = await mountSuspended(SiteNavigation, {
124
- props: { navItemData },
125
- });
126
- // Icon renders with aria-hidden="true"
127
- const firstItem = wrapper.find(".site-nav-list li:not(.nav-indicator-li)");
128
- expect(firstItem.find("[aria-hidden='true']").exists()).toBe(true);
129
- });
130
-
131
- it("does not render an icon when iconName is not provided", async () => {
132
- const wrapper = await mountSuspended(SiteNavigation, {
133
- props: { navItemData: defaultNavItemData },
134
- });
135
- const firstLink = wrapper.find(".site-nav-link");
136
- expect(firstLink.find("[aria-hidden='true']").exists()).toBe(false);
137
- });
138
-
139
- it("renders external links with the correct href", async () => {
140
- const navItemData: NavItemData = {
141
- main: [{ text: "External", href: "https://example.com", isExternal: true }],
142
- };
143
- const wrapper = await mountSuspended(SiteNavigation, {
144
- props: { navItemData },
145
- });
146
- expect(wrapper.find(".site-nav-link").attributes("href")).toBe("https://example.com");
147
- });
148
-
149
- // ─── Alignment variants ────────────────────────────────────────────────────
150
-
151
- it("applies site-navigation--left class by default", async () => {
152
- const wrapper = await mountSuspended(SiteNavigation, {
153
- props: { navItemData: defaultNavItemData },
154
- });
155
- expect(wrapper.classes()).toContain("site-navigation--left");
156
- });
157
-
158
- it("applies site-navigation--center class when navAlign is center", async () => {
159
- const wrapper = await mountSuspended(SiteNavigation, {
160
- props: { navItemData: defaultNavItemData, navAlign: "center" },
161
- });
162
- expect(wrapper.classes()).toContain("site-navigation--center");
163
- });
164
-
165
- it("applies site-navigation--right class when navAlign is right", async () => {
166
- const wrapper = await mountSuspended(SiteNavigation, {
167
- props: { navItemData: defaultNavItemData, navAlign: "right" },
168
- });
169
- expect(wrapper.classes()).toContain("site-navigation--right");
170
- });
171
-
172
- // ─── styleClassPassthrough ─────────────────────────────────────────────────
173
-
174
- it("applies styleClassPassthrough classes to the nav element", async () => {
175
- const wrapper = await mountSuspended(SiteNavigation, {
176
- props: {
177
- navItemData: defaultNavItemData,
178
- styleClassPassthrough: ["custom-nav", "theme-dark"],
179
- },
180
- });
181
- expect(wrapper.classes()).toContain("custom-nav");
182
- expect(wrapper.classes()).toContain("theme-dark");
183
- });
184
-
185
- it("updates classes when styleClassPassthrough prop changes", async () => {
186
- const wrapper = await mountSuspended(SiteNavigation, {
187
- props: {
188
- navItemData: defaultNavItemData,
189
- styleClassPassthrough: ["initial-class"],
190
- },
191
- });
192
- expect(wrapper.classes()).toContain("initial-class");
193
-
194
- await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
195
- await nextTick();
196
-
197
- expect(wrapper.classes()).toContain("updated-class");
198
- expect(wrapper.classes()).not.toContain("initial-class");
199
- });
200
-
201
- // ─── Non-collapsed state (default in JSDOM — dimensions are all 0) ─────────
202
-
203
- it("does not show the burger button when not collapsed", async () => {
204
- const wrapper = await mountSuspended(SiteNavigation, {
205
- props: { navItemData: defaultNavItemData },
206
- });
207
- expect(wrapper.find(".site-nav-burger").exists()).toBe(false);
208
- });
209
-
210
- it("does not show the nav panel when not collapsed", async () => {
211
- const wrapper = await mountSuspended(SiteNavigation, {
212
- props: { navItemData: defaultNavItemData },
213
- });
214
- expect(wrapper.find(".site-nav-panel").exists()).toBe(false);
215
- });
216
-
217
- it("does not have is-collapsed class when not collapsed", async () => {
218
- const wrapper = await mountSuspended(SiteNavigation, {
219
- props: { navItemData: defaultNavItemData },
220
- });
221
- expect(wrapper.classes()).not.toContain("is-collapsed");
222
- });
223
-
224
- // ─── Collapsed state ───────────────────────────────────────────────────────
225
- // isCollapsed is driven by DOM layout measurements (scrollWidth > clientWidth).
226
- // In JSDOM both values are 0 so we cannot trigger collapse via resize.
227
- // Instead we force the state directly through the component's setup context.
228
-
229
- describe("when collapsed", () => {
230
- // setupState auto-unwraps refs — set the plain value directly
231
- interface SiteNavSetup {
232
- isCollapsed: boolean;
233
- isMenuOpen: boolean;
234
- }
235
-
236
- async function mountCollapsed(extraProps: Record<string, unknown> = {}) {
237
- const wrapper = await mountSuspended(SiteNavigation, {
238
- props: { navItemData: defaultNavItemData, ...extraProps },
239
- });
240
- const setup = (wrapper.vm as unknown as { $: { setupState: SiteNavSetup } }).$.setupState;
241
- setup.isCollapsed = true;
242
- await nextTick();
243
- return { wrapper, setup };
244
- }
245
-
246
- it("applies is-collapsed class", async () => {
247
- const { wrapper } = await mountCollapsed();
248
- expect(wrapper.classes()).toContain("is-collapsed");
249
- });
250
-
251
- it("hides the horizontal nav list", async () => {
252
- const { wrapper } = await mountCollapsed();
253
- expect(wrapper.find(".site-nav-list").exists()).toBe(false);
254
- });
255
-
256
- it("shows the burger button", async () => {
257
- const { wrapper } = await mountCollapsed();
258
- expect(wrapper.find(".site-nav-burger").exists()).toBe(true);
259
- });
260
-
261
- it("shows the nav panel", async () => {
262
- const { wrapper } = await mountCollapsed();
263
- expect(wrapper.find(".site-nav-panel").exists()).toBe(true);
264
- });
265
-
266
- it("burger button has aria-expanded false when menu is closed", async () => {
267
- const { wrapper } = await mountCollapsed();
268
- expect(wrapper.find(".site-nav-burger").attributes("aria-expanded")).toBe("false");
269
- });
270
-
271
- it("panel does not have is-open class when menu is closed", async () => {
272
- const { wrapper } = await mountCollapsed();
273
- expect(wrapper.find(".site-nav-panel").classes()).not.toContain("is-open");
274
- });
275
-
276
- it("renders panel links matching main nav items", async () => {
277
- const { wrapper } = await mountCollapsed();
278
- const panelLinks = wrapper.findAll(".site-nav-panel-link");
279
- expect(panelLinks.length).toBe(3);
280
- expect(panelLinks[0]!.text()).toContain("Home");
281
- expect(panelLinks[1]!.text()).toContain("About");
282
- expect(panelLinks[2]!.text()).toContain("Contact");
283
- });
284
-
285
- it("toggles isMenuOpen on burger click", async () => {
286
- const { wrapper, setup } = await mountCollapsed();
287
- expect(setup.isMenuOpen).toBe(false);
288
- await wrapper.find(".site-nav-burger").trigger("click");
289
- expect(setup.isMenuOpen).toBe(true);
290
- await wrapper.find(".site-nav-burger").trigger("click");
291
- expect(setup.isMenuOpen).toBe(false);
292
- });
293
-
294
- it("applies menu-open class when menu is open", async () => {
295
- const { wrapper } = await mountCollapsed();
296
- await wrapper.find(".site-nav-burger").trigger("click");
297
- await nextTick();
298
- expect(wrapper.classes()).toContain("menu-open");
299
- });
300
-
301
- it("burger has aria-expanded true when menu is open", async () => {
302
- const { wrapper } = await mountCollapsed();
303
- await wrapper.find(".site-nav-burger").trigger("click");
304
- await nextTick();
305
- expect(wrapper.find(".site-nav-burger").attributes("aria-expanded")).toBe("true");
306
- });
307
-
308
- it("panel has is-open class when menu is open", async () => {
309
- const { wrapper } = await mountCollapsed();
310
- await wrapper.find(".site-nav-burger").trigger("click");
311
- await nextTick();
312
- expect(wrapper.find(".site-nav-panel").classes()).toContain("is-open");
313
- });
314
-
315
- it("panel is inert when menu is closed", async () => {
316
- const { wrapper } = await mountCollapsed();
317
- expect(wrapper.find(".site-nav-panel").attributes("inert")).toBeDefined();
318
- });
319
-
320
- it("panel is not inert when menu is open", async () => {
321
- const { wrapper } = await mountCollapsed();
322
- await wrapper.find(".site-nav-burger").trigger("click");
323
- await nextTick();
324
- expect(wrapper.find(".site-nav-panel").attributes("inert")).toBeUndefined();
325
- });
326
-
327
- it("burger aria-controls points at the panel's own id", async () => {
328
- const { wrapper } = await mountCollapsed();
329
- const panelId = wrapper.find(".site-nav-panel").attributes("id");
330
- expect(panelId).toMatch(/^site-nav-panel-/);
331
- expect(wrapper.find(".site-nav-burger").attributes("aria-controls")).toBe(panelId);
332
- });
333
-
334
- it("gives two instances on the same page distinct panel ids", async () => {
335
- const Host = defineComponent({
336
- components: { SiteNavigation },
337
- setup: () => ({ navItemData: defaultNavItemData }),
338
- template: `<div><SiteNavigation :nav-item-data="navItemData" /><SiteNavigation :nav-item-data="navItemData" /></div>`,
339
- });
340
- const host = await mountSuspended(Host);
341
- const navs = host.findAllComponents(SiteNavigation);
342
- for (const nav of navs) {
343
- (nav.vm as unknown as { $: { setupState: SiteNavSetup } }).$.setupState.isCollapsed = true;
344
- }
345
- await nextTick();
346
- const ids = host.findAll(".site-nav-panel").map((p) => p.attributes("id"));
347
- expect(ids).toHaveLength(2);
348
- expect(ids[0]).not.toBe(ids[1]);
349
- });
350
-
351
- it("uses the default burger labels", async () => {
352
- const { wrapper } = await mountCollapsed();
353
- expect(wrapper.find(".site-nav-burger").text()).toContain("Open navigation menu");
354
- await wrapper.find(".site-nav-burger").trigger("click");
355
- await nextTick();
356
- expect(wrapper.find(".site-nav-burger").text()).toContain("Close navigation menu");
357
- });
358
-
359
- it("uses openMenuLabel and closeMenuLabel when given", async () => {
360
- const { wrapper } = await mountCollapsed({ openMenuLabel: "Ouvrir le menu", closeMenuLabel: "Fermer le menu" });
361
- expect(wrapper.find(".site-nav-burger").text()).toContain("Ouvrir le menu");
362
- await wrapper.find(".site-nav-burger").trigger("click");
363
- await nextTick();
364
- expect(wrapper.find(".site-nav-burger").text()).toContain("Fermer le menu");
365
- });
366
- });
367
- });
@@ -1,30 +0,0 @@
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="is-active"><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 aria-hidden="true" role="none" class="nav-indicator-li">
16
- <div class="nav__hovered"></div>
17
- </li>
18
- <li aria-hidden="true" role="none" class="nav-indicator-li">
19
- <div class="nav__active"></div>
20
- </li>
21
- <li aria-hidden="true" role="none" class="nav-indicator-li">
22
- <div class="nav__active-indicator"></div>
23
- </li>
24
- </ul>
25
- <!--v-if-->
26
- <!--teleport start-->
27
- <!--teleport end-->
28
- <!--v-if-->
29
- </nav>"
30
- `;