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.
Files changed (66) hide show
  1. package/.claude/rules/consumer/consumer-overrides.md +17 -0
  2. package/.claude/rules/consumer/css-comment-slash.md +13 -0
  3. package/.claude/rules/consumer/css-native-nesting.md +13 -0
  4. package/.claude/rules/consumer/css-no-deep.md +13 -0
  5. package/.claude/rules/consumer/css-rem-base.md +11 -0
  6. package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
  7. package/.claude/rules/consumer/css-webkit-support.md +15 -0
  8. package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
  9. package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
  10. package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
  11. package/.claude/rules/consumer/story-args.md +12 -0
  12. package/.claude/rules/consumer/story-register-components.md +12 -0
  13. package/.claude/rules/consumer/vue-auto-imports.md +14 -0
  14. package/.claude/rules/consumer/vue-no-self-closing.md +11 -0
  15. package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
  16. package/.claude/rules/consumer/vue-props-pattern.md +13 -0
  17. package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
  18. package/.claude/rules/consumer/vue-video-source.md +12 -0
  19. package/.claude/skills/component-compliance-checklist.md +37 -2
  20. package/.claude/skills/component-naming.md +1 -1
  21. package/.claude/skills/components/display-avatar.md +14 -3
  22. package/.claude/skills/components/display-chip.md +2 -1
  23. package/.claude/skills/components/google-reviews.md +2 -2
  24. package/.claude/skills/components/pricing-card.md +68 -84
  25. package/.claude/skills/components/profile-section.md +59 -21
  26. package/.claude/skills/components/social-icons-list.md +2 -0
  27. package/.claude/skills/index.md +13 -6
  28. package/.claude/skills/new-app-scaffold.md +4 -2
  29. package/.claude/skills/setup-postinstall.md +5 -4
  30. package/.claude/skills/storybook-add-story.md +38 -0
  31. package/.claude/skills/theming-component-token-pattern.md +94 -0
  32. package/.claude/skills/theming-partial-override.md +1 -1
  33. package/.vscode/srcdev-component-display-avatar.code-snippets +7 -0
  34. package/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
  35. package/.vscode/srcdev-component-profile-section.code-snippets +27 -4
  36. package/README.md +5 -5
  37. package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
  38. package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
  39. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
  40. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
  41. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +2 -0
  42. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +26 -10
  43. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +75 -34
  44. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +84 -3
  45. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +85 -13
  46. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +3 -3
  47. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +7 -2
  48. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +0 -1
  49. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
  50. package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
  51. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
  52. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
  53. package/app/components/02.molecules/profile-section/CONSUMER-STYLING.md +103 -13
  54. package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
  55. package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
  56. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
  57. package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
  58. package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +6 -2
  59. package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
  60. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
  61. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
  62. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
  63. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
  64. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
  65. package/app/composables/useAriaDescribedById.ts +1 -1
  66. package/package.json +2 -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>