flowboard-react 0.1.0 → 0.3.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 (39) hide show
  1. package/README.md +3 -2
  2. package/lib/module/Flowboard.js +3 -2
  3. package/lib/module/Flowboard.js.map +1 -1
  4. package/lib/module/FlowboardProvider.js +18 -14
  5. package/lib/module/FlowboardProvider.js.map +1 -1
  6. package/lib/module/components/FlowboardFlow.js +70 -37
  7. package/lib/module/components/FlowboardFlow.js.map +1 -1
  8. package/lib/module/components/FlowboardRenderer.js +622 -105
  9. package/lib/module/components/FlowboardRenderer.js.map +1 -1
  10. package/lib/module/core/assetPreloader.js +20 -18
  11. package/lib/module/core/assetPreloader.js.map +1 -1
  12. package/lib/module/index.js +1 -0
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/module/types/react-native-peers.d.js +2 -0
  15. package/lib/module/types/react-native-peers.d.js.map +1 -0
  16. package/lib/module/utils/flowboardUtils.js +20 -14
  17. package/lib/module/utils/flowboardUtils.js.map +1 -1
  18. package/lib/typescript/src/Flowboard.d.ts.map +1 -1
  19. package/lib/typescript/src/FlowboardProvider.d.ts.map +1 -1
  20. package/lib/typescript/src/components/FlowboardFlow.d.ts +1 -2
  21. package/lib/typescript/src/components/FlowboardFlow.d.ts.map +1 -1
  22. package/lib/typescript/src/components/FlowboardRenderer.d.ts.map +1 -1
  23. package/lib/typescript/src/core/assetPreloader.d.ts.map +1 -1
  24. package/lib/typescript/src/index.d.ts +2 -0
  25. package/lib/typescript/src/index.d.ts.map +1 -1
  26. package/lib/typescript/src/types/flowboard.d.ts +1 -0
  27. package/lib/typescript/src/types/flowboard.d.ts.map +1 -1
  28. package/lib/typescript/src/utils/flowboardUtils.d.ts +6 -0
  29. package/lib/typescript/src/utils/flowboardUtils.d.ts.map +1 -1
  30. package/package.json +17 -16
  31. package/src/Flowboard.ts +5 -2
  32. package/src/FlowboardProvider.tsx +20 -16
  33. package/src/components/FlowboardFlow.tsx +89 -49
  34. package/src/components/FlowboardRenderer.tsx +771 -98
  35. package/src/core/assetPreloader.ts +21 -32
  36. package/src/index.tsx +2 -0
  37. package/src/types/flowboard.ts +1 -0
  38. package/src/types/react-native-peers.d.ts +106 -0
  39. package/src/utils/flowboardUtils.ts +28 -14
@@ -27,6 +27,7 @@ import PagerView, {
27
27
  } from 'react-native-pager-view';
28
28
  import {
29
29
  insetsToStyle,
30
+ insetsToMarginStyle,
30
31
  parseAlignment,
31
32
  parseColor,
32
33
  parseCrossAlignment,
@@ -44,10 +45,13 @@ import { resolveFontAwesomeIcon, FontAwesome6 } from '../core/fontAwesome';
44
45
  import { useSliderRegistry } from './widgets/sliderRegistry';
45
46
 
46
47
  const styles = StyleSheet.create({
47
- root: { flex: 1 },
48
+ root: { flex: 1, backgroundColor: '#ffffff' },
48
49
  background: {
49
50
  ...StyleSheet.absoluteFillObject,
50
51
  },
52
+ whiteBg: {
53
+ backgroundColor: '#ffffff',
54
+ },
51
55
  safeArea: { flex: 1 },
52
56
  progressWrapper: { width: '100%' },
53
57
  });
@@ -89,12 +93,19 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
89
93
  const safeArea = screenData.safeArea !== false;
90
94
 
91
95
  const padding = parseInsets(screenData.padding);
92
- const paddingStyle = insetsToStyle(padding);
96
+ const contentPaddingStyle = insetsToStyle(padding);
97
+ const progressPaddingStyle = {
98
+ paddingTop: padding.top,
99
+ paddingRight: padding.right,
100
+ paddingLeft: padding.left,
101
+ };
102
+ const rootCrossAxisAlignment =
103
+ screenData.crossAxisAlignment ?? screenData.crossAxis;
93
104
 
94
105
  const content = (
95
106
  <View style={{ flex: 1 }}>
96
107
  {showProgress && (
97
- <View style={[paddingStyle, { paddingBottom: 8 }]}>
108
+ <View style={[progressPaddingStyle, { paddingBottom: 8 }]}>
98
109
  {renderProgressBar(
99
110
  currentIndex,
100
111
  totalScreens,
@@ -109,7 +120,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
109
120
  {scrollable ? (
110
121
  <ScrollView
111
122
  contentContainerStyle={{
112
- ...paddingStyle,
123
+ ...contentPaddingStyle,
113
124
  paddingTop: showProgress ? 0 : padding.top,
114
125
  }}
115
126
  >
@@ -117,7 +128,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
117
128
  style={{
118
129
  flexGrow: 1,
119
130
  justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
120
- alignItems: parseCrossAlignment(screenData.crossAxisAlignment),
131
+ alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
121
132
  }}
122
133
  >
123
134
  {childrenData.map((child, index) =>
@@ -137,10 +148,10 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
137
148
  <View
138
149
  style={{
139
150
  flex: 1,
140
- ...paddingStyle,
151
+ ...contentPaddingStyle,
141
152
  paddingTop: showProgress ? 0 : padding.top,
142
153
  justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
143
- alignItems: parseCrossAlignment(screenData.crossAxisAlignment),
154
+ alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
144
155
  }}
145
156
  >
146
157
  {childrenData.map((child, index) =>
@@ -163,7 +174,13 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
163
174
  <View style={styles.root}>
164
175
  {renderBackground(backgroundData, bgColorCode)}
165
176
  {safeArea ? (
166
- <SafeAreaView style={styles.safeArea}>{content}</SafeAreaView>
177
+ <SafeAreaView
178
+ style={styles.safeArea}
179
+ mode="padding"
180
+ edges={['top', 'right', 'bottom', 'left']}
181
+ >
182
+ {content}
183
+ </SafeAreaView>
167
184
  ) : (
168
185
  content
169
186
  )}
@@ -173,11 +190,17 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
173
190
 
174
191
  function renderBackground(bgData: any, legacyColorCode?: string) {
175
192
  if (!bgData) {
193
+ const fallbackBackground = parseColor(legacyColorCode);
176
194
  return (
177
195
  <View
178
196
  style={[
179
197
  styles.background,
180
- { backgroundColor: parseColor(legacyColorCode) },
198
+ {
199
+ backgroundColor:
200
+ fallbackBackground === 'transparent'
201
+ ? 'rgba(255,255,255,1)'
202
+ : fallbackBackground,
203
+ },
181
204
  ]}
182
205
  />
183
206
  );
@@ -210,25 +233,17 @@ function renderBackground(bgData: any, legacyColorCode?: string) {
210
233
  }
211
234
  case 'image': {
212
235
  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
- );
236
+ if (!hasImageUri(url)) {
237
+ return <View style={[styles.background, styles.whiteBg]} />;
238
+ }
239
+ return <BackgroundImageLayer uri={String(url)} fit={fit} />;
221
240
  }
222
241
  case 'asset': {
223
242
  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
- );
243
+ if (!hasImageUri(path)) {
244
+ return <View style={[styles.background, styles.whiteBg]} />;
245
+ }
246
+ return <BackgroundImageLayer uri={String(path)} fit={fit} />;
232
247
  }
233
248
  case 'color': {
234
249
  return (
@@ -248,6 +263,46 @@ function renderBackground(bgData: any, legacyColorCode?: string) {
248
263
  return <View style={[styles.background, { backgroundColor: '#ffffff' }]} />;
249
264
  }
250
265
 
266
+ function BackgroundImageLayer({ uri, fit }: { uri: string; fit: any }) {
267
+ const [hasError, setHasError] = React.useState(false);
268
+ const opacity = React.useRef(new Animated.Value(0)).current;
269
+
270
+ React.useEffect(() => {
271
+ opacity.setValue(0);
272
+ }, [uri, opacity]);
273
+
274
+ const fadeIn = React.useCallback(() => {
275
+ Animated.timing(opacity, {
276
+ toValue: 1,
277
+ duration: 160,
278
+ useNativeDriver: true,
279
+ }).start();
280
+ }, [opacity]);
281
+
282
+ if (hasError) {
283
+ return <View style={[styles.background, styles.whiteBg]} />;
284
+ }
285
+ return (
286
+ <Animated.Image
287
+ source={{ uri }}
288
+ style={[styles.background, { opacity }]}
289
+ resizeMode={fit}
290
+ onLoad={fadeIn}
291
+ onError={() => setHasError(true)}
292
+ />
293
+ );
294
+ }
295
+
296
+ function hasImageUri(value: unknown): value is string {
297
+ if (typeof value !== 'string') return false;
298
+ return value.trim().length > 0;
299
+ }
300
+
301
+ function isNetworkLikeUri(value: unknown): value is string {
302
+ if (!hasImageUri(value)) return false;
303
+ return /^(https?:|file:|content:|data:)/i.test(value.trim());
304
+ }
305
+
251
306
  function renderProgressBar(
252
307
  currentIndex: number,
253
308
  totalScreens: number,
@@ -377,8 +432,8 @@ function buildWidget(
377
432
  }
378
433
  case 'container': {
379
434
  const gradient = parseGradient(props.background);
380
- const width = normalizeDimension(parseDimension(props.width));
381
- const height = normalizeDimension(parseDimension(props.height));
435
+ const width = normalizeDimension(parseLayoutDimension(props.width));
436
+ const height = normalizeDimension(parseLayoutDimension(props.height));
382
437
  const borderRadius = props.borderRadius
383
438
  ? Number(props.borderRadius)
384
439
  : undefined;
@@ -504,8 +559,10 @@ function buildWidget(
504
559
  case 'button': {
505
560
  const gradient = parseGradient(props.background);
506
561
  const borderRadius = Number(props.borderRadius ?? 10);
507
- const width = normalizeDimension(parseDimension(props.width)) ?? '100%';
508
- const height = normalizeDimension(parseDimension(props.height)) ?? 50;
562
+ const width =
563
+ normalizeDimension(parseLayoutDimension(props.width)) ?? '100%';
564
+ const height =
565
+ normalizeDimension(parseLayoutDimension(props.height)) ?? 50;
509
566
  const label = props.label ?? 'Button';
510
567
  const textStyle = getTextStyle(
511
568
  { ...props, height: null },
@@ -583,8 +640,8 @@ function buildWidget(
583
640
  node = (
584
641
  <View
585
642
  style={{
586
- height: normalizeDimension(parseDimension(props.height)),
587
- width: normalizeDimension(parseDimension(props.width)),
643
+ height: normalizeDimension(parseLayoutDimension(props.height)),
644
+ width: normalizeDimension(parseLayoutDimension(props.width)),
588
645
  }}
589
646
  />
590
647
  );
@@ -594,10 +651,12 @@ function buildWidget(
594
651
  const expandedChild = childJson
595
652
  ? buildWidget(childJson, { ...params, allowFlexExpansion: true })
596
653
  : null;
597
- if (params.allowFlexExpansion) {
598
- return <View style={{ flex: 1 }}>{expandedChild}</View>;
599
- }
600
- return expandedChild;
654
+ node = params.allowFlexExpansion ? (
655
+ <View style={{ flex: 1 }}>{expandedChild}</View>
656
+ ) : (
657
+ expandedChild
658
+ );
659
+ break;
601
660
  }
602
661
  case 'padding': {
603
662
  const padding = parseInsets(props.padding ?? 0);
@@ -633,7 +692,62 @@ function buildWidget(
633
692
  params.onInputChange(id, value);
634
693
  }
635
694
  }}
636
- onAction={params.onAction}
695
+ onAction={(actionName, payload) => {
696
+ if (
697
+ actionName === 'next' &&
698
+ params.onInputChange &&
699
+ id &&
700
+ Object.prototype.hasOwnProperty.call(
701
+ payload ?? {},
702
+ 'selectedValue'
703
+ )
704
+ ) {
705
+ params.onInputChange(id, payload?.selectedValue);
706
+ }
707
+
708
+ params.onAction(actionName, {
709
+ ...(payload ?? {}),
710
+ ...(id ? { fieldId: id } : {}),
711
+ });
712
+ }}
713
+ buildWidget={(widgetJson) =>
714
+ buildWidget(widgetJson, { ...params, allowFlexExpansion: true })
715
+ }
716
+ />
717
+ );
718
+ break;
719
+ }
720
+ case 'wheel_picker': {
721
+ node = (
722
+ <WheelPicker
723
+ properties={props}
724
+ options={Array.isArray(json.options) ? json.options : []}
725
+ itemTemplate={json.itemTemplate ?? props.itemTemplate}
726
+ formData={params.formData}
727
+ initialValue={params.formData[id ?? '']}
728
+ onChanged={(value) => {
729
+ if (params.onInputChange && id) {
730
+ params.onInputChange(id, value);
731
+ }
732
+ }}
733
+ onAction={(actionName, payload) => {
734
+ if (
735
+ actionName === 'next' &&
736
+ params.onInputChange &&
737
+ id &&
738
+ Object.prototype.hasOwnProperty.call(
739
+ payload ?? {},
740
+ 'selectedValue'
741
+ )
742
+ ) {
743
+ params.onInputChange(id, payload?.selectedValue);
744
+ }
745
+
746
+ params.onAction(actionName, {
747
+ ...(payload ?? {}),
748
+ ...(id ? { fieldId: id } : {}),
749
+ });
750
+ }}
637
751
  buildWidget={(widgetJson) =>
638
752
  buildWidget(widgetJson, { ...params, allowFlexExpansion: true })
639
753
  }
@@ -643,23 +757,28 @@ function buildWidget(
643
757
  }
644
758
  case 'image': {
645
759
  const source = props.source;
646
- const width = normalizeDimension(parseDimension(props.width, true));
647
- const height = normalizeDimension(parseDimension(props.height, true));
760
+ const width = normalizeDimension(parseLayoutDimension(props.width, true));
761
+ const height = normalizeDimension(
762
+ parseLayoutDimension(props.height, true)
763
+ );
648
764
  if (source === 'asset') {
765
+ if (!hasImageUri(props.path)) return null;
649
766
  node = (
650
- <Image
651
- source={{ uri: props.path }}
652
- style={{ width, height }}
653
- resizeMode={parseResizeMode(props.fit)}
767
+ <SduiImage
768
+ uri={String(props.path)}
769
+ fit={parseResizeMode(props.fit)}
770
+ width={width}
771
+ height={height}
654
772
  />
655
773
  );
656
774
  } else if (source === 'network') {
657
- if (!props.url) return null;
775
+ if (!isNetworkLikeUri(props.url)) return null;
658
776
  node = (
659
- <Image
660
- source={{ uri: props.url }}
661
- style={{ width, height }}
662
- resizeMode={parseResizeMode(props.fit)}
777
+ <SduiImage
778
+ uri={String(props.url)}
779
+ fit={parseResizeMode(props.fit)}
780
+ width={width}
781
+ height={height}
663
782
  />
664
783
  );
665
784
  } else {
@@ -669,8 +788,10 @@ function buildWidget(
669
788
  }
670
789
  case 'lottie': {
671
790
  const source = props.source;
672
- const width = normalizeDimension(parseDimension(props.width, true));
673
- const height = normalizeDimension(parseDimension(props.height, true));
791
+ const width = normalizeDimension(parseLayoutDimension(props.width, true));
792
+ const height = normalizeDimension(
793
+ parseLayoutDimension(props.height, true)
794
+ );
674
795
  const loop = props.loop === true;
675
796
  if (source === 'asset') {
676
797
  node = (
@@ -758,7 +879,7 @@ function buildWidget(
758
879
  autoPlay={props.autoPlay === true}
759
880
  autoPlayDelay={Number(props.autoPlayDelay ?? 3000)}
760
881
  loop={props.loop === true}
761
- height={parseDimension(props.height)}
882
+ height={parseLayoutDimension(props.height)}
762
883
  physics={props.physics}
763
884
  >
764
885
  {(childrenJson ?? []).map((child, index) => (
@@ -836,13 +957,13 @@ function buildWidget(
836
957
  break;
837
958
  }
838
959
  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 (
960
+ const left = parseLayoutDimension(props.left);
961
+ const top = parseLayoutDimension(props.top);
962
+ const right = parseLayoutDimension(props.right);
963
+ const bottom = parseLayoutDimension(props.bottom);
964
+ const width = parseLayoutDimension(props.width);
965
+ const height = parseLayoutDimension(props.height);
966
+ node = (
846
967
  <View
847
968
  style={{
848
969
  position: 'absolute',
@@ -857,6 +978,7 @@ function buildWidget(
857
978
  {childJson ? buildWidget(childJson, { ...params }) : null}
858
979
  </View>
859
980
  );
981
+ break;
860
982
  }
861
983
  default:
862
984
  node = null;
@@ -867,7 +989,7 @@ function buildWidget(
867
989
  const marginVal = props.margin;
868
990
  if (marginVal !== undefined && marginVal !== null) {
869
991
  const marginInsets = parseInsets(marginVal);
870
- node = <View style={insetsToStyle(marginInsets)}>{node}</View>;
992
+ node = <View style={insetsToMarginStyle(marginInsets)}>{node}</View>;
871
993
  } else if (type !== 'container' && type !== 'padding' && props.padding) {
872
994
  const paddingInsets = parseInsets(props.padding);
873
995
  node = <View style={insetsToStyle(paddingInsets)}>{node}</View>;
@@ -879,14 +1001,22 @@ function buildWidget(
879
1001
  shouldExpand = true;
880
1002
  }
881
1003
  if (props.height !== undefined) {
882
- const heightVal = parseDimension(props.height);
1004
+ const heightVal = parseLayoutDimension(props.height);
883
1005
  if (heightVal === Number.POSITIVE_INFINITY) {
884
1006
  shouldExpand = true;
885
1007
  }
886
1008
  }
887
1009
 
888
1010
  if (shouldExpand) {
889
- return <View style={{ flex: 1 }}>{node}</View>;
1011
+ node = <View style={{ flex: 1 }}>{node}</View>;
1012
+ }
1013
+ }
1014
+
1015
+ if (params.key !== undefined && params.key !== null) {
1016
+ if (React.isValidElement(node)) {
1017
+ node = React.cloneElement(node, { key: params.key });
1018
+ } else {
1019
+ node = <React.Fragment key={params.key}>{node}</React.Fragment>;
890
1020
  }
891
1021
  }
892
1022
 
@@ -992,7 +1122,7 @@ function withOpacity(color: string, opacity: number): string {
992
1122
  }
993
1123
 
994
1124
  function normalizeDimension(
995
- value: number | undefined
1125
+ value: number | string | undefined
996
1126
  ): number | string | undefined {
997
1127
  if (value === Number.POSITIVE_INFINITY) {
998
1128
  return '100%';
@@ -1000,6 +1130,126 @@ function normalizeDimension(
1000
1130
  return value;
1001
1131
  }
1002
1132
 
1133
+ function parseRootCrossAlignment(value?: string): any {
1134
+ if (!value) return 'stretch';
1135
+ return parseCrossAlignment(value);
1136
+ }
1137
+
1138
+ function parseLayoutDimension(
1139
+ value: any,
1140
+ zeroIsNull = false
1141
+ ): number | string | undefined {
1142
+ if (typeof value === 'string') {
1143
+ const trimmed = value.trim();
1144
+ const lowered = trimmed.toLowerCase();
1145
+
1146
+ if (
1147
+ lowered === 'double.infinity' ||
1148
+ lowered === 'infinity' ||
1149
+ lowered === '+infinity'
1150
+ ) {
1151
+ return Number.POSITIVE_INFINITY;
1152
+ }
1153
+
1154
+ if (trimmed.endsWith('%')) {
1155
+ const numeric = Number(trimmed.slice(0, -1));
1156
+ if (!Number.isNaN(numeric) && Number.isFinite(numeric)) {
1157
+ if (zeroIsNull && numeric === 0) return undefined;
1158
+ return `${numeric}%`;
1159
+ }
1160
+ }
1161
+ }
1162
+
1163
+ return parseDimension(value, zeroIsNull);
1164
+ }
1165
+
1166
+ function SduiImage({
1167
+ uri,
1168
+ fit,
1169
+ width,
1170
+ height,
1171
+ }: {
1172
+ uri: string;
1173
+ fit: any;
1174
+ width: number | string | undefined;
1175
+ height: number | string | undefined;
1176
+ }) {
1177
+ const hasExplicitWidth = width !== undefined;
1178
+ const hasExplicitHeight = height !== undefined;
1179
+ const [aspectRatio, setAspectRatio] = React.useState<number | undefined>(
1180
+ undefined
1181
+ );
1182
+ const [hasError, setHasError] = React.useState(false);
1183
+ const opacity = React.useRef(new Animated.Value(0)).current;
1184
+
1185
+ const updateAspectRatio = (w?: number, h?: number) => {
1186
+ if (!w || !h) return;
1187
+ if (!Number.isFinite(w) || !Number.isFinite(h) || h <= 0) return;
1188
+ const ratio = w / h;
1189
+ if (Number.isFinite(ratio) && ratio > 0) {
1190
+ setAspectRatio(ratio);
1191
+ }
1192
+ };
1193
+
1194
+ React.useEffect(() => {
1195
+ if (hasExplicitWidth && hasExplicitHeight) return;
1196
+ Image.getSize(
1197
+ uri,
1198
+ (w, h) => updateAspectRatio(w, h),
1199
+ () => null
1200
+ );
1201
+ }, [uri, hasExplicitWidth, hasExplicitHeight]);
1202
+
1203
+ React.useEffect(() => {
1204
+ opacity.setValue(0);
1205
+ }, [uri, opacity]);
1206
+
1207
+ if (hasError) {
1208
+ return null;
1209
+ }
1210
+
1211
+ const style: any = { width, height };
1212
+
1213
+ if (!hasExplicitWidth && !hasExplicitHeight) {
1214
+ style.width = '100%';
1215
+ if (aspectRatio) {
1216
+ style.aspectRatio = aspectRatio;
1217
+ } else {
1218
+ style.minHeight = 160;
1219
+ }
1220
+ } else if (hasExplicitWidth && !hasExplicitHeight) {
1221
+ if (aspectRatio) {
1222
+ style.aspectRatio = aspectRatio;
1223
+ } else {
1224
+ style.minHeight = 100;
1225
+ }
1226
+ } else if (!hasExplicitWidth && hasExplicitHeight) {
1227
+ if (aspectRatio) {
1228
+ style.aspectRatio = aspectRatio;
1229
+ } else {
1230
+ style.width = '100%';
1231
+ }
1232
+ }
1233
+
1234
+ return (
1235
+ <Animated.Image
1236
+ source={{ uri }}
1237
+ style={[style, { opacity }]}
1238
+ resizeMode={fit}
1239
+ onLoad={(event) => {
1240
+ const source = event?.nativeEvent?.source;
1241
+ updateAspectRatio(source?.width, source?.height);
1242
+ Animated.timing(opacity, {
1243
+ toValue: 1,
1244
+ duration: 160,
1245
+ useNativeDriver: true,
1246
+ }).start();
1247
+ }}
1248
+ onError={() => setHasError(true)}
1249
+ />
1250
+ );
1251
+ }
1252
+
1003
1253
  function GradientText({
1004
1254
  text,
1005
1255
  gradient,
@@ -1083,7 +1333,7 @@ function DynamicInput({
1083
1333
  {mask ? (
1084
1334
  <MaskInput
1085
1335
  value={value}
1086
- onChangeText={(_masked, unmasked) => {
1336
+ onChangeText={(_masked: string, unmasked: string) => {
1087
1337
  setValue(unmasked);
1088
1338
  onChanged(unmasked);
1089
1339
  }}
@@ -1203,7 +1453,9 @@ function SelectionList({
1203
1453
  next = [value];
1204
1454
  onChanged(value);
1205
1455
  if (properties.autoGoNext === true) {
1206
- onAction('next');
1456
+ requestAnimationFrame(() =>
1457
+ onAction('next', { selectedValue: value })
1458
+ );
1207
1459
  }
1208
1460
  }
1209
1461
  return next;
@@ -1291,6 +1543,364 @@ function SelectionList({
1291
1543
  );
1292
1544
  }
1293
1545
 
1546
+ function WheelPicker({
1547
+ properties,
1548
+ options,
1549
+ itemTemplate,
1550
+ formData,
1551
+ initialValue,
1552
+ onChanged,
1553
+ onAction,
1554
+ buildWidget,
1555
+ }: {
1556
+ properties: Record<string, any>;
1557
+ options: Record<string, any>[];
1558
+ itemTemplate: unknown;
1559
+ formData: Record<string, any>;
1560
+ initialValue: any;
1561
+ onChanged: (value: string) => void;
1562
+ onAction: (action: string, data?: Record<string, any>) => void;
1563
+ buildWidget: (widgetJson: Record<string, any>) => React.ReactNode;
1564
+ }) {
1565
+ const multiSelect = properties.multiSelect === true;
1566
+ const [selectedValues, setSelectedValues] = React.useState<string[]>(() => {
1567
+ if (initialValue === null || initialValue === undefined) return [];
1568
+ if (Array.isArray(initialValue)) return initialValue.map(String);
1569
+ if (typeof initialValue === 'string') {
1570
+ if (multiSelect) {
1571
+ try {
1572
+ return initialValue
1573
+ .replace('[', '')
1574
+ .replace(']', '')
1575
+ .split(',')
1576
+ .map((val) => val.trim())
1577
+ .filter((val) => val.length > 0);
1578
+ } catch {
1579
+ return [initialValue];
1580
+ }
1581
+ }
1582
+ return [initialValue];
1583
+ }
1584
+ return [];
1585
+ });
1586
+
1587
+ const resolvedOptions = React.useMemo(
1588
+ () =>
1589
+ buildWheelPickerOptions({
1590
+ options,
1591
+ properties,
1592
+ formData,
1593
+ itemTemplate,
1594
+ }),
1595
+ [formData, itemTemplate, options, properties]
1596
+ );
1597
+
1598
+ const toggleSelection = (value: string) => {
1599
+ setSelectedValues((prev) => {
1600
+ let next = [...prev];
1601
+ if (multiSelect) {
1602
+ if (next.includes(value)) {
1603
+ next = next.filter((item) => item !== value);
1604
+ } else {
1605
+ next.push(value);
1606
+ }
1607
+ onChanged(next.join(','));
1608
+ } else {
1609
+ next = [value];
1610
+ onChanged(value);
1611
+ if (properties.autoGoNext === true) {
1612
+ requestAnimationFrame(() =>
1613
+ onAction('next', { selectedValue: value })
1614
+ );
1615
+ }
1616
+ }
1617
+ return next;
1618
+ });
1619
+ };
1620
+
1621
+ const layout = properties.layout ?? 'wheel';
1622
+ const spacing = Number(properties.spacing ?? 8);
1623
+
1624
+ const renderItem = (option: Record<string, any>, index: number) => {
1625
+ const value = String(option.value ?? '');
1626
+ const isSelected = selectedValues.includes(value);
1627
+ const styleProps = isSelected
1628
+ ? option.selectedStyle ??
1629
+ properties.selectedStyle ?? {
1630
+ backgroundColor: properties.wheelSelectedBackgroundColor,
1631
+ borderColor: properties.wheelSelectedBorderColor,
1632
+ }
1633
+ : option.unselectedStyle ??
1634
+ properties.unselectedStyle ?? {
1635
+ backgroundColor: properties.wheelUnselectedBackgroundColor,
1636
+ borderColor: properties.wheelUnselectedBorderColor,
1637
+ };
1638
+
1639
+ const backgroundColor = parseColor(styleProps.backgroundColor);
1640
+ const borderColor = parseColor(styleProps.borderColor);
1641
+ const borderRadius = Number(styleProps.borderRadius ?? 8);
1642
+ const borderWidth = Number(styleProps.borderWidth ?? 1);
1643
+
1644
+ return (
1645
+ <Pressable
1646
+ key={`wheel-option-${index}`}
1647
+ onPress={() => toggleSelection(value)}
1648
+ style={{
1649
+ width: '100%',
1650
+ backgroundColor,
1651
+ borderColor,
1652
+ borderWidth,
1653
+ borderRadius,
1654
+ padding: 0,
1655
+ }}
1656
+ >
1657
+ {buildWidget(option.child)}
1658
+ </Pressable>
1659
+ );
1660
+ };
1661
+
1662
+ if (layout === 'row') {
1663
+ return (
1664
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
1665
+ {resolvedOptions.map((option, index) => (
1666
+ <View
1667
+ key={`wheel-row-option-${index}`}
1668
+ style={{ marginRight: spacing, marginBottom: spacing }}
1669
+ >
1670
+ {renderItem(option, index)}
1671
+ </View>
1672
+ ))}
1673
+ </View>
1674
+ );
1675
+ }
1676
+
1677
+ if (layout === 'grid') {
1678
+ const crossAxisCount = Number(properties.gridCrossAxisCount ?? 2);
1679
+ const aspectRatio = Number(properties.gridAspectRatio ?? 1);
1680
+ return (
1681
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
1682
+ {resolvedOptions.map((option, index) => (
1683
+ <View
1684
+ key={`wheel-grid-option-${index}`}
1685
+ style={{
1686
+ width: `${100 / crossAxisCount}%`,
1687
+ paddingRight: (index + 1) % crossAxisCount === 0 ? 0 : spacing,
1688
+ paddingBottom: spacing,
1689
+ aspectRatio,
1690
+ }}
1691
+ >
1692
+ {renderItem(option, index)}
1693
+ </View>
1694
+ ))}
1695
+ </View>
1696
+ );
1697
+ }
1698
+
1699
+ if (layout === 'wheel') {
1700
+ const itemHeight = Math.max(1, Number(properties.wheelItemHeight ?? 48));
1701
+ const requestedVisible = Math.max(
1702
+ 3,
1703
+ Math.floor(Number(properties.visibleItems ?? 5))
1704
+ );
1705
+ const visibleItems =
1706
+ requestedVisible % 2 === 0 ? requestedVisible + 1 : requestedVisible;
1707
+ const containerHeight = itemHeight * visibleItems;
1708
+ const centerPadding = Math.max(0, (containerHeight - itemHeight) / 2);
1709
+
1710
+ return (
1711
+ <View style={{ height: containerHeight }}>
1712
+ <ScrollView
1713
+ showsVerticalScrollIndicator={false}
1714
+ contentContainerStyle={{ paddingVertical: centerPadding }}
1715
+ >
1716
+ {resolvedOptions.map((option, index) => (
1717
+ <View
1718
+ key={`wheel-column-option-${index}`}
1719
+ style={{
1720
+ minHeight: itemHeight,
1721
+ justifyContent: 'center',
1722
+ marginBottom:
1723
+ spacing > 0 && index < resolvedOptions.length - 1 ? spacing : 0,
1724
+ }}
1725
+ >
1726
+ {renderItem(option, index)}
1727
+ </View>
1728
+ ))}
1729
+ </ScrollView>
1730
+ </View>
1731
+ );
1732
+ }
1733
+
1734
+ return (
1735
+ <View>
1736
+ {resolvedOptions.map((option, index) => (
1737
+ <View key={`wheel-list-option-${index}`} style={{ marginBottom: spacing }}>
1738
+ {renderItem(option, index)}
1739
+ </View>
1740
+ ))}
1741
+ </View>
1742
+ );
1743
+ }
1744
+
1745
+ function buildWheelPickerOptions({
1746
+ options,
1747
+ properties,
1748
+ formData,
1749
+ itemTemplate,
1750
+ }: {
1751
+ options: Record<string, any>[];
1752
+ properties: Record<string, any>;
1753
+ formData: Record<string, any>;
1754
+ itemTemplate: unknown;
1755
+ }): Record<string, any>[] {
1756
+ const templateContext = buildWheelTemplateBaseContext(formData, properties);
1757
+ const itemCountRaw = properties.itemCount ?? properties.item_count;
1758
+ const itemCountResolved = resolveNumericValue(itemCountRaw, templateContext);
1759
+ const itemCount = normalizeItemCount(itemCountResolved);
1760
+
1761
+ if (itemCount > 0) {
1762
+ const generatedTemplate = normalizeWheelOptionTemplate(
1763
+ itemTemplate ?? properties.itemTemplate ?? options[0]
1764
+ );
1765
+ if (!generatedTemplate) return [];
1766
+
1767
+ const start =
1768
+ resolveNumericValue(
1769
+ properties.start ?? properties.itemStart ?? 0,
1770
+ templateContext
1771
+ ) ?? 0;
1772
+ const step =
1773
+ resolveNumericValue(
1774
+ properties.step ?? properties.itemStep ?? 1,
1775
+ templateContext
1776
+ ) ?? 1;
1777
+
1778
+ return Array.from({ length: itemCount }).map((_, index) => {
1779
+ const itemValue = start + step * index;
1780
+ const context = {
1781
+ ...templateContext,
1782
+ start,
1783
+ step,
1784
+ index,
1785
+ item: itemValue,
1786
+ };
1787
+ const resolvedOption = resolveWheelTemplateValue(
1788
+ generatedTemplate,
1789
+ context
1790
+ );
1791
+ return ensureWheelOptionShape(resolvedOption, String(itemValue));
1792
+ });
1793
+ }
1794
+
1795
+ return options.map((option, index) => {
1796
+ const itemValue = option?.value ?? index;
1797
+ const context = {
1798
+ ...templateContext,
1799
+ index,
1800
+ item: itemValue,
1801
+ };
1802
+ const resolvedOption = resolveWheelTemplateValue(option, context);
1803
+ return ensureWheelOptionShape(resolvedOption, String(itemValue));
1804
+ });
1805
+ }
1806
+
1807
+ function normalizeItemCount(value: number | null): number {
1808
+ if (value === null || !Number.isFinite(value)) return 0;
1809
+ return Math.max(0, Math.floor(value));
1810
+ }
1811
+
1812
+ function buildWheelTemplateBaseContext(
1813
+ formData: Record<string, any>,
1814
+ properties: Record<string, any>
1815
+ ): Record<string, any> {
1816
+ const context = { ...formData };
1817
+ const start = resolveNumericValue(
1818
+ properties.start ?? properties.itemStart,
1819
+ context
1820
+ );
1821
+ const step = resolveNumericValue(properties.step ?? properties.itemStep, context);
1822
+
1823
+ if (start !== null) context.start = start;
1824
+ if (step !== null) context.step = step;
1825
+ return context;
1826
+ }
1827
+
1828
+ function normalizeWheelOptionTemplate(
1829
+ template: unknown
1830
+ ): Record<string, any> | null {
1831
+ if (!template || typeof template !== 'object') return null;
1832
+ const asMap = template as Record<string, any>;
1833
+ if (
1834
+ Object.prototype.hasOwnProperty.call(asMap, 'value') ||
1835
+ Object.prototype.hasOwnProperty.call(asMap, 'child')
1836
+ ) {
1837
+ return { ...asMap };
1838
+ }
1839
+ if (typeof asMap.type === 'string') {
1840
+ return {
1841
+ value: '{{item}}',
1842
+ child: asMap,
1843
+ };
1844
+ }
1845
+ return null;
1846
+ }
1847
+
1848
+ function ensureWheelOptionShape(
1849
+ option: unknown,
1850
+ fallbackValue: string
1851
+ ): Record<string, any> {
1852
+ if (!option || typeof option !== 'object') {
1853
+ return {
1854
+ value: fallbackValue,
1855
+ child: {
1856
+ type: 'text',
1857
+ properties: { text: fallbackValue },
1858
+ },
1859
+ };
1860
+ }
1861
+
1862
+ const normalized = { ...(option as Record<string, any>) };
1863
+ const rawValue = normalized.value ?? fallbackValue;
1864
+ const value = String(rawValue);
1865
+
1866
+ const child =
1867
+ normalized.child && typeof normalized.child === 'object'
1868
+ ? normalized.child
1869
+ : {
1870
+ type: 'text',
1871
+ properties: { text: value },
1872
+ };
1873
+
1874
+ return {
1875
+ ...normalized,
1876
+ value,
1877
+ child,
1878
+ };
1879
+ }
1880
+
1881
+ function resolveWheelTemplateValue(
1882
+ value: unknown,
1883
+ context: Record<string, any>
1884
+ ): unknown {
1885
+ if (typeof value === 'string') {
1886
+ return resolveText(value, context);
1887
+ }
1888
+ if (Array.isArray(value)) {
1889
+ return value.map((entry) => resolveWheelTemplateValue(entry, context));
1890
+ }
1891
+ if (value && typeof value === 'object') {
1892
+ const result: Record<string, any> = {};
1893
+ Object.keys(value as Record<string, any>).forEach((key) => {
1894
+ result[key] = resolveWheelTemplateValue(
1895
+ (value as Record<string, any>)[key],
1896
+ context
1897
+ );
1898
+ });
1899
+ return result;
1900
+ }
1901
+ return value;
1902
+ }
1903
+
1294
1904
  function FakeProgressBar({
1295
1905
  progressColor,
1296
1906
  backgroundColor,
@@ -1436,38 +2046,42 @@ function SliderWidget({
1436
2046
  autoPlay: boolean;
1437
2047
  autoPlayDelay: number;
1438
2048
  loop: boolean;
1439
- height?: number;
2049
+ height?: number | string;
1440
2050
  physics?: string;
1441
2051
  children: React.ReactNode[];
1442
2052
  }) {
1443
2053
  const registry = useSliderRegistry();
1444
2054
  const pagerRef = React.useRef<PagerView>(null);
1445
2055
  const pageLength = children.length;
1446
- const [currentPage, setCurrentPage] = React.useState(() =>
1447
- loop && pageLength > 0 ? pageLength * 1000 : 0
1448
- );
2056
+ const pageCount = pageLength;
2057
+ const [currentPage, setCurrentPage] = React.useState(0);
1449
2058
  const timerRef = React.useRef<NodeJS.Timeout | null>(null);
1450
2059
 
1451
2060
  React.useEffect(() => {
1452
- if (autoPlay && !linkedTo) {
2061
+ setCurrentPage((prev) => clampSliderPage(prev, pageCount));
2062
+ }, [pageCount]);
2063
+
2064
+ React.useEffect(() => {
2065
+ if (autoPlay && !linkedTo && pageLength > 0) {
1453
2066
  timerRef.current = setInterval(() => {
1454
2067
  setCurrentPage((prev) => {
1455
- if (loop) return prev + 1;
1456
- if (prev < children.length - 1) return prev + 1;
1457
- return 0;
2068
+ const safePrev = clampSliderPage(prev, pageCount);
2069
+ if (safePrev < pageLength - 1) return safePrev + 1;
2070
+ if (loop) return 0;
2071
+ return safePrev;
1458
2072
  });
1459
2073
  }, autoPlayDelay);
1460
2074
  }
1461
2075
  return () => {
1462
2076
  if (timerRef.current) clearInterval(timerRef.current);
1463
2077
  };
1464
- }, [autoPlay, autoPlayDelay, loop, linkedTo, children.length]);
2078
+ }, [autoPlay, autoPlayDelay, linkedTo, loop, pageCount, pageLength]);
1465
2079
 
1466
2080
  React.useEffect(() => {
1467
- if (pagerRef.current) {
1468
- pagerRef.current.setPage(currentPage);
2081
+ if (pagerRef.current && pageCount > 0) {
2082
+ pagerRef.current.setPage(clampSliderPage(currentPage, pageCount));
1469
2083
  }
1470
- }, [currentPage]);
2084
+ }, [currentPage, pageCount]);
1471
2085
 
1472
2086
  React.useEffect(() => {
1473
2087
  if (registry && id && children.length > 0) {
@@ -1478,28 +2092,30 @@ function SliderWidget({
1478
2092
  React.useEffect(() => {
1479
2093
  if (!linkedTo || !registry) return;
1480
2094
  return registry.getNotifier(linkedTo, (value) => {
2095
+ const normalized = normalizeLoopIndex(value, pageLength);
2096
+ const targetPage = clampSliderPage(normalized, pageCount);
2097
+ setCurrentPage(targetPage);
1481
2098
  if (pagerRef.current) {
1482
- pagerRef.current.setPage(value);
2099
+ pagerRef.current.setPage(targetPage);
1483
2100
  }
1484
2101
  });
1485
- }, [linkedTo, registry]);
2102
+ }, [linkedTo, pageCount, pageLength, registry]);
1486
2103
 
1487
2104
  if (pageLength === 0) return null;
1488
2105
 
1489
- const pageCount = loop ? pageLength * 1000 : pageLength;
1490
-
1491
2106
  return (
1492
2107
  <PagerView
1493
2108
  ref={pagerRef}
1494
- style={{ height: height ?? 200 }}
2109
+ style={height === undefined ? { flex: 1 } : { height }}
1495
2110
  orientation={direction}
1496
2111
  scrollEnabled={physics !== 'never'}
1497
- initialPage={currentPage}
2112
+ initialPage={clampSliderPage(currentPage, pageCount)}
1498
2113
  onPageSelected={(event: PagerViewOnPageSelectedEvent) => {
1499
2114
  const page = event.nativeEvent.position;
1500
- setCurrentPage(page);
2115
+ const safePage = clampSliderPage(page, pageCount);
2116
+ setCurrentPage(safePage);
1501
2117
  if (registry && id) {
1502
- registry.update(id, page % pageLength, pageLength);
2118
+ registry.update(id, safePage % pageLength, pageLength);
1503
2119
  }
1504
2120
  }}
1505
2121
  >
@@ -1512,6 +2128,20 @@ function SliderWidget({
1512
2128
  );
1513
2129
  }
1514
2130
 
2131
+ function clampSliderPage(page: number, pageCount: number): number {
2132
+ if (pageCount <= 0) return 0;
2133
+ if (!Number.isFinite(page)) return 0;
2134
+ if (page < 0) return 0;
2135
+ if (page >= pageCount) return pageCount - 1;
2136
+ return Math.floor(page);
2137
+ }
2138
+
2139
+ function normalizeLoopIndex(index: number, size: number): number {
2140
+ if (size <= 0 || !Number.isFinite(index)) return 0;
2141
+ const normalized = Math.floor(index) % size;
2142
+ return normalized < 0 ? normalized + size : normalized;
2143
+ }
2144
+
1515
2145
  function PageViewIndicator({
1516
2146
  linkedTo,
1517
2147
  activeColor,
@@ -1569,16 +2199,39 @@ function RadarChart({
1569
2199
  }) {
1570
2200
  const animate = properties.animate !== false;
1571
2201
  const animationDurationMs = Number(properties.animationDurationMs ?? 800);
1572
- const scale = useMemo(() => new Animated.Value(animate ? 0 : 1), [animate]);
2202
+ const reveal = useMemo(() => new Animated.Value(animate ? 0 : 1), [animate]);
2203
+ const [lineProgress, setLineProgress] = React.useState(animate ? 0 : 1);
1573
2204
 
1574
2205
  React.useEffect(() => {
1575
- if (!animate) return;
1576
- Animated.timing(scale, {
2206
+ if (!animate) {
2207
+ reveal.setValue(1);
2208
+ setLineProgress(1);
2209
+ return;
2210
+ }
2211
+
2212
+ reveal.setValue(0);
2213
+ setLineProgress(0);
2214
+
2215
+ const listenerId = reveal.addListener(({ value }) => {
2216
+ setLineProgress(Math.max(0, Math.min(1, value)));
2217
+ });
2218
+
2219
+ Animated.timing(reveal, {
1577
2220
  toValue: 1,
1578
2221
  duration: animationDurationMs,
1579
- useNativeDriver: true,
2222
+ useNativeDriver: false,
1580
2223
  }).start();
1581
- }, [animate, animationDurationMs, scale]);
2224
+
2225
+ return () => {
2226
+ reveal.removeListener(listenerId);
2227
+ };
2228
+ }, [animate, animationDurationMs, reveal]);
2229
+
2230
+ const parsedWidth = parseLayoutDimension(properties.width);
2231
+ const normalizedWidth = normalizeDimension(parsedWidth);
2232
+ const [measuredWidth, setMeasuredWidth] = React.useState(0);
2233
+ const width = resolveRadarWidth(parsedWidth, measuredWidth);
2234
+
1582
2235
  const axes = parseRadarAxes(properties.axes);
1583
2236
  if (axes.length < 3) {
1584
2237
  return radarPlaceholder('Radar chart requires at least 3 axes.');
@@ -1597,12 +2250,11 @@ function RadarChart({
1597
2250
 
1598
2251
  const normalizedAxes = normalizeAxisMaximums(axes, datasets);
1599
2252
  const backgroundColor = parseColor(properties.backgroundColor);
1600
- const width = parseDimension(properties.width) ?? 300;
1601
- const rawHeight = parseDimension(properties.height);
2253
+ const rawHeight = parseLayoutDimension(properties.height);
1602
2254
  const height =
1603
- rawHeight === undefined || rawHeight === Number.POSITIVE_INFINITY
1604
- ? 300
1605
- : rawHeight;
2255
+ typeof rawHeight === 'number' && Number.isFinite(rawHeight) && rawHeight > 0
2256
+ ? rawHeight
2257
+ : 300;
1606
2258
  const padding = parseInsets(properties.padding);
1607
2259
  const gridLevels = Math.min(
1608
2260
  Math.max(Number(properties.gridLevels ?? 5), 1),
@@ -1628,9 +2280,11 @@ function RadarChart({
1628
2280
  const shape = (properties.shape ?? 'polygon').toLowerCase();
1629
2281
 
1630
2282
  const chartSize = Math.min(width, height);
1631
- const radius =
2283
+ const radius = Math.max(
1632
2284
  chartSize / 2 -
1633
- Math.max(padding.left, padding.right, padding.top, padding.bottom);
2285
+ Math.max(padding.left, padding.right, padding.top, padding.bottom),
2286
+ 8
2287
+ );
1634
2288
  const center = { x: width / 2, y: height / 2 };
1635
2289
 
1636
2290
  const axisAngle = (Math.PI * 2) / axes.length;
@@ -1650,7 +2304,7 @@ function RadarChart({
1650
2304
  const points = dataset.values.map((value, index) => {
1651
2305
  const axis = normalizedAxes[index];
1652
2306
  const maxValue = axis?.maxValue ?? 0;
1653
- const ratio = maxValue > 0 ? value / maxValue : 0;
2307
+ const ratio = (maxValue > 0 ? value / maxValue : 0) * lineProgress;
1654
2308
  const angle = index * axisAngle - Math.PI / 2;
1655
2309
  const x = center.x + radius * ratio * Math.cos(angle);
1656
2310
  const y = center.y + radius * ratio * Math.sin(angle);
@@ -1689,13 +2343,19 @@ function RadarChart({
1689
2343
  const chart = (
1690
2344
  <Animated.View
1691
2345
  style={{
1692
- width,
2346
+ width: normalizedWidth ?? width,
1693
2347
  height,
1694
2348
  paddingLeft: padding.left,
1695
2349
  paddingRight: padding.right,
1696
2350
  paddingTop: padding.top,
1697
2351
  paddingBottom: padding.bottom,
1698
- transform: [{ scale }],
2352
+ opacity: reveal,
2353
+ }}
2354
+ onLayout={(event) => {
2355
+ const nextWidth = Math.floor(event.nativeEvent.layout.width);
2356
+ if (nextWidth > 0 && nextWidth !== measuredWidth) {
2357
+ setMeasuredWidth(nextWidth);
2358
+ }
1699
2359
  }}
1700
2360
  >
1701
2361
  <Svg width={width} height={height}>
@@ -1932,6 +2592,19 @@ export function normalizeAxisMaximums(
1932
2592
  });
1933
2593
  }
1934
2594
 
2595
+ function resolveRadarWidth(
2596
+ parsedWidth: number | string | undefined,
2597
+ measuredWidth: number
2598
+ ): number {
2599
+ if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
2600
+ return parsedWidth;
2601
+ }
2602
+ if (measuredWidth > 0) {
2603
+ return measuredWidth;
2604
+ }
2605
+ return 300;
2606
+ }
2607
+
1935
2608
  function radarPlaceholder(message: string) {
1936
2609
  return (
1937
2610
  <View