flowboard-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/FlowboardReact.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +122 -0
  4. package/android/build.gradle +67 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/flowboardreact/FlowboardReactModule.kt +15 -0
  7. package/android/src/main/java/com/flowboardreact/FlowboardReactPackage.kt +33 -0
  8. package/ios/FlowboardReact.h +5 -0
  9. package/ios/FlowboardReact.mm +21 -0
  10. package/lib/module/Flowboard.js +167 -0
  11. package/lib/module/Flowboard.js.map +1 -0
  12. package/lib/module/FlowboardProvider.js +52 -0
  13. package/lib/module/FlowboardProvider.js.map +1 -0
  14. package/lib/module/NativeFlowboardReact.js +5 -0
  15. package/lib/module/NativeFlowboardReact.js.map +1 -0
  16. package/lib/module/components/FlowboardFlow.js +389 -0
  17. package/lib/module/components/FlowboardFlow.js.map +1 -0
  18. package/lib/module/components/FlowboardRenderer.js +1684 -0
  19. package/lib/module/components/FlowboardRenderer.js.map +1 -0
  20. package/lib/module/components/widgets/sliderRegistry.js +48 -0
  21. package/lib/module/components/widgets/sliderRegistry.js.map +1 -0
  22. package/lib/module/core/analyticsManager.js +110 -0
  23. package/lib/module/core/analyticsManager.js.map +1 -0
  24. package/lib/module/core/assetPreloader.js +72 -0
  25. package/lib/module/core/assetPreloader.js.map +1 -0
  26. package/lib/module/core/clientContext.js +105 -0
  27. package/lib/module/core/clientContext.js.map +1 -0
  28. package/lib/module/core/fontAwesome.js +110 -0
  29. package/lib/module/core/fontAwesome.js.map +1 -0
  30. package/lib/module/core/onboardingRepository.js +62 -0
  31. package/lib/module/core/onboardingRepository.js.map +1 -0
  32. package/lib/module/core/resolverService.js +58 -0
  33. package/lib/module/core/resolverService.js.map +1 -0
  34. package/lib/module/index.js +5 -0
  35. package/lib/module/index.js.map +1 -0
  36. package/lib/module/package.json +1 -0
  37. package/lib/module/types/flowboard.js +4 -0
  38. package/lib/module/types/flowboard.js.map +1 -0
  39. package/lib/module/types/react-native-vector-icons.d.js +2 -0
  40. package/lib/module/types/react-native-vector-icons.d.js.map +1 -0
  41. package/lib/module/utils/flowboardUtils.js +379 -0
  42. package/lib/module/utils/flowboardUtils.js.map +1 -0
  43. package/lib/typescript/package.json +1 -0
  44. package/lib/typescript/src/Flowboard.d.ts +33 -0
  45. package/lib/typescript/src/Flowboard.d.ts.map +1 -0
  46. package/lib/typescript/src/FlowboardProvider.d.ts +5 -0
  47. package/lib/typescript/src/FlowboardProvider.d.ts.map +1 -0
  48. package/lib/typescript/src/NativeFlowboardReact.d.ts +7 -0
  49. package/lib/typescript/src/NativeFlowboardReact.d.ts.map +1 -0
  50. package/lib/typescript/src/components/FlowboardFlow.d.ts +14 -0
  51. package/lib/typescript/src/components/FlowboardFlow.d.ts.map +1 -0
  52. package/lib/typescript/src/components/FlowboardRenderer.d.ts +31 -0
  53. package/lib/typescript/src/components/FlowboardRenderer.d.ts.map +1 -0
  54. package/lib/typescript/src/components/widgets/sliderRegistry.d.ts +16 -0
  55. package/lib/typescript/src/components/widgets/sliderRegistry.d.ts.map +1 -0
  56. package/lib/typescript/src/core/analyticsManager.d.ts +42 -0
  57. package/lib/typescript/src/core/analyticsManager.d.ts.map +1 -0
  58. package/lib/typescript/src/core/assetPreloader.d.ts +8 -0
  59. package/lib/typescript/src/core/assetPreloader.d.ts.map +1 -0
  60. package/lib/typescript/src/core/clientContext.d.ts +27 -0
  61. package/lib/typescript/src/core/clientContext.d.ts.map +1 -0
  62. package/lib/typescript/src/core/fontAwesome.d.ts +8 -0
  63. package/lib/typescript/src/core/fontAwesome.d.ts.map +1 -0
  64. package/lib/typescript/src/core/onboardingRepository.d.ts +15 -0
  65. package/lib/typescript/src/core/onboardingRepository.d.ts.map +1 -0
  66. package/lib/typescript/src/core/resolverService.d.ts +11 -0
  67. package/lib/typescript/src/core/resolverService.d.ts.map +1 -0
  68. package/lib/typescript/src/index.d.ts +4 -0
  69. package/lib/typescript/src/index.d.ts.map +1 -0
  70. package/lib/typescript/src/types/flowboard.d.ts +34 -0
  71. package/lib/typescript/src/types/flowboard.d.ts.map +1 -0
  72. package/lib/typescript/src/utils/flowboardUtils.d.ts +31 -0
  73. package/lib/typescript/src/utils/flowboardUtils.d.ts.map +1 -0
  74. package/package.json +192 -0
  75. package/src/Flowboard.ts +223 -0
  76. package/src/FlowboardProvider.tsx +60 -0
  77. package/src/NativeFlowboardReact.ts +7 -0
  78. package/src/components/FlowboardFlow.tsx +513 -0
  79. package/src/components/FlowboardRenderer.tsx +1957 -0
  80. package/src/components/widgets/sliderRegistry.tsx +56 -0
  81. package/src/core/analyticsManager.ts +125 -0
  82. package/src/core/assetPreloader.ts +103 -0
  83. package/src/core/clientContext.ts +132 -0
  84. package/src/core/fontAwesome.ts +90 -0
  85. package/src/core/onboardingRepository.ts +79 -0
  86. package/src/core/resolverService.ts +69 -0
  87. package/src/index.tsx +11 -0
  88. package/src/types/flowboard.ts +50 -0
  89. package/src/types/react-native-vector-icons.d.ts +15 -0
  90. package/src/utils/flowboardUtils.ts +400 -0
@@ -0,0 +1,1957 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ Animated,
4
+ Pressable,
5
+ ScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ TextInput,
9
+ View,
10
+ Image,
11
+ type TextStyle,
12
+ } from 'react-native';
13
+ import { SafeAreaView } from 'react-native-safe-area-context';
14
+ import MaskedView from '@react-native-masked-view/masked-view';
15
+ import LinearGradient from 'react-native-linear-gradient';
16
+ import LottieView from 'lottie-react-native';
17
+ import MaskInput from 'react-native-mask-input';
18
+ import Svg, {
19
+ Circle,
20
+ G,
21
+ Line,
22
+ Polygon,
23
+ Text as SvgText,
24
+ } from 'react-native-svg';
25
+ import PagerView, {
26
+ type PagerViewOnPageSelectedEvent,
27
+ } from 'react-native-pager-view';
28
+ import {
29
+ insetsToStyle,
30
+ parseAlignment,
31
+ parseColor,
32
+ parseCrossAlignment,
33
+ parseDimension,
34
+ parseFlexAlignment,
35
+ parseFontWeight,
36
+ parseInsets,
37
+ parseResizeMode,
38
+ parseTextAlign,
39
+ parseTextDecoration,
40
+ resolveNumericValue,
41
+ resolveText,
42
+ } from '../utils/flowboardUtils';
43
+ import { resolveFontAwesomeIcon, FontAwesome6 } from '../core/fontAwesome';
44
+ import { useSliderRegistry } from './widgets/sliderRegistry';
45
+
46
+ const styles = StyleSheet.create({
47
+ root: { flex: 1 },
48
+ background: {
49
+ ...StyleSheet.absoluteFillObject,
50
+ },
51
+ safeArea: { flex: 1 },
52
+ progressWrapper: { width: '100%' },
53
+ });
54
+
55
+ type FlowboardRendererProps = {
56
+ screenData: Record<string, any>;
57
+ onAction: (action: string, data?: Record<string, any>) => void;
58
+ formData?: Record<string, any>;
59
+ onInputChange?: (id: string, value: any) => void;
60
+ enableFontAwesomeIcons?: boolean;
61
+ currentIndex?: number;
62
+ totalScreens?: number;
63
+ };
64
+
65
+ export default function FlowboardRenderer(props: FlowboardRendererProps) {
66
+ const {
67
+ screenData,
68
+ onAction,
69
+ formData = {},
70
+ onInputChange,
71
+ enableFontAwesomeIcons = true,
72
+ currentIndex = 0,
73
+ totalScreens = 1,
74
+ } = props;
75
+
76
+ const childrenData: any[] = Array.isArray(screenData.children)
77
+ ? screenData.children
78
+ : [];
79
+
80
+ const backgroundData = screenData.background;
81
+ const bgColorCode = screenData.backgroundColor;
82
+
83
+ const showProgress = screenData.showProgress === true;
84
+ const progressColor = parseColor(screenData.progressColor ?? '0xFF000000');
85
+ const progressThickness = Number(screenData.progressThickness ?? 4);
86
+ const progressRadius = Number(screenData.progressRadius ?? 0);
87
+ const progressStyle = screenData.progressStyle ?? 'linear';
88
+ const scrollable = screenData.scrollable === true;
89
+ const safeArea = screenData.safeArea !== false;
90
+
91
+ const padding = parseInsets(screenData.padding);
92
+ const paddingStyle = insetsToStyle(padding);
93
+
94
+ const content = (
95
+ <View style={{ flex: 1 }}>
96
+ {showProgress && (
97
+ <View style={[paddingStyle, { paddingBottom: 8 }]}>
98
+ {renderProgressBar(
99
+ currentIndex,
100
+ totalScreens,
101
+ progressColor,
102
+ progressThickness,
103
+ progressRadius,
104
+ progressStyle
105
+ )}
106
+ </View>
107
+ )}
108
+
109
+ {scrollable ? (
110
+ <ScrollView
111
+ contentContainerStyle={{
112
+ ...paddingStyle,
113
+ paddingTop: showProgress ? 0 : padding.top,
114
+ }}
115
+ >
116
+ <View
117
+ style={{
118
+ flexGrow: 1,
119
+ justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
120
+ alignItems: parseCrossAlignment(screenData.crossAxisAlignment),
121
+ }}
122
+ >
123
+ {childrenData.map((child, index) =>
124
+ buildWidget(child, {
125
+ onAction,
126
+ formData,
127
+ onInputChange,
128
+ allowFlexExpansion: true,
129
+ screenData,
130
+ enableFontAwesomeIcons,
131
+ key: `child-${index}`,
132
+ })
133
+ )}
134
+ </View>
135
+ </ScrollView>
136
+ ) : (
137
+ <View
138
+ style={{
139
+ flex: 1,
140
+ ...paddingStyle,
141
+ paddingTop: showProgress ? 0 : padding.top,
142
+ justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
143
+ alignItems: parseCrossAlignment(screenData.crossAxisAlignment),
144
+ }}
145
+ >
146
+ {childrenData.map((child, index) =>
147
+ buildWidget(child, {
148
+ onAction,
149
+ formData,
150
+ onInputChange,
151
+ allowFlexExpansion: true,
152
+ screenData,
153
+ enableFontAwesomeIcons,
154
+ key: `child-${index}`,
155
+ })
156
+ )}
157
+ </View>
158
+ )}
159
+ </View>
160
+ );
161
+
162
+ return (
163
+ <View style={styles.root}>
164
+ {renderBackground(backgroundData, bgColorCode)}
165
+ {safeArea ? (
166
+ <SafeAreaView style={styles.safeArea}>{content}</SafeAreaView>
167
+ ) : (
168
+ content
169
+ )}
170
+ </View>
171
+ );
172
+ }
173
+
174
+ function renderBackground(bgData: any, legacyColorCode?: string) {
175
+ if (!bgData) {
176
+ return (
177
+ <View
178
+ style={[
179
+ styles.background,
180
+ { backgroundColor: parseColor(legacyColorCode) },
181
+ ]}
182
+ />
183
+ );
184
+ }
185
+
186
+ if (typeof bgData === 'object') {
187
+ const type = bgData.type;
188
+ const fit = parseResizeMode(bgData.fit ?? 'cover');
189
+
190
+ switch (type) {
191
+ case 'gradient': {
192
+ const colors = Array.isArray(bgData.colors)
193
+ ? bgData.colors.map((c: string) => parseColor(c))
194
+ : [];
195
+ if (colors.length === 0) return null;
196
+ const start = alignmentToGradient(bgData.begin ?? 'topLeft');
197
+ const end = alignmentToGradient(bgData.end ?? 'bottomRight');
198
+ const stops = Array.isArray(bgData.stops)
199
+ ? bgData.stops.map((s: number) => Number(s))
200
+ : undefined;
201
+ return (
202
+ <LinearGradient
203
+ colors={colors}
204
+ start={start}
205
+ end={end}
206
+ locations={stops}
207
+ style={styles.background}
208
+ />
209
+ );
210
+ }
211
+ case 'image': {
212
+ const url = bgData.url;
213
+ if (!url) return null;
214
+ return (
215
+ <Image
216
+ source={{ uri: url }}
217
+ style={styles.background}
218
+ resizeMode={fit}
219
+ />
220
+ );
221
+ }
222
+ case 'asset': {
223
+ const path = bgData.path;
224
+ if (!path) return null;
225
+ return (
226
+ <Image
227
+ source={{ uri: path }}
228
+ style={styles.background}
229
+ resizeMode={fit}
230
+ />
231
+ );
232
+ }
233
+ case 'color': {
234
+ return (
235
+ <View
236
+ style={[
237
+ styles.background,
238
+ { backgroundColor: parseColor(bgData.color) },
239
+ ]}
240
+ />
241
+ );
242
+ }
243
+ default:
244
+ break;
245
+ }
246
+ }
247
+
248
+ return <View style={[styles.background, { backgroundColor: '#ffffff' }]} />;
249
+ }
250
+
251
+ function renderProgressBar(
252
+ currentIndex: number,
253
+ totalScreens: number,
254
+ color: string,
255
+ thickness: number,
256
+ radius: number,
257
+ style: string
258
+ ) {
259
+ if (style === 'dotted') {
260
+ return (
261
+ <View style={{ flexDirection: 'row' }}>
262
+ {Array.from({ length: totalScreens }).map((_, index) => {
263
+ const isActive = index <= currentIndex;
264
+ return (
265
+ <View
266
+ key={`dot-${index}`}
267
+ style={{
268
+ flex: 1,
269
+ height: thickness,
270
+ marginRight: index < totalScreens - 1 ? 4 : 0,
271
+ backgroundColor: isActive ? color : withOpacity(color, 0.1),
272
+ borderRadius: radius,
273
+ }}
274
+ />
275
+ );
276
+ })}
277
+ </View>
278
+ );
279
+ }
280
+
281
+ const progress = totalScreens > 0 ? (currentIndex + 1) / totalScreens : 0;
282
+ return (
283
+ <View
284
+ style={{
285
+ width: '100%',
286
+ height: thickness,
287
+ borderRadius: radius,
288
+ backgroundColor: withOpacity(color, 0.1),
289
+ overflow: 'hidden',
290
+ }}
291
+ >
292
+ <View
293
+ style={{
294
+ width: `${progress * 100}%`,
295
+ height: '100%',
296
+ backgroundColor: color,
297
+ borderRadius: radius,
298
+ }}
299
+ />
300
+ </View>
301
+ );
302
+ }
303
+
304
+ function buildWidget(
305
+ json: Record<string, any>,
306
+ params: {
307
+ onAction: (action: string, data?: Record<string, any>) => void;
308
+ formData: Record<string, any>;
309
+ onInputChange?: (id: string, value: any) => void;
310
+ allowFlexExpansion?: boolean;
311
+ screenData: Record<string, any>;
312
+ enableFontAwesomeIcons: boolean;
313
+ key?: string;
314
+ }
315
+ ): React.ReactNode {
316
+ if (!json || typeof json !== 'object') return null;
317
+ const type = json.type;
318
+ const id = json.id;
319
+ const props = { ...(json.properties ?? {}) } as Record<string, any>;
320
+ const childrenJson = Array.isArray(json.children) ? json.children : undefined;
321
+ const childJson = json.child as Record<string, any> | undefined;
322
+ const action = json.action as string | undefined;
323
+ const actionPayload = action ? buildActionPayload(props, json) : undefined;
324
+
325
+ let node: React.ReactNode = null;
326
+
327
+ switch (type) {
328
+ case 'column': {
329
+ const spacing = Number(props.spacing ?? props.gap ?? 0);
330
+ const mainAxisSize = props.mainAxisSize === 'min' ? 'min' : 'max';
331
+ const childWidgets = (childrenJson ?? []).map((child, index) => (
332
+ <React.Fragment key={`column-${index}`}>
333
+ {buildWidget(child, { ...params, allowFlexExpansion: true })}
334
+ {spacing > 0 && index < (childrenJson?.length ?? 0) - 1 ? (
335
+ <View style={{ height: spacing }} />
336
+ ) : null}
337
+ </React.Fragment>
338
+ ));
339
+ node = (
340
+ <View
341
+ style={{
342
+ flexDirection: 'column',
343
+ justifyContent: parseFlexAlignment(props.mainAxisAlignment),
344
+ alignItems: parseCrossAlignment(props.crossAxisAlignment),
345
+ flexGrow: mainAxisSize === 'max' ? 1 : 0,
346
+ }}
347
+ >
348
+ {childWidgets}
349
+ </View>
350
+ );
351
+ break;
352
+ }
353
+ case 'row': {
354
+ const spacing = Number(props.spacing ?? props.gap ?? 0);
355
+ const mainAxisSize = props.mainAxisSize === 'min' ? 'min' : 'max';
356
+ const childWidgets = (childrenJson ?? []).map((child, index) => (
357
+ <React.Fragment key={`row-${index}`}>
358
+ {buildWidget(child, { ...params, allowFlexExpansion: true })}
359
+ {spacing > 0 && index < (childrenJson?.length ?? 0) - 1 ? (
360
+ <View style={{ width: spacing }} />
361
+ ) : null}
362
+ </React.Fragment>
363
+ ));
364
+ node = (
365
+ <View
366
+ style={{
367
+ flexDirection: 'row',
368
+ justifyContent: parseFlexAlignment(props.mainAxisAlignment),
369
+ alignItems: parseCrossAlignment(props.crossAxisAlignment),
370
+ flexGrow: mainAxisSize === 'max' ? 1 : 0,
371
+ }}
372
+ >
373
+ {childWidgets}
374
+ </View>
375
+ );
376
+ break;
377
+ }
378
+ case 'container': {
379
+ const gradient = parseGradient(props.background);
380
+ const width = normalizeDimension(parseDimension(props.width));
381
+ const height = normalizeDimension(parseDimension(props.height));
382
+ const borderRadius = props.borderRadius
383
+ ? Number(props.borderRadius)
384
+ : undefined;
385
+ const borderColor = props.borderColor
386
+ ? parseColor(props.borderColor)
387
+ : undefined;
388
+ const borderWidth = borderColor ? 1 : 0;
389
+ const padding = parseInsets(props.padding);
390
+
391
+ const content = childJson
392
+ ? buildWidget(childJson, { ...params, allowFlexExpansion: false })
393
+ : null;
394
+
395
+ if (gradient) {
396
+ node = (
397
+ <LinearGradient
398
+ colors={gradient.colors}
399
+ start={gradient.start}
400
+ end={gradient.end}
401
+ locations={gradient.stops}
402
+ style={{
403
+ width,
404
+ height,
405
+ borderRadius,
406
+ borderColor,
407
+ borderWidth,
408
+ overflow: 'hidden',
409
+ ...insetsToStyle(padding),
410
+ }}
411
+ >
412
+ {content}
413
+ </LinearGradient>
414
+ );
415
+ } else {
416
+ node = (
417
+ <View
418
+ style={{
419
+ width,
420
+ height,
421
+ backgroundColor: parseColor(props.backgroundColor),
422
+ borderRadius,
423
+ borderColor,
424
+ borderWidth,
425
+ ...insetsToStyle(padding),
426
+ }}
427
+ >
428
+ {content}
429
+ </View>
430
+ );
431
+ }
432
+ break;
433
+ }
434
+ case 'text': {
435
+ const rawText = props.text ?? '';
436
+ const resolvedText = resolveText(String(rawText), params.formData);
437
+ const gradient = parseGradient(props.foreground);
438
+ const textStyle = getTextStyle(props, 'color');
439
+ const align = parseTextAlign(props.textAlign);
440
+ if (gradient) {
441
+ node = (
442
+ <GradientText
443
+ text={resolvedText}
444
+ gradient={gradient}
445
+ style={[textStyle, { textAlign: align }]}
446
+ />
447
+ );
448
+ } else {
449
+ node = (
450
+ <Text style={[textStyle, { textAlign: align }]}>{resolvedText}</Text>
451
+ );
452
+ }
453
+ break;
454
+ }
455
+ case 'rich_text': {
456
+ const spansList = Array.isArray(props.spans) ? props.spans : [];
457
+ const richTextGradient = parseGradient(props.foreground);
458
+ const align = parseTextAlign(props.textAlign);
459
+ if (richTextGradient) {
460
+ const combined = spansList
461
+ .map((span) => resolveText(String(span.text ?? ''), params.formData))
462
+ .join('');
463
+ node = (
464
+ <GradientText
465
+ text={combined}
466
+ gradient={richTextGradient}
467
+ style={{ textAlign: align }}
468
+ />
469
+ );
470
+ } else {
471
+ node = (
472
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
473
+ {spansList.map((span, index) => {
474
+ const spanText = resolveText(
475
+ String(span.text ?? ''),
476
+ params.formData
477
+ );
478
+ const spanStyle = getTextStyle(span, 'color');
479
+ const spanGradient = parseGradient(span.foreground);
480
+ if (spanGradient) {
481
+ return (
482
+ <GradientText
483
+ key={`span-${index}`}
484
+ text={spanText}
485
+ gradient={spanGradient}
486
+ style={[spanStyle, { textAlign: align }]}
487
+ />
488
+ );
489
+ }
490
+ return (
491
+ <Text
492
+ key={`span-${index}`}
493
+ style={[spanStyle, { textAlign: align }]}
494
+ >
495
+ {spanText}
496
+ </Text>
497
+ );
498
+ })}
499
+ </View>
500
+ );
501
+ }
502
+ break;
503
+ }
504
+ case 'button': {
505
+ const gradient = parseGradient(props.background);
506
+ const borderRadius = Number(props.borderRadius ?? 10);
507
+ const width = normalizeDimension(parseDimension(props.width)) ?? '100%';
508
+ const height = normalizeDimension(parseDimension(props.height)) ?? 50;
509
+ const label = props.label ?? 'Button';
510
+ const textStyle = getTextStyle(
511
+ { ...props, height: null },
512
+ 'textColor',
513
+ '0xFFFFFFFF'
514
+ );
515
+
516
+ const content = (
517
+ <View
518
+ style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
519
+ >
520
+ <Text style={textStyle}>{label}</Text>
521
+ </View>
522
+ );
523
+
524
+ if (gradient) {
525
+ node = (
526
+ <Pressable
527
+ onPress={
528
+ action
529
+ ? () => params.onAction(action, actionPayload ?? props)
530
+ : undefined
531
+ }
532
+ style={{ width, height }}
533
+ >
534
+ <LinearGradient
535
+ colors={gradient.colors}
536
+ start={gradient.start}
537
+ end={gradient.end}
538
+ locations={gradient.stops}
539
+ style={{ flex: 1, borderRadius, overflow: 'hidden' }}
540
+ >
541
+ {content}
542
+ </LinearGradient>
543
+ </Pressable>
544
+ );
545
+ } else {
546
+ node = (
547
+ <Pressable
548
+ onPress={
549
+ action
550
+ ? () => params.onAction(action, actionPayload ?? props)
551
+ : undefined
552
+ }
553
+ style={{
554
+ width,
555
+ height,
556
+ backgroundColor: parseColor(props.color ?? '0xFF2196F3'),
557
+ borderRadius,
558
+ justifyContent: 'center',
559
+ alignItems: 'center',
560
+ }}
561
+ >
562
+ {content}
563
+ </Pressable>
564
+ );
565
+ }
566
+ break;
567
+ }
568
+ case 'text_input': {
569
+ node = (
570
+ <DynamicInput
571
+ initialValue={params.formData[id ?? ''] ?? ''}
572
+ properties={props}
573
+ onChanged={(value) => {
574
+ if (params.onInputChange && id) {
575
+ params.onInputChange(id, value);
576
+ }
577
+ }}
578
+ />
579
+ );
580
+ break;
581
+ }
582
+ case 'spacer': {
583
+ node = (
584
+ <View
585
+ style={{
586
+ height: normalizeDimension(parseDimension(props.height)),
587
+ width: normalizeDimension(parseDimension(props.width)),
588
+ }}
589
+ />
590
+ );
591
+ break;
592
+ }
593
+ case 'expanded': {
594
+ const expandedChild = childJson
595
+ ? buildWidget(childJson, { ...params, allowFlexExpansion: true })
596
+ : null;
597
+ if (params.allowFlexExpansion) {
598
+ return <View style={{ flex: 1 }}>{expandedChild}</View>;
599
+ }
600
+ return expandedChild;
601
+ }
602
+ case 'padding': {
603
+ const padding = parseInsets(props.padding ?? 0);
604
+ node = (
605
+ <View style={insetsToStyle(padding)}>
606
+ {childJson ? buildWidget(childJson, { ...params }) : null}
607
+ </View>
608
+ );
609
+ break;
610
+ }
611
+ case 'align': {
612
+ const alignment = parseAlignment(props.alignment ?? 'center');
613
+ node = (
614
+ <View
615
+ style={{
616
+ justifyContent: alignment.justifyContent,
617
+ alignItems: alignment.alignItems,
618
+ }}
619
+ >
620
+ {childJson ? buildWidget(childJson, { ...params }) : null}
621
+ </View>
622
+ );
623
+ break;
624
+ }
625
+ case 'selection_list': {
626
+ node = (
627
+ <SelectionList
628
+ properties={props}
629
+ options={Array.isArray(json.options) ? json.options : []}
630
+ initialValue={params.formData[id ?? '']}
631
+ onChanged={(value) => {
632
+ if (params.onInputChange && id) {
633
+ params.onInputChange(id, value);
634
+ }
635
+ }}
636
+ onAction={params.onAction}
637
+ buildWidget={(widgetJson) =>
638
+ buildWidget(widgetJson, { ...params, allowFlexExpansion: true })
639
+ }
640
+ />
641
+ );
642
+ break;
643
+ }
644
+ case 'image': {
645
+ const source = props.source;
646
+ const width = normalizeDimension(parseDimension(props.width, true));
647
+ const height = normalizeDimension(parseDimension(props.height, true));
648
+ if (source === 'asset') {
649
+ node = (
650
+ <Image
651
+ source={{ uri: props.path }}
652
+ style={{ width, height }}
653
+ resizeMode={parseResizeMode(props.fit)}
654
+ />
655
+ );
656
+ } else if (source === 'network') {
657
+ if (!props.url) return null;
658
+ node = (
659
+ <Image
660
+ source={{ uri: props.url }}
661
+ style={{ width, height }}
662
+ resizeMode={parseResizeMode(props.fit)}
663
+ />
664
+ );
665
+ } else {
666
+ node = null;
667
+ }
668
+ break;
669
+ }
670
+ case 'lottie': {
671
+ const source = props.source;
672
+ const width = normalizeDimension(parseDimension(props.width, true));
673
+ const height = normalizeDimension(parseDimension(props.height, true));
674
+ const loop = props.loop === true;
675
+ if (source === 'asset') {
676
+ node = (
677
+ <LottieView
678
+ source={{ uri: props.path ?? '' }}
679
+ style={{ width, height }}
680
+ autoPlay
681
+ loop={loop}
682
+ />
683
+ );
684
+ } else if (source === 'network') {
685
+ if (!props.url) return null;
686
+ node = (
687
+ <LottieView
688
+ source={{ uri: props.url }}
689
+ style={{ width, height }}
690
+ autoPlay
691
+ loop={loop}
692
+ />
693
+ );
694
+ } else {
695
+ node = null;
696
+ }
697
+ break;
698
+ }
699
+ case 'grid': {
700
+ const crossAxisCount = Number(props.crossAxisCount ?? 2);
701
+ const spacing = Number(props.spacing ?? 8);
702
+ const mainAxisSpacing = Number(props.mainAxisSpacing ?? spacing);
703
+ const crossAxisSpacing = Number(props.crossAxisSpacing ?? spacing);
704
+ const childAspectRatio = Number(props.childAspectRatio ?? 1);
705
+
706
+ node = (
707
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
708
+ {(childrenJson ?? []).map((child, index) => (
709
+ <View
710
+ key={`grid-${index}`}
711
+ style={{
712
+ width: `${100 / crossAxisCount}%`,
713
+ paddingRight:
714
+ (index + 1) % crossAxisCount === 0 ? 0 : crossAxisSpacing,
715
+ paddingBottom: mainAxisSpacing,
716
+ aspectRatio: childAspectRatio,
717
+ }}
718
+ >
719
+ {buildWidget(child, { ...params })}
720
+ </View>
721
+ ))}
722
+ </View>
723
+ );
724
+ break;
725
+ }
726
+ case 'fake_progress_bar': {
727
+ node = (
728
+ <FakeProgressBar
729
+ progressColor={parseColor(props.progressColor ?? '0xFF2196F3')}
730
+ backgroundColor={parseColor(props.backgroundColor ?? '0xFFE0E0E0')}
731
+ thickness={Number(props.thickness ?? 4)}
732
+ borderRadius={Number(props.borderRadius ?? 0)}
733
+ durationMs={Number(props.duration ?? 3000)}
734
+ startDelayMs={Number(props.startDelay ?? 0)}
735
+ style={props.style ?? 'linear'}
736
+ showProgressText={props.showProgressText === true}
737
+ progressTextStyle={getTextStyle(
738
+ props.progressTextStyle ?? {},
739
+ 'color'
740
+ )}
741
+ progressTextFormat={props.progressTextFormat ?? '{{progress}}%'}
742
+ size={Number(props.size ?? 50)}
743
+ onComplete={() => {
744
+ if (action) {
745
+ params.onAction(action, actionPayload ?? props);
746
+ }
747
+ }}
748
+ />
749
+ );
750
+ break;
751
+ }
752
+ case 'slider': {
753
+ node = (
754
+ <SliderWidget
755
+ id={json._internalId}
756
+ linkedTo={props.linkedTo}
757
+ direction={props.direction === 'vertical' ? 'vertical' : 'horizontal'}
758
+ autoPlay={props.autoPlay === true}
759
+ autoPlayDelay={Number(props.autoPlayDelay ?? 3000)}
760
+ loop={props.loop === true}
761
+ height={parseDimension(props.height)}
762
+ physics={props.physics}
763
+ >
764
+ {(childrenJson ?? []).map((child, index) => (
765
+ <View key={`slider-${index}`} style={{ flex: 1 }}>
766
+ {buildWidget(child, { ...params })}
767
+ </View>
768
+ ))}
769
+ </SliderWidget>
770
+ );
771
+ break;
772
+ }
773
+ case 'pageview_indicator': {
774
+ node = (
775
+ <PageViewIndicator
776
+ linkedTo={props.linkedTo}
777
+ activeColor={parseColor(props.activeColor ?? '0xFF2196F3')}
778
+ inactiveColor={parseColor(props.inactiveColor ?? '0xFFE0E0E0')}
779
+ dotSize={Number(props.dotSize ?? 8)}
780
+ spacing={Number(props.spacing ?? 8)}
781
+ style={props.style ?? 'dots'}
782
+ />
783
+ );
784
+ break;
785
+ }
786
+ case 'radar_chart': {
787
+ node = <RadarChart properties={props} formData={params.formData} />;
788
+ break;
789
+ }
790
+ case 'icon': {
791
+ const iconName = props.icon ?? props.name;
792
+ const iconStyle = props.style ?? 'solid';
793
+ const size = Number(props.size ?? 24);
794
+ const color = parseColor(props.color ?? '0xFF000000');
795
+
796
+ if (!params.enableFontAwesomeIcons) {
797
+ node = <Text style={{ fontSize: size, color }}>?</Text>;
798
+ break;
799
+ }
800
+
801
+ const icon = resolveFontAwesomeIcon(iconName, iconStyle);
802
+ if (!icon.name) {
803
+ node = <Text style={{ fontSize: size, color }}>?</Text>;
804
+ } else {
805
+ node = (
806
+ <FontAwesome6
807
+ name={icon.name}
808
+ size={size}
809
+ color={color}
810
+ {...icon.props}
811
+ />
812
+ );
813
+ }
814
+ break;
815
+ }
816
+ case 'stack': {
817
+ const alignment = parseAlignment(props.alignment ?? 'topLeft');
818
+ const fit = props.fit === 'expand' ? 'expand' : 'loose';
819
+ node = (
820
+ <View
821
+ style={{
822
+ position: 'relative',
823
+ width: fit === 'expand' ? '100%' : undefined,
824
+ height: fit === 'expand' ? '100%' : undefined,
825
+ justifyContent: alignment.justifyContent,
826
+ alignItems: alignment.alignItems,
827
+ }}
828
+ >
829
+ {(childrenJson ?? []).map((child, index) => (
830
+ <React.Fragment key={`stack-${index}`}>
831
+ {buildWidget(child, { ...params })}
832
+ </React.Fragment>
833
+ ))}
834
+ </View>
835
+ );
836
+ break;
837
+ }
838
+ case 'positioned': {
839
+ const left = parseDimension(props.left);
840
+ const top = parseDimension(props.top);
841
+ const right = parseDimension(props.right);
842
+ const bottom = parseDimension(props.bottom);
843
+ const width = parseDimension(props.width);
844
+ const height = parseDimension(props.height);
845
+ return (
846
+ <View
847
+ style={{
848
+ position: 'absolute',
849
+ left,
850
+ top,
851
+ right,
852
+ bottom,
853
+ width,
854
+ height,
855
+ }}
856
+ >
857
+ {childJson ? buildWidget(childJson, { ...params }) : null}
858
+ </View>
859
+ );
860
+ }
861
+ default:
862
+ node = null;
863
+ }
864
+
865
+ if (!node) return null;
866
+
867
+ const marginVal = props.margin;
868
+ if (marginVal !== undefined && marginVal !== null) {
869
+ const marginInsets = parseInsets(marginVal);
870
+ node = <View style={insetsToStyle(marginInsets)}>{node}</View>;
871
+ } else if (type !== 'container' && type !== 'padding' && props.padding) {
872
+ const paddingInsets = parseInsets(props.padding);
873
+ node = <View style={insetsToStyle(paddingInsets)}>{node}</View>;
874
+ }
875
+
876
+ if (params.allowFlexExpansion && params.screenData.scrollable !== true) {
877
+ let shouldExpand = false;
878
+ if (type === 'stack' && props.fit === 'expand') {
879
+ shouldExpand = true;
880
+ }
881
+ if (props.height !== undefined) {
882
+ const heightVal = parseDimension(props.height);
883
+ if (heightVal === Number.POSITIVE_INFINITY) {
884
+ shouldExpand = true;
885
+ }
886
+ }
887
+
888
+ if (shouldExpand) {
889
+ return <View style={{ flex: 1 }}>{node}</View>;
890
+ }
891
+ }
892
+
893
+ return node;
894
+ }
895
+
896
+ function buildActionPayload(
897
+ props: Record<string, any>,
898
+ json: Record<string, any>
899
+ ) {
900
+ const payload = { ...props };
901
+ const actionPermission = json.actionPermission;
902
+ if (actionPermission !== undefined)
903
+ payload.actionPermission = actionPermission;
904
+ const extraData = json.actionData;
905
+ if (extraData && typeof extraData === 'object') {
906
+ Object.assign(payload, extraData);
907
+ }
908
+ return payload;
909
+ }
910
+
911
+ function getTextStyle(
912
+ props: Record<string, any>,
913
+ colorKey: string,
914
+ defaultColor?: string
915
+ ): TextStyle {
916
+ const fontFamily = props.fontFamily;
917
+ const fontSize = Number(props.fontSize ?? 14);
918
+ const fontWeight = parseFontWeight(props.fontWeight);
919
+ const fontStyle = props.fontStyle === 'italic' ? 'italic' : 'normal';
920
+ const color = parseColor(props[colorKey] ?? defaultColor ?? '0xFF000000');
921
+ const letterSpacing =
922
+ props.letterSpacing !== undefined ? Number(props.letterSpacing) : undefined;
923
+ const height = props.height !== undefined ? Number(props.height) : undefined;
924
+ const textDecoration = parseTextDecoration(props.decoration);
925
+
926
+ return {
927
+ fontFamily,
928
+ fontSize,
929
+ fontWeight,
930
+ fontStyle,
931
+ color,
932
+ letterSpacing,
933
+ lineHeight: height ? height * fontSize : undefined,
934
+ textDecorationLine: textDecoration,
935
+ };
936
+ }
937
+
938
+ function parseGradient(value: any):
939
+ | {
940
+ colors: string[];
941
+ start: { x: number; y: number };
942
+ end: { x: number; y: number };
943
+ stops?: number[];
944
+ }
945
+ | undefined {
946
+ if (!value || typeof value !== 'object' || value.type !== 'gradient')
947
+ return undefined;
948
+ const colors = Array.isArray(value.colors)
949
+ ? value.colors.map((c: string) => parseColor(c))
950
+ : [];
951
+ if (colors.length === 0) return undefined;
952
+ const start = alignmentToGradient(value.begin ?? 'topLeft');
953
+ const end = alignmentToGradient(value.end ?? 'bottomRight');
954
+ const stops = Array.isArray(value.stops)
955
+ ? value.stops.map((s: number) => Number(s))
956
+ : undefined;
957
+ return { colors, start, end, stops };
958
+ }
959
+
960
+ function alignmentToGradient(value: string) {
961
+ switch (value) {
962
+ case 'topCenter':
963
+ return { x: 0.5, y: 0 };
964
+ case 'bottomCenter':
965
+ return { x: 0.5, y: 1 };
966
+ case 'centerLeft':
967
+ return { x: 0, y: 0.5 };
968
+ case 'centerRight':
969
+ return { x: 1, y: 0.5 };
970
+ case 'center':
971
+ return { x: 0.5, y: 0.5 };
972
+ case 'bottomLeft':
973
+ return { x: 0, y: 1 };
974
+ case 'bottomRight':
975
+ return { x: 1, y: 1 };
976
+ case 'topRight':
977
+ return { x: 1, y: 0 };
978
+ case 'topLeft':
979
+ default:
980
+ return { x: 0, y: 0 };
981
+ }
982
+ }
983
+
984
+ function withOpacity(color: string, opacity: number): string {
985
+ if (color.startsWith('rgba')) {
986
+ return color.replace(
987
+ /rgba\((\d+),(\d+),(\d+),([\d.]+)\)/,
988
+ (_m, r, g, b) => `rgba(${r},${g},${b},${opacity})`
989
+ );
990
+ }
991
+ return color;
992
+ }
993
+
994
+ function normalizeDimension(
995
+ value: number | undefined
996
+ ): number | string | undefined {
997
+ if (value === Number.POSITIVE_INFINITY) {
998
+ return '100%';
999
+ }
1000
+ return value;
1001
+ }
1002
+
1003
+ function GradientText({
1004
+ text,
1005
+ gradient,
1006
+ style,
1007
+ }: {
1008
+ text: string;
1009
+ gradient: { colors: string[]; start: any; end: any; stops?: number[] };
1010
+ style?: any;
1011
+ }) {
1012
+ return (
1013
+ <MaskedView
1014
+ maskElement={
1015
+ <Text style={[style, { backgroundColor: 'transparent' }]}>{text}</Text>
1016
+ }
1017
+ >
1018
+ <LinearGradient
1019
+ colors={gradient.colors}
1020
+ start={gradient.start}
1021
+ end={gradient.end}
1022
+ locations={gradient.stops}
1023
+ >
1024
+ <Text style={[style, { opacity: 0 }]}>{text}</Text>
1025
+ </LinearGradient>
1026
+ </MaskedView>
1027
+ );
1028
+ }
1029
+
1030
+ function DynamicInput({
1031
+ initialValue,
1032
+ properties,
1033
+ onChanged,
1034
+ }: {
1035
+ initialValue: string;
1036
+ properties: Record<string, any>;
1037
+ onChanged: (value: string) => void;
1038
+ }) {
1039
+ const [value, setValue] = React.useState(initialValue ?? '');
1040
+
1041
+ React.useEffect(() => {
1042
+ setValue(initialValue ?? '');
1043
+ }, [initialValue]);
1044
+ const keyboardType = mapKeyboardType(properties.keyboardType);
1045
+ const autoCapitalize = mapAutoCapitalize(properties.autoCapitalize);
1046
+ const secureTextEntry = properties.obscureText === true;
1047
+ const multiline = properties.keyboardType === 'multiline';
1048
+ const maxLength = properties.maxLength
1049
+ ? Number(properties.maxLength)
1050
+ : undefined;
1051
+ const mask = properties.mask ? String(properties.mask) : null;
1052
+
1053
+ const label = properties.label;
1054
+ const placeholder = properties.placeholder;
1055
+ const labelStyle = getTextStyle(
1056
+ properties.labelStyle ?? {},
1057
+ 'color',
1058
+ '0xFF9E9E9E'
1059
+ );
1060
+ const hintStyle = getTextStyle(
1061
+ properties.hintStyle ?? {},
1062
+ 'color',
1063
+ '0xFF9E9E9E'
1064
+ );
1065
+ const inputStyle = getTextStyle({ ...properties, height: null }, 'textColor');
1066
+
1067
+ const backgroundColor = parseColor(
1068
+ properties.backgroundColor ?? '0xFFF0F0F0'
1069
+ );
1070
+ const borderRadius = Number(properties.borderRadius ?? 8);
1071
+
1072
+ return (
1073
+ <View>
1074
+ {label ? <Text style={labelStyle}>{label}</Text> : null}
1075
+ <View
1076
+ style={{
1077
+ backgroundColor,
1078
+ borderRadius,
1079
+ paddingHorizontal: 16,
1080
+ paddingVertical: 10,
1081
+ }}
1082
+ >
1083
+ {mask ? (
1084
+ <MaskInput
1085
+ value={value}
1086
+ onChangeText={(_masked, unmasked) => {
1087
+ setValue(unmasked);
1088
+ onChanged(unmasked);
1089
+ }}
1090
+ autoFocus
1091
+ autoCapitalize={autoCapitalize}
1092
+ keyboardType={keyboardType}
1093
+ secureTextEntry={secureTextEntry}
1094
+ multiline={multiline}
1095
+ maxLength={maxLength}
1096
+ style={inputStyle}
1097
+ mask={mask.split('').map((char) => {
1098
+ if (char === '#') return /\d/;
1099
+ if (char === 'A') return /[a-zA-Z]/;
1100
+ return char;
1101
+ })}
1102
+ placeholder={placeholder}
1103
+ placeholderTextColor={hintStyle.color}
1104
+ />
1105
+ ) : (
1106
+ <TextInput
1107
+ value={value}
1108
+ onChangeText={(text) => {
1109
+ setValue(text);
1110
+ onChanged(text);
1111
+ }}
1112
+ autoFocus
1113
+ autoCapitalize={autoCapitalize}
1114
+ keyboardType={keyboardType}
1115
+ secureTextEntry={secureTextEntry}
1116
+ multiline={multiline}
1117
+ maxLength={maxLength}
1118
+ style={inputStyle}
1119
+ placeholder={placeholder}
1120
+ placeholderTextColor={hintStyle.color}
1121
+ />
1122
+ )}
1123
+ </View>
1124
+ </View>
1125
+ );
1126
+ }
1127
+
1128
+ function mapKeyboardType(type?: string) {
1129
+ switch (type) {
1130
+ case 'email':
1131
+ return 'email-address';
1132
+ case 'number':
1133
+ return 'numeric';
1134
+ case 'phone':
1135
+ return 'phone-pad';
1136
+ case 'multiline':
1137
+ return 'default';
1138
+ default:
1139
+ return 'default';
1140
+ }
1141
+ }
1142
+
1143
+ function mapAutoCapitalize(type?: string) {
1144
+ switch (type) {
1145
+ case 'words':
1146
+ return 'words';
1147
+ case 'sentences':
1148
+ return 'sentences';
1149
+ case 'characters':
1150
+ return 'characters';
1151
+ default:
1152
+ return 'none';
1153
+ }
1154
+ }
1155
+
1156
+ function SelectionList({
1157
+ properties,
1158
+ options,
1159
+ initialValue,
1160
+ onChanged,
1161
+ onAction,
1162
+ buildWidget,
1163
+ }: {
1164
+ properties: Record<string, any>;
1165
+ options: Record<string, any>[];
1166
+ initialValue: any;
1167
+ onChanged: (value: string) => void;
1168
+ onAction: (action: string, data?: Record<string, any>) => void;
1169
+ buildWidget: (widgetJson: Record<string, any>) => React.ReactNode;
1170
+ }) {
1171
+ const multiSelect = properties.multiSelect === true;
1172
+ const [selectedValues, setSelectedValues] = React.useState<string[]>(() => {
1173
+ if (initialValue === null || initialValue === undefined) return [];
1174
+ if (Array.isArray(initialValue)) return initialValue.map(String);
1175
+ if (typeof initialValue === 'string') {
1176
+ if (multiSelect) {
1177
+ try {
1178
+ return initialValue
1179
+ .replace('[', '')
1180
+ .replace(']', '')
1181
+ .split(',')
1182
+ .map((val) => val.trim());
1183
+ } catch {
1184
+ return [initialValue];
1185
+ }
1186
+ }
1187
+ return [initialValue];
1188
+ }
1189
+ return [];
1190
+ });
1191
+
1192
+ const toggleSelection = (value: string) => {
1193
+ setSelectedValues((prev) => {
1194
+ let next = [...prev];
1195
+ if (multiSelect) {
1196
+ if (next.includes(value)) {
1197
+ next = next.filter((item) => item !== value);
1198
+ } else {
1199
+ next.push(value);
1200
+ }
1201
+ onChanged(next.join(','));
1202
+ } else {
1203
+ next = [value];
1204
+ onChanged(value);
1205
+ if (properties.autoGoNext === true) {
1206
+ onAction('next');
1207
+ }
1208
+ }
1209
+ return next;
1210
+ });
1211
+ };
1212
+
1213
+ const layout = properties.layout ?? 'column';
1214
+ const spacing = Number(properties.spacing ?? 8);
1215
+
1216
+ const renderItem = (option: Record<string, any>, index: number) => {
1217
+ const value = String(option.value ?? '');
1218
+ const isSelected = selectedValues.includes(value);
1219
+ const styleProps = isSelected
1220
+ ? option.selectedStyle ?? properties.selectedStyle ?? {}
1221
+ : option.unselectedStyle ?? properties.unselectedStyle ?? {};
1222
+
1223
+ const backgroundColor = parseColor(styleProps.backgroundColor);
1224
+ const borderColor = parseColor(styleProps.borderColor);
1225
+ const borderRadius = Number(styleProps.borderRadius ?? 8);
1226
+ const borderWidth = Number(styleProps.borderWidth ?? 1);
1227
+
1228
+ return (
1229
+ <Pressable
1230
+ key={`option-${index}`}
1231
+ onPress={() => toggleSelection(value)}
1232
+ style={{
1233
+ width: '100%',
1234
+ backgroundColor,
1235
+ borderColor,
1236
+ borderWidth,
1237
+ borderRadius,
1238
+ padding: 0,
1239
+ }}
1240
+ >
1241
+ {buildWidget(option.child)}
1242
+ </Pressable>
1243
+ );
1244
+ };
1245
+
1246
+ if (layout === 'row') {
1247
+ return (
1248
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
1249
+ {options.map((option, index) => (
1250
+ <View
1251
+ key={`row-option-${index}`}
1252
+ style={{ marginRight: spacing, marginBottom: spacing }}
1253
+ >
1254
+ {renderItem(option, index)}
1255
+ </View>
1256
+ ))}
1257
+ </View>
1258
+ );
1259
+ }
1260
+
1261
+ if (layout === 'grid') {
1262
+ const crossAxisCount = Number(properties.gridCrossAxisCount ?? 2);
1263
+ const aspectRatio = Number(properties.gridAspectRatio ?? 1);
1264
+ return (
1265
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
1266
+ {options.map((option, index) => (
1267
+ <View
1268
+ key={`grid-option-${index}`}
1269
+ style={{
1270
+ width: `${100 / crossAxisCount}%`,
1271
+ paddingRight: (index + 1) % crossAxisCount === 0 ? 0 : spacing,
1272
+ paddingBottom: spacing,
1273
+ aspectRatio,
1274
+ }}
1275
+ >
1276
+ {renderItem(option, index)}
1277
+ </View>
1278
+ ))}
1279
+ </View>
1280
+ );
1281
+ }
1282
+
1283
+ return (
1284
+ <View>
1285
+ {options.map((option, index) => (
1286
+ <View key={`column-option-${index}`} style={{ marginBottom: spacing }}>
1287
+ {renderItem(option, index)}
1288
+ </View>
1289
+ ))}
1290
+ </View>
1291
+ );
1292
+ }
1293
+
1294
+ function FakeProgressBar({
1295
+ progressColor,
1296
+ backgroundColor,
1297
+ thickness,
1298
+ borderRadius,
1299
+ durationMs,
1300
+ startDelayMs,
1301
+ style,
1302
+ showProgressText,
1303
+ progressTextStyle,
1304
+ progressTextFormat,
1305
+ size,
1306
+ onComplete,
1307
+ }: {
1308
+ progressColor: string;
1309
+ backgroundColor: string;
1310
+ thickness: number;
1311
+ borderRadius: number;
1312
+ durationMs: number;
1313
+ startDelayMs: number;
1314
+ style: string;
1315
+ showProgressText: boolean;
1316
+ progressTextStyle: any;
1317
+ progressTextFormat: string;
1318
+ size: number;
1319
+ onComplete?: () => void;
1320
+ }) {
1321
+ const animation = useMemo(() => new Animated.Value(0), []);
1322
+ const [progressValue, setProgressValue] = React.useState(0);
1323
+
1324
+ React.useEffect(() => {
1325
+ const listenerId = animation.addListener(({ value }) => {
1326
+ setProgressValue(Math.round(value * 100));
1327
+ });
1328
+ const timeout = setTimeout(() => {
1329
+ Animated.timing(animation, {
1330
+ toValue: 1,
1331
+ duration: durationMs,
1332
+ useNativeDriver: false,
1333
+ }).start(({ finished }) => {
1334
+ if (finished && onComplete) onComplete();
1335
+ });
1336
+ }, startDelayMs);
1337
+
1338
+ return () => {
1339
+ animation.removeListener(listenerId);
1340
+ clearTimeout(timeout);
1341
+ animation.stopAnimation();
1342
+ };
1343
+ }, [animation, durationMs, startDelayMs, onComplete]);
1344
+
1345
+ const progressText = progressTextFormat.replace(
1346
+ '{{progress}}',
1347
+ progressValue.toString()
1348
+ );
1349
+
1350
+ if (style === 'circular') {
1351
+ const radius = size / 2 - thickness;
1352
+ const circumference = 2 * Math.PI * radius;
1353
+ const strokeDashoffset = animation.interpolate({
1354
+ inputRange: [0, 1],
1355
+ outputRange: [circumference, 0],
1356
+ });
1357
+
1358
+ return (
1359
+ <View style={{ alignItems: 'center' }}>
1360
+ <Svg width={size} height={size}>
1361
+ <Circle
1362
+ cx={size / 2}
1363
+ cy={size / 2}
1364
+ r={radius}
1365
+ stroke={backgroundColor}
1366
+ strokeWidth={thickness}
1367
+ fill="none"
1368
+ />
1369
+ <AnimatedCircle
1370
+ cx={size / 2}
1371
+ cy={size / 2}
1372
+ r={radius}
1373
+ stroke={progressColor}
1374
+ strokeWidth={thickness}
1375
+ strokeDasharray={`${circumference} ${circumference}`}
1376
+ strokeDashoffset={strokeDashoffset as any}
1377
+ fill="none"
1378
+ strokeLinecap="round"
1379
+ />
1380
+ </Svg>
1381
+ {showProgressText && (
1382
+ <Text style={progressTextStyle}>{progressText}</Text>
1383
+ )}
1384
+ </View>
1385
+ );
1386
+ }
1387
+
1388
+ return (
1389
+ <View>
1390
+ <View
1391
+ style={{
1392
+ height: thickness,
1393
+ width: '100%',
1394
+ borderRadius,
1395
+ backgroundColor,
1396
+ overflow: 'hidden',
1397
+ }}
1398
+ >
1399
+ <Animated.View
1400
+ style={{
1401
+ height: '100%',
1402
+ width: animation.interpolate({
1403
+ inputRange: [0, 1],
1404
+ outputRange: ['0%', '100%'],
1405
+ }),
1406
+ backgroundColor: progressColor,
1407
+ borderRadius,
1408
+ }}
1409
+ />
1410
+ </View>
1411
+ {showProgressText ? (
1412
+ <Text style={[progressTextStyle, { marginTop: 8 }]}>
1413
+ {progressText}
1414
+ </Text>
1415
+ ) : null}
1416
+ </View>
1417
+ );
1418
+ }
1419
+
1420
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
1421
+
1422
+ function SliderWidget({
1423
+ id,
1424
+ linkedTo,
1425
+ direction,
1426
+ autoPlay,
1427
+ autoPlayDelay,
1428
+ loop,
1429
+ height,
1430
+ physics,
1431
+ children,
1432
+ }: {
1433
+ id?: string;
1434
+ linkedTo?: string;
1435
+ direction: 'horizontal' | 'vertical';
1436
+ autoPlay: boolean;
1437
+ autoPlayDelay: number;
1438
+ loop: boolean;
1439
+ height?: number;
1440
+ physics?: string;
1441
+ children: React.ReactNode[];
1442
+ }) {
1443
+ const registry = useSliderRegistry();
1444
+ const pagerRef = React.useRef<PagerView>(null);
1445
+ const pageLength = children.length;
1446
+ const [currentPage, setCurrentPage] = React.useState(() =>
1447
+ loop && pageLength > 0 ? pageLength * 1000 : 0
1448
+ );
1449
+ const timerRef = React.useRef<NodeJS.Timeout | null>(null);
1450
+
1451
+ React.useEffect(() => {
1452
+ if (autoPlay && !linkedTo) {
1453
+ timerRef.current = setInterval(() => {
1454
+ setCurrentPage((prev) => {
1455
+ if (loop) return prev + 1;
1456
+ if (prev < children.length - 1) return prev + 1;
1457
+ return 0;
1458
+ });
1459
+ }, autoPlayDelay);
1460
+ }
1461
+ return () => {
1462
+ if (timerRef.current) clearInterval(timerRef.current);
1463
+ };
1464
+ }, [autoPlay, autoPlayDelay, loop, linkedTo, children.length]);
1465
+
1466
+ React.useEffect(() => {
1467
+ if (pagerRef.current) {
1468
+ pagerRef.current.setPage(currentPage);
1469
+ }
1470
+ }, [currentPage]);
1471
+
1472
+ React.useEffect(() => {
1473
+ if (registry && id && children.length > 0) {
1474
+ registry.update(id, currentPage % children.length, children.length);
1475
+ }
1476
+ }, [registry, id, currentPage, children.length]);
1477
+
1478
+ React.useEffect(() => {
1479
+ if (!linkedTo || !registry) return;
1480
+ return registry.getNotifier(linkedTo, (value) => {
1481
+ if (pagerRef.current) {
1482
+ pagerRef.current.setPage(value);
1483
+ }
1484
+ });
1485
+ }, [linkedTo, registry]);
1486
+
1487
+ if (pageLength === 0) return null;
1488
+
1489
+ const pageCount = loop ? pageLength * 1000 : pageLength;
1490
+
1491
+ return (
1492
+ <PagerView
1493
+ ref={pagerRef}
1494
+ style={{ height: height ?? 200 }}
1495
+ orientation={direction}
1496
+ scrollEnabled={physics !== 'never'}
1497
+ initialPage={currentPage}
1498
+ onPageSelected={(event: PagerViewOnPageSelectedEvent) => {
1499
+ const page = event.nativeEvent.position;
1500
+ setCurrentPage(page);
1501
+ if (registry && id) {
1502
+ registry.update(id, page % pageLength, pageLength);
1503
+ }
1504
+ }}
1505
+ >
1506
+ {Array.from({ length: pageCount }).map((_, index) => (
1507
+ <View key={`slider-page-${index}`} style={{ flex: 1 }}>
1508
+ {children[index % pageLength]}
1509
+ </View>
1510
+ ))}
1511
+ </PagerView>
1512
+ );
1513
+ }
1514
+
1515
+ function PageViewIndicator({
1516
+ linkedTo,
1517
+ activeColor,
1518
+ inactiveColor,
1519
+ dotSize,
1520
+ spacing,
1521
+ style,
1522
+ }: {
1523
+ linkedTo?: string;
1524
+ activeColor: string;
1525
+ inactiveColor: string;
1526
+ dotSize: number;
1527
+ spacing: number;
1528
+ style?: string;
1529
+ }) {
1530
+ const registry = useSliderRegistry();
1531
+ const [current, setCurrent] = React.useState(0);
1532
+ const indicatorStyle = style ?? 'dots';
1533
+ const count = linkedTo && registry ? registry.getPageCount(linkedTo) : 0;
1534
+
1535
+ React.useEffect(() => {
1536
+ if (!linkedTo || !registry) return;
1537
+ return registry.getNotifier(linkedTo, setCurrent);
1538
+ }, [linkedTo, registry]);
1539
+
1540
+ if (!linkedTo || !registry || count <= 1) return null;
1541
+ if (indicatorStyle !== 'dots') {
1542
+ // Only dots are supported currently; fallback to dots.
1543
+ }
1544
+
1545
+ return (
1546
+ <View style={{ flexDirection: 'row', justifyContent: 'center' }}>
1547
+ {Array.from({ length: count }).map((_, index) => (
1548
+ <View
1549
+ key={`dot-${index}`}
1550
+ style={{
1551
+ width: dotSize,
1552
+ height: dotSize,
1553
+ borderRadius: dotSize / 2,
1554
+ marginHorizontal: spacing / 2,
1555
+ backgroundColor: index === current ? activeColor : inactiveColor,
1556
+ }}
1557
+ />
1558
+ ))}
1559
+ </View>
1560
+ );
1561
+ }
1562
+
1563
+ function RadarChart({
1564
+ properties,
1565
+ formData,
1566
+ }: {
1567
+ properties: Record<string, any>;
1568
+ formData: Record<string, any>;
1569
+ }) {
1570
+ const animate = properties.animate !== false;
1571
+ const animationDurationMs = Number(properties.animationDurationMs ?? 800);
1572
+ const scale = useMemo(() => new Animated.Value(animate ? 0 : 1), [animate]);
1573
+
1574
+ React.useEffect(() => {
1575
+ if (!animate) return;
1576
+ Animated.timing(scale, {
1577
+ toValue: 1,
1578
+ duration: animationDurationMs,
1579
+ useNativeDriver: true,
1580
+ }).start();
1581
+ }, [animate, animationDurationMs, scale]);
1582
+ const axes = parseRadarAxes(properties.axes);
1583
+ if (axes.length < 3) {
1584
+ return radarPlaceholder('Radar chart requires at least 3 axes.');
1585
+ }
1586
+
1587
+ const datasets = parseRadarDatasets(
1588
+ properties.datasets,
1589
+ axes.length,
1590
+ formData
1591
+ );
1592
+ if (datasets.length === 0) {
1593
+ return radarPlaceholder(
1594
+ 'Add at least one dataset with the same number of values as axes.'
1595
+ );
1596
+ }
1597
+
1598
+ const normalizedAxes = normalizeAxisMaximums(axes, datasets);
1599
+ const backgroundColor = parseColor(properties.backgroundColor);
1600
+ const width = parseDimension(properties.width) ?? 300;
1601
+ const rawHeight = parseDimension(properties.height);
1602
+ const height =
1603
+ rawHeight === undefined || rawHeight === Number.POSITIVE_INFINITY
1604
+ ? 300
1605
+ : rawHeight;
1606
+ const padding = parseInsets(properties.padding);
1607
+ const gridLevels = Math.min(
1608
+ Math.max(Number(properties.gridLevels ?? 5), 1),
1609
+ 12
1610
+ );
1611
+ const gridColor = parseColor(properties.gridColor ?? '0xFFE0E0E0');
1612
+ const gridStrokeWidth = Number(properties.gridStrokeWidth ?? 1);
1613
+ const axisColor = parseColor(properties.axisColor ?? '0xFF9E9E9E');
1614
+ const axisStrokeWidth = Number(properties.axisStrokeWidth ?? 1);
1615
+ const axisLabelStyle = getTextStyle(
1616
+ properties.axisLabelStyle ?? {},
1617
+ 'color',
1618
+ '0xFF424242'
1619
+ );
1620
+ const showLegend = properties.showLegend !== false;
1621
+ const legendPosition = (properties.legendPosition ?? 'bottom').toLowerCase();
1622
+ const legendSpacing = Number(properties.legendSpacing ?? 12);
1623
+ const legendStyle = getTextStyle(
1624
+ properties.legendStyle ?? {},
1625
+ 'color',
1626
+ '0xFF212121'
1627
+ );
1628
+ const shape = (properties.shape ?? 'polygon').toLowerCase();
1629
+
1630
+ const chartSize = Math.min(width, height);
1631
+ const radius =
1632
+ chartSize / 2 -
1633
+ Math.max(padding.left, padding.right, padding.top, padding.bottom);
1634
+ const center = { x: width / 2, y: height / 2 };
1635
+
1636
+ const axisAngle = (Math.PI * 2) / axes.length;
1637
+
1638
+ const gridPolygons = Array.from({ length: gridLevels }).map((_, level) => {
1639
+ const ratio = (level + 1) / gridLevels;
1640
+ const points = axes.map((_, index) => {
1641
+ const angle = index * axisAngle - Math.PI / 2;
1642
+ const x = center.x + radius * ratio * Math.cos(angle);
1643
+ const y = center.y + radius * ratio * Math.sin(angle);
1644
+ return `${x},${y}`;
1645
+ });
1646
+ return { points };
1647
+ });
1648
+
1649
+ const datasetPolygons = datasets.map((dataset) => {
1650
+ const points = dataset.values.map((value, index) => {
1651
+ const axis = normalizedAxes[index];
1652
+ const maxValue = axis?.maxValue ?? 0;
1653
+ const ratio = maxValue > 0 ? value / maxValue : 0;
1654
+ const angle = index * axisAngle - Math.PI / 2;
1655
+ const x = center.x + radius * ratio * Math.cos(angle);
1656
+ const y = center.y + radius * ratio * Math.sin(angle);
1657
+ return `${x},${y}`;
1658
+ });
1659
+ return { dataset, points };
1660
+ });
1661
+
1662
+ const legend = (
1663
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
1664
+ {datasets.map((dataset, index) => (
1665
+ <View
1666
+ key={`legend-${index}`}
1667
+ style={{
1668
+ flexDirection: 'row',
1669
+ alignItems: 'center',
1670
+ marginRight: legendSpacing,
1671
+ marginBottom: 6,
1672
+ }}
1673
+ >
1674
+ <View
1675
+ style={{
1676
+ width: 12,
1677
+ height: 12,
1678
+ borderRadius: 6,
1679
+ backgroundColor: dataset.borderColor,
1680
+ marginRight: 6,
1681
+ }}
1682
+ />
1683
+ <Text style={legendStyle}>{dataset.label}</Text>
1684
+ </View>
1685
+ ))}
1686
+ </View>
1687
+ );
1688
+
1689
+ const chart = (
1690
+ <Animated.View
1691
+ style={{
1692
+ width,
1693
+ height,
1694
+ paddingLeft: padding.left,
1695
+ paddingRight: padding.right,
1696
+ paddingTop: padding.top,
1697
+ paddingBottom: padding.bottom,
1698
+ transform: [{ scale }],
1699
+ }}
1700
+ >
1701
+ <Svg width={width} height={height}>
1702
+ <G>
1703
+ {shape === 'circle'
1704
+ ? gridPolygons.map((_, level) => (
1705
+ <Circle
1706
+ key={`grid-circle-${level}`}
1707
+ cx={center.x}
1708
+ cy={center.y}
1709
+ r={radius * ((level + 1) / gridLevels)}
1710
+ stroke={gridColor}
1711
+ strokeWidth={gridStrokeWidth}
1712
+ fill="none"
1713
+ />
1714
+ ))
1715
+ : gridPolygons.map((grid, level) => (
1716
+ <Polygon
1717
+ key={`grid-${level}`}
1718
+ points={grid.points.join(' ')}
1719
+ stroke={gridColor}
1720
+ strokeWidth={gridStrokeWidth}
1721
+ fill="none"
1722
+ />
1723
+ ))}
1724
+ {axes.map((_axis, index) => {
1725
+ const angle = index * axisAngle - Math.PI / 2;
1726
+ const x = center.x + radius * Math.cos(angle);
1727
+ const y = center.y + radius * Math.sin(angle);
1728
+ return (
1729
+ <Line
1730
+ key={`axis-${index}`}
1731
+ x1={center.x}
1732
+ y1={center.y}
1733
+ x2={x}
1734
+ y2={y}
1735
+ stroke={axisColor}
1736
+ strokeWidth={axisStrokeWidth}
1737
+ />
1738
+ );
1739
+ })}
1740
+ {axes.map((axis, index) => {
1741
+ const angle = index * axisAngle - Math.PI / 2;
1742
+ const labelX = center.x + (radius + 12) * Math.cos(angle);
1743
+ const labelY = center.y + (radius + 12) * Math.sin(angle);
1744
+ return (
1745
+ <SvgText
1746
+ key={`label-${index}`}
1747
+ x={labelX}
1748
+ y={labelY}
1749
+ fontSize={axisLabelStyle.fontSize ?? 12}
1750
+ fontWeight={axisLabelStyle.fontWeight}
1751
+ fill={axisLabelStyle.color}
1752
+ textAnchor="middle"
1753
+ >
1754
+ {axis.label}
1755
+ </SvgText>
1756
+ );
1757
+ })}
1758
+ {datasetPolygons.map((entry, index) => (
1759
+ <Polygon
1760
+ key={`dataset-${index}`}
1761
+ points={entry.points.join(' ')}
1762
+ stroke={entry.dataset.borderColor}
1763
+ strokeWidth={entry.dataset.borderWidth}
1764
+ fill={entry.dataset.fillColor ?? 'transparent'}
1765
+ strokeDasharray={
1766
+ entry.dataset.borderStyle === 'dotted' ||
1767
+ entry.dataset.borderStyle === 'dashed'
1768
+ ? entry.dataset.dashArray ?? '6 4'
1769
+ : undefined
1770
+ }
1771
+ />
1772
+ ))}
1773
+ {datasetPolygons.map((entry, datasetIndex) =>
1774
+ entry.dataset.showPoints
1775
+ ? entry.points.map((point, pointIndex) => {
1776
+ const [x, y] = point.split(',').map((val) => Number(val));
1777
+ return (
1778
+ <Circle
1779
+ key={`point-${datasetIndex}-${pointIndex}`}
1780
+ cx={x}
1781
+ cy={y}
1782
+ r={entry.dataset.pointRadius}
1783
+ fill={entry.dataset.pointColor}
1784
+ />
1785
+ );
1786
+ })
1787
+ : null
1788
+ )}
1789
+ </G>
1790
+ </Svg>
1791
+ </Animated.View>
1792
+ );
1793
+
1794
+ const composed = (
1795
+ <View style={{ backgroundColor }}>
1796
+ {chart}
1797
+ {showLegend && legendPosition === 'bottom' ? (
1798
+ <View style={{ marginTop: 12 }}>{legend}</View>
1799
+ ) : null}
1800
+ </View>
1801
+ );
1802
+
1803
+ if (!showLegend || legendPosition === 'bottom') return composed;
1804
+
1805
+ if (legendPosition === 'top') {
1806
+ return (
1807
+ <View>
1808
+ {legend}
1809
+ <View style={{ height: 12 }} />
1810
+ {chart}
1811
+ </View>
1812
+ );
1813
+ }
1814
+
1815
+ if (legendPosition === 'left') {
1816
+ return (
1817
+ <View style={{ flexDirection: 'row' }}>
1818
+ {legend}
1819
+ <View style={{ width: 12 }} />
1820
+ {chart}
1821
+ </View>
1822
+ );
1823
+ }
1824
+
1825
+ if (legendPosition === 'right') {
1826
+ return (
1827
+ <View style={{ flexDirection: 'row' }}>
1828
+ {chart}
1829
+ <View style={{ width: 12 }} />
1830
+ {legend}
1831
+ </View>
1832
+ );
1833
+ }
1834
+
1835
+ return composed;
1836
+ }
1837
+
1838
+ export type RadarAxis = { label: string; maxValue: number };
1839
+
1840
+ export type RadarDataset = {
1841
+ label: string;
1842
+ values: number[];
1843
+ borderColor: string;
1844
+ borderWidth: number;
1845
+ borderStyle: string;
1846
+ dashArray?: number[];
1847
+ showPoints: boolean;
1848
+ pointRadius: number;
1849
+ pointColor: string;
1850
+ fillColor?: string;
1851
+ };
1852
+
1853
+ export function parseRadarAxes(rawAxes: any): RadarAxis[] {
1854
+ if (!Array.isArray(rawAxes)) return [];
1855
+ const axes: RadarAxis[] = [];
1856
+ rawAxes.forEach((axis) => {
1857
+ if (!axis || typeof axis !== 'object') return;
1858
+ const label = axis.label ?? '';
1859
+ if (!label) return;
1860
+ const maxValue = resolveNumericValue(axis.maxValue, {}) ?? 100;
1861
+ axes.push({ label, maxValue: maxValue > 0 ? maxValue : 100 });
1862
+ });
1863
+ return axes;
1864
+ }
1865
+
1866
+ export function parseRadarDatasets(
1867
+ raw: any,
1868
+ axisLength: number,
1869
+ formData: Record<string, any>
1870
+ ): RadarDataset[] {
1871
+ if (!Array.isArray(raw) || axisLength === 0) return [];
1872
+ const datasets: RadarDataset[] = [];
1873
+ raw.forEach((dataset) => {
1874
+ if (!dataset || typeof dataset !== 'object') return;
1875
+ const valuesRaw = Array.isArray(dataset.data) ? dataset.data : [];
1876
+ const values: number[] = [];
1877
+ for (let i = 0; i < axisLength; i += 1) {
1878
+ const rawValue = i < valuesRaw.length ? valuesRaw[i] : null;
1879
+ const resolved = resolveNumericValue(rawValue, formData) ?? 0;
1880
+ values.push(resolved);
1881
+ }
1882
+
1883
+ const label = dataset.label ?? `Dataset ${datasets.length + 1}`;
1884
+ const borderColor = parseColor(dataset.borderColor ?? '0xFF2196F3');
1885
+ const fillColor = dataset.fillColor
1886
+ ? parseColor(dataset.fillColor)
1887
+ : undefined;
1888
+ const pointColor = parseColor(
1889
+ dataset.pointColor ?? dataset.borderColor ?? '0xFF2196F3'
1890
+ );
1891
+ const borderWidth = Number(dataset.borderWidth ?? 2);
1892
+ const pointRadius = Number(dataset.pointRadius ?? 4);
1893
+ const borderStyle = (dataset.borderStyle ?? 'solid')
1894
+ .toString()
1895
+ .toLowerCase();
1896
+ const normalizedStyle =
1897
+ borderStyle === 'dotted' || borderStyle === 'dashed'
1898
+ ? borderStyle
1899
+ : 'solid';
1900
+ const dashArray = Array.isArray(dataset.dashArray)
1901
+ ? dataset.dashArray.map((val: any) => Number(val))
1902
+ : undefined;
1903
+ const showPoints = dataset.showPoints !== false;
1904
+
1905
+ datasets.push({
1906
+ label,
1907
+ values,
1908
+ borderColor,
1909
+ borderWidth,
1910
+ borderStyle: normalizedStyle,
1911
+ dashArray,
1912
+ showPoints,
1913
+ pointRadius,
1914
+ pointColor,
1915
+ fillColor,
1916
+ });
1917
+ });
1918
+ return datasets;
1919
+ }
1920
+
1921
+ export function normalizeAxisMaximums(
1922
+ axes: RadarAxis[],
1923
+ datasets: RadarDataset[]
1924
+ ): RadarAxis[] {
1925
+ return axes.map((axis, index) => {
1926
+ let maxValue = axis.maxValue;
1927
+ datasets.forEach((dataset) => {
1928
+ const value = dataset.values[index];
1929
+ if (value !== undefined && value > maxValue) maxValue = value;
1930
+ });
1931
+ return { ...axis, maxValue };
1932
+ });
1933
+ }
1934
+
1935
+ function radarPlaceholder(message: string) {
1936
+ return (
1937
+ <View
1938
+ style={{
1939
+ padding: 16,
1940
+ borderRadius: 8,
1941
+ borderWidth: 1,
1942
+ borderColor: 'rgba(255,0,0,0.2)',
1943
+ backgroundColor: 'rgba(255,0,0,0.05)',
1944
+ }}
1945
+ >
1946
+ <Text
1947
+ style={{
1948
+ color: 'rgba(255,0,0,0.7)',
1949
+ fontSize: 12,
1950
+ textAlign: 'center',
1951
+ }}
1952
+ >
1953
+ {message}
1954
+ </Text>
1955
+ </View>
1956
+ );
1957
+ }