srcdev-nuxt-components 9.3.11 → 9.3.13
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 +3 -2
- package/.claude/skills/theming-component-token-pattern.md +126 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +67 -0
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +10 -1
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/input-copy/InputCopy.vue +9 -3
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/pricing-card/PricingCard.vue +6 -3
- 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/app/components/05.forms/input-button/CONSUMER-STYLING.md +117 -0
- package/app/components/05.forms/input-button/InputButtonCore.vue +83 -37
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/playwright/input-button-core.playwright.ts-snapshots/with-both-nuxt-icon-components-chromium-darwin.png +0 -0
- package/app/components/05.forms/input-button/stories/InputButtonCore.stories.ts +35 -0
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +68 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +19 -9
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioCore.vue +8 -5
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +93 -0
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioCore.stories.ts +75 -0
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +41 -0
- package/app/components/05.forms/input-number/InputNumberCore.vue +12 -6
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +35 -29
- package/app/components/05.forms/input-select/InputSelectCore.vue +32 -20
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +69 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +38 -48
- package/app/components/05.forms/input-text/InputTextCore.vue +35 -18
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +184 -0
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +125 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +49 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +23 -16
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +72 -0
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +42 -20
- package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitchCore.stories.ts +63 -0
- package/app/components/display-theme-switch/DisplayThemeSwitch.vue +13 -6
- 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
|
@@ -32,6 +32,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
32
32
|
├── theming-override-default.md — replace the entire default theme with a custom palette (set --theme-hue/--theme-chroma)
|
|
33
33
|
├── theming-partial-override.md — override a specific colour token category (palette, buttons, inputs) without a full theme replacement
|
|
34
34
|
├── theming-form-geometry-tokens.md — non-colour form/button/input tokens (padding, border-radius, gaps): full inventory, why partial override doesn't need duplicating the source files
|
|
35
|
+
├── theming-component-token-pattern.md — standard shape for overridable component colours: public token + inline fallback, no bare var()/no private-only indirection; the three tiers (theme slots / cross-component families / component-scoped); rollout status
|
|
35
36
|
├── colour-scheme-disable.md — disable light/dark scheme support in a consumer app
|
|
36
37
|
├── component-dynamic-slots.md — named dynamic slots ($slots iteration) vs indexed dynamic slots (itemCount pattern)
|
|
37
38
|
├── component-local-style-override.md — styleClassPassthrough + scoped style block for per-usage visual customisation
|
|
@@ -69,8 +70,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
69
70
|
├── page-hero-highlights.md — PageHeroHighlights template: hero + highlights strip grid, CSS custom property theming
|
|
70
71
|
├── services-card.md — ServicesCard props (incl. eyebrowConfig/heroConfig), actions slot, CSS tokens, page boilerplate
|
|
71
72
|
├── services-card-grid.md — ServicesCardGrid props, config pass-through, CSS tokens, full page boilerplate
|
|
72
|
-
├──
|
|
73
|
-
├──
|
|
73
|
+
├── service-summary-grid.md — ServiceSummaryGrid props, useAlternateReverse zigzag layout, page boilerplate
|
|
74
|
+
├── service-summary.md — ServiceSummary props, summary-link slot, DisplayPill duration/price (renamed/stripped from ServicesSection — full mode is now ServiceDetail)
|
|
74
75
|
├── 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
76
|
├── breadcrumb.md — Breadcrumb: items (BreadcrumbItem[]) trail, link vs current-page text, CSS token API
|
|
76
77
|
├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Component Token Pattern — Public Token + Inline Fallback
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The standard shape for every overridable CSS value in a component, from now on:
|
|
6
|
+
|
|
7
|
+
```css
|
|
8
|
+
background-color: var(--{component}-{property}, {real-default-or-shared-fallback});
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
One name, declared inline at the point of use, with a real default right there in the component
|
|
12
|
+
source. No separate "public token must be declared somewhere else or the component breaks"
|
|
13
|
+
requirement, and no private `--_` indirection standing in for the actual override point.
|
|
14
|
+
`ServicesCard.vue` is the reference implementation — read its `<style>` block for the shape in
|
|
15
|
+
practice.
|
|
16
|
+
|
|
17
|
+
## Why this replaced the old pattern
|
|
18
|
+
|
|
19
|
+
Two things used to go wrong, both found while auditing `luxury-locs-by-natasha`'s theming
|
|
20
|
+
overrides (2026-08-25):
|
|
21
|
+
|
|
22
|
+
1. **Bare `var(--theme-checkbox-symbol-color)` with no fallback anywhere.** The only way for this
|
|
23
|
+
to resolve to something was a consuming app declaring it globally. When the component was later
|
|
24
|
+
refactored (buttons moved onto the shared theme-slot system) and stopped reading the token, nothing
|
|
25
|
+
signalled that to the consumer — the app's declaration became permanently dead, silently. A
|
|
26
|
+
`var()` with an undefined custom property and no fallback just resolves to the property's
|
|
27
|
+
initial value; there is no error, no warning, ever.
|
|
28
|
+
2. **Tokens that were "shared" only by accident.** `InputTextCore`, `InputSelectCore`,
|
|
29
|
+
`InputNumberCore` and the checkbox button all read the exact same `--theme-input-surface`, so a
|
|
30
|
+
consumer wanting to restyle just text inputs had no way to do that without also restyling every
|
|
31
|
+
other input type. The private `--_input-text-surface: var(--theme-input-surface);` indirection
|
|
32
|
+
made this look like it had its own override point when it didn't.
|
|
33
|
+
|
|
34
|
+
## The three tiers
|
|
35
|
+
|
|
36
|
+
| Tier | Example | No-fallback allowed? |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| **1 — theme slots** | `--theme-surface`, `--theme-border`, `--theme-ring`, `--theme-text`, ... (`_theme-slots.css`) | Yes — deliberately global, small, stable set. Changing one is meant to reskin everything that shares it. |
|
|
39
|
+
| **2 — cross-component families** | `--theme-input-surface`, `--theme-checkbox-symbol-surface` | No longer created new. Existing ones stay as the *fallback value* inside Tier 3 tokens, not as the thing components read directly. |
|
|
40
|
+
| **3 — component-scoped** | `--input-button-primary-surface`, `--services-card-border-colour` | Never — always `var(--{component}-token, <fallback>)`. |
|
|
41
|
+
|
|
42
|
+
Tier 1 is the one deliberate exception. Everything else gets a real component-prefixed name with
|
|
43
|
+
an inline fallback, even if that fallback happens to be a Tier 1 or Tier 2 token underneath.
|
|
44
|
+
|
|
45
|
+
## Naming convention
|
|
46
|
+
|
|
47
|
+
`--{component-kebab-name}-{variant-if-any}-{property}[-state]`
|
|
48
|
+
|
|
49
|
+
- `--input-button-primary-surface` / `-surface-hover` — variant + property + state
|
|
50
|
+
- `--services-card-border-colour` / `-border-colour-hover` — property + state
|
|
51
|
+
- Only add a `-hover`/`-focus`/`-active` suffix if that state's value actually differs from the
|
|
52
|
+
base token. If hover reuses the exact same value as base (see `InputButtonCore`'s primary text
|
|
53
|
+
colour, fixed on purpose across states), reuse the base token — don't manufacture a second name
|
|
54
|
+
for a value that never diverges.
|
|
55
|
+
|
|
56
|
+
## Multi-level fallback chains are fine
|
|
57
|
+
|
|
58
|
+
A component that wants to *look like* another component's default (rather than duplicating a
|
|
59
|
+
raw value) chains through it:
|
|
60
|
+
|
|
61
|
+
```css
|
|
62
|
+
/* InputCopy's copy button defaults to InputButtonCore's primary look */
|
|
63
|
+
--_button-bg: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Three tiers deep here: instance/global override → "look like the primary button" → the theme
|
|
67
|
+
slot underneath that. Each link in the chain is a real, resolvable token — nothing depends on a
|
|
68
|
+
consuming app having declared anything.
|
|
69
|
+
|
|
70
|
+
## What this does NOT change
|
|
71
|
+
|
|
72
|
+
- Tier 1 theme slots (`_theme-slots.css`) stay bare `var()`, no fallback, by design.
|
|
73
|
+
- Geometry/typography tokens that already have real component-local defaults
|
|
74
|
+
(`--button-padding-block: 1rem;` in `06.button-geometry.css`) were never part of this problem —
|
|
75
|
+
they're declared with real values already, just not inline at the point of use. Leave them.
|
|
76
|
+
|
|
77
|
+
## Rollout status
|
|
78
|
+
|
|
79
|
+
- ✅ `InputButtonCore` (primary/secondary/tertiary), `InputCopy`, `PricingCard` — migrated
|
|
80
|
+
2026-08-25, see their `CONSUMER-STYLING.md` for the full token list. This pass also included a
|
|
81
|
+
deliberate visual redesign (flat borders, `color-mix()`-darkened hover, transparent resting
|
|
82
|
+
outline, separated `:focus-visible`), not just the token API change.
|
|
83
|
+
- ✅ `InputCheckboxRadioCore`/`Button` — migrated 2026-08-25, see their `CONSUMER-STYLING.md`.
|
|
84
|
+
Pure token-rename pass, no visual/default changes (unlike `InputButtonCore`'s redesign above).
|
|
85
|
+
Added Storybook stories for both (`InputCheckboxRadioCore`, `InputCheckboxRadioButton`) —
|
|
86
|
+
neither had one before.
|
|
87
|
+
- ✅ `InputTextCore`, `InputSelectCore`, `InputNumberCore` — migrated 2026-08-25, see their
|
|
88
|
+
`CONSUMER-STYLING.md`. `InputTextCore`/`InputSelectCore` also had this exact problem's original
|
|
89
|
+
form: a private `--_input-text-*`/`--_input-select-*` naming scheme documented as a second
|
|
90
|
+
"local override path" alongside the global tokens, added 2026-08-22 (three days before this
|
|
91
|
+
pattern was formalised) as a workaround for a global-override-not-landing report that was never
|
|
92
|
+
root-caused. That private-var-as-override-point shape is exactly what this pattern replaces —
|
|
93
|
+
renamed public with real fallbacks, old two-path docs removed. `InputNumberCore` had no
|
|
94
|
+
component-scoped tokens at all before this (bare `--theme-input-*` throughout). Added a
|
|
95
|
+
Storybook story for `InputSelectCore` (didn't have one); `InputNumberCore` still doesn't.
|
|
96
|
+
`InputTextCore`/`InputSelectCore`'s `.normal` variant also had hover and `:focus-visible` share
|
|
97
|
+
one token (literally the same rule content duplicated under two selectors) — split into
|
|
98
|
+
`--input-text-border-hover`/`--input-select-border-hover` alongside the existing `-border-focus`
|
|
99
|
+
pair, both still defaulting to `--theme-border-focus` so default rendering is unchanged, but
|
|
100
|
+
mouse and assistive-tech interaction can now be themed independently. `InputNumberCore` has no
|
|
101
|
+
hover state on its border at all (only a `:focus-visible` box-shadow toggle), so there was
|
|
102
|
+
nothing to split there.
|
|
103
|
+
- ✅ `ToggleSwitchCore`, `TripleToggleSwitchCore`, `DisplayThemeSwitch` — migrated 2026-08-25, see
|
|
104
|
+
the first two's `CONSUMER-STYLING.md`. Correction to the two lines this replaces: they claimed
|
|
105
|
+
`ToggleSwitchCore` read a bare `--theme-toggle-symbol-color-*` — that token doesn't exist
|
|
106
|
+
anywhere in this codebase and never did; false memory from conflating it with the checkbox
|
|
107
|
+
fix. `ToggleSwitchCore` itself turned out to be a plain Tier 2 case (real, live tokens, just no
|
|
108
|
+
component-scoped override point) plus five confirmed-dead private locals (declared, never read
|
|
109
|
+
anywhere in the file) removed outright. The real dead-token bug was in `TripleToggleSwitchCore`
|
|
110
|
+
(and duplicated in `DisplayThemeSwitch`'s own override): `--theme-form-radio-border`,
|
|
111
|
+
`--theme-form-radio-outline`, `--theme-form-checkbox-bg` — all three declared nowhere in the
|
|
112
|
+
layer. The first two fed two further-unused private locals (removed); the third fed the
|
|
113
|
+
selected-option marker's background (real bug, narrow impact — only visible when none of the
|
|
114
|
+
component's `system`/`light`/`dark` `:has()` overrides match).
|
|
115
|
+
|
|
116
|
+
Second bug, found while adding the Storybook story `TripleToggleSwitchCore` never had: those
|
|
117
|
+
`:has()` selectors (and the matching `.option-icon.system/.light/.dark` classes, template-bound
|
|
118
|
+
to each option's `id`) said `"auto"`, not `"system"` — but the only known consumer,
|
|
119
|
+
`DisplayThemeSwitch`, has only ever emitted `id`/`value: "system"` (it has to; that literal
|
|
120
|
+
string is `useSettingsStore.setColourScheme`'s type and the CSS class it applies to `<html>`).
|
|
121
|
+
`"auto"` never matched anything real, so the system option's green gradient marker had never
|
|
122
|
+
actually fired — confirmed live before and after the fix. Renamed the selectors to `"system"`
|
|
123
|
+
rather than the data, since the data value is a real external contract this component's
|
|
124
|
+
presentational selectors have no business dictating. `TripleToggleSwitchCore` still hardcodes
|
|
125
|
+
three option values as CSS selectors rather than being genuinely value-agnostic — noted in its
|
|
126
|
+
`CONSUMER-STYLING.md`, left as a larger separate change.
|
|
@@ -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
|
+
}
|
|
@@ -13,5 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
--button-icon-gap: 1.8rem;
|
|
15
15
|
|
|
16
|
-
/* --
|
|
16
|
+
/* Sized so the button's TOTAL rendered height matches --input-min-height, not its content
|
|
17
|
+
box alone — .input-button-core keeps box-sizing: content-box (changing that risks shifting
|
|
18
|
+
icon-only's aspect-ratio: 1 sizing), so its own padding-block and border have to be
|
|
19
|
+
subtracted back out here, unlike --input-min-height itself which happens to need no such
|
|
20
|
+
adjustment (InputTextCore has zero padding-block and no full border in its resting state).
|
|
21
|
+
This is a floor, not a fixed height — icon-only/pill buttons or longer button-text can
|
|
22
|
+
still grow past it. Textareas are the deliberate exception to this alignment entirely. */
|
|
23
|
+
--button-min-height: calc(
|
|
24
|
+
var(--input-min-height) - (2 * var(--button-padding-block)) - (2 * var(--button-border-width))
|
|
25
|
+
);
|
|
17
26
|
}
|