srcdev-nuxt-components 9.4.4 → 9.4.5
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 +68 -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/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/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 +136 -0
- package/.claude/skills/components/display-tooltip.md +84 -0
- package/.claude/skills/composable-tooltips-guide.md +10 -0
- package/.claude/skills/index.md +7 -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-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-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 +14 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +27 -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/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/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 +185 -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/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +42 -0
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +103 -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/types/components/display-tooltip-defined.d.ts +10 -0
- package/app/types/components/index.ts +1 -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
|
@@ -89,6 +89,16 @@ await startGuide()
|
|
|
89
89
|
| `stopGuide()` | `() => void` | Close any open popovers and stop the guide |
|
|
90
90
|
| `initializePopovers()` | `() => void` | Re-scan the container for `[popover]` elements; call if popovers are added dynamically |
|
|
91
91
|
|
|
92
|
+
## Example with DisplayTooltip / DisplayTooltipDefined
|
|
93
|
+
|
|
94
|
+
[`DisplayTooltip`](components/display-tooltip.md) and
|
|
95
|
+
[`DisplayTooltipDefined`](components/display-tooltip-defined.md) already emit the
|
|
96
|
+
`[popover]`/`popovertarget`/`popovertargetaction` markup this composable expects, so no extra
|
|
97
|
+
wiring is needed beyond passing their shared container's ref in. See the `GuidedTour` story in
|
|
98
|
+
`DisplayTooltipDefined.stories.ts` for a full working example — it recreates the old
|
|
99
|
+
`app/pages/ui/tooltips.vue` demo page (removed 2026-08-23 when Storybook became the only demo
|
|
100
|
+
surface), mixing one of each component as guide steps.
|
|
101
|
+
|
|
92
102
|
## Notes
|
|
93
103
|
|
|
94
104
|
- If no trigger button is found for a popover, `togglePopover(true)` is called directly as a fallback — anchor positioning may not apply in this case.
|
package/.claude/skills/index.md
CHANGED
|
@@ -63,6 +63,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
63
63
|
├── composable-analytics.md — useAnalytics: provider-agnostic trackEvent/page-view tracking (google-analytics only implemented), consent-gated, single call site for setup + firing events
|
|
64
64
|
└── components/
|
|
65
65
|
├── alert-content-inner.md — AlertContentInner: shared icon/body/dismiss molecule under AlertContent + AlertMaskedContent; app.config icon map (alertContent.icons + dismissIcon) covers all consumers
|
|
66
|
+
├── animated-svg-text.md — AnimatedSvgText: inline SVG stroke-draw-then-fill animation, text slot, CSS token API
|
|
66
67
|
├── accordian-core.md — AccordianCore indexed dynamic slots (accordian-{n}-summary/icon/content), exclusive-open grouping
|
|
67
68
|
├── eyebrow-text.md — EyebrowText props, usage patterns, styling
|
|
68
69
|
├── hero-text.md — HeroText props, usage patterns, styling
|
|
@@ -109,7 +110,12 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
109
110
|
├── site-header.md — SiteHeader: PageRow + SkipLinks + ResponsiveHeader composition, #branding/#secondaryNavigation slots, dual styleClassPassthrough hooks
|
|
110
111
|
├── responsive-header.md — ResponsiveHeader: overflow-collapsing adaptive nav, measurement-pipeline gotchas (unsized icons, vw font-size drift), full CSS token API
|
|
111
112
|
├── navigation-items.md — NavigationItems: internal overflow-panel renderer for ResponsiveHeader, complement-visibility logic, not used standalone
|
|
112
|
-
|
|
113
|
+
├── cookie-consent-banner.md — CookieConsentBanner: fixed non-modal Accept/Reject banner driven by useCookieConsent, message/acceptLabel/rejectLabel slots, CSS token API
|
|
114
|
+
├── display-banner.md — DisplayBanner: canvas/content stacked overlay banner, conditional slot wrappers, CSS token API
|
|
115
|
+
├── deep-expanding-menu.md — DeepExpandingMenu: anchor-positioned popover nav panels, browser support caveat, CSS token API
|
|
116
|
+
├── deep-expanding-menu-classic.md — DeepExpandingMenuClassic: <details>-based fallback nav, click-outside close, CSS token API
|
|
117
|
+
├── display-tooltip.md — DisplayTooltip: anchor-positioned popover trigger, browser support caveat, CSS token API
|
|
118
|
+
└── display-tooltip-defined.md — DisplayTooltipDefined: structured title/body/action tooltip content with close button, composes DisplayTooltip
|
|
113
119
|
```
|
|
114
120
|
|
|
115
121
|
## Skill file template
|
|
@@ -240,3 +240,33 @@ Key points:
|
|
|
240
240
|
them in the Storybook controls panel (e.g. `"Model"`, `"Basic"`, `"Validation"`, `"Styling"`, `"Slots"`).
|
|
241
241
|
- Export multiple named stories (`Default`, `WithError`, `Outlined`, etc.) when you want
|
|
242
242
|
Playwright to test distinct visual states via separate story URLs.
|
|
243
|
+
|
|
244
|
+
## Scroll/animation-driven effects need surrounding chrome, not just the bare component
|
|
245
|
+
|
|
246
|
+
A story that renders a scroll- or timer-driven component with no other markup often fails to
|
|
247
|
+
show the effect at all — not because the component is broken, but because the demo gives the
|
|
248
|
+
viewer nothing to judge it against. Things that make an animated/scroll-driven effect illegible
|
|
249
|
+
in isolation:
|
|
250
|
+
|
|
251
|
+
- The effect resolves over a short distance/time relative to the page (e.g. a 100px scroll
|
|
252
|
+
window on an otherwise-long page) — easy to scroll straight past it.
|
|
253
|
+
- The visual change is a subtle crop/shift on photographic content, where the eye has no
|
|
254
|
+
reference point to notice a boundary moving.
|
|
255
|
+
- There's no indicator of *where* or *when* the effect completes.
|
|
256
|
+
|
|
257
|
+
Add scaffolding around the component to fix this, rather than assuming a bigger/slower prop
|
|
258
|
+
value alone solves it:
|
|
259
|
+
|
|
260
|
+
- A fixed marker (a line, label) at the point in the viewport where the effect's key transition
|
|
261
|
+
happens (e.g. `position: fixed; top: 0` for a component that resolves when its own top hits
|
|
262
|
+
the viewport top).
|
|
263
|
+
- A ruled/striped overlay or contrasting background behind the animated content so a boundary
|
|
264
|
+
(clip edge, wipe line, fade) is visible against it, not just a crop of photo pixels.
|
|
265
|
+
- Short on-page instructions telling the viewer what to do and what to watch for ("scroll
|
|
266
|
+
slowly — the image clips in over the last 100px before the red line").
|
|
267
|
+
- A second story with an exaggerated prop value (larger distance/duration) purely so the effect
|
|
268
|
+
is easy to preview without precise scrolling/timing — see `ClipElement`'s `LargeClipDistance`
|
|
269
|
+
story for the pattern (`.claude/skills/components/clip-element.md`).
|
|
270
|
+
|
|
271
|
+
This is a documentation-only concern — it doesn't change the component's default props, just
|
|
272
|
+
how the story demonstrates it.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV AnimatedSvgText": {
|
|
3
|
+
"description": "AnimatedSvgText — stroke-draw-then-fill animation for inline SVG text/path content",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<AnimatedSvgText>",
|
|
7
|
+
" <template #text>",
|
|
8
|
+
" <svg viewBox=\"0 0 $1600 $2150\">",
|
|
9
|
+
" <path d=\"$3\" />",
|
|
10
|
+
" </svg>",
|
|
11
|
+
" </template>",
|
|
12
|
+
"</AnimatedSvgText>"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV ClipElement Basic": {
|
|
3
|
+
"description": "ClipElement — scroll-driven clip-path reveal for a single image or block",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<ClipElement :max-clip=\"$1100\">",
|
|
7
|
+
" <img src=\"$2/images/hero.jpg\" alt=\"$3Hero\" style=\"width: 100%; display: block;\" />",
|
|
8
|
+
"</ClipElement>"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"SRCDEV ClipElement With Passthrough": {
|
|
12
|
+
"description": "ClipElement with styleClassPassthrough for a custom wrapper class",
|
|
13
|
+
"scope": "vue,html",
|
|
14
|
+
"body": [
|
|
15
|
+
"<ClipElement :max-clip=\"$1150\" style-class-passthrough=\"$2my-clip-wrapper\">",
|
|
16
|
+
" $0",
|
|
17
|
+
"</ClipElement>"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV ClippedPanel Basic": {
|
|
3
|
+
"description": "ClippedPanel — fixed-size notched/cutout shape panel",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<ClippedPanel variant=\"$1square\">",
|
|
7
|
+
" $0",
|
|
8
|
+
"</ClippedPanel>"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"SRCDEV ClippedPanel Custom Colours": {
|
|
12
|
+
"description": "ClippedPanel with background/outline colour overrides",
|
|
13
|
+
"scope": "vue,html",
|
|
14
|
+
"body": [
|
|
15
|
+
"<ClippedPanel",
|
|
16
|
+
" variant=\"$1square\"",
|
|
17
|
+
" style=\"--clipped-panel-background-colour: $2#fef3e7; --clipped-panel-outline-colour: $3#b5651d;\"",
|
|
18
|
+
">",
|
|
19
|
+
" $0",
|
|
20
|
+
"</ClippedPanel>"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV DisplayBanner Basic": {
|
|
3
|
+
"description": "DisplayBanner — canvas/content stacked overlay banner",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<DisplayBanner>",
|
|
7
|
+
" <template #canvas>",
|
|
8
|
+
" $1",
|
|
9
|
+
" </template>",
|
|
10
|
+
" <template #content>",
|
|
11
|
+
" $0",
|
|
12
|
+
" </template>",
|
|
13
|
+
"</DisplayBanner>"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"SRCDEV DisplayBanner Min Height": {
|
|
17
|
+
"description": "DisplayBanner with min-height override",
|
|
18
|
+
"scope": "vue,html",
|
|
19
|
+
"body": [
|
|
20
|
+
"<DisplayBanner style=\"--display-banner-min-height: $140rem;\">",
|
|
21
|
+
" <template #canvas>",
|
|
22
|
+
" $2",
|
|
23
|
+
" </template>",
|
|
24
|
+
" <template #content>",
|
|
25
|
+
" $0",
|
|
26
|
+
" </template>",
|
|
27
|
+
"</DisplayBanner>"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV DisplayTooltipDefined Basic": {
|
|
3
|
+
"description": "DisplayTooltipDefined — structured title/body/action tooltip",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<DisplayTooltipDefined",
|
|
7
|
+
" :content-text=\"{",
|
|
8
|
+
" tooltipTitle: { tag: 'h4', text: '$1' },",
|
|
9
|
+
" tooltipContent: { tag: 'p', text: '$0' },",
|
|
10
|
+
" }\"",
|
|
11
|
+
"/>"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV DisplayTooltip Basic": {
|
|
3
|
+
"description": "DisplayTooltip — anchor-positioned popover trigger",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<DisplayTooltip>",
|
|
7
|
+
" <template #tooltipContent>",
|
|
8
|
+
" <p>$0</p>",
|
|
9
|
+
" </template>",
|
|
10
|
+
"</DisplayTooltip>"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"SRCDEV DisplayTooltip With Trigger Content": {
|
|
14
|
+
"description": "DisplayTooltip with a triggerContent label",
|
|
15
|
+
"scope": "vue,html",
|
|
16
|
+
"body": [
|
|
17
|
+
"<DisplayTooltip>",
|
|
18
|
+
" <template #triggerContent>",
|
|
19
|
+
" <span>$1</span>",
|
|
20
|
+
" </template>",
|
|
21
|
+
" <template #tooltipContent>",
|
|
22
|
+
" <p>$0</p>",
|
|
23
|
+
" </template>",
|
|
24
|
+
"</DisplayTooltip>"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -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)
|