srcdev-nuxt-components 9.6.1 → 9.6.2

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 (27) hide show
  1. package/.claude/skills/component-compliance-checklist.md +14 -1
  2. package/.claude/skills/components/display-avatar.md +14 -3
  3. package/.claude/skills/components/display-chip.md +2 -1
  4. package/.claude/skills/components/google-reviews.md +2 -2
  5. package/.claude/skills/components/profile-section.md +59 -21
  6. package/.claude/skills/components/social-icons-list.md +2 -0
  7. package/.claude/skills/index.md +4 -3
  8. package/.claude/skills/storybook-add-story.md +2 -0
  9. package/.claude/skills/theming-component-token-pattern.md +48 -0
  10. package/.vscode/srcdev-component-display-avatar.code-snippets +7 -0
  11. package/.vscode/srcdev-component-profile-section.code-snippets +27 -4
  12. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +26 -10
  13. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +75 -34
  14. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +84 -3
  15. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +85 -13
  16. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +3 -3
  17. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +7 -2
  18. package/app/components/02.molecules/profile-section/CONSUMER-STYLING.md +103 -13
  19. package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
  20. package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
  21. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
  22. package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
  23. package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +6 -2
  24. package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
  25. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
  26. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
  27. package/package.json +1 -1
@@ -1,17 +1,27 @@
1
1
  <template>
2
- <component :is="tag" class="profile-section" :class="[elementClasses]" :aria-labelledby="ariaLabelledby">
3
- <header class="profile-section-header">
2
+ <component
3
+ :is="tag"
4
+ class="profile-section"
5
+ :class="[elementClasses]"
6
+ :aria-labelledby="ariaLabelledby"
7
+ :data-header-placement="headerPlacement"
8
+ >
9
+ <header v-if="hasHeader() && !headerInPicture" class="profile-section-header">
4
10
  <slot v-if="hasEyebrowTextSlot()" name="eyebrowText"></slot>
5
11
  <slot v-if="hasHeroTextSlot()" name="heroText" :heading-id="headingId"></slot>
6
12
  </header>
7
13
 
8
14
  <div class="profile-section-inner">
9
15
  <div class="profile-section-picture">
16
+ <header v-if="hasHeader() && headerInPicture" class="profile-section-header">
17
+ <slot v-if="hasEyebrowTextSlot()" name="eyebrowText"></slot>
18
+ <slot v-if="hasHeroTextSlot()" name="heroText" :heading-id="headingId"></slot>
19
+ </header>
10
20
  <NuxtImg
11
21
  :src="profilePicture.src"
12
22
  :alt="profilePicture.alt"
13
- :width="profilePicture.width ?? 828"
14
- :height="profilePicture.height ?? 1104"
23
+ :width="imageWidth"
24
+ :height="imageHeight"
15
25
  class="profile-section-image"
16
26
  />
17
27
  </div>
@@ -36,12 +46,12 @@ import type { ProfilePicture } from "~/types/components";
36
46
  interface Props {
37
47
  tag?: "div" | "section" | "article" | "main";
38
48
  profilePicture: ProfilePicture;
39
- profileInfoCount?: number;
49
+ headerPlacement?: "above" | "picture-bottom" | "picture-top";
40
50
  styleClassPassthrough?: string | string[];
41
51
  }
42
52
  const props = withDefaults(defineProps<Props>(), {
43
53
  tag: "div",
44
- profileInfoCount: 3,
54
+ headerPlacement: "above",
45
55
  styleClassPassthrough: () => [],
46
56
  });
47
57
 
@@ -50,16 +60,21 @@ const { headingId, ariaLabelledby } = useAriaLabelledById(() => props.tag);
50
60
  const slots = useSlots();
51
61
  const hasEyebrowTextSlot = () => Boolean(slots.eyebrowText);
52
62
  const hasHeroTextSlot = () => Boolean(slots.heroText);
63
+ const hasHeader = () => hasEyebrowTextSlot() || hasHeroTextSlot();
53
64
  const hasProfileLinksSlot = () => Boolean(slots.profileLinks);
54
65
 
55
- const profileInfoSlots = () => {
56
- const provided = Object.keys(slots)
66
+ const headerInPicture = computed(() => props.headerPlacement !== "above");
67
+
68
+ const profileInfoSlots = () =>
69
+ Object.keys(slots)
57
70
  .filter((key) => /^profile-info-\d+$/.test(key))
58
71
  .sort((a, b) => parseInt(a.split("-")[2] ?? "0") - parseInt(b.split("-")[2] ?? "0"));
59
- return provided.length > 0
60
- ? provided
61
- : Array.from({ length: props.profileInfoCount }, (_, i) => `profile-info-${i + 1}`);
62
- };
72
+
73
+ const positiveOr = (value: number | undefined, fallback: number) =>
74
+ typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.round(value) : fallback;
75
+
76
+ const imageWidth = computed(() => positiveOr(props.profilePicture.width, 828));
77
+ const imageHeight = computed(() => positiveOr(props.profilePicture.height, 1104));
63
78
 
64
79
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
65
80
 
@@ -74,57 +89,142 @@ watch(
74
89
  <style lang="css">
75
90
  @layer components {
76
91
  .profile-section {
92
+ container: profile-section / inline-size;
93
+ overflow-wrap: anywhere;
94
+
77
95
  .profile-section-inner {
78
96
  display: grid;
79
- grid-template-columns: 1fr;
97
+ grid-template-columns: minmax(0, 1fr);
80
98
  gap: var(--profile-section-gap, 2rem);
81
99
 
82
- @media (min-width: 768px) {
83
- grid-template-columns: var(--profile-section-picture-width, 384px) 1fr;
100
+ @container profile-section (min-width: 768px) {
101
+ grid-template-columns: minmax(0, var(--profile-section-picture-width, 38.4rem)) minmax(0, 1fr);
84
102
  align-items: start;
85
103
  gap: var(--profile-section-gap-wide, 4rem);
86
104
  }
87
105
 
88
106
  .profile-section-picture {
107
+ position: relative;
89
108
  aspect-ratio: var(--profile-section-picture-aspect-ratio, 3 / 4);
90
- border-radius: var(--profile-section-picture-border-radius, 8px);
109
+ border-radius: var(--profile-section-picture-border-radius, 0.8rem);
91
110
  overflow: hidden;
92
111
 
93
112
  .profile-section-image {
113
+ position: absolute;
114
+ inset: 0;
94
115
  object-fit: cover;
95
116
  width: 100%;
96
117
  height: 100%;
118
+ border-radius: inherit;
97
119
  }
98
120
  }
99
121
 
122
+ .profile-section-info {
123
+ color: var(--profile-section-info-text-colour, inherit);
124
+ }
125
+
100
126
  .profile-section-info-block {
101
127
  margin-block-end: var(--profile-section-info-block-gap, 1.5rem);
102
128
 
129
+ .location .highlight,
130
+ .services .highlight {
131
+ font-weight: var(--profile-section-highlight-font-weight, 600);
132
+ font-variation-settings: "wght" var(--profile-section-highlight-font-weight, 600);
133
+ }
134
+
103
135
  .location .highlight {
104
- color: var(--colour-text-accent);
105
- font-weight: 600;
106
- font-variation-settings: "wght" 600;
136
+ color: var(--profile-section-highlight-colour, var(--colour-text-accent));
107
137
  }
108
138
 
109
139
  .services .highlight {
110
- color: var(--colour-link-default);
111
- font-weight: 600;
112
- font-variation-settings: "wght" 600;
140
+ color: var(--profile-section-link-colour, var(--colour-link-default));
113
141
 
114
142
  &:hover {
115
- color: var(--colour-link-hover);
143
+ color: var(--profile-section-link-colour-hover, var(--colour-link-hover));
116
144
  }
117
145
  }
118
146
  }
119
147
 
120
148
  .profile-section-links {
121
149
  display: flex;
150
+ flex-wrap: wrap;
122
151
  flex-grow: 1;
123
152
  gap: var(--profile-section-links-gap, 1rem);
124
153
  align-items: end;
125
154
  justify-content: flex-end;
126
155
  }
127
156
  }
157
+
158
+ &:is([data-header-placement="picture-bottom"], [data-header-placement="picture-top"]) .profile-section-picture {
159
+ /* Visible overflow keeps aspect-ratio a minimum, so a long header grows the frame instead of being clipped. */
160
+ --_scrim-colour: var(--profile-section-picture-header-scrim-colour, #000);
161
+ --_scrim-direction: to top;
162
+ --_scrim-start-colour: color-mix(
163
+ in oklab,
164
+ var(--_scrim-colour) var(--profile-section-picture-header-scrim-opacity, 75%),
165
+ transparent
166
+ );
167
+ --_scrim-solid-stop: var(--profile-section-picture-header-scrim-solid-stop, 0%);
168
+ --_scrim-target-colour: var(--profile-section-picture-header-scrim-target-colour, transparent);
169
+
170
+ display: flex;
171
+ flex-direction: column;
172
+ justify-content: flex-end;
173
+ overflow: visible;
174
+
175
+ .profile-section-header {
176
+ position: relative;
177
+ z-index: 1;
178
+ display: grid;
179
+ gap: var(--profile-section-picture-header-gap, 0.4rem);
180
+ padding: var(--profile-section-picture-header-padding, 2.4rem);
181
+ border-end-start-radius: inherit;
182
+ border-end-end-radius: inherit;
183
+ color: var(--profile-section-picture-header-text-colour, #fff);
184
+ background: var(
185
+ --profile-section-picture-header-scrim,
186
+ linear-gradient(
187
+ var(--_scrim-direction),
188
+ rgb(0 0 0 / 0.75),
189
+ rgb(0 0 0 / 0.75) var(--_scrim-solid-stop),
190
+ var(--_scrim-target-colour)
191
+ )
192
+ );
193
+
194
+ @supports (color: color-mix(in oklab, red, transparent)) {
195
+ background: var(
196
+ --profile-section-picture-header-scrim,
197
+ linear-gradient(
198
+ var(--_scrim-direction),
199
+ var(--_scrim-start-colour),
200
+ var(--_scrim-start-colour) var(--_scrim-solid-stop),
201
+ var(--_scrim-target-colour)
202
+ )
203
+ );
204
+ }
205
+
206
+ @supports (color: contrast-color(red)) {
207
+ color: var(--profile-section-picture-header-text-colour, contrast-color(var(--_scrim-colour)));
208
+ }
209
+
210
+ .eyebrow-text {
211
+ --eyebrow-text-bg-img: none;
212
+ color: var(--profile-section-picture-header-eyebrow-colour, inherit);
213
+ }
214
+ }
215
+ }
216
+
217
+ &[data-header-placement="picture-top"] .profile-section-picture {
218
+ --_scrim-direction: to bottom;
219
+
220
+ justify-content: flex-start;
221
+
222
+ .profile-section-header {
223
+ border-radius: 0;
224
+ border-start-start-radius: inherit;
225
+ border-start-end-radius: inherit;
226
+ }
227
+ }
128
228
  }
129
229
  }
130
230
  </style>