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,5 +1,11 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
1
2
|
import ProfileSection from "../ProfileSection.vue";
|
|
3
|
+
import EyebrowText from "../../../01.atoms/text-blocks/eyebrow-text/EyebrowText.vue";
|
|
4
|
+
import HeroText from "../../../01.atoms/text-blocks/hero-text/HeroText.vue";
|
|
5
|
+
import SocialIconsList from "../../social-icons-list/SocialIconsList.vue";
|
|
6
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
2
7
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
8
|
+
import type { ISocialIcon, MediaCanvas } from "~/types/components";
|
|
3
9
|
|
|
4
10
|
const meta: Meta<typeof ProfileSection> = {
|
|
5
11
|
title: "Molecules/ProfileSection",
|
|
@@ -12,11 +18,12 @@ const meta: Meta<typeof ProfileSection> = {
|
|
|
12
18
|
},
|
|
13
19
|
profilePicture: {
|
|
14
20
|
control: "object",
|
|
15
|
-
description: "Profile picture src and
|
|
21
|
+
description: "Profile picture src, alt text and optional intrinsic width/height",
|
|
16
22
|
},
|
|
17
|
-
|
|
18
|
-
control: { type: "
|
|
19
|
-
|
|
23
|
+
headerPlacement: {
|
|
24
|
+
control: { type: "inline-radio" },
|
|
25
|
+
options: ["above", "picture-bottom", "picture-top"],
|
|
26
|
+
description: "Where the eyebrow and heading go: above the grid, or over the bottom or top of the picture on a scrim",
|
|
20
27
|
},
|
|
21
28
|
styleClassPassthrough: {
|
|
22
29
|
control: "object",
|
|
@@ -27,210 +34,441 @@ const meta: Meta<typeof ProfileSection> = {
|
|
|
27
34
|
tag: "div",
|
|
28
35
|
profilePicture: {
|
|
29
36
|
src: "/images/services/service-balayage.jpg",
|
|
30
|
-
alt: "
|
|
37
|
+
alt: "Alex Morgan, hair stylist",
|
|
31
38
|
},
|
|
32
|
-
profileInfoCount: 3,
|
|
33
39
|
styleClassPassthrough: [],
|
|
34
40
|
},
|
|
41
|
+
decorators: [
|
|
42
|
+
(story, context) => ({
|
|
43
|
+
components: { story, CanvasSwitcher },
|
|
44
|
+
setup() {
|
|
45
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
46
|
+
return { canvasName };
|
|
47
|
+
},
|
|
48
|
+
template: `
|
|
49
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
50
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
51
|
+
</div>
|
|
52
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
53
|
+
<story />
|
|
54
|
+
</div>
|
|
55
|
+
`,
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
58
|
+
parameters: {
|
|
59
|
+
layout: "fullscreen",
|
|
60
|
+
docs: {
|
|
61
|
+
description: {
|
|
62
|
+
component:
|
|
63
|
+
"The layout responds to the component's own width (a container query), not the viewport: it switches to two columns once it is 768px wide. Use the canvas switcher to check each width.",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
35
67
|
};
|
|
36
68
|
|
|
37
69
|
export default meta;
|
|
38
70
|
type Story = StoryObj<typeof ProfileSection>;
|
|
39
71
|
|
|
72
|
+
const headerSlots = `
|
|
73
|
+
<template #eyebrowText>
|
|
74
|
+
<EyebrowText tag="p" font-size="medium" text-content="About Alex" :style-class-passthrough="['mb-0']" />
|
|
75
|
+
</template>
|
|
76
|
+
<template #heroText="{ headingId }">
|
|
77
|
+
<HeroText
|
|
78
|
+
:id="headingId"
|
|
79
|
+
tag="h2"
|
|
80
|
+
font-size="title"
|
|
81
|
+
:text-content="[
|
|
82
|
+
{ text: 'Your', styleClass: 'normal' },
|
|
83
|
+
{ text: 'local hair stylist', styleClass: 'accent' },
|
|
84
|
+
{ text: 'in Northtown', styleClass: 'normal' },
|
|
85
|
+
]"
|
|
86
|
+
:style-class-passthrough="['mb-20']"
|
|
87
|
+
/>
|
|
88
|
+
</template>
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const infoSlots = `
|
|
92
|
+
<template #profile-info-1>
|
|
93
|
+
<p class="page-body-normal">With over 10 years of experience, Alex brings the salon to you, whether you're at home, in the office, or getting ready for a special occasion.</p>
|
|
94
|
+
</template>
|
|
95
|
+
<template #profile-info-2>
|
|
96
|
+
<p class="page-body-normal location">Based in <span class="highlight">Northtown</span>, covering the surrounding area including <span class="highlight">Eastfield</span> and beyond.</p>
|
|
97
|
+
</template>
|
|
98
|
+
<template #profile-info-3>
|
|
99
|
+
<p class="page-body-normal services">Specialising in <a href="#" class="highlight">cuts</a>, <a href="#" class="highlight">colour</a>, and <a href="#" class="highlight">styling</a> for all hair types.</p>
|
|
100
|
+
</template>
|
|
101
|
+
`;
|
|
102
|
+
|
|
40
103
|
// ─── Stories ─────────────────────────────────────────────────────────────────
|
|
41
104
|
|
|
42
105
|
/** Default — three profile-info blocks with eyebrow and hero text header slots. */
|
|
43
106
|
export const Default: Story = {
|
|
44
107
|
render: (args) => ({
|
|
45
|
-
components: { ProfileSection },
|
|
108
|
+
components: { ProfileSection, EyebrowText, HeroText, SocialIconsList },
|
|
46
109
|
setup() {
|
|
47
110
|
return { args };
|
|
48
111
|
},
|
|
49
112
|
template: `
|
|
50
113
|
<ProfileSection v-bind="args">
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</template>
|
|
54
|
-
<template #heroText="{ headingId }">
|
|
55
|
-
<HeroText
|
|
56
|
-
:id="headingId"
|
|
57
|
-
tag="h2"
|
|
58
|
-
axis="vertical"
|
|
59
|
-
font-size="display"
|
|
60
|
-
:text-content="[
|
|
61
|
-
{ text: 'Your', styleClass: 'normal' },
|
|
62
|
-
{ text: 'mobile hairdresser', styleClass: 'accent' },
|
|
63
|
-
{ text: 'in Bath', styleClass: 'normal' },
|
|
64
|
-
]"
|
|
65
|
-
:style-class-passthrough="['mb-20']"
|
|
66
|
-
/>
|
|
67
|
-
</template>
|
|
68
|
-
<template #profile-info-1>
|
|
69
|
-
<div class="experience">
|
|
70
|
-
<p class="page-body-normal">With over 10 years of experience as a mobile hairdresser, Natasha brings the salon to you — whether you're at home, in the office, or getting ready for a special occasion.</p>
|
|
71
|
-
</div>
|
|
72
|
-
</template>
|
|
73
|
-
<template #profile-info-2>
|
|
74
|
-
<div class="location">
|
|
75
|
-
<p class="page-body-normal">Based in <span class="highlight">Bath</span>, covering the surrounding area including Bristol, Frome, and beyond.</p>
|
|
76
|
-
</div>
|
|
77
|
-
</template>
|
|
78
|
-
<template #profile-info-3>
|
|
79
|
-
<div class="services">
|
|
80
|
-
<p class="page-body-normal">Specialising in <a href="#" class="highlight">cuts</a>, <a href="#" class="highlight">colour</a>, and <a href="#" class="highlight">styling</a> for all hair types.</p>
|
|
81
|
-
</div>
|
|
82
|
-
</template>
|
|
114
|
+
${headerSlots}
|
|
115
|
+
${infoSlots}
|
|
83
116
|
</ProfileSection>
|
|
84
117
|
`,
|
|
85
118
|
}),
|
|
119
|
+
parameters: {
|
|
120
|
+
docs: {
|
|
121
|
+
description: {
|
|
122
|
+
story:
|
|
123
|
+
"The second and third blocks show the content hooks: a `.highlight` inside a `.location` element gets the accent colour, and inside a `.services` element the link colour.",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
86
127
|
};
|
|
87
128
|
|
|
88
129
|
/** With profile links — demonstrates the optional profileLinks slot. */
|
|
89
130
|
export const WithProfileLinks: Story = {
|
|
90
131
|
name: "With Profile Links",
|
|
91
132
|
render: (args) => ({
|
|
92
|
-
components: { ProfileSection },
|
|
133
|
+
components: { ProfileSection, EyebrowText, HeroText, SocialIconsList },
|
|
93
134
|
setup() {
|
|
94
135
|
return { args };
|
|
95
136
|
},
|
|
96
137
|
template: `
|
|
97
138
|
<ProfileSection v-bind="args">
|
|
139
|
+
${headerSlots}
|
|
140
|
+
${infoSlots}
|
|
141
|
+
<template #profileLinks>
|
|
142
|
+
<a href="#">Instagram</a>
|
|
143
|
+
<a href="#">Book now</a>
|
|
144
|
+
</template>
|
|
145
|
+
</ProfileSection>
|
|
146
|
+
`,
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const socialIcons: ISocialIcon[] = [
|
|
151
|
+
{ networkName: "Facebook", iconName: "logos:facebook", baseHref: "https://www.facebook.com/", profileId: "yourprofile" },
|
|
152
|
+
{ networkName: "Instagram", iconName: "logos:instagram-icon", baseHref: "https://www.instagram.com/", profileId: "yourprofile" },
|
|
153
|
+
{ networkName: "TikTok", iconName: "logos:tiktok-icon", baseHref: "https://www.tiktok.com/@", profileId: "yourprofile" },
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
/** With social icons — a SocialIconsList in the profileLinks slot. */
|
|
157
|
+
export const WithSocialIcons: Story = {
|
|
158
|
+
name: "With Social Icons",
|
|
159
|
+
render: (args) => ({
|
|
160
|
+
components: { ProfileSection, EyebrowText, HeroText, SocialIconsList },
|
|
161
|
+
setup() {
|
|
162
|
+
return { args, socialIcons };
|
|
163
|
+
},
|
|
164
|
+
template: `
|
|
165
|
+
<ProfileSection v-bind="args">
|
|
166
|
+
${headerSlots}
|
|
167
|
+
${infoSlots}
|
|
168
|
+
<template #profileLinks>
|
|
169
|
+
<SocialIconsList :items="socialIcons" label="Alex Morgan on social media" />
|
|
170
|
+
</template>
|
|
171
|
+
</ProfileSection>
|
|
172
|
+
`,
|
|
173
|
+
}),
|
|
174
|
+
parameters: {
|
|
175
|
+
docs: {
|
|
176
|
+
description: {
|
|
177
|
+
story:
|
|
178
|
+
"The `profileLinks` slot holding a `SocialIconsList`, right-aligned at the bottom of the info column. Size and space the icons with `--social-icons-list-icon-size` and `--social-icons-list-gap` set on your own wrapper.",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// logos:instagram-icon is near-black and vanishes on a dark scheme; mdi:instagram draws in currentColor, so the theme can colour it.
|
|
185
|
+
const themedSocialIcons: ISocialIcon[] = socialIcons.map((icon) =>
|
|
186
|
+
icon.networkName === "Instagram" ? { ...icon, iconName: "mdi:instagram" } : icon
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Every value is a token set on a styleClassPassthrough class; the --eyebrow-text-* tokens are EyebrowText's own.
|
|
190
|
+
const pictureThemes = `
|
|
191
|
+
<component is="style">
|
|
192
|
+
.profile-section.dark-overlay-profile {
|
|
193
|
+
--profile-section-picture-border-radius: 0.4rem;
|
|
194
|
+
--profile-section-picture-header-scrim-colour: #141110;
|
|
195
|
+
--profile-section-picture-header-text-colour: #f3ece4;
|
|
196
|
+
--profile-section-picture-header-eyebrow-colour: #c9a24a;
|
|
197
|
+
--profile-section-picture-header-padding: 3.6rem;
|
|
198
|
+
--profile-section-info-text-colour: #a39a94;
|
|
199
|
+
--profile-section-highlight-colour: #c9a24a;
|
|
200
|
+
--profile-section-link-colour: #e8dfd6;
|
|
201
|
+
--profile-section-link-colour-hover: #c9a24a;
|
|
202
|
+
--social-icons-list-icon-colour: #e8dfd6;
|
|
203
|
+
--eyebrow-text-font-family: "Poppins", sans-serif;
|
|
204
|
+
--eyebrow-text-font-style: normal;
|
|
205
|
+
--eyebrow-text-font-weight: 600;
|
|
206
|
+
--eyebrow-text-letter-spacing: 0.25em;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.profile-section.light-overlay-profile {
|
|
210
|
+
--profile-section-picture-border-radius: 1.6rem;
|
|
211
|
+
--profile-section-picture-header-scrim-colour: #f7f2ec;
|
|
212
|
+
--profile-section-picture-header-scrim-opacity: 90%;
|
|
213
|
+
--profile-section-picture-header-scrim-solid-stop: 45%;
|
|
214
|
+
--profile-section-picture-header-scrim-target-colour: rgb(247 242 236 / 0);
|
|
215
|
+
--profile-section-picture-header-text-colour: #2b2420;
|
|
216
|
+
--profile-section-picture-header-eyebrow-colour: #9a6b3f;
|
|
217
|
+
--profile-section-picture-header-padding: 3.2rem 3.2rem 6.4rem;
|
|
218
|
+
--profile-section-info-text-colour: #5a514b;
|
|
219
|
+
--profile-section-highlight-colour: #9a6b3f;
|
|
220
|
+
--profile-section-link-colour: #2b2420;
|
|
221
|
+
--profile-section-link-colour-hover: #9a6b3f;
|
|
222
|
+
--social-icons-list-icon-colour: #2b2420;
|
|
223
|
+
--eyebrow-text-font-family: "Poppins", sans-serif;
|
|
224
|
+
--eyebrow-text-font-style: normal;
|
|
225
|
+
--eyebrow-text-font-weight: 600;
|
|
226
|
+
--eyebrow-text-letter-spacing: 0.25em;
|
|
227
|
+
}
|
|
228
|
+
</component>
|
|
229
|
+
`;
|
|
230
|
+
|
|
231
|
+
const pictureHeaderRender = (themeClass: string, background: string): Story["render"] => (args) => ({
|
|
232
|
+
components: { ProfileSection, EyebrowText, HeroText, SocialIconsList },
|
|
233
|
+
setup() {
|
|
234
|
+
return { args, themedSocialIcons, themeClass };
|
|
235
|
+
},
|
|
236
|
+
template: `
|
|
237
|
+
${pictureThemes}
|
|
238
|
+
<div style="padding: 2.4rem; background: ${background};">
|
|
239
|
+
<ProfileSection v-bind="args" :style-class-passthrough="[themeClass]">
|
|
98
240
|
<template #eyebrowText>
|
|
99
|
-
<EyebrowText tag="p" font-size="
|
|
241
|
+
<EyebrowText tag="p" font-size="medium" text-content="About Alex" :style-class-passthrough="['mb-0']" />
|
|
100
242
|
</template>
|
|
101
243
|
<template #heroText="{ headingId }">
|
|
102
|
-
<HeroText
|
|
103
|
-
:id="headingId"
|
|
104
|
-
tag="h2"
|
|
105
|
-
axis="vertical"
|
|
106
|
-
font-size="display"
|
|
107
|
-
:text-content="[
|
|
108
|
-
{ text: 'Your', styleClass: 'normal' },
|
|
109
|
-
{ text: 'mobile hairdresser', styleClass: 'accent' },
|
|
110
|
-
{ text: 'in Bath', styleClass: 'normal' },
|
|
111
|
-
]"
|
|
112
|
-
:style-class-passthrough="['mb-20']"
|
|
113
|
-
/>
|
|
244
|
+
<HeroText :id="headingId" tag="h2" font-size="heading" :text-content="[{ text: 'Alex Morgan', styleClass: 'normal' }]" />
|
|
114
245
|
</template>
|
|
246
|
+
${infoSlots}
|
|
247
|
+
<template #profileLinks>
|
|
248
|
+
<SocialIconsList :items="themedSocialIcons" label="Alex Morgan on social media" />
|
|
249
|
+
</template>
|
|
250
|
+
</ProfileSection>
|
|
251
|
+
</div>
|
|
252
|
+
`,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
/** Header in picture, dark — eyebrow and heading at the bottom of the image, on a dark scrim. */
|
|
256
|
+
export const HeaderInPictureDark: Story = {
|
|
257
|
+
name: "Header In Picture (Dark, Bottom)",
|
|
258
|
+
args: {
|
|
259
|
+
tag: "section",
|
|
260
|
+
headerPlacement: "picture-bottom",
|
|
261
|
+
},
|
|
262
|
+
render: pictureHeaderRender("dark-overlay-profile", "#121010"),
|
|
263
|
+
parameters: {
|
|
264
|
+
docs: {
|
|
265
|
+
description: {
|
|
266
|
+
story:
|
|
267
|
+
"With `header-placement=\"picture-bottom\"`, the eyebrow and heading sit at the bottom of the picture on a scrim, like ServicesCard's `titles-within-image-wrapper`. A long header grows the frame rather than being clipped. " +
|
|
268
|
+
"Every value is a token set on a class added with `styleClassPassthrough`: `--profile-section-picture-header-*` for the scrim and header, `--profile-section-info-text-colour` and the highlight/link tokens for the info column, and EyebrowText's own `--eyebrow-text-*` tokens. Instagram uses `mdi:instagram` coloured with `--social-icons-list-icon-colour`, since `logos:instagram-icon` is near-black and vanishes here. Switch `headerPlacement` in Controls to move the header to the top.",
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/** Header in picture, light — eyebrow and heading at the top of the image, on a light scrim. */
|
|
275
|
+
export const HeaderInPictureLight: Story = {
|
|
276
|
+
name: "Header In Picture (Light, Top)",
|
|
277
|
+
args: {
|
|
278
|
+
tag: "section",
|
|
279
|
+
headerPlacement: "picture-top",
|
|
280
|
+
profilePicture: {
|
|
281
|
+
src: "/images/services/lowlights-1.webp",
|
|
282
|
+
alt: "Alex Morgan, hair stylist",
|
|
283
|
+
width: 828,
|
|
284
|
+
height: 958,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
render: pictureHeaderRender("light-overlay-profile", "#fbf8f4"),
|
|
288
|
+
parameters: {
|
|
289
|
+
docs: {
|
|
290
|
+
description: {
|
|
291
|
+
story:
|
|
292
|
+
"With `header-placement=\"picture-top\"`, the header sits at the top of the picture and the scrim fades downward. A light scrim colour needs a dark `--profile-section-picture-header-text-colour`; browsers with `contrast-color()` pick one automatically, but setting it keeps older browsers right too. The photo's top edge is a cream ceiling beam, so the cream scrim reads as part of the image: it stays solid for the first 45% (behind the text), then fades into `--profile-section-picture-header-scrim-target-colour`, a fully transparent version of the same cream, so it thins out into the pale background without greying. Over a darker photo, set the target to roughly the photo's colour where the fade ends. The extra end padding gives the fade room below the text.",
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/** With block headings — each profile-info block has its own h3 title. */
|
|
299
|
+
export const WithBlockHeadings: Story = {
|
|
300
|
+
name: "With Block Headings",
|
|
301
|
+
args: {
|
|
302
|
+
tag: "section",
|
|
303
|
+
},
|
|
304
|
+
render: (args) => ({
|
|
305
|
+
components: { ProfileSection, EyebrowText, HeroText, SocialIconsList },
|
|
306
|
+
setup() {
|
|
307
|
+
return { args, socialIcons };
|
|
308
|
+
},
|
|
309
|
+
template: `
|
|
310
|
+
<component is="style">
|
|
311
|
+
.profile-section.block-headings-profile {
|
|
312
|
+
--profile-section-info-block-gap: 3.2rem;
|
|
313
|
+
|
|
314
|
+
.about-block-heading {
|
|
315
|
+
margin-block: 0 0.8rem;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.profile-section-info-block p {
|
|
319
|
+
margin: 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
</component>
|
|
323
|
+
<ProfileSection v-bind="args" :style-class-passthrough="['block-headings-profile']">
|
|
324
|
+
${headerSlots}
|
|
115
325
|
<template #profile-info-1>
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
</div>
|
|
326
|
+
<h3 class="page-heading-4 about-block-heading">Experience</h3>
|
|
327
|
+
<p class="page-body-normal">With over 10 years of experience, Alex brings the salon to you, whether you're at home, in the office, or getting ready for a special occasion.</p>
|
|
119
328
|
</template>
|
|
120
329
|
<template #profile-info-2>
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
</div>
|
|
330
|
+
<h3 class="page-heading-4 about-block-heading">Where Alex works</h3>
|
|
331
|
+
<p class="page-body-normal location">Based in <span class="highlight">Northtown</span>, covering the surrounding area including <span class="highlight">Eastfield</span> and beyond.</p>
|
|
124
332
|
</template>
|
|
125
333
|
<template #profile-info-3>
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
</div>
|
|
334
|
+
<h3 class="page-heading-4 about-block-heading">Services</h3>
|
|
335
|
+
<p class="page-body-normal services">Specialising in <a href="#" class="highlight">cuts</a>, <a href="#" class="highlight">colour</a>, and <a href="#" class="highlight">styling</a> for all hair types.</p>
|
|
129
336
|
</template>
|
|
130
337
|
<template #profileLinks>
|
|
131
|
-
<
|
|
132
|
-
<a href="#">Book now</a>
|
|
338
|
+
<SocialIconsList :items="socialIcons" label="Alex Morgan on social media" />
|
|
133
339
|
</template>
|
|
134
340
|
</ProfileSection>
|
|
135
341
|
`,
|
|
136
342
|
}),
|
|
343
|
+
parameters: {
|
|
344
|
+
docs: {
|
|
345
|
+
description: {
|
|
346
|
+
story:
|
|
347
|
+
"Each `profile-info-N` slot is free-form, so a block can carry its own heading. Use `h3` under the section's `h2` to keep the heading order. The headings use the `page-heading-4` typography class, and the spacing is set in a consumer stylesheet on a class added with `styleClassPassthrough`, plus `--profile-section-info-block-gap` between blocks.",
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
137
351
|
};
|
|
138
352
|
|
|
139
|
-
/** No header slots —
|
|
353
|
+
/** No header slots — the header element is not rendered at all. */
|
|
140
354
|
export const WithoutHeader: Story = {
|
|
141
355
|
name: "Without Header Slots",
|
|
142
356
|
render: (args) => ({
|
|
143
|
-
components: { ProfileSection },
|
|
357
|
+
components: { ProfileSection, EyebrowText, HeroText, SocialIconsList },
|
|
144
358
|
setup() {
|
|
145
359
|
return { args };
|
|
146
360
|
},
|
|
147
361
|
template: `
|
|
148
362
|
<ProfileSection v-bind="args">
|
|
149
|
-
|
|
150
|
-
<p class="page-body-normal">Profile information block one.</p>
|
|
151
|
-
</template>
|
|
152
|
-
<template #profile-info-2>
|
|
153
|
-
<p class="page-body-normal">Profile information block two.</p>
|
|
154
|
-
</template>
|
|
155
|
-
<template #profile-info-3>
|
|
156
|
-
<p class="page-body-normal">Profile information block three.</p>
|
|
157
|
-
</template>
|
|
363
|
+
${infoSlots}
|
|
158
364
|
</ProfileSection>
|
|
159
365
|
`,
|
|
160
366
|
}),
|
|
161
367
|
};
|
|
162
368
|
|
|
163
|
-
/** Section tag — renders the root as a
|
|
369
|
+
/** Section tag — renders the root as a labelled section landmark. */
|
|
164
370
|
export const AsSectionTag: Story = {
|
|
165
371
|
name: "As section Tag",
|
|
166
372
|
args: {
|
|
167
373
|
tag: "section",
|
|
168
374
|
},
|
|
169
375
|
render: (args) => ({
|
|
170
|
-
components: { ProfileSection },
|
|
376
|
+
components: { ProfileSection, EyebrowText, HeroText, SocialIconsList },
|
|
171
377
|
setup() {
|
|
172
378
|
return { args };
|
|
173
379
|
},
|
|
174
380
|
template: `
|
|
175
381
|
<ProfileSection v-bind="args">
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
</template>
|
|
179
|
-
<template #heroText="{ headingId }">
|
|
180
|
-
<HeroText
|
|
181
|
-
:id="headingId"
|
|
182
|
-
tag="h2"
|
|
183
|
-
axis="horizontal"
|
|
184
|
-
font-size="large"
|
|
185
|
-
:text-content="[{ text: 'Profile section heading', styleClass: 'normal' }]"
|
|
186
|
-
/>
|
|
187
|
-
</template>
|
|
188
|
-
<template #profile-info-1>
|
|
189
|
-
<p class="page-body-normal">Profile information block one.</p>
|
|
190
|
-
</template>
|
|
191
|
-
<template #profile-info-2>
|
|
192
|
-
<p class="page-body-normal">Profile information block two.</p>
|
|
193
|
-
</template>
|
|
194
|
-
<template #profile-info-3>
|
|
195
|
-
<p class="page-body-normal">Profile information block three.</p>
|
|
196
|
-
</template>
|
|
382
|
+
${headerSlots}
|
|
383
|
+
${infoSlots}
|
|
197
384
|
</ProfileSection>
|
|
198
385
|
`,
|
|
199
386
|
}),
|
|
200
387
|
};
|
|
201
388
|
|
|
202
|
-
/**
|
|
203
|
-
export const
|
|
204
|
-
name: "
|
|
389
|
+
/** Narrow canvas — the same section stacked into one column. */
|
|
390
|
+
export const NarrowContainer: Story = {
|
|
391
|
+
name: "Narrow Container",
|
|
392
|
+
render: Default.render,
|
|
393
|
+
parameters: {
|
|
394
|
+
initialCanvas: "mobileCanvas",
|
|
395
|
+
docs: {
|
|
396
|
+
description: {
|
|
397
|
+
story: "At mobile width the picture stacks above the info column.",
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const unbroken = "Pneumonoultramicroscopicsilicovolcanoconiosis".repeat(3);
|
|
404
|
+
const longGerman =
|
|
405
|
+
"Mit über zehn Jahren Berufserfahrung als mobile Friseurmeisterin bringt Alex den Salon direkt zu Ihnen nach Hause, ins Büro oder zur Hochzeitsvorbereitung, Donaudampfschifffahrtsgesellschaftskapitänsmütze inklusive.";
|
|
406
|
+
|
|
407
|
+
export const StressTest: Story = {
|
|
408
|
+
name: "Stress Test (Worst-Case Data)",
|
|
205
409
|
args: {
|
|
206
|
-
|
|
410
|
+
tag: "section",
|
|
411
|
+
profilePicture: {
|
|
412
|
+
src: "/images/does-not-exist.jpg",
|
|
413
|
+
alt: "<b>Broken image</b> with HTML-like alt text that is also rather long",
|
|
414
|
+
width: -1,
|
|
415
|
+
height: 0,
|
|
416
|
+
},
|
|
207
417
|
},
|
|
208
418
|
render: (args) => ({
|
|
209
|
-
components: { ProfileSection },
|
|
419
|
+
components: { ProfileSection, EyebrowText, HeroText, SocialIconsList },
|
|
210
420
|
setup() {
|
|
211
|
-
return { args };
|
|
421
|
+
return { args, unbroken, longGerman };
|
|
212
422
|
},
|
|
213
423
|
template: `
|
|
214
424
|
<ProfileSection v-bind="args">
|
|
215
425
|
<template #eyebrowText>
|
|
216
|
-
<EyebrowText tag="p" font-size="large" text-content="
|
|
426
|
+
<EyebrowText tag="p" font-size="large" :text-content="unbroken" :style-class-passthrough="['mb-0']" />
|
|
217
427
|
</template>
|
|
218
428
|
<template #heroText="{ headingId }">
|
|
219
|
-
<HeroText
|
|
220
|
-
:id="headingId"
|
|
221
|
-
tag="h2"
|
|
222
|
-
axis="horizontal"
|
|
223
|
-
font-size="large"
|
|
224
|
-
:text-content="[{ text: 'Profile section heading', styleClass: 'normal' }]"
|
|
225
|
-
/>
|
|
429
|
+
<HeroText :id="headingId" tag="h2" font-size="title" :text-content="[{ text: longGerman, styleClass: 'normal' }]" />
|
|
226
430
|
</template>
|
|
227
|
-
<template #profile-info-
|
|
228
|
-
<p class="page-body-normal">
|
|
431
|
+
<template #profile-info-12>
|
|
432
|
+
<p class="page-body-normal">Slot 12 (should render last, after 2)</p>
|
|
229
433
|
</template>
|
|
230
434
|
<template #profile-info-2>
|
|
231
|
-
<p class="page-body-normal">
|
|
435
|
+
<p class="page-body-normal location">Covering <span class="highlight">{{ unbroken }}</span> and https://example.com/a/very/long/url/without/any/spaces/that/keeps/going/and/going</p>
|
|
436
|
+
</template>
|
|
437
|
+
<template #profile-info-1>
|
|
438
|
+
<p class="page-body-normal">{{ longGerman }} {{ longGerman }}</p>
|
|
439
|
+
<p class="page-body-normal" dir="rtl">مرحبا بكم في صالوننا، نحن نقدم جميع خدمات الشعر 🦄💇♀️</p>
|
|
440
|
+
<p class="page-body-normal"><script>alert(1)</script> renders as text</p>
|
|
441
|
+
</template>
|
|
442
|
+
<template #profileLinks>
|
|
443
|
+
<a v-for="n in 12" :key="n" href="#">Link number {{ n }}</a>
|
|
444
|
+
<a href="#">{{ unbroken }}</a>
|
|
445
|
+
</template>
|
|
446
|
+
</ProfileSection>
|
|
447
|
+
<ProfileSection v-bind="args" header-placement="picture-bottom" :profile-picture="{ src: '/images/services/service-colour.jpg', alt: 'Header within picture' }" style="margin-block-start: 4.8rem;">
|
|
448
|
+
<template #eyebrowText>
|
|
449
|
+
<EyebrowText tag="p" font-size="medium" :text-content="unbroken" :style-class-passthrough="['mb-0']" />
|
|
450
|
+
</template>
|
|
451
|
+
<template #heroText="{ headingId }">
|
|
452
|
+
<HeroText :id="headingId" tag="h2" font-size="title" :text-content="[{ text: longGerman + ' ' + longGerman, styleClass: 'normal' }]" />
|
|
453
|
+
</template>
|
|
454
|
+
<template #profile-info-1>
|
|
455
|
+
<p class="page-body-normal">The header above is long enough to overflow the 3:4 frame: the frame should grow to fit it, not clip it.</p>
|
|
232
456
|
</template>
|
|
233
457
|
</ProfileSection>
|
|
234
458
|
`,
|
|
235
459
|
}),
|
|
460
|
+
parameters: {
|
|
461
|
+
// Fixed showcase: hardcoded values, so Controls would do nothing here.
|
|
462
|
+
controls: { disable: true },
|
|
463
|
+
docs: {
|
|
464
|
+
description: {
|
|
465
|
+
story:
|
|
466
|
+
"Hostile content: a broken image with an invalid width and height and a long HTML-like alt, an unbroken eyebrow, a long German heading, " +
|
|
467
|
+
"info slots given out of order with a gap (1, 2, 12), unbroken words and a long URL inside a `.location .highlight`, right-to-left text with emoji, " +
|
|
468
|
+
"and a dozen profile links plus an unbroken one. Check at every canvas width that nothing overflows the dashed outline, the picture column " +
|
|
469
|
+
"never pushes the info column off-screen, the info blocks render in numeric order, the links wrap, and the broken image keeps its 3:4 frame. " +
|
|
470
|
+
"The second section puts an oversized header within the picture: the frame should grow taller than 3:4 to fit it, with nothing clipped.",
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
},
|
|
236
474
|
};
|