srcdev-nuxt-components 9.6.1 → 9.6.3
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/consumer-overrides.md +17 -0
- package/.claude/rules/consumer/css-comment-slash.md +13 -0
- package/.claude/rules/consumer/css-native-nesting.md +13 -0
- package/.claude/rules/consumer/css-no-deep.md +13 -0
- package/.claude/rules/consumer/css-rem-base.md +11 -0
- package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
- package/.claude/rules/consumer/css-webkit-support.md +15 -0
- package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
- package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
- package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
- package/.claude/rules/consumer/story-args.md +12 -0
- package/.claude/rules/consumer/story-register-components.md +12 -0
- package/.claude/rules/consumer/vue-auto-imports.md +14 -0
- package/.claude/rules/consumer/vue-no-self-closing.md +11 -0
- package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
- package/.claude/rules/consumer/vue-props-pattern.md +13 -0
- package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
- package/.claude/rules/consumer/vue-video-source.md +12 -0
- package/.claude/skills/component-compliance-checklist.md +37 -2
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/display-avatar.md +14 -3
- package/.claude/skills/components/display-chip.md +2 -1
- package/.claude/skills/components/google-reviews.md +2 -2
- package/.claude/skills/components/pricing-card.md +68 -84
- package/.claude/skills/components/profile-section.md +59 -21
- package/.claude/skills/components/social-icons-list.md +2 -0
- package/.claude/skills/index.md +13 -6
- package/.claude/skills/new-app-scaffold.md +4 -2
- package/.claude/skills/setup-postinstall.md +5 -4
- package/.claude/skills/storybook-add-story.md +38 -0
- package/.claude/skills/theming-component-token-pattern.md +94 -0
- package/.claude/skills/theming-partial-override.md +1 -1
- package/.vscode/srcdev-component-display-avatar.code-snippets +7 -0
- package/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
- package/.vscode/srcdev-component-profile-section.code-snippets +27 -4
- package/README.md +5 -5
- package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
- package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
- package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
- package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
- package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +2 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +26 -10
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +75 -34
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +84 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +85 -13
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +3 -3
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +7 -2
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +0 -1
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
- package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
- package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
- package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
- package/app/components/02.molecules/profile-section/CONSUMER-STYLING.md +103 -13
- package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
- package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
- package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
- package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
- package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +6 -2
- package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
- package/app/composables/useAriaDescribedById.ts +1 -1
- package/package.json +2 -1
|
@@ -1,24 +1,70 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
2
|
import PricingCard from "../PricingCard.vue";
|
|
3
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
5
|
+
import type { MediaCanvas } from "~/types/components";
|
|
6
|
+
|
|
7
|
+
type LineClamp = "none" | "1" | "2" | "3" | "4";
|
|
8
|
+
|
|
9
|
+
// Story-only args: drive CSS tokens, not PricingCard props. See the argTypes entries.
|
|
10
|
+
type StoryArgs = InstanceType<typeof PricingCard>["$props"] & {
|
|
11
|
+
nameLineClamp: LineClamp;
|
|
12
|
+
descriptionLineClamp: LineClamp;
|
|
13
|
+
featureLineClamp: LineClamp;
|
|
14
|
+
};
|
|
3
15
|
|
|
4
|
-
const
|
|
16
|
+
const lineClampControl = (token: string, example: string) => ({
|
|
17
|
+
control: "select" as const,
|
|
18
|
+
options: ["none", "1", "2", "3", "4"] satisfies LineClamp[],
|
|
19
|
+
description:
|
|
20
|
+
`**Story control, not a prop.** Sets the \`${token}\` CSS token on a wrapper so you can try it here. ` +
|
|
21
|
+
`To use it in an app, set the token in your own CSS, e.g. \`${example}\`. ` +
|
|
22
|
+
"`1` is single-line ellipsis, `none` (the default) shows everything.",
|
|
23
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "none" } },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const meta: Meta<StoryArgs> = {
|
|
5
27
|
title: "Molecules/Pricing Card",
|
|
6
28
|
component: PricingCard,
|
|
7
|
-
tags: ["autodocs"],
|
|
8
29
|
argTypes: {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
nameLineClamp: lineClampControl("--pricing-card-name-line-clamp", ".pricing-page { --pricing-card-name-line-clamp: 1; }"),
|
|
31
|
+
descriptionLineClamp: lineClampControl(
|
|
32
|
+
"--pricing-card-description-line-clamp",
|
|
33
|
+
".pricing-page { --pricing-card-description-line-clamp: 2; }"
|
|
34
|
+
),
|
|
35
|
+
featureLineClamp: lineClampControl(
|
|
36
|
+
"--pricing-card-feature-line-clamp",
|
|
37
|
+
".pricing-page { --pricing-card-feature-line-clamp: 1; }"
|
|
38
|
+
),
|
|
39
|
+
tag: { control: "select", options: ["div", "section", "article"], table: { category: "Semantics" } },
|
|
40
|
+
headingTag: {
|
|
41
|
+
control: "select",
|
|
42
|
+
options: ["h2", "h3", "h4", "h5", "h6"],
|
|
43
|
+
description: "Heading level for the plan name; pick to fit the surrounding page outline",
|
|
44
|
+
table: { category: "Semantics", defaultValue: { summary: "h3" } },
|
|
45
|
+
},
|
|
46
|
+
planName: { control: "text", table: { category: "Content" } },
|
|
47
|
+
price: { control: "number", table: { category: "Content" } },
|
|
48
|
+
currencySymbol: { control: "text", table: { category: "Content" } },
|
|
49
|
+
billingPeriod: { control: "text", table: { category: "Content" } },
|
|
50
|
+
description: { control: "text", table: { category: "Content" } },
|
|
51
|
+
features: { control: "object", table: { category: "Content" } },
|
|
52
|
+
isHighlighted: { control: "boolean", table: { category: "Highlight" } },
|
|
53
|
+
badgeText: { control: "text", table: { category: "Highlight" } },
|
|
54
|
+
ribbonText: { control: "text", table: { category: "Highlight" } },
|
|
55
|
+
ctaText: { control: "text", table: { category: "CTA" } },
|
|
56
|
+
ctaDisabled: { control: "boolean", table: { category: "CTA" } },
|
|
57
|
+
styleClassPassthrough: { control: "object", table: { category: "Styling" } },
|
|
18
58
|
},
|
|
19
59
|
args: {
|
|
60
|
+
nameLineClamp: "none",
|
|
61
|
+
descriptionLineClamp: "none",
|
|
62
|
+
featureLineClamp: "none",
|
|
63
|
+
tag: "article",
|
|
64
|
+
headingTag: "h3",
|
|
20
65
|
planName: "Single Site",
|
|
21
66
|
price: 99,
|
|
67
|
+
currencySymbol: "£",
|
|
22
68
|
billingPeriod: "one-time",
|
|
23
69
|
description: "Perfect for freelancers and solo practitioners.",
|
|
24
70
|
features: ["Embed on one domain", "Unlimited consultations", "Email support", "Monthly updates"],
|
|
@@ -28,44 +74,75 @@ const meta: Meta<typeof PricingCard> = {
|
|
|
28
74
|
ctaDisabled: false,
|
|
29
75
|
ribbonText: undefined,
|
|
30
76
|
},
|
|
77
|
+
decorators: [
|
|
78
|
+
(story, context) => ({
|
|
79
|
+
components: { story, CanvasSwitcher },
|
|
80
|
+
setup() {
|
|
81
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
82
|
+
return { canvasName };
|
|
83
|
+
},
|
|
84
|
+
template: `
|
|
85
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
86
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
87
|
+
</div>
|
|
88
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 3.2rem 2rem; outline: 1px dashed currentColor;">
|
|
89
|
+
<story />
|
|
90
|
+
</div>
|
|
91
|
+
`,
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
31
94
|
parameters: {
|
|
95
|
+
layout: "fullscreen",
|
|
32
96
|
docs: {
|
|
33
97
|
description: {
|
|
34
98
|
component:
|
|
35
|
-
"A
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"Use multiple cards in a grid layout to build a pricing comparison table.",
|
|
99
|
+
"A pricing plan card showing plan name, price, features and a CTA button. " +
|
|
100
|
+
"`is-highlighted` adds a top-centred badge and a scaled, emphasised border. " +
|
|
101
|
+
"`ribbon-text` renders a diagonal corner ribbon for marketing callouts like a discount; leave it unset for no ribbon. " +
|
|
102
|
+
"Emits `select` with the plan name when the CTA is clicked; replace the button entirely with the `#cta` slot. " +
|
|
103
|
+
"Put several cards in a grid to build a comparison table.",
|
|
41
104
|
},
|
|
42
105
|
},
|
|
43
106
|
},
|
|
44
107
|
};
|
|
45
108
|
|
|
46
109
|
export default meta;
|
|
47
|
-
type Story = StoryObj<
|
|
110
|
+
type Story = StoryObj<StoryArgs>;
|
|
111
|
+
|
|
112
|
+
function useStorySetup(args: StoryArgs) {
|
|
113
|
+
const componentArgs = computed(() => {
|
|
114
|
+
const {
|
|
115
|
+
nameLineClamp: _nameLineClamp,
|
|
116
|
+
descriptionLineClamp: _descriptionLineClamp,
|
|
117
|
+
featureLineClamp: _featureLineClamp,
|
|
118
|
+
...rest
|
|
119
|
+
} = args;
|
|
120
|
+
return rest;
|
|
121
|
+
});
|
|
122
|
+
const tokenStyles = computed(() => ({
|
|
123
|
+
"--pricing-card-name-line-clamp": args.nameLineClamp,
|
|
124
|
+
"--pricing-card-description-line-clamp": args.descriptionLineClamp,
|
|
125
|
+
"--pricing-card-feature-line-clamp": args.featureLineClamp,
|
|
126
|
+
}));
|
|
127
|
+
const handleSelect = (planName: string) => {
|
|
128
|
+
console.log(`Selected plan: ${planName}`);
|
|
129
|
+
};
|
|
130
|
+
return { componentArgs, tokenStyles, handleSelect };
|
|
131
|
+
}
|
|
48
132
|
|
|
49
|
-
const
|
|
133
|
+
const render = (args: StoryArgs) => ({
|
|
50
134
|
components: { PricingCard },
|
|
51
135
|
setup() {
|
|
52
|
-
return
|
|
136
|
+
return useStorySetup(args);
|
|
53
137
|
},
|
|
54
138
|
template: `
|
|
55
|
-
<div style="
|
|
56
|
-
<PricingCard v-bind="
|
|
139
|
+
<div :style="tokenStyles" style="max-inline-size: 36rem;">
|
|
140
|
+
<PricingCard v-bind="componentArgs" @select="handleSelect" />
|
|
57
141
|
</div>
|
|
58
142
|
`,
|
|
59
143
|
});
|
|
60
144
|
|
|
61
|
-
export const Default: Story = {
|
|
62
|
-
render: (args) => {
|
|
63
|
-
const handleSelect = (planName: string) => {
|
|
64
|
-
console.log(`Selected plan: ${planName}`);
|
|
65
|
-
};
|
|
66
|
-
return storyWrapper(args, handleSelect);
|
|
67
|
-
},
|
|
68
|
-
};
|
|
145
|
+
export const Default: Story = { render };
|
|
69
146
|
|
|
70
147
|
export const Highlighted: Story = {
|
|
71
148
|
args: {
|
|
@@ -75,34 +152,7 @@ export const Highlighted: Story = {
|
|
|
75
152
|
features: ["Embed on up to 3 domains", "Unlimited consultations", "Priority email support", "Weekly updates"],
|
|
76
153
|
isHighlighted: true,
|
|
77
154
|
},
|
|
78
|
-
render
|
|
79
|
-
const handleSelect = (planName: string) => {
|
|
80
|
-
console.log(`Selected plan: ${planName}`);
|
|
81
|
-
};
|
|
82
|
-
return storyWrapper(args, handleSelect);
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const Unlimited: Story = {
|
|
87
|
-
args: {
|
|
88
|
-
planName: "Unlimited",
|
|
89
|
-
price: 799,
|
|
90
|
-
description: "Enterprise plan for unlimited growth.",
|
|
91
|
-
features: [
|
|
92
|
-
"Unlimited domains",
|
|
93
|
-
"Unlimited consultations",
|
|
94
|
-
"24/7 priority support",
|
|
95
|
-
"Real-time updates",
|
|
96
|
-
"Custom integrations",
|
|
97
|
-
],
|
|
98
|
-
isHighlighted: false,
|
|
99
|
-
},
|
|
100
|
-
render: (args) => {
|
|
101
|
-
const handleSelect = (planName: string) => {
|
|
102
|
-
console.log(`Selected plan: ${planName}`);
|
|
103
|
-
};
|
|
104
|
-
return storyWrapper(args, handleSelect);
|
|
105
|
-
},
|
|
155
|
+
render,
|
|
106
156
|
};
|
|
107
157
|
|
|
108
158
|
export const Disabled: Story = {
|
|
@@ -114,12 +164,7 @@ export const Disabled: Story = {
|
|
|
114
164
|
ctaDisabled: true,
|
|
115
165
|
ctaText: "Coming soon",
|
|
116
166
|
},
|
|
117
|
-
render
|
|
118
|
-
const handleSelect = (planName: string) => {
|
|
119
|
-
console.log(`Selected plan: ${planName}`);
|
|
120
|
-
};
|
|
121
|
-
return storyWrapper(args, handleSelect);
|
|
122
|
-
},
|
|
167
|
+
render,
|
|
123
168
|
};
|
|
124
169
|
|
|
125
170
|
export const WithDiscountRibbon: Story = {
|
|
@@ -131,53 +176,145 @@ export const WithDiscountRibbon: Story = {
|
|
|
131
176
|
features: ["Embed on one domain", "Unlimited consultations", "Email support", "Priced annually"],
|
|
132
177
|
ribbonText: "EARLYBIRD50 · 50% off",
|
|
133
178
|
},
|
|
134
|
-
render
|
|
135
|
-
const handleSelect = (planName: string) => {
|
|
136
|
-
console.log(`Selected plan: ${planName}`);
|
|
137
|
-
};
|
|
138
|
-
return storyWrapper(args, handleSelect);
|
|
139
|
-
},
|
|
179
|
+
render,
|
|
140
180
|
};
|
|
141
181
|
|
|
182
|
+
const comparisonPlans = [
|
|
183
|
+
{
|
|
184
|
+
planName: "Single Site",
|
|
185
|
+
price: 99,
|
|
186
|
+
description: "Perfect for freelancers and solo practitioners.",
|
|
187
|
+
features: ["Embed on one domain", "Unlimited consultations", "Email support", "Monthly updates"],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
planName: "Agency",
|
|
191
|
+
price: 299,
|
|
192
|
+
description: "Best for growing teams managing multiple sites.",
|
|
193
|
+
features: ["Embed on up to 3 domains", "Unlimited consultations", "Priority email support", "Weekly updates"],
|
|
194
|
+
isHighlighted: true,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
planName: "Unlimited",
|
|
198
|
+
price: 799,
|
|
199
|
+
description: "Enterprise plan for unlimited growth.",
|
|
200
|
+
features: ["Unlimited domains", "Unlimited consultations", "24/7 priority support", "Real-time updates", "Custom integrations"],
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
142
204
|
export const ThreeCardComparison: Story = {
|
|
143
|
-
|
|
205
|
+
name: "Three Card Comparison",
|
|
206
|
+
render: (args) => ({
|
|
144
207
|
components: { PricingCard },
|
|
145
208
|
setup() {
|
|
146
|
-
|
|
147
|
-
console.log(`Selected plan: ${planName}`);
|
|
148
|
-
};
|
|
149
|
-
return { handleSelect };
|
|
209
|
+
return { ...useStorySetup(args), comparisonPlans };
|
|
150
210
|
},
|
|
151
211
|
template: `
|
|
152
|
-
<div
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
212
|
+
<div
|
|
213
|
+
:style="tokenStyles"
|
|
214
|
+
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr)); gap: 2.4rem; align-items: stretch;"
|
|
215
|
+
>
|
|
216
|
+
<PricingCard
|
|
217
|
+
v-for="plan in comparisonPlans"
|
|
218
|
+
:key="plan.planName"
|
|
219
|
+
v-bind="plan"
|
|
220
|
+
currency-symbol="£"
|
|
221
|
+
@select="handleSelect"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
`,
|
|
225
|
+
}),
|
|
226
|
+
parameters: {
|
|
227
|
+
docs: {
|
|
228
|
+
description: {
|
|
229
|
+
story:
|
|
230
|
+
"Cards in an auto-fit grid. On a narrow canvas they stack; set `--pricing-card-highlight-scale: 1` there " +
|
|
231
|
+
"if the scaled highlighted card shouldn't overhang the column.",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// ─── Stress test ──────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
const unbroken = "Superkalifragilistischexpialigetischesunbegrenztespaket";
|
|
240
|
+
const longGerman =
|
|
241
|
+
"Unbegrenzter Zugang zu allen Beratungsfunktionen einschließlich Prioritätsunterstützung und vierteljährlicher Strategiegespräche";
|
|
242
|
+
|
|
243
|
+
const stressCards: InstanceType<typeof PricingCard>["$props"][] = [
|
|
244
|
+
{
|
|
245
|
+
planName: `${unbroken}${unbroken}`,
|
|
246
|
+
price: 123456789012.345,
|
|
247
|
+
currencySymbol: "CHF ",
|
|
248
|
+
billingPeriod: "pro Monat, jährliche Abrechnung, zuzüglich Mehrwertsteuer",
|
|
249
|
+
description: `${longGerman}. ${longGerman}.`,
|
|
250
|
+
features: [
|
|
251
|
+
longGerman,
|
|
252
|
+
"https://example.com/a/very/long/path/that/never/breaks/segmentsegmentsegmentsegment",
|
|
253
|
+
"💇♀️💅🏽 Emoji first",
|
|
254
|
+
"<script>alert('xss')</script> renders as text",
|
|
255
|
+
"",
|
|
256
|
+
"X",
|
|
257
|
+
],
|
|
258
|
+
isHighlighted: true,
|
|
259
|
+
badgeText: "Am beliebtesten bei Teams mit mehreren Standorten",
|
|
260
|
+
ribbonText: `${unbroken} 50% Rabatt`,
|
|
261
|
+
ctaText: "Jetzt mit dem unbegrenzten Paket beginnen und sparen",
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
planName: "X",
|
|
265
|
+
price: 0,
|
|
266
|
+
billingPeriod: "",
|
|
267
|
+
description: "",
|
|
268
|
+
features: [],
|
|
269
|
+
ctaText: "",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
planName: "خطة الأعمال",
|
|
273
|
+
price: -9.5,
|
|
274
|
+
currencySymbol: "",
|
|
275
|
+
billingPeriod: "شهريا",
|
|
276
|
+
description: "<b>Not bold</b> & HTML-like text",
|
|
277
|
+
features: Array.from({ length: 20 }, (_, i) => `Feature ${i + 1}`),
|
|
278
|
+
ribbonText: "!",
|
|
279
|
+
ctaDisabled: true,
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
export const StressTest: Story = {
|
|
284
|
+
name: "Stress Test (Worst-Case Data)",
|
|
285
|
+
render: (args) => ({
|
|
286
|
+
components: { PricingCard },
|
|
287
|
+
setup() {
|
|
288
|
+
const { tokenStyles, handleSelect } = useStorySetup(args);
|
|
289
|
+
return { tokenStyles, handleSelect, stressCards };
|
|
290
|
+
},
|
|
291
|
+
template: `
|
|
292
|
+
<div
|
|
293
|
+
:style="tokenStyles"
|
|
294
|
+
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr)); gap: 3.2rem; align-items: start;"
|
|
295
|
+
>
|
|
296
|
+
<PricingCard
|
|
297
|
+
v-for="(card, index) in stressCards"
|
|
298
|
+
:key="index"
|
|
299
|
+
v-bind="card"
|
|
300
|
+
:dir="index === 2 ? 'rtl' : undefined"
|
|
301
|
+
@select="handleSelect"
|
|
302
|
+
/>
|
|
180
303
|
</div>
|
|
181
304
|
`,
|
|
182
305
|
}),
|
|
306
|
+
parameters: {
|
|
307
|
+
initialCanvas: "mobileCanvas",
|
|
308
|
+
docs: {
|
|
309
|
+
description: {
|
|
310
|
+
story:
|
|
311
|
+
"Worst-case data, checked at every canvas width. Card 1: unbroken plan name, a 12-digit fractional price, " +
|
|
312
|
+
"long German copy in every text prop, a long badge, an unbroken ribbon and a URL feature. Card 2: one-character " +
|
|
313
|
+
"name, zero price, and every optional field empty. Card 3: right-to-left text, negative fractional price, no " +
|
|
314
|
+
"currency symbol, HTML-like copy (renders as text) and 20 features. Nothing should overflow the card, the badge " +
|
|
315
|
+
"should stay centred on the top border, the ribbon caps at two lines and the plan name keeps clear of it. " +
|
|
316
|
+
"Try the line-clamp controls (CSS tokens, story only) to shorten the name, description and features.",
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
183
320
|
};
|