srcdev-nuxt-components 9.3.7 → 9.3.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.
Files changed (108) hide show
  1. package/.claude/hooks/check-component-update.sh +30 -1
  2. package/.claude/skills/components/breadcrumb.md +49 -0
  3. package/.claude/skills/components/content-docs.md +1 -1
  4. package/.claude/skills/components/service-detail.md +156 -0
  5. package/.claude/skills/components/services-card-grid.md +9 -9
  6. package/.claude/skills/components/services-card.md +82 -30
  7. package/.claude/skills/index.md +3 -1
  8. package/.claude/skills/qa-panel.md +2 -2
  9. package/.vscode/srcdev-component-breadcrumb.code-snippets +14 -0
  10. package/.vscode/srcdev-component-service-detail.code-snippets +49 -0
  11. package/.vscode/srcdev-component-services-card.code-snippets +108 -0
  12. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +94 -0
  13. package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +32 -0
  14. package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +87 -0
  15. package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +87 -0
  16. package/app/components/01.atoms/navigation/breadcrumb/tests/__snapshots__/Breadcrumb.spec.ts.snap +13 -0
  17. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +181 -0
  18. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +480 -0
  19. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +282 -0
  20. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +270 -0
  21. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +99 -0
  22. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +129 -0
  23. package/app/components/03.organisms/services/services-card/ServicesCard.vue +153 -39
  24. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +65 -0
  25. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +82 -0
  26. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +13 -5
  27. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +3 -7
  28. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +126 -2
  29. package/app/components/03.organisms/services/services-grids/stories/ServicesSectionGrid.stories.ts +6 -0
  30. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +51 -27
  31. package/app/types/components/breadcrumb.ts +4 -0
  32. package/app/types/components/index.ts +1 -0
  33. package/package.json +2 -2
  34. package/app/pages/auto-grid.vue +0 -328
  35. package/app/pages/banner-video.vue +0 -238
  36. package/app/pages/forms/examples/buttons/index.vue +0 -259
  37. package/app/pages/forms/examples/material/checkbox-radio-panels.vue +0 -244
  38. package/app/pages/forms/examples/material/cssbattle.vue +0 -60
  39. package/app/pages/forms/examples/material/text-fields.vue +0 -910
  40. package/app/pages/grid-stack.vue +0 -252
  41. package/app/pages/index.vue +0 -127
  42. package/app/pages/page-hero-highlights.vue +0 -204
  43. package/app/pages/samaritan.vue +0 -327
  44. package/app/pages/typography/hero-text.vue +0 -97
  45. package/app/pages/typography/index.vue +0 -39
  46. package/app/pages/typography/page-body.vue +0 -171
  47. package/app/pages/typography/page-heading.vue +0 -76
  48. package/app/pages/typography/page-link.vue +0 -103
  49. package/app/pages/ui/accordian.vue +0 -211
  50. package/app/pages/ui/anchor-scroll-tab-navigation.vue +0 -191
  51. package/app/pages/ui/anchor-scroll.vue +0 -367
  52. package/app/pages/ui/animated-svg-text.vue +0 -85
  53. package/app/pages/ui/block-decorators.vue +0 -142
  54. package/app/pages/ui/card-core.vue +0 -82
  55. package/app/pages/ui/carousel-basic.vue +0 -292
  56. package/app/pages/ui/carousel-flip.vue +0 -449
  57. package/app/pages/ui/carousel-infinite.vue +0 -260
  58. package/app/pages/ui/clipped-panels.vue +0 -101
  59. package/app/pages/ui/contact-section.vue +0 -323
  60. package/app/pages/ui/container-glow.vue +0 -106
  61. package/app/pages/ui/content-width.vue +0 -126
  62. package/app/pages/ui/deep-expanding-menu.vue +0 -138
  63. package/app/pages/ui/display-avatar.vue +0 -253
  64. package/app/pages/ui/display-banner.vue +0 -78
  65. package/app/pages/ui/display-chip.vue +0 -367
  66. package/app/pages/ui/display-details.vue +0 -171
  67. package/app/pages/ui/display-dialog.vue +0 -344
  68. package/app/pages/ui/display-pill.vue +0 -396
  69. package/app/pages/ui/display-prompt.vue +0 -284
  70. package/app/pages/ui/display-toast.vue +0 -311
  71. package/app/pages/ui/expanding-panel.vue +0 -326
  72. package/app/pages/ui/glowing-border.vue +0 -111
  73. package/app/pages/ui/layout-content-docs.vue +0 -72
  74. package/app/pages/ui/layout-grid-a.vue +0 -96
  75. package/app/pages/ui/layout-grid-b.vue +0 -132
  76. package/app/pages/ui/magnetic-navigation.vue +0 -50
  77. package/app/pages/ui/marquee-scroller.vue +0 -99
  78. package/app/pages/ui/mask-element.vue +0 -104
  79. package/app/pages/ui/masonry-columns.vue +0 -55
  80. package/app/pages/ui/masonry-grid-ordered-experiment.vue +0 -79
  81. package/app/pages/ui/masonry-grid-ordered.vue +0 -56
  82. package/app/pages/ui/masonry-grid-sorted.vue +0 -55
  83. package/app/pages/ui/masonry-grid.vue +0 -55
  84. package/app/pages/ui/navigation/navigation-horizontal.vue +0 -484
  85. package/app/pages/ui/navigation/site-navigation/about.vue +0 -36
  86. package/app/pages/ui/navigation/site-navigation/contact.vue +0 -39
  87. package/app/pages/ui/navigation/site-navigation/index.vue +0 -38
  88. package/app/pages/ui/navigation/site-navigation/portfolio.vue +0 -43
  89. package/app/pages/ui/navigation/site-navigation/services.vue +0 -42
  90. package/app/pages/ui/navigation/tab-navigation.vue +0 -493
  91. package/app/pages/ui/page-row.vue +0 -127
  92. package/app/pages/ui/price-list.vue +0 -65
  93. package/app/pages/ui/profile-section.vue +0 -142
  94. package/app/pages/ui/qr-code/[componentName].vue +0 -218
  95. package/app/pages/ui/rotating-carousel.vue +0 -154
  96. package/app/pages/ui/scroll-reveal-image.vue +0 -483
  97. package/app/pages/ui/section-parallax.vue +0 -54
  98. package/app/pages/ui/services/colour-finder.vue +0 -43
  99. package/app/pages/ui/services/services-cards.vue +0 -57
  100. package/app/pages/ui/services/services-section/[slug].vue +0 -61
  101. package/app/pages/ui/services/services-sections-compact.vue +0 -64
  102. package/app/pages/ui/settings.vue +0 -54
  103. package/app/pages/ui/simple-grid.vue +0 -55
  104. package/app/pages/ui/slider-gallery.vue +0 -261
  105. package/app/pages/ui/tabs-y.vue +0 -107
  106. package/app/pages/ui/tabs.vue +0 -189
  107. package/app/pages/ui/tooltips.vue +0 -104
  108. package/app/pages/ui/wipe-away-vertical.vue +0 -129
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # PostToolUse hook (Write|Edit) for app/components/*.vue files. Advisory nudges only
3
3
  # (missing != wrong): skill doc, story, test, CONSUMER-STYLING.md, .vscode snippet,
4
- # legacy tier-folder location, options-style defineProps.
4
+ # legacy tier-folder location, options-style defineProps, single-use private CSS tokens,
5
+ # leftover app/pages/ demo pages (this library is Storybook-only, see Claude.md).
5
6
 
6
7
  root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
7
8
 
@@ -63,4 +64,32 @@ if grep -qE 'defineProps\(\s*\{' "$f" && ! grep -q 'defineProps<' "$f"; then
63
64
  msg="$msg This component uses the options-style defineProps({...}) pattern, an outdated-pattern signal; migrate to interface Props + withDefaults(defineProps<Props>(), {...}) per the Props Pattern in Claude.md."
64
65
  fi
65
66
 
67
+ # Private --_x CSS tokens that appear only twice in the file (once declared, once consumed) are
68
+ # likely pure indirection with no reuse/computation/state to justify them — see the "Public
69
+ # token pattern" rule in Claude.md's Styling Methodology and pitfall #20 (ServicesCard).
70
+ private_vars=$(grep -oE -- '--_[a-zA-Z0-9-]+' "$f" | sort -u)
71
+ if [[ -n "$private_vars" ]]; then
72
+ single_use=""
73
+ while IFS= read -r var; do
74
+ [[ -z "$var" ]] && continue
75
+ count=$(grep -oE -- "\\${var}\\b" "$f" | wc -l | tr -d ' ')
76
+ if [[ "$count" -le 2 ]]; then
77
+ single_use="$single_use $var"
78
+ fi
79
+ done <<< "$private_vars"
80
+ if [[ -n "$single_use" ]]; then
81
+ msg="$msg This component declares private CSS token(s) ($(echo "$single_use" | xargs)) that appear to be used only once — check whether each is genuinely reused across multiple declarations/selectors, composed from a v-bind() value, or swapped by a state (:hover, data-theme). If not, inline the public var(--token, default) directly at its point of use instead, per the Public token pattern rule in Claude.md's Styling Methodology (see pitfall #20 for the ServicesCard precedent), and update CONSUMER-STYLING.md/the skill doc if the token's public name or default changes."
82
+ fi
83
+ fi
84
+
85
+ # This library ships no demo pages — Storybook stories are the only demo surface (see Claude.md's
86
+ # "Storybook is the only demo surface"). Flag any app/pages/ file that mentions this component,
87
+ # whether a pre-existing leftover or a newly (re)added one.
88
+ if [[ -d "$root/app/pages" ]]; then
89
+ demo_pages=$(grep -rlE "\\b${name}\\b" "$root/app/pages" 2>/dev/null | xargs)
90
+ if [[ -n "$demo_pages" ]]; then
91
+ msg="$msg Found file(s) under app/pages/ referencing $name ($demo_pages); this library has no demo pages (Storybook only) — delete them and move any demo-worthy content into the Storybook story instead."
92
+ fi
93
+ fi
94
+
66
95
  jq -n --arg msg "$msg" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
@@ -0,0 +1,49 @@
1
+ # Breadcrumb Component
2
+
3
+ ## Overview
4
+
5
+ `Breadcrumb` renders a `<nav aria-label="Breadcrumb"><ol>...</ol></nav>` trail from an `items`
6
+ array. Routing is delegated to the consumer: an item with a `to` renders as a `NuxtLink`; an item
7
+ without one renders as plain text marked `aria-current="page"` (typically the last item, the
8
+ current page).
9
+
10
+ ## Props
11
+
12
+ | Prop | Type | Default | Required |
13
+ |------|------|---------|----------|
14
+ | `items` | `BreadcrumbItem[]` | — | **yes** |
15
+ | `separator` | `string` | `"/"` | no |
16
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
17
+
18
+ `BreadcrumbItem` is exported from `~/types/components/breadcrumb` (and re-exported from
19
+ `~/types/components`):
20
+
21
+ ```typescript
22
+ interface BreadcrumbItem {
23
+ label: string;
24
+ to?: string;
25
+ }
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```vue
31
+ <Breadcrumb
32
+ :items="[
33
+ { label: 'Services', to: '/services' },
34
+ { label: 'Balayage' },
35
+ ]"
36
+ />
37
+ ```
38
+
39
+ Omit `to` on whichever item represents the current page — usually the last one. Every item with
40
+ a `to` is a real link; the library never guesses which item is "current" from its position.
41
+
42
+ ## Notes
43
+
44
+ - Component is auto-imported in Nuxt — no import needed.
45
+ - No default content/slot — it's data-driven purely from `items`.
46
+ - `ServiceDetail` uses this component internally for its hero banner breadcrumb, overriding
47
+ `--breadcrumb-colour` to `white` so it reads over the hero image. See
48
+ [service-detail.md](service-detail.md).
49
+ - See `CONSUMER-STYLING.md` in the component's own folder for the full `--breadcrumb-*` token API.
@@ -178,4 +178,4 @@ All `--content-docs-*` tokens can be overridden at global, page, or instance sco
178
178
  - File: `app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue`
179
179
  - Types: `app/types/components/content-docs.d.ts` (`DocsNavItem`)
180
180
  - Tests: `app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts`
181
- - Demo page: `app/pages/ui/layout-content-docs.vue`
181
+ - Storybook: `app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts`
@@ -0,0 +1,156 @@
1
+ # ServiceDetail Component
2
+
3
+ ## Overview
4
+
5
+ `ServiceDetail` renders a single service as a full, standalone detail page: a full-bleed hero
6
+ banner (breadcrumb, eyebrow, title, price/duration pills over the service image), a two-column
7
+ body (long-form content in the main column, a sticky sidebar booking card + "You may also like"
8
+ related-services list), and a closing full-width CTA banner. The process steps reuse
9
+ [StepperList](stepper-list.md) (numbered circle indicators with connectors) rather than bespoke
10
+ markup — Ideal For and FAQs are still bespoke (2-column card grid, plain Q&A list) since neither
11
+ matches an existing molecule as directly.
12
+
13
+ It is a different shape from [ServicesSection](services-section.md): `ServicesSection` renders
14
+ image-beside-content (used both as a compact summary card in a list and, in full mode, as a
15
+ same-page detail block); `ServiceDetail` is meant to *be* the whole page for one service —
16
+ `headerTag` defaults to `"h1"` accordingly, and there is no summary/compact mode. There is no
17
+ `isSummary` prop and no `v-if` gating of content — everything the component renders is always
18
+ rendered.
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Required |
23
+ |------|------|---------|----------|
24
+ | `serviceData` | `Service` | — | **yes** |
25
+ | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | no |
26
+ | `headerTag` | `"h1" \| "h2" \| "h3"` | `"h1"` | no |
27
+ | `subheadingTag` | `"h2" \| "h3"` | `"h2"` | no |
28
+ | `breadcrumbItems` | `BreadcrumbItem[]` | auto-built from `serviceData` | no |
29
+ | `processHeading` | `string` | `"The Process"` | no |
30
+ | `idealForHeading` | `string` | `"Ideal For"` | no |
31
+ | `maintenanceHeading` | `string` | `"Aftercare & Maintenance"` | no |
32
+ | `faqsHeading` | `string` | `"Frequently Asked Questions"` | no |
33
+ | `bookingHeading` | `string` | `"Book This Service"` | no |
34
+ | `priceLabel` | `string` | `"Price"` | no |
35
+ | `durationLabel` | `string` | `"Duration"` | no |
36
+ | `locationLabel` | `string` | `"Location"` | no |
37
+ | `location` | `string` | `undefined` | no |
38
+ | `relatedServicesHeading` | `string` | `"You May Also Like"` | no |
39
+ | `relatedServices` | `Service[]` | `[]` | no |
40
+ | `finalCtaHeading` | `string` | `"Ready to book your appointment?"` | no |
41
+ | `finalCtaBody` | `string` | `"Get in touch to book your appointment."` | no |
42
+ | `imageLoading` | `"eager" \| "lazy"` | `"eager"` | no |
43
+ | `imageFetchPriority` | `"high" \| "auto" \| "low"` | `"high"` | no |
44
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
45
+
46
+ `location` and `relatedServices` are **props on ServiceDetail, not fields on the shared `Service`
47
+ type** — neither is universal enough to belong on `~/types/types.services`. The location row and
48
+ the whole "You may also like" block simply don't render when they're omitted.
49
+
50
+ `imageLoading`/`imageFetchPriority` default to eager/high because a `ServiceDetail` page normally
51
+ has exactly one hero image and it's always the LCP candidate — unlike `ServicesSection`, which is
52
+ often looped and needs the `index` prop to lazy-load everything past the first two instances.
53
+
54
+ ### `headerTag` vs `subheadingTag`
55
+
56
+ Unlike `ServicesSection` (one `headerTag` prop applied to every heading, title included),
57
+ `ServiceDetail` splits these: `headerTag` controls only the hero `<h1>` title; `subheadingTag`
58
+ controls every other heading (What Is It, Process, Ideal For, Aftercare, FAQs, final CTA heading).
59
+ This keeps the page's heading hierarchy correct when `ServiceDetail` is the page's own `<h1>`.
60
+
61
+ The FAQ question itself is a hardcoded `<h3>` — if you set `subheadingTag="h3"`, FAQ questions
62
+ end up at the same level as the FAQs section heading rather than one below it.
63
+
64
+ ### `breadcrumbItems`
65
+
66
+ Defaults to a plain, non-linked two-item trail built from `serviceData.category` and
67
+ `serviceData.title` when omitted. Pass real routes once your app's URL structure is known:
68
+
69
+ ```vue
70
+ <ServiceDetail
71
+ :service-data="service"
72
+ :breadcrumb-items="[
73
+ { label: 'Services', to: '/services' },
74
+ { label: service.title },
75
+ ]"
76
+ />
77
+ ```
78
+
79
+ ## Slots
80
+
81
+ | Slot | Slot props | Purpose |
82
+ |------|-----------|---------|
83
+ | `book-cta` | `{ serviceData: Service }` | Booking button inside the sidebar's booking card |
84
+ | `sidebar-note` | — | Freeform note under the booking button (e.g. patch-test wording) — empty by default, deliberately not hardcoded since this library also serves non-hair-and-beauty apps |
85
+ | `related-service` | `{ service: Service, index: number }` | Replaces one related-service item's whole default (non-clickable) thumbnail/title/price markup — use to wrap it in a real link |
86
+ | `final-cta` | `{ serviceData: Service }` | Button in the closing full-width CTA banner |
87
+
88
+ All routing decisions (breadcrumb links, book-cta href, related-service links, final-cta href)
89
+ are delegated to the consumer, matching `ServicesSection`'s slot-based routing pattern.
90
+
91
+ ## Usage
92
+
93
+ ```vue
94
+ <ServiceDetail
95
+ :service-data="service"
96
+ location="Mobile — across Bath"
97
+ :related-services="relatedServices"
98
+ >
99
+ <template #book-cta>
100
+ <InputButtonCore tag="a" href="/contact" button-text="Book now" variant="primary" />
101
+ </template>
102
+ <template #sidebar-note>
103
+ A patch test is required at least 48 hours before any colour treatment.
104
+ </template>
105
+ <template #related-service="{ service: related }">
106
+ <NuxtLink :to="`/services/${related.slug}`">{{ related.title }} — {{ related.price }}</NuxtLink>
107
+ </template>
108
+ <template #final-cta>
109
+ <InputButtonCore tag="a" href="/contact" button-text="Book now" variant="secondary" />
110
+ </template>
111
+ </ServiceDetail>
112
+ ```
113
+
114
+ ## Local style override scaffold
115
+
116
+ ```vue
117
+ <ServiceDetail :style-class-passthrough="['my-service-page']" :service-data="service">
118
+ ...
119
+ </ServiceDetail>
120
+
121
+ <style>
122
+ /* ─── ServiceDetail local overrides ──────────────────────────────
123
+ Colours, borders, geometry only — do not override behaviour.
124
+ Delete this block if no overrides are needed.
125
+ ─────────────────────────────────────────────────────────────────── */
126
+ .service-detail {
127
+ &.my-service-page {
128
+ --service-detail-hero-border-radius: 1.2rem;
129
+ --service-detail-process-index-colour: var(--brand-accent);
130
+ }
131
+ }
132
+ </style>
133
+ ```
134
+
135
+ See [component-local-style-override.md](../component-local-style-override.md) for the general
136
+ pattern, and `CONSUMER-STYLING.md` in the component's own folder for the full token API.
137
+
138
+ ## Notes
139
+
140
+ - Component is auto-imported in Nuxt — no import needed.
141
+ - The `Service` type is imported from `~/types/types.services`; `BreadcrumbItem` from
142
+ `~/types/components/breadcrumb`.
143
+ - No `isSummary` mode and no content gating — this component always renders everything it's
144
+ given. If you need a compact summary card for a services listing page, use
145
+ [ServicesSection](services-section.md) in summary mode or [ServicesCard](services-card.md)
146
+ instead, and reserve `ServiceDetail` for the individual `/services/[slug]` page.
147
+ - Uses [Breadcrumb](breadcrumb.md) internally for the hero banner breadcrumb.
148
+ - The two-/three-column layout breakpoints (body columns, ideal-for grid, final CTA row) are
149
+ `@container` queries against the component's own width, not the viewport.
150
+ - Internal element classes are BEM-namespaced under `service-detail__*` (`service-detail__hero`,
151
+ `service-detail__process`, `service-detail__sidebar`, `service-detail__related`, etc.) — see
152
+ `CONSUMER-STYLING.md` for the full list alongside their tokens.
153
+ - The section gets `aria-labelledby` automatically when `tag` is `"section"`, `"article"`, or
154
+ `"aside"`, pointing at the id `ServiceDetail` binds to its own title `HeroText` internally —
155
+ same mechanism as `ServicesSection`. `tag="main"` renders a `<main>` element but is never
156
+ auto-labelled.
@@ -12,7 +12,7 @@
12
12
  | `tag` | `"div" \| "section" \| "main"` | `"div"` | no |
13
13
  | `eyebrowConfig` | `EyebrowConfig` | `{}` | no |
14
14
  | `heroConfig` | `HeroConfig` | `{}` | no |
15
- | `hrefBase` | `string` | `"/ui/services/services-section/"` | no |
15
+ | `hrefBase` | `string` | `"/services/"` | no |
16
16
  | `buttonTextPrefix` | `string` | `"Enquire about"` | no |
17
17
  | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
18
18
 
@@ -38,8 +38,8 @@ Set on `.services-card-grid` (or scoped to a page class):
38
38
 
39
39
  | Token | Default | Controls |
40
40
  | ---------------------- | --------- | ------------------------------------- |
41
- | `--_gap` | `4rem` | Gap between grid cells |
42
- | `--_column-min-width` | `250px` | Minimum column width before wrapping |
41
+ | `--services-card-grid-gap` | `4rem` | Gap between grid cells |
42
+ | `--services-card-grid-column-min-width` | `250px` | Minimum column width before wrapping |
43
43
 
44
44
  ## Consumer page boilerplate
45
45
 
@@ -87,13 +87,13 @@ if (servicesData.value.length === 0) {
87
87
  .page-services {
88
88
  /* Page-level CSS token overrides — delete any you don't need */
89
89
  .services-card-grid {
90
- --_gap: 4rem;
91
- --_column-min-width: 250px;
90
+ --services-card-grid-gap: 4rem;
91
+ --services-card-grid-column-min-width: 250px;
92
92
 
93
93
  .services-card {
94
- --_eyebrow-text-margin-block: 0.8rem 0;
95
- --_hero-text-margin-block: 2rem 1rem;
96
- --_description-text-colour: var(--colour-text-secondary);
94
+ --eyebrow-text-padding-block: 0.8rem 0;
95
+ --hero-text-padding-block: 2rem 1rem;
96
+ --description-text-colour: var(--colour-text-secondary);
97
97
  }
98
98
  }
99
99
  }
@@ -104,6 +104,6 @@ if (servicesData.value.length === 0) {
104
104
 
105
105
  - Component is auto-imported in Nuxt — no import needed.
106
106
  - The `Service` type is imported from `~/types/types.services`.
107
- - Uses `repeat(auto-fit, minmax(var(--_column-min-width), 1fr))` — columns grow to fill available space and wrap when below the minimum width.
107
+ - Uses `repeat(auto-fit, minmax(var(--services-card-grid-column-min-width, 250px), 1fr))` — columns grow to fill available space and wrap when below the minimum width.
108
108
  - The `#actions` slot template is passed down into each `ServicesCard`; `serviceData` is the scoped prop for the current iteration item.
109
109
  - Data fetching is the page's responsibility — pass an empty array as fallback while loading (`servicesData ?? []`).
@@ -2,51 +2,69 @@
2
2
 
3
3
  ## Overview
4
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.
5
+ `ServicesCard` renders a single service as a portrait card: image, subtitle (eyebrow), title, short description, an optional duration/price meta row, 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
6
 
7
7
  ## Props
8
8
 
9
- | Prop | Type | Default | Required |
10
- | ----------------------- | --------------------------------- | ------- | -------- |
11
- | `serviceData` | `Service` | — | **yes** |
12
- | `tag` | `"div" \| "section" \| "article"` | `"div"` | no |
13
- | `eyebrowConfig` | `EyebrowConfig` | `{}` | no |
14
- | `heroConfig` | `HeroConfig` | `{}` | no |
15
- | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
9
+ | Prop | Type | Default | Required |
10
+ | ----------------------- | ---------------------------------- | ----------- | -------- |
11
+ | `serviceData` | `Service` | — | **yes** |
12
+ | `tag` | `"div" \| "section" \| "article"` | `"div"` | no |
13
+ | `href` | `string` | `undefined` | no |
14
+ | `external` | `boolean` | `false` | no |
15
+ | `eyebrowConfig` | `EyebrowConfig` | `{}` | no |
16
+ | `heroConfig` | `HeroConfig` | `{}` | no |
17
+ | `durationText` | `string` | `undefined` | no |
18
+ | `priceText` | `string` | `undefined` | no |
19
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
20
+
21
+ ### Meta row (`durationText` / `priceText`)
22
+
23
+ A row below the description shows duration (left) and price (right), separated by a top divider. It defaults to `serviceData.duration`/`serviceData.price` — `durationText`/`priceText` props override that text, and the `duration`/`price` slots (see Slots below) fully replace the content (e.g. to add an icon). The row is omitted entirely when there's no duration/price text and no slot content on either side.
24
+
25
+ ### Whole-card clickable (`href`)
26
+
27
+ When `href` is set **and no `actions` slot content is provided**, the root element switches from `tag` to a link (`NuxtLink` for an internal href starting with `/`, a plain `a` otherwise) and the whole card becomes clickable — matching `InputButtonCore`'s link-resolution pattern. `external` forces a plain `a` tag even for an internal-looking href (e.g. a Nitro server route like `/api/auth/github`).
28
+
29
+ If an `actions` slot is provided, the card stays as a static `tag` element (no href rendered) even when `href` is set — this avoids nesting another interactive element (e.g. a button/link in `actions`) inside the card's own anchor, which would be invalid HTML. Use the `href` mode for a plain "whole card links out" card with no separate CTA, and the `actions`-slot mode (see Basic usage below) when the card needs its own button/link.
30
+
31
+ `is-clickable` is added to the root class list when whole-card-clickable mode is active, giving `cursor: pointer` and removing default link colour/underline. It's also what gates the border/outline hover/focus interaction-state tokens — see "Interaction states (whole-card-clickable only)" in `CONSUMER-STYLING.md`; they're a no-op when the card isn't clickable, since only `.is-clickable` has a `:hover`/`:focus-visible` rule.
16
32
 
17
33
  ### EyebrowConfig
18
34
 
19
- | Key | Type | Default |
20
- | ---------- | --------------------------------- | ---------- |
21
- | `tag` | `"p" \| "div" \| "span"` | `"div"` |
22
- | `fontSize` | `"large" \| "medium" \| "small"` | `"large"` |
35
+ | Key | Type | Default |
36
+ | ---------- | ---------------------------------- | --------- |
37
+ | `tag` | `"p" \| "div" \| "span"` | `"div"` |
38
+ | `fontSize` | `"large" \| "medium" \| "small"` | `"large"` |
23
39
 
24
40
  ### HeroConfig
25
41
 
26
- | Key | Type | Default |
27
- | ---------- | -------------------------------------------------------------- | ----------- |
28
- | `tag` | `"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h2"` |
42
+ | Key | Type | Default |
43
+ | ---------- | ----------------------------------------------------------------- | ----------- |
44
+ | `tag` | `"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h2"` |
29
45
  | `fontSize` | `"display" \| "title" \| "heading" \| "subheading" \| "label"` | `"heading"` |
30
46
 
31
47
  Config objects are partial — only specify the keys you want to override. Unset keys fall back to the defaults shown above.
32
48
 
33
49
  ## Slots
34
50
 
35
- | Slot | Slot props | Purpose |
36
- | --------- | -------------------------- | ---------------------------------------------------------------------- |
37
- | `actions` | `{ serviceData: Service }` | CTA area below the description buttons, links, or any action content |
51
+ | Slot | Slot props | Purpose |
52
+ | ---------- | --------------------------- | --------------------------------------------------------------------------------------------------------- |
53
+ | `duration` | `{ serviceData: Service }` | Replaces the duration (left) side of the meta row; defaults to `durationText`/`serviceData.duration` |
54
+ | `price` | `{ serviceData: Service }` | Replaces the price (right) side of the meta row; defaults to `priceText`/`serviceData.price` |
55
+ | `actions` | `{ serviceData: Service }` | CTA area below the meta row — buttons, links, or any action content |
38
56
 
39
- The `actions` slot receives `serviceData` as a scoped prop so the consumer can construct routes or labels from the service data without additional props.
57
+ All three slots receive `serviceData` as a scoped prop so the consumer can construct routes, labels, or formatted text from the service data without additional props.
40
58
 
41
59
  ## CSS custom properties
42
60
 
43
- Set on `.services-card` (or scoped to a page class):
44
-
45
- | Token | Default | Controls |
46
- | ------------------------------ | ----------------------------- | ------------------------------- |
47
- | `--_eyebrow-text-margin-block` | `0.8rem 0` | Space above/below the eyebrow |
48
- | `--_hero-text-margin-block` | `2rem 1rem` | Space above/below the title |
49
- | `--_description-text-colour` | `var(--colour-text-secondary)` | Description paragraph colour |
61
+ Prefer the `--services-card-*`/`--image-wrapper-*`/`--details-wrapper-*`/`--description-*`/`--meta-*`/`--footer-*`
62
+ CSS custom properties documented in `CONSUMER-STYLING.md` (in the component's own folder) over
63
+ raw class overrides — it lists every token, its default, which ones have a global `:root`
64
+ fallback vs. page-scoped-only, and worked examples for global theming, page-scoped overrides,
65
+ and per-instance overrides via `styleClassPassthrough`. For anything the tokens don't cover,
66
+ scaffold a style block using `styleClassPassthrough` instead see
67
+ [component-local-style-override.md](../component-local-style-override.md) for the general pattern.
50
68
 
51
69
  ## Basic usage
52
70
 
@@ -80,6 +98,24 @@ Set on `.services-card` (or scoped to a page class):
80
98
  </ServicesCard>
81
99
  ```
82
100
 
101
+ ## With custom meta row content
102
+
103
+ ```vue
104
+ <ServicesCard :service-data="service">
105
+ <template #duration="{ serviceData }">
106
+ <Icon name="mdi:clock-time-four-outline" class="icon" />
107
+ <span>{{ serviceData.duration }}</span>
108
+ </template>
109
+ <template #price="{ serviceData }">
110
+ <Icon name="mdi:currency-gbp" class="icon" />
111
+ <span>From {{ serviceData.price }}</span>
112
+ </template>
113
+ <template #actions="{ serviceData }">
114
+ <InputButtonCore variant="secondary" :button-text="`Enquire`" :href="`/services/${serviceData.slug}`" />
115
+ </template>
116
+ </ServicesCard>
117
+ ```
118
+
83
119
  ## Consumer page boilerplate
84
120
 
85
121
  ```vue
@@ -107,18 +143,34 @@ Set on `.services-card` (or scoped to a page class):
107
143
  <style lang="css">
108
144
  .page-my-page {
109
145
  .services-card {
110
- --_eyebrow-text-margin-block: 0.8rem 0;
111
- --_hero-text-margin-block: 2rem 1rem;
112
- --_description-text-colour: var(--colour-text-secondary);
146
+ --services-card-gap: 1.4rem;
147
+ --description-line-clamp: 3;
148
+ --eyebrow-text-padding-block: 0.8rem 0;
149
+ --hero-text-padding-block: 2rem 1rem;
150
+ --meta-border-colour: var(--brand-border);
113
151
  }
114
152
  }
115
153
  </style>
116
154
  ```
117
155
 
156
+ See `CONSUMER-STYLING.md` for the full token list — every token is a plain public custom property
157
+ consumed directly at its point of use (no `--_`-prefixed private indirection layer), so all of
158
+ them work both globally (`:root`) and scoped like the example above.
159
+
160
+ ## Whole-card clickable usage
161
+
162
+ ```vue
163
+ <ServicesCard :service-data="service" :href="`/services/${service.slug}`" />
164
+ ```
165
+
166
+ No `actions` slot — the entire card renders as an anchor (`NuxtLink` for the internal href here) and is clickable anywhere within it.
167
+
118
168
  ## Notes
119
169
 
120
170
  - Component is auto-imported in Nuxt — no import needed.
121
171
  - The `Service` type is imported from `~/types/types.services`.
122
- - Grid rows are sized `auto auto auto 5lh auto` the `5lh` row locks description height so actions align across cards.
172
+ - Root markup is two rows (`grid-template-rows: auto 1fr`): `.image-wrapper`, then a `.details-wrapper` (`display: flex; flex-direction: column`) holding the eyebrow, title, description, and a `.footer` wrapper. Description length is controlled by `--description-line-clamp` (default effectively unclamped) rather than a fixed-height grid row.
173
+ - `.footer` groups the meta row and the `actions` slot and gets `margin-block-start: auto`, pinning them to the bottom of the card. Combined with the root's `1fr` details row, this means when `ServicesCardGrid`'s default `align-items: stretch` makes a card taller than its own content (to match a taller sibling in the same row), the extra height goes to `.footer`'s top margin rather than leaving whitespace below the actions slot — so the meta row and actions/button line up across a row of cards regardless of each card's description length. This does the visual job of CSS subgrid without needing a subgrid chain across `ServicesCard`/`ServicesCardGrid` (which would also break whenever cards in a row don't all render the same optional rows — `.meta` and `actions` are both conditional).
123
174
  - Image has a `3/4` aspect ratio with a subtle scale-on-hover effect.
175
+ - `.services-card`, `.image-wrapper`, `.details-wrapper`, and `.footer` all set `min-inline-size: 0`. Without it, an unbreakable child — most commonly a long `actions` slot button label, since `InputButtonCore`'s `.button-text` is `white-space: nowrap` with no ellipsis — forces its content's min-content width up through the flex/grid chain and widens that one card's grid column in `ServicesCardGrid` wider than its siblings (the image just rides along on the widened column; it isn't the actual cause). If you see one card/column wider than the rest with cut-off content, check for a long unbreakable string in a slot before assuming it's an image sizing issue.
124
176
  - Usually consumed via `ServicesCardGrid` rather than directly.
@@ -48,7 +48,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
48
48
  ├── vercel-node-version.md — .nvmrc pinned to Node 24 is required; without it Vercel uses npm 10 which crashes on versionless optional stubs
49
49
  ├── robots-env-aware.md — @nuxtjs/robots: allow crawling on prod domain only, block on preview/staging via env var
50
50
  ├── new-app-scaffold.md — scaffold a new Nuxt consumer app extending this layer (package.json, nuxt.config, app structure, CLAUDE.md)
51
- ├── qa-panel.md — collapsible panel for toggling component props live on a page (always on for /pages/ui/ demo pages; gate with isDev in consuming apps)
51
+ ├── qa-panel.md — collapsible panel for toggling component props live on a page, for consuming apps' own pages (gate with isDev unless demo-only); this library's own demos live in Storybook
52
52
  ├── pull-request-description.md — produce a PR description as a fenced markdown block from git diff vs main
53
53
  ├── using-component-skills.md — discover and use component skills in consumer apps: where skills land, browsing patterns, workflow for deciding build vs. compose
54
54
  ├── composable-canonical-url.md — useCanonicalUrl: set <link rel="canonical"> from runtimeConfig.public.canonicalHost; layout setup, node types
@@ -71,6 +71,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
71
71
  ├── services-card-grid.md — ServicesCardGrid props, config pass-through, CSS tokens, full page boilerplate
72
72
  ├── services-section-grid.md — ServicesSectionGrid props, useAlternateReverse zigzag layout, page boilerplate
73
73
  ├── services-section.md — ServicesSection props (incl. heading/CTA copy), summary-link/cta/cta-panel slots, summary vs full mode
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
+ ├── breadcrumb.md — Breadcrumb: items (BreadcrumbItem[]) trail, link vs current-page text, CSS token API
74
76
  ├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
75
77
  ├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
76
78
  ├── expanding-panel.md — ExpandingPanel v-model, forceOpened, contentIsOnTop overlay mode, slots (summary/icon/content), ARIA wiring, CSS token API
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- A collapsible panel that lets you toggle component props live on a page — without touching the component or breaking the visual layout. Uses a native `<details>`/`<summary>` so it takes up no space when collapsed. Every page under `app/pages/ui/` in this library is a demo-only page already, so the panel always renders there no `isDev` gate needed. If you add one to a page in a consuming app that also serves real production traffic, gate it behind that app's own dev/staging check.
5
+ A collapsible panel that lets you toggle component props live on a page — without touching the component or breaking the visual layout. Uses a native `<details>`/`<summary>` so it takes up no space when collapsed. This library no longer ships its own demo pages (`app/pages/`) component demos live entirely in Storybook now, which has its own live prop-toggling via args/controls, so this panel isn't needed there. It's still useful in a **consuming app's own pages**: gate it behind that app's dev/staging check unless the page is genuinely demo-only.
6
6
 
7
7
  ## Structure
8
8
 
@@ -218,7 +218,7 @@ Scope inside your page body class (e.g. `.my-page`) so styles don't bleed. The p
218
218
 
219
219
  ## Notes
220
220
 
221
- - **Consuming apps on real production traffic**: if the page hosting the panel isn't demo-only (unlike everything under `app/pages/ui/` in this library), gate the panel with that app's own dev/staging check, e.g. `v-if="isDev"` with `const isDev = import.meta.dev;`. `import.meta.dev` is `false` in production builds, so the gated block is tree-shaken with no runtime cost.
221
+ - **Consuming apps on real production traffic**: if the page hosting the panel isn't demo-only, gate the panel with that app's own dev/staging check, e.g. `v-if="isDev"` with `const isDev = import.meta.dev;`. `import.meta.dev` is `false` in production builds, so the gated block is tree-shaken with no runtime cost.
222
222
  - **Consuming apps**: The active chip color (`oklch(55% 0.18 240)`) is a neutral blue. Replace with a brand accent token if preferred: `background: var(--color-brand-accent)`.
223
223
  - **Panel placement**: Outside any `overflow: hidden` or clipping container, otherwise the panel may be clipped or push layout unexpectedly. Placing it as a direct sibling of the component row works well.
224
224
  - **Computed CSS vars**: When a prop controls a CSS custom property (e.g. max-height tiers), use a `computed` that returns a style object and bind it with `:style` on the component wrapper:
@@ -0,0 +1,14 @@
1
+ {
2
+ "SRCDEV Breadcrumb": {
3
+ "description": "Breadcrumb trail — items without a `to` render as plain current-page text",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<Breadcrumb",
7
+ " :items=\"[",
8
+ " { label: '$1Services', to: '$2/services' },",
9
+ " { label: '$3${TM_FILENAME_BASE}' },",
10
+ " ]\"",
11
+ "/>"
12
+ ]
13
+ }
14
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "SRCDEV ServiceDetail": {
3
+ "description": "ServiceDetail full page — hero banner, content, sticky sidebar, closing CTA",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ServiceDetail",
7
+ " :service-data=\"$1service\"",
8
+ " location=\"$2Mobile — across Bath\"",
9
+ " :related-services=\"$3relatedServices\"",
10
+ ">",
11
+ " <template #book-cta>",
12
+ " <button type=\"button\">$4Book Now</button>",
13
+ " </template>",
14
+ " <template #sidebar-note>",
15
+ " $5",
16
+ " </template>",
17
+ " <template #final-cta>",
18
+ " <button type=\"button\">$6Book Now</button>",
19
+ " </template>",
20
+ "</ServiceDetail>"
21
+ ]
22
+ },
23
+ "SRCDEV ServiceDetail Custom Breadcrumb": {
24
+ "description": "ServiceDetail with explicit breadcrumb routes instead of the auto-built category/title trail",
25
+ "scope": "vue,html",
26
+ "body": [
27
+ "<ServiceDetail",
28
+ " :service-data=\"$1service\"",
29
+ " :breadcrumb-items=\"[",
30
+ " { label: '$2Services', to: '$3/services' },",
31
+ " { label: '$4${TM_FILENAME_BASE}' },",
32
+ " ]\"",
33
+ ">",
34
+ " <template #book-cta>",
35
+ " <button type=\"button\">$5Book Now</button>",
36
+ " </template>",
37
+ "</ServiceDetail>"
38
+ ]
39
+ },
40
+ "SRCDEV ServiceDetail Related Service Link Slot": {
41
+ "description": "ServiceDetail related-service scoped slot — wraps each related item in a real link",
42
+ "scope": "vue,html",
43
+ "body": [
44
+ "<template #related-service=\"{ service }\">",
45
+ " <NuxtLink :to=\"`/services/${service.slug}`\">$1{{ service.title }}</NuxtLink>",
46
+ "</template>"
47
+ ]
48
+ }
49
+ }