srcdev-nuxt-components 9.6.5 → 9.6.7

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 (35) hide show
  1. package/.claude/rules/consumer/a11y-no-focus-steal.md +14 -0
  2. package/.claude/skills/component-compliance-checklist.md +5 -1
  3. package/.claude/skills/components/display-toast.md +6 -3
  4. package/.claude/skills/components/page-hero-highlights.md +22 -18
  5. package/.claude/skills/components/social-icons-list.md +32 -5
  6. package/.claude/skills/css-dynamic-border-radius.md +114 -0
  7. package/.claude/skills/index.md +3 -2
  8. package/.claude/skills/storybook-add-story.md +6 -0
  9. package/.vscode/srcdev-component-display-toast.code-snippets +6 -8
  10. package/.vscode/srcdev-component-page-hero-highlights.code-snippets +2 -1
  11. package/.vscode/srcdev-component-social-icons-list.code-snippets +14 -0
  12. package/.vscode/srcdev-utility-dynamic-border-radius.code-snippets +31 -0
  13. package/app/assets/styles/setup/06.utility-classes/_dynamic-border-radius.css +27 -0
  14. package/app/assets/styles/setup/06.utility-classes/index.css +1 -0
  15. package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +97 -8
  16. package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +80 -14
  17. package/app/components/02.molecules/social-icons-list/stories/SocialIconsList.stories.ts +230 -2
  18. package/app/components/02.molecules/social-icons-list/stories/social-icons-list-recipes.css +108 -0
  19. package/app/components/02.molecules/social-icons-list/tests/SocialIconsList.spec.ts +123 -52
  20. package/app/components/02.molecules/social-icons-list/tests/__snapshots__/SocialIconsList.spec.ts.snap +10 -4
  21. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +9 -3
  22. package/app/components/02.molecules/toast/DisplayToast.vue +10 -10
  23. package/app/components/02.molecules/toast/DisplayToastProvider.vue +12 -4
  24. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +200 -2
  25. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +33 -12
  26. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +20 -0
  27. package/app/components/04.templates/page-hero-highlights/CONSUMER-STYLING.md +32 -12
  28. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +56 -47
  29. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +158 -307
  30. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +10 -0
  31. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +49 -27
  32. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +42 -7
  33. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +12 -10
  34. package/app/types/components/social-icons-list.d.ts +9 -0
  35. package/package.json +1 -1
@@ -1,16 +1,29 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
2
  import { h } from "vue";
3
3
  import { mountSuspended } from "@nuxt/test-utils/runtime";
4
4
  import PageHeroHighlights from "../PageHeroHighlights.vue";
5
5
 
6
+ const mounted: { unmount: () => void }[] = [];
7
+
8
+ const mountTracked = (async (...args: Parameters<typeof mountSuspended>) => {
9
+ const wrapper = await mountSuspended(...args);
10
+ mounted.push(wrapper);
11
+ return wrapper;
12
+ }) as typeof mountSuspended;
13
+
14
+ afterEach(() => {
15
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount());
16
+ vi.restoreAllMocks();
17
+ });
18
+
6
19
  describe("PageHeroHighlights", () => {
7
20
  it("mounts without error", async () => {
8
- const wrapper = await mountSuspended(PageHeroHighlights);
21
+ const wrapper = await mountTracked(PageHeroHighlights);
9
22
  expect(wrapper.vm).toBeTruthy();
10
23
  });
11
24
 
12
25
  it("renders correct HTML structure", async () => {
13
- const wrapper = await mountSuspended(PageHeroHighlights, {
26
+ const wrapper = await mountTracked(PageHeroHighlights, {
14
27
  slots: {
15
28
  header: "<h1>Page Title</h1>",
16
29
  highlights: "<div class='highlight'>Highlight 1</div>",
@@ -20,14 +33,23 @@ describe("PageHeroHighlights", () => {
20
33
  expect(wrapper.html()).toMatchSnapshot();
21
34
  });
22
35
 
36
+ it("wraps the rows in .page-hero-highlights-grid, so the root can be the gutter query container", async () => {
37
+ const wrapper = await mountTracked(PageHeroHighlights);
38
+ const grid = wrapper.find(".page-hero-highlights > .page-hero-highlights-grid");
39
+ expect(grid.exists()).toBe(true);
40
+ expect(grid.find(".page-hero-highlights-header-row").exists()).toBe(true);
41
+ expect(grid.find(".page-hero-highlights-strip").exists()).toBe(true);
42
+ expect(grid.find(".page-hero-highlights-content-row").exists()).toBe(true);
43
+ });
44
+
23
45
  it("renders as a div by default", async () => {
24
- const wrapper = await mountSuspended(PageHeroHighlights);
46
+ const wrapper = await mountTracked(PageHeroHighlights);
25
47
  expect(wrapper.element.tagName.toLowerCase()).toBe("div");
26
48
  });
27
49
 
28
50
  it("renders with the correct tag when tag prop is provided", async () => {
29
51
  for (const tag of ["section", "main"] as const) {
30
- const wrapper = await mountSuspended(PageHeroHighlights, {
52
+ const wrapper = await mountTracked(PageHeroHighlights, {
31
53
  props: { tag },
32
54
  });
33
55
  expect(wrapper.element.tagName.toLowerCase()).toBe(tag);
@@ -35,7 +57,7 @@ describe("PageHeroHighlights", () => {
35
57
  });
36
58
 
37
59
  it("renders header slot content", async () => {
38
- const wrapper = await mountSuspended(PageHeroHighlights, {
60
+ const wrapper = await mountTracked(PageHeroHighlights, {
39
61
  slots: { header: "<h1 class='page-title'>Dashboard</h1>" },
40
62
  });
41
63
  expect(wrapper.find(".page-title").exists()).toBe(true);
@@ -43,7 +65,7 @@ describe("PageHeroHighlights", () => {
43
65
  });
44
66
 
45
67
  it("renders highlights slot content", async () => {
46
- const wrapper = await mountSuspended(PageHeroHighlights, {
68
+ const wrapper = await mountTracked(PageHeroHighlights, {
47
69
  slots: {
48
70
  highlights: ["<div class='highlight-card'>Card 1</div>", "<div class='highlight-card'>Card 2</div>"].join(""),
49
71
  },
@@ -52,7 +74,7 @@ describe("PageHeroHighlights", () => {
52
74
  });
53
75
 
54
76
  it("renders content slot content", async () => {
55
- const wrapper = await mountSuspended(PageHeroHighlights, {
77
+ const wrapper = await mountTracked(PageHeroHighlights, {
56
78
  slots: { content: "<p class='body-text'>Page body</p>" },
57
79
  });
58
80
  expect(wrapper.find(".body-text").exists()).toBe(true);
@@ -60,42 +82,42 @@ describe("PageHeroHighlights", () => {
60
82
  });
61
83
 
62
84
  it("content slot is rendered inside .content-slot", async () => {
63
- const wrapper = await mountSuspended(PageHeroHighlights, {
85
+ const wrapper = await mountTracked(PageHeroHighlights, {
64
86
  slots: { content: "<p class='body-text'>Page body</p>" },
65
87
  });
66
88
  expect(wrapper.find(".page-hero-highlights-content-slot .body-text").exists()).toBe(true);
67
89
  });
68
90
 
69
91
  it("header slot is rendered inside .header-slot", async () => {
70
- const wrapper = await mountSuspended(PageHeroHighlights, {
92
+ const wrapper = await mountTracked(PageHeroHighlights, {
71
93
  slots: { header: "<h1 class='page-title'>Dashboard</h1>" },
72
94
  });
73
95
  expect(wrapper.find(".page-hero-highlights-header-slot .page-title").exists()).toBe(true);
74
96
  });
75
97
 
76
98
  it("adds aria-labelledby when tag is section", async () => {
77
- const wrapper = await mountSuspended(PageHeroHighlights, {
99
+ const wrapper = await mountTracked(PageHeroHighlights, {
78
100
  props: { tag: "section" },
79
101
  });
80
102
  expect(wrapper.find(".page-hero-highlights").attributes("aria-labelledby")).toBeTruthy();
81
103
  });
82
104
 
83
105
  it("does not add aria-labelledby when tag is main (main never needs an accessible name)", async () => {
84
- const wrapper = await mountSuspended(PageHeroHighlights, {
106
+ const wrapper = await mountTracked(PageHeroHighlights, {
85
107
  props: { tag: "main" },
86
108
  });
87
109
  expect(wrapper.find(".page-hero-highlights").attributes("aria-labelledby")).toBeUndefined();
88
110
  });
89
111
 
90
112
  it("does not add aria-labelledby when tag is div", async () => {
91
- const wrapper = await mountSuspended(PageHeroHighlights, {
113
+ const wrapper = await mountTracked(PageHeroHighlights, {
92
114
  props: { tag: "div" },
93
115
  });
94
116
  expect(wrapper.find(".page-hero-highlights").attributes("aria-labelledby")).toBeUndefined();
95
117
  });
96
118
 
97
119
  it("exposes headingId via header scoped slot and aria-labelledby matches", async () => {
98
- const wrapper = await mountSuspended(PageHeroHighlights, {
120
+ const wrapper = await mountTracked(PageHeroHighlights, {
99
121
  props: { tag: "section" },
100
122
  slots: {
101
123
  header: (props: Record<string, unknown>) => h("h1", { id: props.headingId, class: "page-title" }, "Dashboard"),
@@ -107,13 +129,13 @@ describe("PageHeroHighlights", () => {
107
129
  });
108
130
 
109
131
  it("applies flexible-widths class by default", async () => {
110
- const wrapper = await mountSuspended(PageHeroHighlights);
132
+ const wrapper = await mountTracked(PageHeroHighlights);
111
133
  expect(wrapper.find(".page-hero-highlights-strip").classes()).toContain("flexible-widths");
112
134
  expect(wrapper.find(".page-hero-highlights-strip").classes()).not.toContain("equal-widths");
113
135
  });
114
136
 
115
137
  it("applies equal-widths class when highlightsEqualWidths is true", async () => {
116
- const wrapper = await mountSuspended(PageHeroHighlights, {
138
+ const wrapper = await mountTracked(PageHeroHighlights, {
117
139
  props: { highlightsEqualWidths: true },
118
140
  });
119
141
  expect(wrapper.find(".page-hero-highlights-strip").classes()).toContain("equal-widths");
@@ -121,14 +143,14 @@ describe("PageHeroHighlights", () => {
121
143
  });
122
144
 
123
145
  it("applies justify-start class by default", async () => {
124
- const wrapper = await mountSuspended(PageHeroHighlights);
146
+ const wrapper = await mountTracked(PageHeroHighlights);
125
147
  expect(wrapper.find(".page-hero-highlights-strip").classes()).toContain("justify-start");
126
148
  });
127
149
 
128
150
  it("applies the correct justify class for each highlightsJustify value", async () => {
129
151
  const values = ["start", "center", "end", "space-between", "space-around"] as const;
130
152
  for (const value of values) {
131
- const wrapper = await mountSuspended(PageHeroHighlights, {
153
+ const wrapper = await mountTracked(PageHeroHighlights, {
132
154
  props: { highlightsJustify: value },
133
155
  });
134
156
  expect(wrapper.find(".page-hero-highlights-strip").classes()).toContain(`justify-${value}`);
@@ -136,31 +158,31 @@ describe("PageHeroHighlights", () => {
136
158
  });
137
159
 
138
160
  it("applies has-content-panel class by default", async () => {
139
- const wrapper = await mountSuspended(PageHeroHighlights);
161
+ const wrapper = await mountTracked(PageHeroHighlights);
140
162
  expect(wrapper.find(".page-hero-highlights").classes()).toContain("has-content-panel");
141
163
  });
142
164
 
143
165
  it("does not apply has-content-panel class when contentPanel is false", async () => {
144
- const wrapper = await mountSuspended(PageHeroHighlights, {
166
+ const wrapper = await mountTracked(PageHeroHighlights, {
145
167
  props: { contentPanel: false },
146
168
  });
147
169
  expect(wrapper.find(".page-hero-highlights").classes()).not.toContain("has-content-panel");
148
170
  });
149
171
 
150
172
  it("does not apply highlight-title-baseline class by default", async () => {
151
- const wrapper = await mountSuspended(PageHeroHighlights);
173
+ const wrapper = await mountTracked(PageHeroHighlights);
152
174
  expect(wrapper.find(".page-hero-highlights").classes()).not.toContain("highlight-title-baseline");
153
175
  });
154
176
 
155
177
  it("applies highlight-title-baseline class when highlightTitleBaseline is true", async () => {
156
- const wrapper = await mountSuspended(PageHeroHighlights, {
178
+ const wrapper = await mountTracked(PageHeroHighlights, {
157
179
  props: { highlightTitleBaseline: true },
158
180
  });
159
181
  expect(wrapper.find(".page-hero-highlights").classes()).toContain("highlight-title-baseline");
160
182
  });
161
183
 
162
184
  it("applies styleClassPassthrough classes", async () => {
163
- const wrapper = await mountSuspended(PageHeroHighlights, {
185
+ const wrapper = await mountTracked(PageHeroHighlights, {
164
186
  props: { styleClassPassthrough: ["extra-class", "another-class"] },
165
187
  });
166
188
  const el = wrapper.find(".page-hero-highlights");
@@ -170,24 +192,24 @@ describe("PageHeroHighlights", () => {
170
192
 
171
193
  describe("grid layout classes", () => {
172
194
  it("applies center class by default", async () => {
173
- const wrapper = await mountSuspended(PageHeroHighlights);
195
+ const wrapper = await mountTracked(PageHeroHighlights);
174
196
  expect(wrapper.find(".page-hero-highlights").classes()).toContain("content-align-center");
175
197
  });
176
198
 
177
199
  it("does not apply width-constrained class by default", async () => {
178
- const wrapper = await mountSuspended(PageHeroHighlights);
200
+ const wrapper = await mountTracked(PageHeroHighlights);
179
201
  expect(wrapper.find(".page-hero-highlights").classes()).not.toContain("width-constrained");
180
202
  });
181
203
 
182
204
  it("applies width-constrained class when widthConstrained is true", async () => {
183
- const wrapper = await mountSuspended(PageHeroHighlights, {
205
+ const wrapper = await mountTracked(PageHeroHighlights, {
184
206
  props: { widthConstrained: true },
185
207
  });
186
208
  expect(wrapper.find(".page-hero-highlights").classes()).toContain("width-constrained");
187
209
  });
188
210
 
189
211
  it("applies start class when contentAlign is start", async () => {
190
- const wrapper = await mountSuspended(PageHeroHighlights, {
212
+ const wrapper = await mountTracked(PageHeroHighlights, {
191
213
  props: { contentAlign: "start" },
192
214
  });
193
215
  expect(wrapper.find(".page-hero-highlights").classes()).toContain("content-align-start");
@@ -1,15 +1,29 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { defineComponent } from "vue";
2
3
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
4
  import PageHeroHighlightsHeader from "../PageHeroHighlightsHeader.vue";
4
5
 
6
+ const mounted: { unmount: () => void }[] = [];
7
+
8
+ const mountTracked = (async (...args: Parameters<typeof mountSuspended>) => {
9
+ const wrapper = await mountSuspended(...args);
10
+ mounted.push(wrapper);
11
+ return wrapper;
12
+ }) as typeof mountSuspended;
13
+
14
+ afterEach(() => {
15
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount());
16
+ vi.restoreAllMocks();
17
+ });
18
+
5
19
  describe("PageHeroHighlightsHeader", () => {
6
20
  it("mounts without error", async () => {
7
- const wrapper = await mountSuspended(PageHeroHighlightsHeader);
21
+ const wrapper = await mountTracked(PageHeroHighlightsHeader);
8
22
  expect(wrapper.vm).toBeTruthy();
9
23
  });
10
24
 
11
25
  it("renders start slot content", async () => {
12
- const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
26
+ const wrapper = await mountTracked(PageHeroHighlightsHeader, {
13
27
  slots: { start: "<h1 class='page-title'>Surplus needs</h1>" },
14
28
  });
15
29
  expect(wrapper.find(".page-hero-highlights-header-start .page-title").exists()).toBe(true);
@@ -17,7 +31,7 @@ describe("PageHeroHighlightsHeader", () => {
17
31
  });
18
32
 
19
33
  it("renders end slot content when provided", async () => {
20
- const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
34
+ const wrapper = await mountTracked(PageHeroHighlightsHeader, {
21
35
  slots: {
22
36
  start: "<h1>Title</h1>",
23
37
  end: "<button class='cta'>Create new need</button>",
@@ -27,14 +41,14 @@ describe("PageHeroHighlightsHeader", () => {
27
41
  });
28
42
 
29
43
  it("does not render .page-hero-highlights-header-end when no end slot is provided", async () => {
30
- const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
44
+ const wrapper = await mountTracked(PageHeroHighlightsHeader, {
31
45
  slots: { start: "<h1>Title</h1>" },
32
46
  });
33
47
  expect(wrapper.find(".page-hero-highlights-header-end").exists()).toBe(false);
34
48
  });
35
49
 
36
50
  it("renders .page-hero-highlights-header-start as full width when no end slot is provided", async () => {
37
- const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
51
+ const wrapper = await mountTracked(PageHeroHighlightsHeader, {
38
52
  slots: { start: "<h1>Title</h1>" },
39
53
  });
40
54
  expect(wrapper.find(".page-hero-highlights-header-start").exists()).toBe(true);
@@ -42,10 +56,31 @@ describe("PageHeroHighlightsHeader", () => {
42
56
  });
43
57
 
44
58
  it("applies styleClassPassthrough classes", async () => {
45
- const wrapper = await mountSuspended(PageHeroHighlightsHeader, {
59
+ const wrapper = await mountTracked(PageHeroHighlightsHeader, {
46
60
  props: { styleClassPassthrough: ["extra-class", "another-class"] },
47
61
  });
48
62
  expect(wrapper.find(".page-hero-highlights-header").classes()).toContain("extra-class");
49
63
  expect(wrapper.find(".page-hero-highlights-header").classes()).toContain("another-class");
50
64
  });
51
65
  });
66
+
67
+ describe("PageHeroHighlightsHeader end slot toggled after mount", () => {
68
+ it("adds and removes .page-hero-highlights-header-end with a v-if'd end slot", async () => {
69
+ const Host = defineComponent({
70
+ components: { PageHeroHighlightsHeader },
71
+ props: { showEnd: { type: Boolean, default: false } },
72
+ template: `
73
+ <PageHeroHighlightsHeader>
74
+ <template #start><h1>Title</h1></template>
75
+ <template v-if="showEnd" #end><button class="cta">Go</button></template>
76
+ </PageHeroHighlightsHeader>
77
+ `,
78
+ });
79
+ const wrapper = await mountTracked(Host);
80
+ expect(wrapper.find(".page-hero-highlights-header-end").exists()).toBe(false);
81
+ await wrapper.setProps({ showEnd: true });
82
+ expect(wrapper.find(".page-hero-highlights-header-end .cta").exists()).toBe(true);
83
+ await wrapper.setProps({ showEnd: false });
84
+ expect(wrapper.find(".page-hero-highlights-header-end").exists()).toBe(false);
85
+ });
86
+ });
@@ -2,17 +2,19 @@
2
2
 
3
3
  exports[`PageHeroHighlights > renders correct HTML structure 1`] = `
4
4
  "<div class="page-hero-highlights content-align-center has-content-panel">
5
- <div class="page-hero-highlights-header-row">
6
- <div class="page-hero-highlights-header-slot">
7
- <h1>Page Title</h1>
5
+ <div class="page-hero-highlights-grid">
6
+ <div class="page-hero-highlights-header-row">
7
+ <div class="page-hero-highlights-header-slot">
8
+ <h1>Page Title</h1>
9
+ </div>
8
10
  </div>
9
- </div>
10
- <div class="page-hero-highlights-strip flexible-widths justify-start">
11
- <div class="highlight">Highlight 1</div>
12
- </div>
13
- <div class="page-hero-highlights-content-row">
14
- <div class="page-hero-highlights-content-slot">
15
- <p>Content</p>
11
+ <div class="page-hero-highlights-strip flexible-widths justify-start">
12
+ <div class="highlight">Highlight 1</div>
13
+ </div>
14
+ <div class="page-hero-highlights-content-row">
15
+ <div class="page-hero-highlights-content-slot">
16
+ <p>Content</p>
17
+ </div>
16
18
  </div>
17
19
  </div>
18
20
  </div>"
@@ -1,3 +1,5 @@
1
+ import type { VNode } from "vue";
2
+
1
3
  export interface ISocialIcon {
2
4
  networkName: string;
3
5
  iconName: string;
@@ -5,4 +7,11 @@ export interface ISocialIcon {
5
7
  profileId: string;
6
8
  /** Accessible name for the link; overrides the list's linkLabelTemplate. */
7
9
  label?: string;
10
+ /** Brand colour, exposed as --social-icons-list-brand-colour on this item for recipes to use. */
11
+ brandColour?: string;
12
+ }
13
+
14
+ export interface SocialIconsListSlots {
15
+ icon?(props: { item: ISocialIcon }): VNode[];
16
+ label?(props: { item: ISocialIcon }): VNode[];
8
17
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.6.5",
4
+ "version": "9.6.7",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",