srcdev-nuxt-components 9.5.2 → 9.5.4

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 (76) hide show
  1. package/.claude/skills/colour-scheme-disable.md +1 -1
  2. package/.claude/skills/component-compliance-checklist.md +214 -0
  3. package/.claude/skills/component-inline-action-button.md +2 -2
  4. package/.claude/skills/components/canvas-switcher.md +5 -1
  5. package/.claude/skills/components/carousel-flip.md +3 -3
  6. package/.claude/skills/components/clipped-panel.md +2 -2
  7. package/.claude/skills/components/dashboard-quad-grid.md +2 -2
  8. package/.claude/skills/components/dashboard-stats-grid.md +1 -1
  9. package/.claude/skills/components/deep-expanding-menu-classic.md +1 -1
  10. package/.claude/skills/components/deep-expanding-menu.md +1 -1
  11. package/.claude/skills/components/eyebrow-text.md +6 -4
  12. package/.claude/skills/components/google-reviews.md +193 -0
  13. package/.claude/skills/components/input-number.md +4 -0
  14. package/.claude/skills/components/input-otp.md +120 -0
  15. package/.claude/skills/components/section-parallax.md +1 -1
  16. package/.claude/skills/components/select-menu.md +9 -5
  17. package/.claude/skills/components/services-card.md +25 -2
  18. package/.claude/skills/composable-colour-scheme.md +2 -0
  19. package/.claude/skills/index.md +5 -1
  20. package/.claude/skills/new-app-scaffold.md +1 -1
  21. package/.claude/skills/storybook-add-story.md +5 -0
  22. package/.claude/skills/theming-colour-ramps.md +6 -33
  23. package/.claude/skills/theming-component-token-pattern.md +30 -0
  24. package/.claude/skills/theming-dark-mode.md +243 -0
  25. package/.claude/skills/theming-override-default.md +6 -7
  26. package/.claude/skills/theming-partial-override.md +11 -11
  27. package/.vscode/srcdev-component-eyebrow-text.code-snippets +4 -0
  28. package/.vscode/srcdev-component-google-reviews.code-snippets +78 -0
  29. package/.vscode/srcdev-component-input-otp.code-snippets +47 -0
  30. package/.vscode/srcdev-component-select-menu.code-snippets +13 -0
  31. package/.vscode/srcdev-component-services-card.code-snippets +38 -7
  32. package/README.md +81 -41
  33. package/app/assets/styles/setup/01.config/_head.css +0 -9
  34. package/app/assets/styles/setup/03.theming/_default.css +29 -29
  35. package/app/assets/styles/setup/03.theming/_error.css +3 -3
  36. package/app/assets/styles/setup/03.theming/_theme-slots.css +13 -16
  37. package/app/assets/styles/setup/a11y/_variables.css +2 -2
  38. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +47 -13
  39. package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +5 -1
  40. package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +4 -1
  41. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +7 -7
  42. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +10 -0
  43. package/app/components/02.molecules/select-menu/SelectMenu.vue +15 -1
  44. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +35 -0
  45. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +9 -0
  46. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +3 -3
  47. package/app/components/03.organisms/google-reviews/CONSUMER-STYLING.md +110 -0
  48. package/app/components/03.organisms/google-reviews/GoogleReviewCard.vue +161 -0
  49. package/app/components/03.organisms/google-reviews/GoogleReviews.vue +275 -0
  50. package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +323 -0
  51. package/app/components/03.organisms/google-reviews/tests/GoogleReviewCard.spec.ts +104 -0
  52. package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +232 -0
  53. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +9 -0
  54. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +43 -0
  55. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +28 -3
  56. package/app/components/03.organisms/services/services-card/ServicesCard.vue +111 -18
  57. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +122 -0
  58. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +4 -1
  59. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +100 -19
  60. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +12 -3
  61. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +1 -1
  62. package/app/components/05.forms/input-number/InputNumber.vue +2 -2
  63. package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +59 -0
  64. package/app/components/05.forms/input-otp/InputOtp.vue +254 -0
  65. package/app/components/05.forms/input-otp/InputOtpField.vue +106 -0
  66. package/app/components/05.forms/input-otp/stories/InputOtp.stories.ts +100 -0
  67. package/app/components/05.forms/input-otp/stories/InputOtpField.stories.ts +163 -0
  68. package/app/components/05.forms/input-otp/tests/InputOtp.spec.ts +275 -0
  69. package/app/components/05.forms/input-otp/tests/InputOtpField.spec.ts +76 -0
  70. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +151 -33
  71. package/app/composables/tests/useAriaDescribedById.spec.ts +5 -5
  72. package/app/composables/useGoogleReviews.ts +4 -0
  73. package/app/types/components/google-reviews.d.ts +18 -0
  74. package/app/types/components/index.ts +1 -0
  75. package/nuxt.config.ts +9 -9
  76. package/package.json +9 -8
@@ -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>
@@ -0,0 +1,275 @@
1
+ <template>
2
+ <component
3
+ :is="tag"
4
+ v-if="filteredReviews.length"
5
+ class="google-reviews"
6
+ :class="elementClasses"
7
+ :aria-labelledby="hasHeading() ? headingId : undefined"
8
+ :aria-label="hasHeading() ? undefined : ariaLabel"
9
+ >
10
+ <div v-if="hasHeading() || showSummary" class="google-reviews-header">
11
+ <slot name="heading" :heading-id="headingId"></slot>
12
+ <div v-if="showSummary && data" class="google-reviews-summary">
13
+ <span class="google-reviews-summary-rating" aria-hidden="true">{{ data.rating.toFixed(1) }}</span>
14
+ <span class="google-reviews-stars" :style="{ '--_rating': data.rating }" aria-hidden="true">★★★★★</span>
15
+ <span class="sr-only">{{ formatLabel(ratingLabel, data.rating.toFixed(1)) }}</span>
16
+ <a v-if="data.mapsUri" :href="data.mapsUri" class="google-reviews-summary-link">
17
+ {{ formatLabel(totalLabel, data.totalReviews) }}
18
+ </a>
19
+ <span v-else class="google-reviews-summary-link">{{ formatLabel(totalLabel, data.totalReviews) }}</span>
20
+ </div>
21
+ </div>
22
+
23
+ <p v-if="minRating > 0" class="google-reviews-filter-notice">{{ formatLabel(filterNotice, minRating) }}</p>
24
+
25
+ <ul ref="listRef" class="google-reviews-list" tabindex="0" @scroll.passive="updateScrollState">
26
+ <li v-for="(review, index) in filteredReviews" :key="review.reviewUri ?? index" class="google-reviews-item">
27
+ <slot name="card" :review="review">
28
+ <GoogleReviewCard :review="review" :rating-label="ratingLabel" :read-more-label="readMoreLabel" />
29
+ </slot>
30
+ </li>
31
+ </ul>
32
+
33
+ <div class="google-reviews-footer">
34
+ <div v-if="isOverflowing" class="google-reviews-controls">
35
+ <InputButton
36
+ variant="secondary"
37
+ :button-text="prevLabel"
38
+ :readonly="atStart"
39
+ class="google-reviews-prev"
40
+ @click="scrollByCard(-1)"
41
+ >
42
+ <template #iconOnly>
43
+ <Icon :name="prevIcon" class="icon" />
44
+ </template>
45
+ </InputButton>
46
+ <InputButton
47
+ variant="secondary"
48
+ :button-text="nextLabel"
49
+ :readonly="atEnd"
50
+ class="google-reviews-next"
51
+ @click="scrollByCard(1)"
52
+ >
53
+ <template #iconOnly>
54
+ <Icon :name="nextIcon" class="icon" />
55
+ </template>
56
+ </InputButton>
57
+ </div>
58
+ <p class="google-reviews-attribution">{{ attributionText }}</p>
59
+ </div>
60
+ </component>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ import type { GoogleReviewsData } from "~/types/components";
65
+
66
+ interface Props {
67
+ data: GoogleReviewsData | null;
68
+ minRating?: number;
69
+ showSummary?: boolean;
70
+ tag?: "section" | "div";
71
+ ariaLabel?: string;
72
+ prevLabel?: string;
73
+ nextLabel?: string;
74
+ prevIcon?: string;
75
+ nextIcon?: string;
76
+ ratingLabel?: string;
77
+ totalLabel?: string;
78
+ readMoreLabel?: string;
79
+ filterNotice?: string;
80
+ attributionText?: string;
81
+ styleClassPassthrough?: string | string[];
82
+ }
83
+
84
+ const props = withDefaults(defineProps<Props>(), {
85
+ minRating: 0,
86
+ showSummary: true,
87
+ tag: "section",
88
+ ariaLabel: "Google reviews",
89
+ prevLabel: "Previous reviews",
90
+ nextLabel: "Next reviews",
91
+ prevIcon: "ic:outline-keyboard-arrow-left",
92
+ nextIcon: "ic:outline-keyboard-arrow-right",
93
+ ratingLabel: "Rated {rating} out of 5",
94
+ totalLabel: "{count} reviews on Google",
95
+ readMoreLabel: "Read on Google",
96
+ filterNotice: "Showing reviews rated {rating} stars and above, in Google's order.",
97
+ attributionText: "Reviews from Google Maps",
98
+ styleClassPassthrough: () => [],
99
+ });
100
+
101
+ const slots = useSlots();
102
+ const headingId = useId();
103
+ const hasHeading = () => Boolean(slots.heading);
104
+
105
+ const formatLabel = (label: string, value: string | number) =>
106
+ label.replace("{rating}", String(value)).replace("{count}", String(value));
107
+
108
+ const filteredReviews = computed(() => (props.data?.reviews ?? []).filter((review) => review.rating >= props.minRating));
109
+
110
+ const listRef = ref<HTMLUListElement | null>(null);
111
+ const atStart = ref(true);
112
+ const atEnd = ref(true);
113
+ const isOverflowing = ref(false);
114
+
115
+ const updateScrollState = () => {
116
+ const list = listRef.value;
117
+ if (!list) return;
118
+ const offset = Math.abs(list.scrollLeft);
119
+ isOverflowing.value = list.scrollWidth > list.clientWidth + 1;
120
+ atStart.value = offset <= 1;
121
+ atEnd.value = offset + list.clientWidth >= list.scrollWidth - 1;
122
+ };
123
+
124
+ const scrollByCard = (direction: 1 | -1) => {
125
+ const list = listRef.value;
126
+ const item = list?.querySelector<HTMLElement>(".google-reviews-item");
127
+ if (!list || !item) return;
128
+ const styles = getComputedStyle(list);
129
+ const step = item.offsetWidth + (parseFloat(styles.columnGap) || 0);
130
+ const sign = styles.direction === "rtl" ? -1 : 1;
131
+ list.scrollBy({ left: direction * step * sign });
132
+ };
133
+
134
+ let resizeObserver: ResizeObserver | null = null;
135
+
136
+ onMounted(() => {
137
+ updateScrollState();
138
+ resizeObserver = new ResizeObserver(updateScrollState);
139
+ if (listRef.value) resizeObserver.observe(listRef.value);
140
+ });
141
+
142
+ onBeforeUnmount(() => resizeObserver?.disconnect());
143
+
144
+ watch(filteredReviews, async () => {
145
+ await nextTick();
146
+ if (listRef.value) {
147
+ resizeObserver?.disconnect();
148
+ resizeObserver?.observe(listRef.value);
149
+ }
150
+ updateScrollState();
151
+ });
152
+
153
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
154
+
155
+ watch(
156
+ () => props.styleClassPassthrough,
157
+ () => {
158
+ resetElementClasses(props.styleClassPassthrough);
159
+ }
160
+ );
161
+ </script>
162
+
163
+ <style lang="css">
164
+ @layer components {
165
+ .google-reviews {
166
+ display: grid;
167
+ gap: var(--google-reviews-gap, 1.6rem);
168
+ min-inline-size: 0;
169
+
170
+ .google-reviews-header {
171
+ display: grid;
172
+ gap: 0.8rem;
173
+ }
174
+
175
+ .google-reviews-summary {
176
+ display: flex;
177
+ flex-wrap: wrap;
178
+ align-items: center;
179
+ gap: 0.8rem;
180
+ }
181
+
182
+ .google-reviews-summary-rating {
183
+ font-size: var(--google-reviews-summary-rating-font-size, 2.4rem);
184
+ font-weight: 700;
185
+ }
186
+
187
+ .google-reviews-stars {
188
+ --_fill: clamp(0%, var(--_rating, 0) / 5 * 100%, 100%);
189
+
190
+ font-size: var(--google-reviews-star-size, 1.6rem);
191
+ letter-spacing: 0.1em;
192
+ background: linear-gradient(
193
+ 90deg,
194
+ var(--google-reviews-star-colour, var(--amber-05)) var(--_fill),
195
+ var(--google-reviews-star-empty-colour, var(--slate-03)) var(--_fill)
196
+ );
197
+ -webkit-background-clip: text;
198
+ background-clip: text;
199
+ color: transparent;
200
+ }
201
+
202
+ .google-reviews-summary-link {
203
+ color: inherit;
204
+ overflow-wrap: anywhere;
205
+ }
206
+
207
+ .google-reviews-filter-notice,
208
+ .google-reviews-attribution {
209
+ margin: 0;
210
+ overflow-wrap: anywhere;
211
+ font-size: var(--google-reviews-meta-font-size, 1.4rem);
212
+ color: var(--google-reviews-meta-text-colour, inherit);
213
+ }
214
+
215
+ .google-reviews-list {
216
+ display: grid;
217
+ grid-auto-flow: column;
218
+ grid-auto-columns: var(--google-reviews-card-width, min(32rem, 85%));
219
+ grid-template-rows: repeat(5, auto);
220
+ column-gap: var(--google-reviews-gap, 1.6rem);
221
+ row-gap: 0;
222
+ margin: 0;
223
+ padding: 0 0 0.8rem;
224
+ list-style: none;
225
+ overflow-x: auto;
226
+ overscroll-behavior-inline: contain;
227
+ scrollbar-width: var(--google-reviews-scrollbar-width, thin);
228
+ scrollbar-color: var(--google-reviews-scrollbar-thumb-colour, var(--slate-04))
229
+ var(--google-reviews-scrollbar-track-colour, transparent);
230
+ scroll-snap-type: inline mandatory;
231
+ scroll-padding-inline: var(--google-reviews-scroll-padding, 0);
232
+ scroll-behavior: smooth;
233
+
234
+ &:focus-visible {
235
+ outline: var(--google-reviews-focus-outline-width, 2px) solid var(--google-reviews-focus-outline-colour, var(--theme-border-focus));
236
+ outline-offset: 2px;
237
+ }
238
+
239
+ @media (prefers-reduced-motion: reduce) {
240
+ scroll-behavior: auto;
241
+ }
242
+ }
243
+
244
+ /* Cards share the list's five rows (name, date, rating, text, link) so each section lines up across the row */
245
+ .google-reviews-item {
246
+ display: grid;
247
+ grid-row: span 5;
248
+ grid-template-rows: subgrid;
249
+ scroll-snap-align: start;
250
+ min-inline-size: 0;
251
+
252
+ > * {
253
+ grid-row: 1 / -1;
254
+ }
255
+
256
+ > .google-review-card {
257
+ grid-template-rows: subgrid;
258
+ }
259
+ }
260
+
261
+ .google-reviews-footer {
262
+ display: flex;
263
+ flex-wrap: wrap;
264
+ align-items: center;
265
+ justify-content: space-between;
266
+ gap: 1.2rem;
267
+ }
268
+
269
+ .google-reviews-controls {
270
+ display: flex;
271
+ gap: 0.8rem;
272
+ }
273
+ }
274
+ }
275
+ </style>