srcdev-nuxt-components 9.4.4 → 9.4.6
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/commands/migrate-component.md +106 -0
- package/.claude/component-ledger/artifact-url.txt +1 -0
- package/.claude/component-ledger/audit.json +1 -0
- package/.claude/component-ledger/build.mjs +111 -0
- package/.claude/component-ledger/output.html +559 -0
- package/.claude/component-ledger/template.html +559 -0
- package/.claude/hooks/refresh-component-ledger.sh +32 -0
- package/.claude/settings.json +10 -0
- package/.claude/skills/components/animated-svg-text.md +78 -0
- package/.claude/skills/components/breadcrumb.md +1 -0
- package/.claude/skills/components/canvas-switcher.md +61 -0
- package/.claude/skills/components/clip-element.md +67 -0
- package/.claude/skills/components/clipped-panel.md +64 -0
- package/.claude/skills/components/content-docs.md +2 -0
- package/.claude/skills/components/deep-expanding-menu-classic.md +94 -0
- package/.claude/skills/components/deep-expanding-menu.md +92 -0
- package/.claude/skills/components/display-banner.md +60 -0
- package/.claude/skills/components/display-tooltip-defined.md +139 -0
- package/.claude/skills/components/display-tooltip.md +85 -0
- package/.claude/skills/components/marquee-scroller.md +130 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/responsive-header.md +3 -0
- package/.claude/skills/components/site-header.md +4 -0
- package/.claude/skills/components/site-navigation.md +1 -0
- package/.claude/skills/components/tab-navigation.md +1 -0
- package/.claude/skills/composable-tooltips-guide.md +10 -0
- package/.claude/skills/index.md +8 -1
- package/.claude/skills/storybook-add-story.md +30 -0
- package/.vscode/srcdev-component-animated-svg-text.code-snippets +15 -0
- package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
- package/.vscode/srcdev-component-canvas-switcher.code-snippets +9 -0
- package/.vscode/srcdev-component-clip-element.code-snippets +20 -0
- package/.vscode/srcdev-component-clipped-panel.code-snippets +23 -0
- package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
- package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +9 -0
- package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +9 -0
- package/.vscode/srcdev-component-display-banner.code-snippets +30 -0
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +29 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +38 -0
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
- package/.vscode/srcdev-component-site-header.code-snippets +17 -0
- package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
- package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
- package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
- package/app/components/01.atoms/animations/animated-svg-text/AnimatedSvgText.vue +58 -0
- package/app/components/01.atoms/animations/animated-svg-text/CONSUMER-STYLING.md +34 -0
- package/app/components/01.atoms/animations/animated-svg-text/stories/AnimatedSvgText.stories.ts +69 -0
- package/app/components/01.atoms/animations/animated-svg-text/tests/AnimatedSvgText.spec.ts +63 -0
- package/app/components/01.atoms/animations/animated-svg-text/tests/__snapshots__/AnimatedSvgText.spec.ts.snap +7 -0
- package/app/components/01.atoms/animations/clip-element/CONSUMER-STYLING.md +27 -0
- package/app/components/{clip-element → 01.atoms/animations/clip-element}/ClipElement.vue +16 -11
- package/app/components/01.atoms/animations/clip-element/stories/ClipElement.stories.ts +133 -0
- package/app/components/01.atoms/animations/clip-element/tests/ClipElement.spec.ts +70 -0
- package/app/components/01.atoms/animations/clip-element/tests/__snapshots__/ClipElement.spec.ts.snap +7 -0
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
- package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +27 -0
- package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +111 -0
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +75 -0
- package/app/components/01.atoms/canvas-switcher/tests/CanvasSwitcher.spec.ts +67 -0
- package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +39 -0
- package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +68 -0
- package/app/components/01.atoms/clipped-panel/stories/ClippedPanel.stories.ts +105 -0
- package/app/components/01.atoms/clipped-panel/tests/ClippedPanel.spec.ts +67 -0
- package/app/components/01.atoms/clipped-panel/tests/__snapshots__/ClippedPanel.spec.ts.snap +3 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
- package/app/components/01.atoms/display-banner/CONSUMER-STYLING.md +35 -0
- package/app/components/{display-banner → 01.atoms/display-banner}/DisplayBanner.vue +13 -35
- package/app/components/01.atoms/display-banner/stories/DisplayBanner.stories.ts +100 -0
- package/app/components/01.atoms/display-banner/tests/DisplayBanner.spec.ts +78 -0
- package/app/components/01.atoms/display-banner/tests/__snapshots__/DisplayBanner.spec.ts.snap +8 -0
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +62 -0
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +188 -0
- package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +94 -0
- package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +75 -0
- package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +12 -0
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
- package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +42 -0
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +116 -0
- package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +126 -0
- package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +66 -0
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +17 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +44 -0
- package/app/components/{deep-expanding-menu → 02.molecules/navigation/deep-expanding-menu}/DeepExpandingMenu.vue +53 -64
- package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +79 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +92 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +17 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +40 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +200 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +79 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +82 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +18 -0
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/types/components/display-tooltip-defined.d.ts +10 -0
- package/app/types/components/index.ts +2 -0
- package/app/types/components/marquee-scroller.d.ts +10 -0
- package/package.json +1 -1
- package/app/components/animated-svg-text/AnimatedSvgText.vue +0 -89
- package/app/components/canvas-switcher/CanvasSwitcher.vue +0 -104
- package/app/components/clipped-panels/ClippedPanel.vue +0 -87
- package/app/components/deep-expanding-menu/DeepExpandingMenuOld.vue +0 -218
- package/app/components/display-details/DisplayDetailsCore.vue +0 -122
- package/app/components/display-tooltip/DisplayTooltip.vue +0 -170
- package/app/components/display-tooltip/DisplayTooltipDefined.vue +0 -103
- package/app/components/magnetic-navigation/MagneticNavigation.vue +0 -162
- package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV MarqueeScroller Basic": {
|
|
3
|
+
"description": "MarqueeScroller — infinite horizontal logo/badge scroller with per-item slots",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<MarqueeScroller",
|
|
7
|
+
" :marquee-data=\"[",
|
|
8
|
+
" { id: 1, content: '$1logo-a' },",
|
|
9
|
+
" { id: 2, content: '$2logo-b' },",
|
|
10
|
+
" ]\"",
|
|
11
|
+
" :item-config=\"{ width: '${3:120px}', height: '${4:60px}', gap: '${5:24px}' }\"",
|
|
12
|
+
">",
|
|
13
|
+
" <template #1>",
|
|
14
|
+
" $6",
|
|
15
|
+
" </template>",
|
|
16
|
+
" <template #2>",
|
|
17
|
+
" $7",
|
|
18
|
+
" </template>",
|
|
19
|
+
"</MarqueeScroller>"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"SRCDEV MarqueeScroller With Controls": {
|
|
23
|
+
"description": "MarqueeScroller with the visible pause/play control button (WCAG 2.2.2)",
|
|
24
|
+
"scope": "vue,html",
|
|
25
|
+
"body": [
|
|
26
|
+
"<MarqueeScroller",
|
|
27
|
+
" :marquee-data=\"$1items\"",
|
|
28
|
+
" show-controls",
|
|
29
|
+
" aria-label=\"$2Scrolling client logos\"",
|
|
30
|
+
">",
|
|
31
|
+
" <template v-for=\"item in $1items\" #[item.id] :key=\"item.id\">",
|
|
32
|
+
" $3",
|
|
33
|
+
" </template>",
|
|
34
|
+
"</MarqueeScroller>"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"SRCDEV MarqueeScroller Custom Control Icon/Copy": {
|
|
38
|
+
"description": "MarqueeScroller control button with custom icons and localised aria-labels",
|
|
39
|
+
"scope": "vue,html",
|
|
40
|
+
"body": [
|
|
41
|
+
"<MarqueeScroller",
|
|
42
|
+
" :marquee-data=\"$1items\"",
|
|
43
|
+
" show-controls",
|
|
44
|
+
" play-icon=\"${2:mdi:play-circle}\"",
|
|
45
|
+
" pause-icon=\"${3:mdi:pause-circle}\"",
|
|
46
|
+
" play-label=\"${4:Play}\"",
|
|
47
|
+
" pause-label=\"${5:Pause}\"",
|
|
48
|
+
">",
|
|
49
|
+
" <template v-for=\"item in $1items\" #[item.id] :key=\"item.id\">",
|
|
50
|
+
" $6",
|
|
51
|
+
" </template>",
|
|
52
|
+
"</MarqueeScroller>"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"SRCDEV MarqueeScroller Custom Toggle Icon Slot": {
|
|
56
|
+
"description": "MarqueeScroller with the toggle-icon slot fully replacing the control button's icon",
|
|
57
|
+
"scope": "vue,html",
|
|
58
|
+
"body": [
|
|
59
|
+
"<MarqueeScroller :marquee-data=\"$1items\" show-controls>",
|
|
60
|
+
" <template #toggle-icon=\"{ isPaused }\">",
|
|
61
|
+
" $2",
|
|
62
|
+
" </template>",
|
|
63
|
+
"</MarqueeScroller>"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"SRCDEV MarqueeScroller CSS Override": {
|
|
67
|
+
"description": "CSS override scaffold for MarqueeScroller — scope to a page or section class",
|
|
68
|
+
"scope": "css",
|
|
69
|
+
"body": [
|
|
70
|
+
".$1my-page {",
|
|
71
|
+
" .marquee-scroller {",
|
|
72
|
+
" --marquee-scroller-fade-width: ;",
|
|
73
|
+
" --marquee-scroller-focus-outline-width: ;",
|
|
74
|
+
" --marquee-scroller-focus-outline-offset: ;",
|
|
75
|
+
" --marquee-scroller-item-border-colour: ;",
|
|
76
|
+
" --marquee-scroller-item-border-width: ;",
|
|
77
|
+
" --marquee-scroller-item-border-radius: ;",
|
|
78
|
+
" --marquee-scroller-item-transition-duration: ;",
|
|
79
|
+
" --marquee-scroller-group-hover-filter: ;",
|
|
80
|
+
" --marquee-scroller-item-hover-filter: ;",
|
|
81
|
+
" --marquee-scroller-control-offset: ;",
|
|
82
|
+
" --marquee-scroller-control-background-colour: ;",
|
|
83
|
+
" --marquee-scroller-control-background-colour-hover: ;",
|
|
84
|
+
" --marquee-scroller-control-text-colour: ;",
|
|
85
|
+
" --marquee-scroller-control-border-radius: ;",
|
|
86
|
+
" --marquee-scroller-control-border-width: ;",
|
|
87
|
+
" --marquee-scroller-control-padding: ;",
|
|
88
|
+
" --marquee-scroller-control-font-size: ;",
|
|
89
|
+
" }",
|
|
90
|
+
"}"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -15,6 +15,18 @@
|
|
|
15
15
|
"</ResponsiveHeader>"
|
|
16
16
|
]
|
|
17
17
|
},
|
|
18
|
+
"SRCDEV ResponsiveHeader Localised Nav Labels": {
|
|
19
|
+
"description": "ResponsiveHeader with localised aria-labels on the main/secondary nav landmarks and overflow menu",
|
|
20
|
+
"scope": "vue,html",
|
|
21
|
+
"body": [
|
|
22
|
+
"<ResponsiveHeader",
|
|
23
|
+
" :responsive-nav-links=\"$1responsiveNavLinks\"",
|
|
24
|
+
" main-nav-aria-label=\"${2:Main navigation}\"",
|
|
25
|
+
" secondary-nav-aria-label=\"${3:Secondary navigation}\"",
|
|
26
|
+
" overflow-menu-aria-label=\"${4:Overflow navigation menu}\"",
|
|
27
|
+
"/>"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
18
30
|
"SRCDEV ResponsiveHeader Nav Data (with dropdown)": {
|
|
19
31
|
"description": "responsiveNavLinks data with a dropdown group",
|
|
20
32
|
"scope": "typescript,vue",
|
|
@@ -30,6 +30,23 @@
|
|
|
30
30
|
"</SiteHeader>"
|
|
31
31
|
]
|
|
32
32
|
},
|
|
33
|
+
"SRCDEV SiteHeader Localised Nav Labels": {
|
|
34
|
+
"description": "SiteHeader with localised aria-labels on all its nav landmarks (home, main, secondary, overflow)",
|
|
35
|
+
"scope": "vue,html",
|
|
36
|
+
"body": [
|
|
37
|
+
"<SiteHeader",
|
|
38
|
+
" :responsive-nav-links=\"$1responsiveNavLinks\"",
|
|
39
|
+
" home-nav-aria-label=\"${2:Home Navigation}\"",
|
|
40
|
+
" main-nav-aria-label=\"${3:Main navigation}\"",
|
|
41
|
+
" secondary-nav-aria-label=\"${4:Secondary navigation}\"",
|
|
42
|
+
" overflow-menu-aria-label=\"${5:Overflow navigation menu}\"",
|
|
43
|
+
">",
|
|
44
|
+
" <template #branding>",
|
|
45
|
+
" <NuxtLink to=\"/\" class=\"home-link\">$6Brand</NuxtLink>",
|
|
46
|
+
" </template>",
|
|
47
|
+
"</SiteHeader>"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
33
50
|
"SRCDEV SiteHeader Nav Data (flat)": {
|
|
34
51
|
"description": "Simple flat responsiveNavLinks data with no dropdowns, for a few-page site",
|
|
35
52
|
"scope": "typescript,vue",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV SiteNavigation Basic": {
|
|
3
|
+
"description": "SiteNavigation horizontal nav with sliding active/hover indicator",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<SiteNavigation :nav-item-data=\"$1navData\" nav-align=\"${2:left}\" />"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"SRCDEV SiteNavigation Localised": {
|
|
10
|
+
"description": "SiteNavigation with a localised aria-label",
|
|
11
|
+
"scope": "vue,html",
|
|
12
|
+
"body": [
|
|
13
|
+
"<SiteNavigation :nav-item-data=\"$1navData\" aria-label=\"${2:Site navigation}\" />"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"SRCDEV SiteNavigation Nav Data Script": {
|
|
17
|
+
"description": "navItemData for SiteNavigation",
|
|
18
|
+
"scope": "typescript,vue",
|
|
19
|
+
"body": [
|
|
20
|
+
"import type { NavItemData } from \"srcdev-nuxt-components\";",
|
|
21
|
+
"",
|
|
22
|
+
"const $1navData: NavItemData = {",
|
|
23
|
+
" main: [",
|
|
24
|
+
" { text: \"$2Home\", href: \"$3/\" },",
|
|
25
|
+
" { text: \"$4About\", href: \"$5/about\" },",
|
|
26
|
+
" ],",
|
|
27
|
+
"};"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV SliderGallery Basic": {
|
|
3
|
+
"description": "SliderGallery full-screen image slider with thumbnails and prev/next arrows",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<SliderGallery v-model:gallery-data=\"$1galleryData\" />"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"SRCDEV SliderGallery Localised": {
|
|
10
|
+
"description": "SliderGallery with localised loading/CTA/arrow copy",
|
|
11
|
+
"scope": "vue,html",
|
|
12
|
+
"body": [
|
|
13
|
+
"<SliderGallery",
|
|
14
|
+
" v-model:gallery-data=\"$1galleryData\"",
|
|
15
|
+
" loading-text=\"${2:Loading gallery...}\"",
|
|
16
|
+
" see-more-text=\"${3:SEE MORE}\"",
|
|
17
|
+
" prev-aria-label=\"${4:Previous image}\"",
|
|
18
|
+
" next-aria-label=\"${5:Next image}\"",
|
|
19
|
+
"/>"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"SRCDEV SliderGallery Data Script": {
|
|
23
|
+
"description": "galleryData ref for SliderGallery",
|
|
24
|
+
"scope": "typescript,vue",
|
|
25
|
+
"body": [
|
|
26
|
+
"import type { IGalleryData } from \"srcdev-nuxt-components\";",
|
|
27
|
+
"",
|
|
28
|
+
"const $1galleryData = ref<IGalleryData[]>([",
|
|
29
|
+
" {",
|
|
30
|
+
" src: \"$2/images/slide-1.jpg\",",
|
|
31
|
+
" alt: \"$3\",",
|
|
32
|
+
" title: \"$4\",",
|
|
33
|
+
" textBrightness: \"${5:light}\",",
|
|
34
|
+
" },",
|
|
35
|
+
"]);"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV TabNavigation Basic": {
|
|
3
|
+
"description": "TabNavigation with route/hash links",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<TabNavigation :nav-item-data=\"$1navData\" nav-align=\"${2:center}\" />"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"SRCDEV TabNavigation Localised": {
|
|
10
|
+
"description": "TabNavigation with a localised aria-label",
|
|
11
|
+
"scope": "vue,html",
|
|
12
|
+
"body": [
|
|
13
|
+
"<TabNavigation :nav-item-data=\"$1navData\" aria-label=\"${2:Site navigation}\" />"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"SRCDEV TabNavigation Nav Data Script": {
|
|
17
|
+
"description": "navItemData for TabNavigation",
|
|
18
|
+
"scope": "typescript,vue",
|
|
19
|
+
"body": [
|
|
20
|
+
"import type { NavItemData } from \"srcdev-nuxt-components\";",
|
|
21
|
+
"",
|
|
22
|
+
"const $1navData: NavItemData = {",
|
|
23
|
+
" main: [",
|
|
24
|
+
" { text: \"$2Home\", href: \"$3/\" },",
|
|
25
|
+
" { text: \"$4Overview\", href: \"$5#overview\" },",
|
|
26
|
+
" ],",
|
|
27
|
+
"};"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="animated-svg-text" :class="[elementClasses]">
|
|
3
|
+
<slot name="text"></slot>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
interface Props {
|
|
9
|
+
styleClassPassthrough?: string | string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
+
styleClassPassthrough: () => [],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
17
|
+
|
|
18
|
+
watch(
|
|
19
|
+
() => props.styleClassPassthrough,
|
|
20
|
+
() => {
|
|
21
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<style lang="css">
|
|
27
|
+
@layer components {
|
|
28
|
+
|
|
29
|
+
@keyframes animatedSvgText {
|
|
30
|
+
0% {
|
|
31
|
+
fill: transparent;
|
|
32
|
+
stroke-dashoffset: var(--animated-svg-text-stroke-dasharray, 1000);
|
|
33
|
+
stroke-width: var(--animated-svg-text-stroke-width, 0.3);
|
|
34
|
+
}
|
|
35
|
+
70% {
|
|
36
|
+
fill: transparent;
|
|
37
|
+
stroke-width: var(--animated-svg-text-stroke-width, 0.3);
|
|
38
|
+
}
|
|
39
|
+
100% {
|
|
40
|
+
fill: var(--animated-svg-text-fill-colour, var(--theme-text));
|
|
41
|
+
stroke-dashoffset: 0;
|
|
42
|
+
stroke-width: 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.animated-svg-text {
|
|
47
|
+
svg path,
|
|
48
|
+
svg text {
|
|
49
|
+
stroke: var(--animated-svg-text-stroke-colour, var(--theme-text));
|
|
50
|
+
stroke-width: var(--animated-svg-text-stroke-width, 0.3);
|
|
51
|
+
stroke-dasharray: var(--animated-svg-text-stroke-dasharray, 1000);
|
|
52
|
+
stroke-dashoffset: var(--animated-svg-text-stroke-dasharray, 1000);
|
|
53
|
+
animation: animatedSvgText var(--animated-svg-text-animation-duration, 2s) linear 1 forwards;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# AnimatedSvgText — Consumer Styling
|
|
2
|
+
|
|
3
|
+
`AnimatedSvgText` has no props of its own beyond `styleClassPassthrough` — the SVG markup is
|
|
4
|
+
provided via the `text` slot, either as `<path>` outlines of the text or a plain SVG `<text>`
|
|
5
|
+
element (both support `stroke`/`fill`/`stroke-dasharray` per the SVG spec). All visual control is
|
|
6
|
+
through public CSS custom properties, consumed directly on `.animated-svg-text svg path, .animated-svg-text svg text`.
|
|
7
|
+
|
|
8
|
+
## Public tokens
|
|
9
|
+
|
|
10
|
+
| Property | Default | Description |
|
|
11
|
+
| -------- | ------- | ------------ |
|
|
12
|
+
| `--animated-svg-text-stroke-colour` | `var(--theme-text)` | Stroke colour during the draw-on phase. |
|
|
13
|
+
| `--animated-svg-text-fill-colour` | `var(--theme-text)` | Fill colour once the draw-on animation completes. |
|
|
14
|
+
| `--animated-svg-text-stroke-width` | `0.3` | Stroke width (in the SVG's own coordinate units) during the draw-on phase. |
|
|
15
|
+
| `--animated-svg-text-stroke-dasharray` | `1000` | Dash length used to drive the "draw" effect. Set this to roughly the total path length of your SVG — too small and the stroke completes before covering the full outline; too large and the animation appears to pause at the start. |
|
|
16
|
+
| `--animated-svg-text-animation-duration` | `2s` | Duration of the full draw-fill animation. |
|
|
17
|
+
|
|
18
|
+
```vue
|
|
19
|
+
<AnimatedSvgText style="--animated-svg-text-stroke-colour: #e63946; --animated-svg-text-animation-duration: 3.5s;">
|
|
20
|
+
<template #text>
|
|
21
|
+
<svg viewBox="0 0 400 100"><path d="..." /></svg>
|
|
22
|
+
</template>
|
|
23
|
+
</AnimatedSvgText>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Notes
|
|
27
|
+
|
|
28
|
+
- The animation runs once on mount (`forwards`, `1` iteration) — it does not loop or replay on re-entry.
|
|
29
|
+
- `--animated-svg-text-stroke-dasharray` is the one token worth tuning per SVG: it depends on the
|
|
30
|
+
total length of the animated content, which varies with font, text length, and the SVG's
|
|
31
|
+
coordinate scale (`viewBox`). The defaults (`0.3` stroke-width, `1000` dasharray) suit path data
|
|
32
|
+
traced at a small/normalised coordinate scale — a larger `viewBox` (e.g. plain SVG `<text>` at a
|
|
33
|
+
large `font-size`) needs a proportionally larger `--animated-svg-text-stroke-width` and
|
|
34
|
+
`--animated-svg-text-stroke-dasharray`, or the stroke renders too thin/short to see.
|
package/app/components/01.atoms/animations/animated-svg-text/stories/AnimatedSvgText.stories.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import AnimatedSvgText from "../AnimatedSvgText.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof AnimatedSvgText> = {
|
|
5
|
+
title: "Atoms/Animations/AnimatedSvgText",
|
|
6
|
+
component: AnimatedSvgText,
|
|
7
|
+
argTypes: {
|
|
8
|
+
styleClassPassthrough: {
|
|
9
|
+
table: { disable: true },
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
parameters: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"Animates inline SVG text outlines with a stroke-draw-then-fill effect on mount. Provide the SVG markup via the `text` slot. Styling is entirely via CSS custom properties — see CONSUMER-STYLING.md.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof AnimatedSvgText>;
|
|
24
|
+
|
|
25
|
+
const svgSlot = `
|
|
26
|
+
<svg viewBox="0 0 600 150" xmlns="http://www.w3.org/2000/svg" style="width: 100%; height: auto;">
|
|
27
|
+
<text x="0" y="110" font-size="120" font-family="sans-serif" font-weight="700" fill="none">Hello</text>
|
|
28
|
+
</svg>
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
// The default tokens (stroke-width 0.3, dasharray 1000) are tuned for path data traced at a
|
|
32
|
+
// small/normalised coordinate scale. This demo's viewBox is much larger, so both stories override
|
|
33
|
+
// stroke-width/dasharray to suit — see CONSUMER-STYLING.md: dasharray should roughly match the
|
|
34
|
+
// total path length of the SVG being animated.
|
|
35
|
+
const demoScaleTokens = "--animated-svg-text-stroke-width: 2; --animated-svg-text-stroke-dasharray: 2200;";
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
render: () => ({
|
|
39
|
+
components: { AnimatedSvgText },
|
|
40
|
+
template: `<AnimatedSvgText style="${demoScaleTokens}"><template #text>${svgSlot}</template></AnimatedSvgText>`,
|
|
41
|
+
}),
|
|
42
|
+
parameters: {
|
|
43
|
+
docs: {
|
|
44
|
+
description: {
|
|
45
|
+
story: "Default draw-on animation using the theme's text colour for both stroke and fill.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const CustomColourAndSpeed: Story = {
|
|
52
|
+
render: () => ({
|
|
53
|
+
components: { AnimatedSvgText },
|
|
54
|
+
template: `
|
|
55
|
+
<AnimatedSvgText
|
|
56
|
+
style="${demoScaleTokens} --animated-svg-text-stroke-colour: #e63946; --animated-svg-text-fill-colour: #e63946; --animated-svg-text-animation-duration: 3.5s;"
|
|
57
|
+
>
|
|
58
|
+
<template #text>${svgSlot}</template>
|
|
59
|
+
</AnimatedSvgText>
|
|
60
|
+
`,
|
|
61
|
+
}),
|
|
62
|
+
parameters: {
|
|
63
|
+
docs: {
|
|
64
|
+
description: {
|
|
65
|
+
story: "Overriding `--animated-svg-text-stroke-colour`, `--animated-svg-text-fill-colour`, and slowing the animation via `--animated-svg-text-animation-duration`.",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import AnimatedSvgText from "../AnimatedSvgText.vue";
|
|
4
|
+
|
|
5
|
+
describe("AnimatedSvgText", () => {
|
|
6
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
it("mounts without error", async () => {
|
|
9
|
+
const wrapper = await mountSuspended(AnimatedSvgText);
|
|
10
|
+
expect(wrapper.vm).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("renders correct HTML structure", async () => {
|
|
14
|
+
const wrapper = await mountSuspended(AnimatedSvgText, {
|
|
15
|
+
slots: { text: '<svg viewBox="0 0 100 100"><path d="M0 0 L100 100" /></svg>' },
|
|
16
|
+
});
|
|
17
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// ─── Root element ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
it("renders a div with the animated-svg-text class", async () => {
|
|
23
|
+
const wrapper = await mountSuspended(AnimatedSvgText);
|
|
24
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
25
|
+
expect(wrapper.classes()).toContain("animated-svg-text");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ─── Slot ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
it("renders the text slot content", async () => {
|
|
31
|
+
const wrapper = await mountSuspended(AnimatedSvgText, {
|
|
32
|
+
slots: { text: '<svg data-testid="my-svg"><path d="M0 0" /></svg>' },
|
|
33
|
+
});
|
|
34
|
+
expect(wrapper.find('[data-testid="my-svg"]').exists()).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
it("applies a single styleClassPassthrough string to the root", async () => {
|
|
40
|
+
const wrapper = await mountSuspended(AnimatedSvgText, {
|
|
41
|
+
props: { styleClassPassthrough: "hero-svg-text" },
|
|
42
|
+
});
|
|
43
|
+
expect(wrapper.classes()).toContain("hero-svg-text");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
47
|
+
const wrapper = await mountSuspended(AnimatedSvgText, {
|
|
48
|
+
props: { styleClassPassthrough: ["hero-svg-text", "mbe-32"] },
|
|
49
|
+
});
|
|
50
|
+
expect(wrapper.classes()).toContain("hero-svg-text");
|
|
51
|
+
expect(wrapper.classes()).toContain("mbe-32");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
55
|
+
const wrapper = await mountSuspended(AnimatedSvgText, {
|
|
56
|
+
props: { styleClassPassthrough: ["original"] },
|
|
57
|
+
});
|
|
58
|
+
expect(wrapper.classes()).toContain("original");
|
|
59
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
60
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
61
|
+
expect(wrapper.classes()).toContain("updated");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# ClipElement — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## No public CSS token API
|
|
4
|
+
|
|
5
|
+
`ClipElement` has no `--clip-element-*` custom properties. Its only visual output is the
|
|
6
|
+
`clip-path` inset applied to `.clipped-element`, and that value is entirely computed at runtime
|
|
7
|
+
from the `maxClip` prop and the element's scroll position — there is no fixed appearance value a
|
|
8
|
+
consumer would plausibly want to override via CSS.
|
|
9
|
+
|
|
10
|
+
## Override surface
|
|
11
|
+
|
|
12
|
+
Use `styleClassPassthrough` to apply your own classes to the root `.clip-element-wrapper`:
|
|
13
|
+
|
|
14
|
+
```vue
|
|
15
|
+
<ClipElement :max-clip="150" style-class-passthrough="my-clip-wrapper">
|
|
16
|
+
<img src="/images/hero.jpg" alt="Hero" />
|
|
17
|
+
</ClipElement>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
.my-clip-wrapper {
|
|
22
|
+
border-radius: 1.6rem;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Style the slotted content directly for anything beyond the clip effect itself (sizing, object-fit,
|
|
27
|
+
borders, etc.) — `ClipElement` does not constrain it.
|
|
@@ -7,18 +7,23 @@
|
|
|
7
7
|
</template>
|
|
8
8
|
|
|
9
9
|
<script lang="ts" setup>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
})
|
|
10
|
+
interface Props {
|
|
11
|
+
/** Scroll distance (px) over which the element clips in/out. */
|
|
12
|
+
maxClip?: number;
|
|
13
|
+
styleClassPassthrough?: string | string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
17
|
+
maxClip: 100,
|
|
18
|
+
styleClassPassthrough: () => [],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
watch(
|
|
24
|
+
() => props.styleClassPassthrough,
|
|
25
|
+
() => resetElementClasses(props.styleClassPassthrough)
|
|
26
|
+
);
|
|
22
27
|
|
|
23
28
|
const container = ref(null)
|
|
24
29
|
const clipElement = ref<HTMLDivElement | null>(null)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import ClipElement from "../ClipElement.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ClipElement> = {
|
|
5
|
+
title: "Atoms/Effects/ClipElement",
|
|
6
|
+
component: ClipElement,
|
|
7
|
+
argTypes: {
|
|
8
|
+
maxClip: {
|
|
9
|
+
control: { type: "number", min: 0, step: 10 },
|
|
10
|
+
description: "Scroll distance (px) over which the element clips in as it enters the viewport, and clips further out as it scrolls above it.",
|
|
11
|
+
table: { category: "Behaviour" },
|
|
12
|
+
},
|
|
13
|
+
styleClassPassthrough: {
|
|
14
|
+
table: { disable: true },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
parameters: {
|
|
18
|
+
docs: {
|
|
19
|
+
description: {
|
|
20
|
+
component:
|
|
21
|
+
"Clips its slot content in from the top edge as it scrolls into the viewport, using a scroll listener to drive a `clip-path: inset(...)` value. For a CSS-only, listener-free parallax reveal, prefer ScrollRevealFrame/ScrollRevealImage instead — reach for ClipElement only when you specifically need this scroll-position-driven clip behaviour.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof ClipElement>;
|
|
29
|
+
|
|
30
|
+
// A fixed marker at the top of the viewport, since that's exactly where the
|
|
31
|
+
// clip finishes resolving (top === 0) — makes the effect's endpoint visible
|
|
32
|
+
// instead of something you have to guess at while scrolling.
|
|
33
|
+
const topMarker = `
|
|
34
|
+
<div style="position: fixed; top: 0; left: 0; right: 0; height: 2px; background: #ff5c5c; z-index: 10;"></div>
|
|
35
|
+
<div style="position: fixed; top: 6px; left: 12px; font-size: 1.2rem; color: #ff5c5c; z-index: 10;">clip fully resolves at this line ↑</div>
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
// Horizontal rules baked into the image itself so the wipe boundary is
|
|
39
|
+
// visible against the photo, not just a subtle crop of pixels.
|
|
40
|
+
const rulerOverlayStyle = `
|
|
41
|
+
background-image: repeating-linear-gradient(
|
|
42
|
+
to bottom,
|
|
43
|
+
transparent,
|
|
44
|
+
transparent 49px,
|
|
45
|
+
rgba(255, 92, 92, 0.6) 49px,
|
|
46
|
+
rgba(255, 92, 92, 0.6) 50px
|
|
47
|
+
);
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const scrollWrapper = (maxClip: number, inner: string) => `
|
|
51
|
+
${topMarker}
|
|
52
|
+
<div style="padding-block: 30vh 6px; max-width: 860px; margin-inline: auto;">
|
|
53
|
+
<p style="text-align: center; font-size: 1.4rem; opacity: 0.6; margin-block-end: 1rem;">
|
|
54
|
+
Scroll slowly — the image clips in over the ${maxClip}px just before it reaches the red line.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
${inner}
|
|
58
|
+
<div style="height: 100vh;"></div>
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
export const Default: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
maxClip: 100,
|
|
64
|
+
},
|
|
65
|
+
render: (args) => ({
|
|
66
|
+
components: { ClipElement },
|
|
67
|
+
setup() {
|
|
68
|
+
return { args };
|
|
69
|
+
},
|
|
70
|
+
template: scrollWrapper(
|
|
71
|
+
args.maxClip ?? 100,
|
|
72
|
+
`
|
|
73
|
+
<div style="max-width: 860px; margin-inline: auto; position: relative;">
|
|
74
|
+
<ClipElement v-bind="args">
|
|
75
|
+
<div style="position: relative;">
|
|
76
|
+
<img
|
|
77
|
+
src="/images/page/hero/hero-blonde.jpg"
|
|
78
|
+
alt="Blonde hair portrait"
|
|
79
|
+
style="display: block; width: 100%;"
|
|
80
|
+
/>
|
|
81
|
+
<div style="position: absolute; inset: 0; ${rulerOverlayStyle}"></div>
|
|
82
|
+
</div>
|
|
83
|
+
</ClipElement>
|
|
84
|
+
</div>
|
|
85
|
+
`
|
|
86
|
+
),
|
|
87
|
+
}),
|
|
88
|
+
parameters: {
|
|
89
|
+
docs: {
|
|
90
|
+
description: {
|
|
91
|
+
story:
|
|
92
|
+
"Default props — the ruled overlay makes the clip boundary visible as it wipes in over the last 100px before the image reaches the top of the viewport (marked by the red line).",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const LargeClipDistance: Story = {
|
|
99
|
+
args: {
|
|
100
|
+
maxClip: 500,
|
|
101
|
+
},
|
|
102
|
+
render: (args) => ({
|
|
103
|
+
components: { ClipElement },
|
|
104
|
+
setup() {
|
|
105
|
+
return { args };
|
|
106
|
+
},
|
|
107
|
+
template: scrollWrapper(
|
|
108
|
+
args.maxClip ?? 500,
|
|
109
|
+
`
|
|
110
|
+
<div style="max-width: 860px; margin-inline: auto; position: relative;">
|
|
111
|
+
<ClipElement v-bind="args">
|
|
112
|
+
<div style="position: relative;">
|
|
113
|
+
<img
|
|
114
|
+
src="/images/page/hero/hero-blonde.jpg"
|
|
115
|
+
alt="Blonde hair portrait"
|
|
116
|
+
style="display: block; width: 100%;"
|
|
117
|
+
/>
|
|
118
|
+
<div style="position: absolute; inset: 0; ${rulerOverlayStyle}"></div>
|
|
119
|
+
</div>
|
|
120
|
+
</ClipElement>
|
|
121
|
+
</div>
|
|
122
|
+
`
|
|
123
|
+
),
|
|
124
|
+
}),
|
|
125
|
+
parameters: {
|
|
126
|
+
docs: {
|
|
127
|
+
description: {
|
|
128
|
+
story:
|
|
129
|
+
"A much larger maxClip (500px) stretches the wipe over a longer, easier-to-watch scroll distance — useful for previewing the effect without scrolling pixel-by-pixel.",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|