@rocapine/react-native-onboarding-ui 1.34.1 → 1.36.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.
Files changed (40) hide show
  1. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts +10 -0
  2. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts.map +1 -1
  3. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js +3 -0
  4. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js.map +1 -1
  5. package/dist/UI/Pages/ComposableScreen/elements/CheckboxGroupElement.d.ts +10 -0
  6. package/dist/UI/Pages/ComposableScreen/elements/CheckboxGroupElement.d.ts.map +1 -1
  7. package/dist/UI/Pages/ComposableScreen/elements/CheckboxGroupElement.js +3 -0
  8. package/dist/UI/Pages/ComposableScreen/elements/CheckboxGroupElement.js.map +1 -1
  9. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts +3 -0
  10. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts.map +1 -1
  11. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js +6 -4
  12. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js.map +1 -1
  13. package/dist/UI/Pages/ComposableScreen/elements/ProgressiveBlurImageElement.d.ts +327 -0
  14. package/dist/UI/Pages/ComposableScreen/elements/ProgressiveBlurImageElement.d.ts.map +1 -0
  15. package/dist/UI/Pages/ComposableScreen/elements/ProgressiveBlurImageElement.js +240 -0
  16. package/dist/UI/Pages/ComposableScreen/elements/ProgressiveBlurImageElement.js.map +1 -0
  17. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts +10 -0
  18. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts.map +1 -1
  19. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js +3 -0
  20. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js.map +1 -1
  21. package/dist/UI/Pages/ComposableScreen/elements/haptics.d.ts +3 -0
  22. package/dist/UI/Pages/ComposableScreen/elements/haptics.d.ts.map +1 -0
  23. package/dist/UI/Pages/ComposableScreen/elements/haptics.js +27 -0
  24. package/dist/UI/Pages/ComposableScreen/elements/haptics.js.map +1 -0
  25. package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts.map +1 -1
  26. package/dist/UI/Pages/ComposableScreen/elements/renderElement.js +4 -0
  27. package/dist/UI/Pages/ComposableScreen/elements/renderElement.js.map +1 -1
  28. package/dist/UI/Pages/ComposableScreen/types.d.ts +8 -0
  29. package/dist/UI/Pages/ComposableScreen/types.d.ts.map +1 -1
  30. package/dist/UI/Pages/ComposableScreen/types.js +8 -0
  31. package/dist/UI/Pages/ComposableScreen/types.js.map +1 -1
  32. package/package.json +9 -1
  33. package/src/UI/Pages/ComposableScreen/elements/ButtonElement.tsx +4 -0
  34. package/src/UI/Pages/ComposableScreen/elements/CheckboxGroupElement.tsx +4 -0
  35. package/src/UI/Pages/ComposableScreen/elements/ImageElement.tsx +15 -6
  36. package/src/UI/Pages/ComposableScreen/elements/ProgressiveBlurImageElement.tsx +348 -0
  37. package/src/UI/Pages/ComposableScreen/elements/RadioGroupElement.tsx +4 -0
  38. package/src/UI/Pages/ComposableScreen/elements/haptics.ts +24 -0
  39. package/src/UI/Pages/ComposableScreen/elements/renderElement.tsx +5 -0
  40. package/src/UI/Pages/ComposableScreen/types.ts +25 -0
@@ -0,0 +1,348 @@
1
+ import React from "react";
2
+ import { z } from "zod";
3
+ import { Image as RNImage, View, StyleSheet, UIManager } from "react-native";
4
+ import Svg, { Defs, Rect, RadialGradient, Stop } from "react-native-svg";
5
+ import { BaseBoxProps, BaseBoxPropsSchema, GradientEdge } from "./BaseBoxProps";
6
+ import type { GradientBackground } from "./BaseBoxProps";
7
+ import { UIElement } from "../types";
8
+ import { RenderContext, dim } from "./shared";
9
+ import { GradientBox } from "./GradientBox";
10
+
11
+ // expo-image (better webp/avif) — optional, falls back to RN Image.
12
+ let ExpoImage: React.ComponentType<any> | null = null;
13
+ try {
14
+ ExpoImage = require("expo-image").Image;
15
+ } catch {
16
+ // expo-image not installed — RN Image fallback below
17
+ }
18
+
19
+ // @react-native-masked-view/masked-view — optional. Absent → no progressive
20
+ // mask, so we degrade to a flat gradient scrim rather than a full-screen blur.
21
+ let MaskedView: React.ComponentType<any> | null = null;
22
+ try {
23
+ MaskedView = require("@react-native-masked-view/masked-view").default;
24
+ } catch {
25
+ // masked-view not installed
26
+ }
27
+
28
+ // expo-linear-gradient — optional. Used for the LINEAR mask + tint/scrim
29
+ // gradients. Absent → plain dark View scrim (radial masks use react-native-svg,
30
+ // a required dep, so they don't depend on this).
31
+ let LinearGradient: React.ComponentType<any> | null = null;
32
+ try {
33
+ LinearGradient = require("expo-linear-gradient").LinearGradient;
34
+ } catch {
35
+ // expo-linear-gradient not installed
36
+ }
37
+
38
+ // Mirror of the headless schema (UI mirrors stay self-contained — they don't
39
+ // import headless internals). Keep in lockstep with
40
+ // packages/onboarding/src/steps/ComposableScreen/elements/ProgressiveBlurImageElement.ts.
41
+ export type BlurMaskStop = { position: number; opacity: number };
42
+ export type LinearBlurMask = { type?: "linear"; from: GradientEdge; to: GradientEdge; stops: BlurMaskStop[] };
43
+ export type RadialBlurMask = {
44
+ type: "radial";
45
+ center?: { x: number; y: number };
46
+ radius?: number;
47
+ stops: BlurMaskStop[];
48
+ };
49
+ export type BlurMask = LinearBlurMask | RadialBlurMask;
50
+
51
+ export type ProgressiveBlurImageElementProps = BaseBoxProps & {
52
+ url: string;
53
+ aspectRatio?: number;
54
+ resizeMode?: "cover" | "contain" | "stretch" | "center";
55
+ intensity: number;
56
+ tint?: "light" | "dark" | "default";
57
+ mask: BlurMask;
58
+ maxBlurOpacity?: number;
59
+ };
60
+
61
+ const BlurMaskStopSchema = z.object({
62
+ position: z.number().min(0).max(1),
63
+ opacity: z.number().min(0).max(1),
64
+ });
65
+
66
+ const EDGE_ENUM = z.enum(["top", "bottom", "left", "right", "topLeft", "topRight", "bottomLeft", "bottomRight"]);
67
+
68
+ const LinearBlurMaskSchema = z.object({
69
+ type: z.literal("linear").optional(),
70
+ from: EDGE_ENUM,
71
+ to: EDGE_ENUM,
72
+ stops: z.array(BlurMaskStopSchema).min(2, "blur mask requires at least 2 stops"),
73
+ });
74
+
75
+ const RadialBlurMaskSchema = z.object({
76
+ type: z.literal("radial"),
77
+ center: z.object({ x: z.number().min(0).max(1), y: z.number().min(0).max(1) }).optional(),
78
+ radius: z.number().positive().optional(),
79
+ stops: z.array(BlurMaskStopSchema).min(2, "blur mask requires at least 2 stops"),
80
+ });
81
+
82
+ export const ProgressiveBlurImageElementPropsSchema = BaseBoxPropsSchema.extend({
83
+ url: z.string().min(1, "url must not be empty"),
84
+ aspectRatio: z.number().optional(),
85
+ resizeMode: z.enum(["cover", "contain", "stretch", "center"]).optional(),
86
+ intensity: z.number().min(0).max(100),
87
+ tint: z.enum(["light", "dark", "default"]).optional(),
88
+ mask: z.union([LinearBlurMaskSchema, RadialBlurMaskSchema]),
89
+ maxBlurOpacity: z.number().min(0).max(1).optional(),
90
+ });
91
+
92
+ type ResizeMode = "cover" | "contain" | "stretch" | "center";
93
+
94
+ const CONTENT_FIT: Record<ResizeMode, "cover" | "contain" | "fill" | "none"> = {
95
+ cover: "cover",
96
+ contain: "contain",
97
+ stretch: "fill",
98
+ center: "none",
99
+ };
100
+
101
+ const EDGE_POINT: Record<string, { x: number; y: number }> = {
102
+ top: { x: 0.5, y: 0 },
103
+ bottom: { x: 0.5, y: 1 },
104
+ left: { x: 0, y: 0.5 },
105
+ right: { x: 1, y: 0.5 },
106
+ topLeft: { x: 0, y: 0 },
107
+ topRight: { x: 1, y: 0 },
108
+ bottomLeft: { x: 0, y: 1 },
109
+ bottomRight: { x: 1, y: 1 },
110
+ };
111
+
112
+ const renderRaster = (
113
+ url: string,
114
+ resizeMode: ResizeMode | undefined,
115
+ style: any,
116
+ blurRadius?: number
117
+ ): React.ReactElement =>
118
+ ExpoImage ? (
119
+ <ExpoImage source={url} contentFit={CONTENT_FIT[resizeMode ?? "cover"]} blurRadius={blurRadius} style={style} />
120
+ ) : (
121
+ <RNImage source={{ uri: url }} resizeMode={resizeMode} blurRadius={blurRadius} style={style} />
122
+ );
123
+
124
+ // expo-blur's BlurView blurs the *backdrop behind it*, but a MaskedView renders
125
+ // its child into an isolated offscreen layer with no backdrop to sample — so a
126
+ // masked BlurView is transparent on iOS (no blur, no tint). Instead we mask a
127
+ // real blurred *copy* of the image, which composites reliably on both platforms.
128
+ // Map the 0–100 intensity onto an expo-image/RN blurRadius in px.
129
+ const intensityToBlurRadius = (intensity: number): number => Math.max(0, Math.round(intensity * 0.3));
130
+
131
+ const isRadialMask = (mask: BlurMask): mask is RadialBlurMask => mask.type === "radial";
132
+
133
+ type BlurUIElement = Extract<UIElement, { type: "ProgressiveBlurImage" }>;
134
+
135
+ type Props = {
136
+ element: BlurUIElement;
137
+ ctx: RenderContext;
138
+ };
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // LINEAR helpers (expo-linear-gradient).
142
+ // ---------------------------------------------------------------------------
143
+
144
+ // Mask alpha (= blur strength) → black with that alpha. MaskedView keys off the
145
+ // alpha channel of its mask element, so a transparent→opaque black ramp reveals
146
+ // the blurred image copy only where the mask is opaque.
147
+ const linearMaskColors = (mask: LinearBlurMask, maxBlurOpacity: number): string[] =>
148
+ mask.stops.map((s) => `rgba(0,0,0,${(s.opacity * maxBlurOpacity).toFixed(3)})`);
149
+
150
+ // `tint` → an rgb triple for the darkening/lightening overlay. "default" = no tint.
151
+ const tintRgb = (tint?: "light" | "dark" | "default"): string | null =>
152
+ tint === "dark" ? "0,0,0" : tint === "light" ? "255,255,255" : null;
153
+
154
+ // A linear color gradient following the mask shape (tint overlay / fallback
155
+ // scrim). Tint alpha tracks the mask strength × maxBlurOpacity so a "dark" tint
156
+ // actually reads dark (no extra dampening).
157
+ const linearColorGradient = (
158
+ mask: LinearBlurMask,
159
+ maxBlurOpacity: number,
160
+ rgb: string
161
+ ): GradientBackground => ({
162
+ type: "linear",
163
+ from: mask.from,
164
+ to: mask.to,
165
+ stops: mask.stops.map((s) => ({
166
+ color: `rgba(${rgb},${(s.opacity * maxBlurOpacity).toFixed(3)})`,
167
+ position: s.position,
168
+ })),
169
+ });
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // RADIAL helpers (react-native-svg — a required dep, always available).
173
+ // A radial color/alpha gradient rect; `objectBoundingBox` units map cx/cy/r to
174
+ // 0..1 fractions of the box (so a non-square box yields the Figma ellipse).
175
+ // ---------------------------------------------------------------------------
176
+
177
+ const RadialSvg = ({
178
+ mask,
179
+ id,
180
+ color,
181
+ opacityScale,
182
+ }: {
183
+ mask: RadialBlurMask;
184
+ id: string;
185
+ /** SVG stop color, e.g. "black" or "rgb(0,0,0)". */
186
+ color: string;
187
+ /** Multiplier applied to each stop's opacity. */
188
+ opacityScale: number;
189
+ }): React.ReactElement => {
190
+ const c = mask.center ?? { x: 0.5, y: 0.5 };
191
+ const r = mask.radius ?? 0.75;
192
+ return (
193
+ <Svg style={StyleSheet.absoluteFillObject} width="100%" height="100%">
194
+ <Defs>
195
+ <RadialGradient id={id} cx={String(c.x)} cy={String(c.y)} r={String(r)} gradientUnits="objectBoundingBox">
196
+ {mask.stops.map((s, i) => (
197
+ <Stop
198
+ key={i}
199
+ offset={String(s.position)}
200
+ stopColor={color}
201
+ stopOpacity={String(Math.min(1, s.opacity * opacityScale))}
202
+ />
203
+ ))}
204
+ </RadialGradient>
205
+ </Defs>
206
+ <Rect x="0" y="0" width="100%" height="100%" fill={`url(#${id})`} />
207
+ </Svg>
208
+ );
209
+ };
210
+
211
+ // The JS package may be present (hoisted in a monorepo / installed) while the
212
+ // NATIVE view manager is missing — e.g. a dev-client binary built before the
213
+ // dep was added. Then `require` succeeds but rendering `<MaskedView>` throws
214
+ // "View config not found for component RNCMaskedView". Probe the native registry
215
+ // when we can; `hasViewManagerConfig` is absent on some arch/versions, so a
216
+ // `false` only suppresses when we can actually tell (the boundary below is the
217
+ // reliable backstop in all other cases).
218
+ const nativeMaskedViewAvailable = (): boolean => {
219
+ const has = (UIManager as any)?.hasViewManagerConfig;
220
+ if (typeof has !== "function") return true; // can't determine → let the boundary guard it
221
+ return !!has("RNCMaskedView");
222
+ };
223
+
224
+ // Backstop: if the masked-blur subtree throws at render/commit (native view
225
+ // missing on the running binary), swap to the plain fallback instead of
226
+ // crashing the whole onboarding screen.
227
+ class ProgressiveBlurBoundary extends React.Component<
228
+ { fallback: React.ReactNode; children: React.ReactNode },
229
+ { failed: boolean }
230
+ > {
231
+ state = { failed: false };
232
+ static getDerivedStateFromError() {
233
+ return { failed: true };
234
+ }
235
+ componentDidCatch() {
236
+ // swallow — degradation is intentional, not an error to surface
237
+ }
238
+ render() {
239
+ return this.state.failed ? this.props.fallback : this.props.children;
240
+ }
241
+ }
242
+
243
+ export const ProgressiveBlurImageElementComponent = ({ element }: Props): React.ReactElement => {
244
+ const p = element.props;
245
+ const maxBlurOpacity = p.maxBlurOpacity ?? 1;
246
+ const radial = isRadialMask(p.mask);
247
+ const rgb = tintRgb(p.tint);
248
+
249
+ const containerStyle = {
250
+ flex: p.flex,
251
+ flexShrink: p.flexShrink,
252
+ flexGrow: p.flexGrow,
253
+ alignSelf: p.alignSelf,
254
+ aspectRatio: p.aspectRatio,
255
+ width: dim(p.width),
256
+ height: dim(p.height),
257
+ minWidth: p.minWidth,
258
+ maxWidth: p.maxWidth,
259
+ minHeight: p.minHeight,
260
+ maxHeight: p.maxHeight,
261
+ borderRadius: p.borderRadius,
262
+ borderWidth: p.borderWidth,
263
+ borderColor: p.borderColor,
264
+ opacity: p.opacity,
265
+ overflow: (p.overflow ?? "hidden") as any,
266
+ margin: p.margin,
267
+ marginHorizontal: p.marginHorizontal,
268
+ marginVertical: p.marginVertical,
269
+ backgroundColor: p.backgroundColor,
270
+ } as any;
271
+
272
+ const sharpImage = renderRaster(p.url, p.resizeMode, StyleSheet.absoluteFillObject);
273
+
274
+ // Dark scrim (degraded path + error-boundary fallback). Radial → SVG (always
275
+ // available), linear → GradientBox (plain View when expo-linear-gradient absent).
276
+ // `isRadialMask(p.mask)` narrows the union in each branch.
277
+ const scrim = isRadialMask(p.mask) ? (
278
+ <RadialSvg mask={p.mask} id={`pbi-fb-${element.id}`} color="black" opacityScale={maxBlurOpacity} />
279
+ ) : (
280
+ <GradientBox
281
+ gradient={linearColorGradient(p.mask, maxBlurOpacity, "0,0,0")}
282
+ style={StyleSheet.absoluteFillObject as any}
283
+ />
284
+ );
285
+
286
+ const fallback = (
287
+ <View style={containerStyle}>
288
+ {sharpImage}
289
+ {scrim}
290
+ </View>
291
+ );
292
+
293
+ // Full path needs masked-view (+ its native view) and, for a LINEAR mask, the
294
+ // expo-linear-gradient dep. A RADIAL mask renders via react-native-svg (always
295
+ // available). expo-blur is intentionally not used — a masked BlurView is
296
+ // transparent on iOS (see renderRaster note).
297
+ const gradientDepReady = radial || !!LinearGradient;
298
+ const canProgressiveBlur = MaskedView && nativeMaskedViewAvailable() && gradientDepReady;
299
+
300
+ if (!canProgressiveBlur) return fallback;
301
+
302
+ const Masked = MaskedView!;
303
+ const Gradient = LinearGradient as React.ComponentType<any>; // non-null for linear masks (gradientDepReady)
304
+ const blurredCopy = renderRaster(
305
+ p.url,
306
+ p.resizeMode,
307
+ StyleSheet.absoluteFillObject,
308
+ intensityToBlurRadius(p.intensity)
309
+ );
310
+
311
+ // Mask element: opaque where the blur should show.
312
+ const maskElement = isRadialMask(p.mask) ? (
313
+ <RadialSvg mask={p.mask} id={`pbi-mask-${element.id}`} color="black" opacityScale={maxBlurOpacity} />
314
+ ) : (
315
+ <Gradient
316
+ colors={linearMaskColors(p.mask, maxBlurOpacity)}
317
+ start={EDGE_POINT[p.mask.from]}
318
+ end={EDGE_POINT[p.mask.to]}
319
+ locations={p.mask.stops.map((s) => s.position)}
320
+ style={StyleSheet.absoluteFillObject}
321
+ />
322
+ );
323
+
324
+ // Tint overlay following the mask shape (the Figma dark tint).
325
+ const tintOverlay =
326
+ rgb == null ? null : isRadialMask(p.mask) ? (
327
+ <RadialSvg mask={p.mask} id={`pbi-tint-${element.id}`} color={`rgb(${rgb})`} opacityScale={maxBlurOpacity} />
328
+ ) : (
329
+ <GradientBox
330
+ gradient={linearColorGradient(p.mask, maxBlurOpacity, rgb)}
331
+ style={StyleSheet.absoluteFillObject as any}
332
+ />
333
+ );
334
+
335
+ return (
336
+ <ProgressiveBlurBoundary fallback={fallback}>
337
+ <View style={containerStyle}>
338
+ {/* Sharp base. */}
339
+ {sharpImage}
340
+ {/* Blurred copy, revealed only where the mask is opaque → progressive blur. */}
341
+ <Masked style={StyleSheet.absoluteFillObject} maskElement={maskElement}>
342
+ {blurredCopy}
343
+ </Masked>
344
+ {tintOverlay}
345
+ </View>
346
+ </ProgressiveBlurBoundary>
347
+ );
348
+ };
@@ -5,10 +5,12 @@ import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
5
5
  import { UIElement } from "../types";
6
6
  import { RenderContext, dim } from "./shared";
7
7
  import { GradientBox } from "./GradientBox";
8
+ import { triggerHaptic, type HapticStyle } from "./haptics";
8
9
 
9
10
  export type RadioGroupElementProps = BaseBoxProps & {
10
11
  variableName?: string;
11
12
  defaultValue?: string;
13
+ haptic?: HapticStyle;
12
14
  gap?: number;
13
15
  direction?: "vertical" | "horizontal";
14
16
  showTick?: boolean;
@@ -33,6 +35,7 @@ export type RadioGroupElementProps = BaseBoxProps & {
33
35
  export const RadioGroupElementPropsSchema = BaseBoxPropsSchema.extend({
34
36
  variableName: z.string().optional(),
35
37
  defaultValue: z.string().optional(),
38
+ haptic: z.enum(["none", "light", "medium", "heavy", "soft", "rigid"]).optional(),
36
39
  gap: z.number().optional(),
37
40
  direction: z.enum(["vertical", "horizontal"]).optional(),
38
41
  showTick: z.boolean().optional(),
@@ -83,6 +86,7 @@ export const RadioGroupComponent = ({ element, ctx }: Props): React.ReactElement
83
86
 
84
87
  const handleSelect = (value: string, label: string) => {
85
88
  if (element.props.variableName) {
89
+ triggerHaptic(element.props.haptic);
86
90
  setVariable(element.props.variableName, { value, label });
87
91
  }
88
92
  };
@@ -0,0 +1,24 @@
1
+ // Optional peer dep: expo-haptics. Mirrors the dynamic-require pattern used by
2
+ // Ratings (expo-store-review) and GradientBox (expo-linear-gradient) — graceful,
3
+ // never throws. Not installed → no-op. "none"/undefined → no-op.
4
+ let Haptics: any;
5
+ try {
6
+ Haptics = require("expo-haptics");
7
+ } catch {
8
+ Haptics = null;
9
+ }
10
+
11
+ export type HapticStyle = "none" | "light" | "medium" | "heavy" | "soft" | "rigid";
12
+
13
+ export function triggerHaptic(style?: HapticStyle): void {
14
+ if (!style || style === "none" || !Haptics?.impactAsync) return;
15
+ const map: Record<Exclude<HapticStyle, "none">, any> = {
16
+ light: Haptics.ImpactFeedbackStyle.Light,
17
+ medium: Haptics.ImpactFeedbackStyle.Medium,
18
+ heavy: Haptics.ImpactFeedbackStyle.Heavy,
19
+ soft: Haptics.ImpactFeedbackStyle.Soft,
20
+ rigid: Haptics.ImpactFeedbackStyle.Rigid,
21
+ };
22
+ // Best-effort: swallow rejection (unsupported device, etc.).
23
+ Haptics.impactAsync(map[style]).catch(() => {});
24
+ }
@@ -7,6 +7,7 @@ import { StackElementComponent } from "./StackElement";
7
7
  import { TextElementComponent } from "./TextElement";
8
8
  import { RichTextElementComponent } from "./RichTextElement";
9
9
  import { ImageElementComponent } from "./ImageElement";
10
+ import { ProgressiveBlurImageElementComponent } from "./ProgressiveBlurImageElement";
10
11
  import { LottieElementComponent } from "./LottieElement";
11
12
  import { RiveElementRenderer } from "./RiveElement";
12
13
  import { IconElementComponent } from "./IconElement";
@@ -56,6 +57,10 @@ export const renderElement = (
56
57
  return <ImageElementComponent key={element.id} element={element} ctx={ctx} />;
57
58
  }
58
59
 
60
+ if (element.type === "ProgressiveBlurImage") {
61
+ return <ProgressiveBlurImageElementComponent key={element.id} element={element} ctx={ctx} />;
62
+ }
63
+
59
64
  if (element.type === "Lottie") {
60
65
  return <LottieElementComponent key={element.id} element={element} ctx={ctx} />;
61
66
  }
@@ -12,6 +12,10 @@ import { type StackElementProps, StackElementPropsSchema } from "./elements/Stac
12
12
  import { type TextElementProps, TextElementPropsSchema } from "./elements/TextElement";
13
13
  import { type RichTextElementProps, RichTextElementPropsSchema } from "./elements/RichTextElement";
14
14
  import { type ImageElementProps, ImageElementPropsSchema } from "./elements/ImageElement";
15
+ import {
16
+ type ProgressiveBlurImageElementProps,
17
+ ProgressiveBlurImageElementPropsSchema,
18
+ } from "./elements/ProgressiveBlurImageElement";
15
19
  import { type LottieElementProps, LottieElementPropsSchema } from "./elements/LottieElement";
16
20
  import { type RiveElementProps, RiveElementPropsSchema } from "./elements/RiveElement";
17
21
  import { type IconElementProps, IconElementPropsSchema } from "./elements/IconElement";
@@ -40,6 +44,13 @@ export type { StackElementProps } from "./elements/StackElement";
40
44
  export type { TextElementProps } from "./elements/TextElement";
41
45
  export type { RichTextElementProps } from "./elements/RichTextElement";
42
46
  export type { ImageElementProps } from "./elements/ImageElement";
47
+ export type {
48
+ ProgressiveBlurImageElementProps,
49
+ BlurMask,
50
+ LinearBlurMask,
51
+ RadialBlurMask,
52
+ BlurMaskStop,
53
+ } from "./elements/ProgressiveBlurImageElement";
43
54
  export type { LottieElementProps } from "./elements/LottieElement";
44
55
  export type { RiveElementProps } from "./elements/RiveElement";
45
56
  export type { IconElementProps } from "./elements/IconElement";
@@ -93,6 +104,13 @@ export type UIElement =
93
104
  type: "Image";
94
105
  props: ImageElementProps;
95
106
  }
107
+ | {
108
+ id: string;
109
+ name?: string;
110
+ renderWhen?: LeafCondition | ConditionGroup;
111
+ type: "ProgressiveBlurImage";
112
+ props: ProgressiveBlurImageElementProps;
113
+ }
96
114
  | {
97
115
  id: string;
98
116
  name?: string;
@@ -248,6 +266,13 @@ export const UIElementSchema: z.ZodType<UIElement> = z.lazy(() =>
248
266
  type: z.literal("Image"),
249
267
  props: ImageElementPropsSchema,
250
268
  }),
269
+ z.object({
270
+ id: z.string(),
271
+ name: z.string().optional(),
272
+ renderWhen: z.union([LeafConditionSchema, ConditionGroupSchema]).optional(),
273
+ type: z.literal("ProgressiveBlurImage"),
274
+ props: ProgressiveBlurImageElementPropsSchema,
275
+ }),
251
276
  z.object({
252
277
  id: z.string(),
253
278
  name: z.string().optional(),