srcdev-nuxt-components 9.6.3 → 9.6.5
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/rules/consumer/vue-no-self-closing.md +3 -2
- package/.claude/skills/component-compliance-checklist.md +17 -3
- package/.claude/skills/component-inline-action-button.md +33 -2
- package/.claude/skills/components/cookie-consent-banner.md +9 -6
- package/.claude/skills/components/input-copy.md +47 -100
- package/.claude/skills/components/progress-tracker.md +140 -0
- package/.claude/skills/components/stepper-list.md +10 -3
- package/.claude/skills/components/tabbed-content.md +4 -0
- package/.claude/skills/composable-cookie-consent.md +1 -1
- package/.claude/skills/index.md +4 -3
- package/.claude/skills/storybook-add-story.md +14 -0
- package/.claude/skills/testing-add-unit-test.md +19 -0
- package/.claude/skills/theming-component-token-pattern.md +1 -1
- package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
- package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
- package/.vscode/srcdev-component-progress-tracker.code-snippets +45 -0
- package/.vscode/srcdev-component-stepper-list.code-snippets +3 -2
- package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +5 -1
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +2 -2
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +26 -0
- package/app/components/02.molecules/contact-section/tests/__snapshots__/ContactSection.spec.ts.snap +28 -28
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
- package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +106 -5
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
- package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -47
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
- package/app/components/02.molecules/progress-tracker/CONSUMER-STYLING.md +116 -0
- package/app/components/02.molecules/progress-tracker/ProgressTracker.vue +275 -0
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +424 -0
- package/app/components/02.molecules/progress-tracker/tests/ProgressTracker.spec.ts +253 -0
- package/app/components/02.molecules/progress-tracker/tests/__snapshots__/ProgressTracker.spec.ts.snap +46 -0
- package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +42 -7
- package/app/components/02.molecules/stepper-list/StepperList.vue +42 -19
- package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +288 -2
- package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +161 -34
- package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +40 -40
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +7 -7
- package/app/types/components/cookie-consent-banner.d.ts +2 -0
- package/app/types/components/index.ts +1 -0
- package/app/types/components/progress-tracker.d.ts +6 -0
- 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
|
-
<
|
|
14
|
+
<InputButton
|
|
14
15
|
type="button"
|
|
16
|
+
variant="inline"
|
|
15
17
|
class="input-copy__button"
|
|
16
|
-
:
|
|
17
|
-
:
|
|
18
|
+
:button-text="copied ? copiedText : buttonText"
|
|
19
|
+
:aria-label="resolvedButtonAriaLabel"
|
|
20
|
+
:disabled="isDisabled"
|
|
21
|
+
:readonly="copied"
|
|
18
22
|
@click="handleCopy"
|
|
19
23
|
>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
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"
|
|
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-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
.input-copy__wrapper {
|
|
134
|
+
display: flex;
|
|
135
|
+
gap: var(--input-copy-gap, 0.8rem);
|
|
136
|
+
align-items: stretch;
|
|
137
|
+
}
|
|
133
138
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
187
|
+
&:disabled {
|
|
188
|
+
opacity: 0.6;
|
|
189
|
+
cursor: not-allowed;
|
|
190
|
+
}
|
|
155
191
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
196
|
+
.input-copy__icon {
|
|
197
|
+
width: 1.2em;
|
|
198
|
+
height: 1.2em;
|
|
199
|
+
flex-shrink: 0;
|
|
200
|
+
}
|
|
180
201
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
202
|
+
.button-text {
|
|
203
|
+
font-size: inherit;
|
|
204
|
+
text-transform: none;
|
|
205
|
+
}
|
|
185
206
|
}
|
|
186
207
|
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
35
|
-
"
|
|
36
|
-
"
|
|
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<
|
|
140
|
+
type Story = StoryObj<StoryArgs>;
|
|
44
141
|
|
|
45
|
-
const
|
|
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
|
-
|
|
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:
|
|
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
|
+
};
|