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,23 +1,79 @@
1
+ import { computed, ref } from "vue";
1
2
  import type { Meta, StoryObj } from "@nuxtjs/storybook";
2
3
  import InputCopy from "../InputCopy.vue";
4
+ import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
5
+ import type { MediaCanvas } from "~/types/components";
3
6
 
4
- const meta: Meta<typeof InputCopy> = {
7
+ type LineClamp = "none" | "1" | "2" | "3" | "4";
8
+
9
+ // Story-only arg: drives a CSS token, not an InputCopy prop. See its argTypes entry.
10
+ type StoryArgs = InstanceType<typeof InputCopy>["$props"] & {
11
+ descriptionLineClamp: LineClamp;
12
+ };
13
+
14
+ const meta: Meta<StoryArgs> = {
5
15
  title: "Molecules/Input Copy",
6
16
  component: InputCopy,
7
- tags: ["autodocs"],
8
17
  argTypes: {
9
- value: { control: "text" },
18
+ value: { control: "text", description: "The text copied to the clipboard", table: { category: "Content" } },
19
+ label: {
20
+ control: "text",
21
+ description: "Accessible name for the input (not rendered visibly) and the default button aria-label",
22
+ table: { category: "Content" },
23
+ },
24
+ description: {
25
+ control: "text",
26
+ description: "Helper text below the input, linked to it with aria-describedby",
27
+ table: { category: "Content" },
28
+ },
29
+ buttonText: { control: "text", description: "Button label", table: { category: "Content" } },
30
+ copiedText: {
31
+ control: "text",
32
+ description: "Button label and status announcement after a successful copy",
33
+ table: { category: "Content" },
34
+ },
35
+ ariaLabel: {
36
+ control: "text",
37
+ description: "Accessible name for the input; overrides label",
38
+ table: { category: "Accessibility" },
39
+ },
40
+ buttonAriaLabel: {
41
+ control: "text",
42
+ description: "Accessible name for the button. Defaults to `Copy <label> to clipboard`; pass translated copy in other languages.",
43
+ table: { category: "Accessibility" },
44
+ },
45
+ icon: { control: "text", description: "Iconify name for the resting icon", table: { category: "Appearance" } },
46
+ copiedIcon: {
47
+ control: "text",
48
+ description: "Iconify name shown after a successful copy",
49
+ table: { category: "Appearance" },
50
+ },
51
+ copiedDuration: {
52
+ control: "number",
53
+ description: "Milliseconds the copied state lasts",
54
+ table: { category: "Behaviour" },
55
+ },
56
+ isDisabled: { control: "boolean", description: "Disables the copy button", table: { category: "Behaviour" } },
10
57
  id: {
11
58
  control: "text",
12
- description: "Defaults to an auto-generated id — pass one only if something needs to target this input.",
59
+ description: "Defaults to an auto-generated id; pass one only if something needs to target this input.",
60
+ table: { category: "Form" },
61
+ },
62
+ name: { control: "text", description: "Defaults to the resolved id", table: { category: "Form" } },
63
+ styleClassPassthrough: {
64
+ control: "object",
65
+ description: "Extra classes on the root element",
66
+ table: { category: "Styling" },
67
+ },
68
+ descriptionLineClamp: {
69
+ control: "select",
70
+ options: ["none", "1", "2", "3", "4"] satisfies LineClamp[],
71
+ description:
72
+ "**Story control, not a prop.** Sets the `--input-copy-description-line-clamp` CSS token on a wrapper so you can try it here. " +
73
+ "To use it in an app, set the token in your own CSS, e.g. `.account-page { --input-copy-description-line-clamp: 2; }`. " +
74
+ "`1` is single-line ellipsis, `none` (the default) shows everything.",
75
+ table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "none" } },
13
76
  },
14
- name: { control: "text", description: "Defaults to the resolved id if not provided." },
15
- label: { control: "text" },
16
- description: { control: "text" },
17
- buttonText: { control: "text" },
18
- copiedText: { control: "text" },
19
- copiedDuration: { control: "number" },
20
- isDisabled: { control: "boolean" },
21
77
  },
22
78
  args: {
23
79
  value: "sk_live_abc123defgh456xyz789uvw",
@@ -25,59 +81,73 @@ const meta: Meta<typeof InputCopy> = {
25
81
  description: "Your license key is ready to use. Copy it to embed in your application.",
26
82
  buttonText: "Copy",
27
83
  copiedText: "Copied!",
84
+ icon: "lucide:copy",
85
+ copiedIcon: "lucide:check",
28
86
  copiedDuration: 2000,
29
87
  isDisabled: false,
88
+ styleClassPassthrough: [],
89
+ descriptionLineClamp: "none",
30
90
  },
91
+ decorators: [
92
+ (story, context) => ({
93
+ components: { story, CanvasSwitcher },
94
+ setup() {
95
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "tabletCanvas");
96
+ return { canvasName };
97
+ },
98
+ template: `
99
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
100
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
101
+ </div>
102
+ <div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
103
+ <story />
104
+ </div>
105
+ `,
106
+ }),
107
+ ],
108
+ render: (args) => ({
109
+ components: { InputCopy },
110
+ setup() {
111
+ const componentArgs = computed(() => {
112
+ const { descriptionLineClamp: _descriptionLineClamp, ...rest } = args;
113
+ return rest;
114
+ });
115
+ const tokenStyles = computed(() => ({ "--input-copy-description-line-clamp": args.descriptionLineClamp }));
116
+ const log = (event: string) => (value: string) => console.log(`${event}: ${value}`);
117
+ return { componentArgs, tokenStyles, log };
118
+ },
119
+ template: `
120
+ <div :style="tokenStyles">
121
+ <InputCopy v-bind="componentArgs" @copy="log('copy')" @copied="log('copied')" />
122
+ </div>
123
+ `,
124
+ }),
31
125
  parameters: {
126
+ layout: "fullscreen",
32
127
  docs: {
33
128
  description: {
34
129
  component:
35
- "A readonly input field with a copy-to-clipboard button. Displays a value that users can quickly copy to their clipboard. " +
36
- "Shows feedback ('Copied!') for a brief duration after successful copy. " +
37
- "Emits `copy` and `copied` events. Useful for license keys, API tokens, or any copyable identifier.",
130
+ "A readonly input with a copy-to-clipboard button, for license keys, API tokens or any copyable identifier. " +
131
+ "The button is an inline `InputButton`; after a successful copy it shows `copiedText` and `copiedIcon` for `copiedDuration` ms, " +
132
+ "announces `copiedText` through a status region and stays focusable (aria-disabled) so focus isn't lost. " +
133
+ "Below 48rem of its own width the button shows only its icon. Emits `copy` and `copied` after the clipboard write succeeds.",
38
134
  },
39
135
  },
40
136
  },
41
137
  };
42
138
 
43
139
  export default meta;
44
- type Story = StoryObj<typeof meta>;
140
+ type Story = StoryObj<StoryArgs>;
45
141
 
46
- const storyWrapper = (args: Record<string, unknown>) => ({
47
- components: { InputCopy },
48
- setup() {
49
- const handleCopy = (value: string) => {
50
- console.log(`Copied: ${value}`);
51
- };
52
- const handleCopied = (value: string) => {
53
- console.log(`Copied event fired: ${value}`);
54
- };
55
- return { args, handleCopy, handleCopied };
56
- },
57
- template: `
58
- <div style="padding: 2rem; background: #f5f5f5; border-radius: 0.5rem; max-width: 600px;">
59
- <InputCopy v-bind="args" @copy="handleCopy" @copied="handleCopied" />
60
- </div>
61
- `,
62
- });
63
-
64
- export const Default: Story = {
65
- render: (args) => storyWrapper(args),
66
- };
67
-
68
- export const LicenseKey: Story = {
69
- args: {
70
- value: "gmh_lic_single_site_abc123def456ghi789",
71
- label: "License key",
72
- description: "Your GuideMyHair license key. Keep this safe and use it to embed the widget.",
73
- },
74
- };
142
+ export const Default: Story = {};
75
143
 
76
144
  export const APIToken: Story = {
77
145
  args: {
78
146
  value: "example_token_not_real_0123456789abcdef",
79
147
  label: "API token",
80
148
  description: "Your API token for authentication. Do not share this token.",
149
+ buttonText: "Copy token",
150
+ copiedText: "Token copied!",
81
151
  },
82
152
  };
83
153
 
@@ -108,18 +178,104 @@ export const NoDescription: Story = {
108
178
 
109
179
  export const WithExplicitIdAndName: Story = {
110
180
  args: {
111
- value: "sk_live_abc123defgh456xyz789uvw",
112
181
  id: "checkout-license-key",
113
182
  name: "licenseKey",
114
- label: "License key",
115
- description: "Demonstrates passing explicit id/name instead of relying on the auto-generated one.",
183
+ description: "Passes an explicit id and name instead of relying on the auto-generated one.",
116
184
  },
117
185
  };
118
186
 
119
187
  export const LongValue: Story = {
120
188
  args: {
121
- value: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POdHyP_DQkT3WTZhu9qLWSwVe8ZlHfHEPBOzzVI3vFA",
189
+ value:
190
+ "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsZXggTW9yZ2FuIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POdHyP_DQkT3WTZhu9qLWSwVe8ZlHfHEPBOzzVI3vFA",
122
191
  label: "JWT token",
123
192
  description: "Your JWT authentication token. This is a long-lived token used for API access.",
124
193
  },
125
194
  };
195
+
196
+ const stressCases = [
197
+ {
198
+ key: "german",
199
+ value: "Lizenzschlüssel-Einzelplatz-Nordstadt-2026-ABCDEF-123456",
200
+ label: "Lizenzschlüssel",
201
+ buttonText: "In die Zwischenablage kopieren",
202
+ copiedText: "Erfolgreich in die Zwischenablage kopiert!",
203
+ buttonAriaLabel: "Lizenzschlüssel in die Zwischenablage kopieren",
204
+ description:
205
+ "Ihr Lizenzschlüssel ist einsatzbereit. Kopieren Sie ihn und fügen Sie ihn in die Einstellungen Ihrer Anwendung ein, um die Komponente zu aktivieren. Bewahren Sie ihn sicher auf.",
206
+ },
207
+ {
208
+ key: "unbroken",
209
+ value: "x".repeat(240),
210
+ label: "Token",
211
+ buttonText: "Zwischenablagekopiervorgangsbestätigung",
212
+ copiedText: "Kopiert",
213
+ description: "https://example.com/a-very-long-path-segment-without-any-spaces-at-all-in-it/and-another-one-after-it",
214
+ },
215
+ {
216
+ key: "emoji-html",
217
+ value: "🔑 <script>alert('xss')</script>",
218
+ label: "<b>Key</b>",
219
+ buttonText: "📋 <i>Copy</i>",
220
+ copiedText: "✅",
221
+ description: "🙂 <em>HTML-like</em> text must render as text & never as markup.",
222
+ },
223
+ {
224
+ key: "rtl",
225
+ value: "مفتاح-الترخيص-١٢٣٤٥",
226
+ label: "مفتاح الترخيص",
227
+ buttonText: "نسخ",
228
+ copiedText: "تم النسخ!",
229
+ buttonAriaLabel: "نسخ مفتاح الترخيص",
230
+ description: "مفتاح الترخيص جاهز للاستخدام. انسخه وأضفه إلى تطبيقك.",
231
+ dir: "rtl",
232
+ },
233
+ {
234
+ key: "minimal",
235
+ value: "",
236
+ label: "",
237
+ buttonText: "",
238
+ copiedText: "",
239
+ description: "A",
240
+ },
241
+ ];
242
+
243
+ export const StressTest: Story = {
244
+ name: "Stress Test (Worst-Case Data)",
245
+ render: (args) => ({
246
+ components: { InputCopy },
247
+ setup() {
248
+ const tokenStyles = computed(() => ({ "--input-copy-description-line-clamp": args.descriptionLineClamp }));
249
+ return { args, tokenStyles, stressCases };
250
+ },
251
+ template: `
252
+ <div :style="tokenStyles" style="display: grid; gap: 2.4rem;">
253
+ <div v-for="item in stressCases" :key="item.key" :dir="item.dir">
254
+ <InputCopy
255
+ :value="item.value"
256
+ :label="item.label"
257
+ :button-text="item.buttonText"
258
+ :copied-text="item.copiedText"
259
+ :button-aria-label="item.buttonAriaLabel"
260
+ :description="item.description"
261
+ :copied-duration="args.copiedDuration"
262
+ />
263
+ </div>
264
+ <InputCopy value="x" description="Broken icon names: the button must still render and work." icon="not-a-real:icon" copied-icon="also-not:real" />
265
+ </div>
266
+ `,
267
+ }),
268
+ parameters: {
269
+ initialCanvas: "mobileCanvas",
270
+ docs: {
271
+ description: {
272
+ story:
273
+ "Hostile data: long German value, label, button text, copied text and description; a 240-character unbroken value with an unbroken " +
274
+ "button label and a long URL as the description; emoji and HTML-like text (must render as text); right-to-left Arabic; an empty " +
275
+ "value, label and button text; and broken icon names. Check at every CanvasSwitcher width that the input never collapses to " +
276
+ "nothing (the button is capped at half the row and its label wraps), the description wraps instead of overflowing, the button " +
277
+ "drops to icon-only below 48rem, and copying still shows the copied state. Try the description line-clamp control (a CSS token).",
278
+ },
279
+ },
280
+ },
281
+ };