srcdev-nuxt-components 9.0.18 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +2 -1
- package/.claude/skills/component-inline-action-button.md +79 -0
- package/.claude/skills/components/input-copy-core.md +66 -0
- package/.claude/skills/components/treatment-consultant.md +128 -0
- package/.claude/skills/icon-sets.md +45 -0
- package/.claude/skills/index.md +7 -1
- package/.claude/skills/performance-review.md +105 -0
- package/.claude/skills/robots-env-aware.md +69 -0
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_error.css +1 -1
- package/app/assets/styles/setup/02.colours/_amber.css +2 -2
- package/app/assets/styles/setup/03.theming/default/_dark.css +20 -2
- package/app/assets/styles/setup/03.theming/default/_light.css +11 -1
- package/app/assets/styles/setup/03.theming/error/_dark.css +1 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +15 -12
- package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +3 -1
- package/app/components/forms/form-errors/InputError.vue +104 -103
- package/app/components/forms/input-copy/InputCopyCore.vue +132 -0
- package/app/components/forms/input-copy/stories/InputCopyCore.stories.ts +89 -0
- package/app/components/forms/input-copy/tests/InputCopyCore.spec.ts +212 -0
- package/app/components/forms/input-copy/tests/__snapshots__/InputCopyCore.spec.ts.snap +28 -0
- package/app/pages/index.vue +0 -5
- package/modules/icon-sets.ts +53 -0
- package/nuxt.config.ts +1 -0
- package/package.json +44 -1
- package/app/components/03.organisms/treatment-consultant/TreatmentConsultant.vue +0 -2204
- package/app/components/03.organisms/treatment-consultant/stories/TreatmentConsultant.stories.ts +0 -38
- package/app/pages/ui/services/treatment-consultant.vue +0 -39
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import InputCopyCore from "../InputCopyCore.vue";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PROPS = {
|
|
6
|
+
id: "copy-input",
|
|
7
|
+
value: "sk_live_abc123def456",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const createWrapper = async (props: Record<string, unknown> = {}) => {
|
|
11
|
+
return mountSuspended(InputCopyCore, {
|
|
12
|
+
props: { ...DEFAULT_PROPS, ...props },
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("InputCopyCore", () => {
|
|
17
|
+
let wrapper: Awaited<ReturnType<typeof createWrapper>>;
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
wrapper?.unmount();
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe("Snapshots", () => {
|
|
27
|
+
it("default", async () => {
|
|
28
|
+
wrapper = await createWrapper();
|
|
29
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("custom labels", async () => {
|
|
33
|
+
wrapper = await createWrapper({ copyLabel: "Copy key", copiedLabel: "Key copied!" });
|
|
34
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("with styleClassPassthrough", async () => {
|
|
38
|
+
wrapper = await createWrapper({ styleClassPassthrough: ["my-copy-input"] });
|
|
39
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ─── Rendering ───────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe("Rendering", () => {
|
|
46
|
+
it("mounts without error", async () => {
|
|
47
|
+
wrapper = await createWrapper();
|
|
48
|
+
expect(wrapper.vm).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("root element has input-copy-core class", async () => {
|
|
52
|
+
wrapper = await createWrapper();
|
|
53
|
+
expect(wrapper.classes()).toContain("input-copy-core");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("root element has data-testid attribute", async () => {
|
|
57
|
+
wrapper = await createWrapper();
|
|
58
|
+
expect(wrapper.attributes("data-testid")).toBe("input-copy-core");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("renders a readonly input", async () => {
|
|
62
|
+
wrapper = await createWrapper();
|
|
63
|
+
const input = wrapper.find(".input-copy-field");
|
|
64
|
+
expect(input.exists()).toBe(true);
|
|
65
|
+
expect(input.attributes("readonly")).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("input has aria-readonly set to true", async () => {
|
|
69
|
+
wrapper = await createWrapper();
|
|
70
|
+
expect(wrapper.find(".input-copy-field").attributes("aria-readonly")).toBe("true");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("input displays the value", async () => {
|
|
74
|
+
wrapper = await createWrapper({ value: "my-license-key" });
|
|
75
|
+
expect(wrapper.find(".input-copy-field").attributes("value")).toBe("my-license-key");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("input has the correct id", async () => {
|
|
79
|
+
wrapper = await createWrapper({ id: "my-input" });
|
|
80
|
+
expect(wrapper.find(".input-copy-field").attributes("id")).toBe("my-input");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("renders the copy button", async () => {
|
|
84
|
+
wrapper = await createWrapper();
|
|
85
|
+
expect(wrapper.find(".input-copy-button").exists()).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("copy button shows copyLabel by default", async () => {
|
|
89
|
+
wrapper = await createWrapper();
|
|
90
|
+
expect(wrapper.find(".input-copy-button").text()).toBe("Copy");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("copy button shows custom copyLabel", async () => {
|
|
94
|
+
wrapper = await createWrapper({ copyLabel: "Copy key" });
|
|
95
|
+
expect(wrapper.find(".input-copy-button").text()).toBe("Copy key");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ─── Props ───────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe("Props", () => {
|
|
102
|
+
it("styleClassPassthrough string is applied to root", async () => {
|
|
103
|
+
wrapper = await createWrapper({ styleClassPassthrough: "my-class" });
|
|
104
|
+
expect(wrapper.classes()).toContain("my-class");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("styleClassPassthrough array is applied to root", async () => {
|
|
108
|
+
wrapper = await createWrapper({ styleClassPassthrough: ["class-a", "class-b"] });
|
|
109
|
+
expect(wrapper.classes()).toContain("class-a");
|
|
110
|
+
expect(wrapper.classes()).toContain("class-b");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
114
|
+
wrapper = await createWrapper({ styleClassPassthrough: ["original"] });
|
|
115
|
+
expect(wrapper.classes()).toContain("original");
|
|
116
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
117
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
118
|
+
expect(wrapper.classes()).toContain("updated");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ─── Copy behaviour ──────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
describe("Copy behaviour", () => {
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
vi.stubGlobal("navigator", {
|
|
127
|
+
clipboard: {
|
|
128
|
+
writeText: vi.fn().mockResolvedValue(undefined),
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("calls navigator.clipboard.writeText with the value on click", async () => {
|
|
134
|
+
wrapper = await createWrapper({ value: "test-key-123" });
|
|
135
|
+
await wrapper.find(".input-copy-button").trigger("click");
|
|
136
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("test-key-123");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("adds copied class to root after clicking", async () => {
|
|
140
|
+
wrapper = await createWrapper();
|
|
141
|
+
await wrapper.find(".input-copy-button").trigger("click");
|
|
142
|
+
await nextTick();
|
|
143
|
+
expect(wrapper.classes()).toContain("copied");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("button shows copiedLabel after clicking", async () => {
|
|
147
|
+
wrapper = await createWrapper({ copiedLabel: "Done!" });
|
|
148
|
+
await wrapper.find(".input-copy-button").trigger("click");
|
|
149
|
+
await nextTick();
|
|
150
|
+
expect(wrapper.find(".input-copy-button").text()).toBe("Done!");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("button aria-label updates to copiedLabel after clicking", async () => {
|
|
154
|
+
wrapper = await createWrapper({ copiedLabel: "Copied!" });
|
|
155
|
+
await wrapper.find(".input-copy-button").trigger("click");
|
|
156
|
+
await nextTick();
|
|
157
|
+
expect(wrapper.find(".input-copy-button").attributes("aria-label")).toBe("Copied!");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("emits copy event with value on successful copy", async () => {
|
|
161
|
+
wrapper = await createWrapper({ value: "emit-test-key" });
|
|
162
|
+
await wrapper.find(".input-copy-button").trigger("click");
|
|
163
|
+
expect(wrapper.emitted("copy")).toBeTruthy();
|
|
164
|
+
expect(wrapper.emitted("copy")?.[0]).toEqual(["emit-test-key"]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("removes copied class after feedbackDuration elapses", async () => {
|
|
168
|
+
wrapper = await createWrapper({ feedbackDuration: 1000 });
|
|
169
|
+
await wrapper.find(".input-copy-button").trigger("click");
|
|
170
|
+
await nextTick();
|
|
171
|
+
expect(wrapper.classes()).toContain("copied");
|
|
172
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
173
|
+
expect(wrapper.classes()).not.toContain("copied");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("does not throw when clipboard is unavailable", async () => {
|
|
177
|
+
vi.stubGlobal("navigator", {
|
|
178
|
+
clipboard: {
|
|
179
|
+
writeText: vi.fn().mockRejectedValue(new Error("Not allowed")),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
wrapper = await createWrapper();
|
|
183
|
+
await expect(wrapper.find(".input-copy-button").trigger("click")).resolves.not.toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("does not add copied class when clipboard fails", async () => {
|
|
187
|
+
vi.stubGlobal("navigator", {
|
|
188
|
+
clipboard: {
|
|
189
|
+
writeText: vi.fn().mockRejectedValue(new Error("Not allowed")),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
wrapper = await createWrapper();
|
|
193
|
+
await wrapper.find(".input-copy-button").trigger("click");
|
|
194
|
+
await nextTick();
|
|
195
|
+
expect(wrapper.classes()).not.toContain("copied");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ─── Accessibility ───────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
describe("Accessibility", () => {
|
|
202
|
+
it("copy button has aria-label matching copyLabel by default", async () => {
|
|
203
|
+
wrapper = await createWrapper({ copyLabel: "Copy" });
|
|
204
|
+
expect(wrapper.find(".input-copy-button").attributes("aria-label")).toBe("Copy");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("copy button type is button", async () => {
|
|
208
|
+
wrapper = await createWrapper();
|
|
209
|
+
expect(wrapper.find(".input-copy-button").attributes("type")).toBe("button");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`InputCopyCore > Snapshots > custom labels 1`] = `
|
|
4
|
+
"<div class="input-copy-core" data-testid="input-copy-core"><input id="copy-input" type="text" readonly="" aria-readonly="true" class="input-copy-field" value="sk_live_abc123def456"><button type="button" aria-disabled="false" data-testid="input-button-core" data-theme="default" class="input-button-core inline input-copy-button" aria-label="Copy key">
|
|
5
|
+
<!--v-if-->
|
|
6
|
+
<!--v-if--><span class="button-text">Copy key</span>
|
|
7
|
+
<!--v-if-->
|
|
8
|
+
<!--v-if-->
|
|
9
|
+
</button></div>"
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
exports[`InputCopyCore > Snapshots > default 1`] = `
|
|
13
|
+
"<div class="input-copy-core" data-testid="input-copy-core"><input id="copy-input" type="text" readonly="" aria-readonly="true" class="input-copy-field" value="sk_live_abc123def456"><button type="button" aria-disabled="false" data-testid="input-button-core" data-theme="default" class="input-button-core inline input-copy-button" aria-label="Copy">
|
|
14
|
+
<!--v-if-->
|
|
15
|
+
<!--v-if--><span class="button-text">Copy</span>
|
|
16
|
+
<!--v-if-->
|
|
17
|
+
<!--v-if-->
|
|
18
|
+
</button></div>"
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
exports[`InputCopyCore > Snapshots > with styleClassPassthrough 1`] = `
|
|
22
|
+
"<div class="input-copy-core my-copy-input" data-testid="input-copy-core"><input id="copy-input" type="text" readonly="" aria-readonly="true" class="input-copy-field" value="sk_live_abc123def456"><button type="button" aria-disabled="false" data-testid="input-button-core" data-theme="default" class="input-button-core inline input-copy-button" aria-label="Copy">
|
|
23
|
+
<!--v-if-->
|
|
24
|
+
<!--v-if--><span class="button-text">Copy</span>
|
|
25
|
+
<!--v-if-->
|
|
26
|
+
<!--v-if-->
|
|
27
|
+
</button></div>"
|
|
28
|
+
`;
|
package/app/pages/index.vue
CHANGED
|
@@ -31,11 +31,6 @@
|
|
|
31
31
|
<NuxtLink class="page-body-normal" to="/ui/contact-section">Contact Section</NuxtLink>
|
|
32
32
|
</LayoutRow>
|
|
33
33
|
|
|
34
|
-
<LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
|
|
35
|
-
<h1 class="page-heading-1">Treatment Consultant</h1>
|
|
36
|
-
<NuxtLink class="page-body-normal" to="/ui/services/treatment-consultant">Treatment Consultant</NuxtLink>
|
|
37
|
-
</LayoutRow>
|
|
38
|
-
|
|
39
34
|
<LayoutRow tag="div" variant="inset-content" :style-class-passthrough="['mbe-20']">
|
|
40
35
|
<h2 class="page-heading-2">PopOver component 1</h2>
|
|
41
36
|
<ClientOnly>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineNuxtModule, logger } from "@nuxt/kit";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Icon sets used by srcdev-nuxt-components components.
|
|
7
|
+
* These must be installed in the consumer app to avoid runtime CDN fetches (FOUC).
|
|
8
|
+
*/
|
|
9
|
+
const ICON_SETS = [
|
|
10
|
+
"@iconify-json/akar-icons",
|
|
11
|
+
"@iconify-json/bi",
|
|
12
|
+
"@iconify-json/bitcoin-icons",
|
|
13
|
+
"@iconify-json/gravity-ui",
|
|
14
|
+
"@iconify-json/ic",
|
|
15
|
+
"@iconify-json/lucide",
|
|
16
|
+
"@iconify-json/material-symbols",
|
|
17
|
+
"@iconify-json/mdi",
|
|
18
|
+
"@iconify-json/radix-icons",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export default defineNuxtModule({
|
|
22
|
+
meta: {
|
|
23
|
+
name: "srcdev-icon-sets",
|
|
24
|
+
},
|
|
25
|
+
setup(_, nuxt) {
|
|
26
|
+
// Skip when running as the layer's own standalone dev/build
|
|
27
|
+
if (process.env.SRCDEV_STANDALONE) return;
|
|
28
|
+
|
|
29
|
+
// Resolve packages from the consumer's project root, not the layer's own node_modules
|
|
30
|
+
const consumerRequire = createRequire(pathToFileURL(nuxt.options.rootDir + "/").href);
|
|
31
|
+
|
|
32
|
+
const missing = ICON_SETS.filter((pkg) => {
|
|
33
|
+
try {
|
|
34
|
+
consumerRequire.resolve(pkg);
|
|
35
|
+
return false;
|
|
36
|
+
} catch {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
logger.info(
|
|
43
|
+
`[srcdev-nuxt-components] Some icon set packages used by layer components are not installed in this project.\n` +
|
|
44
|
+
`If you use those components and see icons flashing in on page load, install the relevant packages.\n\n` +
|
|
45
|
+
`Not installed: ${missing.join(", ")}\n\n` +
|
|
46
|
+
`Install all at once:\n\n` +
|
|
47
|
+
` npm install ${missing.join(" ")}\n\n` +
|
|
48
|
+
`Your own app's icon sets are unaffected — this only covers sets used by layer components.\n` +
|
|
49
|
+
`See .claude/skills/srcdev-nuxt-components/skills/icon-sets.md for the component→package mapping.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
package/nuxt.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "srcdev-nuxt-components",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "9.0
|
|
4
|
+
"version": "9.1.0",
|
|
5
5
|
"main": "nuxt.config.ts",
|
|
6
6
|
"types": "types.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -39,14 +39,57 @@
|
|
|
39
39
|
"nuxt.config.ts",
|
|
40
40
|
"types.d.ts"
|
|
41
41
|
],
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@iconify-json/akar-icons": "*",
|
|
44
|
+
"@iconify-json/bi": "*",
|
|
45
|
+
"@iconify-json/bitcoin-icons": "*",
|
|
46
|
+
"@iconify-json/gravity-ui": "*",
|
|
47
|
+
"@iconify-json/ic": "*",
|
|
48
|
+
"@iconify-json/lucide": "*",
|
|
49
|
+
"@iconify-json/material-symbols": "*",
|
|
50
|
+
"@iconify-json/mdi": "*",
|
|
51
|
+
"@iconify-json/radix-icons": "*"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@iconify-json/akar-icons": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@iconify-json/bi": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"@iconify-json/bitcoin-icons": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@iconify-json/gravity-ui": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"@iconify-json/ic": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@iconify-json/lucide": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@iconify-json/material-symbols": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@iconify-json/mdi": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"@iconify-json/radix-icons": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
42
82
|
"devDependencies": {
|
|
43
83
|
"@chromatic-com/storybook": "4.1.2",
|
|
44
84
|
"@iconify-json/akar-icons": "1.2.7",
|
|
45
85
|
"@iconify-json/bi": "1.2.7",
|
|
46
86
|
"@iconify-json/bitcoin-icons": "1.2.4",
|
|
47
87
|
"@iconify-json/gravity-ui": "1.2.11",
|
|
88
|
+
"@iconify-json/ic": "*",
|
|
89
|
+
"@iconify-json/lucide": "*",
|
|
48
90
|
"@iconify-json/material-symbols": "1.2.53",
|
|
49
91
|
"@iconify-json/mdi": "1.2.3",
|
|
92
|
+
"@iconify-json/radix-icons": "*",
|
|
50
93
|
"@nuxt/eslint": "1.13.0",
|
|
51
94
|
"@nuxt/test-utils": "3.23.0",
|
|
52
95
|
"@nuxtjs/storybook": "9.1.0-29374011.dab79ae",
|