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.
- 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/canvas-switcher.md +5 -1
- 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/eyebrow-text.md +6 -4
- 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 +9 -5
- package/.claude/skills/components/services-card.md +25 -2
- 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/storybook-add-story.md +5 -0
- 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-eyebrow-text.code-snippets +4 -0
- 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/.vscode/srcdev-component-services-card.code-snippets +38 -7
- 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/01.atoms/page-row/stories/PageRow.stories.ts +47 -13
- package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +5 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +4 -1
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +7 -7
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +10 -0
- package/app/components/02.molecules/select-menu/SelectMenu.vue +15 -1
- 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 +3 -3
- 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/03.organisms/services/services-card/CONSUMER-STYLING.md +28 -3
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +111 -18
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +122 -0
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +4 -1
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +100 -19
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +12 -3
- 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/composables/tests/useAriaDescribedById.spec.ts +5 -5
- 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
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
/* Theme semantic slots — 8 shared colour roles used by all themed components.
|
|
2
2
|
Declared on the same selector as theme-ramp.css so the formula re-evaluates
|
|
3
|
-
per [data-theme] element rather than being inherited from html.
|
|
3
|
+
per [data-theme] element rather than being inherited from html.
|
|
4
|
+
Light values only, no light-dark(): dark mode is the consumer app's call. */
|
|
4
5
|
:where(html, [data-theme], [data-invalid]) {
|
|
5
|
-
--theme-surface:
|
|
6
|
-
--theme-surface-inverted:
|
|
7
|
-
--theme-surface-hover:
|
|
8
|
-
--theme-accent:
|
|
9
|
-
--theme-surface-subtle:
|
|
10
|
-
--theme-border:
|
|
11
|
-
--theme-border-focus:
|
|
12
|
-
--theme-ring:
|
|
6
|
+
--theme-surface: var(--colour-theme-7);
|
|
7
|
+
--theme-surface-inverted: var(--colour-theme-9);
|
|
8
|
+
--theme-surface-hover: var(--colour-theme-9);
|
|
9
|
+
--theme-accent: var(--colour-theme-5);
|
|
10
|
+
--theme-surface-subtle: var(--colour-theme-1);
|
|
11
|
+
--theme-border: var(--colour-theme-6);
|
|
12
|
+
--theme-border-focus: var(--colour-theme-4);
|
|
13
|
+
--theme-ring: var(--colour-theme-1);
|
|
13
14
|
--theme-on-surface: var(--colour-theme-0);
|
|
14
|
-
--theme-text:
|
|
15
|
-
/* Light
|
|
16
|
-
|
|
17
|
-
color), so secondary-button text was invisible (dark-on-dark) in light mode. Should be a
|
|
18
|
-
light step to contrast against that dark inverted surface, matching how --theme-on-surface
|
|
19
|
-
(line 13) contrasts against --theme-surface the same way in both modes. */
|
|
20
|
-
--theme-text-inverted: light-dark(var(--colour-theme-0), var(--colour-theme-7));
|
|
15
|
+
--theme-text: var(--colour-theme-9);
|
|
16
|
+
/* Light step: pairs with the dark --theme-surface-inverted (InputButton .secondary). */
|
|
17
|
+
--theme-text-inverted: var(--colour-theme-0);
|
|
21
18
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
:where(html) {
|
|
2
2
|
--focus-box-shadow-colour-off: transparent;
|
|
3
|
-
--focus-box-shadow-colour-on:
|
|
3
|
+
--focus-box-shadow-colour-on: var(--blue-10);
|
|
4
4
|
--form-focus-box-shadow: 0 0 0 0.3rem;
|
|
5
5
|
|
|
6
6
|
--box-shadow-off: 0 0 0 0.3rem transparent;
|
|
7
|
-
--box-shadow-on: 0 0 0 0.3rem
|
|
7
|
+
--box-shadow-on: 0 0 0 0.3rem var(--blue-10);
|
|
8
8
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { ref } from "vue";
|
|
2
3
|
import PageRowComponent from "../PageRow.vue";
|
|
4
|
+
import CanvasSwitcher from "../../canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
+
import type { MediaCanvas } from "~/types/components";
|
|
3
6
|
|
|
4
7
|
interface PageRowArgs {
|
|
5
8
|
tag: string;
|
|
@@ -47,7 +50,27 @@ export default {
|
|
|
47
50
|
id: "",
|
|
48
51
|
styleClassPassthrough: [],
|
|
49
52
|
},
|
|
53
|
+
decorators: [
|
|
54
|
+
// PageRow sizes its tracks from 100% of its container, so a constrained wrapper stands in for a narrower viewport.
|
|
55
|
+
(story) => ({
|
|
56
|
+
components: { story, CanvasSwitcher },
|
|
57
|
+
setup() {
|
|
58
|
+
const canvasName = ref<MediaCanvas>("fullWidthCanvas");
|
|
59
|
+
return { canvasName };
|
|
60
|
+
},
|
|
61
|
+
template: `
|
|
62
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
63
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
64
|
+
</div>
|
|
65
|
+
<div :class="canvasName" style="margin-inline: auto; outline: 1px dashed currentColor;">
|
|
66
|
+
<story />
|
|
67
|
+
</div>
|
|
68
|
+
`,
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
50
71
|
parameters: {
|
|
72
|
+
// Storybook's default padded layout hides the viewport gutter and align edge-bleed.
|
|
73
|
+
layout: "fullscreen",
|
|
51
74
|
docs: {
|
|
52
75
|
description: {
|
|
53
76
|
component:
|
|
@@ -118,26 +141,37 @@ InsetContent.parameters = {
|
|
|
118
141
|
const NestingTemplate: StoryFn = () => ({
|
|
119
142
|
components: { PageRowComponent },
|
|
120
143
|
setup() {
|
|
121
|
-
|
|
144
|
+
const nested = [
|
|
145
|
+
{ variant: "full", background: "#fca5a5", desc: "edge to edge" },
|
|
146
|
+
{ variant: "popout", background: "#fdba74", desc: "max 1400px" },
|
|
147
|
+
{ variant: "content", background: "#fde047", desc: "max 1064px" },
|
|
148
|
+
{ variant: "inset-content", background: "#86efac", desc: "max 840px" },
|
|
149
|
+
];
|
|
150
|
+
const chip =
|
|
151
|
+
"display: inline-block; margin-block-end: 8px; padding: 2px 8px; border-radius: 4px; background: rgba(0,0,0,0.75); color: white; font-family: monospace; font-size: 1.2rem;";
|
|
152
|
+
return { sampleText, nested, chip };
|
|
122
153
|
},
|
|
123
154
|
template: `
|
|
124
155
|
<div style="min-height: 100vh;">
|
|
125
156
|
|
|
126
|
-
<PageRowComponent tag="section"
|
|
157
|
+
<PageRowComponent tag="section" style="background: #1e3a5f; color: white; padding-block: 60px;">
|
|
158
|
+
<span :style="chip">outer PageRow (variant="content")</span>
|
|
127
159
|
<h1 style="margin: 0 0 16px; font-size: 2.4rem;">Full-width background, constrained content</h1>
|
|
128
160
|
<p style="margin: 0; opacity: 0.85; line-height: 1.7;">{{ sampleText }}</p>
|
|
129
161
|
</PageRowComponent>
|
|
130
162
|
|
|
131
|
-
<PageRowComponent tag="section" style="background: #
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
163
|
+
<PageRowComponent tag="section" style="background: #0f172a; padding-block: 48px; display: grid; row-gap: 16px;">
|
|
164
|
+
<span :style="chip" style="background: white; color: #0f172a;">outer PageRow (dark background)</span>
|
|
165
|
+
<PageRowComponent
|
|
166
|
+
v-for="row in nested"
|
|
167
|
+
:key="row.variant"
|
|
168
|
+
:variant="row.variant"
|
|
169
|
+
:style="{ background: row.background, color: '#111827', paddingBlock: '24px' }"
|
|
170
|
+
>
|
|
171
|
+
<div>
|
|
172
|
+
<span :style="chip">nested variant="{{ row.variant }}"</span>
|
|
173
|
+
<p style="margin: 0; line-height: 1.6;">{{ row.desc }}: the background shows the track this nested PageRow occupies.</p>
|
|
174
|
+
</div>
|
|
141
175
|
</PageRowComponent>
|
|
142
176
|
</PageRowComponent>
|
|
143
177
|
|
|
@@ -152,7 +186,7 @@ NestingPattern.parameters = {
|
|
|
152
186
|
docs: {
|
|
153
187
|
description: {
|
|
154
188
|
story:
|
|
155
|
-
"The key composition pattern:
|
|
189
|
+
"The key composition pattern: an outer PageRow carries the full-width background, and nested PageRows pick a track via variant. Each nested row's background marks the track it occupies against the dark outer row. No extra wrapper divs needed.",
|
|
156
190
|
},
|
|
157
191
|
},
|
|
158
192
|
};
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
| `--eyebrow-text-large` | `1.4rem` | Font size for `font-size="large"` |
|
|
9
9
|
| `--eyebrow-text-medium` | `1.2rem` | Font size for `font-size="medium"` (default) |
|
|
10
10
|
| `--eyebrow-text-small` | `1rem` | Font size for `font-size="small"` |
|
|
11
|
+
| `--eyebrow-text-font-family` | `inherit` | Font family |
|
|
12
|
+
| `--eyebrow-text-font-style` | `italic` | Font style, e.g. `normal` for upright text |
|
|
13
|
+
| `--eyebrow-text-font-weight` | `inherit` | Font weight |
|
|
14
|
+
| `--eyebrow-text-letter-spacing` | `inherit` | Letter-spacing, e.g. `0.25em` for wide tracking |
|
|
11
15
|
|
|
12
|
-
The library theme also sets
|
|
16
|
+
The library theme also sets the colour and size tokens on `:root` (`03.theming/_default.css` and
|
|
13
17
|
`05.typography/01.tokens/_reponsive-font-sizes.css`) with the same values.
|
|
14
18
|
|
|
15
19
|
> Changed 2026-09-27: the component now carries those values as fallbacks too. Before, an app that
|
|
@@ -31,7 +31,10 @@ watch(
|
|
|
31
31
|
text-transform: uppercase;
|
|
32
32
|
background-clip: text;
|
|
33
33
|
background-image: var(--eyebrow-text-bg-img, linear-gradient(135deg, #c2a770, #b4747e, #d1bd94));
|
|
34
|
-
font-
|
|
34
|
+
font-family: var(--eyebrow-text-font-family, inherit);
|
|
35
|
+
font-style: var(--eyebrow-text-font-style, italic);
|
|
36
|
+
font-weight: var(--eyebrow-text-font-weight, inherit);
|
|
37
|
+
letter-spacing: var(--eyebrow-text-letter-spacing, inherit);
|
|
35
38
|
color: transparent;
|
|
36
39
|
|
|
37
40
|
&.large {
|
|
@@ -218,7 +218,7 @@ export const InContext: Story = {
|
|
|
218
218
|
align-items: center;
|
|
219
219
|
gap: 1.2rem;
|
|
220
220
|
padding: 1.2rem 1.6rem;
|
|
221
|
-
border-bottom: 0.1rem solid
|
|
221
|
+
border-bottom: 0.1rem solid rgb(128 128 128 / 0.35);
|
|
222
222
|
"
|
|
223
223
|
>
|
|
224
224
|
<div
|
|
@@ -226,7 +226,7 @@ export const InContext: Story = {
|
|
|
226
226
|
width: 3.6rem;
|
|
227
227
|
height: 3.6rem;
|
|
228
228
|
border-radius: 50%;
|
|
229
|
-
background:
|
|
229
|
+
background: rgb(128 128 128 / 0.35);
|
|
230
230
|
display: grid;
|
|
231
231
|
place-items: center;
|
|
232
232
|
font-size: 1.4rem;
|
|
@@ -290,17 +290,17 @@ export const ItemCoreStandalone: Story = {
|
|
|
290
290
|
template: `
|
|
291
291
|
<div style="padding: 4rem; max-width: 32rem; margin: 0 auto;">
|
|
292
292
|
<p style="margin: 0 0 1.2rem; font-size: 1.2rem; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.08em;">Button items</p>
|
|
293
|
-
<div style="border: 0.1rem solid
|
|
293
|
+
<div style="border: 0.1rem solid rgb(128 128 128 / 0.35); border-radius: 0.8rem; overflow: hidden;">
|
|
294
294
|
<ActionMenuItem
|
|
295
295
|
label="Edit"
|
|
296
|
-
style="border-bottom: 0.1rem solid
|
|
296
|
+
style="border-bottom: 0.1rem solid rgb(128 128 128 / 0.35);"
|
|
297
297
|
@click="lastClick = 'Edit'"
|
|
298
298
|
>
|
|
299
299
|
<template #icon><Icon name="lucide:pencil" /></template>
|
|
300
300
|
</ActionMenuItem>
|
|
301
301
|
<ActionMenuItem
|
|
302
302
|
label="Duplicate"
|
|
303
|
-
style="border-bottom: 0.1rem solid
|
|
303
|
+
style="border-bottom: 0.1rem solid rgb(128 128 128 / 0.35);"
|
|
304
304
|
@click="lastClick = 'Duplicate'"
|
|
305
305
|
>
|
|
306
306
|
<template #icon><Icon name="lucide:copy" /></template>
|
|
@@ -314,11 +314,11 @@ export const ItemCoreStandalone: Story = {
|
|
|
314
314
|
</div>
|
|
315
315
|
|
|
316
316
|
<p style="margin: 2.4rem 0 1.2rem; font-size: 1.2rem; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.08em;">Link items</p>
|
|
317
|
-
<div style="border: 0.1rem solid
|
|
317
|
+
<div style="border: 0.1rem solid rgb(128 128 128 / 0.35); border-radius: 0.8rem; overflow: hidden;">
|
|
318
318
|
<ActionMenuItem
|
|
319
319
|
label="Internal link (/about)"
|
|
320
320
|
href="/about"
|
|
321
|
-
style="border-bottom: 0.1rem solid
|
|
321
|
+
style="border-bottom: 0.1rem solid rgb(128 128 128 / 0.35);"
|
|
322
322
|
>
|
|
323
323
|
<template #icon><Icon name="lucide:arrow-up-right" /></template>
|
|
324
324
|
</ActionMenuItem>
|
|
@@ -14,6 +14,7 @@ global token so it matches the rest of the design system out of the box.
|
|
|
14
14
|
| `--select-menu-trigger-padding-block` | `0.8rem` | Trigger vertical padding |
|
|
15
15
|
| `--select-menu-trigger-padding-inline` | `1.2rem` | Trigger horizontal padding |
|
|
16
16
|
| `--select-menu-trigger-border-width` | `0.1rem` | Trigger border thickness |
|
|
17
|
+
| `--select-menu-trigger-border-width-underlined` | `var(--form-element-border-bottom-width-underlined, 0.3rem)` | Bottom border thickness in the `underlined` variant |
|
|
17
18
|
| `--select-menu-trigger-border` | `var(--theme-border)` | Trigger border colour (rest state) |
|
|
18
19
|
| `--select-menu-trigger-border-focus` | `var(--theme-border-focus)` | Trigger border colour on hover/focus |
|
|
19
20
|
| `--select-menu-trigger-border-radius` | `0.5rem` | Trigger corner rounding |
|
|
@@ -54,6 +55,15 @@ global token so it matches the rest of the design system out of the box.
|
|
|
54
55
|
|
|
55
56
|
---
|
|
56
57
|
|
|
58
|
+
## State hooks
|
|
59
|
+
|
|
60
|
+
| Hook | Where | Meaning |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `.normal` / `.underlined` | `.select-menu` root | The `inputVariant` prop. `underlined` removes the trigger's top and side borders and its corner radius, matching `InputSelect`'s underlined variant. |
|
|
63
|
+
| `:has(.select-menu-popover:popover-open)` | `.select-menu` root | Menu open (rotates the chevron). |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
57
67
|
## Global theming
|
|
58
68
|
|
|
59
69
|
Create `assets/styles/setup/07.components/select-menu.css` in the consuming app and set tokens
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="select-menu" :class="[elementClasses]" :style="`--_anchor-name: ${anchorName}`">
|
|
2
|
+
<div class="select-menu" :class="[inputVariant, elementClasses]" :style="`--_anchor-name: ${anchorName}`">
|
|
3
3
|
<button
|
|
4
4
|
ref="triggerRef"
|
|
5
5
|
:popovertarget="menuId"
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
|
|
62
62
|
<script setup lang="ts">
|
|
63
63
|
import type { SelectMenuOption } from "~/types/components/select-menu";
|
|
64
|
+
import type { InputUiVariant } from "~/types/forms/types.forms";
|
|
64
65
|
|
|
65
66
|
interface Props {
|
|
66
67
|
/** The full list of selectable options. */
|
|
@@ -79,6 +80,8 @@ interface Props {
|
|
|
79
80
|
multiple?: boolean;
|
|
80
81
|
/** 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. */
|
|
81
82
|
showSelectionInTrigger?: boolean;
|
|
83
|
+
/** Trigger border style, matching InputSelect: `normal` (bordered box) or `underlined` (bottom border only). */
|
|
84
|
+
inputVariant?: InputUiVariant;
|
|
82
85
|
styleClassPassthrough?: string | string[];
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -89,6 +92,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
89
92
|
showChevron: true,
|
|
90
93
|
multiple: false,
|
|
91
94
|
showSelectionInTrigger: false,
|
|
95
|
+
inputVariant: "normal",
|
|
92
96
|
styleClassPassthrough: () => [],
|
|
93
97
|
});
|
|
94
98
|
|
|
@@ -273,6 +277,16 @@ watch(
|
|
|
273
277
|
}
|
|
274
278
|
}
|
|
275
279
|
|
|
280
|
+
&.underlined .select-menu-trigger {
|
|
281
|
+
border-block-start-width: 0;
|
|
282
|
+
border-inline-width: 0;
|
|
283
|
+
border-block-end-width: var(
|
|
284
|
+
--select-menu-trigger-border-width-underlined,
|
|
285
|
+
var(--form-element-border-bottom-width-underlined, 0.3rem)
|
|
286
|
+
);
|
|
287
|
+
border-radius: 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
276
290
|
&:has(.select-menu-popover:popover-open) .select-menu-trigger-chevron {
|
|
277
291
|
transform: rotate(180deg);
|
|
278
292
|
}
|
|
@@ -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,7 +1,7 @@
|
|
|
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">
|
|
@@ -15,7 +15,7 @@ exports[`SelectMenu > Snapshots > default 1`] = `
|
|
|
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>
|
|
@@ -30,7 +30,7 @@ exports[`SelectMenu > Snapshots > icon-only trigger 1`] = `
|
|
|
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
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>
|
|
@@ -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.
|