@rocapine/react-native-onboarding-ui 1.7.0 → 1.8.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/dist/UI/OnboardingPage.js +1 -1
- package/dist/UI/OnboardingPage.js.map +1 -1
- package/dist/UI/Pages/ComposableScreen/Renderer.d.ts +0 -2
- package/dist/UI/Pages/ComposableScreen/Renderer.d.ts.map +1 -1
- package/dist/UI/Pages/ComposableScreen/Renderer.js +13 -274
- package/dist/UI/Pages/ComposableScreen/Renderer.js.map +1 -1
- package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.d.ts +39 -0
- package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.js +20 -0
- package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts +67 -0
- package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js +65 -0
- package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/IconElement.d.ts +49 -0
- package/dist/UI/Pages/ComposableScreen/elements/IconElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/IconElement.js +37 -0
- package/dist/UI/Pages/ComposableScreen/elements/IconElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts +50 -0
- package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js +34 -0
- package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/InputElement.d.ts +110 -0
- package/dist/UI/Pages/ComposableScreen/elements/InputElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/InputElement.js +66 -0
- package/dist/UI/Pages/ComposableScreen/elements/InputElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/LottieElement.d.ts +47 -0
- package/dist/UI/Pages/ComposableScreen/elements/LottieElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/LottieElement.js +60 -0
- package/dist/UI/Pages/ComposableScreen/elements/LottieElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts +86 -0
- package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js +120 -0
- package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/RiveElement.d.ts +70 -0
- package/dist/UI/Pages/ComposableScreen/elements/RiveElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/RiveElement.js +68 -0
- package/dist/UI/Pages/ComposableScreen/elements/RiveElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts +94 -0
- package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/StackElement.js +66 -0
- package/dist/UI/Pages/ComposableScreen/elements/StackElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts +66 -0
- package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/TextElement.js +59 -0
- package/dist/UI/Pages/ComposableScreen/elements/TextElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/VideoElement.d.ts +49 -0
- package/dist/UI/Pages/ComposableScreen/elements/VideoElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/VideoElement.js +84 -0
- package/dist/UI/Pages/ComposableScreen/elements/VideoElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts +5 -0
- package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/renderElement.js +49 -0
- package/dist/UI/Pages/ComposableScreen/elements/renderElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/shared.d.ts +13 -0
- package/dist/UI/Pages/ComposableScreen/elements/shared.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/shared.js +6 -0
- package/dist/UI/Pages/ComposableScreen/elements/shared.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/types.d.ts +40 -114
- package/dist/UI/Pages/ComposableScreen/types.d.ts.map +1 -1
- package/dist/UI/Pages/ComposableScreen/types.js +33 -122
- package/dist/UI/Pages/ComposableScreen/types.js.map +1 -1
- package/dist/UI/Provider/OnboardingProgressProvider.d.ts +6 -2
- package/dist/UI/Provider/OnboardingProgressProvider.d.ts.map +1 -1
- package/dist/UI/Provider/OnboardingProgressProvider.js +4 -3
- package/dist/UI/Provider/OnboardingProgressProvider.js.map +1 -1
- package/package.json +2 -2
- package/src/UI/OnboardingPage.tsx +1 -1
- package/src/UI/Pages/ComposableScreen/Renderer.tsx +22 -431
- package/src/UI/Pages/ComposableScreen/elements/BaseBoxProps.ts +33 -0
- package/src/UI/Pages/ComposableScreen/elements/ButtonElement.tsx +96 -0
- package/src/UI/Pages/ComposableScreen/elements/IconElement.tsx +67 -0
- package/src/UI/Pages/ComposableScreen/elements/ImageElement.tsx +52 -0
- package/src/UI/Pages/ComposableScreen/elements/InputElement.tsx +109 -0
- package/src/UI/Pages/ComposableScreen/elements/LottieElement.tsx +97 -0
- package/src/UI/Pages/ComposableScreen/elements/RadioGroupElement.tsx +182 -0
- package/src/UI/Pages/ComposableScreen/elements/RiveElement.tsx +105 -0
- package/src/UI/Pages/ComposableScreen/elements/StackElement.tsx +106 -0
- package/src/UI/Pages/ComposableScreen/elements/TextElement.tsx +95 -0
- package/src/UI/Pages/ComposableScreen/elements/VideoElement.tsx +113 -0
- package/src/UI/Pages/ComposableScreen/elements/renderElement.tsx +61 -0
- package/src/UI/Pages/ComposableScreen/elements/shared.ts +15 -0
- package/src/UI/Pages/ComposableScreen/types.ts +56 -235
- package/src/UI/Provider/OnboardingProgressProvider.tsx +8 -5
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Image } from "react-native";
|
|
4
|
+
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
5
|
+
import { UIElement } from "../types";
|
|
6
|
+
import { RenderContext } from "./shared";
|
|
7
|
+
|
|
8
|
+
export type ImageElementProps = BaseBoxProps & {
|
|
9
|
+
url: string;
|
|
10
|
+
aspectRatio?: number;
|
|
11
|
+
resizeMode?: "cover" | "contain" | "stretch" | "center";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const ImageElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
15
|
+
url: z.string().min(1, "url must not be empty"),
|
|
16
|
+
aspectRatio: z.number().optional(),
|
|
17
|
+
resizeMode: z.enum(["cover", "contain", "stretch", "center"]).optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
type ImageUIElement = Extract<UIElement, { type: "Image" }>;
|
|
21
|
+
|
|
22
|
+
type Props = {
|
|
23
|
+
element: ImageUIElement;
|
|
24
|
+
ctx: RenderContext;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ImageElementComponent = ({ element }: Props): React.ReactElement => {
|
|
28
|
+
const hasExplicitHeight = element.props.height !== undefined;
|
|
29
|
+
const aspectRatio = hasExplicitHeight ? undefined : (element.props.aspectRatio ?? 16 / 9);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Image
|
|
33
|
+
source={{ uri: element.props.url }}
|
|
34
|
+
resizeMode={element.props.resizeMode ?? "cover"}
|
|
35
|
+
style={{
|
|
36
|
+
width: element.props.width ?? "100%",
|
|
37
|
+
height: element.props.height,
|
|
38
|
+
aspectRatio,
|
|
39
|
+
borderRadius: element.props.borderRadius,
|
|
40
|
+
borderWidth: element.props.borderWidth,
|
|
41
|
+
borderColor: element.props.borderColor,
|
|
42
|
+
opacity: element.props.opacity,
|
|
43
|
+
margin: element.props.margin,
|
|
44
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
45
|
+
marginVertical: element.props.marginVertical,
|
|
46
|
+
padding: element.props.padding,
|
|
47
|
+
paddingHorizontal: element.props.paddingHorizontal,
|
|
48
|
+
paddingVertical: element.props.paddingVertical,
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { View, TextInput } from "react-native";
|
|
4
|
+
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
5
|
+
import { UIElement } from "../types";
|
|
6
|
+
import { RenderContext } from "./shared";
|
|
7
|
+
|
|
8
|
+
export type InputElementProps = BaseBoxProps & {
|
|
9
|
+
variableName?: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
keyboardType?: "default" | "email-address" | "numeric" | "phone-pad" | "decimal-pad" | "url" | "number-pad" | "ascii-capable" | "numbers-and-punctuation" | "name-phone-pad" | "twitter" | "web-search" | "visible-password";
|
|
13
|
+
returnKeyType?: "done" | "next" | "go" | "search" | "send" | "default" | "emergency-call" | "google" | "join" | "route" | "yahoo" | "none" | "previous";
|
|
14
|
+
autoCapitalize?: "none" | "sentences" | "words" | "characters";
|
|
15
|
+
secureTextEntry?: boolean;
|
|
16
|
+
maxLength?: number;
|
|
17
|
+
multiline?: boolean;
|
|
18
|
+
numberOfLines?: number;
|
|
19
|
+
editable?: boolean;
|
|
20
|
+
color?: string;
|
|
21
|
+
backgroundColor?: string;
|
|
22
|
+
fontSize?: number;
|
|
23
|
+
fontWeight?: string;
|
|
24
|
+
textAlign?: "left" | "center" | "right";
|
|
25
|
+
placeholderColor?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const InputElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
29
|
+
variableName: z.string().min(1).optional(),
|
|
30
|
+
placeholder: z.string().optional(),
|
|
31
|
+
defaultValue: z.string().optional(),
|
|
32
|
+
keyboardType: z.enum(["default", "email-address", "numeric", "phone-pad", "decimal-pad", "url", "number-pad", "ascii-capable", "numbers-and-punctuation", "name-phone-pad", "twitter", "web-search", "visible-password"]).optional(),
|
|
33
|
+
returnKeyType: z.enum(["done", "next", "go", "search", "send", "default", "emergency-call", "google", "join", "route", "yahoo", "none", "previous"]).optional(),
|
|
34
|
+
autoCapitalize: z.enum(["none", "sentences", "words", "characters"]).optional(),
|
|
35
|
+
secureTextEntry: z.boolean().optional(),
|
|
36
|
+
maxLength: z.number().int().nonnegative().optional(),
|
|
37
|
+
multiline: z.boolean().optional(),
|
|
38
|
+
numberOfLines: z.number().int().nonnegative().optional(),
|
|
39
|
+
editable: z.boolean().optional(),
|
|
40
|
+
color: z.string().optional(),
|
|
41
|
+
backgroundColor: z.string().optional(),
|
|
42
|
+
fontSize: z.number().optional(),
|
|
43
|
+
fontWeight: z.string().optional(),
|
|
44
|
+
textAlign: z.enum(["left", "center", "right"]).optional(),
|
|
45
|
+
placeholderColor: z.string().optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
type InputUIElement = Extract<UIElement, { type: "Input" }>;
|
|
49
|
+
|
|
50
|
+
type Props = {
|
|
51
|
+
element: InputUIElement;
|
|
52
|
+
ctx: RenderContext;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const InputElementComponent = ({ element, ctx }: Props): React.ReactElement => {
|
|
56
|
+
const { theme, variables, setVariable } = ctx;
|
|
57
|
+
const persistedValue = element.props.variableName ? variables[element.props.variableName]?.value : undefined;
|
|
58
|
+
const [value, setValue] = useState(persistedValue ?? element.props.defaultValue ?? "");
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (element.props.variableName && element.props.defaultValue !== undefined && persistedValue === undefined) {
|
|
62
|
+
setVariable(element.props.variableName, { value: element.props.defaultValue });
|
|
63
|
+
}
|
|
64
|
+
}, [element.props.variableName, element.props.defaultValue, persistedValue]);
|
|
65
|
+
|
|
66
|
+
const handleChange = (text: string) => {
|
|
67
|
+
setValue(text);
|
|
68
|
+
if (element.props.variableName) {
|
|
69
|
+
setVariable(element.props.variableName, { value: text });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<TextInput
|
|
75
|
+
value={value}
|
|
76
|
+
onChangeText={handleChange}
|
|
77
|
+
placeholder={element.props.placeholder}
|
|
78
|
+
placeholderTextColor={element.props.placeholderColor ?? theme.colors.text.tertiary}
|
|
79
|
+
keyboardType={element.props.keyboardType ?? "default"}
|
|
80
|
+
returnKeyType={element.props.returnKeyType ?? "done"}
|
|
81
|
+
autoCapitalize={element.props.autoCapitalize ?? "sentences"}
|
|
82
|
+
secureTextEntry={element.props.secureTextEntry ?? false}
|
|
83
|
+
maxLength={element.props.maxLength}
|
|
84
|
+
multiline={element.props.multiline ?? false}
|
|
85
|
+
numberOfLines={element.props.numberOfLines}
|
|
86
|
+
editable={element.props.editable ?? true}
|
|
87
|
+
style={{
|
|
88
|
+
alignSelf: element.props.alignSelf,
|
|
89
|
+
width: element.props.width,
|
|
90
|
+
height: element.props.height,
|
|
91
|
+
opacity: element.props.opacity,
|
|
92
|
+
margin: element.props.margin,
|
|
93
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
94
|
+
marginVertical: element.props.marginVertical,
|
|
95
|
+
backgroundColor: element.props.backgroundColor ?? theme.colors.neutral.lowest,
|
|
96
|
+
borderWidth: element.props.borderWidth ?? 1,
|
|
97
|
+
borderRadius: element.props.borderRadius ?? 8,
|
|
98
|
+
borderColor: element.props.borderColor ?? theme.colors.neutral.low,
|
|
99
|
+
color: element.props.color ?? theme.colors.text.primary,
|
|
100
|
+
fontSize: element.props.fontSize ?? theme.typography.textStyles.body.fontSize,
|
|
101
|
+
fontWeight: element.props.fontWeight as any,
|
|
102
|
+
textAlign: element.props.textAlign,
|
|
103
|
+
padding: element.props.padding ?? 12,
|
|
104
|
+
paddingHorizontal: element.props.paddingHorizontal,
|
|
105
|
+
paddingVertical: element.props.paddingVertical,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
4
|
+
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
5
|
+
import { UIElement } from "../types";
|
|
6
|
+
import { RenderContext } from "./shared";
|
|
7
|
+
import { getTextStyle } from "../../../Theme/helpers";
|
|
8
|
+
|
|
9
|
+
export type LottieElementProps = BaseBoxProps & {
|
|
10
|
+
source: string;
|
|
11
|
+
autoPlay?: boolean;
|
|
12
|
+
loop?: boolean;
|
|
13
|
+
speed?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const LottieElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
17
|
+
source: z.string().min(1, "source must not be empty"),
|
|
18
|
+
autoPlay: z.boolean().optional(),
|
|
19
|
+
loop: z.boolean().optional(),
|
|
20
|
+
speed: z.number().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
type LottieUIElement = Extract<UIElement, { type: "Lottie" }>;
|
|
24
|
+
|
|
25
|
+
let LottieView: React.ComponentType<{
|
|
26
|
+
source: string | object;
|
|
27
|
+
autoPlay?: boolean;
|
|
28
|
+
loop?: boolean;
|
|
29
|
+
speed?: number;
|
|
30
|
+
style?: object;
|
|
31
|
+
}> | null = null;
|
|
32
|
+
try {
|
|
33
|
+
LottieView = require("lottie-react-native").default;
|
|
34
|
+
} catch {
|
|
35
|
+
// lottie-react-native not installed
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Props = {
|
|
39
|
+
element: LottieUIElement;
|
|
40
|
+
ctx: RenderContext;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const LottieElementComponent = ({ element, ctx }: Props): React.ReactElement => {
|
|
44
|
+
const { theme } = ctx;
|
|
45
|
+
const wrapperStyle = {
|
|
46
|
+
width: element.props.width ?? ("100%" as `${number}%`),
|
|
47
|
+
height: element.props.height ?? 200,
|
|
48
|
+
opacity: element.props.opacity,
|
|
49
|
+
margin: element.props.margin,
|
|
50
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
51
|
+
marginVertical: element.props.marginVertical,
|
|
52
|
+
padding: element.props.padding,
|
|
53
|
+
paddingHorizontal: element.props.paddingHorizontal,
|
|
54
|
+
paddingVertical: element.props.paddingVertical,
|
|
55
|
+
borderWidth: element.props.borderWidth,
|
|
56
|
+
borderRadius: element.props.borderRadius,
|
|
57
|
+
borderColor: element.props.borderColor,
|
|
58
|
+
overflow: "hidden" as const,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (!LottieView) {
|
|
62
|
+
return (
|
|
63
|
+
<View style={[wrapperStyle, styles.mediaFallback, { backgroundColor: theme.colors.neutral.lowest }]}>
|
|
64
|
+
<Text style={[styles.mediaFallbackText, getTextStyle(theme, "caption"), { color: theme.colors.text.tertiary }]}>
|
|
65
|
+
Install lottie-react-native to render Lottie animations.
|
|
66
|
+
</Text>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<View style={wrapperStyle}>
|
|
73
|
+
<LottieView
|
|
74
|
+
source={{ uri: element.props.source }}
|
|
75
|
+
autoPlay={element.props.autoPlay ?? true}
|
|
76
|
+
loop={element.props.loop ?? true}
|
|
77
|
+
speed={element.props.speed}
|
|
78
|
+
style={styles.fill}
|
|
79
|
+
/>
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const styles = StyleSheet.create({
|
|
85
|
+
fill: {
|
|
86
|
+
width: "100%",
|
|
87
|
+
height: "100%",
|
|
88
|
+
},
|
|
89
|
+
mediaFallback: {
|
|
90
|
+
alignItems: "center",
|
|
91
|
+
justifyContent: "center",
|
|
92
|
+
},
|
|
93
|
+
mediaFallbackText: {
|
|
94
|
+
textAlign: "center",
|
|
95
|
+
paddingHorizontal: 16,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { View, Text, TouchableOpacity } from "react-native";
|
|
4
|
+
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
5
|
+
import { UIElement } from "../types";
|
|
6
|
+
import { RenderContext } from "./shared";
|
|
7
|
+
|
|
8
|
+
export type RadioGroupElementProps = BaseBoxProps & {
|
|
9
|
+
variableName?: string;
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
gap?: number;
|
|
12
|
+
direction?: "vertical" | "horizontal";
|
|
13
|
+
items: Array<{ label: string; value: string }>;
|
|
14
|
+
itemBackgroundColor?: string;
|
|
15
|
+
itemSelectedBackgroundColor?: string;
|
|
16
|
+
itemBorderColor?: string;
|
|
17
|
+
itemSelectedBorderColor?: string;
|
|
18
|
+
itemBorderRadius?: number;
|
|
19
|
+
itemBorderWidth?: number;
|
|
20
|
+
itemColor?: string;
|
|
21
|
+
itemSelectedColor?: string;
|
|
22
|
+
itemFontSize?: number;
|
|
23
|
+
itemFontWeight?: string;
|
|
24
|
+
itemFontFamily?: string;
|
|
25
|
+
itemPadding?: number;
|
|
26
|
+
itemPaddingHorizontal?: number;
|
|
27
|
+
itemPaddingVertical?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const RadioGroupElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
31
|
+
variableName: z.string().optional(),
|
|
32
|
+
defaultValue: z.string().optional(),
|
|
33
|
+
gap: z.number().optional(),
|
|
34
|
+
direction: z.enum(["vertical", "horizontal"]).optional(),
|
|
35
|
+
items: z.array(z.object({ label: z.string().trim().min(1, "item label must not be empty"), value: z.string().trim().min(1, "item value must not be empty") })).min(1, "items must not be empty"),
|
|
36
|
+
itemBackgroundColor: z.string().optional(),
|
|
37
|
+
itemSelectedBackgroundColor: z.string().optional(),
|
|
38
|
+
itemBorderColor: z.string().optional(),
|
|
39
|
+
itemSelectedBorderColor: z.string().optional(),
|
|
40
|
+
itemBorderRadius: z.number().optional(),
|
|
41
|
+
itemBorderWidth: z.number().optional(),
|
|
42
|
+
itemColor: z.string().optional(),
|
|
43
|
+
itemSelectedColor: z.string().optional(),
|
|
44
|
+
itemFontSize: z.number().optional(),
|
|
45
|
+
itemFontWeight: z.string().optional(),
|
|
46
|
+
itemFontFamily: z.string().optional(),
|
|
47
|
+
itemPadding: z.number().optional(),
|
|
48
|
+
itemPaddingHorizontal: z.number().optional(),
|
|
49
|
+
itemPaddingVertical: z.number().optional(),
|
|
50
|
+
}).superRefine((data, ctx) => {
|
|
51
|
+
const values = data.items.map((i) => i.value);
|
|
52
|
+
const unique = new Set(values);
|
|
53
|
+
if (unique.size !== values.length) {
|
|
54
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "item values must be unique", path: ["items"] });
|
|
55
|
+
}
|
|
56
|
+
if (data.defaultValue !== undefined && !unique.has(data.defaultValue)) {
|
|
57
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "defaultValue must match one of the item values", path: ["defaultValue"] });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
type RadioGroupUIElement = Extract<UIElement, { type: "RadioGroup" }>;
|
|
62
|
+
|
|
63
|
+
type Props = {
|
|
64
|
+
element: RadioGroupUIElement;
|
|
65
|
+
ctx: RenderContext;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const RadioGroupComponent = ({ element, ctx }: Props): React.ReactElement => {
|
|
69
|
+
const { theme, variables, setVariable } = ctx;
|
|
70
|
+
const selectedValue = element.props.variableName ? variables[element.props.variableName]?.value : undefined;
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (element.props.variableName && element.props.defaultValue && selectedValue === undefined) {
|
|
74
|
+
const defaultItem = element.props.items.find((i) => i.value === element.props.defaultValue);
|
|
75
|
+
setVariable(element.props.variableName, { value: element.props.defaultValue, label: defaultItem?.label });
|
|
76
|
+
}
|
|
77
|
+
}, [element.props.variableName, element.props.defaultValue, element.props.items, selectedValue]);
|
|
78
|
+
|
|
79
|
+
const handleSelect = (value: string, label: string) => {
|
|
80
|
+
if (element.props.variableName) {
|
|
81
|
+
setVariable(element.props.variableName, { value, label });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const isHorizontal = element.props.direction === "horizontal";
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<View
|
|
89
|
+
accessibilityRole="radiogroup"
|
|
90
|
+
style={{
|
|
91
|
+
flexDirection: isHorizontal ? "row" : "column",
|
|
92
|
+
flexWrap: isHorizontal ? "wrap" : undefined,
|
|
93
|
+
alignSelf: element.props.alignSelf,
|
|
94
|
+
gap: element.props.gap ?? 8,
|
|
95
|
+
width: element.props.width,
|
|
96
|
+
height: element.props.height,
|
|
97
|
+
margin: element.props.margin,
|
|
98
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
99
|
+
marginVertical: element.props.marginVertical,
|
|
100
|
+
padding: element.props.padding,
|
|
101
|
+
paddingHorizontal: element.props.paddingHorizontal,
|
|
102
|
+
paddingVertical: element.props.paddingVertical,
|
|
103
|
+
borderWidth: element.props.borderWidth,
|
|
104
|
+
borderRadius: element.props.borderRadius,
|
|
105
|
+
borderColor: element.props.borderColor,
|
|
106
|
+
opacity: element.props.opacity,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{element.props.items.map((item) => {
|
|
110
|
+
const isSelected = selectedValue === item.value;
|
|
111
|
+
// Note: "+ "15"" appends hex alpha (≈8% opacity) assuming a 6-digit hex primary color.
|
|
112
|
+
// This works for standard hex colors but may produce unexpected results for other color formats.
|
|
113
|
+
const bgColor = isSelected
|
|
114
|
+
? (element.props.itemSelectedBackgroundColor ?? theme.colors.primary + "15")
|
|
115
|
+
: (element.props.itemBackgroundColor ?? "transparent");
|
|
116
|
+
const textColor = isSelected
|
|
117
|
+
? (element.props.itemSelectedColor ?? theme.colors.primary)
|
|
118
|
+
: (element.props.itemColor ?? theme.colors.text.primary);
|
|
119
|
+
const borderColor = isSelected
|
|
120
|
+
? (element.props.itemSelectedBorderColor ?? theme.colors.primary)
|
|
121
|
+
: (element.props.itemBorderColor ?? theme.colors.neutral.low);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<TouchableOpacity
|
|
125
|
+
key={item.value}
|
|
126
|
+
activeOpacity={0.7}
|
|
127
|
+
onPress={() => handleSelect(item.value, item.label)}
|
|
128
|
+
accessibilityRole="radio"
|
|
129
|
+
accessibilityState={{ selected: isSelected, checked: isSelected }}
|
|
130
|
+
accessibilityLabel={item.label}
|
|
131
|
+
style={{
|
|
132
|
+
flexDirection: "row",
|
|
133
|
+
alignItems: "center",
|
|
134
|
+
gap: 12,
|
|
135
|
+
backgroundColor: bgColor,
|
|
136
|
+
borderRadius: element.props.itemBorderRadius ?? 8,
|
|
137
|
+
borderWidth: element.props.itemBorderWidth ?? 1,
|
|
138
|
+
borderColor: borderColor,
|
|
139
|
+
padding: element.props.itemPadding ?? (element.props.itemPaddingHorizontal === undefined && element.props.itemPaddingVertical === undefined ? 12 : undefined),
|
|
140
|
+
paddingHorizontal: element.props.itemPaddingHorizontal,
|
|
141
|
+
paddingVertical: element.props.itemPaddingVertical,
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<View
|
|
145
|
+
style={{
|
|
146
|
+
width: 20,
|
|
147
|
+
height: 20,
|
|
148
|
+
borderRadius: 10,
|
|
149
|
+
borderWidth: 2,
|
|
150
|
+
borderColor: isSelected ? theme.colors.primary : theme.colors.neutral.medium,
|
|
151
|
+
alignItems: "center",
|
|
152
|
+
justifyContent: "center",
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{isSelected && (
|
|
156
|
+
<View
|
|
157
|
+
style={{
|
|
158
|
+
width: 10,
|
|
159
|
+
height: 10,
|
|
160
|
+
borderRadius: 5,
|
|
161
|
+
backgroundColor: theme.colors.primary,
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
</View>
|
|
166
|
+
<Text
|
|
167
|
+
style={{
|
|
168
|
+
flexShrink: 1,
|
|
169
|
+
color: textColor,
|
|
170
|
+
fontSize: element.props.itemFontSize ?? theme.typography.textStyles.body.fontSize,
|
|
171
|
+
fontWeight: (element.props.itemFontWeight as any) ?? theme.typography.textStyles.body.fontWeight,
|
|
172
|
+
fontFamily: element.props.itemFontFamily,
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
{item.label}
|
|
176
|
+
</Text>
|
|
177
|
+
</TouchableOpacity>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
180
|
+
</View>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
4
|
+
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
5
|
+
import { UIElement } from "../types";
|
|
6
|
+
import { RenderContext } from "./shared";
|
|
7
|
+
import { getTextStyle } from "../../../Theme/helpers";
|
|
8
|
+
|
|
9
|
+
export type RiveElementProps = BaseBoxProps & {
|
|
10
|
+
url: string;
|
|
11
|
+
autoplay?: boolean;
|
|
12
|
+
fit?: "Contain" | "Cover" | "Fill" | "FitWidth" | "FitHeight" | "None" | "ScaleDown" | "Layout";
|
|
13
|
+
alignment?: "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight";
|
|
14
|
+
artboardName?: string;
|
|
15
|
+
stateMachineName?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const RiveElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
19
|
+
url: z.string().min(1, "url must not be empty"),
|
|
20
|
+
autoplay: z.boolean().optional(),
|
|
21
|
+
fit: z.enum(["Contain", "Cover", "Fill", "FitWidth", "FitHeight", "None", "ScaleDown", "Layout"]).optional(),
|
|
22
|
+
alignment: z.enum(["TopLeft", "TopCenter", "TopRight", "CenterLeft", "Center", "CenterRight", "BottomLeft", "BottomCenter", "BottomRight"]).optional(),
|
|
23
|
+
artboardName: z.string().optional(),
|
|
24
|
+
stateMachineName: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
type RiveUIElement = Extract<UIElement, { type: "Rive" }>;
|
|
28
|
+
|
|
29
|
+
let RiveElementComponent: React.ComponentType<{ element: RiveUIElement; riveStyle: object }> | null = null;
|
|
30
|
+
try {
|
|
31
|
+
const riveModule = require("rive-react-native");
|
|
32
|
+
const Rive = riveModule.default;
|
|
33
|
+
const { Fit, Alignment } = riveModule;
|
|
34
|
+
RiveElementComponent = ({ element, riveStyle }: { element: RiveUIElement; riveStyle: object }) => {
|
|
35
|
+
return (
|
|
36
|
+
<Rive
|
|
37
|
+
url={element.props.url}
|
|
38
|
+
autoplay={element.props.autoplay ?? true}
|
|
39
|
+
fit={element.props.fit ? Fit[element.props.fit] : Fit.Contain}
|
|
40
|
+
alignment={element.props.alignment ? Alignment[element.props.alignment] : Alignment.Center}
|
|
41
|
+
artboardName={element.props.artboardName}
|
|
42
|
+
stateMachineName={element.props.stateMachineName}
|
|
43
|
+
style={riveStyle}
|
|
44
|
+
onError={console.error}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
// rive-react-native not installed - will show fallback if Rive is used
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Props = {
|
|
53
|
+
element: RiveUIElement;
|
|
54
|
+
ctx: RenderContext;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const RiveElementRenderer = ({ element, ctx }: Props): React.ReactElement => {
|
|
58
|
+
const { theme } = ctx;
|
|
59
|
+
const wrapperStyle = {
|
|
60
|
+
width: element.props.width ?? ("100%" as `${number}%`),
|
|
61
|
+
height: element.props.height ?? 200,
|
|
62
|
+
opacity: element.props.opacity,
|
|
63
|
+
margin: element.props.margin,
|
|
64
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
65
|
+
marginVertical: element.props.marginVertical,
|
|
66
|
+
padding: element.props.padding,
|
|
67
|
+
paddingHorizontal: element.props.paddingHorizontal,
|
|
68
|
+
paddingVertical: element.props.paddingVertical,
|
|
69
|
+
borderWidth: element.props.borderWidth,
|
|
70
|
+
borderRadius: element.props.borderRadius,
|
|
71
|
+
borderColor: element.props.borderColor,
|
|
72
|
+
overflow: "hidden" as const,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!RiveElementComponent) {
|
|
76
|
+
return (
|
|
77
|
+
<View style={[wrapperStyle, styles.mediaFallback, { backgroundColor: theme.colors.neutral.lowest }]}>
|
|
78
|
+
<Text style={[styles.mediaFallbackText, getTextStyle(theme, "caption"), { color: theme.colors.text.tertiary }]}>
|
|
79
|
+
Install rive-react-native to render Rive animations.
|
|
80
|
+
</Text>
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<View style={wrapperStyle}>
|
|
87
|
+
<RiveElementComponent element={element} riveStyle={styles.fill} />
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
fill: {
|
|
94
|
+
width: "100%",
|
|
95
|
+
height: "100%",
|
|
96
|
+
},
|
|
97
|
+
mediaFallback: {
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
justifyContent: "center",
|
|
100
|
+
},
|
|
101
|
+
mediaFallbackText: {
|
|
102
|
+
textAlign: "center",
|
|
103
|
+
paddingHorizontal: 16,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import { UIElement } from "../types";
|
|
5
|
+
import { RenderContext } from "./shared";
|
|
6
|
+
|
|
7
|
+
export type StackElementProps = {
|
|
8
|
+
gap?: number;
|
|
9
|
+
padding?: number;
|
|
10
|
+
paddingHorizontal?: number;
|
|
11
|
+
paddingVertical?: number;
|
|
12
|
+
margin?: number;
|
|
13
|
+
marginHorizontal?: number;
|
|
14
|
+
marginVertical?: number;
|
|
15
|
+
flex?: number;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
minWidth?: number;
|
|
19
|
+
maxWidth?: number;
|
|
20
|
+
minHeight?: number;
|
|
21
|
+
maxHeight?: number;
|
|
22
|
+
alignItems?: "flex-start" | "center" | "flex-end" | "stretch";
|
|
23
|
+
alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline";
|
|
24
|
+
justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-around";
|
|
25
|
+
backgroundColor?: string;
|
|
26
|
+
flexWrap?: "wrap" | "nowrap";
|
|
27
|
+
flexShrink?: number;
|
|
28
|
+
borderWidth?: number;
|
|
29
|
+
borderRadius?: number;
|
|
30
|
+
borderColor?: string;
|
|
31
|
+
overflow?: "hidden" | "visible" | "scroll";
|
|
32
|
+
opacity?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const StackElementPropsSchema = z.object({
|
|
36
|
+
gap: z.number().optional(),
|
|
37
|
+
padding: z.number().optional(),
|
|
38
|
+
paddingHorizontal: z.number().optional(),
|
|
39
|
+
paddingVertical: z.number().optional(),
|
|
40
|
+
margin: z.number().optional(),
|
|
41
|
+
marginHorizontal: z.number().optional(),
|
|
42
|
+
marginVertical: z.number().optional(),
|
|
43
|
+
flex: z.number().optional(),
|
|
44
|
+
width: z.number().optional(),
|
|
45
|
+
height: z.number().optional(),
|
|
46
|
+
minWidth: z.number().optional(),
|
|
47
|
+
maxWidth: z.number().optional(),
|
|
48
|
+
minHeight: z.number().optional(),
|
|
49
|
+
maxHeight: z.number().optional(),
|
|
50
|
+
alignItems: z.enum(["flex-start", "center", "flex-end", "stretch"]).optional(),
|
|
51
|
+
alignSelf: z.enum(["auto", "flex-start", "flex-end", "center", "stretch", "baseline"]).optional(),
|
|
52
|
+
justifyContent: z.enum(["flex-start", "center", "flex-end", "space-between", "space-around"]).optional(),
|
|
53
|
+
backgroundColor: z.string().optional(),
|
|
54
|
+
flexWrap: z.enum(["wrap", "nowrap"]).optional(),
|
|
55
|
+
flexShrink: z.number().optional(),
|
|
56
|
+
borderWidth: z.number().optional(),
|
|
57
|
+
borderRadius: z.number().optional(),
|
|
58
|
+
borderColor: z.string().optional(),
|
|
59
|
+
overflow: z.enum(["hidden", "visible", "scroll"]).optional(),
|
|
60
|
+
opacity: z.number().min(0).max(1).optional(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
type StackUIElement = Extract<UIElement, { type: "YStack" | "XStack" }>;
|
|
64
|
+
|
|
65
|
+
type Props = {
|
|
66
|
+
element: StackUIElement;
|
|
67
|
+
ctx: RenderContext;
|
|
68
|
+
parentType?: "XStack" | "YStack";
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const StackElementComponent = ({ element, ctx, parentType }: Props): React.ReactElement => {
|
|
72
|
+
return (
|
|
73
|
+
<View
|
|
74
|
+
style={{
|
|
75
|
+
flexDirection: element.type === "XStack" ? "row" : "column",
|
|
76
|
+
alignSelf: element.props.alignSelf,
|
|
77
|
+
alignItems: element.props.alignItems,
|
|
78
|
+
gap: element.props.gap,
|
|
79
|
+
padding: element.props.padding,
|
|
80
|
+
paddingHorizontal: element.props.paddingHorizontal,
|
|
81
|
+
paddingVertical: element.props.paddingVertical,
|
|
82
|
+
margin: element.props.margin,
|
|
83
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
84
|
+
marginVertical: element.props.marginVertical,
|
|
85
|
+
flex: element.props.flex,
|
|
86
|
+
width: element.props.width,
|
|
87
|
+
height: element.props.height,
|
|
88
|
+
minWidth: element.props.minWidth,
|
|
89
|
+
maxWidth: element.props.maxWidth,
|
|
90
|
+
minHeight: element.props.minHeight,
|
|
91
|
+
maxHeight: element.props.maxHeight,
|
|
92
|
+
flexShrink: element.props.flexShrink ?? (parentType === "XStack" ? 1 : undefined),
|
|
93
|
+
flexWrap: element.props.flexWrap,
|
|
94
|
+
justifyContent: element.props.justifyContent,
|
|
95
|
+
backgroundColor: element.props.backgroundColor,
|
|
96
|
+
borderWidth: element.props.borderWidth,
|
|
97
|
+
borderRadius: element.props.borderRadius,
|
|
98
|
+
borderColor: element.props.borderColor,
|
|
99
|
+
overflow: element.props.overflow,
|
|
100
|
+
opacity: element.props.opacity,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{ctx.renderChildren(element.children, element.type)}
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
};
|