srcdev-nuxt-components 9.4.8 → 9.4.10
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 +20 -6
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/build.mjs +26 -0
- package/.claude/component-ledger/output.html +8 -3
- package/.claude/component-ledger/template.html +7 -2
- package/.claude/skills/component-dynamic-slots.md +1 -1
- package/.claude/skills/components/alert-mask-core.md +84 -0
- package/.claude/skills/components/canvas-switcher.md +1 -1
- package/.claude/skills/components/carousel-flip.md +112 -168
- package/.claude/skills/components/container-glow.md +123 -0
- package/.claude/skills/components/dashboard-quad-grid.md +98 -0
- package/.claude/skills/components/dashboard-stats-grid.md +103 -0
- package/.claude/skills/components/display-theme-switch.md +60 -0
- package/.claude/skills/components/glowing-border.md +66 -0
- package/.claude/skills/components/input-range-core.md +142 -0
- package/.claude/skills/components/section-parallax.md +32 -10
- package/.claude/skills/components/ui-block-decorated.md +56 -0
- package/.claude/skills/components/wipe-away-vertical.md +98 -0
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/index.md +10 -1
- package/.vscode/css-custom-data.json +38 -0
- package/.vscode/settings.json +3 -0
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
- package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
- package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
- package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
- package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
- package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
- package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
- package/.vscode/srcdev-component-input-range.code-snippets +85 -0
- package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
- package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
- package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
- package/README.md +18 -37
- package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
- package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
- package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
- package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
- package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
- package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
- package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
- package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
- package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
- package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
- package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
- package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
- package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
- package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
- package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
- package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
- package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +7 -1
- package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
- package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
- package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
- package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
- package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +96 -11
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
- package/app/components/05.forms/form-field/FormField.vue +1 -1
- package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
- package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
- package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
- package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
- package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
- package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +135 -0
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +16 -2
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +6 -4
- package/app/types/components/container-glow.d.ts +8 -0
- package/app/types/components/index.ts +1 -1
- package/package.json +3 -2
- package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
- package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
- package/app/components/carousels/CarouselBasic.vue +0 -317
- package/app/components/carousels/CarouselInfinite.vue +0 -358
- package/app/components/container-glow/ContainerGlowCore.vue +0 -296
- package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
- package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
- package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
- package/app/components/glowing-border/GlowingBorder.vue +0 -142
- package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
- package/app/components/layout-grids/LayoutGridB.vue +0 -134
- package/app/components/test-storybook/TestStorybook.vue +0 -49
- package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
- package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
- package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
- package/app/types/components/carousel-basic.d.ts +0 -19
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import SectionParallax from "../SectionParallax.vue";
|
|
4
|
+
|
|
5
|
+
const backgroundImage = "/images/eucalyptus-lavender-and-oil.jpg";
|
|
6
|
+
|
|
7
|
+
describe("SectionParallax", () => {
|
|
8
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
it("mounts without error", async () => {
|
|
11
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
12
|
+
props: { backgroundImage },
|
|
13
|
+
});
|
|
14
|
+
expect(wrapper.vm).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
it("renders correct HTML structure (default)", async () => {
|
|
20
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
21
|
+
props: { backgroundImage },
|
|
22
|
+
});
|
|
23
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders correct HTML structure (with slot content)", async () => {
|
|
27
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
28
|
+
props: { backgroundImage },
|
|
29
|
+
slots: { default: "<p class='overlay-text'>A quiet moment.</p>" },
|
|
30
|
+
});
|
|
31
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── Root element ─────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
it("renders as <div> by default", async () => {
|
|
37
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
38
|
+
props: { backgroundImage },
|
|
39
|
+
});
|
|
40
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it.each(["div", "section", "article", "aside"] as const)("renders as <%s> when tag='%s'", async (tag) => {
|
|
44
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
45
|
+
props: { backgroundImage, tag },
|
|
46
|
+
});
|
|
47
|
+
expect(wrapper.element.tagName).toBe(tag.toUpperCase());
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─── Base class ───────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
it("always has the section-parallax class", async () => {
|
|
53
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
54
|
+
props: { backgroundImage },
|
|
55
|
+
});
|
|
56
|
+
expect(wrapper.classes()).toContain("section-parallax");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ─── Slot ─────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
it("renders default slot content when provided", async () => {
|
|
62
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
63
|
+
props: { backgroundImage },
|
|
64
|
+
slots: { default: "<p class='overlay-text'>A quiet moment.</p>" },
|
|
65
|
+
});
|
|
66
|
+
expect(wrapper.find(".overlay-text").exists()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders no visible inner content when no slot is provided", async () => {
|
|
70
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
71
|
+
props: { backgroundImage },
|
|
72
|
+
});
|
|
73
|
+
expect(wrapper.element.children.length).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
79
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
80
|
+
props: { backgroundImage, styleClassPassthrough: "featured" },
|
|
81
|
+
});
|
|
82
|
+
expect(wrapper.classes()).toContain("featured");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
86
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
87
|
+
props: { backgroundImage, styleClassPassthrough: ["featured", "highlighted"] },
|
|
88
|
+
});
|
|
89
|
+
expect(wrapper.classes()).toContain("featured");
|
|
90
|
+
expect(wrapper.classes()).toContain("highlighted");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
94
|
+
const wrapper = await mountSuspended(SectionParallax, {
|
|
95
|
+
props: { backgroundImage, styleClassPassthrough: ["featured"] },
|
|
96
|
+
});
|
|
97
|
+
expect(wrapper.classes()).toContain("featured");
|
|
98
|
+
await wrapper.setProps({ styleClassPassthrough: ["highlighted"] });
|
|
99
|
+
expect(wrapper.classes()).not.toContain("featured");
|
|
100
|
+
expect(wrapper.classes()).toContain("highlighted");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`SectionParallax > renders correct HTML structure (default) 1`] = `"<div class="section-parallax"></div>"`;
|
|
4
|
+
|
|
5
|
+
exports[`SectionParallax > renders correct HTML structure (with slot content) 1`] = `
|
|
6
|
+
"<div class="section-parallax">
|
|
7
|
+
<p class="overlay-text">A quiet moment.</p>
|
|
8
|
+
</div>"
|
|
9
|
+
`;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# WipeAwayVertical — Consumer Styling
|
|
2
|
+
|
|
3
|
+
## Public tokens
|
|
4
|
+
|
|
5
|
+
| Token | Default | Applies to |
|
|
6
|
+
| ----------------------------------------- | -------- | ------------------------------------------------------------------------------------ |
|
|
7
|
+
| `--wipe-away-vertical-height` | `100vh` | `.sticky-items-container` height — the visible height of the sticky panel stack |
|
|
8
|
+
| `--wipe-away-vertical-border-radius` | `0.5rem` | `.sticky-item` corner radius |
|
|
9
|
+
| `--wipe-away-vertical-animation-duration` | `1s` | `wipe-out` scroll-linked animation, browsers supporting `animation-timeline: view()` |
|
|
10
|
+
| `--wipe-away-vertical-trailing-buffer` | `20vh` | `.trailing-buffer` height — release room after the last panel |
|
|
11
|
+
|
|
12
|
+
## Only itemCount - 1 scrollingItem slots
|
|
13
|
+
|
|
14
|
+
The last panel has nothing left to reveal, so it has no wipe animation and no `.scrolling-section`
|
|
15
|
+
of its own — only `scrollingItem-0` through `scrollingItem-{itemCount - 2}` are rendered.
|
|
16
|
+
Providing a `scrollingItem-{itemCount - 1}` slot is harmless (it's simply never rendered) but
|
|
17
|
+
unnecessary. Earlier versions of this component rendered a full extra `100vh` section for the
|
|
18
|
+
last item purely to hold it in view before release — that made the last panel appear roughly 2x
|
|
19
|
+
as deep on screen as the others, since it got the same reveal lead-in as every other panel _plus_
|
|
20
|
+
a whole dedicated section with no wipe to cut it short. `.trailing-buffer` (see token above)
|
|
21
|
+
replaces that with a much smaller, purpose-built release cushion instead.
|
|
22
|
+
|
|
23
|
+
## Pure CSS, no JS measurement
|
|
24
|
+
|
|
25
|
+
The whole pin/reveal effect is CSS-only (grid overlay + `animation-timeline: view()`); there's
|
|
26
|
+
no scroll listener or `getBoundingClientRect()` measurement involved. The root is a single-column
|
|
27
|
+
CSS grid: each `.scrolling-section` takes an explicit `grid-row` (after a `.leading-buffer` row—
|
|
28
|
+
see below), and `.sticky-items-container` spans `grid-row: 1 / -1` to overlay all of them without
|
|
29
|
+
adding its own row — so the root's height is simply the sum of the scrolling sections' own content
|
|
30
|
+
height plus the leading buffer, nothing extra to size manually.
|
|
31
|
+
|
|
32
|
+
## Leading buffer
|
|
33
|
+
|
|
34
|
+
`.leading-buffer` is a fixed `100vh` spacer rendered before the first scrolling section. Without
|
|
35
|
+
it, a section 0 positioned near the top of the page would already be partway through its `entry`
|
|
36
|
+
timeline range at initial page load (before any scrolling happens), so the first panel would
|
|
37
|
+
appear partially wiped on load. The buffer guarantees a full viewport of scroll run-up regardless
|
|
38
|
+
of how much real content precedes the component.
|
|
39
|
+
|
|
40
|
+
## No `animation-timeline: view()` support
|
|
41
|
+
|
|
42
|
+
Browsers without support (checked via `@supports not (animation-timeline: view())`) get the
|
|
43
|
+
sticky/grid-overlay/animation rules dropped entirely — `.sticky-item`s just render as normal
|
|
44
|
+
stacked panels in document flow, and `.scrolling-section`s are hidden. No JS fallback, no
|
|
45
|
+
opacity crossfade.
|
|
46
|
+
|
|
47
|
+
## Sticky vertical centering — no transform
|
|
48
|
+
|
|
49
|
+
`.sticky-items-container` centers itself with `top: calc((100vh - var(--wipe-away-vertical-height, 100vh)) / 2)` — a plain calc(), not a `transform: translateY()`. Do not
|
|
50
|
+
reintroduce a transform for this: `position: sticky`'s stick/release threshold is computed from
|
|
51
|
+
the element's untransformed layout position, so a transform-based visual shift makes the browser
|
|
52
|
+
release the sticky panel early (by the transformed amount), leaving a gap of empty space between
|
|
53
|
+
the last panel and whatever content follows the component.
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
.wipe-away-vertical {
|
|
57
|
+
--wipe-away-vertical-border-radius: 1rem;
|
|
58
|
+
--wipe-away-vertical-animation-duration: 1.5s;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Reduced motion
|
|
63
|
+
|
|
64
|
+
The wipe animation is disabled under `prefers-reduced-motion: reduce` (`animation: none;
|
|
65
|
+
clip-path: none;`) — panels render fully visible with no clip.
|
|
66
|
+
|
|
67
|
+
## Class passthrough
|
|
68
|
+
|
|
69
|
+
`styleClassPassthrough` (string or string array) is applied to the root element via
|
|
70
|
+
`useStyleClassPassthrough`.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="tag"
|
|
4
|
+
class="wipe-away-vertical"
|
|
5
|
+
:class="[elementClasses]"
|
|
6
|
+
:style="{ 'timeline-scope': timelineScope }"
|
|
7
|
+
>
|
|
8
|
+
<div class="leading-buffer" aria-hidden="true"></div>
|
|
9
|
+
|
|
10
|
+
<section
|
|
11
|
+
v-for="(item, key) in itemCount - 1"
|
|
12
|
+
:key="key"
|
|
13
|
+
class="scrolling-section"
|
|
14
|
+
:style="{
|
|
15
|
+
'grid-row': key + 2,
|
|
16
|
+
'view-timeline-name': `--section-${timelineId}-${key}`,
|
|
17
|
+
}"
|
|
18
|
+
>
|
|
19
|
+
<slot :name="`scrollingItem-${key}`"></slot>
|
|
20
|
+
</section>
|
|
21
|
+
|
|
22
|
+
<div class="trailing-buffer" aria-hidden="true" :style="{ 'grid-row': itemCount + 1 }"></div>
|
|
23
|
+
|
|
24
|
+
<div class="sticky-items-container">
|
|
25
|
+
<div
|
|
26
|
+
v-for="(item, key) in itemCount"
|
|
27
|
+
:key="key"
|
|
28
|
+
class="sticky-item"
|
|
29
|
+
:style="{
|
|
30
|
+
'animation-timeline': key === itemCount - 1 ? 'none' : `--section-${timelineId}-${key}`,
|
|
31
|
+
'z-index': itemCount - key,
|
|
32
|
+
}"
|
|
33
|
+
>
|
|
34
|
+
<slot :name="`stickyItem-${key}`"></slot>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</component>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
interface Props {
|
|
42
|
+
tag?: "div" | "section" | "main" | "article" | "aside";
|
|
43
|
+
itemCount: number;
|
|
44
|
+
styleClassPassthrough?: string | string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
48
|
+
tag: "div",
|
|
49
|
+
styleClassPassthrough: () => [],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
53
|
+
|
|
54
|
+
const timelineId = useId();
|
|
55
|
+
|
|
56
|
+
const timelineScope = computed(() =>
|
|
57
|
+
Array.from({ length: props.itemCount }, (_, i) => `--section-${timelineId}-${i}`).join(", ")
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
watch(
|
|
61
|
+
() => props.styleClassPassthrough,
|
|
62
|
+
() => {
|
|
63
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<style lang="css">
|
|
69
|
+
@layer components {
|
|
70
|
+
.wipe-away-vertical {
|
|
71
|
+
display: grid;
|
|
72
|
+
grid-template-columns: 1fr;
|
|
73
|
+
|
|
74
|
+
.scrolling-section {
|
|
75
|
+
grid-column: 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.leading-buffer {
|
|
79
|
+
grid-column: 1;
|
|
80
|
+
grid-row: 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.trailing-buffer {
|
|
84
|
+
grid-column: 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.sticky-items-container {
|
|
88
|
+
grid-column: 1;
|
|
89
|
+
grid-row: 1 / -1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@supports (animation-timeline: view()) {
|
|
94
|
+
.wipe-away-vertical {
|
|
95
|
+
/* gives entry timelines a full viewport of run-up so section 0 isn't already
|
|
96
|
+
mid-"entry" the moment the page loads, whatever content precedes this component */
|
|
97
|
+
.leading-buffer {
|
|
98
|
+
height: 100vh;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* the last item has no wipe animation of its own (nothing left to reveal), so it
|
|
102
|
+
only needs a little room to release cleanly rather than a whole extra section */
|
|
103
|
+
.trailing-buffer {
|
|
104
|
+
height: var(--wipe-away-vertical-trailing-buffer, 20vh);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.scrolling-section {
|
|
108
|
+
view-timeline-axis: block;
|
|
109
|
+
/* sticky panel is vertically centred via the same calc() as its own top offset below,
|
|
110
|
+
so the entry/exit range needs to be inset by the same fixed amount above and below */
|
|
111
|
+
view-timeline-inset: calc((100vh - var(--wipe-away-vertical-height, 100vh)) / 2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.sticky-items-container {
|
|
115
|
+
position: sticky;
|
|
116
|
+
/* a transform here (e.g. translateY(-50%)) would visually centre the panel, but
|
|
117
|
+
position: sticky's stick/release threshold is computed from the untransformed
|
|
118
|
+
layout position, so it would release the panel early by the transformed amount —
|
|
119
|
+
compute the centring offset directly instead so nothing needs transforming */
|
|
120
|
+
top: calc((100vh - var(--wipe-away-vertical-height, 100vh)) / 2);
|
|
121
|
+
height: var(--wipe-away-vertical-height, 100vh);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.sticky-item {
|
|
125
|
+
position: absolute;
|
|
126
|
+
inset: 0;
|
|
127
|
+
border-radius: var(--wipe-away-vertical-border-radius, 0.5rem);
|
|
128
|
+
width: 100%;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@keyframes wipe-out {
|
|
133
|
+
0% {
|
|
134
|
+
clip-path: inset(0 0 0% 0);
|
|
135
|
+
}
|
|
136
|
+
100% {
|
|
137
|
+
clip-path: inset(0 0 100% 0);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.sticky-item {
|
|
142
|
+
animation: wipe-out var(--wipe-away-vertical-animation-duration, 1s) linear both;
|
|
143
|
+
animation-range: entry 0% entry 100%;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@supports not (animation-timeline: view()) {
|
|
148
|
+
.wipe-away-vertical {
|
|
149
|
+
display: block;
|
|
150
|
+
|
|
151
|
+
.leading-buffer {
|
|
152
|
+
display: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.trailing-buffer {
|
|
156
|
+
display: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.scrolling-section {
|
|
160
|
+
display: none;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.sticky-items-container {
|
|
164
|
+
display: contents;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.sticky-item {
|
|
168
|
+
width: 100%;
|
|
169
|
+
border-radius: var(--wipe-away-vertical-border-radius, 0.5rem);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@media (prefers-reduced-motion: reduce) {
|
|
175
|
+
.sticky-item {
|
|
176
|
+
animation: none !important;
|
|
177
|
+
clip-path: none !important;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import WipeAwayVertical from "../WipeAwayVertical.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof WipeAwayVertical> = {
|
|
5
|
+
title: "Atoms/Effects/WipeAwayVertical",
|
|
6
|
+
component: WipeAwayVertical,
|
|
7
|
+
argTypes: {
|
|
8
|
+
tag: {
|
|
9
|
+
control: "select",
|
|
10
|
+
options: ["div", "section", "main", "article", "aside"],
|
|
11
|
+
description: "Root element tag",
|
|
12
|
+
table: { category: "Structure" },
|
|
13
|
+
},
|
|
14
|
+
itemCount: {
|
|
15
|
+
control: { type: "number", min: 1, max: 5, step: 1 },
|
|
16
|
+
description:
|
|
17
|
+
"Number of sticky/scrolling section pairs. The demo below has a fixed set of 3 slotted panels, so changing this control past what's slotted will leave extra panels empty — see the FivePanels story for a differently-sized example.",
|
|
18
|
+
table: { category: "Structure" },
|
|
19
|
+
},
|
|
20
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
21
|
+
},
|
|
22
|
+
parameters: {
|
|
23
|
+
docs: {
|
|
24
|
+
description: {
|
|
25
|
+
component:
|
|
26
|
+
"Scroll-driven vertical wipe effect: each sticky panel wipes away to reveal the next as its paired scrolling section crosses the viewport. Pure CSS — a single-column grid overlays `.sticky-items-container` across all `.scrolling-section` rows via `animation-timeline: view()`, so there's no scroll listener or JS measurement involved. Consumers supply `stickyItem-{n}` slots for each of `itemCount` panels, and `scrollingItem-{n}` slots for only the first `itemCount - 1` — the last panel has nothing left to reveal, so it has no wipe animation or scrolling section of its own, just a small built-in trailing buffer to release cleanly. The sticky panel's own visible height is `--wipe-away-vertical-height` (default `100vh`) — leave the root's own `height` unset (`auto`) so it sizes itself from its children. Browsers without `animation-timeline: view()` support (via `@supports not (...)`) just get the sticky items stacked in normal document flow instead.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof WipeAwayVertical>;
|
|
34
|
+
|
|
35
|
+
const scrollNote = `
|
|
36
|
+
<p style="text-align: center; font-size: 1.4rem; opacity: 0.5; padding-block: 2rem;">
|
|
37
|
+
Scroll down to see each panel wipe away
|
|
38
|
+
</p>
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const panel = (label: string, colour: string) => `
|
|
42
|
+
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: ${colour}; color: white; font-size: 2.4rem; font-weight: 600; border-radius: inherit;">
|
|
43
|
+
${label}
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export const Default: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
tag: "div",
|
|
50
|
+
itemCount: 3,
|
|
51
|
+
},
|
|
52
|
+
render: (args) => ({
|
|
53
|
+
components: { WipeAwayVertical },
|
|
54
|
+
setup() {
|
|
55
|
+
return { args };
|
|
56
|
+
},
|
|
57
|
+
template: `
|
|
58
|
+
<div>
|
|
59
|
+
${scrollNote}
|
|
60
|
+
<WipeAwayVertical v-bind="args">
|
|
61
|
+
<template #stickyItem-0>${panel("Panel One", "#5b8def")}</template>
|
|
62
|
+
<template #stickyItem-1>${panel("Panel Two", "#e0576b")}</template>
|
|
63
|
+
<template #stickyItem-2>${panel("Panel Three", "#2fb380")}</template>
|
|
64
|
+
<template #scrollingItem-0><div style="height: 100vh;"></div></template>
|
|
65
|
+
<template #scrollingItem-1><div style="height: 100vh;"></div></template>
|
|
66
|
+
</WipeAwayVertical>
|
|
67
|
+
<p style="text-align: center; font-size: 1.4rem; opacity: 0.5; padding-block: 2rem;">End of timeline</p>
|
|
68
|
+
</div>
|
|
69
|
+
`,
|
|
70
|
+
}),
|
|
71
|
+
parameters: {
|
|
72
|
+
docs: {
|
|
73
|
+
description: {
|
|
74
|
+
story:
|
|
75
|
+
"Three panels wiping in sequence as the page scrolls. The root's height is left as `auto` so it sizes itself from its scrolling sections — no manual height calculation needed.",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const RoundedPanels: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
tag: "div",
|
|
84
|
+
itemCount: 3,
|
|
85
|
+
},
|
|
86
|
+
render: (args) => ({
|
|
87
|
+
components: { WipeAwayVertical },
|
|
88
|
+
setup() {
|
|
89
|
+
return { args };
|
|
90
|
+
},
|
|
91
|
+
template: `
|
|
92
|
+
<div>
|
|
93
|
+
${scrollNote}
|
|
94
|
+
<WipeAwayVertical v-bind="args" style="--wipe-away-vertical-border-radius: 2.4rem;">
|
|
95
|
+
<template #stickyItem-0>${panel("Rounded One", "#5b8def")}</template>
|
|
96
|
+
<template #stickyItem-1>${panel("Rounded Two", "#e0576b")}</template>
|
|
97
|
+
<template #stickyItem-2>${panel("Rounded Three", "#2fb380")}</template>
|
|
98
|
+
<template #scrollingItem-0><div style="height: 100vh;"></div></template>
|
|
99
|
+
<template #scrollingItem-1><div style="height: 100vh;"></div></template>
|
|
100
|
+
</WipeAwayVertical>
|
|
101
|
+
<p style="text-align: center; font-size: 1.4rem; opacity: 0.5; padding-block: 2rem;">End of timeline</p>
|
|
102
|
+
</div>
|
|
103
|
+
`,
|
|
104
|
+
}),
|
|
105
|
+
parameters: {
|
|
106
|
+
docs: {
|
|
107
|
+
description: {
|
|
108
|
+
story: "`--wipe-away-vertical-border-radius` rounds the sticky panels' corners.",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const SlowWipe: Story = {
|
|
115
|
+
args: {
|
|
116
|
+
tag: "div",
|
|
117
|
+
itemCount: 2,
|
|
118
|
+
},
|
|
119
|
+
render: (args) => ({
|
|
120
|
+
components: { WipeAwayVertical },
|
|
121
|
+
setup() {
|
|
122
|
+
return { args };
|
|
123
|
+
},
|
|
124
|
+
template: `
|
|
125
|
+
<div>
|
|
126
|
+
${scrollNote}
|
|
127
|
+
<WipeAwayVertical v-bind="args" style="--wipe-away-vertical-animation-duration: 3s;">
|
|
128
|
+
<template #stickyItem-0>${panel("Slow One", "#5b8def")}</template>
|
|
129
|
+
<template #stickyItem-1>${panel("Slow Two", "#e0576b")}</template>
|
|
130
|
+
<template #scrollingItem-0><div style="height: 100vh;"></div></template>
|
|
131
|
+
</WipeAwayVertical>
|
|
132
|
+
<p style="text-align: center; font-size: 1.4rem; opacity: 0.5; padding-block: 2rem;">End of timeline</p>
|
|
133
|
+
</div>
|
|
134
|
+
`,
|
|
135
|
+
}),
|
|
136
|
+
parameters: {
|
|
137
|
+
docs: {
|
|
138
|
+
description: {
|
|
139
|
+
story:
|
|
140
|
+
"`--wipe-away-vertical-animation-duration` stretches the wipe animation across a larger portion of the scroll-linked entry range, in browsers that support `animation-timeline: view()`.",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const CompactHeight: Story = {
|
|
147
|
+
args: {
|
|
148
|
+
tag: "div",
|
|
149
|
+
itemCount: 2,
|
|
150
|
+
},
|
|
151
|
+
render: (args) => ({
|
|
152
|
+
components: { WipeAwayVertical },
|
|
153
|
+
setup() {
|
|
154
|
+
return { args };
|
|
155
|
+
},
|
|
156
|
+
template: `
|
|
157
|
+
<div>
|
|
158
|
+
${scrollNote}
|
|
159
|
+
<WipeAwayVertical v-bind="args" style="--wipe-away-vertical-height: 50vh;">
|
|
160
|
+
<template #stickyItem-0>${panel("Compact One", "#5b8def")}</template>
|
|
161
|
+
<template #stickyItem-1>${panel("Compact Two", "#e0576b")}</template>
|
|
162
|
+
<template #scrollingItem-0><div style="height: 100vh;"></div></template>
|
|
163
|
+
</WipeAwayVertical>
|
|
164
|
+
<p style="text-align: center; font-size: 1.4rem; opacity: 0.5; padding-block: 2rem;">End of timeline</p>
|
|
165
|
+
</div>
|
|
166
|
+
`,
|
|
167
|
+
}),
|
|
168
|
+
parameters: {
|
|
169
|
+
docs: {
|
|
170
|
+
description: {
|
|
171
|
+
story:
|
|
172
|
+
"`--wipe-away-vertical-height` sizes the visible sticky panel independently of the root's total scroll height — here it's shrunk to 50vh instead of the 100vh default. The root's height is still left as `auto`.",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const FivePanels: Story = {
|
|
179
|
+
args: {
|
|
180
|
+
tag: "div",
|
|
181
|
+
itemCount: 5,
|
|
182
|
+
},
|
|
183
|
+
render: (args) => ({
|
|
184
|
+
components: { WipeAwayVertical },
|
|
185
|
+
setup() {
|
|
186
|
+
return { args };
|
|
187
|
+
},
|
|
188
|
+
template: `
|
|
189
|
+
<div>
|
|
190
|
+
${scrollNote}
|
|
191
|
+
<WipeAwayVertical v-bind="args">
|
|
192
|
+
<template #stickyItem-0>${panel("Panel One", "#5b8def")}</template>
|
|
193
|
+
<template #stickyItem-1>${panel("Panel Two", "#e0576b")}</template>
|
|
194
|
+
<template #stickyItem-2>${panel("Panel Three", "#2fb380")}</template>
|
|
195
|
+
<template #stickyItem-3>${panel("Panel Four", "#e0a83a")}</template>
|
|
196
|
+
<template #stickyItem-4>${panel("Panel Five", "#8a5be0")}</template>
|
|
197
|
+
<template #scrollingItem-0><div style="height: 100vh;"></div></template>
|
|
198
|
+
<template #scrollingItem-1><div style="height: 100vh;"></div></template>
|
|
199
|
+
<template #scrollingItem-2><div style="height: 100vh;"></div></template>
|
|
200
|
+
<template #scrollingItem-3><div style="height: 100vh;"></div></template>
|
|
201
|
+
</WipeAwayVertical>
|
|
202
|
+
<p style="text-align: center; font-size: 1.4rem; opacity: 0.5; padding-block: 2rem;">End of timeline</p>
|
|
203
|
+
</div>
|
|
204
|
+
`,
|
|
205
|
+
}),
|
|
206
|
+
parameters: {
|
|
207
|
+
docs: {
|
|
208
|
+
description: {
|
|
209
|
+
story:
|
|
210
|
+
"Five panels wiping in sequence — itemCount scales cleanly: the root's height stays auto regardless of itemCount, and z-index/view-timeline naming are generated per item automatically.",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const loremParagraph = `
|
|
217
|
+
<p style="max-width: 60ch; margin-inline: auto; font-size: 1.6rem; line-height: 1.6; padding-block: 2rem;">
|
|
218
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut
|
|
219
|
+
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
|
220
|
+
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
|
|
221
|
+
voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
|
222
|
+
</p>
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
const loremHeading = (text: string) => `
|
|
226
|
+
<h2 style="max-width: 60ch; margin-inline: auto; font-size: 2.4rem; padding-top: 3rem;">${text}</h2>
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
export const InRealisticPage: Story = {
|
|
230
|
+
args: {
|
|
231
|
+
tag: "div",
|
|
232
|
+
itemCount: 3,
|
|
233
|
+
},
|
|
234
|
+
render: (args) => ({
|
|
235
|
+
components: { WipeAwayVertical },
|
|
236
|
+
setup() {
|
|
237
|
+
return { args };
|
|
238
|
+
},
|
|
239
|
+
template: `
|
|
240
|
+
<div>
|
|
241
|
+
${loremHeading("Before the effect")}
|
|
242
|
+
${loremParagraph}
|
|
243
|
+
${loremParagraph}
|
|
244
|
+
${loremHeading("Another section")}
|
|
245
|
+
${loremParagraph}
|
|
246
|
+
<WipeAwayVertical v-bind="args">
|
|
247
|
+
<template #stickyItem-0>${panel("Panel One", "#5b8def")}</template>
|
|
248
|
+
<template #stickyItem-1>${panel("Panel Two", "#e0576b")}</template>
|
|
249
|
+
<template #stickyItem-2>${panel("Panel Three", "#2fb380")}</template>
|
|
250
|
+
<template #scrollingItem-0><div style="height: 100vh;"></div></template>
|
|
251
|
+
<template #scrollingItem-1><div style="height: 100vh;"></div></template>
|
|
252
|
+
</WipeAwayVertical>
|
|
253
|
+
${loremHeading("After the effect")}
|
|
254
|
+
${loremParagraph}
|
|
255
|
+
${loremParagraph}
|
|
256
|
+
${loremHeading("More reading")}
|
|
257
|
+
${loremParagraph}
|
|
258
|
+
</div>
|
|
259
|
+
`,
|
|
260
|
+
}),
|
|
261
|
+
parameters: {
|
|
262
|
+
docs: {
|
|
263
|
+
description: {
|
|
264
|
+
story:
|
|
265
|
+
"The component dropped into a realistic page: several rows of unrelated heading/paragraph content both before and after it, rather than the minimal scroll-note used by the other stories. Demonstrates that the wipe still starts fully un-wiped at initial page load and the sticky panel releases cleanly afterwards, regardless of how much content precedes or follows it.",
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
};
|