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.
Files changed (113) hide show
  1. package/.claude/commands/migrate-component.md +20 -6
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +26 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/alert-mask-core.md +84 -0
  8. package/.claude/skills/components/canvas-switcher.md +1 -1
  9. package/.claude/skills/components/carousel-flip.md +112 -168
  10. package/.claude/skills/components/container-glow.md +123 -0
  11. package/.claude/skills/components/dashboard-quad-grid.md +98 -0
  12. package/.claude/skills/components/dashboard-stats-grid.md +103 -0
  13. package/.claude/skills/components/display-theme-switch.md +60 -0
  14. package/.claude/skills/components/glowing-border.md +66 -0
  15. package/.claude/skills/components/input-range-core.md +142 -0
  16. package/.claude/skills/components/section-parallax.md +32 -10
  17. package/.claude/skills/components/ui-block-decorated.md +56 -0
  18. package/.claude/skills/components/wipe-away-vertical.md +98 -0
  19. package/.claude/skills/icon-sets.md +1 -1
  20. package/.claude/skills/index.md +10 -1
  21. package/.vscode/css-custom-data.json +38 -0
  22. package/.vscode/settings.json +3 -0
  23. package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
  24. package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
  25. package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
  26. package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
  27. package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
  28. package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
  29. package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
  30. package/.vscode/srcdev-component-input-range.code-snippets +85 -0
  31. package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
  32. package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
  33. package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
  34. package/README.md +18 -37
  35. package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
  36. package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
  37. package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
  38. package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
  39. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
  40. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
  41. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
  42. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
  43. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
  44. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
  45. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
  46. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
  47. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
  48. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
  49. package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
  50. package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
  51. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
  52. package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
  53. package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
  54. package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
  55. package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
  56. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
  57. package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
  58. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +1 -1
  59. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
  60. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
  61. package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
  62. package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
  63. package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
  64. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
  65. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
  66. package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
  67. package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
  68. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +7 -1
  69. package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
  70. package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
  71. package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
  72. package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
  73. package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
  74. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
  75. package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
  76. package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
  77. package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
  78. package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
  79. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
  80. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
  81. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +96 -11
  82. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
  83. package/app/components/05.forms/form-field/FormField.vue +1 -1
  84. package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
  85. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
  86. package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
  87. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
  88. package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
  89. package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
  90. package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
  91. package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
  92. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +135 -0
  93. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +16 -2
  94. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +6 -4
  95. package/app/types/components/container-glow.d.ts +8 -0
  96. package/app/types/components/index.ts +1 -1
  97. package/package.json +3 -2
  98. package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
  99. package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
  100. package/app/components/carousels/CarouselBasic.vue +0 -317
  101. package/app/components/carousels/CarouselInfinite.vue +0 -358
  102. package/app/components/container-glow/ContainerGlowCore.vue +0 -296
  103. package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
  104. package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
  105. package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
  106. package/app/components/glowing-border/GlowingBorder.vue +0 -142
  107. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
  108. package/app/components/layout-grids/LayoutGridB.vue +0 -134
  109. package/app/components/test-storybook/TestStorybook.vue +0 -49
  110. package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
  111. package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
  112. package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
  113. 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
+ };