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,245 @@
1
+ import ExpandingPanel from "../ExpandingPanel.vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+
4
+ const meta: Meta<typeof ExpandingPanel> = {
5
+ title: "Molecules/Expandable/Expanding Panel",
6
+ component: ExpandingPanel,
7
+ argTypes: {
8
+ name: {
9
+ control: { type: "text" },
10
+ description:
11
+ "Unique name used for ARIA attributes and to group panels in a native <details> accordion",
12
+ },
13
+ animationDuration: {
14
+ control: { type: "number", min: 0, step: 50 },
15
+ description: "Expand/collapse animation duration in milliseconds",
16
+ },
17
+ forceOpened: {
18
+ control: { type: "boolean" },
19
+ description: "When true, the panel is always open and the toggle icon is hidden",
20
+ },
21
+ styleClassPassthrough: {
22
+ control: "object",
23
+ description: "Additional CSS classes applied to the root element",
24
+ },
25
+ },
26
+ args: {
27
+ name: "panel-demo",
28
+ animationDuration: 400,
29
+ forceOpened: false,
30
+ styleClassPassthrough: [],
31
+ },
32
+ };
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof ExpandingPanel>;
36
+
37
+ // ─── Stories ─────────────────────────────────────────────────────────────────
38
+
39
+ export const Default: Story = {
40
+ render: (args) => ({
41
+ components: { ExpandingPanel },
42
+ setup() {
43
+ return { args };
44
+ },
45
+ template: `
46
+ <ExpandingPanel v-bind="args">
47
+ <template #summary>
48
+ <span>Panel Summary</span>
49
+ </template>
50
+ <template #content>
51
+ <p style="margin:0;padding:0.5rem 0">
52
+ This is the panel content. It can contain any markup — text, images, or components.
53
+ </p>
54
+ </template>
55
+ </ExpandingPanel>
56
+ `,
57
+ }),
58
+ };
59
+
60
+ export const InitiallyOpen: Story = {
61
+ name: "Initially Open",
62
+ args: {
63
+ name: "initially-open",
64
+ },
65
+ render: (args) => ({
66
+ components: { ExpandingPanel },
67
+ setup() {
68
+ const isOpen = ref(true);
69
+ return { args, isOpen };
70
+ },
71
+ template: `
72
+ <ExpandingPanel v-bind="args" v-model="isOpen">
73
+ <template #summary>
74
+ <span>This panel starts open</span>
75
+ </template>
76
+ <template #content>
77
+ <p style="margin:0;padding:0.5rem 0">
78
+ The panel was opened via v-model on mount.
79
+ </p>
80
+ </template>
81
+ </ExpandingPanel>
82
+ `,
83
+ }),
84
+ };
85
+
86
+ export const ForceOpened: Story = {
87
+ name: "Force Opened",
88
+ args: {
89
+ name: "force-opened",
90
+ forceOpened: true,
91
+ },
92
+ render: (args) => ({
93
+ components: { ExpandingPanel },
94
+ setup() {
95
+ return { args };
96
+ },
97
+ template: `
98
+ <ExpandingPanel v-bind="args">
99
+ <template #summary>
100
+ <span>Always open — toggle icon hidden</span>
101
+ </template>
102
+ <template #content>
103
+ <p style="margin:0;padding:0.5rem 0">
104
+ When <code>forceOpened</code> is true the panel stays open and the icon is not rendered.
105
+ </p>
106
+ </template>
107
+ </ExpandingPanel>
108
+ `,
109
+ }),
110
+ };
111
+
112
+ export const CustomIcon: Story = {
113
+ name: "Custom Icon",
114
+ args: {
115
+ name: "custom-icon",
116
+ },
117
+ render: (args) => ({
118
+ components: { ExpandingPanel },
119
+ setup() {
120
+ return { args };
121
+ },
122
+ template: `
123
+ <ExpandingPanel v-bind="args">
124
+ <template #summary>
125
+ <span>Panel with a custom icon</span>
126
+ </template>
127
+ <template #icon>
128
+ <span style="font-size:1.2rem">+</span>
129
+ </template>
130
+ <template #content>
131
+ <p style="margin:0;padding:0.5rem 0">
132
+ Replace the default caret with any element via the <code>icon</code> slot.
133
+ </p>
134
+ </template>
135
+ </ExpandingPanel>
136
+ `,
137
+ }),
138
+ };
139
+
140
+ export const SlowAnimation: Story = {
141
+ name: "Slow Animation (800ms)",
142
+ args: {
143
+ name: "slow-animation",
144
+ animationDuration: 800,
145
+ },
146
+ render: (args) => ({
147
+ components: { ExpandingPanel },
148
+ setup() {
149
+ return { args };
150
+ },
151
+ template: `
152
+ <ExpandingPanel v-bind="args">
153
+ <template #summary>
154
+ <span>Slow animation panel</span>
155
+ </template>
156
+ <template #content>
157
+ <p style="margin:0;padding:0.5rem 0">
158
+ The expand/collapse transition runs over 800 ms.
159
+ </p>
160
+ </template>
161
+ </ExpandingPanel>
162
+ `,
163
+ }),
164
+ };
165
+
166
+ export const NoAnimation: Story = {
167
+ name: "No Animation (0ms)",
168
+ args: {
169
+ name: "no-animation",
170
+ animationDuration: 0,
171
+ },
172
+ render: (args) => ({
173
+ components: { ExpandingPanel },
174
+ setup() {
175
+ return { args };
176
+ },
177
+ template: `
178
+ <ExpandingPanel v-bind="args">
179
+ <template #summary>
180
+ <span>Instant toggle</span>
181
+ </template>
182
+ <template #content>
183
+ <p style="margin:0;padding:0.5rem 0">
184
+ The panel opens and closes with no transition.
185
+ </p>
186
+ </template>
187
+ </ExpandingPanel>
188
+ `,
189
+ }),
190
+ };
191
+
192
+ export const RichContent: Story = {
193
+ name: "Rich Slot Content",
194
+ args: {
195
+ name: "rich-content",
196
+ animationDuration: 400,
197
+ },
198
+ render: (args) => ({
199
+ components: { ExpandingPanel },
200
+ setup() {
201
+ return { args };
202
+ },
203
+ template: `
204
+ <ExpandingPanel v-bind="args">
205
+ <template #summary>
206
+ <strong>Materials &amp; Care</strong>
207
+ </template>
208
+ <template #content>
209
+ <ul style="margin:0;padding:0.5rem 0 0.5rem 1.2rem">
210
+ <li>100% organic cotton</li>
211
+ <li>Machine wash at 30°C</li>
212
+ <li>Do not tumble dry</li>
213
+ <li>Iron on low heat</li>
214
+ </ul>
215
+ </template>
216
+ </ExpandingPanel>
217
+ `,
218
+ }),
219
+ };
220
+
221
+ export const WithStyleClassPassthrough: Story = {
222
+ name: "With styleClassPassthrough",
223
+ args: {
224
+ name: "style-passthrough",
225
+ styleClassPassthrough: ["custom-class", "another-class"],
226
+ },
227
+ render: (args) => ({
228
+ components: { ExpandingPanel },
229
+ setup() {
230
+ return { args };
231
+ },
232
+ template: `
233
+ <ExpandingPanel v-bind="args">
234
+ <template #summary>
235
+ <span>Custom classes applied to root</span>
236
+ </template>
237
+ <template #content>
238
+ <p style="margin:0;padding:0.5rem 0">
239
+ Inspect the root element to see <code>custom-class</code> and <code>another-class</code>.
240
+ </p>
241
+ </template>
242
+ </ExpandingPanel>
243
+ `,
244
+ }),
245
+ };
@@ -0,0 +1,351 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { nextTick } from "vue";
3
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
4
+ import type { ComponentPublicInstance } from "vue";
5
+ import ExpandingPanel from "../ExpandingPanel.vue";
6
+
7
+ interface ExpandingPanelInstance extends ComponentPublicInstance {
8
+ animationDurationStr: string;
9
+ open: boolean;
10
+ isPanelOpen: boolean;
11
+ }
12
+
13
+ describe("ExpandingPanel", () => {
14
+ // ─── Mount ───────────────────────────────────────────────────────────────
15
+
16
+ it("mounts without error", async () => {
17
+ const wrapper = await mountSuspended(ExpandingPanel, {
18
+ props: { name: "test" },
19
+ });
20
+ expect(wrapper.vm).toBeTruthy();
21
+ });
22
+
23
+ // ─── Snapshots ───────────────────────────────────────────────────────────
24
+
25
+ it("renders correct HTML structure with default props", async () => {
26
+ const wrapper = await mountSuspended(ExpandingPanel, {
27
+ props: { name: "snap-default" },
28
+ });
29
+ expect(wrapper.html()).toMatchSnapshot();
30
+ });
31
+
32
+ it("renders correct HTML structure with all props set", async () => {
33
+ const wrapper = await mountSuspended(ExpandingPanel, {
34
+ props: {
35
+ name: "snap-all",
36
+ animationDuration: 500,
37
+ forceOpened: true,
38
+ styleClassPassthrough: ["extra-class"],
39
+ },
40
+ });
41
+ expect(wrapper.html()).toMatchSnapshot();
42
+ });
43
+
44
+ it("renders correct HTML structure with populated slots", async () => {
45
+ const wrapper = await mountSuspended(ExpandingPanel, {
46
+ props: { name: "snap-slots" },
47
+ slots: {
48
+ summary: "<span>Summary text</span>",
49
+ icon: "<span>▸</span>",
50
+ content: "<p>Content text</p>",
51
+ },
52
+ });
53
+ expect(wrapper.html()).toMatchSnapshot();
54
+ });
55
+
56
+ // ─── Root element ─────────────────────────────────────────────────────────
57
+
58
+ it("renders the root .expanding-panel element", async () => {
59
+ const wrapper = await mountSuspended(ExpandingPanel, {
60
+ props: { name: "root-test" },
61
+ });
62
+ expect(wrapper.find(".expanding-panel").exists()).toBe(true);
63
+ });
64
+
65
+ it("renders a <details> element inside the root", async () => {
66
+ const wrapper = await mountSuspended(ExpandingPanel, {
67
+ props: { name: "details-test" },
68
+ });
69
+ expect(wrapper.find("details.expanding-panel-details").exists()).toBe(true);
70
+ });
71
+
72
+ it("renders a <summary> element inside <details>", async () => {
73
+ const wrapper = await mountSuspended(ExpandingPanel, {
74
+ props: { name: "summary-test" },
75
+ });
76
+ expect(wrapper.find("summary.expanding-panel-summary").exists()).toBe(true);
77
+ });
78
+
79
+ it("renders the content region div", async () => {
80
+ const wrapper = await mountSuspended(ExpandingPanel, {
81
+ props: { name: "content-test" },
82
+ });
83
+ expect(wrapper.find(".expanding-panel-content").exists()).toBe(true);
84
+ });
85
+
86
+ // ─── Default prop values ──────────────────────────────────────────────────
87
+
88
+ it("uses correct default prop values on the component instance", async () => {
89
+ const wrapper = await mountSuspended(ExpandingPanel, {
90
+ props: { name: "defaults-test" },
91
+ });
92
+ expect(wrapper.props("animationDuration")).toBe(400);
93
+ expect(wrapper.props("forceOpened")).toBe(false);
94
+ expect(wrapper.props("styleClassPassthrough")).toEqual([]);
95
+ });
96
+
97
+ it("defaults model value (isPanelOpen) to false", async () => {
98
+ const wrapper = await mountSuspended(ExpandingPanel, {
99
+ props: { name: "model-default" },
100
+ });
101
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
102
+ expect(vm.isPanelOpen).toBe(false);
103
+ });
104
+
105
+ // ─── Computed ─────────────────────────────────────────────────────────────
106
+
107
+ it("computes animationDurationStr as '<value>ms'", async () => {
108
+ const wrapper = await mountSuspended(ExpandingPanel, {
109
+ props: { name: "anim-str", animationDuration: 750 },
110
+ });
111
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
112
+ expect(vm.animationDurationStr).toBe("750ms");
113
+ });
114
+
115
+ it("computes animationDurationStr using the default duration", async () => {
116
+ const wrapper = await mountSuspended(ExpandingPanel, {
117
+ props: { name: "anim-str-default" },
118
+ });
119
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
120
+ expect(vm.animationDurationStr).toBe("400ms");
121
+ });
122
+
123
+ it("open is false when forceOpened is false and model is false", async () => {
124
+ const wrapper = await mountSuspended(ExpandingPanel, {
125
+ props: { name: "open-false", forceOpened: false },
126
+ });
127
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
128
+ expect(vm.open).toBe(false);
129
+ });
130
+
131
+ it("open is true when model is true", async () => {
132
+ const wrapper = await mountSuspended(ExpandingPanel, {
133
+ props: { name: "open-model" },
134
+ attrs: { modelValue: true },
135
+ });
136
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
137
+ expect(vm.open).toBe(true);
138
+ });
139
+
140
+ it("open is true when forceOpened is true regardless of model", async () => {
141
+ const wrapper = await mountSuspended(ExpandingPanel, {
142
+ props: { name: "open-forced", forceOpened: true },
143
+ attrs: { modelValue: false },
144
+ });
145
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
146
+ expect(vm.open).toBe(true);
147
+ });
148
+
149
+ // ─── ARIA & IDs ───────────────────────────────────────────────────────────
150
+
151
+ it("sets id on summary to id-{name}-trigger", async () => {
152
+ const wrapper = await mountSuspended(ExpandingPanel, {
153
+ props: { name: "aria-panel" },
154
+ });
155
+ expect(wrapper.find("summary").attributes("id")).toBe("id-aria-panel-trigger");
156
+ });
157
+
158
+ it("sets aria-controls on summary to id-{name}-content", async () => {
159
+ const wrapper = await mountSuspended(ExpandingPanel, {
160
+ props: { name: "aria-panel" },
161
+ });
162
+ expect(wrapper.find("summary").attributes("aria-controls")).toBe("id-aria-panel-content");
163
+ });
164
+
165
+ it("sets aria-expanded to false when closed", async () => {
166
+ const wrapper = await mountSuspended(ExpandingPanel, {
167
+ props: { name: "aria-expanded" },
168
+ });
169
+ expect(wrapper.find("summary").attributes("aria-expanded")).toBe("false");
170
+ });
171
+
172
+ it("sets aria-expanded to true when open", async () => {
173
+ const wrapper = await mountSuspended(ExpandingPanel, {
174
+ props: { name: "aria-expanded-open" },
175
+ attrs: { modelValue: true },
176
+ });
177
+ expect(wrapper.find("summary").attributes("aria-expanded")).toBe("true");
178
+ });
179
+
180
+ it("sets id on content region to id-{name}-content", async () => {
181
+ const wrapper = await mountSuspended(ExpandingPanel, {
182
+ props: { name: "aria-panel" },
183
+ });
184
+ expect(wrapper.find(".expanding-panel-content").attributes("id")).toBe("id-aria-panel-content");
185
+ });
186
+
187
+ it("sets aria-labelledby on content region to id-{name}-trigger", async () => {
188
+ const wrapper = await mountSuspended(ExpandingPanel, {
189
+ props: { name: "aria-panel" },
190
+ });
191
+ expect(wrapper.find(".expanding-panel-content").attributes("aria-labelledby")).toBe(
192
+ "id-aria-panel-trigger",
193
+ );
194
+ });
195
+
196
+ it("sets role='region' on the content div", async () => {
197
+ const wrapper = await mountSuspended(ExpandingPanel, {
198
+ props: { name: "role-test" },
199
+ });
200
+ expect(wrapper.find(".expanding-panel-content").attributes("role")).toBe("region");
201
+ });
202
+
203
+ // ─── Toggle behaviour ─────────────────────────────────────────────────────
204
+
205
+ it("toggles open state when summary is clicked", async () => {
206
+ const wrapper = await mountSuspended(ExpandingPanel, {
207
+ props: { name: "toggle-test" },
208
+ });
209
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
210
+ expect(vm.isPanelOpen).toBe(false);
211
+
212
+ await wrapper.find("summary").trigger("click");
213
+ expect(vm.isPanelOpen).toBe(true);
214
+
215
+ await wrapper.find("summary").trigger("click");
216
+ expect(vm.isPanelOpen).toBe(false);
217
+ });
218
+
219
+ it("emits update:modelValue when summary is clicked", async () => {
220
+ const wrapper = await mountSuspended(ExpandingPanel, {
221
+ props: { name: "emit-test" },
222
+ });
223
+ await wrapper.find("summary").trigger("click");
224
+ expect(wrapper.emitted("update:modelValue")).toBeTruthy();
225
+ expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([true]);
226
+ });
227
+
228
+ it("updates aria-expanded after toggle", async () => {
229
+ const wrapper = await mountSuspended(ExpandingPanel, {
230
+ props: { name: "aria-toggle" },
231
+ });
232
+ expect(wrapper.find("summary").attributes("aria-expanded")).toBe("false");
233
+
234
+ await wrapper.find("summary").trigger("click");
235
+ await nextTick();
236
+ expect(wrapper.find("summary").attributes("aria-expanded")).toBe("true");
237
+ });
238
+
239
+ it("sets open attribute on <details> when panel is open", async () => {
240
+ const wrapper = await mountSuspended(ExpandingPanel, {
241
+ props: { name: "details-open" },
242
+ attrs: { modelValue: true },
243
+ });
244
+ expect(wrapper.find("details").attributes("open")).toBeDefined();
245
+ });
246
+
247
+ it("does not set open attribute on <details> when panel is closed", async () => {
248
+ const wrapper = await mountSuspended(ExpandingPanel, {
249
+ props: { name: "details-closed" },
250
+ });
251
+ expect(wrapper.find("details").attributes("open")).toBeUndefined();
252
+ });
253
+
254
+ // ─── forceOpened ──────────────────────────────────────────────────────────
255
+
256
+ it("hides the icon-wrapper when forceOpened is true", async () => {
257
+ const wrapper = await mountSuspended(ExpandingPanel, {
258
+ props: { name: "force-icon", forceOpened: true },
259
+ });
260
+ expect(wrapper.find(".icon-wrapper").exists()).toBe(false);
261
+ });
262
+
263
+ it("shows the icon-wrapper when forceOpened is false", async () => {
264
+ const wrapper = await mountSuspended(ExpandingPanel, {
265
+ props: { name: "icon-visible", forceOpened: false },
266
+ });
267
+ expect(wrapper.find(".icon-wrapper").exists()).toBe(true);
268
+ });
269
+
270
+ it("keeps open true after click when forceOpened is true", async () => {
271
+ const wrapper = await mountSuspended(ExpandingPanel, {
272
+ props: { name: "force-click", forceOpened: true },
273
+ });
274
+ const vm = wrapper.vm as unknown as ExpandingPanelInstance;
275
+ await wrapper.find("summary").trigger("click");
276
+ expect(vm.open).toBe(true);
277
+ });
278
+
279
+ // ─── Slots ────────────────────────────────────────────────────────────────
280
+
281
+ it("renders summary slot content inside .label-wrapper", async () => {
282
+ const wrapper = await mountSuspended(ExpandingPanel, {
283
+ props: { name: "slot-summary" },
284
+ slots: { summary: '<span data-testid="sum">My Summary</span>' },
285
+ });
286
+ expect(wrapper.find('[data-testid="sum"]').text()).toBe("My Summary");
287
+ });
288
+
289
+ it("renders content slot content inside .inner", async () => {
290
+ const wrapper = await mountSuspended(ExpandingPanel, {
291
+ props: { name: "slot-content" },
292
+ slots: { content: '<p data-testid="body">My Content</p>' },
293
+ });
294
+ expect(wrapper.find('[data-testid="body"]').text()).toBe("My Content");
295
+ });
296
+
297
+ it("renders custom icon slot content inside .icon-wrapper", async () => {
298
+ const wrapper = await mountSuspended(ExpandingPanel, {
299
+ props: { name: "slot-icon" },
300
+ slots: { icon: '<span data-testid="custom-icon">▸</span>' },
301
+ });
302
+ expect(wrapper.find('[data-testid="custom-icon"]').exists()).toBe(true);
303
+ });
304
+
305
+ it("renders the default icon when no icon slot is provided", async () => {
306
+ const wrapper = await mountSuspended(ExpandingPanel, {
307
+ props: { name: "default-icon" },
308
+ });
309
+ expect(wrapper.find(".icon-wrapper").exists()).toBe(true);
310
+ expect(wrapper.find(".icon-wrapper .icon").exists()).toBe(true);
311
+ });
312
+
313
+ // ─── styleClassPassthrough ────────────────────────────────────────────────
314
+
315
+ it("applies a styleClassPassthrough array to the root element", async () => {
316
+ const wrapper = await mountSuspended(ExpandingPanel, {
317
+ props: {
318
+ name: "scp-array",
319
+ styleClassPassthrough: ["custom-class", "another-class"],
320
+ },
321
+ });
322
+ const root = wrapper.find(".expanding-panel");
323
+ expect(root.classes()).toContain("custom-class");
324
+ expect(root.classes()).toContain("another-class");
325
+ });
326
+
327
+ it("applies a single styleClassPassthrough string to the root element", async () => {
328
+ const wrapper = await mountSuspended(ExpandingPanel, {
329
+ props: { name: "scp-string", styleClassPassthrough: "single-class" },
330
+ });
331
+ expect(wrapper.find(".expanding-panel").classes()).toContain("single-class");
332
+ });
333
+
334
+ // ─── name prop / useId fallback ───────────────────────────────────────────
335
+
336
+ it("uses the provided name in ARIA attributes", async () => {
337
+ const wrapper = await mountSuspended(ExpandingPanel, {
338
+ props: { name: "my-panel" },
339
+ });
340
+ expect(wrapper.find("summary").attributes("id")).toBe("id-my-panel-trigger");
341
+ expect(wrapper.find(".expanding-panel-content").attributes("id")).toBe("id-my-panel-content");
342
+ });
343
+
344
+ it("generates ARIA ids when no name prop is supplied", async () => {
345
+ const wrapper = await mountSuspended(ExpandingPanel);
346
+ const summaryId = wrapper.find("summary").attributes("id") ?? "";
347
+ const contentId = wrapper.find(".expanding-panel-content").attributes("id") ?? "";
348
+ expect(summaryId).toMatch(/^id-.+-trigger$/);
349
+ expect(contentId).toMatch(/^id-.+-content$/);
350
+ });
351
+ });
@@ -0,0 +1,38 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`ExpandingPanel > renders correct HTML structure with all props set 1`] = `
4
+ "<div class="expanding-panel extra-class">
5
+ <details class="expanding-panel-details" name="snap-all" open="">
6
+ <summary id="id-snap-all-trigger" class="expanding-panel-summary" aria-controls="id-snap-all-content" aria-expanded="true"><span class="label-wrapper"></span>
7
+ <!--v-if-->
8
+ </summary>
9
+ </details>
10
+ <div id="id-snap-all-content" class="expanding-panel-content" aria-labelledby="id-snap-all-trigger" role="region">
11
+ <div class="inner"></div>
12
+ </div>
13
+ </div>"
14
+ `;
15
+
16
+ exports[`ExpandingPanel > renders correct HTML structure with default props 1`] = `
17
+ "<div class="expanding-panel">
18
+ <details class="expanding-panel-details" name="snap-default">
19
+ <summary id="id-snap-default-trigger" class="expanding-panel-summary" aria-controls="id-snap-default-content" aria-expanded="false"><span class="label-wrapper"></span><span class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></span></summary>
20
+ </details>
21
+ <div id="id-snap-default-content" class="expanding-panel-content" aria-labelledby="id-snap-default-trigger" role="region">
22
+ <div class="inner"></div>
23
+ </div>
24
+ </div>"
25
+ `;
26
+
27
+ exports[`ExpandingPanel > renders correct HTML structure with populated slots 1`] = `
28
+ "<div class="expanding-panel">
29
+ <details class="expanding-panel-details" name="snap-slots">
30
+ <summary id="id-snap-slots-trigger" class="expanding-panel-summary" aria-controls="id-snap-slots-content" aria-expanded="false"><span class="label-wrapper"><span>Summary text</span></span><span class="icon-wrapper"><span>▸</span></span></summary>
31
+ </details>
32
+ <div id="id-snap-slots-content" class="expanding-panel-content" aria-labelledby="id-snap-slots-trigger" role="region">
33
+ <div class="inner">
34
+ <p>Content text</p>
35
+ </div>
36
+ </div>
37
+ </div>"
38
+ `;