srcdev-nuxt-components 9.5.6 → 9.6.0

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 (113) hide show
  1. package/.claude/skills/component-compliance-checklist.md +6 -2
  2. package/.claude/skills/components/breadcrumb.md +5 -0
  3. package/.claude/skills/components/display-pill.md +2 -0
  4. package/.claude/skills/components/input-button.md +3 -0
  5. package/.claude/skills/components/navigation-items.md +1 -0
  6. package/.claude/skills/components/opening-hours.md +12 -0
  7. package/.claude/skills/components/price-list.md +10 -1
  8. package/.claude/skills/components/responsive-header.md +74 -0
  9. package/.claude/skills/components/service-detail.md +32 -0
  10. package/.claude/skills/components/service-summary.md +8 -0
  11. package/.claude/skills/components/services-card-grid.md +5 -3
  12. package/.claude/skills/components/services-card.md +22 -12
  13. package/.claude/skills/components/site-header.md +2 -0
  14. package/.claude/skills/components/slider-gallery.md +10 -4
  15. package/.claude/skills/components/tab-navigation.md +25 -9
  16. package/.claude/skills/index.md +1 -2
  17. package/.claude/skills/storybook-add-story.md +14 -0
  18. package/.vscode/srcdev-component-responsive-header.code-snippets +13 -1
  19. package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
  20. package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
  21. package/.vscode/srcdev-component-services-card.code-snippets +38 -36
  22. package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
  23. package/.vscode/srcdev-component-site-header.code-snippets +3 -1
  24. package/.vscode/srcdev-component-tab-navigation.code-snippets +1 -1
  25. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +3 -0
  26. package/app/components/01.atoms/display-pill/DisplayPill.vue +5 -2
  27. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
  28. package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
  29. package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
  30. package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
  31. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +52 -11
  32. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +55 -23
  33. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +176 -257
  34. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +42 -2
  35. package/app/components/02.molecules/opening-hours/CONSUMER-STYLING.md +11 -1
  36. package/app/components/02.molecules/opening-hours/OpeningHours.vue +71 -43
  37. package/app/components/02.molecules/opening-hours/stories/OpeningHours.stories.ts +138 -6
  38. package/app/components/02.molecules/opening-hours/tests/OpeningHours.spec.ts +53 -0
  39. package/app/components/02.molecules/opening-hours/tests/__snapshots__/OpeningHours.spec.ts.snap +1 -1
  40. package/app/components/02.molecules/opening-hours/tests/buildOpeningHoursSpecification.spec.ts +37 -1
  41. package/app/components/02.molecules/price-list/CONSUMER-STYLING.md +17 -5
  42. package/app/components/02.molecules/price-list/PriceList.vue +32 -9
  43. package/app/components/02.molecules/price-list/stories/PriceList.stories.ts +125 -29
  44. package/app/components/02.molecules/price-list/tests/PriceList.spec.ts +20 -0
  45. package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +9 -4
  46. package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
  47. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
  48. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
  49. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
  50. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
  51. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +18 -0
  52. package/app/components/03.organisms/responsive-header/NavigationItems.vue +56 -18
  53. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +106 -31
  54. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +201 -18
  55. package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +21 -0
  56. package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +66 -0
  57. package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +30 -0
  58. package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +1 -0
  59. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +35 -0
  60. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +23 -0
  61. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +1 -0
  62. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +71 -0
  63. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +386 -0
  64. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +11 -0
  65. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +79 -0
  66. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +17 -0
  67. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +64 -0
  68. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +29 -0
  69. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +32 -0
  70. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +10 -0
  71. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
  72. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +36 -0
  73. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +131 -6
  74. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +9 -9
  75. package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +1 -0
  76. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
  77. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
  78. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
  79. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
  80. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -3
  81. package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
  82. package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
  83. package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
  84. package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
  85. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
  86. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
  87. package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
  88. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
  89. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
  90. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
  91. package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
  92. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
  93. package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
  94. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
  95. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
  96. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
  97. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
  98. package/app/components/03.organisms/site-header/SiteHeader.vue +8 -0
  99. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  100. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
  101. package/app/components/05.forms/input-button/InputButton.vue +3 -1
  102. package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
  103. package/app/composables/useAnchorScroll.ts +1 -1
  104. package/app/utils/opening-hours.ts +23 -2
  105. package/nuxt.config.ts +1 -1
  106. package/package.json +1 -1
  107. package/.claude/skills/components/site-navigation.md +0 -145
  108. package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
  109. package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
  110. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
  111. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
  112. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
  113. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
@@ -6,6 +6,8 @@
6
6
  <NuxtImg
7
7
  :src="serviceData.image"
8
8
  :alt="serviceData.title"
9
+ width="1920"
10
+ height="1080"
9
11
  :loading="imageLoading"
10
12
  :fetchpriority="imageFetchPriority"
11
13
  class="service-detail__hero-image"
@@ -15,18 +17,23 @@
15
17
  <template #content>
16
18
  <PageRow tag="div" :variant="heroContentVariant" class="service-detail__hero-overlay">
17
19
  <div class="service-detail__hero-content">
18
- <Breadcrumb :items="resolvedBreadcrumbItems" class="service-detail__breadcrumb" />
19
- <EyebrowText font-size="large" :text-content="serviceData.subtitle" />
20
+ <Breadcrumb
21
+ v-if="resolvedBreadcrumbItems.length"
22
+ :items="resolvedBreadcrumbItems"
23
+ :aria-label="breadcrumbAriaLabel"
24
+ class="service-detail__breadcrumb"
25
+ />
26
+ <EyebrowText v-if="serviceData.subtitle" font-size="large" :text-content="serviceData.subtitle" />
20
27
  <HeroText
28
+ v-if="serviceData.title"
21
29
  :id="headingId"
22
30
  :tag="headerTag"
23
31
  font-size="display"
24
32
  :text-content="[{ text: serviceData.title, styleClass: 'normal' }]"
25
- :style-class-passthrough="['mb-20']"
26
33
  />
27
- <div class="service-detail__hero-pills">
28
- <DisplayPill :label="serviceData.duration" size="md" variant="neutral" />
29
- <DisplayPill :label="`From ${serviceData.price}`" size="md" variant="neutral" />
34
+ <div v-if="serviceData.duration || serviceData.price" class="service-detail__hero-pills">
35
+ <DisplayPill v-if="serviceData.duration" :label="serviceData.duration" size="md" variant="neutral" />
36
+ <DisplayPill v-if="serviceData.price" :label="heroPriceLabel" size="md" variant="neutral" />
30
37
  </div>
31
38
  </div>
32
39
  </PageRow>
@@ -36,84 +43,99 @@
36
43
  <PageRow tag="div" :variant="bodyVariant" class="service-detail__body-row">
37
44
  <div class="service-detail__body">
38
45
  <div class="service-detail__main">
39
- <p class="page-body-normal">{{ serviceData.longDescription }}</p>
46
+ <p v-if="serviceData.longDescription" class="page-body-normal">{{ serviceData.longDescription }}</p>
40
47
 
41
48
  <HeroText
49
+ v-if="hasHeroHeading"
42
50
  :tag="subheadingTag"
43
51
  axis="horizontal"
44
52
  font-size="subheading"
45
53
  :text-content="serviceData.heroHeading"
46
54
  :style-class-passthrough="['mb-20']"
47
55
  />
48
- <p class="page-body-normal">{{ serviceData.whatIsIt }}</p>
56
+ <p v-if="serviceData.whatIsIt" class="page-body-normal">{{ serviceData.whatIsIt }}</p>
49
57
 
50
- <HeroText
51
- :tag="subheadingTag"
52
- axis="horizontal"
53
- font-size="subheading"
54
- :text-content="[{ text: processHeading, styleClass: 'normal' }]"
55
- :style-class-passthrough="['mb-20']"
56
- />
57
- <StepperList
58
- tag="ol"
58
+ <template v-if="serviceData.process?.length">
59
+ <HeroText
60
+ v-if="processHeading"
61
+ :tag="subheadingTag"
62
+ axis="horizontal"
63
+ font-size="subheading"
64
+ :text-content="[{ text: processHeading, styleClass: 'normal' }]"
65
+ :style-class-passthrough="['mb-20']"
66
+ />
67
+ <StepperList
68
+ tag="ol"
59
69
  indicator-alignment="top"
60
70
  indicator-variant="circle"
61
71
  :connected="true"
62
72
  :item-count="serviceData.process.length"
63
73
  class="service-detail__process"
64
74
  >
65
- <template v-for="(step, i) in serviceData.process" :key="i" #[`item-${i}`]>
66
- <p class="page-body-normal">{{ step }}</p>
67
- </template>
68
- </StepperList>
75
+ <template v-for="(step, i) in serviceData.process" :key="i" #[`item-${i}`]>
76
+ <p class="page-body-normal">{{ step }}</p>
77
+ </template>
78
+ </StepperList>
79
+ </template>
69
80
 
70
- <HeroText
71
- :tag="subheadingTag"
72
- axis="horizontal"
73
- font-size="subheading"
74
- :text-content="[{ text: idealForHeading, styleClass: 'normal' }]"
75
- :style-class-passthrough="['mb-20']"
76
- />
77
- <ul class="service-detail__ideal-for">
78
- <li v-for="(item, i) in serviceData.idealFor" :key="i" class="service-detail__ideal-for-item">
79
- <Icon name="mdi:diamond-stone" class="service-detail__ideal-for-icon" />
80
- <p class="page-body-normal">{{ item }}</p>
81
- </li>
82
- </ul>
83
-
84
- <HeroText
85
- :tag="subheadingTag"
86
- axis="horizontal"
87
- font-size="subheading"
88
- :text-content="[{ text: maintenanceHeading, styleClass: 'normal' }]"
89
- :style-class-passthrough="['mb-20']"
90
- />
91
- <p class="page-body-normal">{{ serviceData.maintenance }}</p>
81
+ <template v-if="serviceData.idealFor?.length">
82
+ <HeroText
83
+ v-if="idealForHeading"
84
+ :tag="subheadingTag"
85
+ axis="horizontal"
86
+ font-size="subheading"
87
+ :text-content="[{ text: idealForHeading, styleClass: 'normal' }]"
88
+ :style-class-passthrough="['mb-20']"
89
+ />
90
+ <ul class="service-detail__ideal-for">
91
+ <li v-for="(item, i) in serviceData.idealFor" :key="i" class="service-detail__ideal-for-item">
92
+ <Icon :name="idealForIcon" class="service-detail__ideal-for-icon" aria-hidden="true" />
93
+ <p class="page-body-normal">{{ item }}</p>
94
+ </li>
95
+ </ul>
96
+ </template>
97
+
98
+ <template v-if="serviceData.maintenance">
99
+ <HeroText
100
+ v-if="maintenanceHeading"
101
+ :tag="subheadingTag"
102
+ axis="horizontal"
103
+ font-size="subheading"
104
+ :text-content="[{ text: maintenanceHeading, styleClass: 'normal' }]"
105
+ :style-class-passthrough="['mb-20']"
106
+ />
107
+ <p class="page-body-normal">{{ serviceData.maintenance }}</p>
108
+ </template>
92
109
 
93
- <HeroText
94
- :tag="subheadingTag"
95
- axis="horizontal"
96
- font-size="subheading"
97
- :text-content="[{ text: faqsHeading, styleClass: 'normal' }]"
98
- :style-class-passthrough="['mb-20']"
99
- />
100
- <div class="service-detail__faqs">
101
- <div v-for="(faq, i) in serviceData.faqs" :key="i" class="service-detail__faq">
102
- <h3 class="service-detail__faq-question">{{ faq.question }}</h3>
103
- <p class="page-body-normal">{{ faq.answer }}</p>
110
+ <template v-if="serviceData.faqs?.length">
111
+ <HeroText
112
+ v-if="faqsHeading"
113
+ :tag="subheadingTag"
114
+ axis="horizontal"
115
+ font-size="subheading"
116
+ :text-content="[{ text: faqsHeading, styleClass: 'normal' }]"
117
+ :style-class-passthrough="['mb-20']"
118
+ />
119
+ <div class="service-detail__faqs">
120
+ <div v-for="(faq, i) in serviceData.faqs" :key="i" class="service-detail__faq">
121
+ <component :is="faqQuestionTag" v-if="faq.question" class="service-detail__faq-question">
122
+ {{ faq.question }}
123
+ </component>
124
+ <p v-if="faq.answer" class="page-body-normal">{{ faq.answer }}</p>
125
+ </div>
104
126
  </div>
105
- </div>
127
+ </template>
106
128
  </div>
107
129
 
108
130
  <aside class="service-detail__sidebar">
109
131
  <GlassPanel :style-class-passthrough="['service-detail__booking-card']">
110
- <p class="service-detail__sidebar-label">{{ bookingHeading }}</p>
132
+ <p v-if="bookingHeading" class="service-detail__sidebar-label">{{ bookingHeading }}</p>
111
133
 
112
- <div class="service-detail__sidebar-row">
134
+ <div v-if="serviceData.price" class="service-detail__sidebar-row">
113
135
  <span>{{ priceLabel }}</span>
114
136
  <span>{{ serviceData.price }}</span>
115
137
  </div>
116
- <div class="service-detail__sidebar-row">
138
+ <div v-if="serviceData.duration" class="service-detail__sidebar-row">
117
139
  <span>{{ durationLabel }}</span>
118
140
  <span>{{ serviceData.duration }}</span>
119
141
  </div>
@@ -132,12 +154,12 @@
132
154
  </GlassPanel>
133
155
 
134
156
  <div v-if="relatedServices.length" class="service-detail__related">
135
- <p class="service-detail__sidebar-label">{{ relatedServicesHeading }}</p>
157
+ <p v-if="relatedServicesHeading" class="service-detail__sidebar-label">{{ relatedServicesHeading }}</p>
136
158
 
137
159
  <div class="service-detail__related-items">
138
160
  <div
139
161
  v-for="(related, i) in relatedServices"
140
- :key="related.slug"
162
+ :key="`${i}-${related.slug}`"
141
163
  class="service-detail__related-item-slot"
142
164
  >
143
165
  <slot name="related-service" :service="related" :index="i">
@@ -145,6 +167,8 @@
145
167
  <NuxtImg
146
168
  :src="related.image"
147
169
  :alt="related.title"
170
+ width="640"
171
+ height="640"
148
172
  loading="lazy"
149
173
  class="service-detail__related-image"
150
174
  />
@@ -165,15 +189,17 @@
165
189
  <div class="service-detail__final-cta">
166
190
  <div class="service-detail__final-cta-copy">
167
191
  <HeroText
192
+ v-if="finalCtaHeading"
168
193
  :tag="subheadingTag"
169
194
  axis="horizontal"
170
195
  font-size="subheading"
171
196
  :text-content="[{ text: finalCtaHeading, styleClass: 'normal' }]"
172
- :style-class-passthrough="['mbs-0', 'mbe-8']"
173
197
  />
174
- <p class="page-body-normal">{{ finalCtaBody }}</p>
198
+ <p v-if="finalCtaBody" class="page-body-normal">{{ finalCtaBody }}</p>
199
+ </div>
200
+ <div v-if="$slots['final-cta']" class="service-detail__final-cta-action">
201
+ <slot name="final-cta" :service-data="serviceData"></slot>
175
202
  </div>
176
- <slot name="final-cta" :service-data="serviceData"></slot>
177
203
  </div>
178
204
  </PageRow>
179
205
  </component>
@@ -189,6 +215,11 @@ interface Props {
189
215
  subheadingTag?: "h2" | "h3";
190
216
  serviceData: Service;
191
217
  breadcrumbItems?: BreadcrumbItem[];
218
+ /** aria-label on the breadcrumb nav. Defaults to Breadcrumb's own label; override for localisation. */
219
+ breadcrumbAriaLabel?: string;
220
+ /** Text before the price in the hero pill, e.g. "From £95". Override for localisation; "" shows the price alone. */
221
+ pricePrefix?: string;
222
+ idealForIcon?: string;
192
223
  heroImageVariant?: "full" | "popout" | "content" | "inset-content";
193
224
  heroContentVariant?: "full" | "popout" | "content" | "inset-content";
194
225
  bodyVariant?: "full" | "popout" | "content" | "inset-content";
@@ -216,6 +247,9 @@ const props = withDefaults(defineProps<Props>(), {
216
247
  headerTag: "h1",
217
248
  subheadingTag: "h2",
218
249
  breadcrumbItems: undefined,
250
+ breadcrumbAriaLabel: undefined,
251
+ pricePrefix: "From",
252
+ idealForIcon: "mdi:diamond-stone",
219
253
  heroImageVariant: "full",
220
254
  heroContentVariant: "content",
221
255
  bodyVariant: "content",
@@ -238,12 +272,20 @@ const props = withDefaults(defineProps<Props>(), {
238
272
  styleClassPassthrough: () => [],
239
273
  });
240
274
 
241
- const { headingId, ariaLabelledby } = useAriaLabelledById(() => props.tag);
275
+ // No title means no heading to point at, so don't label the landmark from one.
276
+ const { headingId, ariaLabelledby } = useAriaLabelledById(() => (props.serviceData.title ? props.tag : "div"));
277
+
278
+ const heroPriceLabel = computed(() => `${props.pricePrefix} ${props.serviceData.price}`.trim());
279
+ const hasHeroHeading = computed(() => Boolean(props.serviceData.heroHeading?.some((item) => item.text)));
280
+ const faqQuestionTag = computed(() => (props.subheadingTag === "h2" ? "h3" : "h4"));
242
281
 
243
282
  // Falls back to a plain, non-linked breadcrumb built from the service's own category/title
244
283
  // when the consumer doesn't pass real routes — routing is always the consumer's decision.
245
284
  const resolvedBreadcrumbItems = computed<BreadcrumbItem[]>(
246
- () => props.breadcrumbItems ?? [{ label: props.serviceData.category }, { label: props.serviceData.title }]
285
+ () =>
286
+ (props.breadcrumbItems ?? [{ label: props.serviceData.category }, { label: props.serviceData.title }]).filter(
287
+ (item) => item.label
288
+ )
247
289
  );
248
290
 
249
291
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
@@ -259,20 +301,22 @@ watch(
259
301
  <style lang="css">
260
302
  @layer components {
261
303
  .service-detail {
304
+ --_clip-allowance: 0.2em;
305
+
262
306
  container-type: inline-size;
263
307
  container-name: service-detail;
264
308
 
265
309
  .service-detail__hero {
266
310
  border-radius: var(--service-detail-hero-border-radius, 0.8rem);
267
311
  overflow: hidden;
268
- height: var(--service-detail-hero-min-height-mobile, 32rem);
312
+ min-block-size: var(--service-detail-hero-min-height-mobile, 32rem);
269
313
 
270
314
  @container (width >= 768px) {
271
- height: var(--service-detail-hero-min-height-tablet, 36rem);
315
+ min-block-size: var(--service-detail-hero-min-height-tablet, 36rem);
272
316
  }
273
317
 
274
318
  @container (width >= 1024px) {
275
- height: var(--service-detail-hero-min-height-desktop, 42rem);
319
+ min-block-size: var(--service-detail-hero-min-height-desktop, 42rem);
276
320
  }
277
321
 
278
322
  .grid-stack__layer {
@@ -309,6 +353,8 @@ watch(
309
353
 
310
354
  .service-detail__hero-content {
311
355
  display: grid;
356
+ min-inline-size: 0;
357
+ overflow-wrap: anywhere;
312
358
  align-content: var(--service-detail-hero-content-align, end);
313
359
 
314
360
  .service-detail__breadcrumb {
@@ -319,6 +365,25 @@ watch(
319
365
  .eyebrow-text,
320
366
  .hero-text {
321
367
  color: var(--service-detail-hero-text-colour, white);
368
+ display: -webkit-box;
369
+ -webkit-box-orient: vertical;
370
+ overflow: hidden;
371
+ /* Room for descenders and italic overhang inside the clip, cancelled by the margins. */
372
+ padding-block-end: var(--_clip-allowance);
373
+ padding-inline-end: var(--_clip-allowance);
374
+ margin-inline-end: calc(-1 * var(--_clip-allowance));
375
+ }
376
+
377
+ .eyebrow-text {
378
+ margin-block-end: calc(-1 * var(--_clip-allowance));
379
+ -webkit-line-clamp: var(--service-detail-hero-eyebrow-line-clamp, none);
380
+ line-clamp: var(--service-detail-hero-eyebrow-line-clamp, none);
381
+ }
382
+
383
+ .hero-text {
384
+ margin-block: 2rem calc(2rem - var(--_clip-allowance));
385
+ -webkit-line-clamp: var(--service-detail-hero-title-line-clamp, none);
386
+ line-clamp: var(--service-detail-hero-title-line-clamp, none);
322
387
  }
323
388
 
324
389
  .service-detail__hero-pills {
@@ -347,6 +412,7 @@ watch(
347
412
 
348
413
  .service-detail__main {
349
414
  min-inline-size: 0;
415
+ overflow-wrap: anywhere;
350
416
  display: flex;
351
417
  flex-direction: column;
352
418
  gap: var(--service-detail-main-section-gap, 2.4rem);
@@ -378,8 +444,8 @@ watch(
378
444
  align-items: flex-start;
379
445
  gap: var(--service-detail-ideal-for-item-gap, 1rem);
380
446
  padding: var(--service-detail-ideal-for-item-padding, 1.6rem);
381
- background-color: var(--service-detail-ideal-for-item-background, var(--slate-09));
382
- border: 1px solid var(--service-detail-ideal-for-item-border-colour, var(--slate-06));
447
+ background-color: var(--service-detail-ideal-for-item-background, var(--theme-surface-subtle));
448
+ border: 1px solid var(--service-detail-ideal-for-item-border-colour, transparent);
383
449
  border-radius: var(--service-detail-ideal-for-item-border-radius, 0.4rem);
384
450
 
385
451
  .service-detail__ideal-for-icon {
@@ -392,6 +458,7 @@ watch(
392
458
 
393
459
  p {
394
460
  margin: 0;
461
+ min-inline-size: 0;
395
462
  }
396
463
  }
397
464
  }
@@ -420,6 +487,8 @@ watch(
420
487
  }
421
488
 
422
489
  .service-detail__sidebar {
490
+ min-inline-size: 0;
491
+ overflow-wrap: anywhere;
423
492
  display: flex;
424
493
  flex-direction: column;
425
494
  gap: var(--service-detail-sidebar-gap, 2rem);
@@ -428,6 +497,11 @@ watch(
428
497
 
429
498
  .service-detail__sidebar-label {
430
499
  margin: 0 0 var(--service-detail-sidebar-label-margin-block-end, 1.6rem);
500
+ display: -webkit-box;
501
+ -webkit-box-orient: vertical;
502
+ overflow: hidden;
503
+ -webkit-line-clamp: var(--service-detail-sidebar-label-line-clamp, none);
504
+ line-clamp: var(--service-detail-sidebar-label-line-clamp, none);
431
505
  font-size: var(--service-detail-sidebar-label-font-size, 1.2rem);
432
506
  text-transform: uppercase;
433
507
  letter-spacing: var(--service-detail-sidebar-label-letter-spacing, 0.05em);
@@ -451,7 +525,21 @@ watch(
451
525
  justify-content: var(--service-detail-sidebar-row-book-cta-justify-content, end);
452
526
  }
453
527
 
528
+ span:last-child {
529
+ flex: 1 1 0;
530
+ min-inline-size: 0;
531
+ text-align: end;
532
+ display: -webkit-box;
533
+ -webkit-box-orient: vertical;
534
+ overflow: hidden;
535
+ -webkit-line-clamp: var(--service-detail-sidebar-value-line-clamp, none);
536
+ line-clamp: var(--service-detail-sidebar-value-line-clamp, none);
537
+ }
538
+
454
539
  span:first-child {
540
+ flex: 0 0 auto;
541
+ max-inline-size: 50%;
542
+ overflow-wrap: break-word;
455
543
  text-transform: uppercase;
456
544
  font-size: var(--service-detail-sidebar-row-label-font-size, 1.2rem);
457
545
  opacity: var(--service-detail-muted-opacity, 0.7);
@@ -474,14 +562,14 @@ watch(
474
562
  gap: var(--service-detail-related-items-gap, 1rem);
475
563
 
476
564
  .service-detail__related-item-slot {
477
- background-color: var(--service-detail-related-item-background, var(--slate-09));
565
+ background-color: var(--service-detail-related-item-background, var(--theme-surface-subtle));
478
566
  padding: var(--service-detail-related-item-padding, 1rem);
479
- border: 1px solid var(--service-detail-related-item-border-colour, var(--slate-06));
567
+ border: 1px solid var(--service-detail-related-item-border-colour, transparent);
480
568
  border-radius: var(--service-detail-related-item-border-radius, 0.4rem);
481
569
 
482
570
  .service-detail__related-item {
483
571
  display: grid;
484
- grid-template-columns: auto 1fr;
572
+ grid-template-columns: auto minmax(0, 1fr);
485
573
  gap: var(--service-detail-related-item-gap, 1.2rem);
486
574
  align-items: center;
487
575
 
@@ -498,6 +586,14 @@ watch(
498
586
  margin: 0;
499
587
  }
500
588
 
589
+ .service-detail__related-title {
590
+ display: -webkit-box;
591
+ -webkit-box-orient: vertical;
592
+ overflow: hidden;
593
+ -webkit-line-clamp: var(--service-detail-related-title-line-clamp, none);
594
+ line-clamp: var(--service-detail-related-title-line-clamp, none);
595
+ }
596
+
501
597
  .service-detail__related-price {
502
598
  margin-block-start: var(--service-detail-related-price-margin-block-start, 0.6rem);
503
599
  font-size: var(--service-detail-related-price-font-size, 1.2rem);
@@ -523,12 +619,41 @@ watch(
523
619
  @container (width >= 700px) {
524
620
  flex-direction: row;
525
621
  align-items: center;
622
+
623
+ .service-detail__final-cta-copy {
624
+ flex: 1 1 0;
625
+ }
626
+ }
627
+
628
+ .service-detail__final-cta-action {
629
+ flex-shrink: 0;
630
+ max-inline-size: 100%;
526
631
  }
527
632
 
528
633
  .service-detail__final-cta-copy {
634
+ min-inline-size: 0;
635
+ overflow-wrap: anywhere;
636
+
637
+ .hero-text {
638
+ display: -webkit-box;
639
+ -webkit-box-orient: vertical;
640
+ overflow: hidden;
641
+ padding-block-end: var(--_clip-allowance);
642
+ padding-inline-end: var(--_clip-allowance);
643
+ margin-block: 0 calc(0.8rem - var(--_clip-allowance));
644
+ margin-inline-end: calc(-1 * var(--_clip-allowance));
645
+ -webkit-line-clamp: var(--service-detail-final-cta-heading-line-clamp, none);
646
+ line-clamp: var(--service-detail-final-cta-heading-line-clamp, none);
647
+ }
648
+
529
649
  p {
530
650
  margin: 0;
531
651
  opacity: var(--service-detail-muted-opacity, 0.7);
652
+ display: -webkit-box;
653
+ -webkit-box-orient: vertical;
654
+ overflow: hidden;
655
+ -webkit-line-clamp: var(--service-detail-final-cta-body-line-clamp, none);
656
+ line-clamp: var(--service-detail-final-cta-body-line-clamp, none);
532
657
  }
533
658
  }
534
659
  }