srcdev-nuxt-components 9.0.7 → 9.0.8
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/components/services-card.md +61 -0
- package/.claude/skills/components/services-section.md +114 -0
- package/.claude/skills/index.md +3 -1
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +3 -11
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +14 -1
- package/app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue +21 -1
- package/app/components/03.organisms/services/services-section/ServicesSection.vue +3 -10
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# ServicesCard Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`ServicesCard` renders a single service as a portrait card: image, subtitle (eyebrow), title, short description, and an `actions` slot for any CTA content. The component owns the layout and data display; all routing and button decisions are delegated to the consumer via the slot.
|
|
6
|
+
|
|
7
|
+
## Props
|
|
8
|
+
|
|
9
|
+
| Prop | Type | Default | Required |
|
|
10
|
+
| ----------------------- | --------------------------------- | ------- | -------- |
|
|
11
|
+
| `serviceData` | `Service` | — | **yes** |
|
|
12
|
+
| `tag` | `"div" \| "section" \| "article"` | `"div"` | no |
|
|
13
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | no |
|
|
14
|
+
|
|
15
|
+
## Slots
|
|
16
|
+
|
|
17
|
+
| Slot | Slot props | Purpose |
|
|
18
|
+
| --------- | -------------------------- | ---------------------------------------------------------------------- |
|
|
19
|
+
| `actions` | `{ serviceData: Service }` | CTA area below the description — buttons, links, or any action content |
|
|
20
|
+
|
|
21
|
+
The `actions` slot receives `serviceData` as a scoped prop so the consumer can construct routes or labels from the service data without additional props.
|
|
22
|
+
|
|
23
|
+
## Basic usage
|
|
24
|
+
|
|
25
|
+
```vue
|
|
26
|
+
<ServicesCard :service-data="service">
|
|
27
|
+
<template #actions="{ serviceData }">
|
|
28
|
+
<InputButtonCore
|
|
29
|
+
variant="secondary"
|
|
30
|
+
:button-text="`More about ${serviceData.title}`"
|
|
31
|
+
:href="`/services/${serviceData.slug}`"
|
|
32
|
+
>
|
|
33
|
+
<template #right>
|
|
34
|
+
<Icon name="mdi:arrow-right" class="icon" />
|
|
35
|
+
</template>
|
|
36
|
+
</InputButtonCore>
|
|
37
|
+
</template>
|
|
38
|
+
</ServicesCard>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Multiple actions
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<ServicesCard :service-data="service">
|
|
45
|
+
<template #actions="{ serviceData }">
|
|
46
|
+
<InputButtonCore
|
|
47
|
+
variant="secondary"
|
|
48
|
+
:button-text="`More about ${serviceData.title}`"
|
|
49
|
+
:href="`/services/${serviceData.slug}`"
|
|
50
|
+
/>
|
|
51
|
+
<NuxtLink :to="`/services/${serviceData.slug}/contact`">Get in touch</NuxtLink>
|
|
52
|
+
</template>
|
|
53
|
+
</ServicesCard>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Notes
|
|
57
|
+
|
|
58
|
+
- Component is auto-imported in Nuxt — no import needed.
|
|
59
|
+
- The `Service` type is imported from `~/types/types.services`.
|
|
60
|
+
- Grid rows are sized `auto 2ch auto 5lh auto` — the last row (`auto`) accommodates the `actions` slot at any height.
|
|
61
|
+
- Image has a `3/4` aspect ratio with a subtle scale-on-hover effect.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# ServicesSection Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`ServicesSection` renders a single service as a full two-column section: image on one side, detailed content on the other. It has two modes controlled by the `isSummary` prop:
|
|
6
|
+
|
|
7
|
+
- **Summary mode** (`isSummary: true`) — compact view: eyebrow, title, price/duration, `whatIsIt` text, and a `summary-link` slot for a navigational link.
|
|
8
|
+
- **Full mode** (`isSummary: false`, default) — complete view: all summary content plus long description, hero heading, process stepper, ideal-for stepper, aftercare, FAQs, and a `GlassPanel` with a `cta` slot for a booking/contact button.
|
|
9
|
+
|
|
10
|
+
All routing and CTA decisions are delegated to the consumer via slots.
|
|
11
|
+
|
|
12
|
+
## Props
|
|
13
|
+
|
|
14
|
+
| Prop | Type | Default | Required |
|
|
15
|
+
|------|------|---------|----------|
|
|
16
|
+
| `serviceData` | `Service` | — | **yes** |
|
|
17
|
+
| `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | no |
|
|
18
|
+
| `index` | `number` | `0` | no |
|
|
19
|
+
| `isSummary` | `boolean` | `false` | no |
|
|
20
|
+
| `summaryAlignment` | `"start" \| "center" \| "end"` | `"center"` | no |
|
|
21
|
+
| `reverse` | `boolean` | `false` | no |
|
|
22
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | no |
|
|
23
|
+
|
|
24
|
+
### `index` and image loading
|
|
25
|
+
|
|
26
|
+
The `index` prop controls eager vs lazy image loading. The first two sections (`index` 0 and 1) load eagerly; all others load lazily. Pass the loop index when rendering a list of sections.
|
|
27
|
+
|
|
28
|
+
### `reverse`
|
|
29
|
+
|
|
30
|
+
Flips the image to the right column and content to the left (CSS `order: 2` on the image wrapper).
|
|
31
|
+
|
|
32
|
+
## Slots
|
|
33
|
+
|
|
34
|
+
| Slot | Slot props | Rendered when | Purpose |
|
|
35
|
+
|------|-----------|---------------|---------|
|
|
36
|
+
| `summary-link` | `{ serviceData: Service }` | `isSummary` is `true` | Navigation link below the `whatIsIt` text in summary mode |
|
|
37
|
+
| `cta` | `{ serviceData: Service }` | `isSummary` is `false` | CTA button/link inside the closing `GlassPanel` in full mode |
|
|
38
|
+
|
|
39
|
+
Both slots receive `serviceData` as a scoped prop.
|
|
40
|
+
|
|
41
|
+
## Summary mode usage
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<ServicesSection :service-data="service" :is-summary="true" :index="i">
|
|
45
|
+
<template #summary-link="{ serviceData }">
|
|
46
|
+
<LinkText
|
|
47
|
+
:to="`/services/${serviceData.slug}`"
|
|
48
|
+
:link-text="`More about ${serviceData.title}`"
|
|
49
|
+
:style-class-passthrough="['mb-20']"
|
|
50
|
+
>
|
|
51
|
+
<template #right>
|
|
52
|
+
<Icon name="mdi:arrow-right" />
|
|
53
|
+
</template>
|
|
54
|
+
</LinkText>
|
|
55
|
+
</template>
|
|
56
|
+
</ServicesSection>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Full mode usage
|
|
60
|
+
|
|
61
|
+
```vue
|
|
62
|
+
<ServicesSection :service-data="service">
|
|
63
|
+
<template #cta>
|
|
64
|
+
<InputButtonCore
|
|
65
|
+
variant="secondary"
|
|
66
|
+
button-text="Get in touch"
|
|
67
|
+
href="/contact"
|
|
68
|
+
:style-class-passthrough="['mbs-24']"
|
|
69
|
+
>
|
|
70
|
+
<template #right>
|
|
71
|
+
<Icon name="mdi:arrow-right" class="icon" />
|
|
72
|
+
</template>
|
|
73
|
+
</InputButtonCore>
|
|
74
|
+
</template>
|
|
75
|
+
</ServicesSection>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Multiple CTAs in full mode
|
|
79
|
+
|
|
80
|
+
```vue
|
|
81
|
+
<ServicesSection :service-data="service">
|
|
82
|
+
<template #cta="{ serviceData }">
|
|
83
|
+
<InputButtonCore variant="primary" button-text="Book now" href="/book" />
|
|
84
|
+
<InputButtonCore variant="secondary" button-text="Get in touch" href="/contact" />
|
|
85
|
+
</template>
|
|
86
|
+
</ServicesSection>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Rendering a list (summary mode)
|
|
90
|
+
|
|
91
|
+
```vue
|
|
92
|
+
<ServicesSection
|
|
93
|
+
v-for="(service, i) in services"
|
|
94
|
+
:key="service.slug"
|
|
95
|
+
:service-data="service"
|
|
96
|
+
:index="i"
|
|
97
|
+
:is-summary="true"
|
|
98
|
+
:reverse="i % 2 !== 0"
|
|
99
|
+
tag="section"
|
|
100
|
+
>
|
|
101
|
+
<template #summary-link="{ serviceData }">
|
|
102
|
+
<LinkText :to="`/services/${serviceData.slug}`" :link-text="`More about ${serviceData.title}`" />
|
|
103
|
+
</template>
|
|
104
|
+
</ServicesSection>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Notes
|
|
108
|
+
|
|
109
|
+
- Component is auto-imported in Nuxt — no import needed.
|
|
110
|
+
- The `Service` type is imported from `~/types/types.services`.
|
|
111
|
+
- `summary-link` slot is guarded by `v-if="isSummary"` — it will not render in full mode even if provided.
|
|
112
|
+
- `cta` slot lives inside a `v-if="!isSummary"` `GlassPanel` — it will not render in summary mode.
|
|
113
|
+
- The section gets `aria-labelledby` automatically when `tag` is `"section"` or `"article"`, pointing to the internal heading id.
|
|
114
|
+
- `summaryAlignment` only has effect when `isSummary` is `true` — it aligns the info-wrapper content vertically within the grid cell.
|
package/.claude/skills/index.md
CHANGED
|
@@ -28,7 +28,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
28
28
|
├── eyebrow-text.md — EyebrowText props, usage patterns, styling
|
|
29
29
|
├── hero-text.md — HeroText props, usage patterns, styling
|
|
30
30
|
├── layout-row.md — LayoutRow variant guide, width/margin decisions, usage patterns
|
|
31
|
-
|
|
31
|
+
├── link-text.md — LinkText props, slots, usage patterns, styling
|
|
32
|
+
├── services-card.md — ServicesCard props, actions slot, usage patterns
|
|
33
|
+
└── services-section.md — ServicesSection props, summary-link/cta slots, summary vs full mode
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
## Skill file template
|
|
@@ -17,15 +17,7 @@
|
|
|
17
17
|
<div class="description">
|
|
18
18
|
{{ serviceData.shortDescription }}
|
|
19
19
|
</div>
|
|
20
|
-
<
|
|
21
|
-
variant="secondary"
|
|
22
|
-
:button-text="`More about ${serviceData.title}`"
|
|
23
|
-
:href="`/ui/services/services-section/${serviceData.slug}`"
|
|
24
|
-
>
|
|
25
|
-
<template #right>
|
|
26
|
-
<Icon name="mdi:arrow-right" class="icon" />
|
|
27
|
-
</template>
|
|
28
|
-
</InputButtonCore>
|
|
20
|
+
<slot name="actions" :service-data="serviceData"></slot>
|
|
29
21
|
</component>
|
|
30
22
|
</template>
|
|
31
23
|
|
|
@@ -33,7 +25,7 @@
|
|
|
33
25
|
import type { Service } from "~/types/types.services";
|
|
34
26
|
|
|
35
27
|
interface Props {
|
|
36
|
-
tag?: "div" | "section" | "article"
|
|
28
|
+
tag?: "div" | "section" | "article";
|
|
37
29
|
serviceData: Service;
|
|
38
30
|
styleClassPassthrough?: string | string[];
|
|
39
31
|
}
|
|
@@ -60,7 +52,7 @@ watch(
|
|
|
60
52
|
@layer components {
|
|
61
53
|
.services-card {
|
|
62
54
|
display: grid;
|
|
63
|
-
grid-template-rows: auto 2ch auto 5lh
|
|
55
|
+
grid-template-rows: auto 2ch auto 5lh auto;
|
|
64
56
|
gap: 1rem;
|
|
65
57
|
|
|
66
58
|
.image-wrapper {
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component :is="tag" class="services-card-grid" :class="[elementClasses]">
|
|
3
|
-
<ServicesCard v-for="(item, index) in servicesData" :key="index" :service-data="item"
|
|
3
|
+
<ServicesCard v-for="(item, index) in servicesData" :key="index" :service-data="item">
|
|
4
|
+
<template #actions="{ serviceData }">
|
|
5
|
+
<InputButtonCore
|
|
6
|
+
variant="secondary"
|
|
7
|
+
:button-text="`Enquire about ${serviceData.title}`"
|
|
8
|
+
:href="`/services/${serviceData.slug}`"
|
|
9
|
+
:style-class-passthrough="['mbs-24']"
|
|
10
|
+
>
|
|
11
|
+
<template #right>
|
|
12
|
+
<Icon name="mdi:arrow-right" class="icon" />
|
|
13
|
+
</template>
|
|
14
|
+
</InputButtonCore>
|
|
15
|
+
</template>
|
|
16
|
+
</ServicesCard>
|
|
4
17
|
</component>
|
|
5
18
|
</template>
|
|
6
19
|
|
|
@@ -8,7 +8,27 @@
|
|
|
8
8
|
:is-summary="true"
|
|
9
9
|
:reverse="props.useAlternateReverse ? index % 2 !== 0 : false"
|
|
10
10
|
:summary-alignment="summaryAlignment"
|
|
11
|
-
|
|
11
|
+
>
|
|
12
|
+
<template #summary-link="{ serviceData }">
|
|
13
|
+
<LinkText
|
|
14
|
+
:to="`/services/${serviceData.slug}`"
|
|
15
|
+
:link-text="`More about ${serviceData.title}`"
|
|
16
|
+
:style-class-passthrough="['mb-20']"
|
|
17
|
+
/>
|
|
18
|
+
</template>
|
|
19
|
+
<template #cta="{ serviceData }">
|
|
20
|
+
<InputButtonCore
|
|
21
|
+
variant="secondary"
|
|
22
|
+
:button-text="`Enquire about ${serviceData.title}`"
|
|
23
|
+
href="#"
|
|
24
|
+
:style-class-passthrough="['mbs-24']"
|
|
25
|
+
>
|
|
26
|
+
<template #right>
|
|
27
|
+
<Icon name="mdi:arrow-right" class="icon" />
|
|
28
|
+
</template>
|
|
29
|
+
</InputButtonCore>
|
|
30
|
+
</template>
|
|
31
|
+
</ServicesSection>
|
|
12
32
|
</component>
|
|
13
33
|
</template>
|
|
14
34
|
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
{{ serviceData.whatIsIt }}
|
|
49
49
|
</p>
|
|
50
50
|
|
|
51
|
+
<slot v-if="isSummary" name="summary-link" :service-data="serviceData"></slot>
|
|
52
|
+
|
|
51
53
|
<HeroText
|
|
52
54
|
v-if="!isSummary"
|
|
53
55
|
tag="h2"
|
|
@@ -137,16 +139,7 @@
|
|
|
137
139
|
:style-class-passthrough="['mbs-0', 'mbe-20']"
|
|
138
140
|
/>
|
|
139
141
|
<p class="page-body-normal">Mobile service across Bath — I come to you.</p>
|
|
140
|
-
<
|
|
141
|
-
variant="secondary"
|
|
142
|
-
button-text="Get in touch"
|
|
143
|
-
href="#"
|
|
144
|
-
:style-class-passthrough="['mbs-24']"
|
|
145
|
-
>
|
|
146
|
-
<template #right>
|
|
147
|
-
<Icon name="mdi:arrow-right" class="icon" />
|
|
148
|
-
</template>
|
|
149
|
-
</InputButtonCore>
|
|
142
|
+
<slot name="cta" :service-data="serviceData"></slot>
|
|
150
143
|
</GlassPanel>
|
|
151
144
|
</div>
|
|
152
145
|
</div>
|