srcdev-nuxt-components 9.0.15 → 9.0.17

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 (112) hide show
  1. package/.claude/settings.json +25 -0
  2. package/.claude/skills/component-aria-landmark.md +68 -0
  3. package/.claude/skills/component-dynamic-slots.md +150 -0
  4. package/.claude/skills/component-export-types.md +61 -0
  5. package/.claude/skills/component-local-style-override.md +126 -0
  6. package/.claude/skills/component-prop-driven-container-layout.md +42 -0
  7. package/.claude/skills/components/accordian-core.md +159 -0
  8. package/.claude/skills/components/contact-section.md +101 -0
  9. package/.claude/skills/components/expanding-panel.md +156 -0
  10. package/.claude/skills/components/eyebrow-text.md +25 -0
  11. package/.claude/skills/components/hero-text.md +25 -0
  12. package/.claude/skills/components/layout-grid-by-cols.md +147 -0
  13. package/.claude/skills/components/layout-row.md +35 -0
  14. package/.claude/skills/components/link-text.md +33 -0
  15. package/.claude/skills/components/page-hero-highlights.md +224 -0
  16. package/.claude/skills/components/services-card.md +28 -0
  17. package/.claude/skills/components/services-section.md +25 -0
  18. package/.claude/skills/components/stepper-list.md +227 -0
  19. package/.claude/skills/css-grid-max-width-gutters.md +67 -0
  20. package/.claude/skills/index.md +15 -3
  21. package/.claude/skills/storybook-add-story.md +60 -0
  22. package/.claude/skills/testing-add-unit-test.md +56 -0
  23. package/app/assets/styles/setup/01.config/index.css +0 -1
  24. package/app/assets/styles/setup/03.theming/default/_dark.css +2 -2
  25. package/app/assets/styles/setup/04.elements/forms/02.typography.css +1 -0
  26. package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-link.css +14 -14
  27. package/app/assets/styles/setup/index.css +0 -1
  28. package/app/components/01.atoms/card/CardCore.vue +92 -0
  29. package/app/components/01.atoms/card/stories/CardCore.stories.ts +132 -0
  30. package/app/components/01.atoms/card/tests/CardCore.spec.ts +207 -0
  31. package/app/components/01.atoms/card/tests/__snapshots__/CardCore.spec.ts.snap +43 -0
  32. package/app/components/01.atoms/content-wrappers/content-columns-2/ContentColumns2.vue +51 -0
  33. package/app/components/01.atoms/content-wrappers/content-columns-2/stories/ContentColumns2.stories.ts +110 -0
  34. package/app/components/01.atoms/content-wrappers/content-columns-2/tests/ContentColumns2.spec.ts +105 -0
  35. package/app/components/01.atoms/content-wrappers/content-columns-2/tests/__snapshots__/ContentColumns2.spec.ts.snap +14 -0
  36. package/app/components/01.atoms/content-wrappers/content-width/ContentWidth.vue +88 -0
  37. package/app/components/01.atoms/content-wrappers/content-width/stories/ContentWidth.stories.ts +362 -0
  38. package/app/components/01.atoms/content-wrappers/content-width/tests/ContentWidth.spec.ts +132 -0
  39. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +71 -0
  40. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +219 -0
  41. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +174 -0
  42. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
  43. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
  44. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/LayoutGridByWidth.vue +70 -0
  45. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/stories/LayoutGridByWidth.stories.ts +220 -0
  46. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/LayoutGridByWidth.spec.ts +174 -0
  47. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
  48. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
  49. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByWidth.spec.ts.snap +36 -0
  50. package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +1 -1
  51. package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +1 -1
  52. package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +1 -1
  53. package/app/components/02.molecules/contact-section/stories/ContactSection.stories.ts +5 -0
  54. package/app/components/02.molecules/contact-section/tests/ContactSection.spec.ts +15 -0
  55. package/app/components/02.molecules/contact-section/tests/ContactSection.vue +25 -17
  56. package/app/components/{accordian → 02.molecules/expandable/accordian}/stories/AccordianCore.stories.ts +1 -1
  57. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +245 -0
  58. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +351 -0
  59. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +38 -0
  60. package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontal.vue +162 -0
  61. package/app/components/02.molecules/navigation/navigation-horizontal/stories/NavigationHorizontal.stories.ts +373 -0
  62. package/app/components/02.molecules/navigation/navigation-horizontal/tests/NavigationHorizontal.spec.ts +152 -0
  63. package/app/components/02.molecules/navigation/navigation-horizontal/tests/__snapshots__/NavigationHorizontal.spec.ts.snap +17 -0
  64. package/app/components/02.molecules/profile-section/ProfileSection.vue +2 -3
  65. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +2 -2
  66. package/app/components/02.molecules/stepper-list/StepperList.vue +131 -92
  67. package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +31 -0
  68. package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +24 -0
  69. package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +22 -9
  70. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +782 -0
  71. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +233 -0
  72. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +226 -0
  73. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +69 -0
  74. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +1 -1
  75. package/app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue +1 -1
  76. package/app/components/03.organisms/services/services-section/ServicesSection.vue +2 -3
  77. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +239 -0
  78. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +404 -0
  79. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +198 -0
  80. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +19 -0
  81. package/app/components/container-glow/ContainerGlowCore.vue +20 -27
  82. package/app/components/forms/input-button/InputButtonCore.vue +105 -104
  83. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +21 -21
  84. package/app/composables/useAriaLabelledById.ts +13 -0
  85. package/app/layouts/default.vue +8 -3
  86. package/app/pages/forms/examples/buttons/index.vue +6 -6
  87. package/app/pages/forms/examples/material/checkbox-radio-panels.vue +3 -3
  88. package/app/pages/forms/examples/material/text-fields.vue +607 -610
  89. package/app/pages/page-hero-highlights.vue +81 -0
  90. package/app/pages/ui/{display-card.vue → card-core.vue} +15 -15
  91. package/app/pages/ui/contact-section.vue +1 -1
  92. package/app/pages/ui/container-glow.vue +1 -1
  93. package/app/pages/ui/content-width.vue +126 -0
  94. package/app/pages/ui/glowing-border.vue +9 -9
  95. package/app/pages/ui/navigation/navigation-horizontal.vue +484 -0
  96. package/app/pages/ui/services/services-section/[slug].vue +3 -1
  97. package/app/types/components/index.ts +1 -0
  98. package/app/types/components/navigation-horizontal.d.ts +11 -0
  99. package/package.json +2 -2
  100. package/app/assets/styles/setup/01.config/_basic-resets.css +0 -9
  101. package/app/components/content-columns/TwoColumns.vue +0 -59
  102. package/app/components/content-columns/stories/TwoColumns.stories.ts +0 -561
  103. package/app/components/content-containers/ContentContainer.vue +0 -89
  104. package/app/components/content-containers/stories/ContentContainer.stories.ts +0 -465
  105. package/app/components/content-grid/ContentGrid.vue +0 -85
  106. package/app/components/display-card/DisplayCard.vue +0 -122
  107. package/app/components/image-galleries/SliderGallery.vue +0 -786
  108. package/app/pages/ui/content-container.vue +0 -112
  109. /package/app/components/{accordian → 02.molecules/expandable/accordian}/AccordianCore.vue +0 -0
  110. /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/AccordianCore.spec.ts +0 -0
  111. /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -0
  112. /package/app/components/{expanding-panel → 02.molecules/expandable/expanding-panel}/ExpandingPanel.vue +0 -0
@@ -0,0 +1,233 @@
1
+ import SliderGallery from "../SliderGallery.vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+ import type { IGalleryData } from "~/types/components";
4
+
5
+ const meta: Meta<typeof SliderGallery> = {
6
+ title: "Organisms/Image Galleries/Slider Gallery",
7
+ component: SliderGallery,
8
+ argTypes: {
9
+ autoRun: {
10
+ control: { type: "boolean" },
11
+ description: "Automatically advance to the next slide",
12
+ },
13
+ autoRunInterval: {
14
+ control: { type: "number", min: 1000, step: 500 },
15
+ description: "Time between auto-advances in milliseconds",
16
+ },
17
+ animationDuration: {
18
+ control: { type: "number", min: 100, step: 100 },
19
+ description: "Slide transition duration in milliseconds",
20
+ },
21
+ styleClassPassthrough: {
22
+ control: "object",
23
+ description: "Additional CSS classes applied to the root element",
24
+ },
25
+ },
26
+ args: {
27
+ autoRun: true,
28
+ autoRunInterval: 7000,
29
+ animationDuration: 3000,
30
+ styleClassPassthrough: [],
31
+ },
32
+ parameters: {
33
+ layout: "fullscreen",
34
+ docs: {
35
+ description: {
36
+ component:
37
+ "A full-screen image slider gallery with animated slide transitions, thumbnail navigation, arrow controls, and keyboard support. Gallery data is passed via v-model:galleryData.",
38
+ },
39
+ },
40
+ },
41
+ };
42
+
43
+ export default meta;
44
+ type Story = StoryObj<typeof SliderGallery>;
45
+
46
+ // ─── Fixtures ─────────────────────────────────────────────────────────────────
47
+
48
+ const sampleSlides: IGalleryData[] = [
49
+ {
50
+ src: "https://picsum.photos/seed/gallery1/1920/1080",
51
+ alt: "Mountain landscape at sunrise",
52
+ stylist: "NATURE PHOTOGRAPHY",
53
+ title: "Into the Wild",
54
+ category: "Landscape",
55
+ description: "Vast mountain ranges stretching to the horizon at the golden hour of sunrise.",
56
+ thumbnail: { title: "Into the Wild", description: "Landscape" },
57
+ textBrightness: "light",
58
+ },
59
+ {
60
+ src: "https://picsum.photos/seed/gallery2/1920/1080",
61
+ alt: "Urban architecture at dusk",
62
+ stylist: "URBAN SERIES",
63
+ title: "City Lights",
64
+ category: "Architecture",
65
+ description: "Modern skyscrapers reflecting the warm hues of the setting sun.",
66
+ thumbnail: { title: "City Lights", description: "Architecture" },
67
+ textBrightness: "light",
68
+ },
69
+ {
70
+ src: "https://picsum.photos/seed/gallery3/1920/1080",
71
+ alt: "Ocean waves on a sandy beach",
72
+ stylist: "COASTAL COLLECTION",
73
+ title: "Shoreline",
74
+ category: "Seascape",
75
+ description: "Gentle waves rolling over golden sand as the tide comes in.",
76
+ thumbnail: { title: "Shoreline", description: "Seascape" },
77
+ textBrightness: "light",
78
+ },
79
+ {
80
+ src: "https://picsum.photos/seed/gallery4/1920/1080",
81
+ alt: "Dense forest path in autumn",
82
+ stylist: "FOREST SERIES",
83
+ title: "Through the Trees",
84
+ category: "Nature",
85
+ description: "A winding path through autumn foliage in a dense woodland.",
86
+ thumbnail: { title: "Through the Trees", description: "Nature" },
87
+ textBrightness: "dark",
88
+ },
89
+ {
90
+ src: "https://picsum.photos/seed/gallery5/1920/1080",
91
+ alt: "Snow-capped peaks in winter",
92
+ stylist: "WINTER COLLECTION",
93
+ title: "Frozen Peaks",
94
+ category: "Winter",
95
+ description: "Remote snow-capped summits under a crystal-clear winter sky.",
96
+ thumbnail: { title: "Frozen Peaks", description: "Winter" },
97
+ textBrightness: "light",
98
+ },
99
+ ];
100
+
101
+ // ─── Stories ──────────────────────────────────────────────────────────────────
102
+
103
+ export const Default: Story = {
104
+ render: (args) => ({
105
+ components: { SliderGallery },
106
+ setup() {
107
+ const galleryData = ref<IGalleryData[]>(sampleSlides);
108
+ return { args, galleryData };
109
+ },
110
+ template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
111
+ }),
112
+ };
113
+
114
+ export const AutoRunDisabled: Story = {
115
+ name: "Auto-Run Disabled",
116
+ args: {
117
+ autoRun: false,
118
+ },
119
+ render: (args) => ({
120
+ components: { SliderGallery },
121
+ setup() {
122
+ const galleryData = ref<IGalleryData[]>(sampleSlides);
123
+ return { args, galleryData };
124
+ },
125
+ template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
126
+ }),
127
+ parameters: {
128
+ docs: {
129
+ description: {
130
+ story: "Slides only advance when the user clicks the arrow buttons or uses arrow keys.",
131
+ },
132
+ },
133
+ },
134
+ };
135
+
136
+ export const FastTransition: Story = {
137
+ name: "Fast Transition (500ms)",
138
+ args: {
139
+ animationDuration: 500,
140
+ autoRunInterval: 3000,
141
+ },
142
+ render: (args) => ({
143
+ components: { SliderGallery },
144
+ setup() {
145
+ const galleryData = ref<IGalleryData[]>(sampleSlides);
146
+ return { args, galleryData };
147
+ },
148
+ template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
149
+ }),
150
+ };
151
+
152
+ export const SlowTransition: Story = {
153
+ name: "Slow Transition (5000ms)",
154
+ args: {
155
+ animationDuration: 5000,
156
+ autoRunInterval: 10000,
157
+ },
158
+ render: (args) => ({
159
+ components: { SliderGallery },
160
+ setup() {
161
+ const galleryData = ref<IGalleryData[]>(sampleSlides);
162
+ return { args, galleryData };
163
+ },
164
+ template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
165
+ }),
166
+ };
167
+
168
+ export const SingleSlide: Story = {
169
+ name: "Single Slide",
170
+ args: {
171
+ autoRun: false,
172
+ },
173
+ render: (args) => ({
174
+ components: { SliderGallery },
175
+ setup() {
176
+ const galleryData = ref<IGalleryData[]>([sampleSlides[0]!]);
177
+ return { args, galleryData };
178
+ },
179
+ template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
180
+ }),
181
+ parameters: {
182
+ docs: {
183
+ description: {
184
+ story: "Gallery with only one image — navigation buttons are still rendered.",
185
+ },
186
+ },
187
+ },
188
+ };
189
+
190
+ export const MinimalSlideData: Story = {
191
+ name: "Minimal Slide Data",
192
+ args: {
193
+ autoRun: false,
194
+ },
195
+ render: (args) => ({
196
+ components: { SliderGallery },
197
+ setup() {
198
+ const galleryData = ref<IGalleryData[]>([
199
+ { src: "https://picsum.photos/seed/min1/1920/1080", alt: "Image one", textBrightness: "light" },
200
+ { src: "https://picsum.photos/seed/min2/1920/1080", alt: "Image two", textBrightness: "dark" },
201
+ { src: "https://picsum.photos/seed/min3/1920/1080", alt: "Image three", textBrightness: "light" },
202
+ ]);
203
+ return { args, galleryData };
204
+ },
205
+ template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
206
+ }),
207
+ parameters: {
208
+ docs: {
209
+ description: {
210
+ story: "Only src, alt, and textBrightness are required — all content overlay fields are optional.",
211
+ },
212
+ },
213
+ },
214
+ };
215
+
216
+ export const EmptyGallery: Story = {
217
+ name: "Empty Gallery",
218
+ render: (args) => ({
219
+ components: { SliderGallery },
220
+ setup() {
221
+ const galleryData = ref<IGalleryData[]>([]);
222
+ return { args, galleryData };
223
+ },
224
+ template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
225
+ }),
226
+ parameters: {
227
+ docs: {
228
+ description: {
229
+ story: "When galleryData is empty the loading state is dismissed immediately.",
230
+ },
231
+ },
232
+ },
233
+ };
@@ -0,0 +1,226 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import { nextTick } from "vue";
4
+ import SliderGallery from "../SliderGallery.vue";
5
+ import type { IGalleryData } from "~/types/components";
6
+
7
+ // ─── Fixtures ─────────────────────────────────────────────────────────────────
8
+
9
+ const mockGalleryData: IGalleryData[] = [
10
+ {
11
+ src: "/images/slide-1.jpg",
12
+ alt: "Slide 1",
13
+ stylist: "Stylist One",
14
+ title: "First Slide",
15
+ category: "Fashion",
16
+ description: "Description for first slide",
17
+ thumbnail: { title: "Thumb 1", description: "Thumb desc 1" },
18
+ textBrightness: "light",
19
+ },
20
+ {
21
+ src: "/images/slide-2.jpg",
22
+ alt: "Slide 2",
23
+ title: "Second Slide",
24
+ category: "Style",
25
+ thumbnail: { title: "Thumb 2", description: "Thumb desc 2" },
26
+ textBrightness: "dark",
27
+ },
28
+ {
29
+ src: "/images/slide-3.jpg",
30
+ alt: "Slide 3",
31
+ textBrightness: "light",
32
+ },
33
+ ];
34
+
35
+ // ─── Types ────────────────────────────────────────────────────────────────────
36
+
37
+ type MockImageInstance = {
38
+ src: string;
39
+ onload: (() => void) | null;
40
+ onerror: (() => void) | null;
41
+ };
42
+
43
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
44
+
45
+ /** Mount the gallery and simulate the first image loading successfully. */
46
+ async function mountAndLoad(galleryData: IGalleryData[], mockImage: MockImageInstance) {
47
+ const wrapper = await mountSuspended(SliderGallery, {
48
+ props: { galleryData },
49
+ });
50
+ mockImage.onload?.();
51
+ await nextTick(); // let onMounted resume after Promise.race resolves
52
+ vi.advanceTimersByTime(500); // fire the 500ms loading fade-out timeout
53
+ await nextTick(); // let Vue update the DOM
54
+ return wrapper;
55
+ }
56
+
57
+ // ─── Tests ────────────────────────────────────────────────────────────────────
58
+
59
+ describe("SliderGallery", () => {
60
+ let mockImage: MockImageInstance;
61
+
62
+ beforeEach(() => {
63
+ mockImage = { src: "", onload: null, onerror: null };
64
+ vi.stubGlobal(
65
+ "Image",
66
+ vi.fn(() => mockImage)
67
+ );
68
+ });
69
+
70
+ // ─── Mount ───────────────────────────────────────────────────────────────
71
+
72
+ it("mounts without error", async () => {
73
+ const wrapper = await mountSuspended(SliderGallery);
74
+ expect(wrapper.vm).toBeTruthy();
75
+ });
76
+
77
+ it("renders correct HTML structure after load", async () => {
78
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
79
+ expect(wrapper.html()).toMatchSnapshot();
80
+ });
81
+
82
+ // ─── Root element ─────────────────────────────────────────────────────────
83
+
84
+ it("has slider-gallery class on root", async () => {
85
+ const wrapper = await mountSuspended(SliderGallery);
86
+ expect(wrapper.classes()).toContain("slider-gallery");
87
+ });
88
+
89
+ // ─── Loading state ────────────────────────────────────────────────────────
90
+
91
+ it("shows loading state before images load", async () => {
92
+ const wrapper = await mountSuspended(SliderGallery, {
93
+ props: { galleryData: mockGalleryData },
94
+ });
95
+ expect(wrapper.find(".loading-state").exists()).toBe(true);
96
+ });
97
+
98
+ it("hides gallery content before images load", async () => {
99
+ const wrapper = await mountSuspended(SliderGallery, {
100
+ props: { galleryData: mockGalleryData },
101
+ });
102
+ expect(wrapper.find(".gallery-content").exists()).toBe(false);
103
+ });
104
+
105
+ it("shows gallery content after image loads", async () => {
106
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
107
+ expect(wrapper.find(".gallery-content").exists()).toBe(true);
108
+ });
109
+
110
+ it("dismisses loading state when galleryData is empty", async () => {
111
+ const wrapper = await mountSuspended(SliderGallery, {
112
+ props: { galleryData: [] },
113
+ });
114
+ await nextTick();
115
+ // galleryLoaded becomes false → loading-state gains the galleryLoaded class
116
+ expect(wrapper.find(".loading-state.galleryLoaded").exists()).toBe(true);
117
+ });
118
+
119
+ it("resolves loading when first image errors", async () => {
120
+ const wrapper = await mountSuspended(SliderGallery, {
121
+ props: { galleryData: mockGalleryData },
122
+ });
123
+ mockImage.onerror?.();
124
+ await nextTick();
125
+ await nextTick(); // extra tick — onerror → resolve → Promise.race → onMounted resume
126
+ vi.advanceTimersByTime(500);
127
+ await nextTick();
128
+ expect(wrapper.find(".gallery-content").exists()).toBe(true);
129
+ });
130
+
131
+ // ─── Gallery items ────────────────────────────────────────────────────────
132
+
133
+ it("renders correct number of slide items", async () => {
134
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
135
+ expect(wrapper.findAll(".list .item")).toHaveLength(mockGalleryData.length);
136
+ });
137
+
138
+ it("renders correct number of thumbnail items", async () => {
139
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
140
+ expect(wrapper.findAll(".thumbnail .item")).toHaveLength(mockGalleryData.length);
141
+ });
142
+
143
+ it("renders slide image alt text correctly", async () => {
144
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
145
+ const images = wrapper.findAll(".list .item img");
146
+ expect(images[0]?.attributes("alt")).toBe(mockGalleryData[0]?.alt);
147
+ expect(images[1]?.attributes("alt")).toBe(mockGalleryData[1]?.alt);
148
+ });
149
+
150
+ it("renders thumbnail images with lazy loading", async () => {
151
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
152
+ const thumbImages = wrapper.findAll(".thumbnail .item img");
153
+ thumbImages.forEach((img) => {
154
+ expect(img.attributes("loading")).toBe("lazy");
155
+ });
156
+ });
157
+
158
+ it("renders slide content fields when provided", async () => {
159
+ const wrapper = await mountAndLoad([mockGalleryData[0]!], mockImage);
160
+ expect(wrapper.find(".list .item .author").text()).toBe(mockGalleryData[0]!.stylist);
161
+ expect(wrapper.find(".list .item .title").text()).toBe(mockGalleryData[0]!.title);
162
+ expect(wrapper.find(".list .item .topic").text()).toBe(mockGalleryData[0]!.category);
163
+ expect(wrapper.find(".list .item .description").text()).toBe(mockGalleryData[0]!.description);
164
+ });
165
+
166
+ it("renders thumbnail content when provided", async () => {
167
+ const wrapper = await mountAndLoad([mockGalleryData[0]!], mockImage);
168
+ expect(wrapper.find(".thumbnail .item .title").text()).toBe(mockGalleryData[0]!.thumbnail?.title);
169
+ expect(wrapper.find(".thumbnail .item .description").text()).toBe(mockGalleryData[0]!.thumbnail?.description);
170
+ });
171
+
172
+ // ─── Navigation buttons ───────────────────────────────────────────────────
173
+
174
+ it("renders prev and next arrow buttons", async () => {
175
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
176
+ expect(wrapper.find("#prev").exists()).toBe(true);
177
+ expect(wrapper.find("#next").exists()).toBe(true);
178
+ });
179
+
180
+ it("prev button has accessible aria-label", async () => {
181
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
182
+ expect(wrapper.find("#prev").attributes("aria-label")).toBe("Previous image");
183
+ });
184
+
185
+ it("next button has accessible aria-label", async () => {
186
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
187
+ expect(wrapper.find("#next").attributes("aria-label")).toBe("Next image");
188
+ });
189
+
190
+ // ─── styleClassPassthrough ────────────────────────────────────────────────
191
+
192
+ it("applies a single styleClassPassthrough string", async () => {
193
+ const wrapper = await mountSuspended(SliderGallery, {
194
+ props: { styleClassPassthrough: "custom-gallery" },
195
+ });
196
+ expect(wrapper.classes()).toContain("custom-gallery");
197
+ });
198
+
199
+ it("applies multiple styleClassPassthrough classes from an array", async () => {
200
+ const wrapper = await mountSuspended(SliderGallery, {
201
+ props: { styleClassPassthrough: ["class-a", "class-b"] },
202
+ });
203
+ expect(wrapper.classes()).toContain("class-a");
204
+ expect(wrapper.classes()).toContain("class-b");
205
+ });
206
+
207
+ it("updates classes when styleClassPassthrough prop changes", async () => {
208
+ const wrapper = await mountSuspended(SliderGallery, {
209
+ props: { styleClassPassthrough: ["original"] },
210
+ });
211
+ expect(wrapper.classes()).toContain("original");
212
+ await wrapper.setProps({ styleClassPassthrough: ["updated"] });
213
+ expect(wrapper.classes()).not.toContain("original");
214
+ expect(wrapper.classes()).toContain("updated");
215
+ });
216
+
217
+ // ─── Cleanup ──────────────────────────────────────────────────────────────
218
+
219
+ it("removes keydown listener from wrapper on unmount", async () => {
220
+ const wrapper = await mountAndLoad(mockGalleryData, mockImage);
221
+ const el = wrapper.element as HTMLElement;
222
+ const spy = vi.spyOn(el, "removeEventListener");
223
+ wrapper.unmount();
224
+ expect(spy).toHaveBeenCalledWith("keydown", expect.any(Function));
225
+ });
226
+ });
@@ -0,0 +1,69 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`SliderGallery > renders correct HTML structure after load 1`] = `
4
+ "<div class="slider-gallery">
5
+ <div class="loading-state">
6
+ <div class="loading-spinner"></div>
7
+ <p>Loading gallery...</p>
8
+ </div>
9
+ <div class="gallery-content">
10
+ <div class="list">
11
+ <div class="item"><img data-nuxt-img="" srcset="/_ipx/_/images/slide-1.jpg 1x, /_ipx/_/images/slide-1.jpg 2x" alt="Slide 1" src="/_ipx/_/images/slide-1.jpg">
12
+ <div class="content light">
13
+ <div class="author light">Stylist One</div>
14
+ <div class="title light">First Slide</div>
15
+ <div class="topic light">Fashion</div>
16
+ <div class="description light">Description for first slide</div>
17
+ <div class="buttons light"><button>SEE MORE</button></div>
18
+ </div>
19
+ </div>
20
+ <div class="item"><img data-nuxt-img="" srcset="/_ipx/_/images/slide-2.jpg 1x, /_ipx/_/images/slide-2.jpg 2x" alt="Slide 2" src="/_ipx/_/images/slide-2.jpg">
21
+ <div class="content dark">
22
+ <div class="author dark"></div>
23
+ <div class="title dark">Second Slide</div>
24
+ <div class="topic dark">Style</div>
25
+ <div class="description dark"></div>
26
+ <div class="buttons dark"><button>SEE MORE</button></div>
27
+ </div>
28
+ </div>
29
+ <div class="item"><img data-nuxt-img="" srcset="/_ipx/_/images/slide-3.jpg 1x, /_ipx/_/images/slide-3.jpg 2x" alt="Slide 3" src="/_ipx/_/images/slide-3.jpg">
30
+ <div class="content light">
31
+ <div class="author light"></div>
32
+ <div class="title light"></div>
33
+ <div class="topic light"></div>
34
+ <div class="description light"></div>
35
+ <div class="buttons light"><button>SEE MORE</button></div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ <div class="thumbnail">
40
+ <div class="item">
41
+ <div class="inner"><img data-nuxt-img="" srcset="/_ipx/_/images/slide-1.jpg 1x, /_ipx/_/images/slide-1.jpg 2x" alt="Slide 1" loading="lazy" src="/_ipx/_/images/slide-1.jpg">
42
+ <div class="content light">
43
+ <div class="title light">Thumb 1</div>
44
+ <div class="description light">Thumb desc 1</div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ <div class="item">
49
+ <div class="inner"><img data-nuxt-img="" srcset="/_ipx/_/images/slide-2.jpg 1x, /_ipx/_/images/slide-2.jpg 2x" alt="Slide 2" loading="lazy" src="/_ipx/_/images/slide-2.jpg">
50
+ <div class="content dark">
51
+ <div class="title dark">Thumb 2</div>
52
+ <div class="description dark">Thumb desc 2</div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <div class="item">
57
+ <div class="inner"><img data-nuxt-img="" srcset="/_ipx/_/images/slide-3.jpg 1x, /_ipx/_/images/slide-3.jpg 2x" alt="Slide 3" loading="lazy" src="/_ipx/_/images/slide-3.jpg">
58
+ <div class="content light">
59
+ <div class="title light"></div>
60
+ <div class="description light"></div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ <div class="arrows"><button id="prev" aria-label="Previous image"><span class="iconify i-ic:outline-keyboard-arrow-left arrows-icon" aria-hidden="true"></span></button><button id="next" aria-label="Next image"><span class="iconify i-ic:outline-keyboard-arrow-right arrows-icon" aria-hidden="true"></span></button></div>
66
+ <div class="time"></div>
67
+ </div>
68
+ </div>"
69
+ `;
@@ -5,7 +5,7 @@
5
5
  <InputButtonCore
6
6
  variant="secondary"
7
7
  :button-text="`Enquire about ${serviceData.title}`"
8
- :href="`/services/${serviceData.slug}`"
8
+ :href="`/ui/services/services-section/${serviceData.slug}`"
9
9
  :style-class-passthrough="['mbs-24']"
10
10
  >
11
11
  <template #right>
@@ -11,7 +11,7 @@
11
11
  >
12
12
  <template #summary-link="{ serviceData }">
13
13
  <LinkText
14
- :to="`/services/${serviceData.slug}`"
14
+ :to="`/ui/services/services-section/${serviceData.slug}`"
15
15
  :link-text="`More about ${serviceData.title}`"
16
16
  :style-class-passthrough="['mb-20']"
17
17
  />
@@ -3,7 +3,7 @@
3
3
  :is="tag"
4
4
  class="services-section"
5
5
  :class="[elementClasses]"
6
- :aria-labelledby="needsLabel ? headingId : undefined"
6
+ :aria-labelledby="ariaLabelledby"
7
7
  >
8
8
  <div class="services-section__grid" :class="{ 'services-section__grid--reverse': reverse }">
9
9
  <div class="image-wrapper">
@@ -166,8 +166,7 @@ const props = withDefaults(defineProps<Props>(), {
166
166
  styleClassPassthrough: () => [],
167
167
  });
168
168
 
169
- const headingId = useId();
170
- const needsLabel = computed(() => props.tag === "section" || props.tag === "article");
169
+ const { ariaLabelledby } = useAriaLabelledById(() => props.tag);
171
170
 
172
171
  const infoWrapperClasses = computed(() => {
173
172
  return {