srcdev-nuxt-components 9.5.3 → 9.5.5
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/skills/colour-scheme-disable.md +1 -1
- package/.claude/skills/component-compliance-checklist.md +214 -0
- package/.claude/skills/component-inline-action-button.md +2 -2
- package/.claude/skills/components/carousel-flip.md +3 -3
- package/.claude/skills/components/clipped-panel.md +2 -2
- package/.claude/skills/components/dashboard-quad-grid.md +2 -2
- package/.claude/skills/components/dashboard-stats-grid.md +1 -1
- package/.claude/skills/components/deep-expanding-menu-classic.md +1 -1
- package/.claude/skills/components/deep-expanding-menu.md +1 -1
- package/.claude/skills/components/google-reviews.md +193 -0
- package/.claude/skills/components/input-number.md +4 -0
- package/.claude/skills/components/input-otp.md +120 -0
- package/.claude/skills/components/section-parallax.md +1 -1
- package/.claude/skills/components/select-menu.md +19 -5
- package/.claude/skills/components/triple-toggle-switch.md +7 -1
- package/.claude/skills/composable-colour-scheme.md +2 -0
- package/.claude/skills/index.md +5 -1
- package/.claude/skills/new-app-scaffold.md +1 -1
- package/.claude/skills/theming-colour-ramps.md +6 -33
- package/.claude/skills/theming-component-token-pattern.md +30 -0
- package/.claude/skills/theming-dark-mode.md +243 -0
- package/.claude/skills/theming-override-default.md +6 -7
- package/.claude/skills/theming-partial-override.md +11 -11
- package/.vscode/srcdev-component-google-reviews.code-snippets +78 -0
- package/.vscode/srcdev-component-input-otp.code-snippets +47 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +13 -0
- package/README.md +81 -41
- package/app/assets/styles/setup/01.config/_head.css +0 -9
- package/app/assets/styles/setup/03.theming/_default.css +29 -29
- package/app/assets/styles/setup/03.theming/_error.css +3 -3
- package/app/assets/styles/setup/03.theming/_theme-slots.css +13 -16
- package/app/assets/styles/setup/a11y/_variables.css +2 -2
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +7 -7
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +19 -1
- package/app/components/02.molecules/select-menu/SelectMenu.vue +54 -3
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +35 -0
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +9 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +12 -12
- package/app/components/03.organisms/google-reviews/CONSUMER-STYLING.md +110 -0
- package/app/components/03.organisms/google-reviews/GoogleReviewCard.vue +161 -0
- package/app/components/03.organisms/google-reviews/GoogleReviews.vue +275 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +323 -0
- package/app/components/03.organisms/google-reviews/tests/GoogleReviewCard.spec.ts +104 -0
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +232 -0
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +9 -0
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +43 -0
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/input-number/InputNumber.vue +2 -2
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +59 -0
- package/app/components/05.forms/input-otp/InputOtp.vue +254 -0
- package/app/components/05.forms/input-otp/InputOtpField.vue +106 -0
- package/app/components/05.forms/input-otp/stories/InputOtp.stories.ts +100 -0
- package/app/components/05.forms/input-otp/stories/InputOtpField.stories.ts +163 -0
- package/app/components/05.forms/input-otp/tests/InputOtp.spec.ts +275 -0
- package/app/components/05.forms/input-otp/tests/InputOtpField.spec.ts +76 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +151 -33
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +11 -3
- package/app/composables/useGoogleReviews.ts +4 -0
- package/app/types/components/google-reviews.d.ts +18 -0
- package/app/types/components/index.ts +1 -0
- package/nuxt.config.ts +9 -9
- package/package.json +9 -8
|
@@ -14,6 +14,7 @@ interface StoryArgs {
|
|
|
14
14
|
showChevron?: boolean;
|
|
15
15
|
multiple?: boolean;
|
|
16
16
|
showSelectionInTrigger?: boolean;
|
|
17
|
+
inputVariant?: "normal" | "underlined";
|
|
17
18
|
modelValue?: string | number | (string | number)[];
|
|
18
19
|
styleClassPassthrough?: string | string[];
|
|
19
20
|
}
|
|
@@ -57,6 +58,12 @@ const meta: Meta<StoryArgs> = {
|
|
|
57
58
|
description: "In multiple mode, update the trigger text to a comma-separated list of the currently checked options instead of leaving it fixed on placeholder/label. No effect outside multiple mode.",
|
|
58
59
|
table: { category: "Content" },
|
|
59
60
|
},
|
|
61
|
+
inputVariant: {
|
|
62
|
+
control: { type: "select" },
|
|
63
|
+
options: ["normal", "underlined"],
|
|
64
|
+
description: "Trigger border style, matching InputSelect: bordered box or bottom border only.",
|
|
65
|
+
table: { category: "Styling" },
|
|
66
|
+
},
|
|
60
67
|
styleClassPassthrough: {
|
|
61
68
|
table: { disable: true },
|
|
62
69
|
},
|
|
@@ -66,6 +73,7 @@ const meta: Meta<StoryArgs> = {
|
|
|
66
73
|
showIcon: true,
|
|
67
74
|
showLabel: true,
|
|
68
75
|
showChevron: true,
|
|
76
|
+
inputVariant: "normal",
|
|
69
77
|
},
|
|
70
78
|
parameters: {
|
|
71
79
|
docs: {
|
|
@@ -357,3 +365,30 @@ export const CustomTokens: Story = {
|
|
|
357
365
|
`,
|
|
358
366
|
}),
|
|
359
367
|
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Underlined — bottom border only, matching InputSelect's `underlined` variant
|
|
371
|
+
* so a SelectMenu can sit next to underlined form fields.
|
|
372
|
+
*/
|
|
373
|
+
export const Underlined: Story = {
|
|
374
|
+
args: {
|
|
375
|
+
label: "Choose a service",
|
|
376
|
+
showIcon: false,
|
|
377
|
+
inputVariant: "underlined",
|
|
378
|
+
},
|
|
379
|
+
render: (args) => ({
|
|
380
|
+
components: { SelectMenu },
|
|
381
|
+
setup() {
|
|
382
|
+
const value = ref<string | undefined>(undefined);
|
|
383
|
+
return { args, value, serviceOptions };
|
|
384
|
+
},
|
|
385
|
+
template: `
|
|
386
|
+
<div style="padding: 4rem 8rem;">
|
|
387
|
+
<SelectMenu v-bind="args" v-model="value" :options="serviceOptions" />
|
|
388
|
+
<p style="margin: 1.6rem 0 0; font-size: 1.3rem; opacity: 0.6;">
|
|
389
|
+
modelValue: <strong>{{ value === undefined ? 'undefined' : value }}</strong>
|
|
390
|
+
</p>
|
|
391
|
+
</div>
|
|
392
|
+
`,
|
|
393
|
+
}),
|
|
394
|
+
};
|
|
@@ -89,6 +89,15 @@ describe("SelectMenu", () => {
|
|
|
89
89
|
expect(wrapper.find(".select-menu-trigger").attributes("aria-haspopup")).toBe("listbox");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
it("applies the normal variant class by default and underlined when set", async () => {
|
|
93
|
+
wrapper = await createWrapper();
|
|
94
|
+
expect(wrapper.find(".select-menu").classes()).toContain("normal");
|
|
95
|
+
|
|
96
|
+
await wrapper.setProps({ inputVariant: "underlined" });
|
|
97
|
+
expect(wrapper.find(".select-menu").classes()).toContain("underlined");
|
|
98
|
+
expect(wrapper.find(".select-menu").classes()).not.toContain("normal");
|
|
99
|
+
});
|
|
100
|
+
|
|
92
101
|
it("uses label prop as aria-label", async () => {
|
|
93
102
|
wrapper = await createWrapper({ label: "Filter by service" });
|
|
94
103
|
expect(wrapper.find(".select-menu-trigger").attributes("aria-label")).toBe("Filter by service");
|
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
3
|
exports[`SelectMenu > Snapshots > default 1`] = `
|
|
4
|
-
"<div class="select-menu" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false">
|
|
4
|
+
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false">
|
|
5
5
|
<!--v-if--><span class="select-menu-trigger-label">Language</span><span class="iconify i-lucide:chevron-down select-menu-trigger-chevron" aria-hidden="true"></span>
|
|
6
6
|
</button>
|
|
7
7
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
8
8
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
9
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
10
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
11
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
9
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="English"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
10
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Français"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
11
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Deutsch"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
12
12
|
</ul>
|
|
13
13
|
</div>
|
|
14
14
|
</div>"
|
|
15
15
|
`;
|
|
16
16
|
|
|
17
17
|
exports[`SelectMenu > Snapshots > icon-only trigger 1`] = `
|
|
18
|
-
"<div class="select-menu" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:gb-4x3 select-menu-trigger-icon" aria-hidden="true"></span>
|
|
18
|
+
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:gb-4x3 select-menu-trigger-icon" aria-hidden="true"></span>
|
|
19
19
|
<!--v-if-->
|
|
20
20
|
<!--v-if-->
|
|
21
21
|
</button>
|
|
22
22
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
23
23
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
24
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="true"><span class="select-menu-item-check" aria-hidden="true"><span class="iconify i-lucide:check select-menu-item-check-icon" aria-hidden="true"></span></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
25
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
26
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
24
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="true" data-label="English"><span class="select-menu-item-check" aria-hidden="true"><span class="iconify i-lucide:check select-menu-item-check-icon" aria-hidden="true"></span></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
25
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Français"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
26
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Deutsch"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
27
27
|
</ul>
|
|
28
28
|
</div>
|
|
29
29
|
</div>"
|
|
30
30
|
`;
|
|
31
31
|
|
|
32
32
|
exports[`SelectMenu > Snapshots > with a selected value 1`] = `
|
|
33
|
-
"<div class="select-menu" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:fr-4x3 select-menu-trigger-icon" aria-hidden="true"></span><span class="select-menu-trigger-label">Français</span><span class="iconify i-lucide:chevron-down select-menu-trigger-chevron" aria-hidden="true"></span></button>
|
|
33
|
+
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:fr-4x3 select-menu-trigger-icon" aria-hidden="true"></span><span class="select-menu-trigger-label">Français</span><span class="iconify i-lucide:chevron-down select-menu-trigger-chevron" aria-hidden="true"></span></button>
|
|
34
34
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
35
35
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
36
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
37
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="true"><span class="select-menu-item-check" aria-hidden="true"><span class="iconify i-lucide:check select-menu-item-check-icon" aria-hidden="true"></span></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
38
|
-
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
36
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="English"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
37
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="true" data-label="Français"><span class="select-menu-item-check" aria-hidden="true"><span class="iconify i-lucide:check select-menu-item-check-icon" aria-hidden="true"></span></span><span class="iconify i-flag:fr-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Français</span></li>
|
|
38
|
+
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="Deutsch"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:de-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">Deutsch</span></li>
|
|
39
39
|
</ul>
|
|
40
40
|
</div>
|
|
41
41
|
</div>"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# GoogleReviews — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
`GoogleReviews` shows a place's Google reviews as a single horizontally scrolling row of
|
|
4
|
+
`GoogleReviewCard`s, with a summary above and prev/next buttons plus the Google Maps attribution
|
|
5
|
+
below. Both components read the same `--google-reviews-*` tokens, so setting them on an ancestor
|
|
6
|
+
styles a standalone `GoogleReviewCard` too.
|
|
7
|
+
|
|
8
|
+
## Public token API
|
|
9
|
+
|
|
10
|
+
### Row (`GoogleReviews`)
|
|
11
|
+
|
|
12
|
+
| Token | Default | Controls |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `--google-reviews-gap` | `1.6rem` | Space between header, row and footer, and between cards |
|
|
15
|
+
| `--google-reviews-card-width` | `min(32rem, 85%)` | Width of each card in the row |
|
|
16
|
+
| `--google-reviews-scroll-padding` | `0` | Snap inset, e.g. to line cards up with a page gutter |
|
|
17
|
+
| `--google-reviews-scrollbar-width` | `thin` | Row scrollbar: `auto`, `thin` or `none` (`none` hides it; the buttons and touch/keyboard scrolling still work) |
|
|
18
|
+
| `--google-reviews-scrollbar-thumb-colour` | `var(--slate-04)` | Scrollbar thumb |
|
|
19
|
+
| `--google-reviews-scrollbar-track-colour` | `transparent` | Scrollbar track |
|
|
20
|
+
| `--google-reviews-summary-rating-font-size` | `2.4rem` | Overall rating number |
|
|
21
|
+
| `--google-reviews-focus-outline-width` | `2px` | Focus ring on the scrollable row |
|
|
22
|
+
| `--google-reviews-focus-outline-colour` | `var(--theme-border-focus)` | Focus ring colour |
|
|
23
|
+
|
|
24
|
+
### Card (`GoogleReviewCard`)
|
|
25
|
+
|
|
26
|
+
| Token | Default | Controls |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `--google-reviews-card-gap` | `1.2rem` | Space between author, stars, text and link |
|
|
29
|
+
| `--google-reviews-card-padding` | `2rem` | Card padding |
|
|
30
|
+
| `--google-reviews-card-border-width` | `1px` | Card border width |
|
|
31
|
+
| `--google-reviews-card-border-colour` | `transparent` | Card border colour |
|
|
32
|
+
| `--google-reviews-card-border-radius` | `0.8rem` | Card corner radius |
|
|
33
|
+
| `--google-reviews-card-surface` | `var(--theme-surface-subtle)` | Card background |
|
|
34
|
+
| `--google-reviews-card-text-colour` | `var(--theme-text)` | Card text colour |
|
|
35
|
+
| `--google-reviews-avatar-surface` | `var(--theme-surface)` | Initials avatar background (shown when a reviewer has no photo) |
|
|
36
|
+
| `--google-reviews-avatar-text-colour` | `var(--theme-on-surface)` | Initials colour |
|
|
37
|
+
| `--google-reviews-author-font-weight` | `600` | Reviewer name weight |
|
|
38
|
+
| `--google-reviews-author-line-clamp` | `2` | Lines of reviewer name before it's cut off with an ellipsis (`1` for single-line ellipsis, `none` to show it all) |
|
|
39
|
+
| `--google-reviews-text-line-height` | `1.5` | Review text line height |
|
|
40
|
+
| `--google-reviews-text-line-clamp` | `5` | Lines of review text before it's cut off with an ellipsis (`none` to show it all) |
|
|
41
|
+
| `--google-reviews-link-colour` | `inherit` | "Read on Google" link colour |
|
|
42
|
+
|
|
43
|
+
### Shared
|
|
44
|
+
|
|
45
|
+
| Token | Default | Controls |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `--google-reviews-star-size` | `1.6rem` | Star font size (summary and cards) |
|
|
48
|
+
| `--google-reviews-star-colour` | `var(--amber-05)` | Filled star colour |
|
|
49
|
+
| `--google-reviews-star-empty-colour` | `var(--slate-03)` | Unfilled star colour |
|
|
50
|
+
| `--google-reviews-meta-font-size` | `1.4rem` | Date, link, filter notice and attribution |
|
|
51
|
+
| `--google-reviews-meta-text-colour` | `inherit` | Colour of the same |
|
|
52
|
+
|
|
53
|
+
Private tokens (not public API): `--_rating` (set inline from the review's rating) and `--_fill`
|
|
54
|
+
(the star fill percentage derived from it).
|
|
55
|
+
|
|
56
|
+
## State hooks
|
|
57
|
+
|
|
58
|
+
- No `data-*` state. The prev/next buttons are `InputButton`s with `readonly`/`aria-disabled` at
|
|
59
|
+
either end of the row, styled by `InputButton`'s own tokens.
|
|
60
|
+
- Inner classes: `.google-reviews-header`, `.google-reviews-summary`,
|
|
61
|
+
`.google-reviews-summary-rating`, `.google-reviews-stars`, `.google-reviews-summary-link`,
|
|
62
|
+
`.google-reviews-filter-notice`, `.google-reviews-list`, `.google-reviews-item`,
|
|
63
|
+
`.google-reviews-footer`, `.google-reviews-controls`, `.google-reviews-prev`,
|
|
64
|
+
`.google-reviews-next`, `.google-reviews-attribution`.
|
|
65
|
+
- Card classes: `.google-review-card`, `.google-review-card-author`,
|
|
66
|
+
`.google-review-card-author-name`,
|
|
67
|
+
`.google-review-card-time`, `.google-review-card-rating`, `.google-review-card-stars`,
|
|
68
|
+
`.google-review-card-text`, `.google-review-card-link`.
|
|
69
|
+
|
|
70
|
+
## Row alignment
|
|
71
|
+
|
|
72
|
+
Inside `GoogleReviews`, every card spans the list's five shared rows (name, date, rating, text,
|
|
73
|
+
link) through `grid-template-rows: subgrid`, so each section lines up across the whole row
|
|
74
|
+
however tall its neighbours are: a one-line name's date sits level with a two-line name's. The
|
|
75
|
+
author block is itself a subgrid over the name and date rows, with the avatar spanning both.
|
|
76
|
+
Sections sit in fixed rows, so a card with no date, text or link leaves the gap rather than
|
|
77
|
+
shifting up. Spacing between sections is a top margin (`--google-reviews-card-gap`), not a grid
|
|
78
|
+
gap, so the name and date stay close together. A standalone `GoogleReviewCard` uses
|
|
79
|
+
`auto auto auto 1fr auto` instead (link pinned to the bottom). Content from the `card` slot
|
|
80
|
+
spans all five rows as one block. Browsers
|
|
81
|
+
without subgrid fall back to equal-height cards that aren't aligned section by section.
|
|
82
|
+
|
|
83
|
+
## Stars
|
|
84
|
+
|
|
85
|
+
Stars are the text `★★★★★` filled with a hard-stop gradient clipped to the text, so fractional
|
|
86
|
+
ratings (4.6) show a partly filled star. They're `aria-hidden`; the rating is read out from
|
|
87
|
+
visually hidden text (`ratingLabel`).
|
|
88
|
+
|
|
89
|
+
## Local overrides
|
|
90
|
+
|
|
91
|
+
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
92
|
+
`:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
|
|
93
|
+
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
94
|
+
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
|
|
95
|
+
`:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
96
|
+
|
|
97
|
+
**Caveat:** the card re-declares `--display-avatar-background` and `--display-avatar-text-colour` on its
|
|
98
|
+
avatar, so setting those `DisplayAvatar` tokens on an ancestor has no effect here. Use
|
|
99
|
+
`--google-reviews-avatar-surface` and `--google-reviews-avatar-text-colour` instead.
|
|
100
|
+
|
|
101
|
+
## Class passthrough
|
|
102
|
+
|
|
103
|
+
`style-class-passthrough` adds classes to the root (`.google-reviews` or `.google-review-card`).
|
|
104
|
+
`GoogleReviews` doesn't forward it to the cards it renders; set tokens on the root instead, or
|
|
105
|
+
use the `card` slot to render `GoogleReviewCard` yourself with its own passthrough.
|
|
106
|
+
|
|
107
|
+
## Notes
|
|
108
|
+
|
|
109
|
+
Google's terms require the attribution text to stay visible and readable. Restyle it, but don't
|
|
110
|
+
hide it, and keep the words "Google Maps" in `attributionText` when translating.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="tag" class="google-review-card" :class="elementClasses">
|
|
3
|
+
<div class="google-review-card-author">
|
|
4
|
+
<DisplayAvatar :src="review.authorPhotoUri" :text="initials" size="md" aria-hidden="true" />
|
|
5
|
+
<a v-if="review.authorUri" :id="authorId" :href="review.authorUri" class="google-review-card-author-name">
|
|
6
|
+
{{ review.authorName }}
|
|
7
|
+
</a>
|
|
8
|
+
<span v-else :id="authorId" class="google-review-card-author-name">{{ review.authorName }}</span>
|
|
9
|
+
<time v-if="review.relativeTime" :datetime="review.publishTime || undefined" class="google-review-card-time">
|
|
10
|
+
{{ review.relativeTime }}
|
|
11
|
+
</time>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="google-review-card-rating">
|
|
15
|
+
<span class="google-review-card-stars" :style="{ '--_rating': review.rating }" aria-hidden="true">★★★★★</span>
|
|
16
|
+
<span class="sr-only">{{ formattedRatingLabel }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<p v-if="review.text" class="google-review-card-text">{{ review.text }}</p>
|
|
20
|
+
|
|
21
|
+
<a v-if="review.reviewUri" :href="review.reviewUri" :aria-describedby="authorId" class="google-review-card-link">
|
|
22
|
+
{{ readMoreLabel }}
|
|
23
|
+
</a>
|
|
24
|
+
</component>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import type { GoogleReview } from "~/types/components";
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
review: GoogleReview;
|
|
32
|
+
tag?: "article" | "div";
|
|
33
|
+
ratingLabel?: string;
|
|
34
|
+
readMoreLabel?: string;
|
|
35
|
+
styleClassPassthrough?: string | string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
39
|
+
tag: "article",
|
|
40
|
+
ratingLabel: "Rated {rating} out of 5",
|
|
41
|
+
readMoreLabel: "Read on Google",
|
|
42
|
+
styleClassPassthrough: () => [],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const authorId = useId();
|
|
46
|
+
const initials = computed(() =>
|
|
47
|
+
props.review.authorName
|
|
48
|
+
.split(/\s+/)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.slice(0, 2)
|
|
51
|
+
.map((word) => Array.from(word)[0]!.toUpperCase())
|
|
52
|
+
.join("")
|
|
53
|
+
);
|
|
54
|
+
const formattedRatingLabel = computed(() => props.ratingLabel.replace("{rating}", String(props.review.rating)));
|
|
55
|
+
|
|
56
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
57
|
+
|
|
58
|
+
watch(
|
|
59
|
+
() => props.styleClassPassthrough,
|
|
60
|
+
() => {
|
|
61
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<style lang="css">
|
|
67
|
+
@layer components {
|
|
68
|
+
.google-review-card {
|
|
69
|
+
display: grid;
|
|
70
|
+
grid-template-rows: auto auto auto 1fr auto;
|
|
71
|
+
align-items: start;
|
|
72
|
+
block-size: 100%;
|
|
73
|
+
padding: var(--google-reviews-card-padding, 2rem);
|
|
74
|
+
border: var(--google-reviews-card-border-width, 1px) solid var(--google-reviews-card-border-colour, transparent);
|
|
75
|
+
border-radius: var(--google-reviews-card-border-radius, 0.8rem);
|
|
76
|
+
background-color: var(--google-reviews-card-surface, var(--theme-surface-subtle));
|
|
77
|
+
color: var(--google-reviews-card-text-colour, var(--theme-text));
|
|
78
|
+
|
|
79
|
+
/* Name and date take a row each so both line up across a GoogleReviews row */
|
|
80
|
+
.google-review-card-author {
|
|
81
|
+
grid-row: 1 / span 2;
|
|
82
|
+
display: grid;
|
|
83
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
84
|
+
grid-template-rows: subgrid;
|
|
85
|
+
align-items: start;
|
|
86
|
+
column-gap: 1.2rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.google-review-card-author .display-avatar {
|
|
90
|
+
--display-avatar-background: var(--google-reviews-avatar-surface, var(--theme-surface));
|
|
91
|
+
--display-avatar-text-colour: var(--google-reviews-avatar-text-colour, var(--theme-on-surface));
|
|
92
|
+
|
|
93
|
+
grid-column: 1;
|
|
94
|
+
grid-row: 1 / -1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.google-review-card-author-name {
|
|
98
|
+
grid-column: 2;
|
|
99
|
+
grid-row: 1;
|
|
100
|
+
display: -webkit-box;
|
|
101
|
+
-webkit-box-orient: vertical;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
-webkit-line-clamp: var(--google-reviews-author-line-clamp, 2);
|
|
104
|
+
line-clamp: var(--google-reviews-author-line-clamp, 2);
|
|
105
|
+
font-weight: var(--google-reviews-author-font-weight, 600);
|
|
106
|
+
color: inherit;
|
|
107
|
+
overflow-wrap: anywhere;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.google-review-card-time {
|
|
111
|
+
grid-column: 2;
|
|
112
|
+
grid-row: 2;
|
|
113
|
+
font-size: var(--google-reviews-meta-font-size, 1.4rem);
|
|
114
|
+
color: var(--google-reviews-meta-text-colour, inherit);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.google-review-card-rating {
|
|
118
|
+
grid-row: 3;
|
|
119
|
+
margin-block-start: var(--google-reviews-card-gap, 1.2rem);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.google-review-card-stars {
|
|
123
|
+
--_fill: clamp(0%, var(--_rating, 0) / 5 * 100%, 100%);
|
|
124
|
+
|
|
125
|
+
font-size: var(--google-reviews-star-size, 1.6rem);
|
|
126
|
+
letter-spacing: 0.1em;
|
|
127
|
+
background: linear-gradient(
|
|
128
|
+
90deg,
|
|
129
|
+
var(--google-reviews-star-colour, var(--amber-05)) var(--_fill),
|
|
130
|
+
var(--google-reviews-star-empty-colour, var(--slate-03)) var(--_fill)
|
|
131
|
+
);
|
|
132
|
+
-webkit-background-clip: text;
|
|
133
|
+
background-clip: text;
|
|
134
|
+
color: transparent;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.google-review-card-text {
|
|
138
|
+
grid-row: 4;
|
|
139
|
+
margin-block-start: var(--google-reviews-card-gap, 1.2rem);
|
|
140
|
+
margin-inline: 0;
|
|
141
|
+
margin-block-end: 0;
|
|
142
|
+
white-space: pre-line;
|
|
143
|
+
overflow-wrap: anywhere;
|
|
144
|
+
line-height: var(--google-reviews-text-line-height, 1.5);
|
|
145
|
+
display: -webkit-box;
|
|
146
|
+
-webkit-box-orient: vertical;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
-webkit-line-clamp: var(--google-reviews-text-line-clamp, 5);
|
|
149
|
+
line-clamp: var(--google-reviews-text-line-clamp, 5);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.google-review-card-link {
|
|
153
|
+
grid-row: 5;
|
|
154
|
+
margin-block-start: var(--google-reviews-card-gap, 1.2rem);
|
|
155
|
+
justify-self: start;
|
|
156
|
+
color: var(--google-reviews-link-colour, inherit);
|
|
157
|
+
font-size: var(--google-reviews-meta-font-size, 1.4rem);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
</style>
|