srcdev-nuxt-components 9.3.6 → 9.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/check-component-update.sh +30 -1
- package/.claude/skills/components/accordian-core.md +7 -7
- package/.claude/skills/components/breadcrumb.md +49 -0
- package/.claude/skills/components/content-docs.md +2 -1
- package/.claude/skills/components/expanding-panel.md +5 -5
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/responsive-header.md +1 -0
- package/.claude/skills/components/service-detail.md +156 -0
- package/.claude/skills/components/services-card-grid.md +9 -9
- package/.claude/skills/components/services-card.md +82 -30
- package/.claude/skills/index.md +3 -1
- package/.claude/skills/qa-panel.md +2 -2
- package/.vscode/srcdev-component-breadcrumb.code-snippets +14 -0
- package/.vscode/srcdev-component-service-detail.code-snippets +49 -0
- package/.vscode/srcdev-component-services-card.code-snippets +108 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +22 -16
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +32 -0
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +87 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +87 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/__snapshots__/Breadcrumb.spec.ts.snap +13 -0
- package/app/components/02.molecules/expandable/accordian/AccordianCore.vue +5 -1
- package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +22 -14
- package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +42 -28
- package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +8 -10
- package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +15 -47
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +4 -12
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +91 -4
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +5 -1
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +2 -2
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +13 -13
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +13 -13
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +181 -0
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +480 -0
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +282 -0
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +270 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +99 -0
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +129 -0
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +153 -39
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +65 -0
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +82 -0
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +13 -5
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +3 -7
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +126 -2
- package/app/components/03.organisms/services/services-grids/stories/ServicesSectionGrid.stories.ts +6 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +51 -27
- package/app/components/03.organisms/services/services-section/ServicesSection.vue +19 -0
- package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +8 -8
- package/app/types/components/breadcrumb.ts +4 -0
- package/app/types/components/index.ts +1 -0
- package/package.json +2 -2
- package/app/pages/auto-grid.vue +0 -328
- package/app/pages/banner-video.vue +0 -238
- package/app/pages/forms/examples/buttons/index.vue +0 -259
- package/app/pages/forms/examples/material/checkbox-radio-panels.vue +0 -244
- package/app/pages/forms/examples/material/cssbattle.vue +0 -60
- package/app/pages/forms/examples/material/text-fields.vue +0 -910
- package/app/pages/grid-stack.vue +0 -252
- package/app/pages/index.vue +0 -127
- package/app/pages/page-hero-highlights.vue +0 -204
- package/app/pages/samaritan.vue +0 -327
- package/app/pages/typography/hero-text.vue +0 -97
- package/app/pages/typography/index.vue +0 -39
- package/app/pages/typography/page-body.vue +0 -171
- package/app/pages/typography/page-heading.vue +0 -76
- package/app/pages/typography/page-link.vue +0 -103
- package/app/pages/ui/accordian.vue +0 -211
- package/app/pages/ui/anchor-scroll-tab-navigation.vue +0 -191
- package/app/pages/ui/anchor-scroll.vue +0 -367
- package/app/pages/ui/animated-svg-text.vue +0 -85
- package/app/pages/ui/block-decorators.vue +0 -142
- package/app/pages/ui/card-core.vue +0 -82
- package/app/pages/ui/carousel-basic.vue +0 -292
- package/app/pages/ui/carousel-flip.vue +0 -449
- package/app/pages/ui/carousel-infinite.vue +0 -260
- package/app/pages/ui/clipped-panels.vue +0 -101
- package/app/pages/ui/contact-section.vue +0 -323
- package/app/pages/ui/container-glow.vue +0 -106
- package/app/pages/ui/content-width.vue +0 -126
- package/app/pages/ui/deep-expanding-menu.vue +0 -138
- package/app/pages/ui/display-avatar.vue +0 -253
- package/app/pages/ui/display-banner.vue +0 -78
- package/app/pages/ui/display-chip.vue +0 -367
- package/app/pages/ui/display-details.vue +0 -171
- package/app/pages/ui/display-dialog.vue +0 -344
- package/app/pages/ui/display-pill.vue +0 -396
- package/app/pages/ui/display-prompt.vue +0 -284
- package/app/pages/ui/display-toast.vue +0 -311
- package/app/pages/ui/expanding-panel.vue +0 -326
- package/app/pages/ui/glowing-border.vue +0 -111
- package/app/pages/ui/layout-content-docs.vue +0 -72
- package/app/pages/ui/layout-grid-a.vue +0 -96
- package/app/pages/ui/layout-grid-b.vue +0 -132
- package/app/pages/ui/magnetic-navigation.vue +0 -50
- package/app/pages/ui/marquee-scroller.vue +0 -99
- package/app/pages/ui/mask-element.vue +0 -104
- package/app/pages/ui/masonry-columns.vue +0 -55
- package/app/pages/ui/masonry-grid-ordered-experiment.vue +0 -79
- package/app/pages/ui/masonry-grid-ordered.vue +0 -56
- package/app/pages/ui/masonry-grid-sorted.vue +0 -55
- package/app/pages/ui/masonry-grid.vue +0 -55
- package/app/pages/ui/navigation/navigation-horizontal.vue +0 -484
- package/app/pages/ui/navigation/site-navigation/about.vue +0 -36
- package/app/pages/ui/navigation/site-navigation/contact.vue +0 -39
- package/app/pages/ui/navigation/site-navigation/index.vue +0 -38
- package/app/pages/ui/navigation/site-navigation/portfolio.vue +0 -43
- package/app/pages/ui/navigation/site-navigation/services.vue +0 -42
- package/app/pages/ui/navigation/tab-navigation.vue +0 -493
- package/app/pages/ui/page-row.vue +0 -127
- package/app/pages/ui/price-list.vue +0 -65
- package/app/pages/ui/profile-section.vue +0 -142
- package/app/pages/ui/qr-code/[componentName].vue +0 -218
- package/app/pages/ui/rotating-carousel.vue +0 -154
- package/app/pages/ui/scroll-reveal-image.vue +0 -483
- package/app/pages/ui/section-parallax.vue +0 -54
- package/app/pages/ui/services/colour-finder.vue +0 -43
- package/app/pages/ui/services/services-cards.vue +0 -57
- package/app/pages/ui/services/services-section/[slug].vue +0 -61
- package/app/pages/ui/services/services-sections-compact.vue +0 -64
- package/app/pages/ui/settings.vue +0 -54
- package/app/pages/ui/simple-grid.vue +0 -55
- package/app/pages/ui/slider-gallery.vue +0 -261
- package/app/pages/ui/tabs-y.vue +0 -107
- package/app/pages/ui/tabs.vue +0 -189
- package/app/pages/ui/tooltips.vue +0 -104
- package/app/pages/ui/wipe-away-vertical.vue +0 -129
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
A collapsible panel that lets you toggle component props live on a page — without touching the component or breaking the visual layout. Uses a native `<details>`/`<summary>` so it takes up no space when collapsed.
|
|
5
|
+
A collapsible panel that lets you toggle component props live on a page — without touching the component or breaking the visual layout. Uses a native `<details>`/`<summary>` so it takes up no space when collapsed. This library no longer ships its own demo pages (`app/pages/`) — component demos live entirely in Storybook now, which has its own live prop-toggling via args/controls, so this panel isn't needed there. It's still useful in a **consuming app's own pages**: gate it behind that app's dev/staging check unless the page is genuinely demo-only.
|
|
6
6
|
|
|
7
7
|
## Structure
|
|
8
8
|
|
|
@@ -218,7 +218,7 @@ Scope inside your page body class (e.g. `.my-page`) so styles don't bleed. The p
|
|
|
218
218
|
|
|
219
219
|
## Notes
|
|
220
220
|
|
|
221
|
-
- **Consuming apps on real production traffic**: if the page hosting the panel isn't demo-only
|
|
221
|
+
- **Consuming apps on real production traffic**: if the page hosting the panel isn't demo-only, gate the panel with that app's own dev/staging check, e.g. `v-if="isDev"` with `const isDev = import.meta.dev;`. `import.meta.dev` is `false` in production builds, so the gated block is tree-shaken with no runtime cost.
|
|
222
222
|
- **Consuming apps**: The active chip color (`oklch(55% 0.18 240)`) is a neutral blue. Replace with a brand accent token if preferred: `background: var(--color-brand-accent)`.
|
|
223
223
|
- **Panel placement**: Outside any `overflow: hidden` or clipping container, otherwise the panel may be clipped or push layout unexpectedly. Placing it as a direct sibling of the component row works well.
|
|
224
224
|
- **Computed CSS vars**: When a prop controls a CSS custom property (e.g. max-height tiers), use a `computed` that returns a style object and bind it with `:style` on the component wrapper:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV Breadcrumb": {
|
|
3
|
+
"description": "Breadcrumb trail — items without a `to` render as plain current-page text",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<Breadcrumb",
|
|
7
|
+
" :items=\"[",
|
|
8
|
+
" { label: '$1Services', to: '$2/services' },",
|
|
9
|
+
" { label: '$3${TM_FILENAME_BASE}' },",
|
|
10
|
+
" ]\"",
|
|
11
|
+
"/>"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV ServiceDetail": {
|
|
3
|
+
"description": "ServiceDetail full page — hero banner, content, sticky sidebar, closing CTA",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<ServiceDetail",
|
|
7
|
+
" :service-data=\"$1service\"",
|
|
8
|
+
" location=\"$2Mobile — across Bath\"",
|
|
9
|
+
" :related-services=\"$3relatedServices\"",
|
|
10
|
+
">",
|
|
11
|
+
" <template #book-cta>",
|
|
12
|
+
" <button type=\"button\">$4Book Now</button>",
|
|
13
|
+
" </template>",
|
|
14
|
+
" <template #sidebar-note>",
|
|
15
|
+
" $5",
|
|
16
|
+
" </template>",
|
|
17
|
+
" <template #final-cta>",
|
|
18
|
+
" <button type=\"button\">$6Book Now</button>",
|
|
19
|
+
" </template>",
|
|
20
|
+
"</ServiceDetail>"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"SRCDEV ServiceDetail Custom Breadcrumb": {
|
|
24
|
+
"description": "ServiceDetail with explicit breadcrumb routes instead of the auto-built category/title trail",
|
|
25
|
+
"scope": "vue,html",
|
|
26
|
+
"body": [
|
|
27
|
+
"<ServiceDetail",
|
|
28
|
+
" :service-data=\"$1service\"",
|
|
29
|
+
" :breadcrumb-items=\"[",
|
|
30
|
+
" { label: '$2Services', to: '$3/services' },",
|
|
31
|
+
" { label: '$4${TM_FILENAME_BASE}' },",
|
|
32
|
+
" ]\"",
|
|
33
|
+
">",
|
|
34
|
+
" <template #book-cta>",
|
|
35
|
+
" <button type=\"button\">$5Book Now</button>",
|
|
36
|
+
" </template>",
|
|
37
|
+
"</ServiceDetail>"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"SRCDEV ServiceDetail Related Service Link Slot": {
|
|
41
|
+
"description": "ServiceDetail related-service scoped slot — wraps each related item in a real link",
|
|
42
|
+
"scope": "vue,html",
|
|
43
|
+
"body": [
|
|
44
|
+
"<template #related-service=\"{ service }\">",
|
|
45
|
+
" <NuxtLink :to=\"`/services/${service.slug}`\">$1{{ service.title }}</NuxtLink>",
|
|
46
|
+
"</template>"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV ServicesCard Basic": {
|
|
3
|
+
"description": "ServicesCard basic usage with actions slot",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<ServicesCard :service-data=\"$1service\">",
|
|
7
|
+
" <template #actions=\"{ serviceData }\">",
|
|
8
|
+
" <InputButtonCore",
|
|
9
|
+
" variant=\"secondary\"",
|
|
10
|
+
" :button-text=\"`More about ${serviceData.title}`\"",
|
|
11
|
+
" :href=\"`/services/${serviceData.slug}`\"",
|
|
12
|
+
" >",
|
|
13
|
+
" <template #right>",
|
|
14
|
+
" <Icon name=\"mdi:arrow-right\" class=\"icon\" />",
|
|
15
|
+
" </template>",
|
|
16
|
+
" </InputButtonCore>",
|
|
17
|
+
" </template>",
|
|
18
|
+
"</ServicesCard>"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"SRCDEV ServicesCard Whole Card Clickable": {
|
|
22
|
+
"description": "ServicesCard with href and no actions slot — whole card is clickable",
|
|
23
|
+
"scope": "vue,html",
|
|
24
|
+
"body": [
|
|
25
|
+
"<ServicesCard :service-data=\"$1service\" :href=\"`/services/${$1service.slug}`\" />"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"SRCDEV ServicesCard With Config Overrides": {
|
|
29
|
+
"description": "ServicesCard with eyebrowConfig/heroConfig overrides",
|
|
30
|
+
"scope": "vue,html",
|
|
31
|
+
"body": [
|
|
32
|
+
"<ServicesCard",
|
|
33
|
+
" :service-data=\"$1service\"",
|
|
34
|
+
" :eyebrow-config=\"{ fontSize: '$2small' }\"",
|
|
35
|
+
" :hero-config=\"{ tag: '$3h3', fontSize: '$4title' }\"",
|
|
36
|
+
">",
|
|
37
|
+
" <template #actions=\"{ serviceData }\">",
|
|
38
|
+
" <InputButtonCore variant=\"secondary\" button-text=\"$5Enquire\" :href=\"`/services/${serviceData.slug}`\" />",
|
|
39
|
+
" </template>",
|
|
40
|
+
"</ServicesCard>"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"SRCDEV ServicesCard With Custom Meta Row": {
|
|
44
|
+
"description": "ServicesCard with custom duration/price slot content (e.g. icons)",
|
|
45
|
+
"scope": "vue,html",
|
|
46
|
+
"body": [
|
|
47
|
+
"<ServicesCard :service-data=\"$1service\">",
|
|
48
|
+
" <template #duration=\"{ serviceData }\">",
|
|
49
|
+
" <Icon name=\"mdi:clock-time-four-outline\" class=\"icon\" />",
|
|
50
|
+
" <span>{{ serviceData.duration }}</span>",
|
|
51
|
+
" </template>",
|
|
52
|
+
" <template #price=\"{ serviceData }\">",
|
|
53
|
+
" <Icon name=\"mdi:currency-gbp\" class=\"icon\" />",
|
|
54
|
+
" <span>From {{ serviceData.price }}</span>",
|
|
55
|
+
" </template>",
|
|
56
|
+
" <template #actions=\"{ serviceData }\">",
|
|
57
|
+
" <InputButtonCore variant=\"secondary\" button-text=\"$2Enquire\" :href=\"`/services/${serviceData.slug}`\" />",
|
|
58
|
+
" </template>",
|
|
59
|
+
"</ServicesCard>"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
"SRCDEV ServicesCard CSS Override": {
|
|
63
|
+
"description": "CSS override scaffold for ServicesCard — scope to a page or section class. See CONSUMER-STYLING.md for the full token list.",
|
|
64
|
+
"scope": "css",
|
|
65
|
+
"body": [
|
|
66
|
+
".$1my-page {",
|
|
67
|
+
" .services-card {",
|
|
68
|
+
" /* Public tokens — could also be set globally on :root instead */",
|
|
69
|
+
" /* Interaction states — only visible in whole-card-clickable mode (is-clickable) */",
|
|
70
|
+
" --services-card-border-radius: ;",
|
|
71
|
+
" --services-card-border-width: ;",
|
|
72
|
+
" --services-card-border-colour: ;",
|
|
73
|
+
" --services-card-border-colour-hover: ;",
|
|
74
|
+
" --services-card-outline-width: ;",
|
|
75
|
+
" --services-card-outline-colour: ;",
|
|
76
|
+
" --services-card-outline-colour-hover: ;",
|
|
77
|
+
" --services-card-outline-offset: ;",
|
|
78
|
+
" --services-card-outline-offset-hover: ;",
|
|
79
|
+
"",
|
|
80
|
+
" --services-card-gap: ;",
|
|
81
|
+
" --image-wrapper-aspect-ratio: ;",
|
|
82
|
+
" --image-wrapper-border-radius: ;",
|
|
83
|
+
" --image-wrapper-padding-block: ;",
|
|
84
|
+
" --image-wrapper-padding-inline: ;",
|
|
85
|
+
" --image-wrapper-border-image-zoom-transform: ;",
|
|
86
|
+
" --details-wrapper-grid-gap: ;",
|
|
87
|
+
" --details-wrapper-padding-block: ;",
|
|
88
|
+
" --details-wrapper-padding-inline: ;",
|
|
89
|
+
" --description-padding-block: ;",
|
|
90
|
+
" --description-line-height: ;",
|
|
91
|
+
" --description-line-clamp: ;",
|
|
92
|
+
" --meta-padding-block: ;",
|
|
93
|
+
" --meta-text-transform: ;",
|
|
94
|
+
" --footer-padding-block: ;",
|
|
95
|
+
" --footer-wrapper-grid-gap: ;",
|
|
96
|
+
"",
|
|
97
|
+
" /* Local-only tokens — page-scoped override only, no :root fallback */",
|
|
98
|
+
" --_eyebrow-text-padding-block: ;",
|
|
99
|
+
" --_hero-text-padding-block: ;",
|
|
100
|
+
" --_description-text-colour: ;",
|
|
101
|
+
" --_meta-border-colour: ;",
|
|
102
|
+
" --_meta-text-colour: ;",
|
|
103
|
+
" --_meta-font-size: ;",
|
|
104
|
+
" }",
|
|
105
|
+
"}"
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
<slot name="docsContent"></slot>
|
|
6
6
|
</div>
|
|
7
7
|
<div v-if="hasDocsNav" class="docs-nav">
|
|
8
|
-
<
|
|
8
|
+
<component
|
|
9
|
+
:is="panelComponent"
|
|
9
10
|
v-model="docsNavPanelOpen"
|
|
10
11
|
:name="docsNavPanelName"
|
|
11
12
|
:animation-duration="200"
|
|
@@ -37,10 +38,11 @@
|
|
|
37
38
|
</ul>
|
|
38
39
|
</nav>
|
|
39
40
|
</template>
|
|
40
|
-
</
|
|
41
|
+
</component>
|
|
41
42
|
</div>
|
|
42
43
|
<div v-if="hasDocsPageNav" class="docs-page-nav">
|
|
43
|
-
<
|
|
44
|
+
<component
|
|
45
|
+
:is="panelComponent"
|
|
44
46
|
v-model="docsPageNavPanelOpen"
|
|
45
47
|
:name="docsPageNavPanelName"
|
|
46
48
|
:animation-duration="200"
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
</ul>
|
|
73
75
|
</nav>
|
|
74
76
|
</template>
|
|
75
|
-
</
|
|
77
|
+
</component>
|
|
76
78
|
</div>
|
|
77
79
|
</div>
|
|
78
80
|
</component>
|
|
@@ -80,6 +82,8 @@
|
|
|
80
82
|
|
|
81
83
|
<script setup lang="ts">
|
|
82
84
|
import type { DocsNavItem } from "~/types/components";
|
|
85
|
+
import ExpandingPanel from "~/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue";
|
|
86
|
+
import ExpandingPanelClassic from "~/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue";
|
|
83
87
|
|
|
84
88
|
interface Props {
|
|
85
89
|
tag?: "div" | "section" | "article" | "main";
|
|
@@ -87,6 +91,7 @@ interface Props {
|
|
|
87
91
|
docsPageNavItems?: DocsNavItem[];
|
|
88
92
|
docsNavLabel?: string;
|
|
89
93
|
docsPageNavLabel?: string;
|
|
94
|
+
panelVariant?: "modern" | "classic";
|
|
90
95
|
styleClassPassthrough?: string | string[];
|
|
91
96
|
}
|
|
92
97
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -95,9 +100,16 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
95
100
|
docsPageNavItems: () => [],
|
|
96
101
|
docsNavLabel: "Navigation",
|
|
97
102
|
docsPageNavLabel: "On this page",
|
|
103
|
+
// Defaults to "classic" — the docs/admin nav toggle relies on contentIsOnTop on mobile, and
|
|
104
|
+
// ExpandingPanel's ::details-content-based positioning silently fails on WebKit (Safari/iOS),
|
|
105
|
+
// making the nav unreachable there. See AccordianCore's own variant prop and CLAUDE.md
|
|
106
|
+
// pitfall #19 for the full mechanism. "modern" is opt-in.
|
|
107
|
+
panelVariant: "classic",
|
|
98
108
|
styleClassPassthrough: () => [],
|
|
99
109
|
});
|
|
100
110
|
|
|
111
|
+
const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
|
|
112
|
+
|
|
101
113
|
const activeNavItem = defineModel<string | undefined>("activeNavItem");
|
|
102
114
|
const activePageNavItem = defineModel<string | undefined>("activePageNavItem");
|
|
103
115
|
|
|
@@ -238,12 +250,9 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
|
|
|
238
250
|
align-items: start;
|
|
239
251
|
|
|
240
252
|
@container contentDocs (width >= 1024px) {
|
|
241
|
-
.expanding-panel
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
cursor: default;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
253
|
+
.expanding-panel-summary,
|
|
254
|
+
.expanding-panel-classic-summary {
|
|
255
|
+
cursor: default;
|
|
247
256
|
}
|
|
248
257
|
}
|
|
249
258
|
}
|
|
@@ -256,12 +265,9 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
|
|
|
256
265
|
align-items: start;
|
|
257
266
|
|
|
258
267
|
@container contentDocs (width >= 768px) {
|
|
259
|
-
.expanding-panel
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
cursor: default;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
268
|
+
.expanding-panel-summary,
|
|
269
|
+
.expanding-panel-classic-summary {
|
|
270
|
+
cursor: default;
|
|
265
271
|
}
|
|
266
272
|
}
|
|
267
273
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav class="breadcrumb" :class="[elementClasses]" aria-label="Breadcrumb">
|
|
3
|
+
<ol class="breadcrumb__list">
|
|
4
|
+
<li v-for="(item, index) in items" :key="`${item.label}-${index}`" class="breadcrumb__item">
|
|
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 === items.length - 1 ? 'page' : undefined">{{
|
|
7
|
+
item.label
|
|
8
|
+
}}</span>
|
|
9
|
+
<span v-if="index < items.length - 1" class="breadcrumb__separator" aria-hidden="true">{{ separator }}</span>
|
|
10
|
+
</li>
|
|
11
|
+
</ol>
|
|
12
|
+
</nav>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import type { BreadcrumbItem } from "~/types/components/breadcrumb";
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
items: BreadcrumbItem[];
|
|
20
|
+
separator?: string;
|
|
21
|
+
styleClassPassthrough?: string | string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
25
|
+
separator: "/",
|
|
26
|
+
styleClassPassthrough: () => [],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
30
|
+
|
|
31
|
+
watch(
|
|
32
|
+
() => props.styleClassPassthrough,
|
|
33
|
+
() => {
|
|
34
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<style lang="css">
|
|
40
|
+
@layer components {
|
|
41
|
+
.breadcrumb {
|
|
42
|
+
--_font-size: var(--breadcrumb-font-size, 1.3rem);
|
|
43
|
+
--_gap: var(--breadcrumb-gap, 0.8rem);
|
|
44
|
+
--_text-transform: var(--breadcrumb-text-transform, uppercase);
|
|
45
|
+
--_letter-spacing: var(--breadcrumb-letter-spacing, 0.05em);
|
|
46
|
+
--_colour: var(--breadcrumb-colour, currentColor);
|
|
47
|
+
--_colour-current: var(--breadcrumb-colour-current, var(--breadcrumb-colour, currentColor));
|
|
48
|
+
--_link-decoration-hover: var(--breadcrumb-link-decoration-hover, underline);
|
|
49
|
+
|
|
50
|
+
font-size: var(--_font-size);
|
|
51
|
+
text-transform: var(--_text-transform);
|
|
52
|
+
letter-spacing: var(--_letter-spacing);
|
|
53
|
+
|
|
54
|
+
.breadcrumb__list {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-wrap: wrap;
|
|
57
|
+
align-items: center;
|
|
58
|
+
gap: var(--_gap);
|
|
59
|
+
margin: 0;
|
|
60
|
+
padding: 0;
|
|
61
|
+
list-style: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.breadcrumb__item {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: var(--_gap);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.breadcrumb__link,
|
|
71
|
+
.breadcrumb__label {
|
|
72
|
+
color: var(--_colour);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.breadcrumb__label[aria-current="page"] {
|
|
76
|
+
color: var(--_colour-current);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.breadcrumb__link {
|
|
80
|
+
text-decoration: none;
|
|
81
|
+
|
|
82
|
+
&:hover,
|
|
83
|
+
&:focus-visible {
|
|
84
|
+
text-decoration: var(--_link-decoration-hover);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.breadcrumb__separator {
|
|
89
|
+
color: var(--_colour);
|
|
90
|
+
opacity: 0.6;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Breadcrumb — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
| Token | Default | Controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `--breadcrumb-font-size` | `1.3rem` | Font size of the whole trail |
|
|
8
|
+
| `--breadcrumb-gap` | `0.8rem` | Gap between an item's label and its separator, and between items |
|
|
9
|
+
| `--breadcrumb-text-transform` | `uppercase` | Text transform of the whole trail |
|
|
10
|
+
| `--breadcrumb-letter-spacing` | `0.05em` | Letter spacing of the whole trail |
|
|
11
|
+
| `--breadcrumb-colour` | `currentColor` | Colour of links, separators, and non-current labels |
|
|
12
|
+
| `--breadcrumb-colour-current` | `currentColor` | Colour of the current-page item (the one without a `to`) |
|
|
13
|
+
| `--breadcrumb-link-decoration-hover` | `underline` | `text-decoration` on link hover/focus |
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
.breadcrumb {
|
|
17
|
+
--breadcrumb-colour: white;
|
|
18
|
+
--breadcrumb-colour-current: var(--colour-text-accent);
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Text content — props, not CSS
|
|
23
|
+
|
|
24
|
+
`items` and `separator` are props. There is no hardcoded copy to override in CSS.
|
|
25
|
+
|
|
26
|
+
## Notes
|
|
27
|
+
|
|
28
|
+
- An item renders as a `NuxtLink` when it has a `to`, otherwise as `<span aria-current="page">` —
|
|
29
|
+
omit `to` on whichever item represents the current page (usually the last one).
|
|
30
|
+
- The separator (`aria-hidden="true"`) is only rendered between items, never after the last one.
|
|
31
|
+
- `--breadcrumb-colour` is what `ServiceDetail` overrides internally (to `white` by default) so
|
|
32
|
+
the breadcrumb reads over its hero image — see `ServiceDetail`'s `CONSUMER-STYLING.md`.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Breadcrumb from "../Breadcrumb.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import type { BreadcrumbItem } from "~/types/components/breadcrumb";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Breadcrumb> = {
|
|
6
|
+
title: "Atoms/Navigation/Breadcrumb",
|
|
7
|
+
component: Breadcrumb,
|
|
8
|
+
argTypes: {
|
|
9
|
+
items: {
|
|
10
|
+
control: "object",
|
|
11
|
+
description: "Ordered list of { label, to? } — items without `to` render as plain text (typically the last, current-page item)",
|
|
12
|
+
},
|
|
13
|
+
separator: {
|
|
14
|
+
control: "text",
|
|
15
|
+
description: "Character or short string rendered between items",
|
|
16
|
+
},
|
|
17
|
+
styleClassPassthrough: {
|
|
18
|
+
control: "object",
|
|
19
|
+
description: "Additional CSS classes applied to the root element",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
separator: "/",
|
|
24
|
+
styleClassPassthrough: [],
|
|
25
|
+
},
|
|
26
|
+
parameters: {
|
|
27
|
+
docs: {
|
|
28
|
+
description: {
|
|
29
|
+
component:
|
|
30
|
+
"Renders a `<nav aria-label=\"Breadcrumb\">` trail from an items array. Any item with a `to` renders as a NuxtLink; an item without one renders as plain text marked `aria-current=\"page\"` — routing decisions stay with the consumer, same as the rest of this library.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof Breadcrumb>;
|
|
38
|
+
|
|
39
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const items: BreadcrumbItem[] = [
|
|
42
|
+
{ label: "Services", to: "/services" },
|
|
43
|
+
{ label: "Balayage" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// ─── Stories ──────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {
|
|
49
|
+
args: { items },
|
|
50
|
+
render: (args) => ({
|
|
51
|
+
components: { Breadcrumb },
|
|
52
|
+
setup() {
|
|
53
|
+
return { args };
|
|
54
|
+
},
|
|
55
|
+
template: `<Breadcrumb v-bind="args" />`,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const ThreeLevels: Story = {
|
|
60
|
+
name: "Three Levels",
|
|
61
|
+
args: {
|
|
62
|
+
items: [
|
|
63
|
+
{ label: "Home", to: "/" },
|
|
64
|
+
{ label: "Services", to: "/services" },
|
|
65
|
+
{ label: "Balayage" },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
render: (args) => ({
|
|
69
|
+
components: { Breadcrumb },
|
|
70
|
+
setup() {
|
|
71
|
+
return { args };
|
|
72
|
+
},
|
|
73
|
+
template: `<Breadcrumb v-bind="args" />`,
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const CustomSeparator: Story = {
|
|
78
|
+
name: "Custom Separator",
|
|
79
|
+
args: { items, separator: ">" },
|
|
80
|
+
render: (args) => ({
|
|
81
|
+
components: { Breadcrumb },
|
|
82
|
+
setup() {
|
|
83
|
+
return { args };
|
|
84
|
+
},
|
|
85
|
+
template: `<Breadcrumb v-bind="args" />`,
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import Breadcrumb from "../Breadcrumb.vue";
|
|
4
|
+
import type { BreadcrumbItem } from "~/types/components/breadcrumb";
|
|
5
|
+
|
|
6
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const items: BreadcrumbItem[] = [
|
|
9
|
+
{ label: "Services", to: "/services" },
|
|
10
|
+
{ label: "Balayage", to: "/services/balayage" },
|
|
11
|
+
{ label: "Highlights" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe("Breadcrumb", () => {
|
|
17
|
+
it("mounts without error", async () => {
|
|
18
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items } });
|
|
19
|
+
expect(wrapper.vm).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders correct HTML structure", async () => {
|
|
23
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items } });
|
|
24
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders a nav with the Breadcrumb aria-label", async () => {
|
|
28
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items } });
|
|
29
|
+
expect(wrapper.attributes("aria-label")).toBe("Breadcrumb");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders every item's label", async () => {
|
|
33
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items } });
|
|
34
|
+
expect(wrapper.text()).toContain("Services");
|
|
35
|
+
expect(wrapper.text()).toContain("Balayage");
|
|
36
|
+
expect(wrapper.text()).toContain("Highlights");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("renders a link for items with a to prop", async () => {
|
|
40
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items } });
|
|
41
|
+
const links = wrapper.findAll("a");
|
|
42
|
+
expect(links.length).toBe(2);
|
|
43
|
+
expect(links[0]?.text()).toBe("Services");
|
|
44
|
+
expect(links[1]?.text()).toBe("Balayage");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders the last item without a to prop as plain text marked as current page", async () => {
|
|
48
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items } });
|
|
49
|
+
const current = wrapper.find('[aria-current="page"]');
|
|
50
|
+
expect(current.exists()).toBe(true);
|
|
51
|
+
expect(current.text()).toBe("Highlights");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not render a separator after the last item", async () => {
|
|
55
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items } });
|
|
56
|
+
const separators = wrapper.findAll(".breadcrumb__separator");
|
|
57
|
+
expect(separators.length).toBe(items.length - 1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("uses a custom separator", async () => {
|
|
61
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items, separator: ">" } });
|
|
62
|
+
expect(wrapper.find(".breadcrumb__separator").text()).toBe(">");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
66
|
+
const wrapper = await mountSuspended(Breadcrumb, { props: { items, styleClassPassthrough: "custom-class" } });
|
|
67
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
71
|
+
const wrapper = await mountSuspended(Breadcrumb, {
|
|
72
|
+
props: { items, styleClassPassthrough: ["class-a", "class-b"] },
|
|
73
|
+
});
|
|
74
|
+
expect(wrapper.classes()).toContain("class-a");
|
|
75
|
+
expect(wrapper.classes()).toContain("class-b");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
79
|
+
const wrapper = await mountSuspended(Breadcrumb, {
|
|
80
|
+
props: { items, styleClassPassthrough: ["original"] },
|
|
81
|
+
});
|
|
82
|
+
expect(wrapper.classes()).toContain("original");
|
|
83
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
84
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
85
|
+
expect(wrapper.classes()).toContain("updated");
|
|
86
|
+
});
|
|
87
|
+
});
|
package/app/components/01.atoms/navigation/breadcrumb/tests/__snapshots__/Breadcrumb.spec.ts.snap
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`Breadcrumb > renders correct HTML structure 1`] = `
|
|
4
|
+
"<nav class="breadcrumb" aria-label="Breadcrumb">
|
|
5
|
+
<ol class="breadcrumb__list">
|
|
6
|
+
<li class="breadcrumb__item"><a href="/services" class="breadcrumb__link">Services</a><span class="breadcrumb__separator" aria-hidden="true">/</span></li>
|
|
7
|
+
<li class="breadcrumb__item"><a href="/services/balayage" class="breadcrumb__link">Balayage</a><span class="breadcrumb__separator" aria-hidden="true">/</span></li>
|
|
8
|
+
<li class="breadcrumb__item"><span class="breadcrumb__label" aria-current="page">Highlights</span>
|
|
9
|
+
<!--v-if-->
|
|
10
|
+
</li>
|
|
11
|
+
</ol>
|
|
12
|
+
</nav>"
|
|
13
|
+
`;
|
|
@@ -34,11 +34,15 @@ interface Props {
|
|
|
34
34
|
styleClassPassthrough?: string | string[];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Defaults to "classic" (not "modern") — ExpandingPanel's animation/positioning relies on
|
|
38
|
+
// ::details-content, which WebKit (Safari/iOS) doesn't support (see ExpandingPanel's own dev-mode
|
|
39
|
+
// warning and CLAUDE.md pitfall #19). "modern" is opt-in for consumers who've confirmed it works
|
|
40
|
+
// for their audience, not the safe default.
|
|
37
41
|
const props = withDefaults(defineProps<Props>(), {
|
|
38
42
|
name: undefined,
|
|
39
43
|
itemCount: 0,
|
|
40
44
|
animationDuration: 300,
|
|
41
|
-
variant: "
|
|
45
|
+
variant: "classic",
|
|
42
46
|
styleClassPassthrough: () => [],
|
|
43
47
|
});
|
|
44
48
|
|