srcdev-nuxt-components 9.6.3 → 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 (22) hide show
  1. package/.claude/rules/consumer/vue-no-self-closing.md +3 -2
  2. package/.claude/skills/component-compliance-checklist.md +8 -2
  3. package/.claude/skills/component-inline-action-button.md +33 -2
  4. package/.claude/skills/components/cookie-consent-banner.md +9 -6
  5. package/.claude/skills/components/input-copy.md +47 -100
  6. package/.claude/skills/composable-cookie-consent.md +1 -1
  7. package/.claude/skills/index.md +2 -2
  8. package/.claude/skills/storybook-add-story.md +8 -0
  9. package/.claude/skills/testing-add-unit-test.md +19 -0
  10. package/.claude/skills/theming-component-token-pattern.md +1 -1
  11. package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
  12. package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
  13. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
  14. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
  15. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +106 -5
  16. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
  17. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
  18. package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
  19. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -47
  20. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
  21. package/app/types/components/cookie-consent-banner.d.ts +2 -0
  22. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="input-copy" :class="elementClasses">
2
+ <div class="input-copy" :class="[{ 'input-copy-copied': copied }, elementClasses]">
3
3
  <div class="input-copy__wrapper">
4
4
  <input
5
5
  :id="resolvedId"
@@ -8,20 +8,26 @@
8
8
  :name="name || resolvedId"
9
9
  :value="value"
10
10
  :aria-label="ariaLabel || label"
11
+ :aria-describedby="description ? descriptionId : undefined"
11
12
  readonly
12
13
  />
13
- <button
14
+ <InputButton
14
15
  type="button"
16
+ variant="inline"
15
17
  class="input-copy__button"
16
- :aria-label="`Copy ${ariaLabel || label || 'value'} to clipboard`"
17
- :disabled="copied || isDisabled"
18
+ :button-text="copied ? copiedText : buttonText"
19
+ :aria-label="resolvedButtonAriaLabel"
20
+ :disabled="isDisabled"
21
+ :readonly="copied"
18
22
  @click="handleCopy"
19
23
  >
20
- <Icon name="lucide:copy" class="input-copy__icon" />
21
- <span class="input-copy__button-text">{{ copied ? copiedText : buttonText }}</span>
22
- </button>
24
+ <template #left>
25
+ <Icon :name="copied ? copiedIcon : icon" class="input-copy__icon" />
26
+ </template>
27
+ </InputButton>
23
28
  </div>
24
- <p v-if="description" class="input-copy__description">{{ description }}</p>
29
+ <p v-if="description" :id="descriptionId" class="input-copy__description">{{ description }}</p>
30
+ <span class="sr-only" role="status">{{ copied ? copiedText : "" }}</span>
25
31
  </div>
26
32
  </template>
27
33
 
@@ -36,6 +42,9 @@ interface Props {
36
42
  description?: string;
37
43
  buttonText?: string;
38
44
  copiedText?: string;
45
+ buttonAriaLabel?: string;
46
+ icon?: string;
47
+ copiedIcon?: string;
39
48
  copiedDuration?: number;
40
49
  isDisabled?: boolean;
41
50
  styleClassPassthrough?: string | string[];
@@ -49,17 +58,23 @@ const props = withDefaults(defineProps<Props>(), {
49
58
  description: undefined,
50
59
  buttonText: "Copy",
51
60
  copiedText: "Copied!",
61
+ buttonAriaLabel: undefined,
62
+ icon: "lucide:copy",
63
+ copiedIcon: "lucide:check",
52
64
  copiedDuration: 2000,
53
65
  isDisabled: false,
54
66
  styleClassPassthrough: () => [],
55
67
  });
56
68
 
57
69
  // Readonly text inputs still need an id/name to satisfy "a form field element should have an
58
- // id or name attribute" — auto-generated so every existing call site (none of which pass one)
59
- // gets a valid, unique id for free. Named resolvedId, not id, so it can't be confused with (or
60
- // silently shadow) the `id` prop it falls back to.
70
+ // id or name attribute", so one is auto-generated when none is passed.
61
71
  const autoId = useId();
62
72
  const resolvedId = computed(() => props.id ?? autoId);
73
+ const descriptionId = computed(() => `${resolvedId.value}-description`);
74
+
75
+ const resolvedButtonAriaLabel = computed(
76
+ () => props.buttonAriaLabel ?? `Copy ${props.ariaLabel || props.label || "value"} to clipboard`
77
+ );
63
78
 
64
79
  const emit = defineEmits<{
65
80
  copy: [value: string];
@@ -95,7 +110,7 @@ watch(
95
110
  () => props.styleClassPassthrough,
96
111
  () => {
97
112
  resetElementClasses(props.styleClassPassthrough);
98
- },
113
+ }
99
114
  );
100
115
 
101
116
  onUnmounted(() => {
@@ -108,95 +123,105 @@ onUnmounted(() => {
108
123
  <style lang="css">
109
124
  @layer components {
110
125
  .input-copy {
111
- --_input-border-radius: var(--input-copy-input-border-radius, 0.4rem);
112
- --_input-font-size: var(--input-copy-input-font-size, 0.95rem);
113
-
114
- --_button-bg-hover: var(
115
- --input-copy-button-bg-hover,
116
- var(--input-button-primary-surface-hover, var(--theme-surface-hover))
117
- );
118
- --_button-text-color: var(
119
- --input-copy-button-text-color,
120
- var(--input-button-primary-text, var(--theme-on-surface))
121
- );
126
+ --_input-font-size: var(--input-copy-input-font-size, var(--step-4));
122
127
 
128
+ container: input-copy / inline-size;
123
129
  display: flex;
124
130
  flex-direction: column;
125
- gap: 0.8rem;
126
- }
131
+ gap: var(--input-copy-gap, 0.8rem);
127
132
 
128
- .input-copy__wrapper {
129
- display: flex;
130
- gap: 0.8rem;
131
- align-items: stretch;
132
- }
133
+ .input-copy__wrapper {
134
+ display: flex;
135
+ gap: var(--input-copy-gap, 0.8rem);
136
+ align-items: stretch;
137
+ }
133
138
 
134
- .input-copy__input {
135
- flex: 1;
136
- min-width: 0;
139
+ .input-copy__input {
140
+ flex: 1;
141
+ min-width: 0;
142
+ padding: var(--input-copy-input-padding, 0.8rem 1rem);
143
+ background-color: var(--input-copy-input-bg, var(--theme-input-surface));
144
+ border: var(--input-copy-input-border, 0.1rem solid var(--theme-border));
145
+ border-radius: var(--input-copy-input-border-radius, 0.4rem);
146
+ font-size: var(--_input-font-size);
147
+ color: var(--input-copy-input-text-color, var(--theme-text));
148
+ font-family: inherit;
149
+
150
+ &:focus-visible {
151
+ outline: var(--form-element-outline-width-focus, 0.2rem) solid
152
+ var(--input-copy-focus-ring, var(--theme-border-focus));
153
+ outline-offset: var(--form-element-outline-offset-focus, 0.2rem);
154
+ }
155
+ }
137
156
 
138
- padding: var(--input-copy-input-padding, 0.8rem 1rem);
139
- background-color: var(--input-copy-input-bg, var(--slate-00));
140
- border: var(--input-copy-input-border, 1px solid var(--slate-03));
141
- border-radius: var(--_input-border-radius);
142
- font-size: var(--_input-font-size);
143
- color: var(--input-copy-input-text-color, #333);
144
- font-family: inherit;
157
+ .input-copy__button.input-button {
158
+ flex-shrink: 0;
159
+ max-inline-size: 50%;
160
+ box-sizing: border-box;
161
+ min-height: auto;
162
+ gap: var(--input-copy-button-gap, 0.6rem);
163
+ padding: var(--input-copy-button-padding, 0.8rem 1.2rem);
164
+ background-color: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
165
+ color: var(--input-copy-button-text-color, var(--input-button-primary-text, var(--theme-on-surface)));
166
+ border: none;
167
+ border-radius: var(--input-copy-button-border-radius, 0.4rem);
168
+ font-size: var(--_input-font-size);
169
+ font-weight: 500;
170
+ cursor: pointer;
171
+ transition: background-color 0.2s ease;
172
+ --input-button-text-white-space: normal;
173
+
174
+ &:hover:not(:disabled, [aria-disabled="true"]) {
175
+ background-color: var(
176
+ --input-copy-button-bg-hover,
177
+ var(--input-button-primary-surface-hover, var(--theme-surface-hover))
178
+ );
179
+ }
145
180
 
146
- &::placeholder {
147
- color: var(--input-copy-input-placeholder-color, #999);
148
- }
181
+ &:focus-visible {
182
+ outline: var(--form-element-outline-width-focus, 0.2rem) solid
183
+ var(--input-copy-focus-ring, var(--theme-border-focus));
184
+ outline-offset: var(--form-element-outline-offset-focus, 0.2rem);
185
+ }
149
186
 
150
- &:focus {
151
- outline: 2px solid var(--_input-border-radius);
152
- outline-offset: 2px;
153
- }
154
- }
187
+ &:disabled {
188
+ opacity: 0.6;
189
+ cursor: not-allowed;
190
+ }
155
191
 
156
- .input-copy__button {
157
- display: flex;
158
- align-items: center;
159
- gap: var(--input-copy-button-gap, 0.6rem);
160
- padding: var(--input-copy-button-padding, 0.8rem 1.2rem);
161
-
162
- background-color: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
163
- color: var(--_button-text-color);
164
- border: none;
165
- border-radius: var(--input-copy-button-border-radius, 0.4rem);
166
- font-size: var(--_input-font-size);
167
- font-weight: 500;
168
- cursor: pointer;
169
- white-space: nowrap;
170
- transition: background-color 0.2s ease;
171
-
172
- &:hover:not(:disabled) {
173
- background-color: var(--_button-bg-hover);
174
- }
192
+ &[aria-disabled="true"] {
193
+ cursor: default;
194
+ }
175
195
 
176
- &:disabled {
177
- opacity: 0.6;
178
- cursor: not-allowed;
179
- }
196
+ .input-copy__icon {
197
+ width: 1.2em;
198
+ height: 1.2em;
199
+ flex-shrink: 0;
200
+ }
180
201
 
181
- .input-copy__icon {
182
- width: 1.2em;
183
- height: 1.2em;
184
- flex-shrink: 0;
202
+ .button-text {
203
+ font-size: inherit;
204
+ text-transform: none;
205
+ }
185
206
  }
186
207
 
187
- .input-copy__button-text {
188
- display: none;
189
-
190
- @media (min-width: 480px) {
191
- display: inline;
208
+ @container input-copy (width < 48rem) {
209
+ .input-copy__button.input-button .button-text {
210
+ display: none;
192
211
  }
193
212
  }
194
- }
195
213
 
196
- .input-copy__description {
197
- margin: var(--input-copy-description-margin, 0.6rem 0 0 0);
198
- font-size: var(--input-copy-description-font-size, 0.85rem);
199
- color: var(--input-copy-description-color, #666);
214
+ .input-copy__description {
215
+ margin: var(--input-copy-description-margin, 0);
216
+ font-size: var(--input-copy-description-font-size, var(--step-4));
217
+ color: var(--input-copy-description-color, var(--theme-text));
218
+ display: -webkit-box;
219
+ -webkit-box-orient: vertical;
220
+ overflow: hidden;
221
+ overflow-wrap: anywhere;
222
+ -webkit-line-clamp: var(--input-copy-description-line-clamp, none);
223
+ line-clamp: var(--input-copy-description-line-clamp, none);
224
+ }
200
225
  }
201
226
  }
202
227
  </style>
@@ -1,22 +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
17
  argTypes: {
8
- 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" } },
9
57
  id: {
10
58
  control: "text",
11
- 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" } },
12
76
  },
13
- name: { control: "text", description: "Defaults to the resolved id if not provided." },
14
- label: { control: "text" },
15
- description: { control: "text" },
16
- buttonText: { control: "text" },
17
- copiedText: { control: "text" },
18
- copiedDuration: { control: "number" },
19
- isDisabled: { control: "boolean" },
20
77
  },
21
78
  args: {
22
79
  value: "sk_live_abc123defgh456xyz789uvw",
@@ -24,59 +81,73 @@ const meta: Meta<typeof InputCopy> = {
24
81
  description: "Your license key is ready to use. Copy it to embed in your application.",
25
82
  buttonText: "Copy",
26
83
  copiedText: "Copied!",
84
+ icon: "lucide:copy",
85
+ copiedIcon: "lucide:check",
27
86
  copiedDuration: 2000,
28
87
  isDisabled: false,
88
+ styleClassPassthrough: [],
89
+ descriptionLineClamp: "none",
29
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
+ }),
30
125
  parameters: {
126
+ layout: "fullscreen",
31
127
  docs: {
32
128
  description: {
33
129
  component:
34
- "A readonly input field with a copy-to-clipboard button. Displays a value that users can quickly copy to their clipboard. " +
35
- "Shows feedback ('Copied!') for a brief duration after successful copy. " +
36
- "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.",
37
134
  },
38
135
  },
39
136
  },
40
137
  };
41
138
 
42
139
  export default meta;
43
- type Story = StoryObj<typeof meta>;
140
+ type Story = StoryObj<StoryArgs>;
44
141
 
45
- const storyWrapper = (args: Record<string, unknown>) => ({
46
- components: { InputCopy },
47
- setup() {
48
- const handleCopy = (value: string) => {
49
- console.log(`Copied: ${value}`);
50
- };
51
- const handleCopied = (value: string) => {
52
- console.log(`Copied event fired: ${value}`);
53
- };
54
- return { args, handleCopy, handleCopied };
55
- },
56
- template: `
57
- <div style="padding: 2rem; background: #f5f5f5; border-radius: 0.5rem; max-width: 600px;">
58
- <InputCopy v-bind="args" @copy="handleCopy" @copied="handleCopied" />
59
- </div>
60
- `,
61
- });
62
-
63
- export const Default: Story = {
64
- render: (args) => storyWrapper(args),
65
- };
66
-
67
- export const LicenseKey: Story = {
68
- args: {
69
- value: "gmh_lic_single_site_abc123def456ghi789",
70
- label: "License key",
71
- description: "Your GuideMyHair license key. Keep this safe and use it to embed the widget.",
72
- },
73
- };
142
+ export const Default: Story = {};
74
143
 
75
144
  export const APIToken: Story = {
76
145
  args: {
77
146
  value: "example_token_not_real_0123456789abcdef",
78
147
  label: "API token",
79
148
  description: "Your API token for authentication. Do not share this token.",
149
+ buttonText: "Copy token",
150
+ copiedText: "Token copied!",
80
151
  },
81
152
  };
82
153
 
@@ -107,18 +178,104 @@ export const NoDescription: Story = {
107
178
 
108
179
  export const WithExplicitIdAndName: Story = {
109
180
  args: {
110
- value: "sk_live_abc123defgh456xyz789uvw",
111
181
  id: "checkout-license-key",
112
182
  name: "licenseKey",
113
- label: "License key",
114
- 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.",
115
184
  },
116
185
  };
117
186
 
118
187
  export const LongValue: Story = {
119
188
  args: {
120
- value: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POdHyP_DQkT3WTZhu9qLWSwVe8ZlHfHEPBOzzVI3vFA",
189
+ value:
190
+ "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsZXggTW9yZ2FuIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POdHyP_DQkT3WTZhu9qLWSwVe8ZlHfHEPBOzzVI3vFA",
121
191
  label: "JWT token",
122
192
  description: "Your JWT authentication token. This is a long-lived token used for API access.",
123
193
  },
124
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
+ };