srcdev-nuxt-components 9.5.7 → 9.6.1
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-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -8
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/responsive-header.md +59 -0
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/service-detail.md +32 -0
- package/.claude/skills/components/service-summary.md +8 -0
- package/.claude/skills/components/services-card-grid.md +5 -3
- package/.claude/skills/components/services-card.md +22 -12
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -5
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
- package/.vscode/srcdev-component-services-card.code-snippets +38 -36
- package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
- package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
- package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
- package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-button/InputButton.vue +3 -1
- package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
- package/app/composables/useAnchorScroll.ts +1 -1
- package/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.claude/skills/components/tab-navigation.md +0 -272
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- package/app/types/components/nav-item.d.ts +0 -11
|
@@ -53,6 +53,7 @@ interface DisplayToastConfig {
|
|
|
53
53
|
title?: string // bold title line
|
|
54
54
|
description?: string // smaller description line
|
|
55
55
|
customIcon?: string // icon name override (e.g. "akar-icons:check-box")
|
|
56
|
+
dismissLabel?: string // screen-reader text for the close button — default: "Close"
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
```
|
|
@@ -111,6 +112,17 @@ const toastVisible = ref(false)
|
|
|
111
112
|
- `onBeforeRouteLeave` dismisses the toast on navigation. This emits a Vue Router warning in Vitest (no active route record) — harmless.
|
|
112
113
|
- `returnFocusTo` accepts an `HTMLElement` or a component instance with `$el`.
|
|
113
114
|
- The toast uses `<Teleport to="body">`. In tests, query `document.querySelector(".display-toast")` not `wrapper.find()`.
|
|
115
|
+
- Position, alignment and state are `data-position`, `data-alignment` (`full-width` when `fullWidth`) and `data-state="show|hide"`, not classes (changed 2026-10-08: the old bare `.top`/`.hide`/... classes could collide with a consumer's utility classes, since the toast is teleported into `<body>`).
|
|
116
|
+
- In tests, unmount every toast in `afterEach` before clearing `document.body`: a leftover instance's pending dismiss timer otherwise re-renders into the removed container (`insertBefore` of null) during a later test's timer advance.
|
|
117
|
+
|
|
118
|
+
### Auto-dismiss pause
|
|
119
|
+
|
|
120
|
+
Auto-dismiss pauses while hovered, or while keyboard focus is on or inside the toast, and resumes with
|
|
121
|
+
the remaining time (`createPausableTimeout` in `app/utils/pausableTimeout.ts`, plus
|
|
122
|
+
`focusPausesAutoDismiss`). The toast's own programmatic focus only pauses it when the root matches
|
|
123
|
+
`:focus-visible` (keyboard modality), so mouse users still get auto-dismiss. `data-paused` freezes the
|
|
124
|
+
progress bar via `animation-play-state`. In jsdom `:focus-visible` matches any focus, so tests that
|
|
125
|
+
depend on modality spy on `Element.prototype.matches` (see `DisplayToast.spec.ts`).
|
|
114
126
|
|
|
115
127
|
---
|
|
116
128
|
|
|
@@ -208,12 +220,15 @@ Stacked dismissals use a FLIP animation — remaining toasts slide smoothly to f
|
|
|
208
220
|
|
|
209
221
|
### CSS / styling
|
|
210
222
|
|
|
211
|
-
|
|
223
|
+
Both components teleport to `<body>`, so tokens set on a page or layout class don't reach them: set
|
|
224
|
+
them globally. Own tokens: `--display-toast(-provider)-max-width` (default `min(48rem, 100% - 2 * gutter)`),
|
|
225
|
+
`-progress-colour`, `-focus-ring-colour`, `-z-index`. Card styling comes from `--alert-content-*`.
|
|
226
|
+
Full list in `CONSUMER-STYLING.md`.
|
|
212
227
|
|
|
213
228
|
```css
|
|
214
|
-
|
|
215
|
-
--
|
|
216
|
-
--
|
|
229
|
+
:where(html) {
|
|
230
|
+
--display-toast-provider-max-width: min(36rem, 100% - 48px);
|
|
231
|
+
--display-toast-provider-progress-colour: var(--brand-accent);
|
|
217
232
|
}
|
|
218
233
|
```
|
|
219
234
|
|
|
@@ -234,5 +249,7 @@ The inner content layout is handled by the shared `AlertContentInner` molecule
|
|
|
234
249
|
- `useToastQueue` state persists across route changes — no `onBeforeRouteLeave` cleanup needed.
|
|
235
250
|
- `crypto.randomUUID()` generates toast IDs (`toast-<uuid>`). IDs are returned by `show()` for targeted `dismiss(id)` calls.
|
|
236
251
|
- In tests, query via `document.querySelector(".display-toast-provider-item")` — the Teleport renders outside the component wrapper.
|
|
252
|
+
- Every mounted provider reacts to the shared `useToastQueue` state, so tests must unmount providers in `afterEach`; otherwise an earlier test's provider runs its own timer for a new toast and dismisses it.
|
|
253
|
+
- Each item pauses independently (hover / keyboard focus, same rule as the standalone toast), tracked per toast id.
|
|
237
254
|
- The progress bar (`.display-toast-provider-progress`) is only rendered when `autoDismiss: true`.
|
|
238
255
|
- Dismissal can be triggered by: close button click, Escape key (when the toast item has focus), `dismiss(id)`, auto-dismiss timer, or `clear()`.
|
|
@@ -17,7 +17,7 @@ a configurable root element via the `tag` prop.
|
|
|
17
17
|
| Prop | Type | Default | Description |
|
|
18
18
|
|------|------|---------|-------------|
|
|
19
19
|
| `tag` | `"div" \| "p" \| "span" \| "section" \| "article" \| "aside" \| "header" \| "footer" \| "main" \| "nav" \| "ul" \| "ol"` | `"div"` | Root element tag. |
|
|
20
|
-
| `variant` | `"subtle" \| "vivid" \| "silver" \| "steel" \| "green"` | `"subtle"` | Glow colour variant. |
|
|
20
|
+
| `variant` | `"subtle" \| "vivid" \| "silver" \| "steel" \| "green"` | `"subtle"` | Glow colour variant, rendered as `data-variant` on the root (not a class). |
|
|
21
21
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
22
22
|
|
|
23
23
|
## Slots
|
|
@@ -36,7 +36,7 @@ a configurable root element via the `tag` prop.
|
|
|
36
36
|
|
|
37
37
|
## Styling
|
|
38
38
|
|
|
39
|
-
Border width, radius, surface colour, animation duration, and every variant's 5 gradient colour
|
|
39
|
+
Border width, radius, surface colour (defaults to `--theme-surface-subtle`), text colour (`--theme-text`), animation duration, and every variant's 5 gradient colour
|
|
40
40
|
stops are public CSS custom properties — see `CONSUMER-STYLING.md` for the full token table.
|
|
41
41
|
|
|
42
42
|
```vue
|
|
@@ -48,6 +48,14 @@ stops are public CSS custom properties — see `CONSUMER-STYLING.md` for the ful
|
|
|
48
48
|
</GlowingBorder>
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## Highlighting a single control
|
|
52
|
+
|
|
53
|
+
Wrap just an `InputButton` or `InputTextCore` so the glow becomes the control's border (a design
|
|
54
|
+
call-out for something "special"). Set `--glowing-border-radius` to the control's radius plus the
|
|
55
|
+
glow width, `--glowing-border-surface: transparent`, `--glowing-border-overflow: visible` (or the
|
|
56
|
+
control's focus ring is clipped), shrink-to-fit display, and `--input-text-border: transparent` for
|
|
57
|
+
the input. Full recipe in `CONSUMER-STYLING.md`; `HighlightedControls` story.
|
|
58
|
+
|
|
51
59
|
## Motion
|
|
52
60
|
|
|
53
61
|
The glow animation stops automatically under `prefers-reduced-motion: reduce`.
|
|
@@ -64,3 +72,13 @@ The glow animation stops automatically under `prefers-reduced-motion: reduce`.
|
|
|
64
72
|
override surface.
|
|
65
73
|
- 2026-09-20: added `green` variant (vibrant, saturated greens: `#00ff87`, `#39ff14`, `#00c853`,
|
|
66
74
|
`#64dd17`, `#00e676`), each stop overridable via `--glowing-border-green-color-{1-5}`.
|
|
75
|
+
- 2026-10-08 stress-test pass (`StressTest` story): the variant moved from a bare root class
|
|
76
|
+
(`.vivid`, `.green`...) to `data-variant`, since those names collide with common consumer
|
|
77
|
+
classes; `@keyframes glow` renamed to `glowing-border-rotate` for the same reason. Added
|
|
78
|
+
`overflow-wrap: anywhere` so long unbroken text wraps rather than being clipped by
|
|
79
|
+
`overflow: hidden`. `--glowing-border-surface` default moved from `canvas` to
|
|
80
|
+
`--theme-surface-subtle`, and a new `--glowing-border-text-colour` token defaults to
|
|
81
|
+
`--theme-text`. Stories gained story-only canvas and surface background controls with dark
|
|
82
|
+
values; the hardcoded dark stage was removed.
|
|
83
|
+
- 2026-10-08: added `--glowing-border-overflow` (default `hidden`) so a wrapped control's hover/focus
|
|
84
|
+
outlines can show, plus the `HighlightedControls` story and recipe.
|
|
@@ -66,6 +66,9 @@ Colour tokens are `--input-button-{variant}-{property}` with `--theme-*` fallbac
|
|
|
66
66
|
typography are the global `--button-*` tokens. Full reference:
|
|
67
67
|
`app/components/05.forms/input-button/CONSUMER-STYLING.md`.
|
|
68
68
|
|
|
69
|
+
Labels stay on one line by default. Set `--input-button-text-white-space: normal` to let a long
|
|
70
|
+
label wrap inside a narrow container (added 2026-10-06; `ServicesCardGrid` does this for its CTAs).
|
|
71
|
+
|
|
69
72
|
## Notes
|
|
70
73
|
|
|
71
74
|
- Before 2026-09-28, `readonly` only set `pointer-events: none`, so keyboard users could still
|
|
@@ -112,8 +112,64 @@ buggy toggle-based version too.
|
|
|
112
112
|
| `overflowMenuAriaLabel` | `string` | `"Overflow navigation menu"` | Forwarded to `NavigationItems`' `ariaLabel` — override for localisation. |
|
|
113
113
|
| `overflowButtonLabel` | `string` | `"More navigation"` | Accessible name of the overflow/burger button, which shows only icons. |
|
|
114
114
|
| `submenuAriaLabel` | `string` | `"{title} submenu"` | aria-label on each dropdown summary, top bar and overflow panel; `{title}` is replaced with `childLinksTitle` (or `name`). Forwarded to `NavigationItems`. |
|
|
115
|
+
| `anchorScrollOffset` | `number \| (() => number)` | `undefined` (0) | Pixels left above a section when a `#anchor` link scrolls to it, e.g. a sticky header's height. Pass a getter to read it at click time. |
|
|
115
116
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.navigation` element. |
|
|
116
117
|
|
|
118
|
+
## Same-page anchor links (since 2026-10-06)
|
|
119
|
+
|
|
120
|
+
Any item or child link whose `path` starts with `#` is a same-page section link:
|
|
121
|
+
|
|
122
|
+
- It renders as a plain `<a href="#section">`, not a `NuxtLink`, so Vue Router stays out of the scroll.
|
|
123
|
+
- Clicking it smooth-scrolls to the element with that id (`useAnchorScroll`), leaving
|
|
124
|
+
`anchorScrollOffset` pixels above it, and pushes the hash to the URL. Under
|
|
125
|
+
`prefers-reduced-motion: reduce` the browser's instant jump is used instead.
|
|
126
|
+
- It is active (`li.is-active`, the indicator) by hash, not by route. On load, the URL's hash wins;
|
|
127
|
+
with none, the first anchor item in the data is active. Both the top bar and the overflow menu
|
|
128
|
+
share that state.
|
|
129
|
+
- Clicking one closes any open dropdown and the overflow menu (route links close them by navigating).
|
|
130
|
+
|
|
131
|
+
Route links and anchor links can be mixed in the same data. See the "Same-Page Anchor Links" story.
|
|
132
|
+
|
|
133
|
+
## Migrating from TabNavigation
|
|
134
|
+
|
|
135
|
+
> `TabNavigation` was deprecated 2026-10-06 and **removed 2026-10-07**, along with its `NavItem` /
|
|
136
|
+
> `NavItemData` types and the `useNavCollapse` composable. Type the new data as `ResponsiveHeaderProp`.
|
|
137
|
+
|
|
138
|
+
`ResponsiveHeader` covers everything `TabNavigation` does, and collapses progressively (only the items
|
|
139
|
+
that don't fit move into the overflow menu) instead of all-or-nothing. To switch:
|
|
140
|
+
|
|
141
|
+
| TabNavigation | ResponsiveHeader |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `:nav-item-data="{ main: [...] }"` | `:responsive-nav-links="{ main: [...] }"` |
|
|
144
|
+
| item `text` | item `name` |
|
|
145
|
+
| item `href` (route or `#anchor`) | item `path` (route or `#anchor`, same behaviour) |
|
|
146
|
+
| item `iconName` | item `iconName` |
|
|
147
|
+
| item `isExternal` | drop it: `NuxtLink` treats a full URL as external |
|
|
148
|
+
| item `cssName` | no equivalent; drop it (no consumer styles these classes) |
|
|
149
|
+
| `nav-align="right"` | `--responsive-header-main-nav-justify-content: safe end` (CSS token) |
|
|
150
|
+
| `aria-label` | `main-nav-aria-label` |
|
|
151
|
+
| `open-menu-label` / `close-menu-label` | `overflow-button-label` (one label; the button is a `<summary>`) |
|
|
152
|
+
| `anchor-scroll-offset` | `anchor-scroll-offset` |
|
|
153
|
+
| collapse everything to a burger when items don't fit | default is per-item overflow; add `collapse-at-main-nav-intersection` for the old all-or-nothing burger |
|
|
154
|
+
| `--tab-nav-link-color` | `--responsive-header-link-color` |
|
|
155
|
+
| `--tab-nav-link-hover-color` / `--tab-nav-link-active-color` | `--responsive-header-link-color-hover` / `-active` (added 2026-10-07) |
|
|
156
|
+
| `--tab-nav-decorator-hovered-bg` | `--responsive-nav-decorator-hovered-bg` |
|
|
157
|
+
| `--tab-nav-decorator-indicator-color` | `--responsive-nav-decorator-indicator-color` |
|
|
158
|
+
| `--tab-nav-panel-link-color` / `-hover-color` / `-active-color` | `--overflow-nav-link-color` / `-color-hover` / `-color-active` |
|
|
159
|
+
| `--tab-nav-panel-bg` / `--tab-nav-panel-border-color` / `--tab-nav-panel-item-border` | `--responsive-header-overflow-nav-bg` / `--responsive-header-overflow-nav-border` (full shorthand) / `--overflow-nav-link-border-color` |
|
|
160
|
+
| `--tab-nav-burger-color` | `--responsive-header-overflow-btn-icon-color` |
|
|
161
|
+
|
|
162
|
+
```vue
|
|
163
|
+
<!-- Before -->
|
|
164
|
+
<TabNavigation nav-align="right" :nav-item-data="{ main: [{ text: 'About', href: '#about' }] }" />
|
|
165
|
+
|
|
166
|
+
<!-- After -->
|
|
167
|
+
<ResponsiveHeader
|
|
168
|
+
:responsive-nav-links="{ main: [{ name: 'About', path: '#about' }] }"
|
|
169
|
+
style="--responsive-header-main-nav-justify-content: safe end"
|
|
170
|
+
/>
|
|
171
|
+
```
|
|
172
|
+
|
|
117
173
|
## Data edge cases (since 2026-10-05)
|
|
118
174
|
|
|
119
175
|
- A dropdown with no `childLinksTitle` shows its `name` instead (it used to render an empty
|
|
@@ -154,9 +210,12 @@ All tokens are read via `var(--token, default)` — see the full list in the com
|
|
|
154
210
|
|---|---|---|
|
|
155
211
|
| `--responsive-header-link-font-size` | `inherit` | Nav-link font-size. **Set this to a fixed value** — see the measurement-pipeline note above for why leaving it `inherit` from a fluid ancestor is a footgun. |
|
|
156
212
|
| `--responsive-header-link-color` | `inherit` | Link/summary text colour. |
|
|
213
|
+
| `--responsive-header-link-color-hover` / `-active` | the resting colour | Per-state link colour in the top bar (added 2026-10-07). Hover falls back to the item's current state. `--overflow-nav-link-color-hover` / `-active` do the same in the overflow panel. |
|
|
157
214
|
| `--responsive-header-bg` / `--responsive-header-padding-*` / `--responsive-header-border*` | transparent / `0` / `none` | Root element theming. |
|
|
158
215
|
| `--responsive-header-overflow-btn-*` | various | Overflow burger button sizing/colour. |
|
|
159
216
|
| `--responsive-header-sub-nav-*` / `--responsive-header-overflow-nav-*` | various | Top-bar dropdown panel and overflow-panel container theming. |
|
|
217
|
+
|
|
218
|
+
Defaults are for a **light** header (changed 2026-10-07): panels use `--page-bg`, borders and hover tints are `color-mix()`es of `--theme-text`, the overflow button is transparent with a `--theme-border-focus` hover outline. They used to assume a dark bar (`Canvas`, translucent white borders and tints). A dark header sets the tokens.
|
|
160
219
|
| `--responsive-nav-decorator-indicator-color` / `--responsive-nav-decorator-hovered-*` | `currentColor` / inherits | The sliding active/hover indicator bar under the main nav. |
|
|
161
220
|
|
|
162
221
|
---
|
|
@@ -21,14 +21,18 @@ open between picks.
|
|
|
21
21
|
|
|
22
22
|
| Prop | Type | Default | Notes |
|
|
23
23
|
|---|---|---|---|
|
|
24
|
-
| `options` | `SelectMenuOption[]` | — | **Required.** `{ value: string \| number; label: string; icon?: string }[]`. |
|
|
24
|
+
| `options` | `SelectMenuOption[]` | — | **Required.** `{ value: string \| number; label: string; icon?: string; dotColor?: string }[]`. `dotColor` (any CSS colour) colours the trigger's status dot while that option is selected (`indicator="dot"`, single-select) and shows a matching dot beside the option in the list when it has no `icon`. |
|
|
25
25
|
| `label` | `string` | — | **Required.** Accessible name for the trigger + listbox (`aria-label`). Also the trigger's fallback text when nothing is selected and no `placeholder` is set — this doubles as a visible category tag, e.g. `"Choose a service"`. |
|
|
26
26
|
| `placeholder` | `string` | `undefined` | Trigger text shown when nothing is selected. Falls back to `label` when omitted. |
|
|
27
|
-
| `showIcon` | `boolean` | `true` | Show the selected option's icon
|
|
27
|
+
| `showIcon` | `boolean` | `true` | Show the leading icon in the trigger: the selected option's icon (single-select), otherwise `triggerIcon`. |
|
|
28
|
+
| `triggerIcon` | `string` | `undefined` | Fixed leading icon, e.g. a filter category icon. Shown in both modes; in single-select a selected option's own icon replaces it. |
|
|
29
|
+
| `indicator` | `"none" \| "count" \| "dot"` | `"none"` | Marker after the trigger text, visible only while something is selected; otherwise kept in the layout with `visibility: hidden` (`.select-menu-trigger-indicator-empty`) so the trigger width doesn't jump. `count` = badge with the number selected (for `multiple`); `dot` = status dot (for single-select filters, flags the filter as active). `aria-hidden`. |
|
|
30
|
+
| `selectedCountLabel` | `string` | `"{count} selected"` | Appended to the trigger's `aria-label` while the indicator shows (`"Category, 3 selected"`). `{count}` is replaced. Override for translation. |
|
|
28
31
|
| `showLabel` | `boolean` | `true` | Show the selected option's label (or placeholder/label fallback) text in the trigger. Set `false` for an icon-only compact trigger. |
|
|
29
32
|
| `showChevron` | `boolean` | `true` | Show the trailing chevron in the trigger. |
|
|
30
|
-
| `multiple` | `boolean` | `false` | Allow selecting more than one option. Each option gets a checkbox indicator, `v-model` becomes an array, and picking an option leaves the popover open so more can be toggled.
|
|
33
|
+
| `multiple` | `boolean` | `false` | Allow selecting more than one option. Each option gets a checkbox indicator, `v-model` becomes an array, and picking an option leaves the popover open so more can be toggled. No option icon in the trigger when `true` (no single option to represent); `triggerIcon` still shows. |
|
|
31
34
|
| `showSelectionInTrigger` | `boolean` | `false` | Multiple mode only. When `true`, the trigger text updates to a comma-separated list of the currently checked options instead of staying fixed on `placeholder`/`label`. No effect outside `multiple`. |
|
|
35
|
+
| `reserveLabelWidth` | `boolean` | `false` | Single-select only. Renders hidden copies of the placeholder and every option label in the same grid cell as the visible text, so the trigger is always as wide as the longest one and doesn't resize when the selection changes. Off by default, since sometimes a trigger that hugs its text is wanted. No effect with `multiple`. |
|
|
32
36
|
| `inputVariant` | `InputUiVariant` | `"normal"` | Trigger border style, same prop as `InputSelect`: `"normal"` (bordered box) or `"underlined"` (bottom border only, square corners), added as a class on the root. `"outlined"` has no CSS, same as the `05.forms` inputs. |
|
|
33
37
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root `<div>`. |
|
|
34
38
|
|
|
@@ -132,6 +136,39 @@ Set `showSelectionInTrigger` to opt out of that and have the trigger update inst
|
|
|
132
136
|
|
|
133
137
|
---
|
|
134
138
|
|
|
139
|
+
## Filter trigger: icon + count or status dot
|
|
140
|
+
|
|
141
|
+
```vue
|
|
142
|
+
<!-- Multi-select category filter: list icon, "Category", badge "3", chevron -->
|
|
143
|
+
<SelectMenu
|
|
144
|
+
v-model="categories"
|
|
145
|
+
:options="categoryOptions"
|
|
146
|
+
label="Category"
|
|
147
|
+
multiple
|
|
148
|
+
trigger-icon="lucide:list"
|
|
149
|
+
indicator="count"
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
<!-- Single-select status filter: dot appears once a status is picked, in that option's dotColor
|
|
153
|
+
statusOptions = [{ value: "active", label: "Active", dotColor: "var(--status-success)" }, ...] -->
|
|
154
|
+
<SelectMenu
|
|
155
|
+
v-model="status"
|
|
156
|
+
:options="statusOptions"
|
|
157
|
+
label="Status"
|
|
158
|
+
trigger-icon="lucide:circle-dot"
|
|
159
|
+
indicator="dot"
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The indicator is only visible while something is selected, so an unfiltered trigger looks plain,
|
|
164
|
+
but its space is always reserved so the trigger doesn't resize as the filter is set and cleared.
|
|
165
|
+
Per-option `dotColor` beats `--select-menu-trigger-dot-color`, which is the fallback (and the only
|
|
166
|
+
colour in `multiple` mode).
|
|
167
|
+
Badge/dot tokens (`--select-menu-trigger-count-*`, `--select-menu-trigger-dot-*`) are in
|
|
168
|
+
`CONSUMER-STYLING.md`.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
135
172
|
## Filter bar (multiple categories)
|
|
136
173
|
|
|
137
174
|
`SelectMenu` is single-select per instance — a faceted filter bar is built by placing one instance
|
|
@@ -167,6 +204,13 @@ Quick reference:
|
|
|
167
204
|
}
|
|
168
205
|
```
|
|
169
206
|
|
|
207
|
+
### Long content
|
|
208
|
+
|
|
209
|
+
The root caps at its container width, so a long trigger label truncates with an ellipsis. The
|
|
210
|
+
popover caps at `--select-menu-popover-max-width` (default `calc(100vw - 3.2rem)`) and option labels
|
|
211
|
+
wrap; `--select-menu-item-label-line-clamp` (default `none`) caps them, `1` for single-line
|
|
212
|
+
ellipsis. The `StressTest` story covers this at every canvas width.
|
|
213
|
+
|
|
170
214
|
---
|
|
171
215
|
|
|
172
216
|
## Notes
|
|
@@ -188,7 +232,8 @@ Quick reference:
|
|
|
188
232
|
drop the whole rule.
|
|
189
233
|
Confirmed broken on a Safari 16 iPad before this (trigger rendered, menu never opened).
|
|
190
234
|
- **Left-aligned popover** (`left: anchor(left)`) — unlike `ActionMenu` which right-aligns. Matches
|
|
191
|
-
native `<select>` dropdown behaviour. Flips above the trigger near the bottom of the viewport
|
|
235
|
+
native `<select>` dropdown behaviour. Flips above the trigger near the bottom of the viewport,
|
|
236
|
+
and to right-aligned when it would overflow the right edge (anchor-positioning browsers only).
|
|
192
237
|
- **Keyboard navigation** follows the WAI-ARIA listbox pattern: `ArrowDown`/`ArrowUp` move between
|
|
193
238
|
options (wraps around), `Home`/`End` jump to first/last, `Enter`/`Space` select the focused
|
|
194
239
|
option, `Tab` closes without stealing focus back to the trigger (matches `ActionMenu`'s
|
|
@@ -209,7 +254,7 @@ Quick reference:
|
|
|
209
254
|
weight doesn't resize the popover. Hover/focus has `-text-color-hover` and `-border-hover`, which
|
|
210
255
|
fall back to the row's *current* state (selected or resting), not always to resting. There's no
|
|
211
256
|
hover font-weight token on purpose: the width reserve only covers one alternate weight.
|
|
212
|
-
- **Chevron
|
|
257
|
+
- **Chevron flip** (`scaleY(-1)`, was `rotate(180deg)` until 2026-10-07) keys off the trigger's `aria-expanded` (from the composable's `isOpen`), not
|
|
213
258
|
`:has(...:popover-open)`, so it also works without the Popover API (changed 2026-10-05).
|
|
214
259
|
- **No native `<select>`/`appearance: base-select` involved** — this is a from-scratch popover
|
|
215
260
|
listbox, not a styled native select, so it doesn't inherit `InputSelectCore`'s WebKit
|
|
@@ -217,3 +262,7 @@ Quick reference:
|
|
|
217
262
|
- **Not a form field** — `SelectMenu` has no `fieldHasError`/`theme`/`inputVariant` props like the
|
|
218
263
|
`05.forms` inputs. Use `InputSelectCore` instead for an actual form field that needs validation
|
|
219
264
|
state and native `<select>` semantics.
|
|
265
|
+
- **2026-10-08 stress-test pass**: root `max-inline-size: 100%` (a long trigger label overflowed
|
|
266
|
+
narrow containers instead of truncating), popover `--select-menu-popover-max-width` and
|
|
267
|
+
`flip-inline` fallbacks (a long option made the menu wider than the viewport), option labels wrap
|
|
268
|
+
with a `--select-menu-item-label-line-clamp` token instead of `nowrap`.
|
|
@@ -36,6 +36,9 @@ always rendered.
|
|
|
36
36
|
| `headerTag` | `"h1" \| "h2" \| "h3"` | `"h1"` | no |
|
|
37
37
|
| `subheadingTag` | `"h2" \| "h3"` | `"h2"` | no |
|
|
38
38
|
| `breadcrumbItems` | `BreadcrumbItem[]` | auto-built from `serviceData` | no |
|
|
39
|
+
| `breadcrumbAriaLabel` | `string` | Breadcrumb's default | no |
|
|
40
|
+
| `pricePrefix` | `string` | `"From"` | no |
|
|
41
|
+
| `idealForIcon` | `string` | `"mdi:diamond-stone"` | no |
|
|
39
42
|
| `heroImageVariant` | `"full" \| "popout" \| "content" \| "inset-content"` | `"full"` | no |
|
|
40
43
|
| `heroContentVariant` | `"full" \| "popout" \| "content" \| "inset-content"` | `"content"` | no |
|
|
41
44
|
| `bodyVariant` | `"full" \| "popout" \| "content" \| "inset-content"` | `"content"` | no |
|
|
@@ -90,6 +93,35 @@ Defaults to a plain, non-linked two-item trail built from `serviceData.category`
|
|
|
90
93
|
/>
|
|
91
94
|
```
|
|
92
95
|
|
|
96
|
+
> **Changed 2026-10-06:** the hero price prefix (`pricePrefix`), breadcrumb label and ideal-for icon are
|
|
97
|
+
> props (the `From` and the icon were hardcoded). Sections with empty data, and headings set to `""`, are
|
|
98
|
+
> left out instead of rendering an orphan heading; empty fields render nothing; with no title, a landmark
|
|
99
|
+
> `tag` drops `aria-labelledby`. FAQ questions are one level below `subheadingTag` (were always `h3`).
|
|
100
|
+
> The hero tokens are now a real minimum height, so a long title grows the hero instead of being clipped.
|
|
101
|
+
> Ideal-for and related cards default to `--theme-surface-subtle` (were dark slate with no text colour).
|
|
102
|
+
> The `final-cta` slot sits in `.service-detail__final-cta-action`, which never shrinks, so long copy can't
|
|
103
|
+
> push its button out of the banner. Sidebar row labels size to their words (up to half the row); values wrap.
|
|
104
|
+
|
|
105
|
+
## Line-clamp tokens
|
|
106
|
+
|
|
107
|
+
All default to `none` (show everything). `1` is single-line ellipsis; any other number caps the lines
|
|
108
|
+
with an ellipsis on the last. Set them in the consumer's own CSS, e.g.
|
|
109
|
+
`.service-page { --service-detail-hero-title-line-clamp: 3; }`.
|
|
110
|
+
|
|
111
|
+
| Token | Clamps |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `--breadcrumb-item-line-clamp` | Each hero breadcrumb label (Breadcrumb's own token) |
|
|
114
|
+
| `--service-detail-hero-eyebrow-line-clamp` | Hero eyebrow (`subtitle`) |
|
|
115
|
+
| `--service-detail-hero-title-line-clamp` | Hero title |
|
|
116
|
+
| `--service-detail-sidebar-label-line-clamp` | "Book This Service" / "You May Also Like" labels |
|
|
117
|
+
| `--service-detail-sidebar-value-line-clamp` | Sidebar row values (price, duration, location) |
|
|
118
|
+
| `--service-detail-related-title-line-clamp` | Related service titles (default item only, not the `related-service` slot) |
|
|
119
|
+
| `--service-detail-final-cta-heading-line-clamp` | Final CTA heading |
|
|
120
|
+
| `--service-detail-final-cta-body-line-clamp` | Final CTA body |
|
|
121
|
+
|
|
122
|
+
The long-form body sections (description, process, FAQs) have no clamp on purpose: this is the page
|
|
123
|
+
people come to read in full.
|
|
124
|
+
|
|
93
125
|
## Slots
|
|
94
126
|
|
|
95
127
|
| Slot | Slot props | Purpose |
|
|
@@ -25,6 +25,7 @@ Usually consumed via [ServiceSummaryGrid](service-summary-grid.md) rather than d
|
|
|
25
25
|
| `index` | `number` | `0` | no |
|
|
26
26
|
| `alignment` | `"start" \| "center" \| "end"` | `"center"` | no |
|
|
27
27
|
| `reverse` | `boolean` | `false` | no |
|
|
28
|
+
| `pricePrefix` | `string` | `"From"` | no |
|
|
28
29
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | no |
|
|
29
30
|
|
|
30
31
|
### `index` and image loading
|
|
@@ -57,6 +58,13 @@ same pattern `ServiceDetail`'s hero uses — rather than the icon+text row the o
|
|
|
57
58
|
had. There are no icon-customisation props (`durationIcon`/`priceIcon` are gone along with the
|
|
58
59
|
icon row).
|
|
59
60
|
|
|
61
|
+
The price pill reads `` `${pricePrefix} ${price}` `` (trimmed): pass a translated `pricePrefix`, or `""`
|
|
62
|
+
for the price alone. A pill is only rendered when its value is non-empty, and the pill row is left out
|
|
63
|
+
when both are empty. Empty `subtitle`, `title` and `whatIsIt` render nothing either; with no title, a
|
|
64
|
+
landmark `tag` drops its `aria-labelledby` (there is no heading to point at). Added 2026-10-06, along
|
|
65
|
+
with `--service-summary-column-min-width` and `--service-summary-body-line-clamp`. Before, the `From`
|
|
66
|
+
prefix was hardcoded and empty fields rendered empty elements, including an empty heading.
|
|
67
|
+
|
|
60
68
|
## Row height and image aspect ratio
|
|
61
69
|
|
|
62
70
|
By default `.service-summary` has no fixed height — the row sizes to its content: the image
|
|
@@ -40,6 +40,7 @@ Set on `.services-card-grid` (or scoped to a page class):
|
|
|
40
40
|
| Token | Default | Controls |
|
|
41
41
|
| ---------------------- | --------- | ------------------------------------- |
|
|
42
42
|
| `--services-card-grid-gap` | `4rem` | Gap between grid cells |
|
|
43
|
+
| `--services-card-grid-button-text-white-space` | `normal` | CTA label wrapping; `nowrap` keeps labels on one line |
|
|
43
44
|
| `--services-card-grid-column-min-width` | `250px` | Minimum column width before wrapping |
|
|
44
45
|
|
|
45
46
|
## Consumer page boilerplate
|
|
@@ -92,9 +93,9 @@ if (servicesData.value.length === 0) {
|
|
|
92
93
|
--services-card-grid-column-min-width: 250px;
|
|
93
94
|
|
|
94
95
|
.services-card {
|
|
95
|
-
--eyebrow-
|
|
96
|
-
--
|
|
97
|
-
--description-text-colour:
|
|
96
|
+
--services-card-eyebrow-padding-block: 0.8rem 0;
|
|
97
|
+
--services-card-title-padding-block: 2rem 1rem;
|
|
98
|
+
--services-card-description-text-colour: inherit;
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
}
|
|
@@ -107,4 +108,5 @@ if (servicesData.value.length === 0) {
|
|
|
107
108
|
- The `Service` type is imported from `~/types/types.services`.
|
|
108
109
|
- Uses `repeat(auto-fit, minmax(min(var(--services-card-grid-column-min-width, 250px), 100%), 1fr))` (the `min(…, 100%)` stops a single column overflowing containers narrower than the minimum, fixed 2026-09-28) — columns grow to fill available space and wrap when below the minimum width.
|
|
109
110
|
- The `#actions` slot template is passed down into each `ServicesCard`; `serviceData` is the scoped prop for the current iteration item.
|
|
111
|
+
- CTA labels (`buttonTextPrefix` + title, trimmed) wrap inside the card since 2026-10-06; before, InputButton's `nowrap` let a long or translated title run past the card edge and get clipped.
|
|
110
112
|
- Data fetching is the page's responsibility — pass an empty array as fallback while loading (`servicesData ?? []`).
|
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
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
|
+
> **Changed 2026-10-06:** every inner class and card-specific token is now prefixed `services-card-`
|
|
8
|
+
> (`.footer` → `.services-card-footer`, `--meta-font-size` → `--services-card-meta-font-size`,
|
|
9
|
+
> `--image-wrapper-*` → `--services-card-image-*`, and so on; the full old-to-new list is in
|
|
10
|
+
> `CONSUMER-STYLING.md` under State hooks). Bare names like `.footer`, `.description` and
|
|
11
|
+
> `--meta-*` collided with consumer CSS, since the card renders inline. Also: empty `subtitle`,
|
|
12
|
+
> `title` or `shortDescription` no longer render an empty element (no empty heading); long unbroken
|
|
13
|
+
> text wraps; a long duration/price wraps onto its own line; `--services-card-description-line-clamp`
|
|
14
|
+
> defaults to `none`; new `--services-card-meta-gap`; outline hover tokens fall back to the resting ones; a whole-card link now shows a focus ring by default
|
|
15
|
+
> (`--services-card-outline-colour-focus`, falling back to the hover colour then `--theme-ring`).
|
|
16
|
+
|
|
7
17
|
## Props
|
|
8
18
|
|
|
9
19
|
| Prop | Type | Default | Required |
|
|
@@ -33,11 +43,11 @@ If an `actions` slot is provided, the card stays as a static `tag` element (no h
|
|
|
33
43
|
|
|
34
44
|
### Titles over the image (`titlesWithinImageWrapper`)
|
|
35
45
|
|
|
36
|
-
When `true`, the eyebrow and title move out of `.details
|
|
46
|
+
When `true`, the eyebrow and title move out of `.services-card-details` and sit over the bottom of the image instead. The image wrapper is a `GridStack`: the image is layer 1, and `.services-card-image-details` (eyebrow + title) is layer 2. The description, meta row and actions stay below the image.
|
|
37
47
|
|
|
38
|
-
- A gradient scrim behind the text keeps it readable. Its colour comes from `--image-
|
|
39
|
-
- Text colour uses `contrast-color()` against the scrim colour where supported (black or white, whichever reads better), falling back to white. It reads the scrim colour only, not the photo, so if you remove the scrim, set `--image-
|
|
40
|
-
- The eyebrow's gradient fill is turned off over the image; it takes the text colour (or `--image-
|
|
48
|
+
- A gradient scrim behind the text keeps it readable. Its colour comes from `--services-card-image-details-scrim-colour` (default black), and `--services-card-image-details-scrim` replaces the whole background (`none` removes it).
|
|
49
|
+
- Text colour uses `contrast-color()` against the scrim colour where supported (black or white, whichever reads better), falling back to white. It reads the scrim colour only, not the photo, so if you remove the scrim, set `--services-card-image-details-text-colour` yourself.
|
|
50
|
+
- The eyebrow's gradient fill is turned off over the image; it takes the text colour (or `--services-card-image-details-eyebrow-colour`).
|
|
41
51
|
- The eyebrow's default `fontSize` is `"medium"` in this mode (vs `"large"` below the image). `eyebrowConfig`/`heroConfig` still apply.
|
|
42
52
|
|
|
43
53
|
See "Titles over the image" in `CONSUMER-STYLING.md` for the token list.
|
|
@@ -158,10 +168,10 @@ scaffold a style block using `styleClassPassthrough` instead — see
|
|
|
158
168
|
.page-my-page {
|
|
159
169
|
.services-card {
|
|
160
170
|
--services-card-gap: 1.4rem;
|
|
161
|
-
--description-line-clamp: 3;
|
|
162
|
-
--eyebrow-
|
|
163
|
-
--
|
|
164
|
-
--meta-border-colour: var(--brand-border);
|
|
171
|
+
--services-card-description-line-clamp: 3;
|
|
172
|
+
--services-card-eyebrow-padding-block: 0.8rem 0;
|
|
173
|
+
--services-card-title-padding-block: 2rem 1rem;
|
|
174
|
+
--services-card-meta-border-colour: var(--brand-border);
|
|
165
175
|
}
|
|
166
176
|
}
|
|
167
177
|
</style>
|
|
@@ -193,9 +203,9 @@ No `actions` slot — the entire card renders as an anchor (`NuxtLink` for the i
|
|
|
193
203
|
|
|
194
204
|
- Component is auto-imported in Nuxt — no import needed.
|
|
195
205
|
- The `Service` type is imported from `~/types/types.services`.
|
|
196
|
-
- Root markup is two rows (`grid-template-rows: auto 1fr`): `.image-wrapper`, then a `.details
|
|
197
|
-
- `.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).
|
|
198
|
-
- Image has a `3/4` aspect ratio with a subtle scale-on-hover effect, triggered by hovering the image only (not the whole card). When the card is a whole-card link styled with its own hover effect (e.g. `--services-card-transform-hover`), set `--
|
|
199
|
-
- `.services-card`, `.image-wrapper`, `.details
|
|
206
|
+
- Root markup is two rows (`grid-template-rows: auto 1fr`): `.services-card-image-wrapper`, then a `.services-card-details` (`display: flex; flex-direction: column`) holding the eyebrow, title, description, and a `.services-card-footer` wrapper. Description length is controlled by `--services-card-description-line-clamp` (default `none`) rather than a fixed-height grid row.
|
|
207
|
+
- `.services-card-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 `.services-card-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 — `.services-card-meta` and `actions` are both conditional).
|
|
208
|
+
- Image has a `3/4` aspect ratio with a subtle scale-on-hover effect, triggered by hovering the image only (not the whole card). When the card is a whole-card link styled with its own hover effect (e.g. `--services-card-transform-hover`), set `--services-card-image-zoom-transform: none` so the two effects don't stack; the "Dark Overlay Cards" story does this. Keep the zoom for the `actions`-slot layout, where the card itself has no hover state. The hover is on `.services-card-image-wrapper`, not the image, so it still fires when the overlaid titles cover the image. `.services-card-image-details` is `position: relative; z-index: 1` because the image's hover `transform` would otherwise paint over it, and `.services-card-image-wrapper` sets `isolation: isolate` so that z-index stays inside the card.
|
|
209
|
+
- `.services-card`, `.services-card-image-wrapper`, `.services-card-details`, and `.services-card-footer` all set `min-inline-size: 0`. Without it, an unbreakable child — most commonly a long `actions` slot button label, since `InputButton`'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.
|
|
200
210
|
- Usually consumed via `ServicesCardGrid` rather than directly.
|
|
201
211
|
- The "Dark Overlay Cards" story shows a full restyle using tokens only: dark panel, flush image, titles over the image, hover lift (`--services-card-transform-hover`), and EyebrowText typography tokens (`--eyebrow-text-font-style`, `-letter-spacing`, etc.) set on the card so they inherit into its eyebrow.
|
|
@@ -99,6 +99,6 @@ const responsiveNavLinks = {
|
|
|
99
99
|
|
|
100
100
|
## Migration notes
|
|
101
101
|
|
|
102
|
-
- Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used.
|
|
103
|
-
- `TabNavigation`
|
|
102
|
+
- Consuming layouts previously assembling `PageRow` + `SkipLinks` + `ResponsiveHeader` (or the removed `TabNavigation`) by hand can switch to `SiteHeader` directly; the two `styleClassPassthrough` props map 1:1 onto the two independent passthrough hooks that pattern used.
|
|
103
|
+
- `TabNavigation` (removed 2026-10-07) took `NavItemData` (`{ main: [{ text, href, iconName, cssName }] }`, also removed), which is **not** the same shape as `ResponsiveHeaderProp` (`{ groupKey: [{ name, path, childLinksTitle, childLinks }] }`): migrating means reshaping the nav data, not just swapping the component. See "Migrating from TabNavigation" in `responsive-header.md`. Its `navAlign` prop has no `SiteHeader` equivalent since `ResponsiveHeader`'s layout is fixed (branding column + nav column).
|
|
104
104
|
- Requires the consuming layout's main/footer regions to carry `id="main-content"` / `id="footer-content"` — same requirement as using `SkipLinks` directly.
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
`SliderGallery` (`03.organisms/image-galleries/slider-gallery`) is a full-screen image carousel:
|
|
6
6
|
a large slide with staggered text, a thumbnail strip that animates into place, prev/next arrows, a
|
|
7
7
|
progress bar, and optional auto-advance. It's `position: absolute; inset: 0` at `100svh × 100vw`,
|
|
8
|
-
so place it inside a positioned container sized for it (usually a full-viewport hero).
|
|
8
|
+
so place it inside a positioned container sized for it (usually a full-viewport hero). To fill a
|
|
9
|
+
smaller positioned container instead, set `--slider-gallery-width: 100%` and `--slider-gallery-height: 100%`.
|
|
10
|
+
|
|
11
|
+
> **Changed 2026-10-06:** worst-case timings are clamped: `animationDuration` below 0 (or not a
|
|
12
|
+
> finite number) becomes 0, and auto-advance waits at least `max(1000, animationDuration, autoRunInterval)`
|
|
13
|
+
> ms. Before, an `autoRunInterval` at or below `animationDuration` stopped auto-advance after the first
|
|
14
|
+
> slide. Long unbroken text now wraps everywhere, and thumbnail text is clamped to 2 lines by default.
|
|
9
15
|
|
|
10
16
|
> **Changed 2026-09-28:** every inner class is now `slider-gallery-*` (was bare `.list`, `.item`,
|
|
11
17
|
> `.content`, `.title`, `.thumbnail`, `.arrows`, `.time` and so on, which collided with consumer CSS
|
|
@@ -20,8 +26,8 @@ so place it inside a positioned container sized for it (usually a full-viewport
|
|
|
20
26
|
|---|---|---|---|
|
|
21
27
|
| `v-model:gallery-data` | `IGalleryData[]` | required | Slides (see type below) |
|
|
22
28
|
| `autoRun` | `boolean` | `true` | Auto-advance. Paused while hovered or focused; never runs under `prefers-reduced-motion: reduce` |
|
|
23
|
-
| `autoRunInterval` | `number` | `7000` | ms between auto-advances |
|
|
24
|
-
| `animationDuration` | `number` | `3000` | ms a transition locks the controls (also drives the progress bar) |
|
|
29
|
+
| `autoRunInterval` | `number` | `7000` | ms between auto-advances. Never shorter than `animationDuration` or 1000ms |
|
|
30
|
+
| `animationDuration` | `number` | `3000` | ms a transition locks the controls (also drives the progress bar). Negative values become 0 |
|
|
25
31
|
| `ariaLabel` | `string` | `"Image gallery"` | Carousel region label |
|
|
26
32
|
| `loadingText` | `string` | `"Loading gallery..."` | Loading-state copy |
|
|
27
33
|
| `seeMoreText` | `string` | `"SEE MORE"` | Link text for slides with an `href` |
|
|
@@ -78,7 +84,7 @@ const slides = ref<IGalleryData[]>([
|
|
|
78
84
|
## Styling
|
|
79
85
|
|
|
80
86
|
Public tokens (`--slider-gallery-*`) cover height, z-index, accent (spinner and progress), text
|
|
81
|
-
colours, thumbnail size/border/radius/overlay, arrow row position (base, tablet, desktop), arrow size, colours, border, outline (resting, hover and focus) and CTA colours. Full reference:
|
|
87
|
+
width, colours, line clamps for each text element (`--slider-gallery-*-line-clamp`; thumbnails default to 2 lines), thumbnail size/border/radius/overlay, arrow row position (base, tablet, desktop), arrow size, colours, border, outline (resting, hover and focus) and CTA colours. Full reference:
|
|
82
88
|
`app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md`.
|
|
83
89
|
|
|
84
90
|
## Notes
|