srcdev-nuxt-components 9.1.28 → 9.1.30
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/settings.json +7 -2
- package/.claude/settings.local.json +2 -1
- package/.claude/skills/components/banner-video.md +178 -0
- package/.claude/skills/components/grid-stack.md +133 -0
- package/.claude/skills/components/scroll-reveal-frame.md +136 -0
- package/.claude/skills/components/scroll-reveal-image.md +161 -0
- package/.claude/skills/index.md +9 -1
- package/.claude/skills/new-app-scaffold.md +301 -0
- package/.claude/skills/setup-postinstall.md +45 -0
- package/.claude/skills/theming-override-default.md +6 -0
- package/.claude/skills/theming-partial-override.md +131 -0
- package/.claude/skills/vue-video-autoplay.md +74 -0
- package/README.md +30 -5
- package/app/components/01.atoms/banner-video/BannerVideo.vue +207 -0
- package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +180 -0
- package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +350 -0
- package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +13 -0
- package/app/components/01.atoms/grid-stack/GridStack.vue +34 -0
- package/app/components/01.atoms/grid-stack/stories/GridStack.stories.ts +162 -0
- package/app/components/01.atoms/grid-stack/tests/GridStack.spec.ts +66 -0
- package/app/components/01.atoms/scroll-reveal-frame/ScrollRevealFrame.vue +125 -0
- package/app/components/01.atoms/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +166 -0
- package/app/components/01.atoms/scroll-reveal-frame/tests/ScrollRevealFrame.spec.ts +148 -0
- package/app/components/01.atoms/scroll-reveal-frame/tests/__snapshots__/ScrollRevealFrame.spec.ts.snap +13 -0
- package/app/components/01.atoms/scroll-reveal-image/ScrollRevealImage.vue +96 -0
- package/app/components/01.atoms/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +181 -0
- package/app/components/01.atoms/scroll-reveal-image/tests/ScrollRevealImage.spec.ts +235 -0
- package/app/components/01.atoms/scroll-reveal-image/tests/__snapshots__/ScrollRevealImage.spec.ts.snap +13 -0
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +1 -0
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +1 -0
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +1 -0
- package/app/components/03.organisms/services/services-grids/stories/ServicesSectionGrid.stories.ts +1 -0
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -0
- package/app/components/03.organisms/services/services-grids/tests/ServicesSectionGrid.spec.ts +1 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesSectionGrid.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-section/stories/ServicesSection.stories.ts +1 -0
- package/app/components/03.organisms/services/services-section/tests/ServicesSection.spec.ts +1 -0
- package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +1 -1
- package/app/layouts/default.vue +2 -0
- package/app/pages/banner-video.vue +238 -0
- package/app/pages/grid-stack.vue +252 -0
- package/app/pages/index.vue +5 -0
- package/app/pages/ui/scroll-reveal-image.vue +483 -0
- package/app/types/types.services.ts +1 -0
- package/package.json +1 -1
- package/app/assets/styles/extends-layer/srcdev-components/components/_display-prompt-core.css +0 -94
- package/app/assets/styles/extends-layer/srcdev-components/components/_display-toast.css +0 -5
- package/app/assets/styles/extends-layer/srcdev-components/components/_expanding-panel.css +0 -37
- package/app/assets/styles/extends-layer/srcdev-components/components/index.css +0 -3
- package/app/assets/styles/extends-layer/srcdev-components/index.css +0 -1
- package/app/assets/styles/extends-layer/srcdev-forms/components/_form-fieldset.css +0 -38
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-button.css +0 -67
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-core.css +0 -87
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-options-button.css +0 -74
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-checkbox-radio-with-label.css +0 -14
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-description.css +0 -13
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-error.css +0 -54
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-label.css +0 -21
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-select.css +0 -143
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-text.css +0 -190
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-textarea.css +0 -96
- package/app/assets/styles/extends-layer/srcdev-forms/components/_input-toggle-switch.css +0 -68
- package/app/assets/styles/extends-layer/srcdev-forms/components/index.css +0 -12
- package/app/assets/styles/extends-layer/srcdev-forms/index.css +0 -2
- package/app/assets/styles/extends-layer/srcdev-forms/setup/_generic.css +0 -21
- package/app/assets/styles/extends-layer/srcdev-forms/setup/index.css +0 -3
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_default.css +0 -13
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_large.css +0 -12
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_medium.css +0 -12
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_small.css +0 -12
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/_x-small.css +0 -11
- package/app/assets/styles/extends-layer/srcdev-forms/setup/sizes/index.css +0 -23
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_default.css +0 -62
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_error.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_ghost.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_info.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_primary.css +0 -66
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_secondary.css +0 -64
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_success.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_tertiary.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_warning.css +0 -63
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/index.css +0 -7
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import ScrollRevealImage from "../ScrollRevealImage.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ScrollRevealImage> = {
|
|
5
|
+
title: "Atoms/Effects/ScrollRevealImage",
|
|
6
|
+
component: ScrollRevealImage,
|
|
7
|
+
argTypes: {
|
|
8
|
+
src: {
|
|
9
|
+
control: "text",
|
|
10
|
+
description: "Image source path",
|
|
11
|
+
table: { category: "Image" },
|
|
12
|
+
},
|
|
13
|
+
alt: {
|
|
14
|
+
control: "text",
|
|
15
|
+
description: "Alt text for the image",
|
|
16
|
+
table: { category: "Image" },
|
|
17
|
+
},
|
|
18
|
+
imgWidth: {
|
|
19
|
+
control: { type: "number" },
|
|
20
|
+
description: "Intrinsic width of the source image — required for NuxtImg optimisation",
|
|
21
|
+
table: { category: "Image" },
|
|
22
|
+
},
|
|
23
|
+
imgHeight: {
|
|
24
|
+
control: { type: "number" },
|
|
25
|
+
description: "Intrinsic height of the source image — required for NuxtImg optimisation",
|
|
26
|
+
table: { category: "Image" },
|
|
27
|
+
},
|
|
28
|
+
frameHeight: {
|
|
29
|
+
control: "text",
|
|
30
|
+
description: "Height of the visible clipping frame",
|
|
31
|
+
table: { category: "Layout" },
|
|
32
|
+
},
|
|
33
|
+
parallaxOffset: {
|
|
34
|
+
control: "text",
|
|
35
|
+
description: "Distance the image travels vertically as the frame scrolls through the viewport",
|
|
36
|
+
table: { category: "Layout" },
|
|
37
|
+
},
|
|
38
|
+
focalX: {
|
|
39
|
+
control: "text",
|
|
40
|
+
description: "Horizontal focal point — CSS object-position x-axis value (e.g. '50%', 'left', '30%')",
|
|
41
|
+
table: { category: "Layout" },
|
|
42
|
+
},
|
|
43
|
+
radius: {
|
|
44
|
+
control: "text",
|
|
45
|
+
description: "Border-radius applied to the clipping frame",
|
|
46
|
+
table: { category: "Appearance" },
|
|
47
|
+
},
|
|
48
|
+
styleClassPassthrough: {
|
|
49
|
+
table: { disable: true },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
parameters: {
|
|
53
|
+
docs: {
|
|
54
|
+
description: {
|
|
55
|
+
component:
|
|
56
|
+
"Single-image parallax reveal built on ScrollRevealFrame. Adds NuxtImg optimisation (src, alt, intrinsic dimensions) and a horizontal focal-point prop (focalX). For arbitrary slot content — grids, video, etc. — use ScrollRevealFrame directly.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default meta;
|
|
63
|
+
type Story = StoryObj<typeof ScrollRevealImage>;
|
|
64
|
+
|
|
65
|
+
const scrollWrapper = (inner: string) => `
|
|
66
|
+
<div style="padding-block: 60vh; max-width: 860px; margin-inline: auto;">
|
|
67
|
+
<p style="text-align: center; font-size: 1.4rem; opacity: 0.5; margin-block-end: 4rem;">Scroll to see the parallax effect</p>
|
|
68
|
+
${inner}
|
|
69
|
+
</div>
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
export const Default: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
src: "/images/page/hero/hero-blonde.jpg",
|
|
75
|
+
alt: "Blonde hair portrait",
|
|
76
|
+
imgWidth: 1280,
|
|
77
|
+
imgHeight: 1920,
|
|
78
|
+
frameHeight: "540px",
|
|
79
|
+
parallaxOffset: "36rem",
|
|
80
|
+
focalX: "50%",
|
|
81
|
+
radius: "0px",
|
|
82
|
+
},
|
|
83
|
+
render: (args) => ({
|
|
84
|
+
components: { ScrollRevealImage },
|
|
85
|
+
setup() {
|
|
86
|
+
return { args };
|
|
87
|
+
},
|
|
88
|
+
template: scrollWrapper(`<ScrollRevealImage v-bind="args" />`),
|
|
89
|
+
}),
|
|
90
|
+
parameters: {
|
|
91
|
+
docs: {
|
|
92
|
+
description: {
|
|
93
|
+
story: "All props at their default values. A portrait image panning from top to bottom as the frame crosses the viewport.",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const ShortFrame: Story = {
|
|
100
|
+
args: {
|
|
101
|
+
src: "/images/banners/banner-mid-brown.webp",
|
|
102
|
+
alt: "Mid brown hair banner",
|
|
103
|
+
imgWidth: 1920,
|
|
104
|
+
imgHeight: 800,
|
|
105
|
+
frameHeight: "320px",
|
|
106
|
+
parallaxOffset: "24rem",
|
|
107
|
+
focalX: "50%",
|
|
108
|
+
radius: "0px",
|
|
109
|
+
},
|
|
110
|
+
render: Default.render,
|
|
111
|
+
parameters: {
|
|
112
|
+
docs: {
|
|
113
|
+
description: {
|
|
114
|
+
story: "A shallow banner frame with a wide landscape image. parallaxOffset is reduced proportionally to match the shorter frame height.",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const RoundedCorners: Story = {
|
|
121
|
+
args: {
|
|
122
|
+
src: "/images/banners/banner-ginger.webp",
|
|
123
|
+
alt: "Ginger hair banner",
|
|
124
|
+
imgWidth: 1920,
|
|
125
|
+
imgHeight: 800,
|
|
126
|
+
frameHeight: "480px",
|
|
127
|
+
parallaxOffset: "36rem",
|
|
128
|
+
focalX: "50%",
|
|
129
|
+
radius: "2.4rem",
|
|
130
|
+
},
|
|
131
|
+
render: Default.render,
|
|
132
|
+
parameters: {
|
|
133
|
+
docs: {
|
|
134
|
+
description: {
|
|
135
|
+
story: "The radius prop applies border-radius to the clipping frame. The image reveal stays contained within the rounded shape.",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const FocalPoint: Story = {
|
|
142
|
+
args: {
|
|
143
|
+
src: "/images/page/hero/hero-red.jpg",
|
|
144
|
+
alt: "Red hair portrait",
|
|
145
|
+
imgWidth: 1280,
|
|
146
|
+
imgHeight: 1920,
|
|
147
|
+
frameHeight: "540px",
|
|
148
|
+
parallaxOffset: "36rem",
|
|
149
|
+
focalX: "75%",
|
|
150
|
+
radius: "0px",
|
|
151
|
+
},
|
|
152
|
+
render: Default.render,
|
|
153
|
+
parameters: {
|
|
154
|
+
docs: {
|
|
155
|
+
description: {
|
|
156
|
+
story: "focalX pins the horizontal crop axis. Here it is pushed right to keep the subject centred in a narrower frame.",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const TallFrame: Story = {
|
|
163
|
+
args: {
|
|
164
|
+
src: "/images/page/hero/hero-hair.jpg",
|
|
165
|
+
alt: "Hair portrait",
|
|
166
|
+
imgWidth: 1280,
|
|
167
|
+
imgHeight: 1920,
|
|
168
|
+
frameHeight: "70vh",
|
|
169
|
+
parallaxOffset: "60rem",
|
|
170
|
+
focalX: "50%",
|
|
171
|
+
radius: "0px",
|
|
172
|
+
},
|
|
173
|
+
render: Default.render,
|
|
174
|
+
parameters: {
|
|
175
|
+
docs: {
|
|
176
|
+
description: {
|
|
177
|
+
story: "A tall frame combined with a large parallaxOffset creates the most dramatic reveal — nearly the full image height is traversed as you scroll.",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import ScrollRevealImage from "../ScrollRevealImage.vue";
|
|
4
|
+
|
|
5
|
+
describe("ScrollRevealImage", () => {
|
|
6
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
it("mounts without error", async () => {
|
|
9
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
10
|
+
props: { src: "/images/test.jpg" },
|
|
11
|
+
});
|
|
12
|
+
expect(wrapper.vm).toBeTruthy();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
it("renders correct HTML structure (default props)", async () => {
|
|
18
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
19
|
+
props: { src: "/images/test.jpg", alt: "Test image" },
|
|
20
|
+
});
|
|
21
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders correct HTML structure (all props set)", async () => {
|
|
25
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
26
|
+
props: {
|
|
27
|
+
src: "/images/test.jpg",
|
|
28
|
+
alt: "All props",
|
|
29
|
+
imgWidth: 800,
|
|
30
|
+
imgHeight: 600,
|
|
31
|
+
frameHeight: "400px",
|
|
32
|
+
parallaxOffset: "20rem",
|
|
33
|
+
focalX: "30%",
|
|
34
|
+
radius: "1.6rem",
|
|
35
|
+
styleClassPassthrough: ["custom-class"],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ─── Root element ────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
it("renders a <figure> as the root element", async () => {
|
|
44
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
45
|
+
props: { src: "/images/test.jpg" },
|
|
46
|
+
});
|
|
47
|
+
expect(wrapper.element.tagName).toBe("FIGURE");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("always has the reveal-frame class", async () => {
|
|
51
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
52
|
+
props: { src: "/images/test.jpg" },
|
|
53
|
+
});
|
|
54
|
+
expect(wrapper.classes()).toContain("reveal-frame");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── CSS custom properties ───────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
it("sets --_frame-height to 540px by default", async () => {
|
|
60
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
61
|
+
props: { src: "/images/test.jpg" },
|
|
62
|
+
});
|
|
63
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
64
|
+
expect(style.getPropertyValue("--_frame-height")).toBe("540px");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("sets --_parallax-offset to 36rem by default", async () => {
|
|
68
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
69
|
+
props: { src: "/images/test.jpg" },
|
|
70
|
+
});
|
|
71
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
72
|
+
expect(style.getPropertyValue("--_parallax-offset")).toBe("36rem");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("sets --_radius to 0px by default", async () => {
|
|
76
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
77
|
+
props: { src: "/images/test.jpg" },
|
|
78
|
+
});
|
|
79
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
80
|
+
expect(style.getPropertyValue("--_radius")).toBe("0px");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("sets --_focal-x to 50% by default", async () => {
|
|
84
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
85
|
+
props: { src: "/images/test.jpg" },
|
|
86
|
+
});
|
|
87
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
88
|
+
expect(style.getPropertyValue("--_focal-x")).toBe("50%");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("reflects frameHeight prop in --_frame-height", async () => {
|
|
92
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
93
|
+
props: { src: "/images/test.jpg", frameHeight: "80vh" },
|
|
94
|
+
});
|
|
95
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
96
|
+
expect(style.getPropertyValue("--_frame-height")).toBe("80vh");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("reflects parallaxOffset prop in --_parallax-offset", async () => {
|
|
100
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
101
|
+
props: { src: "/images/test.jpg", parallaxOffset: "48rem" },
|
|
102
|
+
});
|
|
103
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
104
|
+
expect(style.getPropertyValue("--_parallax-offset")).toBe("48rem");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("reflects radius prop in --_radius", async () => {
|
|
108
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
109
|
+
props: { src: "/images/test.jpg", radius: "2.4rem" },
|
|
110
|
+
});
|
|
111
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
112
|
+
expect(style.getPropertyValue("--_radius")).toBe("2.4rem");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("reflects focalX prop in --_focal-x", async () => {
|
|
116
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
117
|
+
props: { src: "/images/test.jpg", focalX: "75%" },
|
|
118
|
+
});
|
|
119
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
120
|
+
expect(style.getPropertyValue("--_focal-x")).toBe("75%");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── Image element ───────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
it("renders an img child element with the reveal-image class", async () => {
|
|
126
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
127
|
+
props: { src: "/images/test.jpg" },
|
|
128
|
+
});
|
|
129
|
+
// NuxtImg resolves to a real <img> element in the Nuxt test environment,
|
|
130
|
+
// identified by the data-nuxt-img attribute it injects.
|
|
131
|
+
const img = wrapper.find("img[data-nuxt-img]");
|
|
132
|
+
expect(img.exists()).toBe(true);
|
|
133
|
+
expect(img.classes()).toContain("reveal-image");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("passes src through to the image element", async () => {
|
|
137
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
138
|
+
props: { src: "/images/hero.jpg" },
|
|
139
|
+
});
|
|
140
|
+
// NuxtImg transforms the URL via /_ipx/, so check the original path is present.
|
|
141
|
+
const src = wrapper.find("img[data-nuxt-img]").attributes("src") ?? "";
|
|
142
|
+
expect(src).toContain("hero.jpg");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("passes alt to the image element (default empty string)", async () => {
|
|
146
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
147
|
+
props: { src: "/images/test.jpg" },
|
|
148
|
+
});
|
|
149
|
+
expect(wrapper.find("img[data-nuxt-img]").attributes("alt")).toBe("");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("passes alt to the image element when provided", async () => {
|
|
153
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
154
|
+
props: { src: "/images/test.jpg", alt: "A hair portrait" },
|
|
155
|
+
});
|
|
156
|
+
expect(wrapper.find("img[data-nuxt-img]").attributes("alt")).toBe("A hair portrait");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("passes imgWidth to the image element (default 1920)", async () => {
|
|
160
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
161
|
+
props: { src: "/images/test.jpg" },
|
|
162
|
+
});
|
|
163
|
+
expect(wrapper.find("img[data-nuxt-img]").attributes("width")).toBe("1920");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("passes imgHeight to the image element (default 1080)", async () => {
|
|
167
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
168
|
+
props: { src: "/images/test.jpg" },
|
|
169
|
+
});
|
|
170
|
+
expect(wrapper.find("img[data-nuxt-img]").attributes("height")).toBe("1080");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("passes custom imgWidth and imgHeight to the image element", async () => {
|
|
174
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
175
|
+
props: { src: "/images/test.jpg", imgWidth: 1280, imgHeight: 1920 },
|
|
176
|
+
});
|
|
177
|
+
const img = wrapper.find("img[data-nuxt-img]");
|
|
178
|
+
expect(img.attributes("width")).toBe("1280");
|
|
179
|
+
expect(img.attributes("height")).toBe("1920");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("sets loading=lazy on the image element", async () => {
|
|
183
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
184
|
+
props: { src: "/images/test.jpg" },
|
|
185
|
+
});
|
|
186
|
+
expect(wrapper.find("img[data-nuxt-img]").attributes("loading")).toBe("lazy");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("sets decoding=async on the image element", async () => {
|
|
190
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
191
|
+
props: { src: "/images/test.jpg" },
|
|
192
|
+
});
|
|
193
|
+
expect(wrapper.find("img[data-nuxt-img]").attributes("decoding")).toBe("async");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
it("applies a single styleClassPassthrough string to the root", async () => {
|
|
199
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
200
|
+
props: { src: "/images/test.jpg", styleClassPassthrough: "hero-image" },
|
|
201
|
+
});
|
|
202
|
+
expect(wrapper.classes()).toContain("hero-image");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
206
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
207
|
+
props: {
|
|
208
|
+
src: "/images/test.jpg",
|
|
209
|
+
styleClassPassthrough: ["hero-image", "mbe-32"],
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
expect(wrapper.classes()).toContain("hero-image");
|
|
213
|
+
expect(wrapper.classes()).toContain("mbe-32");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("does not apply styleClassPassthrough to the image element", async () => {
|
|
217
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
218
|
+
props: {
|
|
219
|
+
src: "/images/test.jpg",
|
|
220
|
+
styleClassPassthrough: "hero-image",
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
expect(wrapper.find("img[data-nuxt-img]").classes()).not.toContain("hero-image");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
227
|
+
const wrapper = await mountSuspended(ScrollRevealImage, {
|
|
228
|
+
props: { src: "/images/test.jpg", styleClassPassthrough: ["original"] },
|
|
229
|
+
});
|
|
230
|
+
expect(wrapper.classes()).toContain("original");
|
|
231
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
232
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
233
|
+
expect(wrapper.classes()).toContain("updated");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`ScrollRevealImage > renders correct HTML structure (all props set) 1`] = `
|
|
4
|
+
"<figure class="reveal-frame custom-class" style="--_frame-height: 400px; --_parallax-offset: 20rem; --_radius: 1.6rem; --_focal-x: 30%;">
|
|
5
|
+
<div class="reveal-content"><img width="800" height="600" data-nuxt-img="" srcset="/_ipx/s_800x600/images/test.jpg 1x, /_ipx/s_1600x1200/images/test.jpg 2x" class="reveal-image" alt="All props" loading="lazy" decoding="async" src="/_ipx/s_800x600/images/test.jpg"></div>
|
|
6
|
+
</figure>"
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
exports[`ScrollRevealImage > renders correct HTML structure (default props) 1`] = `
|
|
10
|
+
"<figure class="reveal-frame" style="--_frame-height: 540px; --_parallax-offset: 36rem; --_radius: 0px; --_focal-x: 50%;">
|
|
11
|
+
<div class="reveal-content"><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/test.jpg 1x, /_ipx/s_3840x2160/images/test.jpg 2x" class="reveal-image" alt="Test image" loading="lazy" decoding="async" src="/_ipx/s_1920x1080/images/test.jpg"></div>
|
|
12
|
+
</figure>"
|
|
13
|
+
`;
|
|
@@ -4,7 +4,7 @@ exports[`ServicesSectionGrid > renders correct HTML structure 1`] = `
|
|
|
4
4
|
"<div class="services-grid">
|
|
5
5
|
<div class="services-section">
|
|
6
6
|
<div class="services-section__grid">
|
|
7
|
-
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-installation.jpg 1x, /_ipx/_/images/locs-installation.jpg 2x" alt="Locs Installation" loading="eager" class="image" src="/_ipx/_/images/locs-installation.jpg"></div>
|
|
7
|
+
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-installation.jpg 1x, /_ipx/_/images/locs-installation.jpg 2x" alt="Locs Installation" loading="eager" fetchpriority="high" class="image" src="/_ipx/_/images/locs-installation.jpg"></div>
|
|
8
8
|
<div class="info-wrapper is-summary align-center">
|
|
9
9
|
<div class="eyebrow-text large">Subtitle for Locs Installation</div>
|
|
10
10
|
<h2 class="hero-text mb-20 title axis-horizontal">
|
|
@@ -31,7 +31,7 @@ exports[`ServicesSectionGrid > renders correct HTML structure 1`] = `
|
|
|
31
31
|
</div>
|
|
32
32
|
<div class="services-section">
|
|
33
33
|
<div class="services-section__grid">
|
|
34
|
-
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-retwist.jpg 1x, /_ipx/_/images/locs-retwist.jpg 2x" alt="Locs Retwist" loading="eager" class="image" src="/_ipx/_/images/locs-retwist.jpg"></div>
|
|
34
|
+
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/locs-retwist.jpg 1x, /_ipx/_/images/locs-retwist.jpg 2x" alt="Locs Retwist" loading="eager" fetchpriority="auto" class="image" src="/_ipx/_/images/locs-retwist.jpg"></div>
|
|
35
35
|
<div class="info-wrapper is-summary align-center">
|
|
36
36
|
<div class="eyebrow-text large">Subtitle for Locs Retwist</div>
|
|
37
37
|
<h2 class="hero-text mb-20 title axis-horizontal">
|
|
@@ -58,7 +58,7 @@ exports[`ServicesSectionGrid > renders correct HTML structure 1`] = `
|
|
|
58
58
|
</div>
|
|
59
59
|
<div class="services-section">
|
|
60
60
|
<div class="services-section__grid">
|
|
61
|
-
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/colour-treatment.jpg 1x, /_ipx/_/images/colour-treatment.jpg 2x" alt="Colour Treatment" loading="lazy" class="image" src="/_ipx/_/images/colour-treatment.jpg"></div>
|
|
61
|
+
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/colour-treatment.jpg 1x, /_ipx/_/images/colour-treatment.jpg 2x" alt="Colour Treatment" loading="lazy" fetchpriority="auto" class="image" src="/_ipx/_/images/colour-treatment.jpg"></div>
|
|
62
62
|
<div class="info-wrapper is-summary align-center">
|
|
63
63
|
<div class="eyebrow-text large">Subtitle for Colour Treatment</div>
|
|
64
64
|
<h2 class="hero-text mb-20 title axis-horizontal">
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports[`ServicesSection > renders correct HTML structure 1`] = `
|
|
4
4
|
"<div class="services-section">
|
|
5
5
|
<div class="services-section__grid">
|
|
6
|
-
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/test.jpg 1x, /_ipx/_/images/test.jpg 2x" alt="Test Service" loading="eager" class="image" src="/_ipx/_/images/test.jpg"></div>
|
|
6
|
+
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/test.jpg 1x, /_ipx/_/images/test.jpg 2x" alt="Test Service" loading="eager" fetchpriority="high" class="image" src="/_ipx/_/images/test.jpg"></div>
|
|
7
7
|
<div class="info-wrapper">
|
|
8
8
|
<div class="eyebrow-text large">A subtitle</div>
|
|
9
9
|
<h2 class="hero-text mb-20 title axis-horizontal">
|
package/app/layouts/default.vue
CHANGED
|
@@ -62,9 +62,11 @@ const responsiveNavLinks = {
|
|
|
62
62
|
childLinksTitle: "UI Components",
|
|
63
63
|
childLinks: [
|
|
64
64
|
{ name: "Wipe Away Vertical", path: "/ui/wipe-away-vertical" },
|
|
65
|
+
{ name: "Scroll Reveal Image", path: "/ui/scroll-reveal-image" },
|
|
65
66
|
{ name: "Tooltips", path: "/ui/tooltips" },
|
|
66
67
|
{ name: "Marquee Scroller", path: "/ui/marquee-scroller" },
|
|
67
68
|
{ name: "Banner", path: "/ui/display-banner" },
|
|
69
|
+
{ name: "Banner Video", path: "/banner-video" },
|
|
68
70
|
{ name: "Section Parallax", path: "/ui/section-parallax" },
|
|
69
71
|
{ name: "Animated SVG Text", path: "/ui/animated-svg-text" },
|
|
70
72
|
{ name: "Carousel (Basic)", path: "/ui/carousel-basic" },
|