srcdev-nuxt-components 9.3.10 → 9.3.12

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 (30) hide show
  1. package/.claude/skills/component-aria-landmark.md +3 -3
  2. package/.claude/skills/components/auto-grid.md +1 -1
  3. package/.claude/skills/components/page-hero-highlights.md +1 -1
  4. package/.claude/skills/components/service-detail.md +13 -14
  5. package/.claude/skills/components/service-summary-grid.md +141 -0
  6. package/.claude/skills/components/service-summary.md +165 -0
  7. package/.claude/skills/icon-sets.md +1 -1
  8. package/.claude/skills/index.md +2 -2
  9. package/.vscode/srcdev-component-service-summary.code-snippets +67 -0
  10. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +8 -9
  11. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +3 -2
  12. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +2 -2
  13. package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +116 -0
  14. package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +183 -0
  15. package/app/components/03.organisms/services/{services-section/stories/ServicesSection.stories.ts → service-summary/stories/ServiceSummary.stories.ts} +78 -148
  16. package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +210 -0
  17. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +17 -0
  18. package/app/components/03.organisms/services/services-grids/{ServicesSectionGrid.vue → ServiceSummaryGrid.vue} +8 -10
  19. package/app/components/03.organisms/services/services-grids/stories/{ServicesSectionGrid.stories.ts → ServiceSummaryGrid.stories.ts} +21 -21
  20. package/app/components/03.organisms/services/services-grids/tests/{ServicesSectionGrid.spec.ts → ServiceSummaryGrid.spec.ts} +28 -28
  21. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +45 -0
  22. package/package.json +1 -1
  23. package/.claude/skills/components/services-section-grid.md +0 -130
  24. package/.claude/skills/components/services-section.md +0 -211
  25. package/.vscode/srcdev-component-services-section.code-snippets +0 -84
  26. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesSectionGrid.spec.ts.snap +0 -87
  27. package/app/components/03.organisms/services/services-section/CONSUMER-STYLING.md +0 -149
  28. package/app/components/03.organisms/services/services-section/ServicesSection.vue +0 -345
  29. package/app/components/03.organisms/services/services-section/tests/ServicesSection.spec.ts +0 -297
  30. package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +0 -87
@@ -31,9 +31,9 @@ correctly** — always check the console (or run an accessibility audit) after a
31
31
 
32
32
  This bug class previously shipped to production undetected: eight sections across one site had
33
33
  `aria-labelledby` pointing at ids that were never applied to anything, only surfaced by a WAVE
34
- audit. Two components (`ServicesSection`, `AutoGrid`) were also found to be *structurally* broken
34
+ audit. Two components (`ServiceSummary` (formerly ServicesSection), `AutoGrid`) were also found to be *structurally* broken
35
35
  — they set `aria-labelledby` from `tag` alone without ever exposing `headingId` anywhere a
36
- consumer could bind it, so it was impossible to satisfy correctly. `ServicesSection` now binds
36
+ consumer could bind it, so it was impossible to satisfy correctly. `ServiceSummary` now binds
37
37
  `headingId` to its own internal title heading; `AutoGrid` has no heading concept at all, so it no
38
38
  longer sets `aria-labelledby` under any circumstance (pass `aria-label` directly if needed).
39
39
 
@@ -87,7 +87,7 @@ must bind it to their own heading):
87
87
  Self-bound (the component renders its own heading and binds `headingId` internally — no consumer
88
88
  action needed):
89
89
 
90
- - `ServicesSection` (03.organisms) — binds it to its own title `HeroText`
90
+ - `ServiceSummary` (03.organisms) — binds it to its own title `HeroText`
91
91
  - `LayoutGridByCols` / `LayoutGridByWidth` (01.atoms) — render their own visually-hidden `<p>` from the `label` prop
92
92
 
93
93
  Not using this pattern:
@@ -95,7 +95,7 @@ Override `grid-template-columns` directly — there is no single token for this:
95
95
 
96
96
  `AutoGrid` has no heading concept of its own — its slots are arbitrary named items, not a
97
97
  header + body — so it does **not** auto-generate `aria-labelledby` the way `PageRow` or
98
- `ServicesSection` do (an earlier version of this component did attempt to, and it produced a
98
+ `ServiceSummary` do (an earlier version of this component did attempt to, and it produced a
99
99
  guaranteed broken ARIA reference, since there was never any way to bind a heading to it). If
100
100
  `tag="section"` needs an accessible name, pass `aria-label` directly:
101
101
 
@@ -61,7 +61,7 @@ Each slot accepts any content, but these library components are natural fits:
61
61
  | `#header` | `HeroText` | Heading with accent text, icon, and configurable size — wires `headingId` for `aria-labelledby` |
62
62
  | `#highlights` | `ServicesCard` (×n) | Portrait cards with image, title, description, and CTA slot |
63
63
  | `#highlights` | `LayoutGridByCols` wrapping cards | When you want a responsive column grid rather than a single row of cards |
64
- | `#content` | Any content component | Below the straddle — safe to use `PageRow`, `ServicesSection`, etc. |
64
+ | `#content` | Any content component | Below the straddle — safe to use `PageRow`, `ServiceSummary`, `ServiceDetail`, etc. |
65
65
 
66
66
  Example with `HeroText` in the header slot:
67
67
 
@@ -21,12 +21,11 @@ CTA banner are each their own `PageRow` too, with their own variant props (`body
21
21
  if a particular page wants a section to sit in a different track (e.g.
22
22
  `heroContentVariant="inset-content"` for a narrower hero text column).
23
23
 
24
- It is a different shape from [ServicesSection](services-section.md): `ServicesSection` renders
25
- image-beside-content (used both as a compact summary card in a list and, in full mode, as a
26
- same-page detail block); `ServiceDetail` is meant to *be* the whole page for one service —
27
- `headerTag` defaults to `"h1"` accordingly, and there is no summary/compact mode. There is no
28
- `isSummary` prop and no `v-if` gating of content — everything the component renders is always
29
- rendered.
24
+ It is a different shape from [ServiceSummary](service-summary.md): `ServiceSummary` renders a
25
+ compact image-beside-content preview for a listing page; `ServiceDetail` is meant to *be* the
26
+ whole page for one service — `headerTag` defaults to `"h1"` accordingly, and there is no
27
+ summary/compact mode. There is no `v-if` gating of content — everything the component renders is
28
+ always rendered.
30
29
 
31
30
  ## Props
32
31
 
@@ -63,12 +62,12 @@ type** — neither is universal enough to belong on `~/types/types.services`. Th
63
62
  the whole "You may also like" block simply don't render when they're omitted.
64
63
 
65
64
  `imageLoading`/`imageFetchPriority` default to eager/high because a `ServiceDetail` page normally
66
- has exactly one hero image and it's always the LCP candidate — unlike `ServicesSection`, which is
65
+ has exactly one hero image and it's always the LCP candidate — unlike `ServiceSummary`, which is
67
66
  often looped and needs the `index` prop to lazy-load everything past the first two instances.
68
67
 
69
68
  ### `headerTag` vs `subheadingTag`
70
69
 
71
- Unlike `ServicesSection` (one `headerTag` prop applied to every heading, title included),
70
+ Unlike `ServiceSummary` (one `headerTag` prop applied to its single title heading),
72
71
  `ServiceDetail` splits these: `headerTag` controls only the hero `<h1>` title; `subheadingTag`
73
72
  controls every other heading (What Is It, Process, Ideal For, Aftercare, FAQs, final CTA heading).
74
73
  This keeps the page's heading hierarchy correct when `ServiceDetail` is the page's own `<h1>`.
@@ -101,7 +100,7 @@ Defaults to a plain, non-linked two-item trail built from `serviceData.category`
101
100
  | `final-cta` | `{ serviceData: Service }` | Button in the closing full-width CTA banner |
102
101
 
103
102
  All routing decisions (breadcrumb links, book-cta href, related-service links, final-cta href)
104
- are delegated to the consumer, matching `ServicesSection`'s slot-based routing pattern.
103
+ are delegated to the consumer, matching `ServiceSummary`'s slot-based routing pattern.
105
104
 
106
105
  ## Usage
107
106
 
@@ -155,10 +154,10 @@ pattern, and `CONSUMER-STYLING.md` in the component's own folder for the full to
155
154
  - Component is auto-imported in Nuxt — no import needed.
156
155
  - The `Service` type is imported from `~/types/types.services`; `BreadcrumbItem` from
157
156
  `~/types/components/breadcrumb`.
158
- - No `isSummary` mode and no content gating — this component always renders everything it's
159
- given. If you need a compact summary card for a services listing page, use
160
- [ServicesSection](services-section.md) in summary mode or [ServicesCard](services-card.md)
161
- instead, and reserve `ServiceDetail` for the individual `/services/[slug]` page.
157
+ - No content gating — this component always renders everything it's given. If you need a compact
158
+ summary card for a services listing page, use [ServiceSummary](service-summary.md) (via
159
+ [ServiceSummaryGrid](service-summary-grid.md)) or [ServicesCard](services-card.md) instead, and
160
+ reserve `ServiceDetail` for the individual `/services/[slug]` page.
162
161
  - Uses [Breadcrumb](breadcrumb.md) internally for the hero banner breadcrumb.
163
162
  - The two-/three-column layout breakpoints (body columns, ideal-for grid, final CTA row) are
164
163
  `@container` queries against the component's own width, not the viewport.
@@ -177,5 +176,5 @@ pattern, and `CONSUMER-STYLING.md` in the component's own folder for the full to
177
176
  carry the full-bleed scrim while its child still lands in the configured column.
178
177
  - The section gets `aria-labelledby` automatically when `tag` is `"section"`, `"article"`, or
179
178
  `"aside"`, pointing at the id `ServiceDetail` binds to its own title `HeroText` internally —
180
- same mechanism as `ServicesSection`. `tag="main"` renders a `<main>` element but is never
179
+ same mechanism as `ServiceSummary`. `tag="main"` renders a `<main>` element but is never
181
180
  auto-labelled.
@@ -0,0 +1,141 @@
1
+ # ServiceSummaryGrid Component
2
+
3
+ ## Overview
4
+
5
+ `ServiceSummaryGrid` renders a vertical stack of [ServiceSummary](service-summary.md) components
6
+ from a `Service[]` array. Column order can be alternated on every other summary via
7
+ `useAlternateReverse`.
8
+
9
+ All routing decisions are delegated to the consumer via the `summary-link` scoped slot — the grid
10
+ forwards `serviceData` from each `ServiceSummary` iteration so the consumer can build hrefs and
11
+ labels freely.
12
+
13
+ This is the renamed successor to `ServicesSectionGrid`, following `ServicesSection`'s rename/
14
+ strip to `ServiceSummary` — see [service-summary.md](service-summary.md) for why the old "full
15
+ mode" content went away.
16
+
17
+ ## Props
18
+
19
+ | Prop | Type | Default | Required |
20
+ |------|------|---------|----------|
21
+ | `servicesData` | `Service[]` | — | **yes** |
22
+ | `tag` | `"div" \| "section" \| "main"` | `"div"` | no |
23
+ | `useAlternateReverse` | `boolean` | `false` | no |
24
+ | `alignment` | `"start" \| "center" \| "end"` | `"center"` | no |
25
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
26
+
27
+ ### `useAlternateReverse`
28
+
29
+ When `true`, odd-indexed summaries flip their image/content column order (`reverse` prop on each
30
+ `ServiceSummary`), creating a visual zigzag layout. Commonly used on services landing pages with
31
+ three or more services.
32
+
33
+ ### `alignment`
34
+
35
+ Passed to every `ServiceSummary` as `alignment` — controls vertical alignment of the info column
36
+ content within the grid cell.
37
+
38
+ ## Slots
39
+
40
+ | Slot | Slot props | Purpose |
41
+ |------|-----------|---------|
42
+ | `summary-link` | `{ serviceData: Service }` | Navigation link below the `whatIsIt` text in each summary |
43
+
44
+ The slot is forwarded from the inner `ServiceSummary` and receives the current iteration's
45
+ `serviceData` as a scoped prop. It is optional — omitting it leaves that area empty.
46
+
47
+ ## CSS custom properties
48
+
49
+ | Token | Default | Controls |
50
+ |---|---|---|
51
+ | `--service-summary-grid-row-gap` | `4rem` | Gap between stacked `ServiceSummary` rows |
52
+
53
+ Each inner `ServiceSummary`'s own tokens (`--service-summary-*`) still apply — see
54
+ `CONSUMER-STYLING.md` in the `service-summary` component folder for the full list.
55
+
56
+ ## Basic usage
57
+
58
+ ```vue
59
+ <ServiceSummaryGrid
60
+ :services-data="servicesData ?? []"
61
+ :use-alternate-reverse="true"
62
+ tag="section"
63
+ >
64
+ <template #summary-link="{ serviceData }">
65
+ <LinkText
66
+ :to="`/services/${serviceData.slug}`"
67
+ :link-text="`More about ${serviceData.title}`"
68
+ :style-class-passthrough="['mb-20']"
69
+ />
70
+ </template>
71
+ </ServiceSummaryGrid>
72
+ ```
73
+
74
+ ## Consumer page boilerplate
75
+
76
+ ```vue
77
+ <template>
78
+ <div>
79
+ <NuxtLayout name="default">
80
+ <template #layout-content>
81
+ <PageRow tag="div" variant="content" :style-class-passthrough="['mbe-20']">
82
+ <h1 class="page-heading-1">Our Services</h1>
83
+ </PageRow>
84
+
85
+ <PageRow tag="div" variant="content" :style-class-passthrough="['mbe-20']">
86
+ <ServiceSummaryGrid
87
+ :services-data="servicesData ?? []"
88
+ :use-alternate-reverse="true"
89
+ tag="section"
90
+ >
91
+ <template #summary-link="{ serviceData }">
92
+ <LinkText
93
+ :to="`/services/${serviceData.slug}`"
94
+ :link-text="`More about ${serviceData.title}`"
95
+ :style-class-passthrough="['mb-20']"
96
+ />
97
+ </template>
98
+ </ServiceSummaryGrid>
99
+ </PageRow>
100
+ </template>
101
+ </NuxtLayout>
102
+ </div>
103
+ </template>
104
+
105
+ <script setup lang="ts">
106
+ definePageMeta({ layout: false });
107
+
108
+ useHead({
109
+ title: "Services",
110
+ meta: [{ name: "description", content: "Browse our services" }],
111
+ bodyAttrs: { class: "page-services" },
112
+ });
113
+
114
+ const store = useServicesStore();
115
+ const { servicesData } = storeToRefs(store);
116
+
117
+ if (servicesData.value.length === 0) {
118
+ await store.fetchServicesData();
119
+ }
120
+ </script>
121
+
122
+ <style lang="css">
123
+ .page-services {
124
+ .service-summary-grid {
125
+ --service-summary-grid-row-gap: 6rem;
126
+ }
127
+ }
128
+ </style>
129
+ ```
130
+
131
+ ## Notes
132
+
133
+ - Component is auto-imported in Nuxt — no import needed.
134
+ - The `Service` type is imported from `~/types/types.services`.
135
+ - Each `ServiceSummary` receives its array `index` for eager/lazy image loading decisions (first
136
+ two summaries load eagerly).
137
+ - Pass `servicesData ?? []` as a safe fallback while data loads asynchronously.
138
+ - If a single page needs a per-item entry animation (e.g. wrapping each item in `EntryAnimation`
139
+ for a scroll-in effect), render `ServiceSummary` directly in your own `v-for` loop instead of
140
+ using this grid wrapper — `ServiceSummaryGrid` has no slot for wrapping individual items.
141
+ - See [service-summary.md](service-summary.md) for the full `ServiceSummary` prop and slot API.
@@ -0,0 +1,165 @@
1
+ # ServiceSummary Component
2
+
3
+ ## Overview
4
+
5
+ `ServiceSummary` renders a single service as a compact two-column preview: image on one side,
6
+ eyebrow/title/price-duration pills/`whatIsIt` text on the other, with a `summary-link` slot for
7
+ navigating to the full service page. All routing decisions are delegated to the consumer via the
8
+ slot.
9
+
10
+ This is a renamed, stripped-down successor to `ServicesSection` — `ServicesSection` used to have
11
+ a second "full mode" (`isSummary: false`) with process/ideal-for/maintenance/FAQ/CTA content, but
12
+ that content shape is now [ServiceDetail](service-detail.md)'s job (a dedicated full-page
13
+ component). `ServiceSummary` only ever renders the compact preview; there is no `isSummary` prop
14
+ because there is no other mode.
15
+
16
+ Usually consumed via [ServiceSummaryGrid](service-summary-grid.md) rather than directly.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Default | Required |
21
+ |------|------|---------|----------|
22
+ | `serviceData` | `Service` | — | **yes** |
23
+ | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | no |
24
+ | `headerTag` | `"h1" \| "h2" \| "h3"` | `"h2"` | no |
25
+ | `index` | `number` | `0` | no |
26
+ | `alignment` | `"start" \| "center" \| "end"` | `"center"` | no |
27
+ | `reverse` | `boolean` | `false` | no |
28
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
29
+
30
+ ### `index` and image loading
31
+
32
+ The `index` prop controls both eager/lazy loading and fetch priority. The first two summaries
33
+ (`index` 0 and 1) load eagerly; all others load lazily. Only `index 0` gets `fetchpriority="high"`
34
+ (LCP candidate). Pass the loop index when rendering a list.
35
+
36
+ ### `reverse`
37
+
38
+ Flips the image to the right column and content to the left (CSS `order: 2` on the image
39
+ wrapper).
40
+
41
+ ### `alignment`
42
+
43
+ Vertical alignment of the info column's content within the grid cell — useful when the image is
44
+ taller than the text content and you want the eyebrow/title/pills/link block anchored to the top,
45
+ center, or bottom of that space rather than stretching.
46
+
47
+ ## Slots
48
+
49
+ | Slot | Slot props | Purpose |
50
+ |------|-----------|---------|
51
+ | `summary-link` | `{ serviceData: Service }` | Navigation link below the `whatIsIt` text |
52
+
53
+ ## Duration/price pills
54
+
55
+ Duration and price render as `DisplayPill` instances (`variant="neutral"`, `size="md"`) — the
56
+ same pattern `ServiceDetail`'s hero uses — rather than the icon+text row the old `ServicesSection`
57
+ had. There are no icon-customisation props (`durationIcon`/`priceIcon` are gone along with the
58
+ icon row).
59
+
60
+ ## Row height and image aspect ratio
61
+
62
+ By default `.service-summary` has no fixed height — the row sizes to its content: the image
63
+ sizes itself via `--service-summary-image-aspect-ratio` (default `1 / 1`), and the text column
64
+ sizes to its own content, independently. Set `--service-summary-height-mobile`/`-tablet`/
65
+ `-desktop` (on `.service-summary` itself, via `styleClassPassthrough` or page-scoped CSS — there
66
+ is no prop for this) to force a shared row height instead: both the image (which then crops via
67
+ `object-fit: cover`, and the aspect-ratio token stops having any visible effect) and the text
68
+ column (which then has real leftover space for `alignment` to position within) fill that height.
69
+ See `CONSUMER-STYLING.md` for the full token list and the mobile-stacked-layout caveat.
70
+
71
+ ## Basic usage
72
+
73
+ ```vue
74
+ <ServiceSummary :service-data="service" :index="i">
75
+ <template #summary-link="{ serviceData }">
76
+ <LinkText
77
+ :to="`/services/${serviceData.slug}`"
78
+ :link-text="`More about ${serviceData.title}`"
79
+ :style-class-passthrough="['mb-20']"
80
+ >
81
+ <template #right>
82
+ <Icon name="mdi:arrow-right" />
83
+ </template>
84
+ </LinkText>
85
+ </template>
86
+ </ServiceSummary>
87
+ ```
88
+
89
+ ## Rendering a list
90
+
91
+ Prefer [ServiceSummaryGrid](service-summary-grid.md) for this — it already handles alternating
92
+ `reverse` and index-based image loading. Rendering the loop by hand looks like:
93
+
94
+ ```vue
95
+ <ServiceSummary
96
+ v-for="(service, i) in services"
97
+ :key="service.slug"
98
+ :service-data="service"
99
+ :index="i"
100
+ :reverse="i % 2 !== 0"
101
+ tag="section"
102
+ >
103
+ <template #summary-link="{ serviceData }">
104
+ <LinkText :to="`/services/${serviceData.slug}`" :link-text="`More about ${serviceData.title}`" />
105
+ </template>
106
+ </ServiceSummary>
107
+ ```
108
+
109
+ ## Local style override scaffold
110
+
111
+ When consuming this component, prefer the `--service-summary-*` CSS custom properties documented
112
+ in `CONSUMER-STYLING.md` (in the component's own folder) over raw class overrides. For anything
113
+ the tokens don't cover, scaffold a style block using `styleClassPassthrough`. Delete the block if
114
+ unused.
115
+
116
+ See [component-local-style-override.md](../component-local-style-override.md) for the general
117
+ pattern.
118
+
119
+ ```vue
120
+ <ServiceSummary :style-class-passthrough="['my-summary']" :service-data="service">
121
+ ...
122
+ </ServiceSummary>
123
+
124
+ <style>
125
+ /* ─── ServiceSummary local overrides ──────────────────────────────
126
+ Colours, borders, geometry only — do not override behaviour.
127
+ Delete this block if no overrides are needed.
128
+ ─────────────────────────────────────────────────────────────────── */
129
+ .service-summary {
130
+ &.my-summary {
131
+ --service-summary-image-border-radius: 1.2rem;
132
+ --service-summary-height-tablet: 32rem;
133
+ --service-summary-height-desktop: 40rem;
134
+ --service-summary-image-aspect-ratio: 1 / 1;
135
+ --service-summary-image-padding-block-mobile: 1.2rem;
136
+ --service-summary-image-padding-inline-mobile: 1.2rem;
137
+
138
+ /* Deeper overrides target the BEM element classes directly, e.g.: */
139
+ /* .service-summary__pills { } */
140
+ }
141
+ }
142
+ </style>
143
+ ```
144
+
145
+ ## Notes
146
+
147
+ - Component is auto-imported in Nuxt — no import needed.
148
+ - The `Service` type is imported from `~/types/types.services`.
149
+ - Internal element classes are BEM-namespaced: `service-summary__grid`,
150
+ `service-summary__image-wrapper`, `service-summary__image`, `service-summary__info-wrapper`,
151
+ `service-summary__pills`.
152
+ - The section gets `aria-labelledby` automatically when `tag` is `"section"`, `"article"`, or
153
+ `"aside"`, pointing at the id `ServiceSummary` binds to its own title `HeroText` internally — no
154
+ consumer action needed. (`tag="main"` renders a `<main>` element but is never auto-labelled; see
155
+ [component-aria-landmark.md](../component-aria-landmark.md).)
156
+ - For a full single-service page (process, ideal-for, aftercare, FAQs, booking CTA, related
157
+ services), use [ServiceDetail](service-detail.md) instead — that's the component this one used
158
+ to duplicate in its old "full mode".
159
+ - The two-column breakpoint, the row-height tokens (`--service-summary-height-*`), and the image
160
+ padding tokens (`--service-summary-image-padding-block-*`/`-inline-*`, each `-mobile`/`-tablet`/
161
+ `-desktop`) are all **`@container` queries** against `.service-summary`'s own inline size, not
162
+ `@media` viewport queries — this keeps the layout correct when the component sits next to a
163
+ persistent side nav that shrinks its available width without shrinking the viewport. See
164
+ `CONSUMER-STYLING.md` for the token defaults and fallback
165
+ chain.
@@ -23,7 +23,7 @@ The layer's `modules/icon-sets.ts` runs at dev/build time and logs an info messa
23
23
  | `gravity-ui` | `@iconify-json/gravity-ui` | NavigationItems (burger/ellipsis overflow) |
24
24
  | `ic` | `@iconify-json/ic` | CarouselBasic, CarouselFlip, CarouselInfinite, SliderGallery, CanvasSwitcher |
25
25
  | `lucide` | `@iconify-json/lucide` | ColourFinder, TreatmentConsultant |
26
- | `material-symbols` | `@iconify-json/material-symbols` | ServicesSection, form components |
26
+ | `material-symbols` | `@iconify-json/material-symbols` | form components |
27
27
  | `mdi` | `@iconify-json/mdi` | NavigationHorizontal, form components, ServicesCard |
28
28
  | `radix-icons` | `@iconify-json/radix-icons` | InputPasswordWithLabel, InputError, DisplayThemeSwitch |
29
29
 
@@ -69,8 +69,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
69
69
  ├── page-hero-highlights.md — PageHeroHighlights template: hero + highlights strip grid, CSS custom property theming
70
70
  ├── services-card.md — ServicesCard props (incl. eyebrowConfig/heroConfig), actions slot, CSS tokens, page boilerplate
71
71
  ├── services-card-grid.md — ServicesCardGrid props, config pass-through, CSS tokens, full page boilerplate
72
- ├── services-section-grid.md — ServicesSectionGrid props, useAlternateReverse zigzag layout, page boilerplate
73
- ├── services-section.md — ServicesSection props (incl. heading/CTA copy), summary-link/cta/cta-panel slots, summary vs full mode
72
+ ├── service-summary-grid.md — ServiceSummaryGrid props, useAlternateReverse zigzag layout, page boilerplate
73
+ ├── service-summary.md — ServiceSummary props, summary-link slot, DisplayPill duration/price (renamed/stripped from ServicesSection — full mode is now ServiceDetail)
74
74
  ├── service-detail.md — ServiceDetail: full service detail page (hero banner, sticky sidebar booking card + related services, closing CTA), headerTag vs subheadingTag split, book-cta/sidebar-note/related-service/final-cta slots
75
75
  ├── breadcrumb.md — Breadcrumb: items (BreadcrumbItem[]) trail, link vs current-page text, CSS token API
76
76
  ├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
@@ -0,0 +1,67 @@
1
+ {
2
+ "SRCDEV ServiceSummary": {
3
+ "description": "ServiceSummary — compact preview with summary-link slot",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ServiceSummary",
7
+ " :service-data=\"$1service\"",
8
+ " :index=\"$2index\"",
9
+ " alignment=\"$3center\"",
10
+ ">",
11
+ " <template #summary-link=\"{ serviceData }\">",
12
+ " <NuxtLink :to=\"`/services/${serviceData.slug}`\">$4More about {{ serviceData.title }} →</NuxtLink>",
13
+ " </template>",
14
+ "</ServiceSummary>"
15
+ ]
16
+ },
17
+ "SRCDEV ServiceSummary Reversed": {
18
+ "description": "ServiceSummary with image/content columns swapped",
19
+ "scope": "vue,html",
20
+ "body": [
21
+ "<ServiceSummary :service-data=\"$1service\" :reverse=\"$2true\">",
22
+ " <template #summary-link=\"{ serviceData }\">",
23
+ " <NuxtLink :to=\"`/services/${serviceData.slug}`\">$3More about {{ serviceData.title }} →</NuxtLink>",
24
+ " </template>",
25
+ "</ServiceSummary>"
26
+ ]
27
+ },
28
+ "SRCDEV ServiceSummaryGrid": {
29
+ "description": "ServiceSummaryGrid — alternating zigzag listing from a Service[] array",
30
+ "scope": "vue,html",
31
+ "body": [
32
+ "<ServiceSummaryGrid",
33
+ " :services-data=\"$1servicesData ?? []\"",
34
+ " :use-alternate-reverse=\"$2true\"",
35
+ ">",
36
+ " <template #summary-link=\"{ serviceData }\">",
37
+ " <NuxtLink :to=\"`/services/${serviceData.slug}`\">$3More about {{ serviceData.title }} →</NuxtLink>",
38
+ " </template>",
39
+ "</ServiceSummaryGrid>"
40
+ ]
41
+ },
42
+ "SRCDEV ServiceSummary CSS Override": {
43
+ "description": "CSS override scaffold for ServiceSummary — scope to a page or section class",
44
+ "scope": "css",
45
+ "body": [
46
+ ".$1my-page {",
47
+ " .service-summary {",
48
+ " --service-summary-grid-gap: ;",
49
+ " --service-summary-grid-gap-desktop: ;",
50
+ " --service-summary-image-border-radius: ;",
51
+ " --service-summary-height-mobile: ;",
52
+ " --service-summary-height-tablet: ;",
53
+ " --service-summary-height-desktop: ;",
54
+ " --service-summary-image-aspect-ratio: ;",
55
+ " --service-summary-image-padding-block-mobile: ;",
56
+ " --service-summary-image-padding-inline-mobile: ;",
57
+ " --service-summary-pills-gap: ;",
58
+ " --service-summary-pill-border-colour: ;",
59
+ "",
60
+ " .service-summary__image-wrapper {}",
61
+ " .service-summary__info-wrapper {}",
62
+ " .service-summary__pills {}",
63
+ " }",
64
+ "}"
65
+ ]
66
+ }
67
+ }
@@ -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
- [services-section.md](../../../../../.claude/skills/components/services-section.md)-style nesting
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
 
@@ -37,7 +37,7 @@ compose.
37
37
  | Token | Default | Controls |
38
38
  |---|---|---|
39
39
  | `--service-detail-hero-border-radius` | `0.8rem` | Corner rounding on the hero (clips the image/content layers — set to `0` for a true edge-to-edge bleed hero) |
40
- | `--service-detail-hero-content-align` | `end` | `align-content` of the content overlay grid — where the breadcrumb/title/pills sit within the hero's block axis (`end` = bottom-anchored) |
40
+ | `--service-detail-hero-content-align` | `end` | `align-content` on `.service-detail__hero-content` (the breadcrumb/title/pills wrapper) — where that content sits within the hero's block axis (`end` = bottom-anchored) |
41
41
  | `--service-detail-hero-min-height-mobile` | `32rem` | Hero banner height below the `768px` container breakpoint. The hero image always fills this height (`object-fit`/`object-position` control cropping) |
42
42
  | `--service-detail-hero-min-height-tablet` | `36rem` | Hero banner height from the `768px` container breakpoint |
43
43
  | `--service-detail-hero-min-height-desktop` | `42rem` | Hero banner height from the `1024px` container breakpoint |
@@ -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's crop behaviour also changes responsively (position only, not tokened as fit
53
- itself): below the `768px` container breakpoint it's `object-fit: cover` anchored with
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
- `object-fit: contain` anchored with `--service-detail-hero-image-position-medium` (default
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 `ServicesSection`'s `cta`/`summary-link` slots. Only `sidebar-note` has no scoped
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 `ServicesSection`, which uses one `headerTag` prop for all of them. This
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 `ServicesSection`'s `--services-section-grid-gap-desktop`).
233
+ just spacing (same rationale as `ServiceSummary`'s `--service-summary-grid-gap-desktop`).
@@ -294,7 +294,6 @@ watch(
294
294
  object-position: var(--service-detail-hero-image-position-small, bottom);
295
295
 
296
296
  @container (width >= 768px) {
297
- object-fit: contain;
298
297
  object-position: var(--service-detail-hero-image-position-medium, center);
299
298
  }
300
299
  }
@@ -302,7 +301,6 @@ watch(
302
301
  .service-detail__hero-overlay {
303
302
  display: grid;
304
303
  height: 100%;
305
- align-content: var(--service-detail-hero-content-align, end);
306
304
  padding-block: var(--service-detail-hero-padding, 3.2rem);
307
305
  background: var(
308
306
  --service-detail-hero-scrim,
@@ -310,6 +308,9 @@ watch(
310
308
  );
311
309
 
312
310
  .service-detail__hero-content {
311
+ display: grid;
312
+ align-content: var(--service-detail-hero-content-align, end);
313
+
313
314
  .service-detail__breadcrumb {
314
315
  --breadcrumb-colour: var(--service-detail-hero-text-colour, white);
315
316
  margin-block-end: var(--service-detail-breadcrumb-margin-block-end, 0.8rem);
@@ -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 ServicesSection.",
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 ServicesSection's summary-link slot.",
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
  },