srcdev-nuxt-components 9.3.11 → 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.
- 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 +2 -2
- package/.vscode/srcdev-component-service-summary.code-snippets +67 -0
- 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/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
|
@@ -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
|
|
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. `
|
|
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
|
-
- `
|
|
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
|
-
`
|
|
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`, `
|
|
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 [
|
|
25
|
-
image-beside-content
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
|
159
|
-
|
|
160
|
-
[
|
|
161
|
-
|
|
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 `
|
|
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` |
|
|
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
|
|
package/.claude/skills/index.md
CHANGED
|
@@ -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
|
-
├──
|
|
73
|
-
├──
|
|
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
|
-
[
|
|
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
|
},
|