@revenuecat/purchases-ui-js 4.0.0 → 4.1.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/paywall/Paywall.stories.svelte +1 -10
- package/dist/components/paywall/Paywall.svelte +36 -6
- package/dist/components/{layout/Main → paywall}/ViewportBackdrop.svelte +4 -8
- package/dist/components/{layout/Main → paywall}/ViewportBackdrop.svelte.d.ts +1 -1
- package/dist/components/video/Video.svelte +208 -19
- package/dist/components/workflows/Screen.stories.svelte +3 -6
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/utils/document-background.d.ts +3 -0
- package/dist/utils/document-background.js +59 -0
- package/package.json +2 -2
- package/dist/components/layout/Main/Main.stories.svelte +0 -263
- package/dist/components/layout/Main/Main.stories.svelte.d.ts +0 -19
- package/dist/components/layout/Main/Main.svelte +0 -117
- package/dist/components/layout/Main/Main.svelte.d.ts +0 -11
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import type { ComponentProps } from "svelte";
|
|
4
4
|
|
|
5
5
|
import Paywall from "./Paywall.svelte";
|
|
6
|
-
import Main from "../layout/Main/Main.svelte";
|
|
7
6
|
import {
|
|
8
7
|
alignmentPaywallData,
|
|
9
8
|
calmPaywallData,
|
|
@@ -63,9 +62,7 @@
|
|
|
63
62
|
|
|
64
63
|
{#snippet template(props: ComponentProps<typeof Paywall>)}
|
|
65
64
|
<div class="paywall-story-frame">
|
|
66
|
-
<
|
|
67
|
-
<Paywall {...props} />
|
|
68
|
-
</Main>
|
|
65
|
+
<Paywall {...props} />
|
|
69
66
|
</div>
|
|
70
67
|
{/snippet}
|
|
71
68
|
|
|
@@ -510,10 +507,4 @@
|
|
|
510
507
|
background-color: var(--rc-purchases-ui-bg-color, Canvas);
|
|
511
508
|
color-scheme: light dark;
|
|
512
509
|
}
|
|
513
|
-
|
|
514
|
-
/* Allow chromatic to capture the full content snapshot */
|
|
515
|
-
.paywall-story-frame :global(.paywall-content-scroll) {
|
|
516
|
-
flex: 1 1 auto;
|
|
517
|
-
overflow-y: visible;
|
|
518
|
-
}
|
|
519
510
|
</style>
|
|
@@ -32,13 +32,17 @@
|
|
|
32
32
|
type PackageInfo,
|
|
33
33
|
type VariableDictionary,
|
|
34
34
|
} from "../../types/variables";
|
|
35
|
+
import { paywallRootBackgroundModel } from "../../utils/background-utils";
|
|
36
|
+
import { css } from "../../utils/base-utils";
|
|
35
37
|
import { STICKY_OVERLAY_Z_INDEX } from "../../utils/constants";
|
|
38
|
+
import { applyDocumentBackground } from "../../utils/document-background";
|
|
36
39
|
import { registerFonts } from "../../utils/font-utils";
|
|
37
40
|
import { findSelectedPackageId } from "../../utils/style-utils";
|
|
38
41
|
import { onMount } from "svelte";
|
|
39
42
|
import { derived, readable, writable } from "svelte/store";
|
|
40
43
|
import Stack from "../stack/Stack.svelte";
|
|
41
44
|
import Sheet from "./Sheet.svelte";
|
|
45
|
+
import ViewportBackdrop from "./ViewportBackdrop.svelte";
|
|
42
46
|
import {
|
|
43
47
|
type PackageInfoStore,
|
|
44
48
|
setPackageInfoContext,
|
|
@@ -128,13 +132,39 @@
|
|
|
128
132
|
customVariables = {},
|
|
129
133
|
}: Props = $props();
|
|
130
134
|
|
|
131
|
-
setColorModeContext(() => preferredColorMode);
|
|
135
|
+
const getColorMode = setColorModeContext(() => preferredColorMode);
|
|
136
|
+
|
|
137
|
+
const viewportBackdropModel = $derived(
|
|
138
|
+
paywallRootBackgroundModel(paywallData, getColorMode()),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const instanceId: symbol = Symbol();
|
|
142
|
+
$effect(() =>
|
|
143
|
+
applyDocumentBackground(instanceId, viewportBackdropModel, paywallData),
|
|
144
|
+
);
|
|
132
145
|
|
|
133
146
|
const { default_locale, components_config, components_localizations } =
|
|
134
147
|
paywallData;
|
|
135
148
|
const { base } = components_config;
|
|
136
149
|
const defaultPackageId = findSelectedPackageId(base);
|
|
137
150
|
|
|
151
|
+
// Solid colours paint here so the paywall has its own bg without requiring
|
|
152
|
+
// consumer CSS. Gradients and images are deliberately skipped — ViewportBackdrop
|
|
153
|
+
// paints those at viewport scale, and rendering them on both surfaces would
|
|
154
|
+
// anchor the gradient stops to two different boxes and produce a visible seam
|
|
155
|
+
// at the paywall edge.
|
|
156
|
+
const paywallStyle = $derived.by((): string => {
|
|
157
|
+
const m = viewportBackdropModel;
|
|
158
|
+
if (
|
|
159
|
+
m.kind === "style" &&
|
|
160
|
+
m.style.background &&
|
|
161
|
+
!m.style.background.includes("gradient")
|
|
162
|
+
) {
|
|
163
|
+
return css(m.style);
|
|
164
|
+
}
|
|
165
|
+
return "";
|
|
166
|
+
});
|
|
167
|
+
|
|
138
168
|
const { getLocalizedString } = setLocalizationContext(() => ({
|
|
139
169
|
defaultLocale: default_locale,
|
|
140
170
|
selectedLocale,
|
|
@@ -300,7 +330,8 @@
|
|
|
300
330
|
</script>
|
|
301
331
|
|
|
302
332
|
<svelte:boundary onerror={onError}>
|
|
303
|
-
<div class={paywallClass}>
|
|
333
|
+
<div class={paywallClass} style={paywallStyle}>
|
|
334
|
+
<ViewportBackdrop model={viewportBackdropModel} />
|
|
304
335
|
{#if header}
|
|
305
336
|
<div
|
|
306
337
|
class="header-wrapper"
|
|
@@ -345,10 +376,9 @@
|
|
|
345
376
|
.paywall {
|
|
346
377
|
position: relative;
|
|
347
378
|
display: flex;
|
|
348
|
-
flex: 1 1 auto;
|
|
349
379
|
flex-direction: column;
|
|
350
380
|
align-items: stretch;
|
|
351
|
-
|
|
381
|
+
height: 100%;
|
|
352
382
|
|
|
353
383
|
transition-property: filter, transform;
|
|
354
384
|
transition-duration: 0.1s;
|
|
@@ -370,13 +400,13 @@
|
|
|
370
400
|
.paywall-content {
|
|
371
401
|
z-index: 2;
|
|
372
402
|
width: 100%;
|
|
373
|
-
flex: 1;
|
|
403
|
+
flex: 1 1 auto;
|
|
374
404
|
display: flex;
|
|
375
405
|
flex-direction: column;
|
|
376
406
|
min-height: 0;
|
|
377
407
|
|
|
378
408
|
& > :global(.paywall-content-scroll) {
|
|
379
|
-
flex: 1 1
|
|
409
|
+
flex: 1 1 auto;
|
|
380
410
|
min-height: 0;
|
|
381
411
|
overflow-y: auto;
|
|
382
412
|
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { PaywallRootBackgroundModel } from "
|
|
2
|
+
import type { PaywallRootBackgroundModel } from "../../utils/background-utils";
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// explicitly fills the full viewport (including safe areas) is the consistent paint
|
|
4
|
+
// iOS WebKit only reliably propagates background-color (not background-image)
|
|
5
|
+
// through to the safe-area canvas. A position:fixed element that explicitly
|
|
6
|
+
// fills the viewport — including safe areas — is the only consistent paint
|
|
8
7
|
// surface for gradients and images on iOS Safari.
|
|
9
|
-
//
|
|
10
|
-
// Solid colours are NOT handled here — they use background-color on body instead,
|
|
11
|
-
// which avoids compositor surface churn on navigation.
|
|
12
8
|
const { model }: { model: PaywallRootBackgroundModel } = $props();
|
|
13
9
|
|
|
14
10
|
const backdropStyle = $derived.by((): string => {
|
|
@@ -70,7 +70,13 @@
|
|
|
70
70
|
});
|
|
71
71
|
let videoElement = $state<HTMLVideoElement | null>(null);
|
|
72
72
|
let hasVideoError = $state(false);
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* When `mute_audio` is false, unmuted `play()` often fails after a full reload (autoplay policy).
|
|
75
|
+
* Retrying with muted playback requires keeping the `<video muted>` binding in sync with the element.
|
|
76
|
+
*/
|
|
77
|
+
let playbackMutedOverride = $state(false);
|
|
78
|
+
/** Prevents repeated `load()` / `play()` nudge when the autoplay $effect re-runs (was resetting iOS to readyState 0). */
|
|
79
|
+
let lastAutoplayBootstrapUrl = $state<string | null>(null);
|
|
74
80
|
|
|
75
81
|
// Load video or fallback image metadata to get dimensions
|
|
76
82
|
$effect(() => {
|
|
@@ -138,7 +144,13 @@
|
|
|
138
144
|
if (size.height.type === "fixed") {
|
|
139
145
|
return size.height.value;
|
|
140
146
|
}
|
|
141
|
-
|
|
147
|
+
const w = videoSize.width;
|
|
148
|
+
const h = videoSize.height;
|
|
149
|
+
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
|
|
150
|
+
// Avoid 0-height box and NaN before metadata (`NaN <= 0` is false); 16:9 placeholder until dimensions resolve.
|
|
151
|
+
return wrapperWidth > 0 ? Math.round(wrapperWidth * (9 / 16)) : 200;
|
|
152
|
+
}
|
|
153
|
+
return Math.round(wrapperWidth * (h / w));
|
|
142
154
|
});
|
|
143
155
|
|
|
144
156
|
const style = $derived(
|
|
@@ -195,42 +207,211 @@
|
|
|
195
207
|
|
|
196
208
|
const shouldShowFallback = $derived(hasVideoError || !video);
|
|
197
209
|
|
|
198
|
-
//
|
|
210
|
+
// Autoplay: native `autoplay` + programmatic `play()`. Unmuted autoplay is often blocked; we retry
|
|
211
|
+
// once with muted playback (`playbackMutedOverride`) then try to restore sound. Controls follow
|
|
212
|
+
// `show_controls` only. Bootstrap once per URL; `play()` when `readyState >= HAVE_CURRENT_DATA`.
|
|
213
|
+
// Microtask + rAF retries cover decode/policy ordering; refresh leaves `document` already
|
|
214
|
+
// `complete` (`load` won't fire)—extra microtask retries `tryPlay` for muted→unmuted paths.
|
|
215
|
+
// Pair with `translateZ(0)` + rc-workflows `screen-slide-fade`.
|
|
199
216
|
$effect(() => {
|
|
200
217
|
if (!auto_play) {
|
|
201
218
|
untrack(() => {
|
|
202
|
-
|
|
219
|
+
playbackMutedOverride = false;
|
|
203
220
|
});
|
|
204
221
|
return;
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
if (shouldShowFallback || !video) {
|
|
208
225
|
untrack(() => {
|
|
209
|
-
|
|
226
|
+
playbackMutedOverride = false;
|
|
210
227
|
});
|
|
211
228
|
return;
|
|
212
229
|
}
|
|
213
230
|
|
|
214
231
|
const el = videoElement;
|
|
215
|
-
if (!el)
|
|
216
|
-
|
|
232
|
+
if (!el) return;
|
|
233
|
+
|
|
234
|
+
const vurl = video.url;
|
|
235
|
+
|
|
236
|
+
const needsBootstrap = lastAutoplayBootstrapUrl !== vurl;
|
|
237
|
+
if (needsBootstrap) {
|
|
238
|
+
lastAutoplayBootstrapUrl = vurl;
|
|
239
|
+
untrack(() => {
|
|
240
|
+
playbackMutedOverride = false;
|
|
241
|
+
});
|
|
217
242
|
}
|
|
218
243
|
|
|
219
|
-
|
|
220
|
-
|
|
244
|
+
let cancelled = false;
|
|
245
|
+
|
|
246
|
+
/** Keep `muted` / `playsInline` aligned with `mute_audio`, `playbackMutedOverride`, and the template. */
|
|
247
|
+
const applyVideoPlaybackFlags = () => {
|
|
248
|
+
el.muted = mute_audio || playbackMutedOverride;
|
|
249
|
+
el.playsInline = true;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
let playingUnmuteListener: (() => void) | null = null;
|
|
253
|
+
|
|
254
|
+
const clearPlayingUnmuteListener = () => {
|
|
255
|
+
if (playingUnmuteListener !== null) {
|
|
256
|
+
el.removeEventListener("playing", playingUnmuteListener);
|
|
257
|
+
playingUnmuteListener = null;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const tryPlay = () => {
|
|
262
|
+
if (cancelled) return;
|
|
263
|
+
if (!el.paused) return;
|
|
221
264
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
265
|
+
applyVideoPlaybackFlags();
|
|
266
|
+
|
|
267
|
+
if (el.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
225
270
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
271
|
+
const playPromise = el.play();
|
|
272
|
+
if (playPromise !== undefined) {
|
|
273
|
+
playPromise.catch(() => {
|
|
274
|
+
if (!mute_audio && !playbackMutedOverride) {
|
|
275
|
+
untrack(() => {
|
|
276
|
+
playbackMutedOverride = true;
|
|
277
|
+
});
|
|
278
|
+
applyVideoPlaybackFlags();
|
|
279
|
+
|
|
280
|
+
/** Unmuted `play()` after a forced-muted start sometimes only works tied to playback (WebKit). */
|
|
281
|
+
let unmuteAfterMutedStartHandled = false;
|
|
282
|
+
const tryRestoreAudioAfterMutedAutoplay = () => {
|
|
283
|
+
if (unmuteAfterMutedStartHandled || mute_audio || cancelled)
|
|
284
|
+
return;
|
|
285
|
+
unmuteAfterMutedStartHandled = true;
|
|
286
|
+
queueMicrotask(() => {
|
|
287
|
+
if (cancelled) return;
|
|
288
|
+
untrack(() => {
|
|
289
|
+
playbackMutedOverride = false;
|
|
290
|
+
});
|
|
291
|
+
applyVideoPlaybackFlags();
|
|
292
|
+
el.volume = 1;
|
|
293
|
+
const p3 = el.play();
|
|
294
|
+
if (p3 === undefined) return;
|
|
295
|
+
p3.catch(() => {
|
|
296
|
+
untrack(() => {
|
|
297
|
+
playbackMutedOverride = true;
|
|
298
|
+
});
|
|
299
|
+
applyVideoPlaybackFlags();
|
|
300
|
+
void el.play().catch(() => {});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const onPlayingAfterMutedBootstrap = () => {
|
|
306
|
+
playingUnmuteListener = null;
|
|
307
|
+
tryRestoreAudioAfterMutedAutoplay();
|
|
308
|
+
};
|
|
309
|
+
playingUnmuteListener = onPlayingAfterMutedBootstrap;
|
|
310
|
+
el.addEventListener("playing", onPlayingAfterMutedBootstrap, {
|
|
311
|
+
once: true,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const p2 = el.play();
|
|
315
|
+
if (p2 !== undefined) {
|
|
316
|
+
p2.then(() => {
|
|
317
|
+
// `playing` may have fired before our listener ran; retry unmute once playback exists.
|
|
318
|
+
queueMicrotask(() => {
|
|
319
|
+
if (
|
|
320
|
+
cancelled ||
|
|
321
|
+
mute_audio ||
|
|
322
|
+
unmuteAfterMutedStartHandled ||
|
|
323
|
+
el.paused
|
|
324
|
+
) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (!el.muted) {
|
|
328
|
+
clearPlayingUnmuteListener();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
tryRestoreAudioAfterMutedAutoplay();
|
|
332
|
+
});
|
|
333
|
+
}).catch(() => {
|
|
334
|
+
clearPlayingUnmuteListener();
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
231
338
|
});
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const onMediaReady = () => {
|
|
343
|
+
tryPlay();
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
if (el.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
347
|
+
el.addEventListener("loadeddata", onMediaReady);
|
|
348
|
+
el.addEventListener("canplay", onMediaReady);
|
|
349
|
+
} else {
|
|
350
|
+
tryPlay();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Once per `video.url`: muted `play()` nudge helps iOS decode before `HAVE_CURRENT_DATA`.
|
|
355
|
+
* When `mute_audio` is false, `load()` reloads the media so poster/metadata can attach; unmuted
|
|
356
|
+
* autoplay may still require a user gesture (handled in `tryPlay`).
|
|
357
|
+
*/
|
|
358
|
+
const nudgePlayback = () => {
|
|
359
|
+
applyVideoPlaybackFlags();
|
|
360
|
+
|
|
361
|
+
if (mute_audio) {
|
|
362
|
+
void el.play().catch(() => {});
|
|
363
|
+
} else {
|
|
364
|
+
try {
|
|
365
|
+
el.load();
|
|
366
|
+
} catch {
|
|
367
|
+
/* ignore */
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
if (needsBootstrap && el.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
372
|
+
nudgePlayback();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Extra `tryPlay` ticks: next microtask vs next frame vs cold `load`; WebKit reordering varies. */
|
|
376
|
+
const deferTryPlayWithReadyGate = () => {
|
|
377
|
+
queueMicrotask(() => {
|
|
378
|
+
if (cancelled) return;
|
|
379
|
+
if (el.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
380
|
+
tryPlay();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
requestAnimationFrame(() => {
|
|
384
|
+
if (cancelled) return;
|
|
385
|
+
if (el.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
386
|
+
tryPlay();
|
|
387
|
+
}
|
|
232
388
|
});
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
deferTryPlayWithReadyGate();
|
|
392
|
+
|
|
393
|
+
const onWindowLoad = () => {
|
|
394
|
+
tryPlay();
|
|
395
|
+
};
|
|
396
|
+
if (typeof document !== "undefined" && typeof window !== "undefined") {
|
|
397
|
+
if (document.readyState === "complete") {
|
|
398
|
+
queueMicrotask(() => {
|
|
399
|
+
if (!cancelled) tryPlay();
|
|
400
|
+
});
|
|
401
|
+
} else {
|
|
402
|
+
window.addEventListener("load", onWindowLoad);
|
|
403
|
+
}
|
|
233
404
|
}
|
|
405
|
+
|
|
406
|
+
return () => {
|
|
407
|
+
cancelled = true;
|
|
408
|
+
clearPlayingUnmuteListener();
|
|
409
|
+
el.removeEventListener("loadeddata", onMediaReady);
|
|
410
|
+
el.removeEventListener("canplay", onMediaReady);
|
|
411
|
+
if (typeof window !== "undefined") {
|
|
412
|
+
window.removeEventListener("load", onWindowLoad);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
234
415
|
});
|
|
235
416
|
|
|
236
417
|
const isVisible = $derived(
|
|
@@ -262,9 +443,11 @@
|
|
|
262
443
|
src={video.url}
|
|
263
444
|
style="object-fit:{mapFitMode(fit_mode)}"
|
|
264
445
|
{loop}
|
|
265
|
-
muted={mute_audio}
|
|
266
|
-
controls={show_controls
|
|
446
|
+
muted={mute_audio || playbackMutedOverride}
|
|
447
|
+
controls={show_controls}
|
|
267
448
|
playsinline
|
|
449
|
+
autoplay={auto_play}
|
|
450
|
+
preload={auto_play ? "auto" : "metadata"}
|
|
268
451
|
>
|
|
269
452
|
<track kind="captions" />
|
|
270
453
|
</video>
|
|
@@ -352,4 +535,10 @@
|
|
|
352
535
|
object-fit: contain;
|
|
353
536
|
object-position: center;
|
|
354
537
|
}
|
|
538
|
+
|
|
539
|
+
/* Own compositing layer — mitigates WebKit freezing video frames under animated/transform ancestors */
|
|
540
|
+
video {
|
|
541
|
+
transform: translateZ(0);
|
|
542
|
+
-webkit-transform: translateZ(0);
|
|
543
|
+
}
|
|
355
544
|
</style>
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import type { ComponentProps } from "svelte";
|
|
4
4
|
|
|
5
5
|
import Screen from "./Screen.svelte";
|
|
6
|
-
import Main from "../layout/Main/Main.svelte";
|
|
7
6
|
import { paywallData, uiConfigData } from "../../stories/fixtures";
|
|
8
7
|
import type { WorkflowScreen } from "../../types/workflow";
|
|
9
8
|
|
|
@@ -33,11 +32,9 @@
|
|
|
33
32
|
|
|
34
33
|
{#snippet template(props: ComponentProps<typeof Screen>)}
|
|
35
34
|
<div class="viewport-frame">
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
</div>
|
|
40
|
-
</Main>
|
|
35
|
+
<div class="content-wrapper">
|
|
36
|
+
<Screen {...props} />
|
|
37
|
+
</div>
|
|
41
38
|
</div>
|
|
42
39
|
{/snippet}
|
|
43
40
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ export { default as Stack } from "./components/stack/Stack.svelte";
|
|
|
13
13
|
export { default as Text } from "./components/text/Text.svelte";
|
|
14
14
|
export { default as Timeline } from "./components/timeline/Timeline.svelte";
|
|
15
15
|
export { default as Screen } from "./components/workflows/Screen.svelte";
|
|
16
|
-
export { default as Main } from "./components/layout/Main/Main.svelte";
|
|
17
16
|
export { default as Video } from "./components/video/Video.svelte";
|
|
18
17
|
export * from "./types";
|
|
19
18
|
export { type PaywallData } from "./types/paywall";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,6 @@ export { default as Stack } from "./components/stack/Stack.svelte";
|
|
|
14
14
|
export { default as Text } from "./components/text/Text.svelte";
|
|
15
15
|
export { default as Timeline } from "./components/timeline/Timeline.svelte";
|
|
16
16
|
export { default as Screen } from "./components/workflows/Screen.svelte";
|
|
17
|
-
export { default as Main } from "./components/layout/Main/Main.svelte";
|
|
18
17
|
export { default as Video } from "./components/video/Video.svelte";
|
|
19
18
|
export * from "./types";
|
|
20
19
|
export {} from "./types/paywall";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PaywallData } from "../types/paywall";
|
|
2
|
+
import type { PaywallRootBackgroundModel } from "./background-utils";
|
|
3
|
+
export declare function applyDocumentBackground(instanceId: symbol, model: PaywallRootBackgroundModel, paywallData: PaywallData | null | undefined): () => void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Last writer wins, but cleanup only fires for the last writer — otherwise an
|
|
2
|
+
// unmounting paywall would clear a variable set by another paywall that overlapped
|
|
3
|
+
// it during a transition.
|
|
4
|
+
let lastBgWriter = null;
|
|
5
|
+
// iOS promotes sticky header/footer to its own compositor layer, which breaks
|
|
6
|
+
// safe-area painting for the opposite strip. A vertical gradient's edge stop
|
|
7
|
+
// can stand in as a solid fallback for that strip; any other angle would smear
|
|
8
|
+
// a horizontal colour range across the strip that no single colour represents.
|
|
9
|
+
function gradientSafeAreaFallbackColour(gradient, hasHeader, hasFooter) {
|
|
10
|
+
if (!hasHeader && !hasFooter)
|
|
11
|
+
return null;
|
|
12
|
+
if (hasHeader && hasFooter)
|
|
13
|
+
return null;
|
|
14
|
+
const angleMatch = gradient.match(/linear-gradient\((\d+)deg/);
|
|
15
|
+
if (!angleMatch)
|
|
16
|
+
return null;
|
|
17
|
+
if (parseInt(angleMatch[1], 10) % 180 !== 0)
|
|
18
|
+
return null;
|
|
19
|
+
const stops = gradient.match(/#[0-9a-fA-F]{6,8}/g);
|
|
20
|
+
if (!stops || stops.length < 2)
|
|
21
|
+
return null;
|
|
22
|
+
const candidate = hasHeader ? stops[stops.length - 1] : stops[0];
|
|
23
|
+
if (candidate.length === 9 && candidate.slice(-2).toLowerCase() !== "ff") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
// Published as a CSS variable so consumer surfaces can opt in to safe-area
|
|
29
|
+
// painting (`background: var(--rc-purchases-ui-bg-color, Canvas)` on any
|
|
30
|
+
// element that extends into the safe-area canvas — typically html, body, or
|
|
31
|
+
// a position:fixed root). Writing to documentElement.backgroundColor directly
|
|
32
|
+
// would bleed the paywall colour onto host page chrome that shouldn't take
|
|
33
|
+
// it on, so we expose the value rather than apply it.
|
|
34
|
+
export function applyDocumentBackground(instanceId, model, paywallData) {
|
|
35
|
+
if (typeof document === "undefined")
|
|
36
|
+
return () => { };
|
|
37
|
+
const root = document.documentElement;
|
|
38
|
+
let value = null;
|
|
39
|
+
if (model.kind === "style" && model.style.background) {
|
|
40
|
+
const bg = model.style.background;
|
|
41
|
+
if (!bg.includes("gradient")) {
|
|
42
|
+
value = bg;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const base = paywallData?.components_config?.base;
|
|
46
|
+
value = gradientSafeAreaFallbackColour(bg, !!base?.header, !!base?.sticky_footer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (value !== null) {
|
|
50
|
+
lastBgWriter = instanceId;
|
|
51
|
+
root.style.setProperty("--rc-purchases-ui-bg-color", value);
|
|
52
|
+
}
|
|
53
|
+
return () => {
|
|
54
|
+
if (lastBgWriter === instanceId) {
|
|
55
|
+
lastBgWriter = null;
|
|
56
|
+
root.style.removeProperty("--rc-purchases-ui-bg-color");
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@revenuecat/purchases-ui-js",
|
|
3
3
|
"description": "Web components for Paywalls. Powered by RevenueCat",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.1.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "RevenueCat, Inc."
|
|
8
8
|
},
|
|
@@ -123,4 +123,4 @@
|
|
|
123
123
|
"eslint --fix"
|
|
124
124
|
]
|
|
125
125
|
}
|
|
126
|
-
}
|
|
126
|
+
}
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
<script module lang="ts">
|
|
2
|
-
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
3
|
-
import type { ComponentProps } from "svelte";
|
|
4
|
-
import Main from "./Main.svelte";
|
|
5
|
-
import Screen from "../../workflows/Screen.svelte";
|
|
6
|
-
import type { Background } from "../../../types/background";
|
|
7
|
-
import type { Component } from "../../../types/component";
|
|
8
|
-
import type { HeaderProps } from "../../../types/components/header";
|
|
9
|
-
import type { FooterProps } from "../../../types/components/footer";
|
|
10
|
-
import type { TextNodeProps } from "../../../types/components/text";
|
|
11
|
-
import type { WorkflowScreen } from "../../../types/workflow";
|
|
12
|
-
import { uiConfigData } from "../../../stories/fixtures";
|
|
13
|
-
|
|
14
|
-
function textComponent(id: string, textLid: string): TextNodeProps {
|
|
15
|
-
return {
|
|
16
|
-
type: "text",
|
|
17
|
-
id,
|
|
18
|
-
name: "",
|
|
19
|
-
text_lid: textLid,
|
|
20
|
-
font_name: null,
|
|
21
|
-
font_size: "body_m",
|
|
22
|
-
font_weight: "bold",
|
|
23
|
-
horizontal_alignment: "center",
|
|
24
|
-
color: { light: { type: "hex", value: "#ffffffff" } },
|
|
25
|
-
background_color: null,
|
|
26
|
-
padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
|
|
27
|
-
margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
|
|
28
|
-
size: { width: { type: "fit" }, height: { type: "fit" } },
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function visibleStack(components: Component[] = []) {
|
|
33
|
-
return {
|
|
34
|
-
type: "stack" as const,
|
|
35
|
-
id: "s",
|
|
36
|
-
name: "",
|
|
37
|
-
components,
|
|
38
|
-
size: {
|
|
39
|
-
width: { type: "fill" as const },
|
|
40
|
-
height: { type: "fit" as const },
|
|
41
|
-
},
|
|
42
|
-
dimension: {
|
|
43
|
-
type: "vertical" as const,
|
|
44
|
-
alignment: "center" as const,
|
|
45
|
-
distribution: "center" as const,
|
|
46
|
-
},
|
|
47
|
-
spacing: 0,
|
|
48
|
-
margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
|
|
49
|
-
padding: { top: 16, bottom: 16, leading: 0, trailing: 0 },
|
|
50
|
-
background_color: { light: { type: "hex" as const, value: "#00000033" } },
|
|
51
|
-
background: null,
|
|
52
|
-
border: null,
|
|
53
|
-
shape: null,
|
|
54
|
-
shadow: null,
|
|
55
|
-
badge: null,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const minimalHeader: HeaderProps = {
|
|
60
|
-
id: "header",
|
|
61
|
-
name: "Header",
|
|
62
|
-
type: "header",
|
|
63
|
-
stack: visibleStack([textComponent("header-label", "header_label")]),
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const minimalFooter: FooterProps = {
|
|
67
|
-
id: "footer",
|
|
68
|
-
name: "Footer",
|
|
69
|
-
type: "footer",
|
|
70
|
-
stack: visibleStack([textComponent("footer-label", "footer_label")]),
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
function shellData(
|
|
74
|
-
background: Background,
|
|
75
|
-
hasHeader: boolean,
|
|
76
|
-
hasFooter: boolean,
|
|
77
|
-
): WorkflowScreen {
|
|
78
|
-
return {
|
|
79
|
-
id: "main-story",
|
|
80
|
-
default_locale: "en_US",
|
|
81
|
-
components_localizations: {
|
|
82
|
-
en_US: { header_label: "Header", footer_label: "Footer" },
|
|
83
|
-
},
|
|
84
|
-
asset_base_url: "https://assets.pawwalls.com",
|
|
85
|
-
config: {},
|
|
86
|
-
localized_strings: {},
|
|
87
|
-
localized_strings_by_tier: {},
|
|
88
|
-
name: "Main story",
|
|
89
|
-
offering_id: null,
|
|
90
|
-
revision: 1,
|
|
91
|
-
template_name: "stack",
|
|
92
|
-
components_config: {
|
|
93
|
-
base: {
|
|
94
|
-
background,
|
|
95
|
-
header: hasHeader ? minimalHeader : null,
|
|
96
|
-
sticky_footer: hasFooter ? minimalFooter : null,
|
|
97
|
-
stack: {
|
|
98
|
-
type: "stack" as const,
|
|
99
|
-
id: "content",
|
|
100
|
-
name: "",
|
|
101
|
-
components: [],
|
|
102
|
-
size: {
|
|
103
|
-
width: { type: "fill" as const },
|
|
104
|
-
height: { type: "fill" as const },
|
|
105
|
-
},
|
|
106
|
-
dimension: {
|
|
107
|
-
type: "vertical" as const,
|
|
108
|
-
alignment: "center" as const,
|
|
109
|
-
distribution: "start" as const,
|
|
110
|
-
},
|
|
111
|
-
spacing: 0,
|
|
112
|
-
margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
|
|
113
|
-
padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
|
|
114
|
-
background_color: null,
|
|
115
|
-
background: null,
|
|
116
|
-
border: null,
|
|
117
|
-
shape: null,
|
|
118
|
-
shadow: null,
|
|
119
|
-
badge: null,
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const solidColour: Background = {
|
|
127
|
-
type: "color",
|
|
128
|
-
value: { light: { type: "hex", value: "#6B4FBBFF" } },
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const gradient: Background = {
|
|
132
|
-
type: "color",
|
|
133
|
-
value: {
|
|
134
|
-
light: {
|
|
135
|
-
type: "linear",
|
|
136
|
-
degrees: 180,
|
|
137
|
-
points: [
|
|
138
|
-
{ percent: 0, color: "#3B82F6FF" },
|
|
139
|
-
{ percent: 100, color: "#9333EAFF" },
|
|
140
|
-
],
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const image: Background = {
|
|
146
|
-
type: "image",
|
|
147
|
-
fit_mode: "fill",
|
|
148
|
-
color_overlay: null,
|
|
149
|
-
value: {
|
|
150
|
-
light: {
|
|
151
|
-
width: 1170,
|
|
152
|
-
height: 2532,
|
|
153
|
-
original: "https://placehold.co/1170x2532",
|
|
154
|
-
heic: "https://placehold.co/1170x2532",
|
|
155
|
-
heic_low_res: "https://placehold.co/1170x2532",
|
|
156
|
-
webp: "https://placehold.co/1170x2532",
|
|
157
|
-
webp_low_res: "https://placehold.co/1170x2532",
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const { Story } = defineMeta({
|
|
163
|
-
title: "Components/Layout/Main",
|
|
164
|
-
component: Main,
|
|
165
|
-
render: template,
|
|
166
|
-
args: { paywallData: shellData(solidColour, false, false) },
|
|
167
|
-
});
|
|
168
|
-
</script>
|
|
169
|
-
|
|
170
|
-
{#snippet template({
|
|
171
|
-
paywallData,
|
|
172
|
-
preferredColorMode,
|
|
173
|
-
}: ComponentProps<typeof Main>)}
|
|
174
|
-
<div class="viewport-frame">
|
|
175
|
-
<Main {paywallData} {preferredColorMode}>
|
|
176
|
-
<div class="content-wrapper">
|
|
177
|
-
<Screen paywallComponents={paywallData} uiConfig={uiConfigData} />
|
|
178
|
-
</div>
|
|
179
|
-
</Main>
|
|
180
|
-
</div>
|
|
181
|
-
{/snippet}
|
|
182
|
-
|
|
183
|
-
<Story
|
|
184
|
-
name="Solid colour"
|
|
185
|
-
args={{ paywallData: shellData(solidColour, false, false) }}
|
|
186
|
-
/>
|
|
187
|
-
<Story
|
|
188
|
-
name="Solid colour — header"
|
|
189
|
-
args={{ paywallData: shellData(solidColour, true, false) }}
|
|
190
|
-
/>
|
|
191
|
-
<Story
|
|
192
|
-
name="Solid colour — footer"
|
|
193
|
-
args={{ paywallData: shellData(solidColour, false, true) }}
|
|
194
|
-
/>
|
|
195
|
-
<Story
|
|
196
|
-
name="Solid colour — header and footer"
|
|
197
|
-
args={{ paywallData: shellData(solidColour, true, true) }}
|
|
198
|
-
/>
|
|
199
|
-
|
|
200
|
-
<Story
|
|
201
|
-
name="Gradient"
|
|
202
|
-
args={{ paywallData: shellData(gradient, false, false) }}
|
|
203
|
-
/>
|
|
204
|
-
<Story
|
|
205
|
-
name="Gradient — header"
|
|
206
|
-
args={{ paywallData: shellData(gradient, true, false) }}
|
|
207
|
-
/>
|
|
208
|
-
<Story
|
|
209
|
-
name="Gradient — footer"
|
|
210
|
-
args={{ paywallData: shellData(gradient, false, true) }}
|
|
211
|
-
/>
|
|
212
|
-
<Story
|
|
213
|
-
name="Gradient — header and footer"
|
|
214
|
-
args={{ paywallData: shellData(gradient, true, true) }}
|
|
215
|
-
/>
|
|
216
|
-
|
|
217
|
-
<Story name="Image" args={{ paywallData: shellData(image, false, false) }} />
|
|
218
|
-
<Story
|
|
219
|
-
name="Image — header"
|
|
220
|
-
args={{ paywallData: shellData(image, true, false) }}
|
|
221
|
-
/>
|
|
222
|
-
<Story
|
|
223
|
-
name="Image — footer"
|
|
224
|
-
args={{ paywallData: shellData(image, false, true) }}
|
|
225
|
-
/>
|
|
226
|
-
<Story
|
|
227
|
-
name="Image — header and footer"
|
|
228
|
-
args={{ paywallData: shellData(image, true, true) }}
|
|
229
|
-
/>
|
|
230
|
-
|
|
231
|
-
<style>
|
|
232
|
-
/* A fixed-position frame that emulates rc-workflows' <body> (see app.css).
|
|
233
|
-
Using position:fixed escapes Storybook's DOM chain (.sb-show-main, #storybook-root, PaywallWrapper) */
|
|
234
|
-
.viewport-frame {
|
|
235
|
-
position: fixed;
|
|
236
|
-
inset: 0;
|
|
237
|
-
display: flex;
|
|
238
|
-
flex-direction: column;
|
|
239
|
-
padding: env(safe-area-inset-top) env(safe-area-inset-right)
|
|
240
|
-
env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
241
|
-
background-color: var(--rc-purchases-ui-bg-color, Canvas);
|
|
242
|
-
color-scheme: light dark;
|
|
243
|
-
overflow: hidden;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
.content-wrapper {
|
|
247
|
-
width: 100%;
|
|
248
|
-
display: flex;
|
|
249
|
-
flex: 1 1 auto;
|
|
250
|
-
flex-direction: column;
|
|
251
|
-
min-height: 0;
|
|
252
|
-
overflow: hidden;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/* Screen's containerId is "screen-container" by default */
|
|
256
|
-
:global(#screen-container) {
|
|
257
|
-
display: flex;
|
|
258
|
-
flex-direction: column;
|
|
259
|
-
flex: 1 1 auto;
|
|
260
|
-
min-height: 0;
|
|
261
|
-
overflow: hidden;
|
|
262
|
-
}
|
|
263
|
-
</style>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import Main from "./Main.svelte";
|
|
2
|
-
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
-
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
-
$$bindings?: Bindings;
|
|
5
|
-
} & Exports;
|
|
6
|
-
(internal: unknown, props: {
|
|
7
|
-
$$events?: Events;
|
|
8
|
-
$$slots?: Slots;
|
|
9
|
-
}): Exports & {
|
|
10
|
-
$set?: any;
|
|
11
|
-
$on?: any;
|
|
12
|
-
};
|
|
13
|
-
z_$$bindings?: Bindings;
|
|
14
|
-
}
|
|
15
|
-
declare const Main: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
-
[evt: string]: CustomEvent<any>;
|
|
17
|
-
}, {}, {}, string>;
|
|
18
|
-
type Main = InstanceType<typeof Main>;
|
|
19
|
-
export default Main;
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { Snippet } from "svelte";
|
|
3
|
-
import type { ColorMode } from "../../../types";
|
|
4
|
-
import type { PaywallData } from "../../../types/paywall";
|
|
5
|
-
import { paywallRootBackgroundModel } from "../../../utils/background-utils";
|
|
6
|
-
import { MediaQuery } from "svelte/reactivity";
|
|
7
|
-
import ViewportBackdrop from "./ViewportBackdrop.svelte";
|
|
8
|
-
|
|
9
|
-
// Tracks the last Main instance that wrote --rc-purchases-ui-bg-color.
|
|
10
|
-
// Cleanup only removes the variable if this instance is still the last writer,
|
|
11
|
-
// so an unmounting shell won't clear a value set by a shell that overlapped it
|
|
12
|
-
// during a page transition.
|
|
13
|
-
let lastBgWriter: symbol | null = null;
|
|
14
|
-
|
|
15
|
-
interface Props {
|
|
16
|
-
paywallData: PaywallData | null | undefined;
|
|
17
|
-
preferredColorMode?: ColorMode;
|
|
18
|
-
children: Snippet;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const { paywallData, preferredColorMode, children }: Props = $props();
|
|
22
|
-
|
|
23
|
-
const instanceId: symbol = Symbol();
|
|
24
|
-
|
|
25
|
-
const prefersDark = new MediaQuery("prefers-color-scheme: dark", false);
|
|
26
|
-
const colorMode: ColorMode = $derived(
|
|
27
|
-
preferredColorMode ?? (prefersDark.current ? "dark" : "light"),
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const viewportBackdropModel = $derived(
|
|
31
|
-
paywallRootBackgroundModel(paywallData, colorMode),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
// When a sticky header or footer is present, iOS promotes it to its own compositor
|
|
35
|
-
// layer which breaks the fixed backdrop's safe-area painting for the opposite strip.
|
|
36
|
-
// Returns the edge stop colour to paint on body to patch the uncovered strip, or null.
|
|
37
|
-
// Only works for exactly vertical gradients (0°/180°) — any other angle produces a
|
|
38
|
-
// horizontal colour range across the strip that no single solid colour can represent.
|
|
39
|
-
function gradientSafeAreaFallbackColour(
|
|
40
|
-
gradient: string,
|
|
41
|
-
hasHeader: boolean,
|
|
42
|
-
hasFooter: boolean,
|
|
43
|
-
): string | null {
|
|
44
|
-
if (!hasHeader && !hasFooter) return null;
|
|
45
|
-
if (hasHeader && hasFooter) return null;
|
|
46
|
-
const angleMatch = gradient.match(/linear-gradient\((\d+)deg/);
|
|
47
|
-
if (!angleMatch) return null;
|
|
48
|
-
if (parseInt(angleMatch[1], 10) % 180 !== 0) return null;
|
|
49
|
-
const stops = gradient.match(/#[0-9a-fA-F]{6,8}/g);
|
|
50
|
-
if (!stops || stops.length < 2) return null;
|
|
51
|
-
const candidate = hasHeader ? stops[stops.length - 1] : stops[0];
|
|
52
|
-
if (candidate.length === 9 && candidate.slice(-2).toLowerCase() !== "ff") {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
return candidate;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Solid colours use --rc-purchases-ui-bg-color on body — iOS reliably propagates
|
|
59
|
-
// background-color to the viewport canvas (safe-area strips). Gradients and images
|
|
60
|
-
// are handled by ViewportBackdrop (position:fixed) instead.
|
|
61
|
-
$effect(() => {
|
|
62
|
-
if (typeof document === "undefined") return;
|
|
63
|
-
const model = viewportBackdropModel;
|
|
64
|
-
const root = document.documentElement;
|
|
65
|
-
|
|
66
|
-
let value: string | null = null;
|
|
67
|
-
if (model.kind === "style" && model.style.background) {
|
|
68
|
-
const bg = model.style.background;
|
|
69
|
-
if (!bg.includes("gradient")) {
|
|
70
|
-
value = bg;
|
|
71
|
-
} else {
|
|
72
|
-
const base = paywallData?.components_config?.base;
|
|
73
|
-
value = gradientSafeAreaFallbackColour(
|
|
74
|
-
bg,
|
|
75
|
-
!!base?.header,
|
|
76
|
-
!!base?.sticky_footer,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (value !== null) {
|
|
82
|
-
lastBgWriter = instanceId;
|
|
83
|
-
root.style.setProperty("--rc-purchases-ui-bg-color", value);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return () => {
|
|
87
|
-
if (lastBgWriter === instanceId) {
|
|
88
|
-
lastBgWriter = null;
|
|
89
|
-
root.style.removeProperty("--rc-purchases-ui-bg-color");
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
});
|
|
93
|
-
</script>
|
|
94
|
-
|
|
95
|
-
<div class="main">
|
|
96
|
-
<ViewportBackdrop model={viewportBackdropModel} />
|
|
97
|
-
<main class="main-content">
|
|
98
|
-
{@render children()}
|
|
99
|
-
</main>
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
<style>
|
|
103
|
-
.main {
|
|
104
|
-
flex: 1 1 auto;
|
|
105
|
-
display: flex;
|
|
106
|
-
flex-direction: column;
|
|
107
|
-
min-height: 0;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.main-content {
|
|
111
|
-
position: relative;
|
|
112
|
-
z-index: 1;
|
|
113
|
-
display: flex;
|
|
114
|
-
flex: 1 1 auto;
|
|
115
|
-
min-height: 0;
|
|
116
|
-
}
|
|
117
|
-
</style>
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Snippet } from "svelte";
|
|
2
|
-
import type { ColorMode } from "../../../types";
|
|
3
|
-
import type { PaywallData } from "../../../types/paywall";
|
|
4
|
-
interface Props {
|
|
5
|
-
paywallData: PaywallData | null | undefined;
|
|
6
|
-
preferredColorMode?: ColorMode;
|
|
7
|
-
children: Snippet;
|
|
8
|
-
}
|
|
9
|
-
declare const Main: import("svelte").Component<Props, {}, "">;
|
|
10
|
-
type Main = ReturnType<typeof Main>;
|
|
11
|
-
export default Main;
|