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.
- package/.claude/skills/component-compliance-checklist.md +6 -2
- package/.claude/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/display-pill.md +2 -0
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/opening-hours.md +12 -0
- package/.claude/skills/components/price-list.md +10 -1
- package/.claude/skills/components/responsive-header.md +74 -0
- 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 -0
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tab-navigation.md +25 -9
- package/.claude/skills/index.md +1 -2
- package/.claude/skills/storybook-add-story.md +14 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +13 -1
- 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-site-header.code-snippets +3 -1
- package/.vscode/srcdev-component-tab-navigation.code-snippets +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/display-pill/DisplayPill.vue +5 -2
- 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/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +52 -11
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +55 -23
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +176 -257
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +42 -2
- package/app/components/02.molecules/opening-hours/CONSUMER-STYLING.md +11 -1
- package/app/components/02.molecules/opening-hours/OpeningHours.vue +71 -43
- package/app/components/02.molecules/opening-hours/stories/OpeningHours.stories.ts +138 -6
- package/app/components/02.molecules/opening-hours/tests/OpeningHours.spec.ts +53 -0
- package/app/components/02.molecules/opening-hours/tests/__snapshots__/OpeningHours.spec.ts.snap +1 -1
- package/app/components/02.molecules/opening-hours/tests/buildOpeningHoursSpecification.spec.ts +37 -1
- package/app/components/02.molecules/price-list/CONSUMER-STYLING.md +17 -5
- package/app/components/02.molecules/price-list/PriceList.vue +32 -9
- package/app/components/02.molecules/price-list/stories/PriceList.stories.ts +125 -29
- package/app/components/02.molecules/price-list/tests/PriceList.spec.ts +20 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +9 -4
- 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 +18 -0
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +56 -18
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +106 -31
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +201 -18
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +21 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +66 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +30 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +35 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +23 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +71 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +386 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +11 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +79 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +17 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +64 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +29 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +32 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +10 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +36 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +131 -6
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +9 -9
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +1 -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 +4 -3
- 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 +1 -1
- 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 +3 -3
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- package/app/components/03.organisms/site-header/SiteHeader.vue +8 -0
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- 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/utils/opening-hours.ts +23 -2
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- 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
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
" --services-card-outline-width: ;",
|
|
86
86
|
" --services-card-outline-colour: ;",
|
|
87
87
|
" --services-card-outline-colour-hover: ;",
|
|
88
|
+
" --services-card-outline-colour-focus: ;",
|
|
88
89
|
" --services-card-outline-offset: ;",
|
|
89
90
|
" --services-card-outline-offset-hover: ;",
|
|
90
91
|
" --services-card-background-color-hover: ;",
|
|
@@ -94,44 +95,45 @@
|
|
|
94
95
|
" --services-card-background-color: ;",
|
|
95
96
|
" --services-card-padding-block: ;",
|
|
96
97
|
" --services-card-padding-inline: ;",
|
|
97
|
-
" --image-
|
|
98
|
-
" --image-
|
|
99
|
-
" --image-
|
|
100
|
-
" --image-
|
|
101
|
-
" --
|
|
102
|
-
" --
|
|
103
|
-
" --details-
|
|
104
|
-
" --details-
|
|
105
|
-
" --eyebrow-
|
|
106
|
-
" --
|
|
107
|
-
" --description-text-colour: ;",
|
|
108
|
-
" --description-padding-block: ;",
|
|
109
|
-
" --description-line-height: ;",
|
|
110
|
-
" --description-font-weight: ;",
|
|
111
|
-
" --description-line-clamp: ;",
|
|
112
|
-
" --meta-padding-block: ;",
|
|
113
|
-
" --meta-border-colour: ;",
|
|
114
|
-
" --meta-text-colour: ;",
|
|
115
|
-
" --meta-duration-text-colour: ;",
|
|
116
|
-
" --meta-price-text-colour: ;",
|
|
117
|
-
" --meta-duration-font-weight: ;",
|
|
118
|
-
" --meta-price-font-weight: ;",
|
|
119
|
-
" --meta-letter-spacing: ;",
|
|
120
|
-
" --meta-font-size: ;",
|
|
121
|
-
" --meta-text-transform: ;",
|
|
122
|
-
" --
|
|
123
|
-
" --footer-
|
|
98
|
+
" --services-card-image-aspect-ratio: ;",
|
|
99
|
+
" --services-card-image-border-radius: ;",
|
|
100
|
+
" --services-card-image-padding-block: ;",
|
|
101
|
+
" --services-card-image-padding-inline: ;",
|
|
102
|
+
" --services-card-image-zoom-transform: ;",
|
|
103
|
+
" --services-card-details-gap: ;",
|
|
104
|
+
" --services-card-details-padding-block: ;",
|
|
105
|
+
" --services-card-details-padding-inline: ;",
|
|
106
|
+
" --services-card-eyebrow-padding-block: ;",
|
|
107
|
+
" --services-card-title-padding-block: ;",
|
|
108
|
+
" --services-card-description-text-colour: ;",
|
|
109
|
+
" --services-card-description-padding-block: ;",
|
|
110
|
+
" --services-card-description-line-height: ;",
|
|
111
|
+
" --services-card-description-font-weight: ;",
|
|
112
|
+
" --services-card-description-line-clamp: ;",
|
|
113
|
+
" --services-card-meta-padding-block: ;",
|
|
114
|
+
" --services-card-meta-border-colour: ;",
|
|
115
|
+
" --services-card-meta-text-colour: ;",
|
|
116
|
+
" --services-card-meta-duration-text-colour: ;",
|
|
117
|
+
" --services-card-meta-price-text-colour: ;",
|
|
118
|
+
" --services-card-meta-duration-font-weight: ;",
|
|
119
|
+
" --services-card-meta-price-font-weight: ;",
|
|
120
|
+
" --services-card-meta-letter-spacing: ;",
|
|
121
|
+
" --services-card-meta-font-size: ;",
|
|
122
|
+
" --services-card-meta-text-transform: ;",
|
|
123
|
+
" --services-card-meta-gap: ;",
|
|
124
|
+
" --services-card-footer-padding-block: ;",
|
|
125
|
+
" --services-card-footer-gap: ;",
|
|
124
126
|
"",
|
|
125
127
|
" /* Titles over the image (titlesWithinImageWrapper) */",
|
|
126
|
-
" --image-
|
|
127
|
-
" --image-
|
|
128
|
-
" --image-
|
|
129
|
-
" --image-
|
|
130
|
-
" --image-
|
|
131
|
-
" --image-
|
|
132
|
-
" --image-
|
|
133
|
-
" --image-
|
|
134
|
-
" --image-
|
|
128
|
+
" --services-card-image-details-scrim-colour: ;",
|
|
129
|
+
" --services-card-image-details-scrim: ;",
|
|
130
|
+
" --services-card-image-details-text-colour: ;",
|
|
131
|
+
" --services-card-image-details-eyebrow-colour: ;",
|
|
132
|
+
" --services-card-image-details-gap: ;",
|
|
133
|
+
" --services-card-image-details-eyebrow-padding-block: ;",
|
|
134
|
+
" --services-card-image-details-eyebrow-padding-inline: ;",
|
|
135
|
+
" --services-card-image-details-title-padding-block: ;",
|
|
136
|
+
" --services-card-image-details-title-padding-inline: ;",
|
|
135
137
|
" }",
|
|
136
138
|
"}"
|
|
137
139
|
]
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
".$1my-page {",
|
|
30
30
|
" --services-card-grid-column-min-width: $2;",
|
|
31
31
|
" --services-card-grid-gap: $3;",
|
|
32
|
-
" --
|
|
32
|
+
" --services-card-grid-button-text-white-space: $4;",
|
|
33
|
+
" --service-summary-grid-row-gap: $5;",
|
|
33
34
|
"}"
|
|
34
35
|
]
|
|
35
36
|
}
|
|
@@ -40,9 +40,11 @@
|
|
|
40
40
|
" main-nav-aria-label=\"${3:Main navigation}\"",
|
|
41
41
|
" secondary-nav-aria-label=\"${4:Secondary navigation}\"",
|
|
42
42
|
" overflow-menu-aria-label=\"${5:Overflow navigation menu}\"",
|
|
43
|
+
" overflow-button-label=\"${6:More navigation}\"",
|
|
44
|
+
" submenu-aria-label=\"${7:{title\\} submenu}\"",
|
|
43
45
|
">",
|
|
44
46
|
" <template #branding>",
|
|
45
|
-
" <NuxtLink to=\"/\" class=\"home-link\">$
|
|
47
|
+
" <NuxtLink to=\"/\" class=\"home-link\">$8Brand</NuxtLink>",
|
|
46
48
|
" </template>",
|
|
47
49
|
"</SiteHeader>"
|
|
48
50
|
]
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"description": "TabNavigation with a localised aria-label",
|
|
11
11
|
"scope": "vue,html",
|
|
12
12
|
"body": [
|
|
13
|
-
"<TabNavigation :nav-item-data=\"$1navData\" aria-label=\"${2:Site navigation}\" />"
|
|
13
|
+
"<TabNavigation :nav-item-data=\"$1navData\" aria-label=\"${2:Site navigation}\" open-menu-label=\"${3:Open navigation menu}\" close-menu-label=\"${4:Close navigation menu}\" />"
|
|
14
14
|
]
|
|
15
15
|
},
|
|
16
16
|
"SRCDEV TabNavigation Nav Data Script": {
|
|
@@ -73,6 +73,9 @@ keeps the same proportions. Tune a **ratio** to change spacing across all sizes,
|
|
|
73
73
|
Private tokens (not public API): `--_background`, `--_text-colour`, `--_font-size`, `--_padding-inline`,
|
|
74
74
|
`--_padding-block`, `--_padding-inline-icon` (derived from `--_icon-size`), `--_icon-size`, swapped by the size and variant classes.
|
|
75
75
|
|
|
76
|
+
> **Changed 2026-10-06**: a pill is capped at its container's width (`max-inline-size: 100%`), and a
|
|
77
|
+
> label too long for it ends in an ellipsis instead of overflowing. Short labels are unaffected.
|
|
78
|
+
|
|
76
79
|
## State hooks
|
|
77
80
|
|
|
78
81
|
- Size class: `.sm`, `.md`, `.lg`. Variant class: `.default`, `.primary`, `.success`, `.warning`,
|
|
@@ -105,6 +105,7 @@ watch(
|
|
|
105
105
|
line-height: 1;
|
|
106
106
|
white-space: nowrap;
|
|
107
107
|
width: fit-content;
|
|
108
|
+
max-inline-size: 100%;
|
|
108
109
|
cursor: default;
|
|
109
110
|
user-select: none;
|
|
110
111
|
|
|
@@ -128,8 +129,10 @@ watch(
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
.display-pill-label {
|
|
131
|
-
display:
|
|
132
|
-
|
|
132
|
+
display: block;
|
|
133
|
+
min-inline-size: 0;
|
|
134
|
+
overflow-x: clip;
|
|
135
|
+
text-overflow: ellipsis;
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
&.sm {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<nav class="breadcrumb" :class="[elementClasses]" :aria-label="ariaLabel">
|
|
2
|
+
<nav v-if="visibleItems.length" class="breadcrumb" :class="[elementClasses]" :aria-label="ariaLabel">
|
|
3
3
|
<ol class="breadcrumb__list">
|
|
4
|
-
<li v-for="(item, index) in
|
|
4
|
+
<li v-for="(item, index) in visibleItems" :key="`${item.label}-${index}`" class="breadcrumb__item">
|
|
5
5
|
<NuxtLink v-if="item.to" :to="item.to" class="breadcrumb__link">{{ item.label }}</NuxtLink>
|
|
6
|
-
<span v-else class="breadcrumb__label" :aria-current="index ===
|
|
6
|
+
<span v-else class="breadcrumb__label" :aria-current="index === visibleItems.length - 1 ? 'page' : undefined">{{
|
|
7
7
|
item.label
|
|
8
8
|
}}</span>
|
|
9
|
-
<span v-if="index <
|
|
9
|
+
<span v-if="separator && index < visibleItems.length - 1" class="breadcrumb__separator" aria-hidden="true">{{ separator }}</span>
|
|
10
10
|
</li>
|
|
11
11
|
</ol>
|
|
12
12
|
</nav>
|
|
@@ -29,6 +29,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
29
29
|
styleClassPassthrough: () => [],
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
// Blank labels would render an empty crumb with a dangling separator.
|
|
33
|
+
const visibleItems = computed(() => props.items.filter((item) => item.label?.trim()));
|
|
34
|
+
|
|
32
35
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
33
36
|
|
|
34
37
|
watch(
|
|
@@ -63,11 +66,19 @@ watch(
|
|
|
63
66
|
display: flex;
|
|
64
67
|
align-items: center;
|
|
65
68
|
gap: var(--_gap);
|
|
69
|
+
min-inline-size: 0;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
.breadcrumb__link,
|
|
69
73
|
.breadcrumb__label {
|
|
70
74
|
color: var(--_colour);
|
|
75
|
+
min-inline-size: 0;
|
|
76
|
+
overflow-wrap: anywhere;
|
|
77
|
+
display: -webkit-box;
|
|
78
|
+
-webkit-box-orient: vertical;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
-webkit-line-clamp: var(--breadcrumb-item-line-clamp, none);
|
|
81
|
+
line-clamp: var(--breadcrumb-item-line-clamp, none);
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
.breadcrumb__label[aria-current="page"] {
|
|
@@ -84,6 +95,7 @@ watch(
|
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
.breadcrumb__separator {
|
|
98
|
+
flex-shrink: 0;
|
|
87
99
|
color: var(--_colour);
|
|
88
100
|
opacity: 0.6;
|
|
89
101
|
}
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
| `--breadcrumb-text-transform` | `uppercase` | Text transform of the whole trail |
|
|
10
10
|
| `--breadcrumb-letter-spacing` | `0.05em` | Letter spacing of the whole trail |
|
|
11
11
|
| `--breadcrumb-colour` | `currentColor` | Colour of links, separators, and non-current labels |
|
|
12
|
-
| `--breadcrumb-colour-current` | `currentColor` | Colour of the current-page item (the
|
|
12
|
+
| `--breadcrumb-colour-current` | `currentColor` | Colour of the current-page item (the last item, when it has no `to`) |
|
|
13
13
|
| `--breadcrumb-link-decoration-hover` | `underline` | `text-decoration` on link hover/focus |
|
|
14
|
+
| `--breadcrumb-item-line-clamp` | `none` | Max lines of each item's label, with an ellipsis on the last. `1` is single-line ellipsis, `none` shows everything |
|
|
15
|
+
|
|
16
|
+
> Added 2026-10-06: `--breadcrumb-item-line-clamp`; long unbroken labels now wrap inside the trail.
|
|
14
17
|
|
|
15
18
|
```css
|
|
16
19
|
.breadcrumb {
|
|
@@ -21,9 +24,21 @@
|
|
|
21
24
|
|
|
22
25
|
---
|
|
23
26
|
|
|
27
|
+
## State hooks
|
|
28
|
+
|
|
29
|
+
| Hook | When |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `.breadcrumb__label[aria-current="page"]` | The last item, when it has no `to` (styled by `--breadcrumb-colour-current`) |
|
|
32
|
+
|
|
33
|
+
Inner classes: `.breadcrumb__list`, `.breadcrumb__item`, `.breadcrumb__link`, `.breadcrumb__label`,
|
|
34
|
+
`.breadcrumb__separator`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
24
38
|
## Text content — props, not CSS
|
|
25
39
|
|
|
26
|
-
`items` and `
|
|
40
|
+
`items`, `separator` and `ariaLabel` (the nav landmark's label) are props. There is no hardcoded copy to
|
|
41
|
+
override in CSS.
|
|
27
42
|
|
|
28
43
|
---
|
|
29
44
|
|
|
@@ -37,11 +52,19 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
37
52
|
|
|
38
53
|
---
|
|
39
54
|
|
|
55
|
+
## Class passthrough
|
|
56
|
+
|
|
57
|
+
`style-class-passthrough` adds classes to the root `nav.breadcrumb`. Reactive after mount.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
40
61
|
## Notes
|
|
41
62
|
|
|
42
|
-
- An item renders as a `NuxtLink` when it has a `to`, otherwise as `<span
|
|
43
|
-
omit `to` on
|
|
44
|
-
- The separator (`aria-hidden="true"`) is only rendered between items, never after the last one
|
|
63
|
+
- An item renders as a `NuxtLink` when it has a `to`, otherwise as a plain `<span>`. The last item gets
|
|
64
|
+
`aria-current="page"` when it has no `to`, so omit `to` on the current page's item.
|
|
65
|
+
- The separator (`aria-hidden="true"`) is only rendered between items, never after the last one, and
|
|
66
|
+
not at all when `separator` is `""`.
|
|
67
|
+
- Items with a blank `label` are skipped, and no `<nav>` is rendered when none are left (added 2026-10-06).
|
|
45
68
|
- `--breadcrumb-colour` is what `ServiceDetail` overrides internally (to `white` by default) so
|
|
46
69
|
the breadcrumb reads over its hero image — see `ServiceDetail`'s `CONSUMER-STYLING.md`.
|
|
47
70
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
1
2
|
import Breadcrumb from "../Breadcrumb.vue";
|
|
3
|
+
import CanvasSwitcher from "../../../canvas-switcher/CanvasSwitcher.vue";
|
|
2
4
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
5
|
import type { BreadcrumbItem } from "~/types/components/breadcrumb";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
4
7
|
|
|
5
8
|
const meta: Meta<typeof Breadcrumb> = {
|
|
6
9
|
title: "Atoms/Navigation/Breadcrumb",
|
|
@@ -14,6 +17,10 @@ const meta: Meta<typeof Breadcrumb> = {
|
|
|
14
17
|
control: "text",
|
|
15
18
|
description: "Character or short string rendered between items",
|
|
16
19
|
},
|
|
20
|
+
ariaLabel: {
|
|
21
|
+
control: "text",
|
|
22
|
+
description: "aria-label on the nav landmark; override for localisation",
|
|
23
|
+
},
|
|
17
24
|
styleClassPassthrough: {
|
|
18
25
|
control: "object",
|
|
19
26
|
description: "Additional CSS classes applied to the root element",
|
|
@@ -21,6 +28,7 @@ const meta: Meta<typeof Breadcrumb> = {
|
|
|
21
28
|
},
|
|
22
29
|
args: {
|
|
23
30
|
separator: "/",
|
|
31
|
+
ariaLabel: "Breadcrumb",
|
|
24
32
|
styleClassPassthrough: [],
|
|
25
33
|
},
|
|
26
34
|
parameters: {
|
|
@@ -85,3 +93,89 @@ export const CustomSeparator: Story = {
|
|
|
85
93
|
template: `<Breadcrumb v-bind="args" />`,
|
|
86
94
|
}),
|
|
87
95
|
};
|
|
96
|
+
|
|
97
|
+
// ─── Stress test ──────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
type LineClamp = "none" | "1" | "2" | "3";
|
|
100
|
+
|
|
101
|
+
// Story-only arg: drives a CSS token, not a Breadcrumb prop. See the argTypes entry.
|
|
102
|
+
type StressArgs = InstanceType<typeof Breadcrumb>["$props"] & { itemLineClamp: LineClamp };
|
|
103
|
+
|
|
104
|
+
const longGerman = "Haarverlängerungsbehandlungsberatungsterminvereinbarung";
|
|
105
|
+
|
|
106
|
+
const stressItems: BreadcrumbItem[] = [
|
|
107
|
+
{ label: "Startseite", to: "/" },
|
|
108
|
+
{ label: `Kategorie ${longGerman}`, to: "/kategorie" },
|
|
109
|
+
{ label: "" },
|
|
110
|
+
{ label: " " },
|
|
111
|
+
{ label: "😀 Emoji first", to: "/emoji" },
|
|
112
|
+
{ label: "<b>not bold</b>", to: "/html" },
|
|
113
|
+
{ label: "تسريحات شعر جديدة لموسم الخريف", to: "/rtl" },
|
|
114
|
+
{ label: "https://example.com/a/very/long/url/that/never/breaks/because/it/has/no/spaces", to: "/url" },
|
|
115
|
+
{ label: "A", to: "/a" },
|
|
116
|
+
{ label: `${longGerman} und ${longGerman}` },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
export const StressTest: StoryObj<StressArgs> = {
|
|
120
|
+
name: "Stress Test (Worst-Case Data)",
|
|
121
|
+
argTypes: {
|
|
122
|
+
itemLineClamp: {
|
|
123
|
+
control: "select",
|
|
124
|
+
options: ["none", "1", "2", "3"] satisfies LineClamp[],
|
|
125
|
+
description:
|
|
126
|
+
"**Story control, not a prop.** Sets the `--breadcrumb-item-line-clamp` CSS token on a wrapper so you can try it here. " +
|
|
127
|
+
"To use it in an app, set the token in your own CSS, e.g. `.page-header { --breadcrumb-item-line-clamp: 1; }`. " +
|
|
128
|
+
"`1` is single-line ellipsis, `none` (the default) shows everything.",
|
|
129
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "none" } },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
args: {
|
|
133
|
+
items: stressItems,
|
|
134
|
+
separator: "→→",
|
|
135
|
+
ariaLabel: "Navigationspfad",
|
|
136
|
+
itemLineClamp: "none",
|
|
137
|
+
},
|
|
138
|
+
render: (args) => ({
|
|
139
|
+
components: { Breadcrumb },
|
|
140
|
+
setup() {
|
|
141
|
+
const componentArgs = computed(() => {
|
|
142
|
+
const { itemLineClamp: _itemLineClamp, ...rest } = args;
|
|
143
|
+
return rest;
|
|
144
|
+
});
|
|
145
|
+
const tokenStyles = computed(() => ({ "--breadcrumb-item-line-clamp": args.itemLineClamp }));
|
|
146
|
+
return { componentArgs, tokenStyles };
|
|
147
|
+
},
|
|
148
|
+
template: `<div :style="tokenStyles"><Breadcrumb v-bind="componentArgs" /></div>`,
|
|
149
|
+
}),
|
|
150
|
+
decorators: [
|
|
151
|
+
(story, context) => ({
|
|
152
|
+
components: { story, CanvasSwitcher },
|
|
153
|
+
setup() {
|
|
154
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
155
|
+
return { canvasName };
|
|
156
|
+
},
|
|
157
|
+
template: `
|
|
158
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
159
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
160
|
+
</div>
|
|
161
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
162
|
+
<story />
|
|
163
|
+
</div>
|
|
164
|
+
`,
|
|
165
|
+
}),
|
|
166
|
+
],
|
|
167
|
+
parameters: {
|
|
168
|
+
layout: "fullscreen",
|
|
169
|
+
initialCanvas: "mobileCanvas",
|
|
170
|
+
docs: {
|
|
171
|
+
description: {
|
|
172
|
+
story:
|
|
173
|
+
"Deliberately hostile data to find breakage: ten items including a long German compound word, an unbroken URL, " +
|
|
174
|
+
"emoji, HTML-like text (must render as text), right-to-left Arabic, a single character, and two blank labels " +
|
|
175
|
+
"(skipped, with no dangling separator), plus a two-character separator and a German nav label. Check at every " +
|
|
176
|
+
"canvas width: labels wrap inside the trail, separators keep their size, and the last item is the current page. " +
|
|
177
|
+
"Try the Item line clamp control too: it\x27s a CSS token set by the story, not a prop.",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
@@ -84,4 +84,31 @@ describe("Breadcrumb", () => {
|
|
|
84
84
|
expect(wrapper.classes()).not.toContain("original");
|
|
85
85
|
expect(wrapper.classes()).toContain("updated");
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
// ─── Worst-case data ───────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
it("skips blank labels without leaving a dangling separator", async () => {
|
|
91
|
+
const wrapper = await mountSuspended(Breadcrumb, {
|
|
92
|
+
props: { items: [{ label: "Services", to: "/services" }, { label: " " }, { label: "Balayage" }, { label: "" }] },
|
|
93
|
+
});
|
|
94
|
+
expect(wrapper.findAll(".breadcrumb__item")).toHaveLength(2);
|
|
95
|
+
expect(wrapper.findAll(".breadcrumb__separator")).toHaveLength(1);
|
|
96
|
+
expect(wrapper.find(".breadcrumb__label").attributes("aria-current")).toBe("page");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("renders no nav landmark when there is nothing to show", async () => {
|
|
100
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items: [{ label: "" }] } });
|
|
101
|
+
expect(wrapper.find("nav").exists()).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("renders no separator element when separator is empty", async () => {
|
|
105
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items, separator: "" } });
|
|
106
|
+
expect(wrapper.find(".breadcrumb__separator").exists()).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders HTML-like labels as text", async () => {
|
|
110
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items: [{ label: "<b>bold</b>" }] } });
|
|
111
|
+
expect(wrapper.find(".breadcrumb__label").text()).toBe("<b>bold</b>");
|
|
112
|
+
expect(wrapper.find(".breadcrumb__label b").exists()).toBe(false);
|
|
113
|
+
});
|
|
87
114
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# TabNavigation — Consumer Styling Guide
|
|
2
2
|
|
|
3
|
+
> **Deprecated (2026-10-06):** use `ResponsiveHeader`. The token mapping is in "Migrating from
|
|
4
|
+
> TabNavigation" in `.claude/skills/components/responsive-header.md`.
|
|
5
|
+
|
|
3
6
|
## Public token API
|
|
4
7
|
|
|
5
8
|
All `--tab-nav-*` tokens are the stable override surface. Set them at any scope (global, page, or
|
|
@@ -9,32 +12,46 @@ instance) without touching the component itself.
|
|
|
9
12
|
|
|
10
13
|
| Token | Default | Controls |
|
|
11
14
|
|---|---|---|
|
|
12
|
-
| `--tab-nav-link-color` | `var(--
|
|
13
|
-
| `--tab-nav-link-hover-color` | `var(--
|
|
14
|
-
| `--tab-nav-link-active-color` |
|
|
15
|
+
| `--tab-nav-link-color` | `var(--theme-text)` | Link text colour (rest state) |
|
|
16
|
+
| `--tab-nav-link-hover-color` | `var(--theme-accent)` | Link text colour on hover |
|
|
17
|
+
| `--tab-nav-link-active-color` | the link colour | Link text colour when route is active |
|
|
15
18
|
| `--tab-nav-link-size` | `1.6rem` | Link font size |
|
|
16
19
|
| `--tab-nav-link-weight` | `400` | Link font weight |
|
|
17
20
|
| `--tab-nav-link-tracking` | `0.06em` | Link letter spacing |
|
|
18
21
|
| `--tab-nav-gap` | `2.2rem` | Gap between nav items |
|
|
19
22
|
| `--tab-nav-transition` | `250ms ease` | Colour transition on hover/active |
|
|
23
|
+
| `--tab-nav-focus-ring-width` | `2px` | Keyboard focus ring width, on bar and panel links |
|
|
24
|
+
| `--tab-nav-focus-ring-colour` | `currentColor` | Keyboard focus ring colour |
|
|
25
|
+
| `--tab-nav-focus-ring-offset` | `2px` | Focus ring gap on bar links (panel links draw it inset) |
|
|
26
|
+
|
|
27
|
+
> Changed 2026-10-06: keyboard focus now shows a ring. Before, `:focus-visible` only changed the
|
|
28
|
+
> link colour (`outline: none`).
|
|
29
|
+
|
|
30
|
+
> **Changed 2026-10-06: light defaults.** The colours used to assume a dark header (light `--slate-01`
|
|
31
|
+
> links, burger and indicator; dark `#1a1614` panel), so on a light page they were light-on-light.
|
|
32
|
+
> They now come from the light theme slots: links and burger `--theme-text`, hover and the indicator
|
|
33
|
+
> `--theme-accent`, the panel `--page-bg` (else `--theme-surface-subtle`), dividers mixed from the link
|
|
34
|
+
> colour. For a dark header, or dark mode, set the colour tokens yourself (e.g. with `light-dark()` in
|
|
35
|
+
> your own overrides); the library ships light values only. New: `--tab-nav-panel-item-hover-bg`.
|
|
20
36
|
|
|
21
37
|
### Indicator decorators
|
|
22
38
|
|
|
23
39
|
| Token | Default | Controls |
|
|
24
40
|
|---|---|---|
|
|
25
|
-
| `--tab-nav-decorator-indicator-color` | `var(--
|
|
41
|
+
| `--tab-nav-decorator-indicator-color` | `var(--theme-accent)` | Active-item underline bar colour |
|
|
26
42
|
| `--tab-nav-decorator-hovered-bg` | `transparent` | Background fill pill that follows the pointer |
|
|
27
43
|
|
|
28
44
|
### Mobile panel
|
|
29
45
|
|
|
30
46
|
| Token | Default | Controls |
|
|
31
47
|
|---|---|---|
|
|
32
|
-
| `--tab-nav-panel-bg` | `var(--page-bg,
|
|
33
|
-
| `--tab-nav-panel-border-color` |
|
|
34
|
-
| `--tab-nav-panel-item-border` |
|
|
35
|
-
| `--tab-nav-panel-
|
|
36
|
-
| `--tab-nav-panel-link-
|
|
37
|
-
| `--tab-nav-panel-link-
|
|
48
|
+
| `--tab-nav-panel-bg` | `var(--page-bg, var(--theme-surface-subtle))` | Panel background colour |
|
|
49
|
+
| `--tab-nav-panel-border-color` | the link colour at 20% | Border between nav bar and open panel |
|
|
50
|
+
| `--tab-nav-panel-item-border` | the panel link colour at 12% | Divider between panel items |
|
|
51
|
+
| `--tab-nav-panel-item-hover-bg` | the panel link colour at 5% | Panel item background on hover |
|
|
52
|
+
| `--tab-nav-panel-link-color` | the link colour | Panel link text colour |
|
|
53
|
+
| `--tab-nav-panel-link-hover-color` | the link hover colour | Panel link text colour on hover |
|
|
54
|
+
| `--tab-nav-panel-link-active-color` | the panel link colour | Panel active link colour |
|
|
38
55
|
| `--tab-nav-panel-padding-block` | `1.4rem` | Panel link vertical padding |
|
|
39
56
|
| `--tab-nav-panel-padding-inline` | `1.5rem` | Panel link horizontal padding |
|
|
40
57
|
| `--tab-nav-panel-slide-duration` | `350ms` | Panel open/close animation duration |
|
|
@@ -44,7 +61,7 @@ instance) without touching the component itself.
|
|
|
44
61
|
|
|
45
62
|
| Token | Default | Controls |
|
|
46
63
|
|---|---|---|
|
|
47
|
-
| `--tab-nav-burger-color` |
|
|
64
|
+
| `--tab-nav-burger-color` | the link colour | Burger bar colour |
|
|
48
65
|
| `--tab-nav-burger-width` | `22px` | Width of each burger bar |
|
|
49
66
|
| `--tab-nav-burger-height` | `1.5px` | Height of each burger bar |
|
|
50
67
|
| `--tab-nav-burger-gap` | `5px` | Gap between burger bars |
|
|
@@ -60,6 +77,24 @@ instance) without touching the component itself.
|
|
|
60
77
|
|
|
61
78
|
---
|
|
62
79
|
|
|
80
|
+
## State hooks
|
|
81
|
+
|
|
82
|
+
| Hook | When |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `.tab-navigation.is-collapsed` | The items don't fit, so the bar shows the burger instead |
|
|
85
|
+
| `.tab-navigation.menu-open`, `.tab-nav-burger.is-open`, `.tab-nav-panel.is-open`, `.tab-nav-backdrop.is-open` | The burger menu is open |
|
|
86
|
+
| `.tab-navigation.is-loaded` | First measurement done (the nav is hidden before it, to avoid a wrong-state flash) |
|
|
87
|
+
| `.tab-navigation.is-animated` | Indicator transitions are on (off for a frame during route changes) |
|
|
88
|
+
| `.tab-navigation--left` / `--center` / `--right` | The `navAlign` prop |
|
|
89
|
+
| `.tab-nav-list li.is-active` / `.is-hovered` | The active and hovered items (the indicators anchor to these) |
|
|
90
|
+
| `.tab-nav-link.router-link-exact-active` | Route link for the current page |
|
|
91
|
+
|
|
92
|
+
Inner classes: `.tab-nav-list`, `.tab-nav-link`, `.nav__hovered`, `.nav__active-indicator`, `.tab-nav-burger`,
|
|
93
|
+
`.burger-bar`, `.tab-nav-panel`, `.tab-nav-panel-inner`, `.tab-nav-panel-list`, `.tab-nav-panel-link`,
|
|
94
|
+
`.tab-nav-backdrop` (teleported to `<body>`). Each item's `cssName` is added to its `<li>`.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
63
98
|
## Global theming
|
|
64
99
|
|
|
65
100
|
Create `assets/styles/setup/07.components/tab-navigation.css` in the consuming app and set tokens
|
|
@@ -134,6 +169,12 @@ Override tokens for a single page by scoping them under a page wrapper. No `:dee
|
|
|
134
169
|
|
|
135
170
|
---
|
|
136
171
|
|
|
172
|
+
## Class passthrough
|
|
173
|
+
|
|
174
|
+
`style-class-passthrough` adds classes to the root `nav.tab-navigation`. Reactive after mount.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
137
178
|
## Notes
|
|
138
179
|
|
|
139
180
|
- The active underline indicator and hover pill use CSS Anchor Positioning. They animate via
|