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,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { h } from "vue";
2
+ import { defineComponent, h, nextTick, ref } from "vue";
3
3
  import { mountSuspended } from "@nuxt/test-utils/runtime";
4
4
  import ProfileSection from "../ProfileSection.vue";
5
5
 
@@ -52,25 +52,28 @@ describe("ProfileSection", () => {
52
52
  expect(img.attributes("alt")).toBe("Jane Doe");
53
53
  });
54
54
 
55
- it("renders 3 profile-info blocks by default", async () => {
55
+ it("renders no profile-info blocks when no profile-info slots are given", async () => {
56
56
  const wrapper = await mountSuspended(ProfileSection, {
57
57
  props: defaultProps,
58
58
  });
59
- const blocks = wrapper.findAll(".profile-section-info-block");
60
- expect(blocks.length).toBe(3);
59
+ expect(wrapper.findAll(".profile-section-info-block").length).toBe(0);
61
60
  });
62
61
 
63
- it("renders the correct number of profile-info blocks from profileInfoCount", async () => {
62
+ it("renders one block per provided profile-info slot, in numeric order, with gaps", async () => {
64
63
  const wrapper = await mountSuspended(ProfileSection, {
65
- props: { ...defaultProps, profileInfoCount: 5 },
64
+ props: defaultProps,
65
+ slots: {
66
+ "profile-info-10": "<p>Ten</p>",
67
+ "profile-info-3": "<p>Three</p>",
68
+ "profile-info-1": "<p>One</p>",
69
+ },
66
70
  });
67
- const blocks = wrapper.findAll(".profile-section-info-block");
68
- expect(blocks.length).toBe(5);
71
+ expect(wrapper.findAll(".profile-section-info-block").map((b) => b.text())).toEqual(["One", "Three", "Ten"]);
69
72
  });
70
73
 
71
74
  it("renders named profile-info slot content", async () => {
72
75
  const wrapper = await mountSuspended(ProfileSection, {
73
- props: { ...defaultProps, profileInfoCount: 2 },
76
+ props: defaultProps,
74
77
  slots: {
75
78
  "profile-info-1": "<p>First info block</p>",
76
79
  "profile-info-2": "<p>Second info block</p>",
@@ -91,11 +94,11 @@ describe("ProfileSection", () => {
91
94
  const wrapper = await mountSuspended(ProfileSection, {
92
95
  props: defaultProps,
93
96
  slots: {
94
- eyebrowText: "<p class='eyebrow'>About Natasha</p>",
97
+ eyebrowText: "<p class='eyebrow'>About the author</p>",
95
98
  },
96
99
  });
97
100
  expect(wrapper.find(".eyebrow").exists()).toBe(true);
98
- expect(wrapper.html()).toContain("About Natasha");
101
+ expect(wrapper.html()).toContain("About the author");
99
102
  });
100
103
 
101
104
  it("does not render heroText slot area when slot is not provided", async () => {
@@ -188,4 +191,134 @@ describe("ProfileSection", () => {
188
191
  expect(el.classes()).toContain("extra-class");
189
192
  expect(el.classes()).toContain("another-class");
190
193
  });
194
+
195
+ // ─── Stress test 2026-10-14 ────────────────────────────────────────────────
196
+
197
+ it("does not render an empty header when neither header slot is given", async () => {
198
+ const wrapper = await mountSuspended(ProfileSection, { props: defaultProps });
199
+ expect(wrapper.find(".profile-section-header").exists()).toBe(false);
200
+ });
201
+
202
+ it("adds and removes the header and info blocks when slots are toggled after mount", async () => {
203
+ const show = ref(false);
204
+ const Host = defineComponent({
205
+ components: { ProfileSection },
206
+ setup: () => ({ show, defaultProps }),
207
+ template: `
208
+ <ProfileSection v-bind="defaultProps">
209
+ <template v-if="show" #heroText><h2>Heading</h2></template>
210
+ <template v-if="show" #profile-info-1><p>Info</p></template>
211
+ </ProfileSection>
212
+ `,
213
+ });
214
+ const wrapper = await mountSuspended(Host);
215
+ expect(wrapper.find(".profile-section-header").exists()).toBe(false);
216
+ expect(wrapper.findAll(".profile-section-info-block").length).toBe(0);
217
+
218
+ show.value = true;
219
+ await nextTick();
220
+ expect(wrapper.find(".profile-section-header").exists()).toBe(true);
221
+ expect(wrapper.findAll(".profile-section-info-block").length).toBe(1);
222
+
223
+ show.value = false;
224
+ await nextTick();
225
+ expect(wrapper.find(".profile-section-header").exists()).toBe(false);
226
+ expect(wrapper.findAll(".profile-section-info-block").length).toBe(0);
227
+ });
228
+
229
+ it("uses the given picture width and height", async () => {
230
+ const wrapper = await mountSuspended(ProfileSection, {
231
+ props: { profilePicture: { src: "/a.jpg", alt: "A", width: 640, height: 480 } },
232
+ });
233
+ expect(wrapper.find("img").attributes("width")).toBe("640");
234
+ expect(wrapper.find("img").attributes("height")).toBe("480");
235
+ });
236
+
237
+ it.each([0, -100, Number.NaN, Number.POSITIVE_INFINITY])(
238
+ "falls back to 828x1104 for an invalid picture size (%s)",
239
+ async (size) => {
240
+ const wrapper = await mountSuspended(ProfileSection, {
241
+ props: { profilePicture: { src: "/a.jpg", alt: "A", width: size, height: size } },
242
+ });
243
+ expect(wrapper.find("img").attributes("width")).toBe("828");
244
+ expect(wrapper.find("img").attributes("height")).toBe("1104");
245
+ }
246
+ );
247
+
248
+ it("renders HTML-like alt text as text", async () => {
249
+ const wrapper = await mountSuspended(ProfileSection, {
250
+ props: { profilePicture: { src: "/a.jpg", alt: "<script>alert(1)</script>" } },
251
+ });
252
+ expect(wrapper.find("script").exists()).toBe(false);
253
+ expect(wrapper.find("img").attributes("alt")).toBe("<script>alert(1)</script>");
254
+ });
255
+
256
+ // ─── headerPlacement ─────────────────────────────────────────────────────────────────────────────────────────────────────────
257
+
258
+ const headerSlots = {
259
+ eyebrowText: "<p class='eyebrow'>About</p>",
260
+ heroText: (props: Record<string, unknown>) => h("h2", { id: props.headingId, class: "hero-heading" }, "Heading"),
261
+ };
262
+
263
+ it("renders the header above the grid by default", async () => {
264
+ const wrapper = await mountSuspended(ProfileSection, { props: defaultProps, slots: headerSlots });
265
+ expect(wrapper.find(".profile-section > .profile-section-header").exists()).toBe(true);
266
+ expect(wrapper.find(".profile-section-picture .profile-section-header").exists()).toBe(false);
267
+ expect(wrapper.attributes("data-header-placement")).toBe("above");
268
+ });
269
+
270
+ it("renders the header inside the picture frame, before the image, for picture-bottom", async () => {
271
+ const wrapper = await mountSuspended(ProfileSection, {
272
+ props: { ...defaultProps, headerPlacement: "picture-bottom" },
273
+ slots: headerSlots,
274
+ });
275
+ expect(wrapper.attributes("data-header-placement")).toBe("picture-bottom");
276
+ expect(wrapper.findAll(".profile-section-header").length).toBe(1);
277
+ const picture = wrapper.find(".profile-section-picture");
278
+ expect(picture.element.firstElementChild?.classList.contains("profile-section-header")).toBe(true);
279
+ expect(picture.find(".eyebrow").exists()).toBe(true);
280
+ expect(picture.find(".hero-heading").exists()).toBe(true);
281
+ });
282
+
283
+ it("keeps aria-labelledby pointing at the heading when it is inside the picture", async () => {
284
+ const wrapper = await mountSuspended(ProfileSection, {
285
+ props: { ...defaultProps, tag: "section", headerPlacement: "picture-bottom" },
286
+ slots: headerSlots,
287
+ });
288
+ expect(wrapper.attributes("aria-labelledby")).toBe(wrapper.find(".hero-heading").attributes("id"));
289
+ });
290
+
291
+ it("renders no header in the picture when no header slot is given", async () => {
292
+ const wrapper = await mountSuspended(ProfileSection, {
293
+ props: { ...defaultProps, headerPlacement: "picture-bottom" },
294
+ });
295
+ expect(wrapper.find(".profile-section-header").exists()).toBe(false);
296
+ });
297
+
298
+ it("moves the header when headerPlacement changes after mount", async () => {
299
+ const wrapper = await mountSuspended(ProfileSection, { props: defaultProps, slots: headerSlots });
300
+ await wrapper.setProps({ headerPlacement: "picture-bottom" });
301
+ expect(wrapper.find(".profile-section-picture .profile-section-header").exists()).toBe(true);
302
+ expect(wrapper.find(".profile-section > .profile-section-header").exists()).toBe(false);
303
+ });
304
+
305
+ it("renders the header inside the picture for picture-top too", async () => {
306
+ const wrapper = await mountSuspended(ProfileSection, {
307
+ props: { ...defaultProps, headerPlacement: "picture-top" },
308
+ slots: headerSlots,
309
+ });
310
+ expect(wrapper.attributes("data-header-placement")).toBe("picture-top");
311
+ expect(wrapper.find(".profile-section-picture > .profile-section-header").exists()).toBe(true);
312
+ expect(wrapper.find(".profile-section > .profile-section-header").exists()).toBe(false);
313
+ });
314
+
315
+ it("moves the header back above the grid when headerPlacement returns to above", async () => {
316
+ const wrapper = await mountSuspended(ProfileSection, {
317
+ props: { ...defaultProps, headerPlacement: "picture-top" },
318
+ slots: headerSlots,
319
+ });
320
+ await wrapper.setProps({ headerPlacement: "above" });
321
+ expect(wrapper.find(".profile-section > .profile-section-header").exists()).toBe(true);
322
+ expect(wrapper.find(".profile-section-picture .profile-section-header").exists()).toBe(false);
323
+ });
191
324
  });
@@ -1,19 +1,14 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`ProfileSection > renders correct HTML structure 1`] = `
4
- "<div class="profile-section custom-class">
5
- <header class="profile-section-header">
6
- <!--v-if-->
7
- <!--v-if-->
8
- </header>
4
+ "<div class="profile-section custom-class" data-header-placement="above">
5
+ <!--v-if-->
9
6
  <div class="profile-section-inner">
10
- <div class="profile-section-picture"><img width="828" height="1104" data-nuxt-img="" srcset="/_ipx/s_828x1104/images/test.jpg 1x, /_ipx/s_1656x2208/images/test.jpg 2x" alt="Test profile picture" class="profile-section-image" src="/_ipx/s_828x1104/images/test.jpg"></div>
7
+ <div class="profile-section-picture">
8
+ <!--v-if--><img width="828" height="1104" data-nuxt-img="" srcset="/_ipx/s_828x1104/images/test.jpg 1x, /_ipx/s_1656x2208/images/test.jpg 2x" alt="Test profile picture" class="profile-section-image" src="/_ipx/s_828x1104/images/test.jpg">
9
+ </div>
11
10
  <div class="profile-section-info">
12
- <div class="profile-section-info-content">
13
- <div class="profile-section-info-block"></div>
14
- <div class="profile-section-info-block"></div>
15
- <div class="profile-section-info-block"></div>
16
- </div>
11
+ <div class="profile-section-info-content"></div>
17
12
  <!--v-if-->
18
13
  </div>
19
14
  </div>
@@ -6,12 +6,16 @@
6
6
  |---|---|---|
7
7
  | `--social-icons-list-icon-size` | `2.4rem` | Icon size (applied as `font-size`) |
8
8
  | `--social-icons-list-gap` | `1.2rem` | Gap between icons |
9
+ | `--social-icons-list-icon-colour` | `currentColor` | Colour of mono icons (sets that draw in `currentColor`, e.g. `mdi:instagram`). No effect on `logos:` icons, whose colours are fixed in the SVG |
9
10
  | `--social-icons-list-hover-scale` | `1.15` | Scale on hover/focus (no scaling under `prefers-reduced-motion: reduce`) |
10
11
  | `--social-icons-list-hover-opacity` | `0.85` | Opacity on hover/focus |
11
12
 
12
- The focus outline uses `--theme-ring`, falling back to `currentColor`. Icon colour is whatever the
13
- Iconify icon renders (`logos:` icons carry their own brand colours; mono sets follow `color`).
13
+ The focus outline uses `--theme-ring`, falling back to `currentColor`. `logos:` icons carry their own brand colours. Some are dark (`logos:instagram-icon`, `logos:x`,
14
+ `logos:github-icon`) and vanish on a dark background: use a mono icon there (`mdi:instagram`,
15
+ `bi:instagram`, ...) and colour it with `--social-icons-list-icon-colour`.
14
16
 
17
+ > Changed 2026-10-14: new `--social-icons-list-icon-colour` token for mono icons.
18
+ >
15
19
  > Changed 2026-09-28: `--theme-social-icon-size` and `--theme-social-icon-gap` were renamed to
16
20
  > `--social-icons-list-icon-size` and `--social-icons-list-gap`; the old names no longer work. The
17
21
  > icon is now sized with `font-size` (it used an inline `width`/`height` style). The hover tokens
@@ -82,6 +82,7 @@ watch(
82
82
 
83
83
  .social-icon {
84
84
  font-size: var(--social-icons-list-icon-size, 2.4rem);
85
+ color: var(--social-icons-list-icon-colour, currentColor);
85
86
  }
86
87
  }
87
88
  }
@@ -87,6 +87,10 @@ export default {
87
87
  description: "This is a toast notification message.",
88
88
  customIcon: "",
89
89
  },
90
+ parameters: {
91
+ // Own iframe on the Docs page: each story opens a viewport-fixed toast on mount.
92
+ docs: { story: { inline: false, height: "320px" } },
93
+ },
90
94
  } as Meta<ToastStoryArgs>;
91
95
 
92
96
  const Template: StoryFn<ToastStoryArgs> = (args) => ({
@@ -74,6 +74,10 @@ export default {
74
74
  autoDismiss: true,
75
75
  duration: 4000,
76
76
  },
77
+ parameters: {
78
+ // Own iframe on the Docs page: toasts are fixed to the viewport.
79
+ docs: { story: { inline: false, height: "400px" } },
80
+ },
77
81
  } as Meta<StoryArgs>;
78
82
 
79
83
  const Template: StoryFn<StoryArgs> = (args) => ({
@@ -2,7 +2,7 @@
2
2
 
3
3
  exports[`GoogleReviewCard > renders correct HTML structure 1`] = `
4
4
  "<article class="google-review-card">
5
- <div class="google-review-card-author"><span class="display-avatar md" aria-hidden="true"><img data-nuxt-img="" srcset="/_ipx/_/https://lh3.googleusercontent.com/a 1x, /_ipx/_/https://lh3.googleusercontent.com/a 2x" alt="" class="display-avatar-image" src="/_ipx/_/https://lh3.googleusercontent.com/a"></span><a id="v-0-0" href="https://maps.google.com/contrib/1" class="google-review-card-author-name">Alex Doe</a><time datetime="2026-09-18T10:00:00Z" class="google-review-card-time">2 weeks ago</time></div>
5
+ <div class="google-review-card-author"><span class="display-avatar" data-size="md" aria-hidden="true"><span class="display-avatar-surface"><img width="112" height="112" data-nuxt-img="" srcset="/_ipx/s_112x112/https://lh3.googleusercontent.com/a 1x, /_ipx/s_224x224/https://lh3.googleusercontent.com/a 2x" alt="" class="display-avatar-image" src="/_ipx/s_112x112/https://lh3.googleusercontent.com/a"></span></span><a id="v-0-0" href="https://maps.google.com/contrib/1" class="google-review-card-author-name">Alex Doe</a><time datetime="2026-09-18T10:00:00Z" class="google-review-card-time">2 weeks ago</time></div>
6
6
  <div class="google-review-card-rating"><span class="google-review-card-stars" style="--_rating: 4;" aria-hidden="true">★★★★★</span><span class="sr-only">Rated 4 out of 5</span></div>
7
7
  <p class="google-review-card-text">Lovely visit, would come again.</p><a href="https://maps.google.com/review/1" aria-describedby="v-0-0" class="google-review-card-link">Read on Google</a>
8
8
  </article>"
@@ -9,8 +9,8 @@ exports[`GoogleReviews > renders correct HTML structure 1`] = `
9
9
  <ul class="google-reviews-list" tabindex="0">
10
10
  <li class="google-reviews-item">
11
11
  <article class="google-review-card">
12
- <div class="google-review-card-author"><span class="display-avatar md" aria-hidden="true"><span>R1</span>
13
- <!--v-if--></span><a id="v-0-0-0" href="https://maps.google.com/contrib/1" class="google-review-card-author-name">Reviewer 1</a><time datetime="2026-09-18T10:00:00Z" class="google-review-card-time">1 weeks ago</time>
12
+ <div class="google-review-card-author"><span class="display-avatar" data-size="md" aria-hidden="true"><span class="display-avatar-surface"><span class="display-avatar-fallback">R1</span>
13
+ <!--v-if--></span></span><a id="v-0-0-0" href="https://maps.google.com/contrib/1" class="google-review-card-author-name">Reviewer 1</a><time datetime="2026-09-18T10:00:00Z" class="google-review-card-time">1 weeks ago</time>
14
14
  </div>
15
15
  <div class="google-review-card-rating"><span class="google-review-card-stars" style="--_rating: 5;" aria-hidden="true">★★★★★</span><span class="sr-only">Rated 5 out of 5</span></div>
16
16
  <p class="google-review-card-text">Review text 1</p><a href="https://maps.google.com/review/1" aria-describedby="v-0-0-0" class="google-review-card-link">Read on Google</a>
@@ -18,8 +18,8 @@ exports[`GoogleReviews > renders correct HTML structure 1`] = `
18
18
  </li>
19
19
  <li class="google-reviews-item">
20
20
  <article class="google-review-card">
21
- <div class="google-review-card-author"><span class="display-avatar md" aria-hidden="true"><span>R2</span>
22
- <!--v-if--></span><a id="v-0-0-1" href="https://maps.google.com/contrib/2" class="google-review-card-author-name">Reviewer 2</a><time datetime="2026-09-18T10:00:00Z" class="google-review-card-time">2 weeks ago</time>
21
+ <div class="google-review-card-author"><span class="display-avatar" data-size="md" aria-hidden="true"><span class="display-avatar-surface"><span class="display-avatar-fallback">R2</span>
22
+ <!--v-if--></span></span><a id="v-0-0-1" href="https://maps.google.com/contrib/2" class="google-review-card-author-name">Reviewer 2</a><time datetime="2026-09-18T10:00:00Z" class="google-review-card-time">2 weeks ago</time>
23
23
  </div>
24
24
  <div class="google-review-card-rating"><span class="google-review-card-stars" style="--_rating: 4;" aria-hidden="true">★★★★★</span><span class="sr-only">Rated 4 out of 5</span></div>
25
25
  <p class="google-review-card-text">Review text 2</p><a href="https://maps.google.com/review/2" aria-describedby="v-0-0-1" class="google-review-card-link">Read on Google</a>
@@ -27,8 +27,8 @@ exports[`GoogleReviews > renders correct HTML structure 1`] = `
27
27
  </li>
28
28
  <li class="google-reviews-item">
29
29
  <article class="google-review-card">
30
- <div class="google-review-card-author"><span class="display-avatar md" aria-hidden="true"><span>R3</span>
31
- <!--v-if--></span><a id="v-0-0-2" href="https://maps.google.com/contrib/3" class="google-review-card-author-name">Reviewer 3</a><time datetime="2026-09-18T10:00:00Z" class="google-review-card-time">3 weeks ago</time>
30
+ <div class="google-review-card-author"><span class="display-avatar" data-size="md" aria-hidden="true"><span class="display-avatar-surface"><span class="display-avatar-fallback">R3</span>
31
+ <!--v-if--></span></span><a id="v-0-0-2" href="https://maps.google.com/contrib/3" class="google-review-card-author-name">Reviewer 3</a><time datetime="2026-09-18T10:00:00Z" class="google-review-card-time">3 weeks ago</time>
32
32
  </div>
33
33
  <div class="google-review-card-rating"><span class="google-review-card-stars" style="--_rating: 3;" aria-hidden="true">★★★★★</span><span class="sr-only">Rated 3 out of 5</span></div>
34
34
  <p class="google-review-card-text">Review text 3</p><a href="https://maps.google.com/review/3" aria-describedby="v-0-0-2" class="google-review-card-link">Read on Google</a>
@@ -87,7 +87,7 @@ const descriptionId = computed(() => `${id}-description`);
87
87
  const errorId = computed(() => `${id}-error-message`);
88
88
 
89
89
  // A plain function, like useAriaDescribedById's: slots aren't reactive, so a computed would keep
90
- // its first answer if a description slot is added or removed after mount (Claude.md pitfall #25).
90
+ // its first answer if a description slot is added or removed after mount (CLAUDE.md pitfall #25).
91
91
  // Lists both ids when there's a description and an error, so the description is still announced
92
92
  // while the field is invalid.
93
93
  const ariaDescribedby = () => {
@@ -7,7 +7,7 @@ export function useAriaDescribedById(name: string | Ref<string>, fieldHasError:
7
7
 
8
8
  // A plain function, not a computed: the slots object isn't reactive, so a computed would cache
9
9
  // its first answer and keep pointing at (or away from) the description after a description slot
10
- // is added or removed post-mount (Claude.md pitfall #25). Call it from the template,
10
+ // is added or removed post-mount (CLAUDE.md pitfall #25). Call it from the template,
11
11
  // `:aria-describedby="ariaDescribedby()"`, so it's re-evaluated on every render.
12
12
  const ariaDescribedby = () => {
13
13
  const hasDescription = slots.descriptionText || slots.descriptionHtml || slots.description;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.6.1",
4
+ "version": "9.6.3",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "files": [
38
38
  ".claude/skills/",
39
+ ".claude/rules/consumer/",
39
40
  ".vscode/",
40
41
  "app/",
41
42
  "modules/",