srcdev-nuxt-components 9.2.0 → 9.2.1
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 +5 -2
- package/.claude/skills/components/banner-video.md +88 -23
- package/.claude/skills/index.md +1 -1
- package/app/components/01.atoms/banner-video/BannerVideo.vue +62 -29
- package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +120 -0
- package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +53 -2
- package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +95 -0
- package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +2 -2
- package/package.json +1 -1
package/.claude/settings.json
CHANGED
|
@@ -57,11 +57,14 @@
|
|
|
57
57
|
"Bash(git fetch *)",
|
|
58
58
|
"Bash(python3 -m json.tool /Users/simoncornforth/websites/nuxt-components/.vscode/srcdev-component-services-section.code-snippets)",
|
|
59
59
|
"Bash(node -e \"JSON.parse\\(require\\('fs'\\).readFileSync\\('/Users/simoncornforth/websites/nuxt-components/.vscode/srcdev-component-services-section.code-snippets','utf8'\\)\\); console.log\\('valid json'\\)\")",
|
|
60
|
-
"WebFetch(domain:luxury-locs-by-natasha-nuxt3.vercel.app)"
|
|
60
|
+
"WebFetch(domain:luxury-locs-by-natasha-nuxt3.vercel.app)",
|
|
61
|
+
"Bash(cd /Users/simoncornforth/websites/nuxt-components *)",
|
|
62
|
+
"Bash(node -e \"JSON.parse\\(require\\('fs'\\).readFileSync\\('/Users/simoncornforth/websites/nuxt-components/.vscode/srcdev-component-banner-video.code-snippets','utf8'\\)\\); console.log\\('valid json'\\)\")"
|
|
61
63
|
],
|
|
62
64
|
"additionalDirectories": [
|
|
63
65
|
"/Users/simoncornforth/websites/instepreflexology",
|
|
64
|
-
"/Users/simoncornforth/websites/nuxt-components/.vscode"
|
|
66
|
+
"/Users/simoncornforth/websites/nuxt-components/.vscode",
|
|
67
|
+
"/Users/simoncornforth/websites/nuxt-components/.claude/skills/components"
|
|
65
68
|
]
|
|
66
69
|
}
|
|
67
70
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: BannerVideo
|
|
3
|
-
description: BannerVideo full-width hero video banner — props, depth tier system, verticalPosition/horizontalPosition, reduced-motion fallback, CSS tokens, consumer styling
|
|
3
|
+
description: BannerVideo full-width hero video banner — props, depth tier system, verticalPosition/horizontalPosition, playIcon/pauseIcon/toggle-icon slot, reduced-motion fallback, CSS tokens, consumer styling
|
|
4
4
|
type: reference
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,9 +8,9 @@ type: reference
|
|
|
8
8
|
|
|
9
9
|
## Overview
|
|
10
10
|
|
|
11
|
-
`BannerVideo` renders a full-width banner section that plays a muted, looping mp4 video. A poster image is shown as fallback when the user has `prefers-reduced-motion: reduce` set — handled entirely in CSS, no JS.
|
|
11
|
+
`BannerVideo` renders a full-width banner section that plays a muted, looping mp4 video. A poster image is shown as fallback when the user has `prefers-reduced-motion: reduce` set — handled entirely in CSS, no JS. A visible pause/play toggle is always rendered over the video (see "Pause/play toggle" below) to satisfy WCAG 2.2.2 (Pause, Stop, Hide), since the video autoplays and loops indefinitely.
|
|
12
12
|
|
|
13
|
-
The banner is sized via `aspect-ratio` so it scales naturally. The `depth` prop selects a responsive `max-height` tier (`xs` → `xl`) implemented with `clamp()` — no breakpoint props needed. Each tier exposes a `--
|
|
13
|
+
The banner is sized via `aspect-ratio` so it scales naturally. The `depth` prop selects a responsive `max-height` tier (`xs` → `xl`) implemented with `clamp()` — no breakpoint props needed. Each tier exposes a `--banner-video-max-height-{depth}` CSS token that consuming pages can override.
|
|
14
14
|
|
|
15
15
|
### Autoplay mechanism
|
|
16
16
|
|
|
@@ -26,13 +26,21 @@ The video uses `autoplay muted loop playsinline preload="auto"` attributes on th
|
|
|
26
26
|
| `imgWidth` | `number` | `1920` | Intrinsic width of the poster image — required for NuxtImg/IPX optimisation. |
|
|
27
27
|
| `imgHeight` | `number` | `1080` | Intrinsic height of the poster image — required for NuxtImg/IPX optimisation. |
|
|
28
28
|
| `tag` | `"section" \| "div" \| "header" \| "main" \| "article"` | `"section"` | HTML element rendered as the root. |
|
|
29
|
-
| `depth` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Responsive max-height tier. Each maps to a `clamp()` scale; override via `--
|
|
29
|
+
| `depth` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Responsive max-height tier. Each maps to a `clamp()` scale; override via `--banner-video-max-height-{depth}`. |
|
|
30
30
|
| `aspectRatio` | `string` | `"21/9"` | CSS `aspect-ratio` of the container (e.g. `"16/9"`, `"21/9"`, `"4/3"`). |
|
|
31
31
|
| `objectFit` | `"cover" \| "contain" \| "fill" \| "none" \| "scale-down"` | `"cover"` | How the video and fallback image fill the banner frame. |
|
|
32
32
|
| `verticalPosition` | `"start" \| "center" \| "end"` | `"center"` | Vertical crop position. Maps to `align-self` on the video element and `object-position` Y on the fallback image. |
|
|
33
33
|
| `horizontalPosition` | `"start" \| "center" \| "end"` | `"center"` | Horizontal crop position. Maps to `object-position` X on the fallback image. |
|
|
34
|
+
| `playIcon` | `string` | `"mdi:play"` | Iconify icon name shown on the toggle button before playback starts. |
|
|
35
|
+
| `pauseIcon` | `string` | `"mdi:pause"` | Iconify icon name shown on the toggle button once playback starts. |
|
|
34
36
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
35
37
|
|
|
38
|
+
## Slots
|
|
39
|
+
|
|
40
|
+
| Slot | Slot props | Purpose |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `toggle-icon` | `{ isPlaying: boolean }` | Replaces the toggle button's icon entirely. Default renders `<Icon :name="isPlaying ? pauseIcon : playIcon" />` — use this slot instead of `playIcon`/`pauseIcon` when an Iconify name isn't enough (custom SVG, different icon set). |
|
|
43
|
+
|
|
36
44
|
## Basic usage
|
|
37
45
|
|
|
38
46
|
```vue
|
|
@@ -87,6 +95,20 @@ The video uses `autoplay muted loop playsinline preload="auto"` attributes on th
|
|
|
87
95
|
/>
|
|
88
96
|
```
|
|
89
97
|
|
|
98
|
+
### Custom toggle icon
|
|
99
|
+
|
|
100
|
+
```vue
|
|
101
|
+
<!-- Swap the Iconify icon name -->
|
|
102
|
+
<BannerVideo src="…" poster="…" play-icon="mdi:play-circle" pause-icon="mdi:pause-circle" />
|
|
103
|
+
|
|
104
|
+
<!-- Replace the icon entirely (custom SVG, different icon set) -->
|
|
105
|
+
<BannerVideo src="…" poster="…">
|
|
106
|
+
<template #toggle-icon="{ isPlaying }">
|
|
107
|
+
<MyCustomIcon :name="isPlaying ? 'pause' : 'play'" />
|
|
108
|
+
</template>
|
|
109
|
+
</BannerVideo>
|
|
110
|
+
```
|
|
111
|
+
|
|
90
112
|
## Positioning explained
|
|
91
113
|
|
|
92
114
|
The video element uses `height: auto; min-height: 100%` so it overflows the container naturally when aspect ratios differ. `overflow: hidden` on the root clips it. `verticalPosition` maps to `align-self` on the video (shifting which portion of the overflow is visible) and to `object-position` Y on the fallback image.
|
|
@@ -135,20 +157,55 @@ Inline style tokens (set from props):
|
|
|
135
157
|
|
|
136
158
|
| depth | token | clamp value |
|
|
137
159
|
|---|---|---|
|
|
138
|
-
| `xs` | `--
|
|
139
|
-
| `sm` | `--
|
|
140
|
-
| `md` | `--
|
|
141
|
-
| `lg` | `--
|
|
142
|
-
| `xl` | `--
|
|
160
|
+
| `xs` | `--banner-video-max-height-xs` | `clamp(12rem, 15vw, 24rem)` |
|
|
161
|
+
| `sm` | `--banner-video-max-height-sm` | `clamp(18rem, 22vw, 36rem)` |
|
|
162
|
+
| `md` | `--banner-video-max-height-md` | `clamp(28rem, 38vw, 56rem)` |
|
|
163
|
+
| `lg` | `--banner-video-max-height-lg` | `clamp(40rem, 52vw, 72rem)` |
|
|
164
|
+
| `xl` | `--banner-video-max-height-xl` | `clamp(52rem, 65vw, 90rem)` |
|
|
143
165
|
|
|
144
166
|
**Override a tier in a consuming page:**
|
|
145
167
|
|
|
146
168
|
```css
|
|
147
169
|
.my-page {
|
|
148
|
-
--
|
|
170
|
+
--banner-video-max-height-md: clamp(32rem, 45vw, 64rem);
|
|
149
171
|
}
|
|
150
172
|
```
|
|
151
173
|
|
|
174
|
+
### Toggle button token defaults
|
|
175
|
+
|
|
176
|
+
| token | default | controls |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `--banner-video-toggle-size` | `3.2rem` | Width and height of the toggle button |
|
|
179
|
+
| `--banner-video-toggle-offset` | `1.2rem` | Margin from the bottom-right corner of the banner |
|
|
180
|
+
| `--banner-video-toggle-background` | `oklch(0% 0 0 / 0.4)` | Button background |
|
|
181
|
+
| `--banner-video-toggle-background-hover` | `oklch(0% 0 0 / 0.6)` | Button background on hover/focus |
|
|
182
|
+
| `--banner-video-toggle-icon-color` | `white` | Icon colour |
|
|
183
|
+
|
|
184
|
+
Full details, plus global/page-scoped/per-instance override patterns, live in the
|
|
185
|
+
component's own `CONSUMER-STYLING.md`.
|
|
186
|
+
|
|
187
|
+
## Pause/play toggle
|
|
188
|
+
|
|
189
|
+
A `<button class="banner-video__toggle">` is always rendered in the bottom-right corner of the
|
|
190
|
+
banner, overlaying the video. It calls `videoEl.pause()` / `.play()` directly and its
|
|
191
|
+
`aria-label` reflects actual playback state ("Pause background video" / "Play background video"),
|
|
192
|
+
tracked via the video's native `play`/`pause` events rather than assumed — autoplay can be
|
|
193
|
+
blocked by the browser, so the label starts as "Play background video" until a `play` event
|
|
194
|
+
actually fires.
|
|
195
|
+
|
|
196
|
+
This exists to satisfy **WCAG 2.2.2 (Pause, Stop, Hide)**: content that autoplays and lasts more
|
|
197
|
+
than 5 seconds needs an on-page mechanism to pause it. The `prefers-reduced-motion` fallback only
|
|
198
|
+
covers users who have set that OS-level preference — everyone else still needs a way to stop the
|
|
199
|
+
loop, which is why the toggle is unconditional, not opt-in via a prop.
|
|
200
|
+
|
|
201
|
+
The toggle is hidden under `prefers-reduced-motion: reduce` (alongside the `<video>` itself,
|
|
202
|
+
since the fallback image is already static and has nothing to pause).
|
|
203
|
+
|
|
204
|
+
No captions/transcript are needed for this component's use case (silent, decorative ambient
|
|
205
|
+
background video) — if a consumer ever uses `BannerVideo` for a video that conveys information,
|
|
206
|
+
that content needs its own accessible alternative (transcript, captions) handled outside this
|
|
207
|
+
component, since `BannerVideo` has no slot for it.
|
|
208
|
+
|
|
152
209
|
## CSS classes
|
|
153
210
|
|
|
154
211
|
| Class | Applied when |
|
|
@@ -156,6 +213,7 @@ Inline style tokens (set from props):
|
|
|
156
213
|
| `.banner-video` | Always — the root element |
|
|
157
214
|
| `.video` | The `<video>` element |
|
|
158
215
|
| `.fallback` | The `<NuxtImg>` fallback — hidden by default, shown via `prefers-reduced-motion` CSS |
|
|
216
|
+
| `.banner-video__toggle` | The pause/play button — always rendered except under `prefers-reduced-motion: reduce` |
|
|
159
217
|
|
|
160
218
|
## Consumer styling
|
|
161
219
|
|
|
@@ -164,12 +222,19 @@ Use an unscoped style block scoped by a page or section wrapper class. No `:deep
|
|
|
164
222
|
```vue
|
|
165
223
|
<style>
|
|
166
224
|
.my-page {
|
|
167
|
-
|
|
168
|
-
|
|
225
|
+
.banner-video {
|
|
226
|
+
/* Override the md tier's clamp range and the toggle's colours for this page */
|
|
227
|
+
--banner-video-max-height-md: clamp(32rem, 45vw, 64rem);
|
|
228
|
+
--banner-video-toggle-background: var(--brand-overlay);
|
|
229
|
+
--banner-video-toggle-background-hover: var(--brand-overlay-strong);
|
|
230
|
+
}
|
|
169
231
|
}
|
|
170
232
|
</style>
|
|
171
233
|
```
|
|
172
234
|
|
|
235
|
+
See the component's `CONSUMER-STYLING.md` for the full token API and global/page-scoped/
|
|
236
|
+
per-instance override patterns.
|
|
237
|
+
|
|
173
238
|
## Gotcha: aspect-ratio is overridden by max-height at wide viewports
|
|
174
239
|
|
|
175
240
|
`max-height` from the depth tier silently wins over `aspect-ratio` once the viewport is wide enough. For example, at `depth="md"` the max-height clamp caps at `56rem` — so changing `aspectRatio` from `"21/9"` to `"1/1"` produces no visible change on a wide desktop because `max-height` is the binding constraint.
|
|
@@ -178,11 +243,11 @@ Use an unscoped style block scoped by a page or section wrapper class. No `:deep
|
|
|
178
243
|
|
|
179
244
|
```css
|
|
180
245
|
.my-page {
|
|
181
|
-
--
|
|
182
|
-
--
|
|
183
|
-
--
|
|
184
|
-
--
|
|
185
|
-
--
|
|
246
|
+
--banner-video-max-height-xs: 80rem;
|
|
247
|
+
--banner-video-max-height-sm: 80rem;
|
|
248
|
+
--banner-video-max-height-md: 80rem;
|
|
249
|
+
--banner-video-max-height-lg: 80rem;
|
|
250
|
+
--banner-video-max-height-xl: 80rem;
|
|
186
251
|
}
|
|
187
252
|
```
|
|
188
253
|
|
|
@@ -190,11 +255,11 @@ Or via inline `:style` on the parent element (useful for dev QA exploration):
|
|
|
190
255
|
|
|
191
256
|
```vue
|
|
192
257
|
<div :style="{
|
|
193
|
-
'--
|
|
194
|
-
'--
|
|
195
|
-
'--
|
|
196
|
-
'--
|
|
197
|
-
'--
|
|
258
|
+
'--banner-video-max-height-xs': '80rem',
|
|
259
|
+
'--banner-video-max-height-sm': '80rem',
|
|
260
|
+
'--banner-video-max-height-md': '80rem',
|
|
261
|
+
'--banner-video-max-height-lg': '80rem',
|
|
262
|
+
'--banner-video-max-height-xl': '80rem',
|
|
198
263
|
}">
|
|
199
264
|
<BannerVideo aspect-ratio="1/1" depth="lg" ... />
|
|
200
265
|
</div>
|
|
@@ -205,6 +270,6 @@ You must override **all five tier tokens** — overriding only the active depth
|
|
|
205
270
|
## Notes
|
|
206
271
|
|
|
207
272
|
- `loading="eager"` and `decoding="async"` are hardcoded on the fallback `NuxtImg` — it is above the fold by definition.
|
|
208
|
-
- The video has `autoplay muted loop playsinline preload="auto"` — intentional and not configurable. This component is for ambient background video only, not
|
|
273
|
+
- The video has `autoplay muted loop playsinline preload="auto"` — intentional and not configurable. This component is for ambient background video only, not a media player — the pause/play toggle exists purely for WCAG 2.2.2 compliance, not as a general playback UI (no seek bar, volume, etc.).
|
|
209
274
|
- `prefers-reduced-motion` is handled in CSS (`.video { display: none }` + `.fallback { display: block }`), not via JS.
|
|
210
275
|
- Storybook: the `"none"` image provider is active, so `poster` paths pass through unchanged. Always provide explicit `img-width` and `img-height` to avoid the `w=1536` fallback in deployed Storybook.
|
package/.claude/skills/index.md
CHANGED
|
@@ -74,7 +74,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
74
74
|
├── glass-panel.md — GlassPanel props, slots, CSS token API (--glass-panel-bg/border-color/shadow/highlight), theming override
|
|
75
75
|
├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
|
|
76
76
|
├── input-copy-core.md — InputCopyCore: readonly copy-to-clipboard input; props, emits, slots, CSS classes, usage
|
|
77
|
-
├── banner-video.md — BannerVideo: full-width hero video banner, depth tier system, objectFit/objectPosition, reduced-motion fallback, CSS tokens
|
|
77
|
+
├── banner-video.md — BannerVideo: full-width hero video banner, depth tier system, objectFit/objectPosition, playIcon/pauseIcon/toggle-icon slot, reduced-motion fallback, CSS tokens
|
|
78
78
|
├── grid-stack.md — GridStack: CSS Grid z-axis stacking, slot API, z-order rules, sizing, video+overlay and image+text patterns
|
|
79
79
|
├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, CSS tokens, browser support
|
|
80
80
|
├── scroll-reveal-image.md — ScrollRevealImage: single-image parallax reveal, focalX, imgWidth/imgHeight, responsive frame height
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
:style="{ objectFit: props.objectFit }"
|
|
25
25
|
@loadeddata="handleLoadedData"
|
|
26
26
|
@canplay="handleCanPlay"
|
|
27
|
+
@play="isPlaying = true"
|
|
28
|
+
@pause="isPlaying = false"
|
|
27
29
|
>
|
|
28
30
|
<source :src="src" type="video/mp4" />
|
|
29
31
|
</video>
|
|
@@ -38,40 +40,34 @@
|
|
|
38
40
|
decoding="async"
|
|
39
41
|
:style="{ objectFit: props.objectFit, objectPosition: imgObjectPosition }"
|
|
40
42
|
/>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
class="banner-video__toggle"
|
|
46
|
+
:aria-label="isPlaying ? 'Pause background video' : 'Play background video'"
|
|
47
|
+
@click="togglePlayback"
|
|
48
|
+
>
|
|
49
|
+
<slot name="toggle-icon" :is-playing="isPlaying">
|
|
50
|
+
<Icon :name="isPlaying ? pauseIcon : playIcon" aria-hidden="true" />
|
|
51
|
+
</slot>
|
|
52
|
+
</button>
|
|
41
53
|
</component>
|
|
42
54
|
</template>
|
|
43
55
|
|
|
44
56
|
<script setup lang="ts">
|
|
45
57
|
interface Props {
|
|
46
|
-
/** HTML element to render as the root. Defaults to `section` for landmark semantics. */
|
|
47
58
|
tag?: "section" | "div" | "header" | "main" | "article";
|
|
48
|
-
/** Path to the video source file (mp4). */
|
|
49
59
|
src: string;
|
|
50
|
-
/** Path to the fallback/poster image. Used as video poster and as the visible fallback
|
|
51
|
-
* when the video cannot play or when the user prefers reduced motion. */
|
|
52
60
|
poster: string;
|
|
53
61
|
alt?: string;
|
|
54
|
-
/** Intrinsic width of the poster image — required for NuxtImg optimisation. */
|
|
55
62
|
imgWidth?: number;
|
|
56
|
-
/** Intrinsic height of the poster image — required for NuxtImg optimisation. */
|
|
57
63
|
imgHeight?: number;
|
|
58
|
-
/**
|
|
59
|
-
* Depth tier controlling the responsive max-height via a `clamp()` scale.
|
|
60
|
-
* Each tier maps to a `--theme-banner-video-max-height-{depth}` CSS token that
|
|
61
|
-
* consuming pages can override. Defaults to `"md"`.
|
|
62
|
-
*/
|
|
63
64
|
depth?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
64
|
-
/**
|
|
65
|
-
* CSS aspect-ratio of the banner container (e.g. `"16/9"`, `"21/9"`, `"4/3"`).
|
|
66
|
-
* Provides the intrinsic height that `max-height` caps at larger viewport widths.
|
|
67
|
-
*/
|
|
68
65
|
aspectRatio?: string;
|
|
69
|
-
/** How the video and fallback image fill the banner frame. Defaults to `"cover"`. */
|
|
70
66
|
objectFit?: "cover" | "contain" | "fill" | "none" | "scale-down";
|
|
71
|
-
/** Vertical crop position within the banner. Maps to `align-self` on the video and `object-position` on the fallback image. Defaults to `"center"`. */
|
|
72
67
|
verticalPosition?: "start" | "center" | "end";
|
|
73
|
-
/** Horizontal crop position within the banner. Maps to `object-position` on the fallback image. Defaults to `"center"`. */
|
|
74
68
|
horizontalPosition?: "start" | "center" | "end";
|
|
69
|
+
playIcon?: string;
|
|
70
|
+
pauseIcon?: string;
|
|
75
71
|
styleClassPassthrough?: string | string[];
|
|
76
72
|
}
|
|
77
73
|
|
|
@@ -85,6 +81,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
85
81
|
objectFit: "cover",
|
|
86
82
|
verticalPosition: "center",
|
|
87
83
|
horizontalPosition: "center",
|
|
84
|
+
playIcon: "mdi:play",
|
|
85
|
+
pauseIcon: "mdi:pause",
|
|
88
86
|
styleClassPassthrough: () => [],
|
|
89
87
|
});
|
|
90
88
|
|
|
@@ -106,12 +104,22 @@ watch(
|
|
|
106
104
|
);
|
|
107
105
|
|
|
108
106
|
const videoEl = shallowRef<HTMLVideoElement | null>(null);
|
|
107
|
+
const isPlaying = ref(false);
|
|
108
|
+
|
|
109
|
+
const togglePlayback = () => {
|
|
110
|
+
const v = videoEl.value;
|
|
111
|
+
if (!v) return;
|
|
112
|
+
if (v.paused) {
|
|
113
|
+
void v.play();
|
|
114
|
+
} else {
|
|
115
|
+
v.pause();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
109
118
|
|
|
110
119
|
const tryPlay = async () => {
|
|
111
120
|
const v = videoEl.value;
|
|
112
121
|
if (!v) return;
|
|
113
122
|
try {
|
|
114
|
-
// Ensure muted stays true — required for programmatic autoplay in all browsers
|
|
115
123
|
v.muted = true;
|
|
116
124
|
await v.play();
|
|
117
125
|
} catch {
|
|
@@ -123,9 +131,7 @@ const kickOffLoad = async () => {
|
|
|
123
131
|
await nextTick();
|
|
124
132
|
const v = videoEl.value;
|
|
125
133
|
if (!v) return;
|
|
126
|
-
// Force the media element to (re)read its source child and begin fetching
|
|
127
134
|
v.load();
|
|
128
|
-
// Attempt immediate play; loadeddata/canplay handlers will retry once data arrives
|
|
129
135
|
void tryPlay();
|
|
130
136
|
};
|
|
131
137
|
|
|
@@ -136,7 +142,6 @@ const handleCanPlay = () => {
|
|
|
136
142
|
void tryPlay();
|
|
137
143
|
};
|
|
138
144
|
|
|
139
|
-
// Runs on mount AND whenever src changes (covers route-change re-use edge cases)
|
|
140
145
|
watch(
|
|
141
146
|
() => props.src,
|
|
142
147
|
() => {
|
|
@@ -145,7 +150,6 @@ watch(
|
|
|
145
150
|
{ immediate: true, flush: "post" }
|
|
146
151
|
);
|
|
147
152
|
|
|
148
|
-
// Extra safety: when the component becomes active again (e.g. returning via keep-alive)
|
|
149
153
|
onActivated(() => {
|
|
150
154
|
void kickOffLoad();
|
|
151
155
|
});
|
|
@@ -154,7 +158,7 @@ onActivated(() => {
|
|
|
154
158
|
<style lang="css">
|
|
155
159
|
@layer components {
|
|
156
160
|
.banner-video {
|
|
157
|
-
--_max-height: var(--
|
|
161
|
+
--_max-height: var(--banner-video-max-height, clamp(28rem, 38vw, 56rem));
|
|
158
162
|
|
|
159
163
|
display: grid;
|
|
160
164
|
grid-template-areas: "media";
|
|
@@ -164,19 +168,19 @@ onActivated(() => {
|
|
|
164
168
|
overflow: hidden;
|
|
165
169
|
|
|
166
170
|
&[data-depth="xs"] {
|
|
167
|
-
--_max-height: var(--
|
|
171
|
+
--_max-height: var(--banner-video-max-height-xs, clamp(12rem, 15vw, 24rem));
|
|
168
172
|
}
|
|
169
173
|
&[data-depth="sm"] {
|
|
170
|
-
--_max-height: var(--
|
|
174
|
+
--_max-height: var(--banner-video-max-height-sm, clamp(18rem, 22vw, 36rem));
|
|
171
175
|
}
|
|
172
176
|
&[data-depth="md"] {
|
|
173
|
-
--_max-height: var(--
|
|
177
|
+
--_max-height: var(--banner-video-max-height-md, clamp(28rem, 38vw, 56rem));
|
|
174
178
|
}
|
|
175
179
|
&[data-depth="lg"] {
|
|
176
|
-
--_max-height: var(--
|
|
180
|
+
--_max-height: var(--banner-video-max-height-lg, clamp(40rem, 52vw, 72rem));
|
|
177
181
|
}
|
|
178
182
|
&[data-depth="xl"] {
|
|
179
|
-
--_max-height: var(--
|
|
183
|
+
--_max-height: var(--banner-video-max-height-xl, clamp(52rem, 65vw, 90rem));
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
.video {
|
|
@@ -196,6 +200,32 @@ onActivated(() => {
|
|
|
196
200
|
height: 100%;
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
.banner-video__toggle {
|
|
204
|
+
grid-area: media;
|
|
205
|
+
align-self: end;
|
|
206
|
+
justify-self: end;
|
|
207
|
+
margin: var(--banner-video-toggle-offset, 1.2rem);
|
|
208
|
+
z-index: 1;
|
|
209
|
+
|
|
210
|
+
display: grid;
|
|
211
|
+
place-items: center;
|
|
212
|
+
width: var(--banner-video-toggle-size, 3.2rem);
|
|
213
|
+
height: var(--banner-video-toggle-size, 3.2rem);
|
|
214
|
+
padding: 0;
|
|
215
|
+
|
|
216
|
+
background-color: var(--banner-video-toggle-background, oklch(0% 0 0 / 0.4));
|
|
217
|
+
color: var(--banner-video-toggle-icon-color, white);
|
|
218
|
+
border: none;
|
|
219
|
+
border-radius: 100vw;
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
transition: background-color 0.2s ease;
|
|
222
|
+
|
|
223
|
+
&:hover,
|
|
224
|
+
&:focus-visible {
|
|
225
|
+
background-color: var(--banner-video-toggle-background-hover, oklch(0% 0 0 / 0.6));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
199
229
|
@media (prefers-reduced-motion: reduce) {
|
|
200
230
|
.video {
|
|
201
231
|
display: none;
|
|
@@ -203,6 +233,9 @@ onActivated(() => {
|
|
|
203
233
|
.fallback {
|
|
204
234
|
display: block;
|
|
205
235
|
}
|
|
236
|
+
.banner-video__toggle {
|
|
237
|
+
display: none;
|
|
238
|
+
}
|
|
206
239
|
}
|
|
207
240
|
}
|
|
208
241
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# BannerVideo — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
All `--banner-video-*` tokens are the stable override surface. Set them globally in a theme
|
|
6
|
+
file, scoped to a page wrapper, or per-instance via `styleClassPassthrough`.
|
|
7
|
+
|
|
8
|
+
### Depth tier max-height
|
|
9
|
+
|
|
10
|
+
| Token | Default | Controls |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `--banner-video-max-height-xs` | `clamp(12rem, 15vw, 24rem)` | Max height at `depth="xs"` |
|
|
13
|
+
| `--banner-video-max-height-sm` | `clamp(18rem, 22vw, 36rem)` | Max height at `depth="sm"` |
|
|
14
|
+
| `--banner-video-max-height-md` | `clamp(28rem, 38vw, 56rem)` | Max height at `depth="md"` (default) |
|
|
15
|
+
| `--banner-video-max-height-lg` | `clamp(40rem, 52vw, 72rem)` | Max height at `depth="lg"` |
|
|
16
|
+
| `--banner-video-max-height-xl` | `clamp(52rem, 65vw, 90rem)` | Max height at `depth="xl"` |
|
|
17
|
+
| `--banner-video-max-height` | `clamp(28rem, 38vw, 56rem)` | Fallback used if the active depth tier's own token isn't set |
|
|
18
|
+
|
|
19
|
+
### Pause/play toggle button
|
|
20
|
+
|
|
21
|
+
| Token | Default | Controls |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `--banner-video-toggle-size` | `3.2rem` | Width and height of the toggle button |
|
|
24
|
+
| `--banner-video-toggle-offset` | `1.2rem` | Margin from the bottom-right corner of the banner |
|
|
25
|
+
| `--banner-video-toggle-background` | `oklch(0% 0 0 / 0.4)` | Button background |
|
|
26
|
+
| `--banner-video-toggle-background-hover` | `oklch(0% 0 0 / 0.6)` | Button background on hover/focus |
|
|
27
|
+
| `--banner-video-toggle-icon-color` | `white` | Icon colour (and any text colour, via `color`) |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Icon and content overrides — props and slots, not CSS
|
|
32
|
+
|
|
33
|
+
The toggle's icon is prop-driven, not hardcoded:
|
|
34
|
+
|
|
35
|
+
```vue
|
|
36
|
+
<BannerVideo
|
|
37
|
+
src="/videos/hero.mp4"
|
|
38
|
+
poster="/images/hero-poster.jpg"
|
|
39
|
+
play-icon="mdi:play-circle"
|
|
40
|
+
pause-icon="mdi:pause-circle"
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For anything beyond swapping the icon name (e.g. a custom SVG, or an icon set the `Icon`
|
|
45
|
+
component doesn't cover), replace it with the `toggle-icon` scoped slot, which receives
|
|
46
|
+
`isPlaying`:
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<BannerVideo src="/videos/hero.mp4" poster="/images/hero-poster.jpg">
|
|
50
|
+
<template #toggle-icon="{ isPlaying }">
|
|
51
|
+
<MyCustomIcon :name="isPlaying ? 'pause' : 'play'" />
|
|
52
|
+
</template>
|
|
53
|
+
</BannerVideo>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Global theming — recommended approach
|
|
59
|
+
|
|
60
|
+
Create `assets/styles/setup/07.components/banner-video.css` in the consuming app and set
|
|
61
|
+
tokens on `:root`. This applies to every `BannerVideo` across the site.
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
/* assets/styles/setup/07.components/banner-video.css */
|
|
65
|
+
:root {
|
|
66
|
+
--banner-video-max-height-md: clamp(32rem, 45vw, 64rem);
|
|
67
|
+
--banner-video-toggle-background: var(--brand-overlay);
|
|
68
|
+
--banner-video-toggle-background-hover: var(--brand-overlay-strong);
|
|
69
|
+
--banner-video-toggle-icon-color: var(--brand-on-overlay);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Page-scoped overrides
|
|
76
|
+
|
|
77
|
+
Override tokens for a specific banner by scoping them under the page or layout wrapper.
|
|
78
|
+
No `:deep()` is required (component styles are unscoped).
|
|
79
|
+
|
|
80
|
+
```css
|
|
81
|
+
/* In the consuming page's unscoped <style> block */
|
|
82
|
+
.homepage-hero {
|
|
83
|
+
.banner-video {
|
|
84
|
+
--banner-video-max-height-xl: 80rem;
|
|
85
|
+
--banner-video-toggle-size: 4rem;
|
|
86
|
+
--banner-video-toggle-offset: 2rem;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Per-instance overrides via styleClassPassthrough
|
|
94
|
+
|
|
95
|
+
Use sparingly — prefer global or page-scoped CSS. When a single instance needs a distinct
|
|
96
|
+
visual style, pass a modifier class:
|
|
97
|
+
|
|
98
|
+
```vue
|
|
99
|
+
<BannerVideo :style-class-passthrough="['quiet-toggle']" src="…" poster="…" />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```css
|
|
103
|
+
.banner-video.quiet-toggle {
|
|
104
|
+
--banner-video-toggle-background: transparent;
|
|
105
|
+
--banner-video-toggle-background-hover: oklch(0% 0 0 / 0.3);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Notes
|
|
112
|
+
|
|
113
|
+
- `--banner-video-max-height` (no suffix) only applies if the active `data-depth` tier's own
|
|
114
|
+
token isn't set — in practice this only matters if you delete a depth tier's token without
|
|
115
|
+
replacing it. Prefer setting the specific `-{depth}` token you need.
|
|
116
|
+
- The toggle button's `border-radius` (`100vw`, i.e. a full circle) and `transition-duration`
|
|
117
|
+
(`0.2s`) are not tokenized — they're low-variance visual details rather than something
|
|
118
|
+
consuming apps are expected to reskin per-brand.
|
|
119
|
+
- `aspectRatio`, `objectFit`, `verticalPosition`, and `horizontalPosition` are props, not
|
|
120
|
+
tokens — set them directly on the component.
|
|
@@ -39,7 +39,7 @@ const meta: Meta<typeof BannerVideo> = {
|
|
|
39
39
|
depth: {
|
|
40
40
|
control: "select",
|
|
41
41
|
options: ["xs", "sm", "md", "lg", "xl"],
|
|
42
|
-
description: "Responsive max-height tier. Each maps to a clamp() scale; override via --
|
|
42
|
+
description: "Responsive max-height tier. Each maps to a clamp() scale; override via --banner-video-max-height-{depth}",
|
|
43
43
|
table: { category: "Layout" },
|
|
44
44
|
},
|
|
45
45
|
aspectRatio: {
|
|
@@ -65,6 +65,16 @@ const meta: Meta<typeof BannerVideo> = {
|
|
|
65
65
|
description: "Horizontal crop position — start (left), center, or end (right). Maps to object-position on the fallback image",
|
|
66
66
|
table: { category: "Appearance" },
|
|
67
67
|
},
|
|
68
|
+
playIcon: {
|
|
69
|
+
control: "text",
|
|
70
|
+
description: "Icon name for the toggle button before playback starts (any Iconify icon)",
|
|
71
|
+
table: { category: "Appearance" },
|
|
72
|
+
},
|
|
73
|
+
pauseIcon: {
|
|
74
|
+
control: "text",
|
|
75
|
+
description: "Icon name for the toggle button once playback starts (any Iconify icon)",
|
|
76
|
+
table: { category: "Appearance" },
|
|
77
|
+
},
|
|
68
78
|
styleClassPassthrough: {
|
|
69
79
|
table: { disable: true },
|
|
70
80
|
},
|
|
@@ -73,7 +83,7 @@ const meta: Meta<typeof BannerVideo> = {
|
|
|
73
83
|
docs: {
|
|
74
84
|
description: {
|
|
75
85
|
component:
|
|
76
|
-
"A full-width banner that plays a muted, looping mp4 video. The poster image is shown before the video loads, when the video fails to play, and whenever the user has `prefers-reduced-motion: reduce` set. The banner is sized via `aspect-ratio` with a responsive `max-height` driven by the `depth` tier (`xs` → `xl`). Each tier uses a `clamp()` scale and exposes a `--
|
|
86
|
+
"A full-width banner that plays a muted, looping mp4 video. The poster image is shown before the video loads, when the video fails to play, and whenever the user has `prefers-reduced-motion: reduce` set. The banner is sized via `aspect-ratio` with a responsive `max-height` driven by the `depth` tier (`xs` → `xl`). Each tier uses a `clamp()` scale and exposes a `--banner-video-max-height-{depth}` token for consuming pages to override. `objectFit` controls how media fills the frame. `verticalPosition` and `horizontalPosition` control the crop focal point. The pause/play toggle's icon is configurable via `playIcon`/`pauseIcon` props, or fully replaceable via the `toggle-icon` scoped slot.",
|
|
77
87
|
},
|
|
78
88
|
},
|
|
79
89
|
},
|
|
@@ -178,3 +188,44 @@ export const Standard169: Story = {
|
|
|
178
188
|
},
|
|
179
189
|
},
|
|
180
190
|
};
|
|
191
|
+
|
|
192
|
+
export const CustomToggleIcons: Story = {
|
|
193
|
+
args: {
|
|
194
|
+
...Default.args,
|
|
195
|
+
playIcon: "mdi:play-circle",
|
|
196
|
+
pauseIcon: "mdi:pause-circle",
|
|
197
|
+
},
|
|
198
|
+
parameters: {
|
|
199
|
+
docs: {
|
|
200
|
+
description: {
|
|
201
|
+
story: "Custom icon names passed via playIcon and pauseIcon props — swaps the toggle button's icon without touching markup.",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const CustomToggleIconSlot: Story = {
|
|
208
|
+
args: {
|
|
209
|
+
...Default.args,
|
|
210
|
+
},
|
|
211
|
+
render: (args) => ({
|
|
212
|
+
components: { BannerVideo },
|
|
213
|
+
setup() {
|
|
214
|
+
return { args };
|
|
215
|
+
},
|
|
216
|
+
template: `
|
|
217
|
+
<BannerVideo v-bind="args">
|
|
218
|
+
<template #toggle-icon="{ isPlaying }">
|
|
219
|
+
<span style="font-size:1.4rem;font-weight:700;">{{ isPlaying ? 'II' : '▶' }}</span>
|
|
220
|
+
</template>
|
|
221
|
+
</BannerVideo>
|
|
222
|
+
`,
|
|
223
|
+
}),
|
|
224
|
+
parameters: {
|
|
225
|
+
docs: {
|
|
226
|
+
description: {
|
|
227
|
+
story: "The toggle-icon scoped slot replaces the toggle button's icon entirely, receiving isPlaying — use when an Iconify icon name isn't enough (a custom SVG, or a different icon set).",
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
};
|
|
@@ -127,6 +127,51 @@ describe("BannerVideo", () => {
|
|
|
127
127
|
expect(play).toHaveBeenCalled();
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
// ─── Pause/play toggle (WCAG 2.2.2) ─────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
it("renders a pause/play toggle button", async () => {
|
|
133
|
+
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
134
|
+
expect(wrapper.find(".banner-video__toggle").exists()).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("toggle defaults to a 'Pause background video' label once playback starts", async () => {
|
|
138
|
+
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
139
|
+
await wrapper.find("video").trigger("play");
|
|
140
|
+
expect(wrapper.find(".banner-video__toggle").attributes("aria-label")).toBe("Pause background video");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("toggle reads 'Play background video' before playback has started", async () => {
|
|
144
|
+
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
145
|
+
expect(wrapper.find(".banner-video__toggle").attributes("aria-label")).toBe("Play background video");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("clicking the toggle calls pause() while playing", async () => {
|
|
149
|
+
const pause = vi.fn();
|
|
150
|
+
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
151
|
+
const videoElement = wrapper.find("video").element as HTMLVideoElement;
|
|
152
|
+
videoElement.pause = pause;
|
|
153
|
+
Object.defineProperty(videoElement, "paused", { value: false, configurable: true });
|
|
154
|
+
await wrapper.find(".banner-video__toggle").trigger("click");
|
|
155
|
+
expect(pause).toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("clicking the toggle calls play() while paused", async () => {
|
|
159
|
+
const play = vi.fn().mockResolvedValue(undefined);
|
|
160
|
+
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
161
|
+
const videoElement = wrapper.find("video").element as HTMLVideoElement;
|
|
162
|
+
videoElement.play = play;
|
|
163
|
+
Object.defineProperty(videoElement, "paused", { value: true, configurable: true });
|
|
164
|
+
await wrapper.find(".banner-video__toggle").trigger("click");
|
|
165
|
+
expect(play).toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("updates aria-label to 'Play background video' after the video pauses", async () => {
|
|
169
|
+
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
170
|
+
await wrapper.find("video").trigger("play");
|
|
171
|
+
await wrapper.find("video").trigger("pause");
|
|
172
|
+
expect(wrapper.find(".banner-video__toggle").attributes("aria-label")).toBe("Play background video");
|
|
173
|
+
});
|
|
174
|
+
|
|
130
175
|
// ─── Fallback image ──────────────────────────────────────────────────────
|
|
131
176
|
|
|
132
177
|
it("renders a NuxtImg fallback element", async () => {
|
|
@@ -349,4 +394,54 @@ describe("BannerVideo", () => {
|
|
|
349
394
|
expect(wrapper.classes()).not.toContain("original");
|
|
350
395
|
expect(wrapper.classes()).toContain("updated");
|
|
351
396
|
});
|
|
397
|
+
|
|
398
|
+
// ─── Toggle icon ─────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
it("renders the default play icon before playback starts", async () => {
|
|
401
|
+
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
402
|
+
expect(wrapper.find(".banner-video__toggle").html()).toContain("mdi:play");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("renders the default pause icon once playback starts", async () => {
|
|
406
|
+
const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
407
|
+
await wrapper.find("video").trigger("play");
|
|
408
|
+
expect(wrapper.find(".banner-video__toggle").html()).toContain("mdi:pause");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("renders a custom playIcon", async () => {
|
|
412
|
+
const wrapper = await mountSuspended(BannerVideo, {
|
|
413
|
+
props: { ...defaultProps, playIcon: "mdi:play-circle" },
|
|
414
|
+
});
|
|
415
|
+
expect(wrapper.find(".banner-video__toggle").html()).toContain("mdi:play-circle");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("renders a custom pauseIcon", async () => {
|
|
419
|
+
const wrapper = await mountSuspended(BannerVideo, {
|
|
420
|
+
props: { ...defaultProps, pauseIcon: "mdi:pause-circle" },
|
|
421
|
+
});
|
|
422
|
+
await wrapper.find("video").trigger("play");
|
|
423
|
+
expect(wrapper.find(".banner-video__toggle").html()).toContain("mdi:pause-circle");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("replaces the toggle icon via the toggle-icon slot", async () => {
|
|
427
|
+
const wrapper = await mountSuspended(BannerVideo, {
|
|
428
|
+
props: defaultProps,
|
|
429
|
+
slots: {
|
|
430
|
+
"toggle-icon": '<span class="test-custom-icon">custom</span>',
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
expect(wrapper.find(".test-custom-icon").exists()).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("passes isPlaying to the toggle-icon slot", async () => {
|
|
437
|
+
const wrapper = await mountSuspended(BannerVideo, {
|
|
438
|
+
props: defaultProps,
|
|
439
|
+
slots: {
|
|
440
|
+
"toggle-icon": `<span class="test-is-playing">{{ params.isPlaying }}</span>`,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
expect(wrapper.find(".test-is-playing").text()).toBe("false");
|
|
444
|
+
await wrapper.find("video").trigger("play");
|
|
445
|
+
expect(wrapper.find(".test-is-playing").text()).toBe("true");
|
|
446
|
+
});
|
|
352
447
|
});
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
exports[`BannerVideo > renders correct HTML structure (all props set) 1`] = `
|
|
4
4
|
"<header class="banner-video full-bleed" data-depth="xl" style="--_aspect-ratio: 16/9; --_align-self: center; --_justify-self: center;"><video class="video" autoplay="" muted="" loop="" playsinline="" preload="auto" fetchpriority="high" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;">
|
|
5
5
|
<source src="/images/banners/video/lake-banner.mp4" type="video/mp4">
|
|
6
|
-
</video><img width="1280" height="720" data-nuxt-img="" srcset="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg 1x, /_ipx/s_2560x1440/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="Lake banner" loading="eager" fetchpriority="high" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg"></header>"
|
|
6
|
+
</video><img width="1280" height="720" data-nuxt-img="" srcset="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg 1x, /_ipx/s_2560x1440/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="Lake banner" loading="eager" fetchpriority="high" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg"><button type="button" class="banner-video__toggle" aria-label="Play background video"><span class="iconify i-mdi:play" aria-hidden="true"></span></button></header>"
|
|
7
7
|
`;
|
|
8
8
|
|
|
9
9
|
exports[`BannerVideo > renders correct HTML structure (default props) 1`] = `
|
|
10
10
|
"<div class="banner-video" data-depth="md" style="--_aspect-ratio: 21/9; --_align-self: center; --_justify-self: center;"><video class="video" autoplay="" muted="" loop="" playsinline="" preload="auto" fetchpriority="high" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;">
|
|
11
11
|
<source src="/images/banners/video/lake-banner.mp4" type="video/mp4">
|
|
12
|
-
</video><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg 1x, /_ipx/s_3840x2160/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="" loading="eager" fetchpriority="high" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg"></div>"
|
|
12
|
+
</video><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg 1x, /_ipx/s_3840x2160/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="" loading="eager" fetchpriority="high" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg"><button type="button" class="banner-video__toggle" aria-label="Play background video"><span class="iconify i-mdi:play" aria-hidden="true"></span></button></div>"
|
|
13
13
|
`;
|