@rocapine/react-native-onboarding-ui 1.7.0 → 1.8.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/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 +30 -0
- package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.js +19 -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 +41 -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 +42 -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 +102 -0
- package/dist/UI/Pages/ComposableScreen/elements/InputElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/InputElement.js +68 -0
- package/dist/UI/Pages/ComposableScreen/elements/InputElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/LottieElement.d.ts +39 -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 +78 -0
- package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js +119 -0
- package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/RiveElement.d.ts +62 -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 +85 -0
- package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts.map +1 -0
- package/dist/UI/Pages/ComposableScreen/elements/StackElement.js +64 -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 +41 -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 +31 -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 +115 -0
- package/src/UI/Pages/ComposableScreen/elements/LottieElement.tsx +97 -0
- package/src/UI/Pages/ComposableScreen/elements/RadioGroupElement.tsx +181 -0
- package/src/UI/Pages/ComposableScreen/elements/RiveElement.tsx +105 -0
- package/src/UI/Pages/ComposableScreen/elements/StackElement.tsx +103 -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,96 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { 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 ButtonElementProps = BaseBoxProps & {
|
|
9
|
+
label: string;
|
|
10
|
+
action?: "continue";
|
|
11
|
+
variant?: "filled" | "outlined" | "ghost";
|
|
12
|
+
backgroundColor?: string;
|
|
13
|
+
color?: string;
|
|
14
|
+
fontSize?: number;
|
|
15
|
+
fontWeight?: string;
|
|
16
|
+
fontFamily?: string;
|
|
17
|
+
textAlign?: "left" | "center" | "right";
|
|
18
|
+
alignSelf?: "auto" | "flex-start" | "center" | "flex-end" | "stretch";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const ButtonElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
22
|
+
label: z.string().min(1, "label must not be empty"),
|
|
23
|
+
action: z.enum(["continue"]).optional(),
|
|
24
|
+
variant: z.enum(["filled", "outlined", "ghost"]).optional(),
|
|
25
|
+
backgroundColor: z.string().optional(),
|
|
26
|
+
color: z.string().optional(),
|
|
27
|
+
fontSize: z.number().optional(),
|
|
28
|
+
fontWeight: z.string().optional(),
|
|
29
|
+
fontFamily: z.string().optional(),
|
|
30
|
+
textAlign: z.enum(["left", "center", "right"]).optional(),
|
|
31
|
+
alignSelf: z.enum(["auto", "flex-start", "center", "flex-end", "stretch"]).optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
type ButtonUIElement = Extract<UIElement, { type: "Button" }>;
|
|
35
|
+
|
|
36
|
+
type Props = {
|
|
37
|
+
element: ButtonUIElement;
|
|
38
|
+
ctx: RenderContext;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const ButtonElementComponent = ({ element, ctx }: Props): React.ReactElement => {
|
|
42
|
+
const { theme, onContinue } = ctx;
|
|
43
|
+
const action = element.props.action;
|
|
44
|
+
const handlePress = () => {
|
|
45
|
+
if (action === undefined || action === "continue") {
|
|
46
|
+
onContinue();
|
|
47
|
+
}
|
|
48
|
+
// other action values are no-ops
|
|
49
|
+
};
|
|
50
|
+
const variant = element.props.variant ?? "filled";
|
|
51
|
+
const isFilled = variant === "filled";
|
|
52
|
+
const isOutlined = variant === "outlined";
|
|
53
|
+
const bgColor = isFilled
|
|
54
|
+
? (element.props.backgroundColor ?? theme.colors.primary)
|
|
55
|
+
: "transparent";
|
|
56
|
+
const textColor = isFilled
|
|
57
|
+
? (element.props.color ?? theme.colors.text.opposite)
|
|
58
|
+
: (element.props.color ?? theme.colors.primary);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<TouchableOpacity
|
|
62
|
+
activeOpacity={0.8}
|
|
63
|
+
onPress={handlePress}
|
|
64
|
+
style={{
|
|
65
|
+
backgroundColor: bgColor,
|
|
66
|
+
borderRadius: element.props.borderRadius ?? 90,
|
|
67
|
+
borderWidth: isOutlined ? (element.props.borderWidth ?? 1) : (element.props.borderWidth ?? 0),
|
|
68
|
+
borderColor: isOutlined ? (element.props.borderColor ?? theme.colors.primary) : element.props.borderColor,
|
|
69
|
+
padding: element.props.padding,
|
|
70
|
+
paddingVertical: element.props.paddingVertical ?? 14,
|
|
71
|
+
paddingHorizontal: element.props.paddingHorizontal ?? 24,
|
|
72
|
+
width: element.props.width,
|
|
73
|
+
height: element.props.height,
|
|
74
|
+
margin: element.props.margin,
|
|
75
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
76
|
+
marginVertical: element.props.marginVertical,
|
|
77
|
+
opacity: element.props.opacity,
|
|
78
|
+
alignSelf: element.props.alignSelf ?? (element.props.width ? undefined : "stretch"),
|
|
79
|
+
alignItems: "center",
|
|
80
|
+
justifyContent: "center",
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Text
|
|
84
|
+
style={{
|
|
85
|
+
color: textColor,
|
|
86
|
+
fontSize: element.props.fontSize ?? theme.typography.textStyles.button.fontSize,
|
|
87
|
+
fontWeight: (element.props.fontWeight as any) ?? theme.typography.textStyles.button.fontWeight,
|
|
88
|
+
fontFamily: element.props.fontFamily,
|
|
89
|
+
textAlign: element.props.textAlign ?? "center",
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{element.props.label}
|
|
93
|
+
</Text>
|
|
94
|
+
</TouchableOpacity>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
5
|
+
import { UIElement } from "../types";
|
|
6
|
+
import { RenderContext } from "./shared";
|
|
7
|
+
|
|
8
|
+
export type IconElementProps = BaseBoxProps & {
|
|
9
|
+
name: string;
|
|
10
|
+
size?: number;
|
|
11
|
+
color?: string;
|
|
12
|
+
strokeWidth?: number;
|
|
13
|
+
backgroundColor?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const IconElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
17
|
+
name: z.string().min(1, "icon name must not be empty"),
|
|
18
|
+
size: z.number().nonnegative().optional(),
|
|
19
|
+
color: z.string().optional(),
|
|
20
|
+
strokeWidth: z.number().nonnegative().optional(),
|
|
21
|
+
backgroundColor: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type IconUIElement = Extract<UIElement, { type: "Icon" }>;
|
|
25
|
+
|
|
26
|
+
type Props = {
|
|
27
|
+
element: IconUIElement;
|
|
28
|
+
ctx: RenderContext;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const IconElementComponent = ({ element, ctx }: Props): React.ReactElement => {
|
|
32
|
+
const { theme } = ctx;
|
|
33
|
+
const icons = require("lucide-react-native");
|
|
34
|
+
const IconComp = icons[element.props.name] as React.ComponentType<{
|
|
35
|
+
size?: number;
|
|
36
|
+
color?: string;
|
|
37
|
+
strokeWidth?: number;
|
|
38
|
+
}> | undefined;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View
|
|
42
|
+
style={{
|
|
43
|
+
width: element.props.width,
|
|
44
|
+
height: element.props.height,
|
|
45
|
+
margin: element.props.margin,
|
|
46
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
47
|
+
marginVertical: element.props.marginVertical,
|
|
48
|
+
padding: element.props.padding,
|
|
49
|
+
paddingHorizontal: element.props.paddingHorizontal,
|
|
50
|
+
paddingVertical: element.props.paddingVertical,
|
|
51
|
+
borderWidth: element.props.borderWidth,
|
|
52
|
+
borderRadius: element.props.borderRadius,
|
|
53
|
+
borderColor: element.props.borderColor,
|
|
54
|
+
backgroundColor: element.props.backgroundColor,
|
|
55
|
+
opacity: element.props.opacity,
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{IconComp ? (
|
|
59
|
+
<IconComp
|
|
60
|
+
size={element.props.size ?? 24}
|
|
61
|
+
color={element.props.color ?? theme.colors.text.primary}
|
|
62
|
+
strokeWidth={element.props.strokeWidth ?? 2}
|
|
63
|
+
/>
|
|
64
|
+
) : null}
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -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,115 @@
|
|
|
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
|
+
<View
|
|
75
|
+
style={{
|
|
76
|
+
backgroundColor: element.props.backgroundColor ?? theme.colors.neutral.lowest,
|
|
77
|
+
borderWidth: element.props.borderWidth ?? 1,
|
|
78
|
+
borderRadius: element.props.borderRadius ?? 8,
|
|
79
|
+
borderColor: element.props.borderColor ?? theme.colors.neutral.low,
|
|
80
|
+
width: element.props.width,
|
|
81
|
+
height: element.props.height,
|
|
82
|
+
opacity: element.props.opacity,
|
|
83
|
+
margin: element.props.margin,
|
|
84
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
85
|
+
marginVertical: element.props.marginVertical,
|
|
86
|
+
overflow: "hidden",
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<TextInput
|
|
90
|
+
value={value}
|
|
91
|
+
onChangeText={handleChange}
|
|
92
|
+
placeholder={element.props.placeholder}
|
|
93
|
+
placeholderTextColor={element.props.placeholderColor ?? theme.colors.text.tertiary}
|
|
94
|
+
keyboardType={element.props.keyboardType ?? "default"}
|
|
95
|
+
returnKeyType={element.props.returnKeyType ?? "done"}
|
|
96
|
+
autoCapitalize={element.props.autoCapitalize ?? "sentences"}
|
|
97
|
+
secureTextEntry={element.props.secureTextEntry ?? false}
|
|
98
|
+
maxLength={element.props.maxLength}
|
|
99
|
+
multiline={element.props.multiline ?? false}
|
|
100
|
+
numberOfLines={element.props.numberOfLines}
|
|
101
|
+
editable={element.props.editable ?? true}
|
|
102
|
+
style={{
|
|
103
|
+
flex: 1,
|
|
104
|
+
color: element.props.color ?? theme.colors.text.primary,
|
|
105
|
+
fontSize: element.props.fontSize ?? theme.typography.textStyles.body.fontSize,
|
|
106
|
+
fontWeight: element.props.fontWeight as any,
|
|
107
|
+
textAlign: element.props.textAlign,
|
|
108
|
+
padding: element.props.padding ?? 12,
|
|
109
|
+
paddingHorizontal: element.props.paddingHorizontal,
|
|
110
|
+
paddingVertical: element.props.paddingVertical,
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
</View>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
@@ -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,181 @@
|
|
|
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
|
+
gap: element.props.gap ?? 8,
|
|
94
|
+
width: element.props.width,
|
|
95
|
+
height: element.props.height,
|
|
96
|
+
margin: element.props.margin,
|
|
97
|
+
marginHorizontal: element.props.marginHorizontal,
|
|
98
|
+
marginVertical: element.props.marginVertical,
|
|
99
|
+
padding: element.props.padding,
|
|
100
|
+
paddingHorizontal: element.props.paddingHorizontal,
|
|
101
|
+
paddingVertical: element.props.paddingVertical,
|
|
102
|
+
borderWidth: element.props.borderWidth,
|
|
103
|
+
borderRadius: element.props.borderRadius,
|
|
104
|
+
borderColor: element.props.borderColor,
|
|
105
|
+
opacity: element.props.opacity,
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{element.props.items.map((item) => {
|
|
109
|
+
const isSelected = selectedValue === item.value;
|
|
110
|
+
// Note: "+ "15"" appends hex alpha (≈8% opacity) assuming a 6-digit hex primary color.
|
|
111
|
+
// This works for standard hex colors but may produce unexpected results for other color formats.
|
|
112
|
+
const bgColor = isSelected
|
|
113
|
+
? (element.props.itemSelectedBackgroundColor ?? theme.colors.primary + "15")
|
|
114
|
+
: (element.props.itemBackgroundColor ?? "transparent");
|
|
115
|
+
const textColor = isSelected
|
|
116
|
+
? (element.props.itemSelectedColor ?? theme.colors.primary)
|
|
117
|
+
: (element.props.itemColor ?? theme.colors.text.primary);
|
|
118
|
+
const borderColor = isSelected
|
|
119
|
+
? (element.props.itemSelectedBorderColor ?? theme.colors.primary)
|
|
120
|
+
: (element.props.itemBorderColor ?? theme.colors.neutral.low);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<TouchableOpacity
|
|
124
|
+
key={item.value}
|
|
125
|
+
activeOpacity={0.7}
|
|
126
|
+
onPress={() => handleSelect(item.value, item.label)}
|
|
127
|
+
accessibilityRole="radio"
|
|
128
|
+
accessibilityState={{ selected: isSelected, checked: isSelected }}
|
|
129
|
+
accessibilityLabel={item.label}
|
|
130
|
+
style={{
|
|
131
|
+
flexDirection: "row",
|
|
132
|
+
alignItems: "center",
|
|
133
|
+
gap: 12,
|
|
134
|
+
backgroundColor: bgColor,
|
|
135
|
+
borderRadius: element.props.itemBorderRadius ?? 8,
|
|
136
|
+
borderWidth: element.props.itemBorderWidth ?? 1,
|
|
137
|
+
borderColor: borderColor,
|
|
138
|
+
padding: element.props.itemPadding ?? (element.props.itemPaddingHorizontal === undefined && element.props.itemPaddingVertical === undefined ? 12 : undefined),
|
|
139
|
+
paddingHorizontal: element.props.itemPaddingHorizontal,
|
|
140
|
+
paddingVertical: element.props.itemPaddingVertical,
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<View
|
|
144
|
+
style={{
|
|
145
|
+
width: 20,
|
|
146
|
+
height: 20,
|
|
147
|
+
borderRadius: 10,
|
|
148
|
+
borderWidth: 2,
|
|
149
|
+
borderColor: isSelected ? theme.colors.primary : theme.colors.neutral.medium,
|
|
150
|
+
alignItems: "center",
|
|
151
|
+
justifyContent: "center",
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
{isSelected && (
|
|
155
|
+
<View
|
|
156
|
+
style={{
|
|
157
|
+
width: 10,
|
|
158
|
+
height: 10,
|
|
159
|
+
borderRadius: 5,
|
|
160
|
+
backgroundColor: theme.colors.primary,
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
</View>
|
|
165
|
+
<Text
|
|
166
|
+
style={{
|
|
167
|
+
flex: 1,
|
|
168
|
+
color: textColor,
|
|
169
|
+
fontSize: element.props.itemFontSize ?? theme.typography.textStyles.body.fontSize,
|
|
170
|
+
fontWeight: (element.props.itemFontWeight as any) ?? theme.typography.textStyles.body.fontWeight,
|
|
171
|
+
fontFamily: element.props.itemFontFamily,
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{item.label}
|
|
175
|
+
</Text>
|
|
176
|
+
</TouchableOpacity>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
</View>
|
|
180
|
+
);
|
|
181
|
+
};
|