@revenuecat/purchases-ui-js 4.1.0 → 4.2.0
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/dist/components/carousel/Carousel.stories.svelte +29 -0
- package/dist/components/carousel/Carousel.svelte +74 -8
- package/dist/components/paywall/BackgroundVideoSurface.svelte +162 -0
- package/dist/components/paywall/BackgroundVideoSurface.svelte.d.ts +12 -0
- package/dist/components/paywall/Paywall.stories.svelte +133 -0
- package/dist/components/paywall/Sheet.svelte +66 -3
- package/dist/components/paywall/ViewportBackdrop.svelte +40 -9
- package/dist/components/paywall/fixtures/sheet-video-stacking-paywall.d.ts +6 -0
- package/dist/components/paywall/fixtures/sheet-video-stacking-paywall.js +42 -0
- package/dist/components/stack/Stack.stories.svelte +74 -0
- package/dist/components/stack/Stack.svelte +58 -3
- package/dist/components/tabs/Tabs.stories.svelte +27 -0
- package/dist/components/tabs/Tabs.svelte +69 -4
- package/dist/components/tabs/tabs-video-stacking-story-args.d.ts +3 -0
- package/dist/components/tabs/tabs-video-stacking-story-args.js +131 -0
- package/dist/components/video/Video.svelte +32 -56
- package/dist/stories/video-background-story-fixture.d.ts +3 -0
- package/dist/stories/video-background-story-fixture.js +36 -0
- package/dist/types/background.d.ts +9 -1
- package/dist/ui/molecules/button.svelte +2 -2
- package/dist/utils/background-utils.d.ts +30 -1
- package/dist/utils/background-utils.js +53 -5
- package/dist/utils/match-media-compat.d.ts +4 -0
- package/dist/utils/match-media-compat.js +11 -0
- package/dist/utils/video-background-host.d.ts +31 -0
- package/dist/utils/video-background-host.js +25 -0
- package/dist/utils/video-inline-playback.d.ts +25 -0
- package/dist/utils/video-inline-playback.js +78 -0
- package/dist/web-components/index.css +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { viewportDecorator } from "../../stories/viewport-decorator";
|
|
5
5
|
import type { CarouselProps } from "../../types/components/carousel";
|
|
6
6
|
import { DEFAULT_SPACING } from "../../utils/constants";
|
|
7
|
+
import { STORY_BACKGROUND_VIDEO_FILL } from "../../stories/video-background-story-fixture";
|
|
7
8
|
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
8
9
|
import Carousel from "./Carousel.svelte";
|
|
9
10
|
|
|
@@ -442,6 +443,34 @@
|
|
|
442
443
|
});
|
|
443
444
|
</script>
|
|
444
445
|
|
|
446
|
+
<Story
|
|
447
|
+
name="Surface — video background (stacking / z-order)"
|
|
448
|
+
parameters={{
|
|
449
|
+
chromatic: { disableSnapshot: true },
|
|
450
|
+
docs: {
|
|
451
|
+
description: {
|
|
452
|
+
story:
|
|
453
|
+
"**Z-order:** video layer → tinted `::before` → carousel clip (slides + controls). Rounded corners should clip the backdrop; Chromatic skips motion snapshots.",
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
}}
|
|
457
|
+
args={{
|
|
458
|
+
background: STORY_BACKGROUND_VIDEO_FILL,
|
|
459
|
+
shape: {
|
|
460
|
+
type: "rectangle",
|
|
461
|
+
corners: {
|
|
462
|
+
bottom_leading: 20,
|
|
463
|
+
bottom_trailing: 20,
|
|
464
|
+
top_leading: 20,
|
|
465
|
+
top_trailing: 20,
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
auto_advance: null,
|
|
469
|
+
loop: false,
|
|
470
|
+
initial_page_index: 0,
|
|
471
|
+
}}
|
|
472
|
+
/>
|
|
473
|
+
|
|
445
474
|
<Story
|
|
446
475
|
name="No loop - first page"
|
|
447
476
|
args={{
|
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
import { getColorModeContext } from "../../stores/color-mode";
|
|
3
3
|
import { getPaywallContext } from "../../stores/paywall";
|
|
4
4
|
import { getSelectedStateContext } from "../../stores/selected";
|
|
5
|
+
import BackgroundVideoSurface from "../paywall/BackgroundVideoSurface.svelte";
|
|
5
6
|
import type { CarouselProps } from "../../types/components/carousel";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
mapBackground,
|
|
9
|
+
resolveBackgroundVideoForSurface,
|
|
10
|
+
} from "../../utils/background-utils";
|
|
11
|
+
import {
|
|
12
|
+
rcVideoBackgroundHostClass,
|
|
13
|
+
videoBackgroundHostStyles,
|
|
14
|
+
} from "../../utils/video-background-host";
|
|
7
15
|
import {
|
|
8
16
|
css,
|
|
9
17
|
mapBorder,
|
|
@@ -33,7 +41,7 @@
|
|
|
33
41
|
page_peek,
|
|
34
42
|
page_control,
|
|
35
43
|
initial_page_index,
|
|
36
|
-
loop,
|
|
44
|
+
loop: carouselPagingLoop,
|
|
37
45
|
auto_advance,
|
|
38
46
|
} = $derived.by(() => {
|
|
39
47
|
return {
|
|
@@ -46,13 +54,26 @@
|
|
|
46
54
|
const colorMode = $derived(getColorMode());
|
|
47
55
|
const { emitComponentInteraction } = getPaywallContext();
|
|
48
56
|
|
|
57
|
+
const hasVideoBg = $derived(background?.type === "video");
|
|
58
|
+
const videoSurface = $derived.by(() => {
|
|
59
|
+
if (!hasVideoBg) return null;
|
|
60
|
+
const bg = background;
|
|
61
|
+
if (!bg || bg.type !== "video") return null;
|
|
62
|
+
return resolveBackgroundVideoForSurface(colorMode, bg);
|
|
63
|
+
});
|
|
64
|
+
const borderRadiusCssForBg = $derived(mapBorderRadius(shape));
|
|
65
|
+
|
|
49
66
|
const carouselStyle = $derived(
|
|
50
67
|
css({
|
|
51
68
|
margin: mapSpacing(margin),
|
|
52
69
|
padding: mapSpacing(padding),
|
|
70
|
+
...videoBackgroundHostStyles({
|
|
71
|
+
hasVideoBg,
|
|
72
|
+
positionRelative: true,
|
|
73
|
+
}),
|
|
53
74
|
...mapBackground(colorMode, null, background),
|
|
54
75
|
...mapBorder(colorMode, border),
|
|
55
|
-
"border-radius":
|
|
76
|
+
"border-radius": borderRadiusCssForBg,
|
|
56
77
|
"box-shadow": mapShadow(colorMode, shadow),
|
|
57
78
|
"transition-duration": `${auto_advance?.ms_transition_time ?? 0}ms`,
|
|
58
79
|
}),
|
|
@@ -67,7 +88,7 @@
|
|
|
67
88
|
);
|
|
68
89
|
|
|
69
90
|
const prevPages = $derived.by(() => {
|
|
70
|
-
if (!
|
|
91
|
+
if (!carouselPagingLoop) {
|
|
71
92
|
return [];
|
|
72
93
|
}
|
|
73
94
|
|
|
@@ -85,7 +106,7 @@
|
|
|
85
106
|
});
|
|
86
107
|
|
|
87
108
|
const nextPages = $derived.by(() => {
|
|
88
|
-
if (!
|
|
109
|
+
if (!carouselPagingLoop) {
|
|
89
110
|
return [];
|
|
90
111
|
}
|
|
91
112
|
|
|
@@ -138,7 +159,7 @@
|
|
|
138
159
|
}
|
|
139
160
|
}
|
|
140
161
|
|
|
141
|
-
const loopOffset = $derived(
|
|
162
|
+
const loopOffset = $derived(carouselPagingLoop ? 2 : 0);
|
|
142
163
|
|
|
143
164
|
function setSliderTranslate(translate: number) {
|
|
144
165
|
if (slider !== null) {
|
|
@@ -238,7 +259,7 @@
|
|
|
238
259
|
return;
|
|
239
260
|
}
|
|
240
261
|
|
|
241
|
-
const [min, max] =
|
|
262
|
+
const [min, max] = carouselPagingLoop
|
|
242
263
|
? [startTranslation - carouselWidth, startTranslation + carouselWidth]
|
|
243
264
|
: [-(pages.length - 1) * pageWidth, 0];
|
|
244
265
|
const translation = getTranslation(slider);
|
|
@@ -252,7 +273,29 @@
|
|
|
252
273
|
}
|
|
253
274
|
</script>
|
|
254
275
|
|
|
255
|
-
<div
|
|
276
|
+
<div
|
|
277
|
+
class={[
|
|
278
|
+
"carousel",
|
|
279
|
+
"rc-gradient-border",
|
|
280
|
+
hasVideoBg ? rcVideoBackgroundHostClass("carousel") : "",
|
|
281
|
+
]
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
.join(" ")}
|
|
284
|
+
style={carouselStyle}
|
|
285
|
+
>
|
|
286
|
+
{#if videoSurface}
|
|
287
|
+
<div class="carousel-bg-video-mount">
|
|
288
|
+
<BackgroundVideoSurface
|
|
289
|
+
url={videoSurface.url}
|
|
290
|
+
urlLowRes={videoSurface.url_low_res}
|
|
291
|
+
objectFit={videoSurface.fit}
|
|
292
|
+
objectPosition={videoSurface.position}
|
|
293
|
+
poster={videoSurface.posterWebp}
|
|
294
|
+
mute={videoSurface.mute}
|
|
295
|
+
loop={videoSurface.loop}
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
{/if}
|
|
256
299
|
<div class="carousel-clip">
|
|
257
300
|
<div
|
|
258
301
|
bind:this={slider}
|
|
@@ -324,6 +367,29 @@
|
|
|
324
367
|
border-radius: inherit;
|
|
325
368
|
}
|
|
326
369
|
|
|
370
|
+
.carousel-bg-video-mount {
|
|
371
|
+
position: absolute;
|
|
372
|
+
inset: 0;
|
|
373
|
+
z-index: 1;
|
|
374
|
+
border-radius: inherit;
|
|
375
|
+
overflow: hidden;
|
|
376
|
+
pointer-events: none;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.carousel.rc-carousel-video-bg::before {
|
|
380
|
+
content: "";
|
|
381
|
+
position: absolute;
|
|
382
|
+
inset: 0;
|
|
383
|
+
background: var(--overlay);
|
|
384
|
+
pointer-events: none;
|
|
385
|
+
z-index: 2;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.carousel.rc-carousel-video-bg .carousel-clip {
|
|
389
|
+
position: relative;
|
|
390
|
+
z-index: 3;
|
|
391
|
+
}
|
|
392
|
+
|
|
327
393
|
.slider {
|
|
328
394
|
display: flex;
|
|
329
395
|
flex-direction: row;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, untrack } from "svelte";
|
|
3
|
+
import { subscribeCompatMediaChange } from "../../utils/match-media-compat";
|
|
4
|
+
import {
|
|
5
|
+
subscribeDecorativeVideoAutoplayAttempts,
|
|
6
|
+
tryDecorativeAutoplayWithMuteFallback,
|
|
7
|
+
} from "../../utils/video-inline-playback";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
url: string;
|
|
11
|
+
urlLowRes?: string | null;
|
|
12
|
+
objectFit: string;
|
|
13
|
+
objectPosition: string;
|
|
14
|
+
poster?: string | null;
|
|
15
|
+
mute: boolean;
|
|
16
|
+
loop: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
url,
|
|
21
|
+
urlLowRes = null,
|
|
22
|
+
objectFit,
|
|
23
|
+
objectPosition,
|
|
24
|
+
poster = null,
|
|
25
|
+
mute,
|
|
26
|
+
loop,
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
let videoElement = $state<HTMLVideoElement | null>(null);
|
|
30
|
+
let hasError = $state(false);
|
|
31
|
+
let reduceMotion = $state(false);
|
|
32
|
+
/** When unmuted autoplay fails, retry muted (Safari autoplay policy). */
|
|
33
|
+
let playbackMutedOverride = $state(false);
|
|
34
|
+
|
|
35
|
+
onMount(() => {
|
|
36
|
+
if (typeof window === "undefined") return;
|
|
37
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
38
|
+
reduceMotion = mq.matches;
|
|
39
|
+
const onChange = () => {
|
|
40
|
+
reduceMotion = mq.matches;
|
|
41
|
+
};
|
|
42
|
+
return subscribeCompatMediaChange(mq, onChange);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
url;
|
|
47
|
+
urlLowRes;
|
|
48
|
+
untrack(() => {
|
|
49
|
+
hasError = false;
|
|
50
|
+
playbackMutedOverride = false;
|
|
51
|
+
});
|
|
52
|
+
queueMicrotask(() => {
|
|
53
|
+
videoElement?.load();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const showPosterOnly = $derived(reduceMotion || hasError);
|
|
58
|
+
|
|
59
|
+
const mediaStyle = $derived(
|
|
60
|
+
`object-fit:${objectFit};object-position:${objectPosition}`,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
$effect(() => {
|
|
64
|
+
if (showPosterOnly || !videoElement) {
|
|
65
|
+
untrack(() => {
|
|
66
|
+
playbackMutedOverride = false;
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const el = videoElement;
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
const isCancelled = () => cancelled;
|
|
74
|
+
|
|
75
|
+
const tryPlay = () => {
|
|
76
|
+
tryDecorativeAutoplayWithMuteFallback(el, {
|
|
77
|
+
cancelled: isCancelled,
|
|
78
|
+
preferMuted: mute,
|
|
79
|
+
getMutedOverride: () => playbackMutedOverride,
|
|
80
|
+
setMutedOverride: (v) =>
|
|
81
|
+
untrack(() => {
|
|
82
|
+
playbackMutedOverride = v;
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const removeMediaListeners = subscribeDecorativeVideoAutoplayAttempts(
|
|
88
|
+
el,
|
|
89
|
+
tryPlay,
|
|
90
|
+
isCancelled,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
cancelled = true;
|
|
95
|
+
removeMediaListeners();
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function onVideoError() {
|
|
100
|
+
hasError = true;
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
{#if showPosterOnly && poster}
|
|
105
|
+
<div class="rc-bg-video" aria-hidden="true">
|
|
106
|
+
<img src={poster} alt="" class="fill" style={mediaStyle} />
|
|
107
|
+
</div>
|
|
108
|
+
{:else if showPosterOnly}
|
|
109
|
+
<!-- No poster/fallback asset for reduced motion or failed load -->
|
|
110
|
+
{:else}
|
|
111
|
+
<!-- svelte-ignore a11y_media_has_caption decorative background -->
|
|
112
|
+
<div class="rc-bg-video" aria-hidden="true">
|
|
113
|
+
<video
|
|
114
|
+
bind:this={videoElement}
|
|
115
|
+
class="fill"
|
|
116
|
+
style={mediaStyle}
|
|
117
|
+
{loop}
|
|
118
|
+
muted={mute || playbackMutedOverride}
|
|
119
|
+
playsinline
|
|
120
|
+
autoplay
|
|
121
|
+
preload="auto"
|
|
122
|
+
poster={poster ?? undefined}
|
|
123
|
+
onerror={onVideoError}
|
|
124
|
+
>
|
|
125
|
+
{#if urlLowRes}
|
|
126
|
+
<!-- Narrow viewports use low-res asset first; larger screens use primary URL. -->
|
|
127
|
+
<source src={urlLowRes} media="(max-width: 600px)" />
|
|
128
|
+
{/if}
|
|
129
|
+
<source src={url} />
|
|
130
|
+
</video>
|
|
131
|
+
</div>
|
|
132
|
+
{/if}
|
|
133
|
+
|
|
134
|
+
<style>
|
|
135
|
+
.rc-bg-video {
|
|
136
|
+
position: absolute;
|
|
137
|
+
inset: 0;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
z-index: 1;
|
|
140
|
+
pointer-events: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fill the layer with absolute edges (percent heights are unreliable under nested
|
|
145
|
+
* flex/absolute hosts). Beat Paywall's global `.paywall video { max-width: 100% }`
|
|
146
|
+
* so `object-fit: cover|contain` applies to the full backdrop box (e.g. carousel).
|
|
147
|
+
*/
|
|
148
|
+
.rc-bg-video :is(video, img).fill {
|
|
149
|
+
position: absolute;
|
|
150
|
+
inset: 0;
|
|
151
|
+
width: 100%;
|
|
152
|
+
height: 100%;
|
|
153
|
+
max-width: none;
|
|
154
|
+
max-height: none;
|
|
155
|
+
display: block;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.rc-bg-video video.fill {
|
|
159
|
+
transform: translateZ(0);
|
|
160
|
+
-webkit-transform: translateZ(0);
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
url: string;
|
|
3
|
+
urlLowRes?: string | null;
|
|
4
|
+
objectFit: string;
|
|
5
|
+
objectPosition: string;
|
|
6
|
+
poster?: string | null;
|
|
7
|
+
mute: boolean;
|
|
8
|
+
loop: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare const BackgroundVideoSurface: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type BackgroundVideoSurface = ReturnType<typeof BackgroundVideoSurface>;
|
|
12
|
+
export default BackgroundVideoSurface;
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { BACKGROUND_PAYWALL } from "./fixtures/background-paywall";
|
|
28
28
|
import { OVERRIDE_PAYWALL } from "./fixtures/override-paywall";
|
|
29
29
|
import { SHEET_PAYWALL } from "./fixtures/sheet-paywall";
|
|
30
|
+
import { SHEET_PAYWALL_VIDEO_STACKING } from "./fixtures/sheet-video-stacking-paywall";
|
|
30
31
|
import { STACK_PAYWALL } from "./fixtures/stack-paywall";
|
|
31
32
|
import { VARIABLES } from "./fixtures/variables";
|
|
32
33
|
import { CUSTOM_VARIABLES_PAYWALL } from "./fixtures/custom-variables-paywall";
|
|
@@ -93,6 +94,28 @@
|
|
|
93
94
|
}}
|
|
94
95
|
/>
|
|
95
96
|
|
|
97
|
+
<Story
|
|
98
|
+
name="Sheet — video background (stacking / z-order)"
|
|
99
|
+
parameters={{
|
|
100
|
+
chromatic: { disableSnapshot: true },
|
|
101
|
+
docs: {
|
|
102
|
+
description: {
|
|
103
|
+
story:
|
|
104
|
+
"**Z-order:** `BackgroundVideoSurface` under the sheet panel (`z-index: 1`), CSS `::before` tint (`z-index: 2`), inner `Stack` (`z-index: 3`). Backdrop dim is outside the sheet chrome. Motion is disabled in Chromatic.",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}}
|
|
108
|
+
decorators={[viewportDecorator(500, 500, 0)]}
|
|
109
|
+
play={async ({ canvasElement }) => {
|
|
110
|
+
const button = canvasElement.querySelector("button");
|
|
111
|
+
button?.click();
|
|
112
|
+
await waitForAnimations();
|
|
113
|
+
}}
|
|
114
|
+
args={{
|
|
115
|
+
paywallData: SHEET_PAYWALL_VIDEO_STACKING,
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
|
|
96
119
|
<Story
|
|
97
120
|
name="Background - Color"
|
|
98
121
|
decorators={[viewportDecorator(500, 500, 0)]}
|
|
@@ -231,6 +254,116 @@
|
|
|
231
254
|
}}
|
|
232
255
|
/>
|
|
233
256
|
|
|
257
|
+
<Story
|
|
258
|
+
name="Background - Video fill"
|
|
259
|
+
decorators={[viewportDecorator(500, 500, 0)]}
|
|
260
|
+
parameters={{ chromatic: { disableSnapshot: true } }}
|
|
261
|
+
args={{
|
|
262
|
+
paywallData: BACKGROUND_PAYWALL({
|
|
263
|
+
type: "video",
|
|
264
|
+
fit_mode: "fill",
|
|
265
|
+
mute_audio: true,
|
|
266
|
+
loop: true,
|
|
267
|
+
color_overlay: null,
|
|
268
|
+
fallback_image: {
|
|
269
|
+
light: {
|
|
270
|
+
width: 640,
|
|
271
|
+
height: 360,
|
|
272
|
+
original:
|
|
273
|
+
"https://placehold.co/640x360/1a1a2e/ffffff.webp?text=Poster",
|
|
274
|
+
heic: "https://placehold.co/640x360/1a1a2e/ffffff.webp?text=Poster",
|
|
275
|
+
heic_low_res:
|
|
276
|
+
"https://placehold.co/640x360/1a1a2e/ffffff.webp?text=Poster",
|
|
277
|
+
webp: "https://placehold.co/640x360/1a1a2e/ffffff.webp?text=Poster",
|
|
278
|
+
webp_low_res:
|
|
279
|
+
"https://placehold.co/640x360/1a1a2e/ffffff.webp?text=Poster",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
value: {
|
|
283
|
+
light: {
|
|
284
|
+
width: 1920,
|
|
285
|
+
height: 1080,
|
|
286
|
+
url: "https://videos.pexels.com/video-files/10288594/10288594-hd_1920_1080_25fps.mp4",
|
|
287
|
+
url_low_res:
|
|
288
|
+
"https://videos.pexels.com/video-files/10288594/10288594-hd_1920_1080_25fps.mp4",
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
}}
|
|
293
|
+
/>
|
|
294
|
+
|
|
295
|
+
<Story
|
|
296
|
+
name="Background - Video fit"
|
|
297
|
+
decorators={[viewportDecorator(500, 500, 0)]}
|
|
298
|
+
parameters={{ chromatic: { disableSnapshot: true } }}
|
|
299
|
+
args={{
|
|
300
|
+
paywallData: BACKGROUND_PAYWALL({
|
|
301
|
+
type: "video",
|
|
302
|
+
fit_mode: "fit",
|
|
303
|
+
mute_audio: true,
|
|
304
|
+
loop: true,
|
|
305
|
+
color_overlay: null,
|
|
306
|
+
fallback_image: null,
|
|
307
|
+
value: {
|
|
308
|
+
light: {
|
|
309
|
+
width: 1920,
|
|
310
|
+
height: 1080,
|
|
311
|
+
url: "https://videos.pexels.com/video-files/10288594/10288594-hd_1920_1080_25fps.mp4",
|
|
312
|
+
url_low_res:
|
|
313
|
+
"https://videos.pexels.com/video-files/10288594/10288594-hd_1920_1080_25fps.mp4",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
317
|
+
}}
|
|
318
|
+
/>
|
|
319
|
+
|
|
320
|
+
<Story
|
|
321
|
+
name="Background - Video with tint overlay"
|
|
322
|
+
decorators={[viewportDecorator(500, 500, 0)]}
|
|
323
|
+
parameters={{ chromatic: { disableSnapshot: true } }}
|
|
324
|
+
args={{
|
|
325
|
+
paywallData: BACKGROUND_PAYWALL({
|
|
326
|
+
type: "video",
|
|
327
|
+
fit_mode: "fill",
|
|
328
|
+
mute_audio: true,
|
|
329
|
+
loop: true,
|
|
330
|
+
color_overlay: {
|
|
331
|
+
light: {
|
|
332
|
+
type: "linear",
|
|
333
|
+
degrees: 180,
|
|
334
|
+
points: [
|
|
335
|
+
{ percent: 0, color: "#00000066" },
|
|
336
|
+
{ percent: 100, color: "#1a0a3080" },
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
fallback_image: {
|
|
341
|
+
light: {
|
|
342
|
+
width: 640,
|
|
343
|
+
height: 360,
|
|
344
|
+
original:
|
|
345
|
+
"https://placehold.co/640x360/2d1b4e/ffffff.webp?text=Poster",
|
|
346
|
+
heic: "https://placehold.co/640x360/2d1b4e/ffffff.webp?text=Poster",
|
|
347
|
+
heic_low_res:
|
|
348
|
+
"https://placehold.co/640x360/2d1b4e/ffffff.webp?text=Poster",
|
|
349
|
+
webp: "https://placehold.co/640x360/2d1b4e/ffffff.webp?text=Poster",
|
|
350
|
+
webp_low_res:
|
|
351
|
+
"https://placehold.co/640x360/2d1b4e/ffffff.webp?text=Poster",
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
value: {
|
|
355
|
+
light: {
|
|
356
|
+
width: 1920,
|
|
357
|
+
height: 1080,
|
|
358
|
+
url: "https://videos.pexels.com/video-files/10288594/10288594-hd_1920_1080_25fps.mp4",
|
|
359
|
+
url_low_res:
|
|
360
|
+
"https://videos.pexels.com/video-files/10288594/10288594-hd_1920_1080_25fps.mp4",
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
}),
|
|
364
|
+
}}
|
|
365
|
+
/>
|
|
366
|
+
|
|
234
367
|
<Story
|
|
235
368
|
name="Primary"
|
|
236
369
|
args={{
|
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
import { getColorModeContext } from "../../stores/color-mode";
|
|
3
3
|
import { getPaywallContext } from "../../stores/paywall";
|
|
4
4
|
import { getSelectedStateContext } from "../../stores/selected";
|
|
5
|
+
import BackgroundVideoSurface from "./BackgroundVideoSurface.svelte";
|
|
5
6
|
import type { SheetProps } from "../../types/components/sheet";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
mapBackground,
|
|
9
|
+
resolveBackgroundVideoForSurface,
|
|
10
|
+
} from "../../utils/background-utils";
|
|
11
|
+
import {
|
|
12
|
+
rcVideoBackgroundHostClass,
|
|
13
|
+
videoBackgroundHostStyles,
|
|
14
|
+
} from "../../utils/video-background-host";
|
|
7
15
|
import { css, mapSize } from "../../utils/base-utils";
|
|
8
16
|
import { getActiveStateProps } from "../../utils/style-utils";
|
|
9
17
|
import { onMount } from "svelte";
|
|
@@ -23,6 +31,14 @@
|
|
|
23
31
|
const getColorMode = getColorModeContext();
|
|
24
32
|
const colorMode = $derived(getColorMode());
|
|
25
33
|
|
|
34
|
+
const hasVideoBg = $derived(background?.type === "video");
|
|
35
|
+
const videoSurface = $derived.by(() => {
|
|
36
|
+
if (!hasVideoBg) return null;
|
|
37
|
+
const bg = background;
|
|
38
|
+
if (!bg || bg.type !== "video") return null;
|
|
39
|
+
return resolveBackgroundVideoForSurface(colorMode, bg);
|
|
40
|
+
});
|
|
41
|
+
|
|
26
42
|
const backdropStyle = $derived(
|
|
27
43
|
css({
|
|
28
44
|
background: background_blur ? "#0000003f" : "",
|
|
@@ -32,6 +48,10 @@
|
|
|
32
48
|
const sheetStyle = $derived(
|
|
33
49
|
css({
|
|
34
50
|
height: mapSize(size.height),
|
|
51
|
+
...videoBackgroundHostStyles({
|
|
52
|
+
hasVideoBg,
|
|
53
|
+
clipOverflow: true,
|
|
54
|
+
}),
|
|
35
55
|
...mapBackground(colorMode, null, background),
|
|
36
56
|
}),
|
|
37
57
|
);
|
|
@@ -75,7 +95,14 @@
|
|
|
75
95
|
}
|
|
76
96
|
};
|
|
77
97
|
|
|
78
|
-
let sheetClass = $derived(
|
|
98
|
+
let sheetClass = $derived(
|
|
99
|
+
[
|
|
100
|
+
`sheet ${visible ? "visible" : ""}`,
|
|
101
|
+
hasVideoBg ? rcVideoBackgroundHostClass("sheet") : "",
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(" "),
|
|
105
|
+
);
|
|
79
106
|
</script>
|
|
80
107
|
|
|
81
108
|
<div
|
|
@@ -95,7 +122,21 @@
|
|
|
95
122
|
aria-modal="true"
|
|
96
123
|
{ontransitionend}
|
|
97
124
|
>
|
|
98
|
-
|
|
125
|
+
{#if videoSurface}
|
|
126
|
+
<BackgroundVideoSurface
|
|
127
|
+
url={videoSurface.url}
|
|
128
|
+
urlLowRes={videoSurface.url_low_res}
|
|
129
|
+
objectFit={videoSurface.fit}
|
|
130
|
+
objectPosition={videoSurface.position}
|
|
131
|
+
poster={videoSurface.posterWebp}
|
|
132
|
+
mute={videoSurface.mute}
|
|
133
|
+
loop={videoSurface.loop}
|
|
134
|
+
/>
|
|
135
|
+
{/if}
|
|
136
|
+
<Stack
|
|
137
|
+
{...stack}
|
|
138
|
+
class={hasVideoBg ? "rc-sheet-stack-above-video" : undefined}
|
|
139
|
+
/>
|
|
99
140
|
</div>
|
|
100
141
|
</div>
|
|
101
142
|
|
|
@@ -119,8 +160,30 @@
|
|
|
119
160
|
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
|
120
161
|
transform: translateY(100%);
|
|
121
162
|
|
|
163
|
+
&::before {
|
|
164
|
+
content: "";
|
|
165
|
+
position: absolute;
|
|
166
|
+
inset: 0;
|
|
167
|
+
background: var(--overlay);
|
|
168
|
+
pointer-events: none;
|
|
169
|
+
}
|
|
170
|
+
|
|
122
171
|
&.visible {
|
|
123
172
|
transform: translateY(0);
|
|
124
173
|
}
|
|
125
174
|
}
|
|
175
|
+
|
|
176
|
+
.sheet.rc-sheet-video-bg::before {
|
|
177
|
+
z-index: 2;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Match stack video stacking: tint above video (::before z-index). */
|
|
181
|
+
.sheet.rc-sheet-video-bg > :global(.rc-bg-video) {
|
|
182
|
+
z-index: 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.sheet.rc-sheet-video-bg > :global(.rc-sheet-stack-above-video) {
|
|
186
|
+
position: relative;
|
|
187
|
+
z-index: 3;
|
|
188
|
+
}
|
|
126
189
|
</style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { PaywallRootBackgroundModel } from "../../utils/background-utils";
|
|
3
|
+
import BackgroundVideoSurface from "./BackgroundVideoSurface.svelte";
|
|
3
4
|
|
|
4
5
|
// iOS WebKit only reliably propagates background-color (not background-image)
|
|
5
6
|
// through to the safe-area canvas. A position:fixed element that explicitly
|
|
@@ -27,19 +28,43 @@
|
|
|
27
28
|
return "";
|
|
28
29
|
});
|
|
29
30
|
|
|
30
|
-
const overlayStyle = $derived(
|
|
31
|
-
model.kind
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const overlayStyle = $derived.by((): string | null => {
|
|
32
|
+
if (model.kind !== "image" && model.kind !== "video") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!model.overlay || model.overlay === "none") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return `background:${model.overlay}`;
|
|
39
|
+
});
|
|
35
40
|
|
|
36
|
-
const
|
|
41
|
+
const shouldRenderBackdrop = $derived(
|
|
42
|
+
backdropStyle !== "" || model.kind === "video",
|
|
43
|
+
);
|
|
37
44
|
</script>
|
|
38
45
|
|
|
39
|
-
{#if
|
|
40
|
-
<div
|
|
46
|
+
{#if shouldRenderBackdrop}
|
|
47
|
+
<div
|
|
48
|
+
class="viewport-backdrop"
|
|
49
|
+
style={backdropStyle !== "" ? backdropStyle : undefined}
|
|
50
|
+
>
|
|
51
|
+
{#if model.kind === "video"}
|
|
52
|
+
<BackgroundVideoSurface
|
|
53
|
+
url={model.url}
|
|
54
|
+
urlLowRes={model.url_low_res}
|
|
55
|
+
objectFit={model.fit}
|
|
56
|
+
objectPosition={model.position}
|
|
57
|
+
poster={model.posterWebp}
|
|
58
|
+
mute={model.mute}
|
|
59
|
+
loop={model.loop}
|
|
60
|
+
/>
|
|
61
|
+
{/if}
|
|
41
62
|
{#if overlayStyle}
|
|
42
|
-
<div
|
|
63
|
+
<div
|
|
64
|
+
class="viewport-backdrop-overlay"
|
|
65
|
+
class:z-over-video-bg={model.kind === "video"}
|
|
66
|
+
style={overlayStyle}
|
|
67
|
+
></div>
|
|
43
68
|
{/if}
|
|
44
69
|
</div>
|
|
45
70
|
{/if}
|
|
@@ -51,11 +76,17 @@
|
|
|
51
76
|
z-index: 0;
|
|
52
77
|
pointer-events: none;
|
|
53
78
|
overflow: hidden;
|
|
79
|
+
isolation: isolate;
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
.viewport-backdrop-overlay {
|
|
57
83
|
position: absolute;
|
|
58
84
|
inset: 0;
|
|
59
85
|
pointer-events: none;
|
|
86
|
+
z-index: 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.viewport-backdrop-overlay.z-over-video-bg {
|
|
90
|
+
z-index: 2;
|
|
60
91
|
}
|
|
61
92
|
</style>
|