srcdev-nuxt-components 9.6.2 → 9.6.4

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 (58) hide show
  1. package/.claude/rules/consumer/consumer-overrides.md +17 -0
  2. package/.claude/rules/consumer/css-comment-slash.md +13 -0
  3. package/.claude/rules/consumer/css-native-nesting.md +13 -0
  4. package/.claude/rules/consumer/css-no-deep.md +13 -0
  5. package/.claude/rules/consumer/css-rem-base.md +11 -0
  6. package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
  7. package/.claude/rules/consumer/css-webkit-support.md +15 -0
  8. package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
  9. package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
  10. package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
  11. package/.claude/rules/consumer/story-args.md +12 -0
  12. package/.claude/rules/consumer/story-register-components.md +12 -0
  13. package/.claude/rules/consumer/vue-auto-imports.md +14 -0
  14. package/.claude/rules/consumer/vue-no-self-closing.md +12 -0
  15. package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
  16. package/.claude/rules/consumer/vue-props-pattern.md +13 -0
  17. package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
  18. package/.claude/rules/consumer/vue-video-source.md +12 -0
  19. package/.claude/skills/component-compliance-checklist.md +31 -3
  20. package/.claude/skills/component-inline-action-button.md +33 -2
  21. package/.claude/skills/component-naming.md +1 -1
  22. package/.claude/skills/components/cookie-consent-banner.md +9 -6
  23. package/.claude/skills/components/input-copy.md +47 -100
  24. package/.claude/skills/components/pricing-card.md +68 -84
  25. package/.claude/skills/composable-cookie-consent.md +1 -1
  26. package/.claude/skills/index.md +11 -5
  27. package/.claude/skills/new-app-scaffold.md +4 -2
  28. package/.claude/skills/setup-postinstall.md +5 -4
  29. package/.claude/skills/storybook-add-story.md +44 -0
  30. package/.claude/skills/testing-add-unit-test.md +19 -0
  31. package/.claude/skills/theming-component-token-pattern.md +47 -1
  32. package/.claude/skills/theming-partial-override.md +1 -1
  33. package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
  34. package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
  35. package/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
  36. package/README.md +5 -5
  37. package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
  38. package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
  39. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
  40. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
  41. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
  42. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
  43. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +108 -5
  44. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
  45. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
  46. package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
  47. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -48
  48. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
  49. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
  50. package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
  51. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
  52. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
  53. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
  54. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
  55. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
  56. package/app/composables/useAriaDescribedById.ts +1 -1
  57. package/app/types/components/cookie-consent-banner.d.ts +2 -0
  58. package/package.json +2 -1
@@ -1,222 +1,198 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { mount } from "@vue/test-utils";
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { nextTick } from "vue";
3
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
4
+ import type { VueWrapper } from "@vue/test-utils";
3
5
  import InputCopy from "../InputCopy.vue";
4
6
 
7
+ let wrapper: VueWrapper | undefined;
8
+ let writeText: ReturnType<typeof vi.fn>;
9
+
10
+ async function mountCopy(props: Record<string, unknown> = {}) {
11
+ wrapper = await mountSuspended(InputCopy, { props: { value: "test-key-123", ...props } });
12
+ return wrapper;
13
+ }
14
+
15
+ async function clickCopy(w: VueWrapper) {
16
+ await w.find("button").trigger("click");
17
+ await nextTick();
18
+ await nextTick();
19
+ }
20
+
5
21
  describe("InputCopy", () => {
6
22
  beforeEach(() => {
7
- vi.clearAllMocks();
23
+ writeText = vi.fn(() => Promise.resolve());
24
+ Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true });
8
25
  });
9
26
 
10
- it("renders readonly input with value", () => {
11
- const wrapper = mount(InputCopy, {
12
- props: {
13
- value: "test-key-123",
14
- label: "Test key",
15
- },
16
- });
17
-
18
- const input = wrapper.find("input");
19
- expect(input.element.value).toBe("test-key-123");
20
- expect(input.element.readOnly).toBe(true);
27
+ afterEach(() => {
28
+ wrapper?.unmount();
29
+ wrapper = undefined;
30
+ vi.restoreAllMocks();
21
31
  });
22
32
 
23
- it("displays description when provided", () => {
24
- const wrapper = mount(InputCopy, {
25
- props: {
26
- value: "test-key",
27
- description: "This is your license key.",
28
- },
33
+ describe("rendering", () => {
34
+ it("renders a readonly input with the value", async () => {
35
+ const w = await mountCopy();
36
+ const input = w.find("input").element as HTMLInputElement;
37
+ expect(input.value).toBe("test-key-123");
38
+ expect(input.readOnly).toBe(true);
29
39
  });
30
40
 
31
- expect(wrapper.text()).toContain("This is your license key.");
32
- });
33
-
34
- it("does not display description when not provided", () => {
35
- const wrapper = mount(InputCopy, {
36
- props: {
37
- value: "test-key",
38
- description: undefined,
39
- },
41
+ it("renders the root class and styleClassPassthrough classes", async () => {
42
+ const w = await mountCopy({ styleClassPassthrough: ["custom-class", "another-class"] });
43
+ expect(w.classes()).toContain("input-copy");
44
+ expect(w.classes()).toContain("custom-class");
45
+ expect(w.classes()).toContain("another-class");
40
46
  });
41
47
 
42
- const description = wrapper.find(".input-copy__description");
43
- expect(description.exists()).toBe(false);
44
- });
45
-
46
- it("renders copy button with default text", () => {
47
- const wrapper = mount(InputCopy, {
48
- props: {
49
- value: "test-key",
50
- buttonText: "Copy",
51
- },
48
+ it("updates passthrough classes when the prop changes", async () => {
49
+ const w = await mountCopy({ styleClassPassthrough: ["first"] });
50
+ await w.setProps({ styleClassPassthrough: ["second"] });
51
+ expect(w.classes()).toContain("second");
52
+ expect(w.classes()).not.toContain("first");
52
53
  });
53
54
 
54
- const button = wrapper.find("button");
55
- expect(button.text()).toContain("Copy");
56
- });
57
-
58
- it("renders copy button with custom text", () => {
59
- const wrapper = mount(InputCopy, {
60
- props: {
61
- value: "test-key",
62
- buttonText: "Copy to clipboard",
63
- },
55
+ it("renders the copy button as an inline InputButton", async () => {
56
+ const w = await mountCopy();
57
+ const button = w.find("button");
58
+ expect(button.classes()).toContain("input-button");
59
+ expect(button.classes()).toContain("inline");
60
+ expect(button.classes()).toContain("input-copy__button");
61
+ expect(button.attributes("type")).toBe("button");
64
62
  });
65
63
 
66
- const button = wrapper.find("button");
67
- expect(button.text()).toContain("Copy to clipboard");
68
- });
69
-
70
- it("disables button when isDisabled is true", () => {
71
- const wrapper = mount(InputCopy, {
72
- props: {
73
- value: "test-key",
74
- isDisabled: true,
75
- },
64
+ it("renders default and custom button text", async () => {
65
+ const w = await mountCopy();
66
+ expect(w.find("button").text()).toContain("Copy");
67
+ await w.setProps({ buttonText: "Copy to clipboard" });
68
+ expect(w.find("button").text()).toContain("Copy to clipboard");
76
69
  });
77
70
 
78
- const button = wrapper.find("button");
79
- expect(button.attributes("disabled")).toBeDefined();
80
- });
81
-
82
- it("button is not disabled when isDisabled is false", () => {
83
- const wrapper = mount(InputCopy, {
84
- props: {
85
- value: "test-key",
86
- isDisabled: false,
87
- },
71
+ it("renders the description only when provided", async () => {
72
+ const w = await mountCopy();
73
+ expect(w.find(".input-copy__description").exists()).toBe(false);
74
+ await w.setProps({ description: "This is your license key." });
75
+ expect(w.find(".input-copy__description").text()).toBe("This is your license key.");
88
76
  });
89
77
 
90
- const button = wrapper.find("button");
91
- expect(button.attributes("disabled")).toBeUndefined();
92
- });
93
-
94
- it("has proper aria-label on input from label prop", () => {
95
- const wrapper = mount(InputCopy, {
96
- props: {
97
- value: "test-key",
98
- label: "License key",
99
- },
78
+ it("renders the icon in the button", async () => {
79
+ const w = await mountCopy();
80
+ expect(w.find("button .input-copy__icon").exists()).toBe(true);
100
81
  });
101
-
102
- const input = wrapper.find("input");
103
- expect(input.attributes("aria-label")).toBe("License key");
104
82
  });
105
83
 
106
- it("has proper aria-label on input from ariaLabel prop (takes precedence)", () => {
107
- const wrapper = mount(InputCopy, {
108
- props: {
109
- value: "test-key",
110
- label: "License key",
111
- ariaLabel: "Unique license identifier",
112
- },
84
+ describe("id and name", () => {
85
+ it("auto-generates an id and mirrors it as the name", async () => {
86
+ const w = await mountCopy();
87
+ const input = w.find("input");
88
+ expect(input.attributes("id")).toBeTruthy();
89
+ expect(input.attributes("name")).toBe(input.attributes("id"));
113
90
  });
114
91
 
115
- const input = wrapper.find("input");
116
- expect(input.attributes("aria-label")).toBe("Unique license identifier");
117
- });
118
-
119
- it("has proper aria-label on button", () => {
120
- const wrapper = mount(InputCopy, {
121
- props: {
122
- value: "test-key",
123
- label: "API token",
124
- },
92
+ it("uses the provided id, and name over the id", async () => {
93
+ const w = await mountCopy({ id: "license-key-input" });
94
+ expect(w.find("input").attributes("id")).toBe("license-key-input");
95
+ expect(w.find("input").attributes("name")).toBe("license-key-input");
96
+ await w.setProps({ name: "licenseKey" });
97
+ expect(w.find("input").attributes("name")).toBe("licenseKey");
125
98
  });
126
-
127
- const button = wrapper.find("button");
128
- expect(button.attributes("aria-label")).toContain("Copy");
129
- expect(button.attributes("aria-label")).toContain("API token");
130
99
  });
131
100
 
132
- it("has icon element in button", () => {
133
- const wrapper = mount(InputCopy, {
134
- props: {
135
- value: "test-key",
136
- },
101
+ describe("accessibility", () => {
102
+ it("labels the input from label, with ariaLabel taking precedence", async () => {
103
+ const w = await mountCopy({ label: "License key" });
104
+ expect(w.find("input").attributes("aria-label")).toBe("License key");
105
+ await w.setProps({ ariaLabel: "Unique license identifier" });
106
+ expect(w.find("input").attributes("aria-label")).toBe("Unique license identifier");
137
107
  });
138
108
 
139
- const icon = wrapper.find(".input-copy__icon");
140
- expect(icon.exists()).toBe(true);
141
- });
142
-
143
- it("input wrapper contains both input and button", () => {
144
- const wrapper = mount(InputCopy, {
145
- props: {
146
- value: "test-key",
147
- },
109
+ it("builds a default button aria-label from the label", async () => {
110
+ const w = await mountCopy({ label: "API token" });
111
+ expect(w.find("button").attributes("aria-label")).toBe("Copy API token to clipboard");
148
112
  });
149
113
 
150
- const wrapper_el = wrapper.find(".input-copy__wrapper");
151
- expect(wrapper_el.exists()).toBe(true);
152
-
153
- const input = wrapper_el.find("input");
154
- const button = wrapper_el.find("button");
114
+ it("uses buttonAriaLabel when provided", async () => {
115
+ const w = await mountCopy({ label: "API token", buttonAriaLabel: "API-Token kopieren" });
116
+ expect(w.find("button").attributes("aria-label")).toBe("API-Token kopieren");
117
+ });
155
118
 
156
- expect(input.exists()).toBe(true);
157
- expect(button.exists()).toBe(true);
158
- });
119
+ it("links the description to the input with aria-describedby", async () => {
120
+ const w = await mountCopy({ id: "key", description: "Keep it safe." });
121
+ expect(w.find("input").attributes("aria-describedby")).toBe("key-description");
122
+ expect(w.find("#key-description").text()).toBe("Keep it safe.");
123
+ });
159
124
 
160
- it("renders with correct component classes", () => {
161
- const wrapper = mount(InputCopy, {
162
- props: {
163
- value: "test-key",
164
- },
125
+ it("has no aria-describedby without a description", async () => {
126
+ const w = await mountCopy();
127
+ expect(w.find("input").attributes("aria-describedby")).toBeUndefined();
165
128
  });
166
129
 
167
- expect(wrapper.classes()).toContain("input-copy");
130
+ it("has an empty status region until a copy succeeds", async () => {
131
+ const w = await mountCopy();
132
+ expect(w.find("[role='status']").text()).toBe("");
133
+ });
168
134
  });
169
135
 
170
- it("applies styleClassPassthrough classes", () => {
171
- const wrapper = mount(InputCopy, {
172
- props: {
173
- value: "test-key",
174
- styleClassPassthrough: ["custom-class", "another-class"],
175
- },
136
+ describe("disabled", () => {
137
+ it("disables the button when isDisabled is true", async () => {
138
+ const w = await mountCopy({ isDisabled: true });
139
+ expect(w.find("button").attributes("disabled")).toBeDefined();
176
140
  });
177
141
 
178
- expect(wrapper.classes()).toContain("custom-class");
179
- expect(wrapper.classes()).toContain("another-class");
142
+ it("leaves the button enabled by default", async () => {
143
+ const w = await mountCopy();
144
+ expect(w.find("button").attributes("disabled")).toBeUndefined();
145
+ });
180
146
  });
181
147
 
182
- it("auto-generates an id and mirrors it as the name when neither is provided", () => {
183
- const wrapper = mount(InputCopy, {
184
- props: {
185
- value: "test-key",
186
- },
148
+ describe("copying", () => {
149
+ it("writes the value to the clipboard and emits copy and copied", async () => {
150
+ const w = await mountCopy();
151
+ await clickCopy(w);
152
+ expect(writeText).toHaveBeenCalledWith("test-key-123");
153
+ expect(w.emitted("copy")).toEqual([["test-key-123"]]);
154
+ expect(w.emitted("copied")).toEqual([["test-key-123"]]);
187
155
  });
188
156
 
189
- const input = wrapper.find("input");
190
- const id = input.attributes("id");
191
-
192
- expect(id).toBeTruthy();
193
- expect(input.attributes("name")).toBe(id);
194
- });
195
-
196
- it("uses the provided id prop instead of auto-generating one", () => {
197
- const wrapper = mount(InputCopy, {
198
- props: {
199
- value: "test-key",
200
- id: "license-key-input",
201
- },
157
+ it("shows copiedText, announces it and adds the copied class", async () => {
158
+ const w = await mountCopy({ copiedText: "Kopiert!" });
159
+ await clickCopy(w);
160
+ expect(w.find("button").text()).toContain("Kopiert!");
161
+ expect(w.find("[role='status']").text()).toBe("Kopiert!");
162
+ expect(w.classes()).toContain("input-copy-copied");
202
163
  });
203
164
 
204
- const input = wrapper.find("input");
205
- expect(input.attributes("id")).toBe("license-key-input");
206
- expect(input.attributes("name")).toBe("license-key-input");
207
- });
165
+ it("keeps the button focusable but aria-disabled while copied, ignoring repeat clicks", async () => {
166
+ const w = await mountCopy();
167
+ await clickCopy(w);
168
+ const button = w.find("button");
169
+ expect(button.attributes("disabled")).toBeUndefined();
170
+ expect(button.attributes("aria-disabled")).toBe("true");
171
+ await clickCopy(w);
172
+ expect(writeText).toHaveBeenCalledOnce();
173
+ });
208
174
 
209
- it("uses the provided name prop instead of falling back to the id", () => {
210
- const wrapper = mount(InputCopy, {
211
- props: {
212
- value: "test-key",
213
- id: "license-key-input",
214
- name: "licenseKey",
215
- },
175
+ it("resets after copiedDuration", async () => {
176
+ const w = await mountCopy({ copiedDuration: 1000 });
177
+ await clickCopy(w);
178
+ vi.advanceTimersByTime(999);
179
+ await nextTick();
180
+ expect(w.classes()).toContain("input-copy-copied");
181
+ vi.advanceTimersByTime(1);
182
+ await nextTick();
183
+ expect(w.classes()).not.toContain("input-copy-copied");
184
+ expect(w.find("button").text()).toContain("Copy");
185
+ expect(w.find("button").attributes("aria-disabled")).toBe("false");
216
186
  });
217
187
 
218
- const input = wrapper.find("input");
219
- expect(input.attributes("id")).toBe("license-key-input");
220
- expect(input.attributes("name")).toBe("licenseKey");
188
+ it("logs and stays in the resting state when the clipboard write fails", async () => {
189
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
190
+ writeText.mockImplementation(() => Promise.reject(new Error("denied")));
191
+ const w = await mountCopy();
192
+ await clickCopy(w);
193
+ expect(errorSpy).toHaveBeenCalled();
194
+ expect(w.emitted("copied")).toBeUndefined();
195
+ expect(w.classes()).not.toContain("input-copy-copied");
196
+ });
221
197
  });
222
198
  });