srcdev-nuxt-components 9.3.11 → 9.3.13
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/component-aria-landmark.md +3 -3
- package/.claude/skills/components/auto-grid.md +1 -1
- package/.claude/skills/components/page-hero-highlights.md +1 -1
- package/.claude/skills/components/service-detail.md +13 -14
- package/.claude/skills/components/service-summary-grid.md +141 -0
- package/.claude/skills/components/service-summary.md +165 -0
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/index.md +3 -2
- package/.claude/skills/theming-component-token-pattern.md +126 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +67 -0
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +10 -1
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/input-copy/InputCopy.vue +9 -3
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/pricing-card/PricingCard.vue +6 -3
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +7 -8
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +0 -1
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +2 -2
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +116 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +183 -0
- package/app/components/03.organisms/services/{services-section/stories/ServicesSection.stories.ts → service-summary/stories/ServiceSummary.stories.ts} +78 -148
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +210 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +17 -0
- package/app/components/03.organisms/services/services-grids/{ServicesSectionGrid.vue → ServiceSummaryGrid.vue} +8 -10
- package/app/components/03.organisms/services/services-grids/stories/{ServicesSectionGrid.stories.ts → ServiceSummaryGrid.stories.ts} +21 -21
- package/app/components/03.organisms/services/services-grids/tests/{ServicesSectionGrid.spec.ts → ServiceSummaryGrid.spec.ts} +28 -28
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +45 -0
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +117 -0
- package/app/components/05.forms/input-button/InputButtonCore.vue +83 -37
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/with-both-nuxt-icon-components-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/stories/InputButtonCore.stories.ts +35 -0
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +68 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +19 -9
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioCore.vue +8 -5
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +93 -0
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioCore.stories.ts +75 -0
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +41 -0
- package/app/components/05.forms/input-number/InputNumberCore.vue +12 -6
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +35 -29
- package/app/components/05.forms/input-select/InputSelectCore.vue +32 -20
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +69 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +38 -48
- package/app/components/05.forms/input-text/InputTextCore.vue +35 -18
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +184 -0
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +125 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +49 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +23 -16
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +72 -0
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +42 -20
- package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitchCore.stories.ts +63 -0
- package/app/components/display-theme-switch/DisplayThemeSwitch.vue +13 -6
- package/package.json +1 -1
- package/.claude/skills/components/services-section-grid.md +0 -130
- package/.claude/skills/components/services-section.md +0 -211
- package/.vscode/srcdev-component-services-section.code-snippets +0 -84
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesSectionGrid.spec.ts.snap +0 -87
- package/app/components/03.organisms/services/services-section/CONSUMER-STYLING.md +0 -149
- package/app/components/03.organisms/services/services-section/ServicesSection.vue +0 -345
- package/app/components/03.organisms/services/services-section/tests/ServicesSection.spec.ts +0 -297
- package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +0 -87
|
@@ -14,9 +14,9 @@ or instance) without touching the component itself.
|
|
|
14
14
|
| `--input-copy-input-text-color` | `#333` | Input text colour |
|
|
15
15
|
| `--input-copy-input-placeholder-color` | `#999` | Placeholder text colour |
|
|
16
16
|
| `--input-copy-input-font-size` | `0.95rem` | Input field font size |
|
|
17
|
-
| `--input-copy-button-bg` | `var(--
|
|
18
|
-
| `--input-copy-button-bg-hover` | `var(--
|
|
19
|
-
| `--input-copy-button-text-color` | `var(--
|
|
17
|
+
| `--input-copy-button-bg` | `var(--input-button-primary-surface)` | Copy button background |
|
|
18
|
+
| `--input-copy-button-bg-hover` | `var(--input-button-primary-surface-hover)` | Copy button background on hover |
|
|
19
|
+
| `--input-copy-button-text-color` | `var(--input-button-primary-text)` | Copy button text colour |
|
|
20
20
|
| `--input-copy-button-padding` | `0.8rem 1.2rem` | Copy button padding |
|
|
21
21
|
| `--input-copy-button-border-radius` | `0.4rem` | Copy button corner rounding |
|
|
22
22
|
| `--input-copy-button-gap` | `0.6rem` | Gap between icon and text in button |
|
|
@@ -102,9 +102,15 @@ onUnmounted(() => {
|
|
|
102
102
|
--_input-placeholder-color: var(--input-copy-input-placeholder-color, #999);
|
|
103
103
|
--_input-font-size: var(--input-copy-input-font-size, 0.95rem);
|
|
104
104
|
|
|
105
|
-
--_button-bg: var(--input-copy-button-bg, var(--
|
|
106
|
-
--_button-bg-hover: var(
|
|
107
|
-
|
|
105
|
+
--_button-bg: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
|
|
106
|
+
--_button-bg-hover: var(
|
|
107
|
+
--input-copy-button-bg-hover,
|
|
108
|
+
var(--input-button-primary-surface-hover, var(--theme-surface-hover))
|
|
109
|
+
);
|
|
110
|
+
--_button-text-color: var(
|
|
111
|
+
--input-copy-button-text-color,
|
|
112
|
+
var(--input-button-primary-text, var(--theme-on-surface))
|
|
113
|
+
);
|
|
108
114
|
--_button-padding: var(--input-copy-button-padding, 0.8rem 1.2rem);
|
|
109
115
|
--_button-border-radius: var(--input-copy-button-border-radius, 0.4rem);
|
|
110
116
|
--_button-gap: var(--input-copy-button-gap, 0.6rem);
|
|
@@ -33,9 +33,9 @@ override it — there is no locale-aware formatting, just a literal prefix chara
|
|
|
33
33
|
| `--pricing-card-description-color` | `#555` | Plan description text colour |
|
|
34
34
|
| `--pricing-card-feature-color` | `#333` | Feature list text colour |
|
|
35
35
|
| `--pricing-card-cta-align` | `flex-start` | CTA wrapper `align-self` — set to `stretch`, `center`, or `flex-end` to change CTA alignment/width within the card |
|
|
36
|
-
| `--pricing-card-cta-bg` | `var(--
|
|
37
|
-
| `--pricing-card-cta-bg-hover` | `var(--
|
|
38
|
-
| `--pricing-card-cta-text` | `var(--
|
|
36
|
+
| `--pricing-card-cta-bg` | `var(--input-button-primary-surface)` | CTA button background |
|
|
37
|
+
| `--pricing-card-cta-bg-hover` | `var(--input-button-primary-surface-hover)` | CTA button background on hover |
|
|
38
|
+
| `--pricing-card-cta-text` | `var(--input-button-primary-text)` | CTA button text colour |
|
|
39
39
|
| `--pricing-card-cta-padding` | `1rem 1.6rem` | CTA button padding |
|
|
40
40
|
| `--pricing-card-cta-border-radius` | `0.4rem` | CTA button corner rounding |
|
|
41
41
|
|
|
@@ -111,9 +111,12 @@ watch(
|
|
|
111
111
|
--_feature-color: var(--pricing-card-feature-color, #333);
|
|
112
112
|
|
|
113
113
|
--_cta-align: var(--pricing-card-cta-align, flex-start);
|
|
114
|
-
--_cta-bg: var(--pricing-card-cta-bg, var(--
|
|
115
|
-
--_cta-bg-hover: var(
|
|
116
|
-
|
|
114
|
+
--_cta-bg: var(--pricing-card-cta-bg, var(--input-button-primary-surface, var(--theme-surface)));
|
|
115
|
+
--_cta-bg-hover: var(
|
|
116
|
+
--pricing-card-cta-bg-hover,
|
|
117
|
+
var(--input-button-primary-surface-hover, var(--theme-surface-hover))
|
|
118
|
+
);
|
|
119
|
+
--_cta-text: var(--pricing-card-cta-text, var(--input-button-primary-text, var(--theme-on-surface)));
|
|
117
120
|
--_cta-padding: var(--pricing-card-cta-padding, 1rem 1.6rem);
|
|
118
121
|
--_cta-border-radius: var(--pricing-card-cta-border-radius, 0.4rem);
|
|
119
122
|
|
|
@@ -26,7 +26,7 @@ The closing full-width CTA banner is wrapped the same way via `finalCtaVariant`
|
|
|
26
26
|
outer `PageRow` purely to constrain either section's width — pass `body-variant`/`final-cta-variant`
|
|
27
27
|
directly. It's still fine (and necessary for a genuine full-bleed hero) to place `ServiceDetail`
|
|
28
28
|
inside an outer, wider `PageRow` — see
|
|
29
|
-
[
|
|
29
|
+
[service-summary.md](../../../../../.claude/skills/components/service-summary.md)-style nesting
|
|
30
30
|
notes in [page-row.md](../../../../../.claude/skills/components/page-row.md) for how nested page-rows
|
|
31
31
|
compose.
|
|
32
32
|
|
|
@@ -49,11 +49,10 @@ compose.
|
|
|
49
49
|
| `--service-detail-hero-pill-bg` | `transparent` | Background of the price/duration pills (via `DisplayPill`'s `--theme-pill-bg`) |
|
|
50
50
|
| `--service-detail-hero-pill-border-colour` | `currentColor` | Border colour of the price/duration pills |
|
|
51
51
|
|
|
52
|
-
The hero image
|
|
53
|
-
|
|
52
|
+
The hero image is always `object-fit: cover` (not tokened) — only its anchor point changes
|
|
53
|
+
responsively: below the `768px` container breakpoint it's anchored with
|
|
54
54
|
`--service-detail-hero-image-position-small` (default `bottom`); from `768px` up it switches to
|
|
55
|
-
|
|
56
|
-
`center`) so the full photo stays visible once there's room for it, rather than continuing to crop.
|
|
55
|
+
`--service-detail-hero-image-position-medium` (default `center`).
|
|
57
56
|
|
|
58
57
|
For anything not covered by these (e.g. the `full`/`content` variant tracks themselves), target
|
|
59
58
|
`--page-row-*` tokens under `.service-detail__hero-image-row`/`.service-detail__hero-overlay` — see
|
|
@@ -211,7 +210,7 @@ Section headings, sidebar labels, and CTA copy are **props**, not hardcoded stri
|
|
|
211
210
|
|
|
212
211
|
- Routing is entirely delegated to the consumer via slots: `book-cta` (scoped `serviceData`),
|
|
213
212
|
`final-cta` (scoped `serviceData`), and `related-service` (scoped `service`, `index`) — same
|
|
214
|
-
pattern as `
|
|
213
|
+
pattern as `ServiceSummary`'s `summary-link` slot. Only `sidebar-note` has no scoped
|
|
215
214
|
data (it's plain content, e.g. patch-test wording).
|
|
216
215
|
- `related-service`'s fallback content is a non-clickable thumbnail + title + price — pass the
|
|
217
216
|
slot yourself to wrap it in a real link once you have a routing convention.
|
|
@@ -224,11 +223,11 @@ Section headings, sidebar labels, and CTA copy are **props**, not hardcoded stri
|
|
|
224
223
|
`serviceData` — pass real `to` routes once your app's URL structure is known.
|
|
225
224
|
- `headerTag` (default `"h1"`) controls only the hero title; `subheadingTag` (default `"h2"`)
|
|
226
225
|
controls every other heading in the component (What Is It, Process, Ideal For, Aftercare, FAQs,
|
|
227
|
-
final CTA) — unlike `
|
|
226
|
+
final CTA) — unlike `ServiceSummary`, which uses one `headerTag` prop for its (single) title heading. This
|
|
228
227
|
component is meant to be the whole page's `<h1>`, so its subheadings need their own level.
|
|
229
228
|
- The FAQ question is a hardcoded `<h3>` — if you set `subheadingTag="h3"`, the FAQ questions will
|
|
230
229
|
sit at the same level as their own section heading rather than one below it.
|
|
231
230
|
- Two-/three-column breakpoints (900px body, 500px ideal-for grid, 700px final CTA) use
|
|
232
231
|
`@container` queries against the component's own width, not the viewport — they are not tokens
|
|
233
232
|
since resizing them would need corresponding `minmax()`/`grid-template-columns` changes too, not
|
|
234
|
-
just spacing (same rationale as `
|
|
233
|
+
just spacing (same rationale as `ServiceSummary`'s `--service-summary-grid-gap-desktop`).
|
package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts
CHANGED
|
@@ -82,7 +82,7 @@ const meta: Meta<typeof ServiceDetail> = {
|
|
|
82
82
|
docs: {
|
|
83
83
|
description: {
|
|
84
84
|
component:
|
|
85
|
-
"Renders a full service detail page: a full-bleed hero banner (breadcrumb, eyebrow, title, price/duration pills over the service image), a two-column body (long-form content plus a sticky sidebar booking card and related-services list), and a closing full-width CTA banner. Routing (breadcrumb links, book-cta, related-service links, final-cta) is delegated to the consumer via slots, same pattern as
|
|
85
|
+
"Renders a full service detail page: a full-bleed hero banner (breadcrumb, eyebrow, title, price/duration pills over the service image), a two-column body (long-form content plus a sticky sidebar booking card and related-services list), and a closing full-width CTA banner. Routing (breadcrumb links, book-cta, related-service links, final-cta) is delegated to the consumer via slots, same pattern as ServiceSummary.",
|
|
86
86
|
},
|
|
87
87
|
},
|
|
88
88
|
},
|
|
@@ -299,7 +299,7 @@ export const CustomRelatedServiceLink: Story = {
|
|
|
299
299
|
docs: {
|
|
300
300
|
description: {
|
|
301
301
|
story:
|
|
302
|
-
"The related-service scoped slot replaces each related item's default (non-clickable) markup — use it to wrap the item in a real link, matching the pattern of
|
|
302
|
+
"The related-service scoped slot replaces each related item's default (non-clickable) markup — use it to wrap the item in a real link, matching the pattern of ServiceSummary's summary-link slot.",
|
|
303
303
|
},
|
|
304
304
|
},
|
|
305
305
|
},
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# ServiceSummary — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
All `--service-summary-*` tokens are the stable override surface. Set them globally in a
|
|
6
|
+
theme file, scoped to a page wrapper, or per-instance via `styleClassPassthrough`.
|
|
7
|
+
|
|
8
|
+
### Layout & sizing
|
|
9
|
+
|
|
10
|
+
| Token | Default | Controls |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `--service-summary-grid-gap` | `2rem` | Gap between image and content columns below the 768px container breakpoint |
|
|
13
|
+
| `--service-summary-grid-gap-desktop` | `3rem` | Gap between columns at the 768px container breakpoint and above |
|
|
14
|
+
| `--service-summary-height-mobile` | `auto` | Height of the whole `.service-summary` row below the 768px container breakpoint. `auto` (the default) lets the row size to its content — the image sizes itself via `--service-summary-image-aspect-ratio` and the text column sizes to its own content. Set a fixed length to force both columns to share a common row height instead (the image then crops via `object-fit: cover` to fill it, and `alignment` positions the text within the leftover space) |
|
|
15
|
+
| `--service-summary-height-tablet` | `auto` (falls back to `--service-summary-height-mobile`) | Row height from the 768px container breakpoint |
|
|
16
|
+
| `--service-summary-height-desktop` | `auto` (falls back to `--service-summary-height-tablet`, then `-mobile`) | Row height from the 1024px container breakpoint |
|
|
17
|
+
| `--service-summary-image-aspect-ratio` | `1 / 1` | Aspect ratio of `.service-summary__image-wrapper` when its height isn't otherwise forced by a `--service-summary-height-*` override (i.e. the default, row-height-auto case) — has no visible effect once a row height override makes both dimensions definite |
|
|
18
|
+
| `--service-summary-image-padding-block-mobile` | `0` | Padding-block (top/bottom) inset around the image, inside `.service-summary__image-wrapper`, below the 768px container breakpoint — creates a visible frame/border effect since the image itself sizes to the wrapper's content box, not the padding box |
|
|
19
|
+
| `--service-summary-image-padding-block-tablet` | `0` (falls back to `-mobile`) | Padding-block from the 768px container breakpoint |
|
|
20
|
+
| `--service-summary-image-padding-block-desktop` | `0` (falls back to `-tablet`, then `-mobile`) | Padding-block from the 1024px container breakpoint |
|
|
21
|
+
| `--service-summary-image-padding-inline-mobile` | `0` | Padding-inline (left/right) inset around the image below the 768px container breakpoint |
|
|
22
|
+
| `--service-summary-image-padding-inline-tablet` | `0` (falls back to `-mobile`) | Padding-inline from the 768px container breakpoint |
|
|
23
|
+
| `--service-summary-image-padding-inline-desktop` | `0` (falls back to `-tablet`, then `-mobile`) | Padding-inline from the 1024px container breakpoint |
|
|
24
|
+
| `--service-summary-image-border-radius` | `0.8rem` | Corner rounding on the service image |
|
|
25
|
+
| `--service-summary-pills-gap` | `0.8rem` | Gap between the duration and price pills |
|
|
26
|
+
| `--service-summary-pills-margin-block-end` | `2rem` | Space below the pill row |
|
|
27
|
+
|
|
28
|
+
All breakpoints above (row height, image padding, and the two-column grid switch) are
|
|
29
|
+
**container queries** against `.service-summary`'s own inline size (`container-name:
|
|
30
|
+
service-summary`), not the viewport — this keeps the layout responsive to the space
|
|
31
|
+
`ServiceSummary` actually has, which matters when it sits next to a persistent side nav that a
|
|
32
|
+
viewport-based `@media` query can't see.
|
|
33
|
+
|
|
34
|
+
Below the 768px breakpoint the grid is a single stacked column (image above text), so a
|
|
35
|
+
`--service-summary-height-mobile` override sets a height for the *whole stack*, not just the
|
|
36
|
+
image — usually left `auto` on mobile and only set from `--service-summary-height-tablet` up,
|
|
37
|
+
where the layout is genuinely two columns side by side.
|
|
38
|
+
|
|
39
|
+
### Pill colours
|
|
40
|
+
|
|
41
|
+
The duration/price pills are `DisplayPill` instances — `ServiceSummary` maps its own tokens onto
|
|
42
|
+
`DisplayPill`'s `--theme-pill-*` custom properties scoped to `.service-summary__pills`:
|
|
43
|
+
|
|
44
|
+
| Token | Default | Controls |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `--service-summary-pill-bg` | `transparent` | Background of the price/duration pills |
|
|
47
|
+
| `--service-summary-pill-colour` | `currentColor` | Text colour of the pills |
|
|
48
|
+
| `--service-summary-pill-border-colour` | `currentColor` | Border colour of the pills |
|
|
49
|
+
|
|
50
|
+
For anything not covered by these (e.g. pill size, font weight), target `--theme-pill-*` directly
|
|
51
|
+
under `.service-summary__pills` — see [display-pill.md](../../../../../.claude/skills/components/display-pill.md).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Global theming — recommended approach
|
|
56
|
+
|
|
57
|
+
Create `assets/styles/setup/07.components/service-summary.css` in the consuming app and
|
|
58
|
+
set tokens on `:root`. This applies to every `ServiceSummary` across the site.
|
|
59
|
+
|
|
60
|
+
```css
|
|
61
|
+
/* assets/styles/setup/07.components/service-summary.css */
|
|
62
|
+
:root {
|
|
63
|
+
--service-summary-grid-gap: 2.4rem;
|
|
64
|
+
--service-summary-grid-gap-desktop: 4rem;
|
|
65
|
+
--service-summary-image-border-radius: 1.2rem;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Page-scoped overrides
|
|
72
|
+
|
|
73
|
+
Override tokens for a specific section by scoping them under the page or layout wrapper.
|
|
74
|
+
No `:deep()` is required (component styles are unscoped).
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
/* In the consuming page's unscoped <style> block */
|
|
78
|
+
.our-services-page {
|
|
79
|
+
.service-summary {
|
|
80
|
+
--service-summary-pill-border-colour: var(--brand-accent);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Per-instance overrides via styleClassPassthrough
|
|
88
|
+
|
|
89
|
+
Use sparingly — prefer global or page-scoped CSS. When a single instance needs a distinct
|
|
90
|
+
visual style, pass a modifier class:
|
|
91
|
+
|
|
92
|
+
```vue
|
|
93
|
+
<ServiceSummary :style-class-passthrough="['highlight-service']" :service-data="service" />
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
.service-summary.highlight-service {
|
|
98
|
+
--service-summary-image-border-radius: 2rem;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Notes
|
|
105
|
+
|
|
106
|
+
- `--service-summary-grid-gap-desktop` only applies at `768px` and above — the breakpoint
|
|
107
|
+
itself is not a token, since changing it would affect the grid's `minmax()` column sizing
|
|
108
|
+
too, not just spacing. The same 768px/1024px breakpoints are shared with the row-height
|
|
109
|
+
tokens above.
|
|
110
|
+
- The row-height tokens live on `.service-summary` itself (the whole row), not on
|
|
111
|
+
`.service-summary__image-wrapper` — `.service-summary__grid` and
|
|
112
|
+
`.service-summary__image-wrapper` both inherit it via `height: 100%`, so overriding the row
|
|
113
|
+
height also gives `alignment` real leftover space to position the text column within.
|
|
114
|
+
- `ServiceSummary` is summary-only — it has no "full mode". For a complete single-service page
|
|
115
|
+
(process, ideal-for list, FAQs, booking CTA), use
|
|
116
|
+
[ServiceDetail](../../../../../.claude/skills/components/service-detail.md) instead.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="tag" class="service-summary" :class="[elementClasses]" :aria-labelledby="ariaLabelledby">
|
|
3
|
+
<div class="service-summary__grid" :class="{ 'service-summary__grid--reverse': reverse }">
|
|
4
|
+
<div class="service-summary__image-wrapper">
|
|
5
|
+
<NuxtImg
|
|
6
|
+
:src="serviceData.image"
|
|
7
|
+
:alt="serviceData.title"
|
|
8
|
+
:loading="imageLoading"
|
|
9
|
+
:fetchpriority="imageFetchPriority"
|
|
10
|
+
class="service-summary__image"
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="service-summary__info-wrapper" :class="infoWrapperClasses">
|
|
14
|
+
<EyebrowText font-size="large" :text-content="serviceData.subtitle" />
|
|
15
|
+
<HeroText
|
|
16
|
+
:id="headingId"
|
|
17
|
+
:tag="headerTag"
|
|
18
|
+
font-size="title"
|
|
19
|
+
:text-content="[{ text: serviceData.title, styleClass: 'normal' }]"
|
|
20
|
+
:style-class-passthrough="['mb-20']"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<div class="service-summary__pills">
|
|
24
|
+
<DisplayPill :label="serviceData.duration" size="md" variant="neutral" />
|
|
25
|
+
<DisplayPill :label="`From ${serviceData.price}`" size="md" variant="neutral" />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<p class="page-body-normal">{{ serviceData.whatIsIt }}</p>
|
|
29
|
+
|
|
30
|
+
<slot name="summary-link" :service-data="serviceData"></slot>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</component>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import type { Service } from "~/types/types.services";
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
tag?: "div" | "section" | "article" | "main";
|
|
41
|
+
headerTag?: "h1" | "h2" | "h3";
|
|
42
|
+
index?: number;
|
|
43
|
+
serviceData: Service;
|
|
44
|
+
alignment?: "start" | "center" | "end";
|
|
45
|
+
reverse?: boolean;
|
|
46
|
+
styleClassPassthrough?: string | string[];
|
|
47
|
+
}
|
|
48
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
49
|
+
tag: "div",
|
|
50
|
+
headerTag: "h2",
|
|
51
|
+
index: 0,
|
|
52
|
+
alignment: "center",
|
|
53
|
+
reverse: false,
|
|
54
|
+
styleClassPassthrough: () => [],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const { headingId, ariaLabelledby } = useAriaLabelledById(() => props.tag);
|
|
58
|
+
|
|
59
|
+
const infoWrapperClasses = computed(() => ({
|
|
60
|
+
[`service-summary__info-wrapper--align-${props.alignment}`]: true,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// Computed to return loading="lazy" if index is greater than 1, so that the first two summaries prioritise image loading, but if there are more than 2 summaries, the rest will lazy load their images to improve performance.
|
|
64
|
+
const imageLoading = computed(() => (props.index !== undefined && props.index > 1 ? "lazy" : "eager"));
|
|
65
|
+
|
|
66
|
+
// Only the first image (LCP candidate) gets fetchpriority="high" to reduce resource load delay.
|
|
67
|
+
const imageFetchPriority = computed(() => (props.index === 0 ? "high" : "auto"));
|
|
68
|
+
|
|
69
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
70
|
+
|
|
71
|
+
watch(
|
|
72
|
+
() => props.styleClassPassthrough,
|
|
73
|
+
() => {
|
|
74
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<style lang="css">
|
|
80
|
+
@layer components {
|
|
81
|
+
.service-summary {
|
|
82
|
+
container-type: inline-size;
|
|
83
|
+
container-name: service-summary;
|
|
84
|
+
height: var(--service-summary-height-mobile, auto);
|
|
85
|
+
|
|
86
|
+
@container service-summary (width >= 768px) {
|
|
87
|
+
height: var(--service-summary-height-tablet, var(--service-summary-height-mobile, auto));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@container service-summary (width >= 1024px) {
|
|
91
|
+
height: var(
|
|
92
|
+
--service-summary-height-desktop,
|
|
93
|
+
var(--service-summary-height-tablet, var(--service-summary-height-mobile, auto))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.service-summary__grid {
|
|
98
|
+
--_grid-gap: var(--service-summary-grid-gap, 2rem);
|
|
99
|
+
|
|
100
|
+
display: grid;
|
|
101
|
+
grid-template-columns: 1fr;
|
|
102
|
+
gap: var(--_grid-gap);
|
|
103
|
+
height: 100%;
|
|
104
|
+
|
|
105
|
+
@container service-summary (width >= 768px) {
|
|
106
|
+
--_grid-gap: var(--service-summary-grid-gap-desktop, 3rem);
|
|
107
|
+
|
|
108
|
+
grid-template-columns: repeat(auto-fit, minmax(246px, 1fr));
|
|
109
|
+
|
|
110
|
+
&.service-summary__grid--reverse {
|
|
111
|
+
.service-summary__image-wrapper {
|
|
112
|
+
/* The order: 2 on .service-summary__image-wrapper pushes it after the content div, since by default both children have order: 0 and source order wins — so the content div naturally sits first. */
|
|
113
|
+
order: 2;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.service-summary__info-wrapper {
|
|
119
|
+
display: grid;
|
|
120
|
+
|
|
121
|
+
&.service-summary__info-wrapper--align-start {
|
|
122
|
+
align-content: start;
|
|
123
|
+
}
|
|
124
|
+
&.service-summary__info-wrapper--align-center {
|
|
125
|
+
align-content: center;
|
|
126
|
+
}
|
|
127
|
+
&.service-summary__info-wrapper--align-end {
|
|
128
|
+
align-content: end;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.service-summary__image-wrapper {
|
|
133
|
+
height: 100%;
|
|
134
|
+
aspect-ratio: var(--service-summary-image-aspect-ratio, 1 / 1);
|
|
135
|
+
padding-block: var(--service-summary-image-padding-block-mobile, 0);
|
|
136
|
+
padding-inline: var(--service-summary-image-padding-inline-mobile, 0);
|
|
137
|
+
border-radius: var(--service-summary-image-border-radius, 0.8rem);
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
|
|
140
|
+
@container service-summary (width >= 768px) {
|
|
141
|
+
padding-block: var(
|
|
142
|
+
--service-summary-image-padding-block-tablet,
|
|
143
|
+
var(--service-summary-image-padding-block-mobile, 0)
|
|
144
|
+
);
|
|
145
|
+
padding-inline: var(
|
|
146
|
+
--service-summary-image-padding-inline-tablet,
|
|
147
|
+
var(--service-summary-image-padding-inline-mobile, 0)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@container service-summary (width >= 1024px) {
|
|
152
|
+
padding-block: var(
|
|
153
|
+
--service-summary-image-padding-block-desktop,
|
|
154
|
+
var(--service-summary-image-padding-block-tablet, var(--service-summary-image-padding-block-mobile, 0))
|
|
155
|
+
);
|
|
156
|
+
padding-inline: var(
|
|
157
|
+
--service-summary-image-padding-inline-desktop,
|
|
158
|
+
var(--service-summary-image-padding-inline-tablet, var(--service-summary-image-padding-inline-mobile, 0))
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.service-summary__image {
|
|
163
|
+
display: block;
|
|
164
|
+
width: 100%;
|
|
165
|
+
height: 100%;
|
|
166
|
+
object-fit: cover;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.service-summary__pills {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-wrap: wrap;
|
|
173
|
+
gap: var(--service-summary-pills-gap, 0.8rem);
|
|
174
|
+
margin-block-end: var(--service-summary-pills-margin-block-end, 2rem);
|
|
175
|
+
|
|
176
|
+
--theme-pill-bg: var(--service-summary-pill-bg, transparent);
|
|
177
|
+
--theme-pill-color: var(--service-summary-pill-colour, currentColor);
|
|
178
|
+
--theme-pill-border-color: var(--service-summary-pill-border-colour, currentColor);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
</style>
|