srcdev-nuxt-components 9.0.7 → 9.0.9

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.
@@ -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.
@@ -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
- └── link-text.md — LinkText props, slots, usage patterns, styling
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
@@ -1,13 +1,17 @@
1
1
  /* @link https://utopia.fyi/clamp/calculator?a=360,1240,1—2|2—4|4—8|6—12|8—16|10—20|12—24|16—32|20—40 */
2
2
 
3
3
  :where(html) {
4
- --fluid-1-2: clamp(0.0625rem, 0.0369rem + 0.1136vw, 0.125rem);
5
- --fluid-2-4: clamp(0.125rem, 0.0739rem + 0.2273vw, 0.25rem);
6
- --fluid-4-8: clamp(0.25rem, 0.1477rem + 0.4545vw, 0.5rem);
7
- --fluid-6-12: clamp(0.375rem, 0.2216rem + 0.6818vw, 0.75rem);
8
- --fluid-8-16: clamp(0.5rem, 0.2955rem + 0.9091vw, 1rem);
9
- --fluid-10-20: clamp(0.625rem, 0.3693rem + 1.1364vw, 1.25rem);
10
- --fluid-12-24: clamp(0.75rem, 0.4432rem + 1.3636vw, 1.5rem);
11
- --fluid-16-32: clamp(1rem, 0.5909rem + 1.8182vw, 2rem);
12
- --fluid-20-40: clamp(1.25rem, 0.7386rem + 2.2727vw, 2.5rem);
4
+ --fluid-space-1-2: clamp(0.0625rem, 0.0369rem + 0.1136vw, 0.125rem);
5
+ --fluid-space-2-4: clamp(0.125rem, 0.0739rem + 0.2273vw, 0.25rem);
6
+ --fluid-space-4-8: clamp(0.25rem, 0.1477rem + 0.4545vw, 0.5rem);
7
+ --fluid-space-6-12: clamp(0.375rem, 0.2216rem + 0.6818vw, 0.75rem);
8
+ --fluid-space-8-16: clamp(0.5rem, 0.2955rem + 0.9091vw, 1rem);
9
+ --fluid-space-10-20: clamp(0.625rem, 0.3693rem + 1.1364vw, 1.25rem);
10
+ --fluid-space-12-24: clamp(0.75rem, 0.4432rem + 1.3636vw, 1.5rem);
11
+ --fluid-space-16-32: clamp(1rem, 0.5909rem + 1.8182vw, 2rem);
12
+ --fluid-space-20-40: clamp(1.25rem, 0.7386rem + 2.2727vw, 2.5rem);
13
+ --fluid-space-24-48: clamp(1.5rem, 0.8863rem + 2.7273vw, 3rem);
14
+ --fluid-space-32-64: clamp(2rem, 1.1818rem + 3.6364vw, 4rem);
15
+ --fluid-space-40-80: clamp(2.5rem, 1.4773rem + 4.5455vw, 5rem);
16
+ --fluid-space-48-96: clamp(3rem, 1.7727rem + 5.4545vw, 6rem);
13
17
  }
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <component :is="tag" class="text-block" :class="[elementClasses]">
3
+ <slot name="default"></slot>
4
+ </component>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ interface Props {
9
+ tag?: "div" | "section" | "article" | "main";
10
+ styleClassPassthrough?: string | string[];
11
+ }
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ tag: "div",
14
+ styleClassPassthrough: () => [],
15
+ });
16
+
17
+ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
18
+ </script>
19
+
20
+ <style lang="css">
21
+ @layer components {
22
+ .text-block {
23
+ padding-block-start: var(--fluid-40-80);
24
+ padding-block-end: var(--fluid-40-80);
25
+ }
26
+ }
27
+ </style>
@@ -17,15 +17,7 @@
17
17
  <div class="description">
18
18
  {{ serviceData.shortDescription }}
19
19
  </div>
20
- <InputButtonCore
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" | "main" | "header" | "footer";
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 4.4rem;
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
- <InputButtonCore
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>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.0.7",
4
+ "version": "9.0.9",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",