@rocapine/react-native-onboarding-ui 1.6.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.
Files changed (84) hide show
  1. package/dist/UI/OnboardingPage.js +1 -1
  2. package/dist/UI/OnboardingPage.js.map +1 -1
  3. package/dist/UI/Pages/ComposableScreen/Renderer.d.ts +0 -2
  4. package/dist/UI/Pages/ComposableScreen/Renderer.d.ts.map +1 -1
  5. package/dist/UI/Pages/ComposableScreen/Renderer.js +13 -273
  6. package/dist/UI/Pages/ComposableScreen/Renderer.js.map +1 -1
  7. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.d.ts +30 -0
  8. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.d.ts.map +1 -0
  9. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.js +19 -0
  10. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.js.map +1 -0
  11. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts +67 -0
  12. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts.map +1 -0
  13. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js +65 -0
  14. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js.map +1 -0
  15. package/dist/UI/Pages/ComposableScreen/elements/IconElement.d.ts +41 -0
  16. package/dist/UI/Pages/ComposableScreen/elements/IconElement.d.ts.map +1 -0
  17. package/dist/UI/Pages/ComposableScreen/elements/IconElement.js +37 -0
  18. package/dist/UI/Pages/ComposableScreen/elements/IconElement.js.map +1 -0
  19. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts +42 -0
  20. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts.map +1 -0
  21. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js +34 -0
  22. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.js.map +1 -0
  23. package/dist/UI/Pages/ComposableScreen/elements/InputElement.d.ts +102 -0
  24. package/dist/UI/Pages/ComposableScreen/elements/InputElement.d.ts.map +1 -0
  25. package/dist/UI/Pages/ComposableScreen/elements/InputElement.js +68 -0
  26. package/dist/UI/Pages/ComposableScreen/elements/InputElement.js.map +1 -0
  27. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.d.ts +39 -0
  28. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.d.ts.map +1 -0
  29. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.js +60 -0
  30. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.js.map +1 -0
  31. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts +78 -0
  32. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts.map +1 -0
  33. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js +119 -0
  34. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.js.map +1 -0
  35. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.d.ts +62 -0
  36. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.d.ts.map +1 -0
  37. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.js +68 -0
  38. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.js.map +1 -0
  39. package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts +85 -0
  40. package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts.map +1 -0
  41. package/dist/UI/Pages/ComposableScreen/elements/StackElement.js +64 -0
  42. package/dist/UI/Pages/ComposableScreen/elements/StackElement.js.map +1 -0
  43. package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts +66 -0
  44. package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts.map +1 -0
  45. package/dist/UI/Pages/ComposableScreen/elements/TextElement.js +59 -0
  46. package/dist/UI/Pages/ComposableScreen/elements/TextElement.js.map +1 -0
  47. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.d.ts +41 -0
  48. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.d.ts.map +1 -0
  49. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.js +84 -0
  50. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.js.map +1 -0
  51. package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts +5 -0
  52. package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts.map +1 -0
  53. package/dist/UI/Pages/ComposableScreen/elements/renderElement.js +49 -0
  54. package/dist/UI/Pages/ComposableScreen/elements/renderElement.js.map +1 -0
  55. package/dist/UI/Pages/ComposableScreen/elements/shared.d.ts +13 -0
  56. package/dist/UI/Pages/ComposableScreen/elements/shared.d.ts.map +1 -0
  57. package/dist/UI/Pages/ComposableScreen/elements/shared.js +6 -0
  58. package/dist/UI/Pages/ComposableScreen/elements/shared.js.map +1 -0
  59. package/dist/UI/Pages/ComposableScreen/types.d.ts +40 -113
  60. package/dist/UI/Pages/ComposableScreen/types.d.ts.map +1 -1
  61. package/dist/UI/Pages/ComposableScreen/types.js +33 -121
  62. package/dist/UI/Pages/ComposableScreen/types.js.map +1 -1
  63. package/dist/UI/Provider/OnboardingProgressProvider.d.ts +6 -2
  64. package/dist/UI/Provider/OnboardingProgressProvider.d.ts.map +1 -1
  65. package/dist/UI/Provider/OnboardingProgressProvider.js +4 -3
  66. package/dist/UI/Provider/OnboardingProgressProvider.js.map +1 -1
  67. package/package.json +2 -2
  68. package/src/UI/OnboardingPage.tsx +1 -1
  69. package/src/UI/Pages/ComposableScreen/Renderer.tsx +22 -430
  70. package/src/UI/Pages/ComposableScreen/elements/BaseBoxProps.ts +31 -0
  71. package/src/UI/Pages/ComposableScreen/elements/ButtonElement.tsx +96 -0
  72. package/src/UI/Pages/ComposableScreen/elements/IconElement.tsx +67 -0
  73. package/src/UI/Pages/ComposableScreen/elements/ImageElement.tsx +52 -0
  74. package/src/UI/Pages/ComposableScreen/elements/InputElement.tsx +115 -0
  75. package/src/UI/Pages/ComposableScreen/elements/LottieElement.tsx +97 -0
  76. package/src/UI/Pages/ComposableScreen/elements/RadioGroupElement.tsx +181 -0
  77. package/src/UI/Pages/ComposableScreen/elements/RiveElement.tsx +105 -0
  78. package/src/UI/Pages/ComposableScreen/elements/StackElement.tsx +103 -0
  79. package/src/UI/Pages/ComposableScreen/elements/TextElement.tsx +95 -0
  80. package/src/UI/Pages/ComposableScreen/elements/VideoElement.tsx +113 -0
  81. package/src/UI/Pages/ComposableScreen/elements/renderElement.tsx +61 -0
  82. package/src/UI/Pages/ComposableScreen/elements/shared.ts +15 -0
  83. package/src/UI/Pages/ComposableScreen/types.ts +56 -233
  84. 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
+ };