Skip to content

Commit 4cca47e

Browse files
committed
fix: de-hardcode ServicesSection copy/CSS and add cta-panel override slot
GlassPanel CTA text and section subheadings were hardcoded strings ported from another project (including a copy bug where every service's CTA read "Ready to book your highlights appointment?" regardless of the service). These are now props (processHeading, idealForHeading, maintenanceHeading, faqsHeading, ctaHeading, ctaBody), with a new cta-panel scoped slot for consumers who need to replace the CTA block entirely. Also namespaces internal CSS classes to services-section__* BEM naming (previously unprefixed and collision-prone), removes a dead .glass-card rule, and exposes layout/spacing values as --services-section-* custom properties. Adds CONSUMER-STYLING.md, updates the services-section skill doc and VS Code snippet, and refreshes the Storybook stories with content matching the real service detail page.
1 parent c3c9ff7 commit 4cca47e

9 files changed

Lines changed: 661 additions & 123 deletions

File tree

.claude/skills/components/services-section.md

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,40 @@ All routing and CTA decisions are delegated to the consumer via slots.
1515
|------|------|---------|----------|
1616
| `serviceData` | `Service` || **yes** |
1717
| `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | no |
18+
| `headerTag` | `"h1" \| "h2" \| "h3"` | `"h2"` | no |
1819
| `index` | `number` | `0` | no |
1920
| `isSummary` | `boolean` | `false` | no |
2021
| `summaryAlignment` | `"start" \| "center" \| "end"` | `"center"` | no |
2122
| `reverse` | `boolean` | `false` | no |
2223
| `durationIcon` | `string` | `"mdi:clock-time-four-outline"` | no |
2324
| `priceIcon` | `string` | `"mdi:currency-gbp"` | no |
25+
| `processHeading` | `string` | `"The Process"` | no |
26+
| `idealForHeading` | `string` | `"Ideal For"` | no |
27+
| `maintenanceHeading` | `string` | `"Aftercare & Maintenance"` | no |
28+
| `faqsHeading` | `string` | `"Frequently Asked Questions"` | no |
29+
| `ctaHeading` | `string` | `"Ready to book your appointment?"` | no |
30+
| `ctaBody` | `string` | `"Get in touch to book your appointment."` | no |
2431
| `styleClassPassthrough` | `string \| string[]` | `[]` | no |
2532

33+
### Heading and CTA copy props
34+
35+
All section subheadings and the default CTA panel's heading/body are props, not hardcoded
36+
text — override them with wording specific to the consuming business:
37+
38+
```vue
39+
<ServicesSection
40+
:service-data="service"
41+
process-heading="How It Works"
42+
cta-heading="Ready to book your colour appointment?"
43+
cta-body="Mobile service across Bath — I come to you."
44+
/>
45+
```
46+
47+
Name the actual service in `ctaHeading` (rather than a generic phrase) if every service page
48+
shares one CTA wrapper — a single hardcoded heading applied to every service previously caused
49+
a real bug in production, where a "Colour" service page's CTA read "Ready to book your
50+
**highlights** appointment?" regardless of which service was being viewed.
51+
2652
### Icon customisation
2753

2854
The two icons in the price/duration row default to `mdi:clock-time-four-outline` and `mdi:currency-gbp`. Override them for a different currency or icon style:
@@ -51,8 +77,12 @@ Flips the image to the right column and content to the left (CSS `order: 2` on t
5177
|------|-----------|---------------|---------|
5278
| `summary-link` | `{ serviceData: Service }` | `isSummary` is `true` | Navigation link below the `whatIsIt` text in summary mode |
5379
| `cta` | `{ serviceData: Service }` | `isSummary` is `false` | CTA button/link inside the closing `GlassPanel` in full mode |
80+
| `cta-panel` | `{ serviceData: Service, ctaHeading: string, ctaBody: string }` | `isSummary` is `false` | Replaces the entire default `GlassPanel` CTA block — use when a consumer needs a different component or layout, not just different copy |
5481

55-
Both slots receive `serviceData` as a scoped prop.
82+
All three slots receive `serviceData` as a scoped prop. `cta-panel`'s default content *is*
83+
the `GlassPanel` + `ctaHeading`/`ctaBody` + `cta` slot combination described above — providing
84+
`cta-panel` replaces that whole block, so the `ctaHeading`/`ctaBody` props and `cta` slot are
85+
only useful if the `cta-panel` slot fallback (or the consumer's own override) still renders them.
5686

5787
## Summary mode usage
5888

@@ -102,6 +132,21 @@ Both slots receive `serviceData` as a scoped prop.
102132
</ServicesSection>
103133
```
104134

135+
## Replacing the whole CTA panel (slot override)
136+
137+
Use `cta-panel` instead of `ctaHeading`/`ctaBody` when the CTA needs a different component
138+
entirely — not just different text inside `GlassPanel`:
139+
140+
```vue
141+
<ServicesSection :service-data="service">
142+
<template #cta-panel="{ serviceData, ctaHeading, ctaBody }">
143+
<MyPromoBanner :heading="ctaHeading" :body="ctaBody">
144+
<template #cta><InputButtonCore button-text="Enquire Now" href="/contact" /></template>
145+
</MyPromoBanner>
146+
</template>
147+
</ServicesSection>
148+
```
149+
105150
## Rendering a list (summary mode)
106151

107152
```vue
@@ -122,9 +167,12 @@ Both slots receive `serviceData` as a scoped prop.
122167

123168
## Local style override scaffold
124169

125-
When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
170+
When consuming this component, prefer the `--services-section-*` CSS custom properties
171+
documented in `CONSUMER-STYLING.md` (in the component's own folder) over raw class overrides.
172+
For anything the tokens don't cover, scaffold a style block using `styleClassPassthrough`.
173+
Delete the block if unused.
126174

127-
See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
175+
See [component-local-style-override.md](../component-local-style-override.md) for the general pattern.
128176

129177
```vue
130178
<ServicesSection :style-class-passthrough="['my-section']" :service-data="service">
@@ -138,8 +186,10 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
138186
─────────────────────────────────────────────────────────────────── */
139187
.services-section {
140188
&.my-section {
141-
/* Geometry — image wrapper */
142-
/* .image-wrapper { border-radius: 1.2rem; } */
189+
--services-section-image-border-radius: 1.2rem;
190+
191+
/* Deeper overrides target the BEM element classes directly, e.g.: */
192+
/* .services-section__faq { } */
143193
}
144194
}
145195
</style>
@@ -150,6 +200,12 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
150200
- Component is auto-imported in Nuxt — no import needed.
151201
- The `Service` type is imported from `~/types/types.services`.
152202
- `summary-link` slot is guarded by `v-if="isSummary"` — it will not render in full mode even if provided.
153-
- `cta` slot lives inside a `v-if="!isSummary"` `GlassPanel` — it will not render in summary mode.
203+
- `cta` and `cta-panel` slots live inside `v-if="!isSummary"` — neither renders in summary mode.
204+
- Internal element classes are BEM-namespaced: `services-section__image-wrapper`,
205+
`services-section__info-wrapper`, `services-section__price-duration`,
206+
`services-section__decorator`, `services-section__faq`, `services-section__faq-answer`,
207+
`services-section__glass-panel`. If a consuming app has CSS overrides referencing older
208+
unprefixed names (`.image-wrapper`, `.price-duration`, `.services-faq`, etc. — from before
209+
this component's classnames were namespaced), those selectors need updating to match.
154210
- The section gets `aria-labelledby` automatically when `tag` is `"section"` or `"article"`, pointing to the internal heading id.
155211
- `summaryAlignment` only has effect when `isSummary` is `true` — it aligns the info-wrapper content vertically within the grid cell.

.claude/skills/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
6767
├── services-card.md — ServicesCard props (incl. eyebrowConfig/heroConfig), actions slot, CSS tokens, page boilerplate
6868
├── services-card-grid.md — ServicesCardGrid props, config pass-through, CSS tokens, full page boilerplate
6969
├── services-section-grid.md — ServicesSectionGrid props, useAlternateReverse zigzag layout, page boilerplate
70-
├── services-section.md — ServicesSection props, summary-link/cta slots, summary vs full mode
70+
├── services-section.md — ServicesSection props (incl. heading/CTA copy), summary-link/cta/cta-panel slots, summary vs full mode
7171
├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
7272
├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
7373
├── expanding-panel.md — ExpandingPanel v-model, forceOpened, slots (summary/icon/content), ARIA wiring
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"SRCDEV ServicesSection Full Mode": {
3+
"description": "ServicesSection full mode — complete content with CTA",
4+
"scope": "vue,html",
5+
"body": [
6+
"<ServicesSection",
7+
" :service-data=\"$1service\"",
8+
" :index=\"$2index\"",
9+
">",
10+
" <template #cta>",
11+
" <button type=\"button\">$3Book Now</button>",
12+
" </template>",
13+
"</ServicesSection>"
14+
]
15+
},
16+
"SRCDEV ServicesSection Summary Mode": {
17+
"description": "ServicesSection summary mode — compact card with summary-link slot",
18+
"scope": "vue,html",
19+
"body": [
20+
"<ServicesSection",
21+
" :service-data=\"$1service\"",
22+
" is-summary",
23+
" summary-alignment=\"$2center\"",
24+
">",
25+
" <template #summary-link=\"{ serviceData }\">",
26+
" <NuxtLink :to=\"`/services/${serviceData.slug}`\">$3More about {{ serviceData.title }} →</NuxtLink>",
27+
" </template>",
28+
"</ServicesSection>"
29+
]
30+
},
31+
"SRCDEV ServicesSection Custom Headings And Cta": {
32+
"description": "ServicesSection with custom section headings and CTA copy props",
33+
"scope": "vue,html",
34+
"body": [
35+
"<ServicesSection",
36+
" :service-data=\"$1service\"",
37+
" process-heading=\"$2How It Works\"",
38+
" ideal-for-heading=\"$3Who Is This For\"",
39+
" maintenance-heading=\"$4Aftercare\"",
40+
" faqs-heading=\"$5FAQs\"",
41+
" cta-heading=\"$6Ready to book?\"",
42+
" cta-body=\"$7Get in touch to book your appointment.\"",
43+
">",
44+
" <template #cta>",
45+
" <button type=\"button\">$8Book Now</button>",
46+
" </template>",
47+
"</ServicesSection>"
48+
]
49+
},
50+
"SRCDEV ServicesSection Custom Cta Panel Slot": {
51+
"description": "ServicesSection with the cta-panel slot fully replacing the default GlassPanel",
52+
"scope": "vue,html",
53+
"body": [
54+
"<ServicesSection :service-data=\"$1service\">",
55+
" <template #cta-panel=\"{ serviceData, ctaHeading, ctaBody }\">",
56+
" $2",
57+
" </template>",
58+
"</ServicesSection>"
59+
]
60+
},
61+
"SRCDEV ServicesSection CSS Override": {
62+
"description": "CSS override scaffold for ServicesSection — scope to a page or section class",
63+
"scope": "css",
64+
"body": [
65+
".$1my-page {",
66+
" .services-section {",
67+
" --services-section-grid-gap: ;",
68+
" --services-section-grid-gap-desktop: ;",
69+
" --services-section-image-border-radius: ;",
70+
" --services-section-price-duration-gap: ;",
71+
" --services-section-decorator-size: ;",
72+
" --services-section-faq-divider-color: ;",
73+
" --services-section-faq-answer-line-height: ;",
74+
"",
75+
" .services-section__image-wrapper {}",
76+
" .services-section__info-wrapper {}",
77+
" .services-section__price-duration {}",
78+
" .services-section__faq {}",
79+
" .services-section__glass-panel {}",
80+
" }",
81+
"}"
82+
]
83+
}
84+
}

app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesSectionGrid.spec.ts.snap

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ exports[`ServicesSectionGrid > renders correct HTML structure 1`] = `
44
"<div class="services-grid">
55
<div class="services-section">
66
<div class="services-section__grid">
7-
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-installation.jpg 1x, /_ipx/_/images/locs-installation.jpg 2x" alt="Locs Installation" loading="eager" fetchpriority="high" class="image" src="/_ipx/_/images/locs-installation.jpg"></div>
8-
<div class="info-wrapper is-summary align-center">
7+
<div class="services-section__image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-installation.jpg 1x, /_ipx/_/images/locs-installation.jpg 2x" alt="Locs Installation" loading="eager" fetchpriority="high" class="services-section__image" src="/_ipx/_/images/locs-installation.jpg"></div>
8+
<div class="services-section__info-wrapper services-section__info-wrapper--summary services-section__info-wrapper--align-center">
99
<div class="eyebrow-text large">Subtitle for Locs Installation</div>
1010
<h2 class="hero-text mb-20 title axis-horizontal">
1111
<!--v-if--><span class="text-block-0 normal">Locs Installation</span>
1212
</h2>
13-
<div class="price-duration">
14-
<div class="flex-row"><span class="iconify i-mdi:clock-time-four-outline decorator" aria-hidden="true"></span><span>60 mins</span></div>
15-
<div class="flex-row"><span class="iconify i-mdi:currency-gbp decorator" aria-hidden="true"></span><span>£50</span></div>
13+
<div class="services-section__price-duration">
14+
<div class="flex-row"><span class="iconify i-mdi:clock-time-four-outline services-section__decorator" aria-hidden="true"></span><span>60 mins</span></div>
15+
<div class="flex-row"><span class="iconify i-mdi:currency-gbp services-section__decorator" aria-hidden="true"></span><span>£50</span></div>
1616
</div>
1717
<!--v-if-->
1818
<!--v-if-->
@@ -31,15 +31,15 @@ exports[`ServicesSectionGrid > renders correct HTML structure 1`] = `
3131
</div>
3232
<div class="services-section">
3333
<div class="services-section__grid">
34-
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-retwist.jpg 1x, /_ipx/_/images/locs-retwist.jpg 2x" alt="Locs Retwist" loading="eager" fetchpriority="auto" class="image" src="/_ipx/_/images/locs-retwist.jpg"></div>
35-
<div class="info-wrapper is-summary align-center">
34+
<div class="services-section__image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-retwist.jpg 1x, /_ipx/_/images/locs-retwist.jpg 2x" alt="Locs Retwist" loading="eager" fetchpriority="auto" class="services-section__image" src="/_ipx/_/images/locs-retwist.jpg"></div>
35+
<div class="services-section__info-wrapper services-section__info-wrapper--summary services-section__info-wrapper--align-center">
3636
<div class="eyebrow-text large">Subtitle for Locs Retwist</div>
3737
<h2 class="hero-text mb-20 title axis-horizontal">
3838
<!--v-if--><span class="text-block-0 normal">Locs Retwist</span>
3939
</h2>
40-
<div class="price-duration">
41-
<div class="flex-row"><span class="iconify i-mdi:clock-time-four-outline decorator" aria-hidden="true"></span><span>60 mins</span></div>
42-
<div class="flex-row"><span class="iconify i-mdi:currency-gbp decorator" aria-hidden="true"></span><span>£50</span></div>
40+
<div class="services-section__price-duration">
41+
<div class="flex-row"><span class="iconify i-mdi:clock-time-four-outline services-section__decorator" aria-hidden="true"></span><span>60 mins</span></div>
42+
<div class="flex-row"><span class="iconify i-mdi:currency-gbp services-section__decorator" aria-hidden="true"></span><span>£50</span></div>
4343
</div>
4444
<!--v-if-->
4545
<!--v-if-->
@@ -58,15 +58,15 @@ exports[`ServicesSectionGrid > renders correct HTML structure 1`] = `
5858
</div>
5959
<div class="services-section">
6060
<div class="services-section__grid">
61-
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/colour-treatment.jpg 1x, /_ipx/_/images/colour-treatment.jpg 2x" alt="Colour Treatment" loading="lazy" fetchpriority="auto" class="image" src="/_ipx/_/images/colour-treatment.jpg"></div>
62-
<div class="info-wrapper is-summary align-center">
61+
<div class="services-section__image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/colour-treatment.jpg 1x, /_ipx/_/images/colour-treatment.jpg 2x" alt="Colour Treatment" loading="lazy" fetchpriority="auto" class="services-section__image" src="/_ipx/_/images/colour-treatment.jpg"></div>
62+
<div class="services-section__info-wrapper services-section__info-wrapper--summary services-section__info-wrapper--align-center">
6363
<div class="eyebrow-text large">Subtitle for Colour Treatment</div>
6464
<h2 class="hero-text mb-20 title axis-horizontal">
6565
<!--v-if--><span class="text-block-0 normal">Colour Treatment</span>
6666
</h2>
67-
<div class="price-duration">
68-
<div class="flex-row"><span class="iconify i-mdi:clock-time-four-outline decorator" aria-hidden="true"></span><span>60 mins</span></div>
69-
<div class="flex-row"><span class="iconify i-mdi:currency-gbp decorator" aria-hidden="true"></span><span>£50</span></div>
67+
<div class="services-section__price-duration">
68+
<div class="flex-row"><span class="iconify i-mdi:clock-time-four-outline services-section__decorator" aria-hidden="true"></span><span>60 mins</span></div>
69+
<div class="flex-row"><span class="iconify i-mdi:currency-gbp services-section__decorator" aria-hidden="true"></span><span>£50</span></div>
7070
</div>
7171
<!--v-if-->
7272
<!--v-if-->

0 commit comments

Comments
 (0)