flowboard-react 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import {
3
3
  Animated,
4
+ KeyboardAvoidingView,
5
+ Platform,
4
6
  Pressable,
5
7
  ScrollView,
6
8
  StyleSheet,
@@ -10,8 +12,9 @@ import {
10
12
  Image,
11
13
  Vibration,
12
14
  type TextStyle,
15
+ type ViewStyle,
13
16
  } from 'react-native';
14
- import { SafeAreaView } from 'react-native-safe-area-context';
17
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
15
18
  import MaskedView from '@react-native-masked-view/masked-view';
16
19
  import LinearGradient from 'react-native-linear-gradient';
17
20
  import LottieView from 'lottie-react-native';
@@ -53,7 +56,7 @@ const styles = StyleSheet.create({
53
56
  whiteBg: {
54
57
  backgroundColor: '#ffffff',
55
58
  },
56
- safeArea: { flex: 1 },
59
+ contentLayer: { flex: 1 },
57
60
  progressWrapper: { width: '100%' },
58
61
  });
59
62
 
@@ -67,6 +70,32 @@ type FlowboardRendererProps = {
67
70
  totalScreens?: number;
68
71
  };
69
72
 
73
+ type PageScrollAxis = 'vertical' | 'none';
74
+
75
+ type AxisBounds = {
76
+ widthBounded: boolean;
77
+ heightBounded: boolean;
78
+ };
79
+
80
+ function resolvePageScrollAxis(
81
+ screenData: Record<string, any>
82
+ ): PageScrollAxis {
83
+ const raw = String(
84
+ screenData.pageScroll ??
85
+ screenData.scrollDirection ??
86
+ (screenData.scrollable === true ? 'vertical' : 'none')
87
+ ).toLowerCase();
88
+ if (raw === 'none') return 'none';
89
+ return 'vertical';
90
+ }
91
+
92
+ function getAxisBoundsForPageScroll(pageScroll: PageScrollAxis): AxisBounds {
93
+ if (pageScroll === 'vertical') {
94
+ return { widthBounded: true, heightBounded: false };
95
+ }
96
+ return { widthBounded: true, heightBounded: true };
97
+ }
98
+
70
99
  export default function FlowboardRenderer(props: FlowboardRendererProps) {
71
100
  const {
72
101
  screenData,
@@ -90,8 +119,12 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
90
119
  const progressThickness = Number(screenData.progressThickness ?? 4);
91
120
  const progressRadius = Number(screenData.progressRadius ?? 0);
92
121
  const progressStyle = screenData.progressStyle ?? 'linear';
93
- const scrollable = screenData.scrollable === true;
122
+ const pageScroll = resolvePageScrollAxis(screenData);
123
+ const pageAxisBounds = getAxisBoundsForPageScroll(pageScroll);
124
+ const scrollable = pageScroll !== 'none';
94
125
  const safeArea = screenData.safeArea !== false;
126
+ const safeAreaInsets = useSafeAreaInsets();
127
+ const rootAxis: 'vertical' = 'vertical';
95
128
 
96
129
  const padding = parseInsets(screenData.padding);
97
130
  const contentPaddingStyle = insetsToStyle(padding);
@@ -102,6 +135,14 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
102
135
  };
103
136
  const rootCrossAxisAlignment =
104
137
  screenData.crossAxisAlignment ?? screenData.crossAxis;
138
+ const safeAreaPaddingStyle = safeArea
139
+ ? {
140
+ paddingTop: safeAreaInsets.top,
141
+ paddingRight: safeAreaInsets.right,
142
+ paddingBottom: safeAreaInsets.bottom,
143
+ paddingLeft: safeAreaInsets.left,
144
+ }
145
+ : null;
105
146
 
106
147
  const content = (
107
148
  <View style={{ flex: 1 }}>
@@ -120,6 +161,11 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
120
161
 
121
162
  {scrollable ? (
122
163
  <ScrollView
164
+ horizontal={false}
165
+ keyboardShouldPersistTaps="handled"
166
+ keyboardDismissMode={
167
+ Platform.OS === 'ios' ? 'interactive' : 'on-drag'
168
+ }
123
169
  contentContainerStyle={{
124
170
  ...contentPaddingStyle,
125
171
  paddingTop: showProgress ? 0 : padding.top,
@@ -128,8 +174,11 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
128
174
  <View
129
175
  style={{
130
176
  flexGrow: 1,
177
+ flexDirection: 'column',
131
178
  justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
132
179
  alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
180
+ minHeight: 0,
181
+ minWidth: 0,
133
182
  }}
134
183
  >
135
184
  {childrenData.map((child, index) =>
@@ -138,6 +187,9 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
138
187
  formData,
139
188
  onInputChange,
140
189
  allowFlexExpansion: true,
190
+ parentFlexAxis: rootAxis,
191
+ parentMainAxisBounded: pageAxisBounds.heightBounded,
192
+ pageAxisBounds,
141
193
  screenData,
142
194
  enableFontAwesomeIcons,
143
195
  key: `child-${index}`,
@@ -161,6 +213,9 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
161
213
  formData,
162
214
  onInputChange,
163
215
  allowFlexExpansion: true,
216
+ parentFlexAxis: rootAxis,
217
+ parentMainAxisBounded: true,
218
+ pageAxisBounds,
164
219
  screenData,
165
220
  enableFontAwesomeIcons,
166
221
  key: `child-${index}`,
@@ -174,17 +229,21 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
174
229
  return (
175
230
  <View style={styles.root}>
176
231
  {renderBackground(backgroundData, bgColorCode)}
177
- {safeArea ? (
178
- <SafeAreaView
179
- style={styles.safeArea}
180
- mode="padding"
181
- edges={['top', 'right', 'bottom', 'left']}
182
- >
232
+ <KeyboardAvoidingView
233
+ style={styles.contentLayer}
234
+ behavior={
235
+ Platform.OS === 'ios'
236
+ ? 'padding'
237
+ : Platform.OS === 'android'
238
+ ? 'height'
239
+ : undefined
240
+ }
241
+ keyboardVerticalOffset={0}
242
+ >
243
+ <View style={[styles.contentLayer, safeAreaPaddingStyle]}>
183
244
  {content}
184
- </SafeAreaView>
185
- ) : (
186
- content
187
- )}
245
+ </View>
246
+ </KeyboardAvoidingView>
188
247
  </View>
189
248
  );
190
249
  }
@@ -357,6 +416,484 @@ function renderProgressBar(
357
416
  );
358
417
  }
359
418
 
419
+ const STACK_SPACE_DISTRIBUTIONS = new Set([
420
+ 'spaceBetween',
421
+ 'spaceAround',
422
+ 'spaceEvenly',
423
+ ]);
424
+
425
+ function normalizeStackSpacingConfig(value: any): Record<string, number> {
426
+ if (typeof value === 'number' && Number.isFinite(value)) {
427
+ return { x: value, y: value };
428
+ }
429
+ if (!value || typeof value !== 'object') {
430
+ return { x: 0, y: 0 };
431
+ }
432
+
433
+ const x = Number(value.x ?? value.horizontal);
434
+ const y = Number(value.y ?? value.vertical);
435
+ const top = Number(value.top);
436
+ const right = Number(value.right);
437
+ const bottom = Number(value.bottom);
438
+ const left = Number(value.left);
439
+
440
+ const normalized: Record<string, number> = {};
441
+ if (!Number.isNaN(x)) normalized.x = x;
442
+ if (!Number.isNaN(y)) normalized.y = y;
443
+ if (!Number.isNaN(top)) normalized.top = top;
444
+ if (!Number.isNaN(right)) normalized.right = right;
445
+ if (!Number.isNaN(bottom)) normalized.bottom = bottom;
446
+ if (!Number.isNaN(left)) normalized.left = left;
447
+
448
+ if (Object.keys(normalized).length === 0) {
449
+ return { x: 0, y: 0 };
450
+ }
451
+ return normalized;
452
+ }
453
+
454
+ function stackSpacingToInsets(value: any) {
455
+ const normalized = normalizeStackSpacingConfig(value);
456
+ return parseInsets({
457
+ horizontal: normalized.x,
458
+ vertical: normalized.y,
459
+ top: normalized.top,
460
+ right: normalized.right,
461
+ bottom: normalized.bottom,
462
+ left: normalized.left,
463
+ });
464
+ }
465
+
466
+ function isLegacyOverlayAlignment(value: unknown): boolean {
467
+ return [
468
+ 'topLeft',
469
+ 'topCenter',
470
+ 'topRight',
471
+ 'centerLeft',
472
+ 'center',
473
+ 'centerRight',
474
+ 'bottomLeft',
475
+ 'bottomCenter',
476
+ 'bottomRight',
477
+ ].includes(String(value ?? ''));
478
+ }
479
+
480
+ function normalizeStackAxis(type: string, props: Record<string, any>) {
481
+ const explicit = String(props.axis ?? '').toLowerCase();
482
+ if (
483
+ explicit === 'vertical' ||
484
+ explicit === 'horizontal' ||
485
+ explicit === 'overlay'
486
+ ) {
487
+ return explicit as 'vertical' | 'horizontal' | 'overlay';
488
+ }
489
+ if (type === 'container') return 'overlay';
490
+ if (type === 'row') return 'horizontal';
491
+ if (type === 'column') return 'vertical';
492
+ if (type === 'layout') {
493
+ return props.direction === 'horizontal' ? 'horizontal' : 'vertical';
494
+ }
495
+ if (
496
+ type === 'stack' &&
497
+ (props.fit !== undefined || isLegacyOverlayAlignment(props.alignment))
498
+ ) {
499
+ return 'overlay';
500
+ }
501
+ return 'vertical';
502
+ }
503
+
504
+ function normalizeStackDistribution(value: unknown) {
505
+ const normalized = String(value ?? '').trim();
506
+ if (
507
+ normalized === 'center' ||
508
+ normalized === 'end' ||
509
+ normalized === 'spaceBetween' ||
510
+ normalized === 'spaceAround' ||
511
+ normalized === 'spaceEvenly'
512
+ ) {
513
+ return normalized;
514
+ }
515
+ return 'start';
516
+ }
517
+
518
+ function normalizeStackCrossAlignment(value: unknown) {
519
+ const raw = String(value ?? '').toLowerCase();
520
+ if (raw === 'center' || raw.includes('center')) return 'center';
521
+ if (raw === 'end' || raw.endsWith('right') || raw.startsWith('bottom'))
522
+ return 'end';
523
+ return 'start';
524
+ }
525
+
526
+ function normalizeOverlayAlignment(value: unknown) {
527
+ const raw = String(value ?? '').trim();
528
+ switch (raw) {
529
+ case 'topStart':
530
+ case 'top':
531
+ case 'topEnd':
532
+ case 'start':
533
+ case 'center':
534
+ case 'end':
535
+ case 'bottomStart':
536
+ case 'bottom':
537
+ case 'bottomEnd':
538
+ return raw;
539
+ case 'topLeft':
540
+ return 'topStart';
541
+ case 'topCenter':
542
+ return 'top';
543
+ case 'topRight':
544
+ return 'topEnd';
545
+ case 'centerLeft':
546
+ return 'start';
547
+ case 'centerRight':
548
+ return 'end';
549
+ case 'bottomLeft':
550
+ return 'bottomStart';
551
+ case 'bottomCenter':
552
+ return 'bottom';
553
+ case 'bottomRight':
554
+ return 'bottomEnd';
555
+ default:
556
+ return 'center';
557
+ }
558
+ }
559
+
560
+ function isFillDimensionValue(value: unknown) {
561
+ if (typeof value === 'number') return value === Number.POSITIVE_INFINITY;
562
+ if (typeof value !== 'string') return false;
563
+ const normalized = value.trim().toLowerCase();
564
+ return (
565
+ normalized === 'infinity' ||
566
+ normalized === 'double.infinity' ||
567
+ normalized === '+infinity' ||
568
+ normalized === '100%'
569
+ );
570
+ }
571
+
572
+ function isFixedDimensionValue(value: unknown) {
573
+ if (isFillDimensionValue(value)) return false;
574
+ const parsed = Number(value);
575
+ return Number.isFinite(parsed);
576
+ }
577
+
578
+ function normalizeStackSize(
579
+ props: Record<string, any>,
580
+ axis: 'vertical' | 'horizontal' | 'overlay'
581
+ ) {
582
+ const widthMode = String(props.size?.width ?? '').toLowerCase();
583
+ const heightMode = String(props.size?.height ?? '').toLowerCase();
584
+
585
+ const width =
586
+ widthMode === 'fill' || widthMode === 'fit' || widthMode === 'fixed'
587
+ ? widthMode
588
+ : props.fit === 'expand' || props.width === 'infinity'
589
+ ? 'fill'
590
+ : isFixedDimensionValue(props.width)
591
+ ? 'fixed'
592
+ : axis === 'horizontal'
593
+ ? 'fit'
594
+ : 'fill';
595
+ const height =
596
+ heightMode === 'fill' || heightMode === 'fit' || heightMode === 'fixed'
597
+ ? heightMode
598
+ : props.fit === 'expand' || props.height === 'infinity'
599
+ ? 'fill'
600
+ : isFixedDimensionValue(props.height)
601
+ ? 'fixed'
602
+ : axis === 'vertical'
603
+ ? 'fit'
604
+ : 'fill';
605
+
606
+ return { width, height };
607
+ }
608
+
609
+ function normalizeStackFill(props: Record<string, any>) {
610
+ if (props.fill && typeof props.fill === 'object') {
611
+ const next = { ...props.fill };
612
+ if (!next.type && next.color) next.type = 'solid';
613
+ return next;
614
+ }
615
+ if (props.background && typeof props.background === 'object') {
616
+ if (props.background.type === 'color') {
617
+ return { type: 'solid', color: props.background.color };
618
+ }
619
+ return { ...props.background };
620
+ }
621
+ if (props.backgroundColor) {
622
+ return { type: 'solid', color: props.backgroundColor };
623
+ }
624
+ return undefined;
625
+ }
626
+
627
+ function normalizeStackBorder(props: Record<string, any>) {
628
+ if (props.border && typeof props.border === 'object') {
629
+ return { ...props.border };
630
+ }
631
+ if (props.borderColor || props.borderWidth !== undefined) {
632
+ return {
633
+ width: Number(props.borderWidth ?? 1),
634
+ color: props.borderColor,
635
+ };
636
+ }
637
+ return undefined;
638
+ }
639
+
640
+ function normalizeStackShadow(props: Record<string, any>) {
641
+ if (props.shadow && typeof props.shadow === 'object') {
642
+ return { ...props.shadow };
643
+ }
644
+ return undefined;
645
+ }
646
+
647
+ function spacingToInsets(value: any) {
648
+ const normalized = normalizeStackSpacingConfig(value);
649
+ const x = normalized.x ?? 0;
650
+ const y = normalized.y ?? 0;
651
+ return {
652
+ top: normalized.top ?? y,
653
+ right: normalized.right ?? x,
654
+ bottom: normalized.bottom ?? y,
655
+ left: normalized.left ?? x,
656
+ };
657
+ }
658
+
659
+ function insetsToSpacing(insets: {
660
+ top: number;
661
+ right: number;
662
+ bottom: number;
663
+ left: number;
664
+ }) {
665
+ if (insets.top === insets.bottom && insets.left === insets.right) {
666
+ return { x: insets.left, y: insets.top };
667
+ }
668
+ return insets;
669
+ }
670
+
671
+ function mergeStackProps(
672
+ parentProps: Record<string, any>,
673
+ childProps: Record<string, any>
674
+ ) {
675
+ const parentAxis = String(parentProps.axis ?? 'vertical');
676
+ const childAxis = String(childProps.axis ?? 'vertical');
677
+
678
+ let axis = childAxis;
679
+ if (parentAxis !== childAxis) {
680
+ if (parentAxis === 'overlay' && childAxis !== 'overlay') axis = childAxis;
681
+ else if (childAxis === 'overlay' && parentAxis !== 'overlay')
682
+ axis = parentAxis;
683
+ }
684
+
685
+ const axisSource =
686
+ axis === parentAxis && axis !== childAxis ? parentProps : childProps;
687
+ const fallbackAxisSource =
688
+ axisSource === childProps ? parentProps : childProps;
689
+
690
+ const parentPadding = spacingToInsets(parentProps.layout?.padding);
691
+ const childPadding = spacingToInsets(childProps.layout?.padding);
692
+ const parentMargin = spacingToInsets(parentProps.layout?.margin);
693
+ const childMargin = spacingToInsets(childProps.layout?.margin);
694
+
695
+ const mergedProps: Record<string, any> = {
696
+ axis,
697
+ alignment: axisSource.alignment ?? fallbackAxisSource.alignment ?? 'start',
698
+ distribution:
699
+ axisSource.distribution ?? fallbackAxisSource.distribution ?? 'start',
700
+ childSpacing:
701
+ Number(axisSource.childSpacing ?? fallbackAxisSource.childSpacing ?? 0) ||
702
+ 0,
703
+ size: {
704
+ width: childProps.size?.width ?? parentProps.size?.width ?? 'fill',
705
+ height: childProps.size?.height ?? parentProps.size?.height ?? 'fit',
706
+ },
707
+ layout: {
708
+ padding: insetsToSpacing({
709
+ top: parentPadding.top + childPadding.top,
710
+ right: parentPadding.right + childPadding.right,
711
+ bottom: parentPadding.bottom + childPadding.bottom,
712
+ left: parentPadding.left + childPadding.left,
713
+ }),
714
+ margin: insetsToSpacing({
715
+ top: parentMargin.top + childMargin.top,
716
+ right: parentMargin.right + childMargin.right,
717
+ bottom: parentMargin.bottom + childMargin.bottom,
718
+ left: parentMargin.left + childMargin.left,
719
+ }),
720
+ },
721
+ appearance: {
722
+ shape: 'rectangle',
723
+ cornerRadius: Number(
724
+ childProps.appearance?.cornerRadius ??
725
+ parentProps.appearance?.cornerRadius ??
726
+ 0
727
+ ),
728
+ },
729
+ };
730
+
731
+ if (axis === 'overlay') {
732
+ mergedProps.overlayAlignment =
733
+ childProps.overlayAlignment ?? parentProps.overlayAlignment ?? 'center';
734
+ }
735
+
736
+ if (parentProps.fill || childProps.fill) {
737
+ mergedProps.fill = childProps.fill ?? parentProps.fill;
738
+ }
739
+ if (parentProps.border || childProps.border) {
740
+ mergedProps.border = childProps.border ?? parentProps.border;
741
+ }
742
+ if (parentProps.shadow || childProps.shadow) {
743
+ mergedProps.shadow = childProps.shadow ?? parentProps.shadow;
744
+ }
745
+
746
+ if (childProps.width !== undefined || parentProps.width !== undefined) {
747
+ mergedProps.width = childProps.width ?? parentProps.width;
748
+ }
749
+ if (childProps.height !== undefined || parentProps.height !== undefined) {
750
+ mergedProps.height = childProps.height ?? parentProps.height;
751
+ }
752
+
753
+ return mergedProps;
754
+ }
755
+
756
+ function normalizeStackLikeNode(
757
+ json: Record<string, any>
758
+ ): Record<string, any> {
759
+ if (!json || typeof json !== 'object') return json;
760
+ const type = String(json.type ?? '');
761
+ if (
762
+ !['stack', 'container', 'layout', 'row', 'column', 'grid'].includes(type)
763
+ ) {
764
+ return json;
765
+ }
766
+
767
+ const props = { ...(json.properties ?? {}) } as Record<string, any>;
768
+ const axis = normalizeStackAxis(type, props);
769
+ const size = normalizeStackSize(props, axis);
770
+ const children = Array.isArray(json.children) ? [...json.children] : [];
771
+ if (json.child) {
772
+ children.push(json.child);
773
+ }
774
+
775
+ const normalizedProps: Record<string, any> = {
776
+ axis,
777
+ alignment: normalizeStackCrossAlignment(
778
+ props.alignment ?? props.crossAxisAlignment
779
+ ),
780
+ distribution: normalizeStackDistribution(
781
+ props.distribution ?? props.mainAxisAlignment
782
+ ),
783
+ childSpacing: Number(props.childSpacing ?? props.gap ?? props.spacing ?? 0),
784
+ size,
785
+ layout: {
786
+ padding: normalizeStackSpacingConfig(
787
+ props.layout?.padding ?? props.padding
788
+ ),
789
+ margin: normalizeStackSpacingConfig(props.layout?.margin ?? props.margin),
790
+ },
791
+ appearance: {
792
+ shape: 'rectangle',
793
+ cornerRadius: Number(
794
+ props.appearance?.cornerRadius ?? props.borderRadius ?? 0
795
+ ),
796
+ },
797
+ };
798
+
799
+ if (axis === 'overlay') {
800
+ normalizedProps.overlayAlignment = normalizeOverlayAlignment(
801
+ props.overlayAlignment ?? props.alignment
802
+ );
803
+ }
804
+
805
+ const fill = normalizeStackFill(props);
806
+ if (fill) normalizedProps.fill = fill;
807
+
808
+ const border = normalizeStackBorder(props);
809
+ if (border) normalizedProps.border = border;
810
+
811
+ const shadow = normalizeStackShadow(props);
812
+ if (shadow) normalizedProps.shadow = shadow;
813
+
814
+ if (props.width !== undefined) normalizedProps.width = props.width;
815
+ if (props.height !== undefined) normalizedProps.height = props.height;
816
+
817
+ if ((type === 'container' || type === 'layout') && children.length === 1) {
818
+ const childCandidate = children[0];
819
+ if (childCandidate && typeof childCandidate === 'object') {
820
+ const normalizedChild: Record<string, any> = normalizeStackLikeNode(
821
+ childCandidate as Record<string, any>
822
+ );
823
+ if (normalizedChild?.type === 'stack') {
824
+ const childProps = {
825
+ ...(normalizedChild.properties ?? {}),
826
+ } as Record<string, any>;
827
+ const mergedProps = mergeStackProps(normalizedProps, childProps);
828
+ return {
829
+ ...json,
830
+ type: 'stack',
831
+ properties: mergedProps,
832
+ children: Array.isArray(normalizedChild.children)
833
+ ? normalizedChild.children
834
+ : [],
835
+ child: undefined,
836
+ };
837
+ }
838
+ }
839
+ }
840
+
841
+ return {
842
+ ...json,
843
+ type: 'stack',
844
+ properties: normalizedProps,
845
+ children,
846
+ child: undefined,
847
+ };
848
+ }
849
+
850
+ function parseOverlayGridAlignment(
851
+ value?: string
852
+ ): Pick<ViewStyle, 'justifyContent' | 'alignItems'> {
853
+ switch (normalizeOverlayAlignment(value)) {
854
+ case 'topStart':
855
+ return { justifyContent: 'flex-start', alignItems: 'flex-start' };
856
+ case 'top':
857
+ return { justifyContent: 'flex-start', alignItems: 'center' };
858
+ case 'topEnd':
859
+ return { justifyContent: 'flex-start', alignItems: 'flex-end' };
860
+ case 'start':
861
+ return { justifyContent: 'center', alignItems: 'flex-start' };
862
+ case 'end':
863
+ return { justifyContent: 'center', alignItems: 'flex-end' };
864
+ case 'bottomStart':
865
+ return { justifyContent: 'flex-end', alignItems: 'flex-start' };
866
+ case 'bottom':
867
+ return { justifyContent: 'flex-end', alignItems: 'center' };
868
+ case 'bottomEnd':
869
+ return { justifyContent: 'flex-end', alignItems: 'flex-end' };
870
+ case 'center':
871
+ default:
872
+ return { justifyContent: 'center', alignItems: 'center' };
873
+ }
874
+ }
875
+
876
+ function resolveStackDimension(
877
+ props: Record<string, any>,
878
+ axis: 'vertical' | 'horizontal' | 'overlay',
879
+ key: 'width' | 'height',
880
+ axisBounds: AxisBounds
881
+ ) {
882
+ const mode = props.size?.[key];
883
+ const legacy = normalizeDimension(parseLayoutDimension(props[key]));
884
+ const isBounded =
885
+ key === 'width' ? axisBounds.widthBounded : axisBounds.heightBounded;
886
+ if (mode === 'fill') return isBounded ? '100%' : legacy;
887
+ if (mode === 'fit') return legacy;
888
+ if (mode === 'fixed') return legacy;
889
+ if (legacy !== undefined) return legacy;
890
+ if (key === 'width' && axis !== 'horizontal' && axisBounds.widthBounded)
891
+ return '100%';
892
+ if (key === 'height' && axis === 'horizontal' && axisBounds.heightBounded)
893
+ return '100%';
894
+ return undefined;
895
+ }
896
+
360
897
  function buildWidget(
361
898
  json: Record<string, any>,
362
899
  params: {
@@ -364,19 +901,32 @@ function buildWidget(
364
901
  formData: Record<string, any>;
365
902
  onInputChange?: (id: string, value: any) => void;
366
903
  allowFlexExpansion?: boolean;
904
+ parentFlexAxis?: 'vertical' | 'horizontal';
905
+ parentMainAxisBounded?: boolean;
906
+ pageAxisBounds?: AxisBounds;
367
907
  screenData: Record<string, any>;
368
908
  enableFontAwesomeIcons: boolean;
369
909
  key?: string;
370
910
  }
371
911
  ): React.ReactNode {
372
912
  if (!json || typeof json !== 'object') return null;
373
- const type = json.type;
374
- const id = json.id;
375
- const props = { ...(json.properties ?? {}) } as Record<string, any>;
376
- const childrenJson = Array.isArray(json.children) ? json.children : undefined;
377
- const childJson = json.child as Record<string, any> | undefined;
378
- const action = json.action as string | undefined;
379
- const actionPayload = action ? buildActionPayload(props, json) : undefined;
913
+ const normalizedJson = normalizeStackLikeNode(json);
914
+ const type = normalizedJson.type;
915
+ const id = normalizedJson.id;
916
+ const props = { ...(normalizedJson.properties ?? {}) } as Record<string, any>;
917
+ const pageAxisBounds =
918
+ params.pageAxisBounds ??
919
+ getAxisBoundsForPageScroll(resolvePageScrollAxis(params.screenData));
920
+ const childrenJson: Record<string, any>[] | undefined = Array.isArray(
921
+ normalizedJson.children
922
+ )
923
+ ? (normalizedJson.children as Record<string, any>[])
924
+ : undefined;
925
+ const childJson = normalizedJson.child as Record<string, any> | undefined;
926
+ const action = normalizedJson.action as string | undefined;
927
+ const actionPayload = action
928
+ ? buildActionPayload(props, normalizedJson)
929
+ : undefined;
380
930
 
381
931
  let node: React.ReactNode = null;
382
932
 
@@ -565,62 +1115,121 @@ function buildWidget(
565
1115
  const height =
566
1116
  normalizeDimension(parseLayoutDimension(props.height)) ?? 50;
567
1117
  const label = props.label ?? 'Button';
1118
+ const normalizedStroke = normalizeButtonStroke(props.stroke);
1119
+ const normalizedEffects = normalizeButtonEffects(
1120
+ props.effects,
1121
+ props.shadow
1122
+ );
568
1123
  const textStyle = getTextStyle(
569
1124
  { ...props, height: null },
570
1125
  'textColor',
571
1126
  '0xFFFFFFFF'
572
1127
  );
1128
+ const backgroundColor = parseColor(props.color ?? '0xFF2196F3');
1129
+ const shadowBounds = getButtonShadowLayerBounds(
1130
+ normalizedStroke,
1131
+ borderRadius
1132
+ );
1133
+ const webShadowStyle = buildButtonWebShadowStyle(normalizedEffects);
1134
+ const firstNativeEffect =
1135
+ normalizedEffects.length > 0 ? normalizedEffects[0] : null;
1136
+ const nativePrimaryShadowStyle =
1137
+ Platform.OS === 'web' || !firstNativeEffect
1138
+ ? null
1139
+ : buildButtonNativeShadowStyle(firstNativeEffect);
1140
+ const centeredStrokeStyle =
1141
+ normalizedStroke && normalizedStroke.position === 'center'
1142
+ ? {
1143
+ borderWidth: normalizedStroke.width,
1144
+ borderColor: normalizedStroke.color,
1145
+ }
1146
+ : null;
1147
+ const fillLayerStyle: any = {
1148
+ position: 'absolute',
1149
+ top: 0,
1150
+ right: 0,
1151
+ bottom: 0,
1152
+ left: 0,
1153
+ borderRadius,
1154
+ overflow: 'hidden',
1155
+ ...(centeredStrokeStyle ?? {}),
1156
+ };
1157
+ const nativeShadowEffectsInPaintOrder = normalizedEffects
1158
+ .slice()
1159
+ .reverse();
573
1160
 
574
- const content = (
575
- <View
576
- style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
1161
+ const pressableStyle: any = {
1162
+ width,
1163
+ height,
1164
+ borderRadius,
1165
+ justifyContent: 'center',
1166
+ alignItems: 'center',
1167
+ overflow: 'visible',
1168
+ ...(nativePrimaryShadowStyle ?? {}),
1169
+ };
1170
+
1171
+ node = (
1172
+ <Pressable
1173
+ collapsable={false}
1174
+ onPress={
1175
+ action
1176
+ ? () => params.onAction(action, actionPayload ?? props)
1177
+ : undefined
1178
+ }
1179
+ style={pressableStyle}
577
1180
  >
578
- <Text style={textStyle}>{label}</Text>
579
- </View>
580
- );
1181
+ {Platform.OS === 'web' ? (
1182
+ webShadowStyle ? (
1183
+ <View
1184
+ pointerEvents="none"
1185
+ style={{
1186
+ position: 'absolute',
1187
+ ...shadowBounds,
1188
+ ...webShadowStyle,
1189
+ }}
1190
+ />
1191
+ ) : null
1192
+ ) : (
1193
+ nativeShadowEffectsInPaintOrder.map((effect, index) => (
1194
+ <View
1195
+ key={`shadow-${index}`}
1196
+ collapsable={false}
1197
+ pointerEvents="none"
1198
+ style={{
1199
+ position: 'absolute',
1200
+ ...shadowBounds,
1201
+ ...buildButtonNativeShadowStyle(effect),
1202
+ }}
1203
+ />
1204
+ ))
1205
+ )}
581
1206
 
582
- if (gradient) {
583
- node = (
584
- <Pressable
585
- onPress={
586
- action
587
- ? () => params.onAction(action, actionPayload ?? props)
588
- : undefined
589
- }
590
- style={{ width, height }}
591
- >
1207
+ {gradient ? (
592
1208
  <LinearGradient
593
1209
  colors={gradient.colors}
594
1210
  start={gradient.start}
595
1211
  end={gradient.end}
596
1212
  locations={gradient.stops}
597
- style={{ flex: 1, borderRadius, overflow: 'hidden' }}
598
- >
599
- {content}
600
- </LinearGradient>
601
- </Pressable>
602
- );
603
- } else {
604
- node = (
605
- <Pressable
606
- onPress={
607
- action
608
- ? () => params.onAction(action, actionPayload ?? props)
609
- : undefined
610
- }
611
- style={{
612
- width,
613
- height,
614
- backgroundColor: parseColor(props.color ?? '0xFF2196F3'),
615
- borderRadius,
616
- justifyContent: 'center',
617
- alignItems: 'center',
618
- }}
1213
+ style={fillLayerStyle}
1214
+ />
1215
+ ) : (
1216
+ <View
1217
+ style={{
1218
+ ...fillLayerStyle,
1219
+ backgroundColor,
1220
+ }}
1221
+ />
1222
+ )}
1223
+
1224
+ {renderButtonStrokeOverlay(normalizedStroke, borderRadius)}
1225
+
1226
+ <View
1227
+ style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
619
1228
  >
620
- {content}
621
- </Pressable>
622
- );
623
- }
1229
+ <Text style={textStyle}>{label}</Text>
1230
+ </View>
1231
+ </Pressable>
1232
+ );
624
1233
  break;
625
1234
  }
626
1235
  case 'text_input': {
@@ -793,12 +1402,15 @@ function buildWidget(
793
1402
  const height = normalizeDimension(
794
1403
  parseLayoutDimension(props.height, true)
795
1404
  );
1405
+ const style = resolveLottieStyle(width, height);
1406
+ const resizeMode = parseResizeMode(props.fit);
796
1407
  const loop = props.loop === true;
797
1408
  if (source === 'asset') {
798
1409
  node = (
799
1410
  <LottieView
800
1411
  source={{ uri: props.path ?? '' }}
801
- style={{ width, height }}
1412
+ style={style}
1413
+ resizeMode={resizeMode}
802
1414
  autoPlay
803
1415
  loop={loop}
804
1416
  />
@@ -808,7 +1420,8 @@ function buildWidget(
808
1420
  node = (
809
1421
  <LottieView
810
1422
  source={{ uri: props.url }}
811
- style={{ width, height }}
1423
+ style={style}
1424
+ resizeMode={resizeMode}
812
1425
  autoPlay
813
1426
  loop={loop}
814
1427
  />
@@ -936,25 +1549,170 @@ function buildWidget(
936
1549
  break;
937
1550
  }
938
1551
  case 'stack': {
939
- const alignment = parseAlignment(props.alignment ?? 'topLeft');
940
- const fit = props.fit === 'expand' ? 'expand' : 'loose';
941
- node = (
1552
+ const axis =
1553
+ props.axis === 'horizontal' || props.axis === 'overlay'
1554
+ ? props.axis
1555
+ : 'vertical';
1556
+ const isOverlay = axis === 'overlay';
1557
+ const spacing = Number(props.childSpacing ?? 0);
1558
+ const applySpacing = !STACK_SPACE_DISTRIBUTIONS.has(
1559
+ String(props.distribution ?? 'start')
1560
+ );
1561
+ const paddingInsets = stackSpacingToInsets(
1562
+ props.layout?.padding ?? props.padding
1563
+ );
1564
+ const width = resolveStackDimension(props, axis, 'width', pageAxisBounds);
1565
+ const height = resolveStackDimension(
1566
+ props,
1567
+ axis,
1568
+ 'height',
1569
+ pageAxisBounds
1570
+ );
1571
+ const mainAxisBounded =
1572
+ axis === 'horizontal'
1573
+ ? pageAxisBounds.widthBounded
1574
+ : pageAxisBounds.heightBounded;
1575
+ const borderRadius = Number(
1576
+ props.appearance?.cornerRadius ?? props.borderRadius ?? 0
1577
+ );
1578
+
1579
+ const borderWidth = Number(props.border?.width ?? 0);
1580
+ const borderColor =
1581
+ borderWidth > 0 ? parseColor(props.border?.color ?? '#E5E7EB') : null;
1582
+ const shadowColor = props.shadow?.color
1583
+ ? parseColor(props.shadow.color)
1584
+ : null;
1585
+ const shadowStyle: Record<string, any> =
1586
+ shadowColor && props.shadow
1587
+ ? Platform.OS === 'web'
1588
+ ? {
1589
+ boxShadow: `${Number(props.shadow.x ?? 0)}px ${Number(
1590
+ props.shadow.y ?? 8
1591
+ )}px ${Number(props.shadow.blur ?? 24)}px ${shadowColor}`,
1592
+ }
1593
+ : {
1594
+ shadowColor,
1595
+ shadowOffset: {
1596
+ width: Number(props.shadow.x ?? 0),
1597
+ height: Number(props.shadow.y ?? 8),
1598
+ },
1599
+ shadowOpacity: 0.35,
1600
+ shadowRadius: Math.max(0, Number(props.shadow.blur ?? 24) / 2),
1601
+ elevation: Math.max(
1602
+ 1,
1603
+ Math.round(
1604
+ (Number(props.shadow.blur ?? 24) +
1605
+ Math.abs(Number(props.shadow.y ?? 8))) /
1606
+ 3
1607
+ )
1608
+ ),
1609
+ }
1610
+ : {};
1611
+
1612
+ const fill = props.fill;
1613
+ const stackGradient =
1614
+ fill?.type === 'gradient'
1615
+ ? parseGradient({ ...fill, type: 'gradient' })
1616
+ : undefined;
1617
+ const stackColor =
1618
+ fill?.type === 'solid' || (!fill?.type && fill?.color)
1619
+ ? parseColor(fill?.color ?? '#FFFFFFFF')
1620
+ : parseColor(props.backgroundColor ?? '#00000000');
1621
+
1622
+ const baseStackStyle: any = {
1623
+ width,
1624
+ height,
1625
+ borderRadius: borderRadius > 0 ? borderRadius : undefined,
1626
+ overflow: borderRadius > 0 ? 'hidden' : undefined,
1627
+ borderWidth: borderColor ? borderWidth : undefined,
1628
+ borderColor: borderColor ?? undefined,
1629
+ ...insetsToStyle(paddingInsets),
1630
+ ...shadowStyle,
1631
+ };
1632
+
1633
+ const content = isOverlay ? (
1634
+ <View style={{ position: 'relative', minHeight: 0, minWidth: 0 }}>
1635
+ {(childrenJson ?? []).map((child, index) => {
1636
+ const alignment = parseOverlayGridAlignment(
1637
+ props.overlayAlignment ?? props.alignment
1638
+ );
1639
+ return (
1640
+ <View
1641
+ key={`stack-overlay-${index}`}
1642
+ style={{
1643
+ position: 'absolute',
1644
+ top: 0,
1645
+ right: 0,
1646
+ bottom: 0,
1647
+ left: 0,
1648
+ justifyContent: alignment.justifyContent,
1649
+ alignItems: alignment.alignItems,
1650
+ }}
1651
+ >
1652
+ {buildWidget(child, {
1653
+ ...params,
1654
+ allowFlexExpansion: true,
1655
+ pageAxisBounds,
1656
+ })}
1657
+ </View>
1658
+ );
1659
+ })}
1660
+ {(childrenJson ?? []).length === 0 ? null : null}
1661
+ </View>
1662
+ ) : (
942
1663
  <View
943
1664
  style={{
944
- position: 'relative',
945
- width: fit === 'expand' ? '100%' : undefined,
946
- height: fit === 'expand' ? '100%' : undefined,
947
- justifyContent: alignment.justifyContent,
948
- alignItems: alignment.alignItems,
1665
+ flexDirection: axis === 'horizontal' ? 'row' : 'column',
1666
+ justifyContent: parseFlexAlignment(props.distribution),
1667
+ alignItems: parseCrossAlignment(props.alignment),
1668
+ minHeight: 0,
1669
+ minWidth: 0,
949
1670
  }}
950
1671
  >
951
1672
  {(childrenJson ?? []).map((child, index) => (
952
1673
  <React.Fragment key={`stack-${index}`}>
953
- {buildWidget(child, { ...params })}
1674
+ {buildWidget(child, {
1675
+ ...params,
1676
+ allowFlexExpansion: true,
1677
+ parentFlexAxis:
1678
+ axis === 'horizontal' ? 'horizontal' : 'vertical',
1679
+ parentMainAxisBounded: mainAxisBounded,
1680
+ pageAxisBounds,
1681
+ })}
1682
+ {applySpacing &&
1683
+ spacing > 0 &&
1684
+ index < (childrenJson?.length ?? 0) - 1 ? (
1685
+ <View
1686
+ style={{
1687
+ width: axis === 'horizontal' ? spacing : undefined,
1688
+ height: axis === 'vertical' ? spacing : undefined,
1689
+ }}
1690
+ />
1691
+ ) : null}
954
1692
  </React.Fragment>
955
1693
  ))}
956
1694
  </View>
957
1695
  );
1696
+
1697
+ if (stackGradient) {
1698
+ node = (
1699
+ <LinearGradient
1700
+ colors={stackGradient.colors}
1701
+ start={stackGradient.start}
1702
+ end={stackGradient.end}
1703
+ locations={stackGradient.stops}
1704
+ style={baseStackStyle}
1705
+ >
1706
+ {content}
1707
+ </LinearGradient>
1708
+ );
1709
+ } else {
1710
+ node = (
1711
+ <View style={{ ...baseStackStyle, backgroundColor: stackColor }}>
1712
+ {content}
1713
+ </View>
1714
+ );
1715
+ }
958
1716
  break;
959
1717
  }
960
1718
  case 'positioned': {
@@ -987,29 +1745,76 @@ function buildWidget(
987
1745
 
988
1746
  if (!node) return null;
989
1747
 
990
- const marginVal = props.margin;
1748
+ const marginVal =
1749
+ type === 'stack' ? props.layout?.margin ?? props.margin : props.margin;
991
1750
  if (marginVal !== undefined && marginVal !== null) {
992
- const marginInsets = parseInsets(marginVal);
1751
+ const marginInsets =
1752
+ type === 'stack'
1753
+ ? stackSpacingToInsets(marginVal)
1754
+ : parseInsets(marginVal);
993
1755
  node = <View style={insetsToMarginStyle(marginInsets)}>{node}</View>;
994
- } else if (type !== 'container' && type !== 'padding' && props.padding) {
1756
+ } else if (
1757
+ type !== 'container' &&
1758
+ type !== 'padding' &&
1759
+ type !== 'stack' &&
1760
+ props.padding
1761
+ ) {
995
1762
  const paddingInsets = parseInsets(props.padding);
996
1763
  node = <View style={insetsToStyle(paddingInsets)}>{node}</View>;
997
1764
  }
998
1765
 
999
- if (params.allowFlexExpansion && params.screenData.scrollable !== true) {
1766
+ const shouldStretchTextInput =
1767
+ type === 'text_input' &&
1768
+ props.width === undefined &&
1769
+ props.size?.width === undefined;
1770
+ if (shouldStretchTextInput) {
1771
+ node = (
1772
+ <View style={{ width: '100%', alignSelf: 'stretch', minWidth: 0 }}>
1773
+ {node}
1774
+ </View>
1775
+ );
1776
+ }
1777
+
1778
+ if (params.allowFlexExpansion) {
1000
1779
  let shouldExpand = false;
1001
- if (type === 'stack' && props.fit === 'expand') {
1002
- shouldExpand = true;
1003
- }
1004
- if (props.height !== undefined) {
1005
- const heightVal = parseLayoutDimension(props.height);
1006
- if (heightVal === Number.POSITIVE_INFINITY) {
1007
- shouldExpand = true;
1780
+ const parentAxis = params.parentFlexAxis ?? 'vertical';
1781
+ const parentMainAxisBounded = params.parentMainAxisBounded ?? true;
1782
+
1783
+ if (parentMainAxisBounded) {
1784
+ if (type === 'stack') {
1785
+ const legacyExpand = props.fit === 'expand';
1786
+ const widthMode = String(props.size?.width ?? '').toLowerCase();
1787
+ const heightMode = String(props.size?.height ?? '').toLowerCase();
1788
+ if (
1789
+ parentAxis === 'vertical' &&
1790
+ (legacyExpand || heightMode === 'fill')
1791
+ ) {
1792
+ shouldExpand = true;
1793
+ }
1794
+ if (
1795
+ parentAxis === 'horizontal' &&
1796
+ (legacyExpand || widthMode === 'fill')
1797
+ ) {
1798
+ shouldExpand = true;
1799
+ }
1800
+ }
1801
+
1802
+ if (parentAxis === 'vertical' && props.height !== undefined) {
1803
+ const heightVal = parseLayoutDimension(props.height);
1804
+ if (heightVal === Number.POSITIVE_INFINITY) {
1805
+ shouldExpand = true;
1806
+ }
1807
+ }
1808
+ if (parentAxis === 'horizontal' && props.width !== undefined) {
1809
+ const widthVal = parseLayoutDimension(props.width);
1810
+ if (widthVal === Number.POSITIVE_INFINITY) {
1811
+ shouldExpand = true;
1812
+ }
1008
1813
  }
1009
1814
  }
1010
1815
 
1011
1816
  if (shouldExpand) {
1012
- node = <View style={{ flex: 1 }}>{node}</View>;
1817
+ node = <View style={{ flex: 1, minHeight: 0, minWidth: 0 }}>{node}</View>;
1013
1818
  }
1014
1819
  }
1015
1820
 
@@ -1066,26 +1871,247 @@ function getTextStyle(
1066
1871
  };
1067
1872
  }
1068
1873
 
1069
- function parseGradient(value: any):
1070
- | {
1071
- colors: string[];
1072
- start: { x: number; y: number };
1073
- end: { x: number; y: number };
1074
- stops?: number[];
1075
- }
1076
- | undefined {
1077
- if (!value || typeof value !== 'object' || value.type !== 'gradient')
1078
- return undefined;
1079
- const colors = Array.isArray(value.colors)
1080
- ? value.colors.map((c: string) => parseColor(c))
1081
- : [];
1082
- if (colors.length === 0) return undefined;
1083
- const start = alignmentToGradient(value.begin ?? 'topLeft');
1084
- const end = alignmentToGradient(value.end ?? 'bottomRight');
1085
- const stops = Array.isArray(value.stops)
1086
- ? value.stops.map((s: number) => Number(s))
1087
- : undefined;
1088
- return { colors, start, end, stops };
1874
+ function parseGradient(value: any):
1875
+ | {
1876
+ colors: string[];
1877
+ start: { x: number; y: number };
1878
+ end: { x: number; y: number };
1879
+ stops?: number[];
1880
+ }
1881
+ | undefined {
1882
+ if (!value || typeof value !== 'object' || value.type !== 'gradient')
1883
+ return undefined;
1884
+ const colors = Array.isArray(value.colors)
1885
+ ? value.colors.map((c: string) => parseColor(c))
1886
+ : [];
1887
+ if (colors.length === 0) return undefined;
1888
+ const start = alignmentToGradient(value.begin ?? 'topLeft');
1889
+ const end = alignmentToGradient(value.end ?? 'bottomRight');
1890
+ const stops = Array.isArray(value.stops)
1891
+ ? value.stops.map((s: number) => Number(s))
1892
+ : undefined;
1893
+ return { colors, start, end, stops };
1894
+ }
1895
+
1896
+ type ButtonStrokePosition = 'inside' | 'center' | 'outside';
1897
+
1898
+ interface ButtonStroke {
1899
+ enabled?: boolean;
1900
+ color?: string;
1901
+ opacity?: number;
1902
+ width?: number;
1903
+ position?: ButtonStrokePosition;
1904
+ }
1905
+
1906
+ interface ButtonDropShadowEffect {
1907
+ type?: 'dropShadow' | string;
1908
+ enabled?: boolean;
1909
+ x?: number;
1910
+ y?: number;
1911
+ blur?: number;
1912
+ spread?: number;
1913
+ color?: string;
1914
+ opacity?: number;
1915
+ }
1916
+
1917
+ interface LegacyButtonShadow {
1918
+ enabled?: boolean;
1919
+ x?: number;
1920
+ y?: number;
1921
+ blur?: number;
1922
+ color?: string;
1923
+ }
1924
+
1925
+ type NormalizedButtonStroke = {
1926
+ color: string;
1927
+ width: number;
1928
+ position: ButtonStrokePosition;
1929
+ };
1930
+
1931
+ type NormalizedButtonDropShadow = {
1932
+ x: number;
1933
+ y: number;
1934
+ blur: number;
1935
+ spread: number;
1936
+ color: string;
1937
+ opacity: number;
1938
+ };
1939
+
1940
+ function normalizeButtonStroke(
1941
+ input: ButtonStroke | undefined
1942
+ ): NormalizedButtonStroke | null {
1943
+ if (!input || typeof input !== 'object') return null;
1944
+ const legacyStrokeWithoutEnabled = input.enabled === undefined;
1945
+ const enabled = legacyStrokeWithoutEnabled ? true : input.enabled === true;
1946
+ if (!enabled) return null;
1947
+
1948
+ const width = Number(input.width ?? 1);
1949
+ if (!Number.isFinite(width) || width <= 0) return null;
1950
+
1951
+ const position =
1952
+ input.position === 'inside' ||
1953
+ input.position === 'center' ||
1954
+ input.position === 'outside'
1955
+ ? input.position
1956
+ : 'center';
1957
+ const opacity = clamp01(Number(input.opacity ?? 1));
1958
+ const color = normalizeColorWithOpacity(input.color, opacity);
1959
+ if (color === 'transparent') return null;
1960
+
1961
+ return { color, width, position };
1962
+ }
1963
+
1964
+ function normalizeButtonEffects(
1965
+ effectsInput: ButtonDropShadowEffect[] | undefined,
1966
+ legacyShadowInput: LegacyButtonShadow | undefined
1967
+ ): NormalizedButtonDropShadow[] {
1968
+ if (Array.isArray(effectsInput)) {
1969
+ return effectsInput
1970
+ .map(normalizeDropShadowEffect)
1971
+ .filter(
1972
+ (effect): effect is NormalizedButtonDropShadow => effect !== null
1973
+ );
1974
+ }
1975
+
1976
+ if (!legacyShadowInput || typeof legacyShadowInput !== 'object') {
1977
+ return [];
1978
+ }
1979
+ if (legacyShadowInput.enabled !== true) return [];
1980
+
1981
+ const migrated = normalizeDropShadowEffect({
1982
+ type: 'dropShadow',
1983
+ enabled: true,
1984
+ x: legacyShadowInput.x,
1985
+ y: legacyShadowInput.y,
1986
+ blur: legacyShadowInput.blur,
1987
+ spread: 0,
1988
+ color: legacyShadowInput.color,
1989
+ opacity: 1,
1990
+ });
1991
+
1992
+ return migrated ? [migrated] : [];
1993
+ }
1994
+
1995
+ function normalizeDropShadowEffect(
1996
+ effect: ButtonDropShadowEffect
1997
+ ): NormalizedButtonDropShadow | null {
1998
+ if (!effect || typeof effect !== 'object') return null;
1999
+ if ((effect.type ?? 'dropShadow') !== 'dropShadow') return null;
2000
+ if (effect.enabled === false) return null;
2001
+
2002
+ const opacity = clamp01(Number(effect.opacity ?? 1));
2003
+ const color = normalizeColorWithOpacity(effect.color, opacity);
2004
+ if (color === 'transparent') return null;
2005
+
2006
+ return {
2007
+ x: Number(effect.x ?? 0),
2008
+ y: Number(effect.y ?? 0),
2009
+ blur: Math.max(0, Number(effect.blur ?? 0)),
2010
+ spread: Number(effect.spread ?? 0),
2011
+ color,
2012
+ opacity,
2013
+ };
2014
+ }
2015
+
2016
+ function normalizeColorWithOpacity(
2017
+ input: string | undefined,
2018
+ opacity: number
2019
+ ): string {
2020
+ const raw = typeof input === 'string' ? input.trim() : '';
2021
+ const normalizedOpacity = clamp01(opacity);
2022
+ if (raw.startsWith('rgba(') || raw.startsWith('rgb(')) {
2023
+ return withOpacity(raw, normalizedOpacity);
2024
+ }
2025
+ return withOpacity(parseColor(input ?? '0xFF000000'), normalizedOpacity);
2026
+ }
2027
+
2028
+ function buildButtonWebShadowStyle(
2029
+ effects: NormalizedButtonDropShadow[]
2030
+ ): Record<string, any> | null {
2031
+ if (effects.length === 0) return null;
2032
+ const layers = effects.map(
2033
+ (effect) =>
2034
+ `${effect.x}px ${effect.y}px ${effect.blur}px ${effect.spread}px ${effect.color}`
2035
+ );
2036
+ return { boxShadow: layers.join(', ') };
2037
+ }
2038
+
2039
+ function buildButtonNativeShadowStyle(
2040
+ effect: NormalizedButtonDropShadow
2041
+ ): Record<string, any> {
2042
+ const rgba = parseRgbaColor(effect.color);
2043
+ return {
2044
+ // Android elevation requires a drawable host shape; keep it effectively invisible.
2045
+ backgroundColor: 'rgba(255,255,255,0.02)',
2046
+ shadowColor: rgba ? `rgba(${rgba.r},${rgba.g},${rgba.b},1)` : effect.color,
2047
+ shadowOffset: { width: effect.x, height: effect.y },
2048
+ shadowOpacity: rgba ? rgba.a : effect.opacity,
2049
+ shadowRadius: effect.blur / 2,
2050
+ elevation: Math.max(1, Math.round((effect.blur + Math.abs(effect.y)) / 3)),
2051
+ };
2052
+ }
2053
+
2054
+ function getButtonShadowLayerBounds(
2055
+ stroke: NormalizedButtonStroke | null,
2056
+ borderRadius: number
2057
+ ): Record<string, number> {
2058
+ const expandBy =
2059
+ !stroke || stroke.position === 'inside'
2060
+ ? 0
2061
+ : stroke.position === 'center'
2062
+ ? stroke.width / 2
2063
+ : stroke.width;
2064
+
2065
+ return {
2066
+ top: -expandBy,
2067
+ right: -expandBy,
2068
+ bottom: -expandBy,
2069
+ left: -expandBy,
2070
+ borderRadius: borderRadius + expandBy,
2071
+ };
2072
+ }
2073
+
2074
+ function renderButtonStrokeOverlay(
2075
+ stroke: NormalizedButtonStroke | null,
2076
+ borderRadius: number
2077
+ ) {
2078
+ if (!stroke || stroke.position === 'center') {
2079
+ return null;
2080
+ }
2081
+
2082
+ if (stroke.position === 'inside') {
2083
+ return (
2084
+ <View
2085
+ pointerEvents="none"
2086
+ style={{
2087
+ position: 'absolute',
2088
+ top: stroke.width / 2,
2089
+ left: stroke.width / 2,
2090
+ right: stroke.width / 2,
2091
+ bottom: stroke.width / 2,
2092
+ borderRadius: Math.max(0, borderRadius - stroke.width / 2),
2093
+ borderWidth: stroke.width,
2094
+ borderColor: stroke.color,
2095
+ }}
2096
+ />
2097
+ );
2098
+ }
2099
+
2100
+ return (
2101
+ <View
2102
+ pointerEvents="none"
2103
+ style={{
2104
+ position: 'absolute',
2105
+ top: -stroke.width,
2106
+ left: -stroke.width,
2107
+ right: -stroke.width,
2108
+ bottom: -stroke.width,
2109
+ borderRadius: borderRadius + stroke.width,
2110
+ borderWidth: stroke.width,
2111
+ borderColor: stroke.color,
2112
+ }}
2113
+ />
2114
+ );
1089
2115
  }
1090
2116
 
1091
2117
  function alignmentToGradient(value: string) {
@@ -1113,13 +2139,29 @@ function alignmentToGradient(value: string) {
1113
2139
  }
1114
2140
 
1115
2141
  function withOpacity(color: string, opacity: number): string {
1116
- if (color.startsWith('rgba')) {
1117
- return color.replace(
1118
- /rgba\((\d+),(\d+),(\d+),([\d.]+)\)/,
1119
- (_m, r, g, b) => `rgba(${r},${g},${b},${opacity})`
1120
- );
1121
- }
1122
- return color;
2142
+ const rgba = parseRgbaColor(color);
2143
+ if (!rgba) return color;
2144
+ return `rgba(${rgba.r},${rgba.g},${rgba.b},${clamp01(rgba.a * opacity)})`;
2145
+ }
2146
+
2147
+ function parseRgbaColor(
2148
+ color: string
2149
+ ): { r: number; g: number; b: number; a: number } | null {
2150
+ const match = color.match(
2151
+ /^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i
2152
+ );
2153
+ if (!match) return null;
2154
+ return {
2155
+ r: Math.round(Number(match[1])),
2156
+ g: Math.round(Number(match[2])),
2157
+ b: Math.round(Number(match[3])),
2158
+ a: match[4] === undefined ? 1 : clamp01(Number(match[4])),
2159
+ };
2160
+ }
2161
+
2162
+ function clamp01(value: number): number {
2163
+ if (!Number.isFinite(value)) return 1;
2164
+ return Math.max(0, Math.min(1, value));
1123
2165
  }
1124
2166
 
1125
2167
  function normalizeDimension(
@@ -1251,6 +2293,45 @@ function SduiImage({
1251
2293
  );
1252
2294
  }
1253
2295
 
2296
+ function resolveLottieStyle(
2297
+ width: number | string | undefined,
2298
+ height: number | string | undefined
2299
+ ) {
2300
+ const hasExplicitWidth = width !== undefined;
2301
+ const hasExplicitHeight = height !== undefined;
2302
+ const style: Record<string, number | string> = {};
2303
+
2304
+ if (hasExplicitWidth) {
2305
+ style.width = width;
2306
+ } else {
2307
+ style.width = 'auto';
2308
+ }
2309
+
2310
+ if (hasExplicitHeight) {
2311
+ style.height = height;
2312
+ } else {
2313
+ style.height = 'auto';
2314
+ }
2315
+
2316
+ if (width === '100%') {
2317
+ style.width = '100%';
2318
+ style.alignSelf = 'stretch';
2319
+ style.minWidth = 0;
2320
+ }
2321
+
2322
+ if (hasExplicitWidth && !hasExplicitHeight) {
2323
+ delete style.height;
2324
+ style.aspectRatio = 1;
2325
+ }
2326
+
2327
+ if (!hasExplicitWidth && hasExplicitHeight) {
2328
+ delete style.width;
2329
+ style.aspectRatio = 1;
2330
+ }
2331
+
2332
+ return style;
2333
+ }
2334
+
1254
2335
  function GradientText({
1255
2336
  text,
1256
2337
  gradient,
@@ -1278,6 +2359,81 @@ function GradientText({
1278
2359
  );
1279
2360
  }
1280
2361
 
2362
+ type TextInputStrokeStyle = 'solid' | 'dashed' | 'dotted';
2363
+
2364
+ type NormalizedTextInputStroke = {
2365
+ width: number;
2366
+ color: string;
2367
+ radius: number;
2368
+ style: TextInputStrokeStyle;
2369
+ };
2370
+
2371
+ function resolveTextInputBackgroundColor(properties: Record<string, any>) {
2372
+ const canonical = properties.backgroundColor;
2373
+ if (typeof canonical === 'string' && canonical.trim().length > 0) {
2374
+ return parseColor(canonical);
2375
+ }
2376
+
2377
+ const legacyBackground = properties.background;
2378
+ if (
2379
+ legacyBackground &&
2380
+ typeof legacyBackground === 'object' &&
2381
+ !Array.isArray(legacyBackground)
2382
+ ) {
2383
+ const type = String(legacyBackground.type ?? '').toLowerCase();
2384
+ if (type === 'color' && typeof legacyBackground.color === 'string') {
2385
+ return parseColor(legacyBackground.color);
2386
+ }
2387
+ if (type === 'gradient' && Array.isArray(legacyBackground.colors)) {
2388
+ const firstGradientColor = legacyBackground.colors.find(
2389
+ (value: unknown) => typeof value === 'string' && value.trim().length > 0
2390
+ );
2391
+ if (typeof firstGradientColor === 'string') {
2392
+ return parseColor(firstGradientColor);
2393
+ }
2394
+ }
2395
+ } else if (
2396
+ typeof legacyBackground === 'string' &&
2397
+ legacyBackground.trim().length > 0
2398
+ ) {
2399
+ return parseColor(legacyBackground);
2400
+ }
2401
+
2402
+ return parseColor('0xFFF0F0F0');
2403
+ }
2404
+
2405
+ function normalizeTextInputStroke(
2406
+ properties: Record<string, any>
2407
+ ): NormalizedTextInputStroke {
2408
+ const parsedWidth = Number(properties.strokeWidth ?? 0);
2409
+ const width =
2410
+ Number.isFinite(parsedWidth) && parsedWidth > 0 ? parsedWidth : 0;
2411
+ const enabled =
2412
+ typeof properties.strokeEnabled === 'boolean'
2413
+ ? properties.strokeEnabled
2414
+ : width > 0;
2415
+ const normalizedWidth = enabled ? width : 0;
2416
+ const color =
2417
+ normalizedWidth > 0
2418
+ ? parseColor(properties.strokeColor ?? '#000000')
2419
+ : 'transparent';
2420
+ const parsedRadius = Number(
2421
+ properties.strokeRadius ?? properties.borderRadius ?? 8
2422
+ );
2423
+ const radius = Number.isFinite(parsedRadius) ? Math.max(0, parsedRadius) : 8;
2424
+ const style: TextInputStrokeStyle =
2425
+ properties.strokeStyle === 'dashed' || properties.strokeStyle === 'dotted'
2426
+ ? properties.strokeStyle
2427
+ : 'solid';
2428
+
2429
+ return {
2430
+ width: normalizedWidth,
2431
+ color,
2432
+ radius,
2433
+ style,
2434
+ };
2435
+ }
2436
+
1281
2437
  function DynamicInput({
1282
2438
  initialValue,
1283
2439
  properties,
@@ -1315,20 +2471,25 @@ function DynamicInput({
1315
2471
  );
1316
2472
  const inputStyle = getTextStyle({ ...properties, height: null }, 'textColor');
1317
2473
 
1318
- const backgroundColor = parseColor(
1319
- properties.backgroundColor ?? '0xFFF0F0F0'
1320
- );
1321
- const borderRadius = Number(properties.borderRadius ?? 8);
2474
+ const backgroundColor = resolveTextInputBackgroundColor(properties);
2475
+ const stroke = normalizeTextInputStroke(properties);
2476
+ const parsedPadding = Number(properties.padding ?? 12);
2477
+ const padding = Number.isFinite(parsedPadding)
2478
+ ? Math.max(0, parsedPadding)
2479
+ : 12;
1322
2480
 
1323
2481
  return (
1324
- <View>
2482
+ <View style={{ width: '100%' }}>
1325
2483
  {label ? <Text style={labelStyle}>{label}</Text> : null}
1326
2484
  <View
1327
2485
  style={{
2486
+ width: '100%',
1328
2487
  backgroundColor,
1329
- borderRadius,
1330
- paddingHorizontal: 16,
1331
- paddingVertical: 10,
2488
+ borderRadius: stroke.radius,
2489
+ borderStyle: stroke.width > 0 ? stroke.style : undefined,
2490
+ borderWidth: stroke.width,
2491
+ borderColor: stroke.color,
2492
+ padding,
1332
2493
  }}
1333
2494
  >
1334
2495
  {mask ? (
@@ -1498,7 +2659,7 @@ function SelectionList({
1498
2659
 
1499
2660
  if (layout === 'row') {
1500
2661
  return (
1501
- <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
2662
+ <View style={{ width: '100%', flexDirection: 'row', flexWrap: 'wrap' }}>
1502
2663
  {options.map((option, index) => (
1503
2664
  <View
1504
2665
  key={`row-option-${index}`}
@@ -1515,7 +2676,7 @@ function SelectionList({
1515
2676
  const crossAxisCount = Number(properties.gridCrossAxisCount ?? 2);
1516
2677
  const aspectRatio = Number(properties.gridAspectRatio ?? 1);
1517
2678
  return (
1518
- <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
2679
+ <View style={{ width: '100%', flexDirection: 'row', flexWrap: 'wrap' }}>
1519
2680
  {options.map((option, index) => (
1520
2681
  <View
1521
2682
  key={`grid-option-${index}`}
@@ -1534,9 +2695,12 @@ function SelectionList({
1534
2695
  }
1535
2696
 
1536
2697
  return (
1537
- <View>
2698
+ <View style={{ width: '100%' }}>
1538
2699
  {options.map((option, index) => (
1539
- <View key={`column-option-${index}`} style={{ marginBottom: spacing }}>
2700
+ <View
2701
+ key={`column-option-${index}`}
2702
+ style={{ width: '100%', marginBottom: spacing }}
2703
+ >
1540
2704
  {renderItem(option, index)}
1541
2705
  </View>
1542
2706
  ))}
@@ -1653,7 +2817,11 @@ function WheelPicker({
1653
2817
  const parsed = Number(raw);
1654
2818
  if (!Number.isFinite(parsed)) return 1;
1655
2819
  return Math.max(1, Math.floor(parsed));
1656
- }, [properties.startIndex, properties.startItemIndex, properties.start_index]);
2820
+ }, [
2821
+ properties.startIndex,
2822
+ properties.startItemIndex,
2823
+ properties.start_index,
2824
+ ]);
1657
2825
 
1658
2826
  const clampWheelIndex = React.useCallback(
1659
2827
  (index: number) =>
@@ -1697,7 +2865,14 @@ function WheelPicker({
1697
2865
  }
1698
2866
  }
1699
2867
  },
1700
- [clampWheelIndex, onAction, onChanged, properties.autoGoNext, resolvedOptions, triggerLightHaptic]
2868
+ [
2869
+ clampWheelIndex,
2870
+ onAction,
2871
+ onChanged,
2872
+ properties.autoGoNext,
2873
+ resolvedOptions,
2874
+ triggerLightHaptic,
2875
+ ]
1701
2876
  );
1702
2877
 
1703
2878
  React.useEffect(() => {
@@ -1809,7 +2984,14 @@ function WheelPicker({
1809
2984
 
1810
2985
  if (layout === 'row') {
1811
2986
  return (
1812
- <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
2987
+ <View
2988
+ style={{
2989
+ width: '100%',
2990
+ alignSelf: 'stretch',
2991
+ flexDirection: 'row',
2992
+ flexWrap: 'wrap',
2993
+ }}
2994
+ >
1813
2995
  {resolvedOptions.map((option, index) => (
1814
2996
  <View
1815
2997
  key={`wheel-row-option-${index}`}
@@ -1826,7 +3008,14 @@ function WheelPicker({
1826
3008
  const crossAxisCount = Number(properties.gridCrossAxisCount ?? 2);
1827
3009
  const aspectRatio = Number(properties.gridAspectRatio ?? 1);
1828
3010
  return (
1829
- <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
3011
+ <View
3012
+ style={{
3013
+ width: '100%',
3014
+ alignSelf: 'stretch',
3015
+ flexDirection: 'row',
3016
+ flexWrap: 'wrap',
3017
+ }}
3018
+ >
1830
3019
  {resolvedOptions.map((option, index) => (
1831
3020
  <View
1832
3021
  key={`wheel-grid-option-${index}`}
@@ -1861,10 +3050,19 @@ function WheelPicker({
1861
3050
  const overlayBackgroundColor = parseColor(
1862
3051
  properties.wheelCenterBackgroundColor ?? '#24FFFFFF'
1863
3052
  );
3053
+ const wheelEdgeFadeOpacity = Math.max(
3054
+ 0,
3055
+ Math.min(1, Number(properties.wheelEdgeFadeOpacity ?? 0))
3056
+ );
3057
+ const wheelEdgeFadeColor = parseColor(
3058
+ properties.wheelEdgeFadeColor ?? '#FFFFFF'
3059
+ );
1864
3060
 
1865
3061
  const handleWheelMomentumEnd = (event: any) => {
1866
3062
  const offsetY = Number(event?.nativeEvent?.contentOffset?.y ?? 0);
1867
- const settledIndex = clampWheelIndex(Math.round(offsetY / wheelRowStride));
3063
+ const settledIndex = clampWheelIndex(
3064
+ Math.round(offsetY / wheelRowStride)
3065
+ );
1868
3066
  const targetOffset = settledIndex * wheelRowStride;
1869
3067
  if (Math.abs(targetOffset - offsetY) > 0.5) {
1870
3068
  wheelScrollRef.current?.scrollTo({
@@ -1876,9 +3074,18 @@ function WheelPicker({
1876
3074
  };
1877
3075
 
1878
3076
  return (
1879
- <View style={{ height: wheelContainerHeight, overflow: 'hidden' }}>
3077
+ <View
3078
+ style={{
3079
+ width: '100%',
3080
+ alignSelf: 'stretch',
3081
+ height: wheelContainerHeight,
3082
+ overflow: 'hidden',
3083
+ justifyContent: 'center',
3084
+ }}
3085
+ >
1880
3086
  <Animated.ScrollView
1881
3087
  ref={wheelScrollRef}
3088
+ style={{ width: '100%' }}
1882
3089
  showsVerticalScrollIndicator={false}
1883
3090
  bounces={false}
1884
3091
  decelerationRate="fast"
@@ -1886,7 +3093,11 @@ function WheelPicker({
1886
3093
  snapToAlignment="start"
1887
3094
  disableIntervalMomentum={false}
1888
3095
  onMomentumScrollEnd={handleWheelMomentumEnd}
1889
- contentContainerStyle={{ paddingVertical: centerPadding }}
3096
+ contentContainerStyle={{
3097
+ width: '100%',
3098
+ paddingVertical: centerPadding,
3099
+ alignItems: 'center',
3100
+ }}
1890
3101
  onScroll={Animated.event(
1891
3102
  [{ nativeEvent: { contentOffset: { y: wheelScrollY } } }],
1892
3103
  { useNativeDriver: true }
@@ -1926,8 +3137,10 @@ function WheelPicker({
1926
3137
  <Animated.View
1927
3138
  key={`wheel-column-option-${index}`}
1928
3139
  style={{
3140
+ width: '100%',
1929
3141
  height: wheelRowStride,
1930
3142
  justifyContent: 'center',
3143
+ alignItems: 'center',
1931
3144
  opacity,
1932
3145
  transform: [
1933
3146
  { perspective: 1200 },
@@ -1937,7 +3150,14 @@ function WheelPicker({
1937
3150
  ],
1938
3151
  }}
1939
3152
  >
1940
- <View style={{ minHeight: wheelItemHeight, justifyContent: 'center' }}>
3153
+ <View
3154
+ style={{
3155
+ width: '100%',
3156
+ minHeight: wheelItemHeight,
3157
+ justifyContent: 'center',
3158
+ alignItems: 'center',
3159
+ }}
3160
+ >
1941
3161
  {renderItem(option, index)}
1942
3162
  </View>
1943
3163
  </Animated.View>
@@ -1959,30 +3179,49 @@ function WheelPicker({
1959
3179
  backgroundColor: overlayBackgroundColor,
1960
3180
  }}
1961
3181
  />
1962
- <LinearGradient
1963
- pointerEvents="none"
1964
- colors={['rgba(255,255,255,0.92)', 'rgba(255,255,255,0)']}
1965
- style={{ position: 'absolute', top: 0, left: 0, right: 0, height: centerPadding }}
1966
- />
1967
- <LinearGradient
1968
- pointerEvents="none"
1969
- colors={['rgba(255,255,255,0)', 'rgba(255,255,255,0.92)']}
1970
- style={{
1971
- position: 'absolute',
1972
- bottom: 0,
1973
- left: 0,
1974
- right: 0,
1975
- height: centerPadding,
1976
- }}
1977
- />
3182
+ {wheelEdgeFadeOpacity > 0 && centerPadding > 0 ? (
3183
+ <LinearGradient
3184
+ pointerEvents="none"
3185
+ colors={[
3186
+ withOpacity(wheelEdgeFadeColor, wheelEdgeFadeOpacity),
3187
+ withOpacity(wheelEdgeFadeColor, 0),
3188
+ ]}
3189
+ style={{
3190
+ position: 'absolute',
3191
+ top: 0,
3192
+ left: 0,
3193
+ right: 0,
3194
+ height: centerPadding,
3195
+ }}
3196
+ />
3197
+ ) : null}
3198
+ {wheelEdgeFadeOpacity > 0 && centerPadding > 0 ? (
3199
+ <LinearGradient
3200
+ pointerEvents="none"
3201
+ colors={[
3202
+ withOpacity(wheelEdgeFadeColor, 0),
3203
+ withOpacity(wheelEdgeFadeColor, wheelEdgeFadeOpacity),
3204
+ ]}
3205
+ style={{
3206
+ position: 'absolute',
3207
+ bottom: 0,
3208
+ left: 0,
3209
+ right: 0,
3210
+ height: centerPadding,
3211
+ }}
3212
+ />
3213
+ ) : null}
1978
3214
  </View>
1979
3215
  );
1980
3216
  }
1981
3217
 
1982
3218
  return (
1983
- <View>
3219
+ <View style={{ width: '100%', alignSelf: 'stretch' }}>
1984
3220
  {resolvedOptions.map((option, index) => (
1985
- <View key={`wheel-list-option-${index}`} style={{ marginBottom: spacing }}>
3221
+ <View
3222
+ key={`wheel-list-option-${index}`}
3223
+ style={{ width: '100%', marginBottom: spacing }}
3224
+ >
1986
3225
  {renderItem(option, index)}
1987
3226
  </View>
1988
3227
  ))}
@@ -2066,7 +3305,10 @@ function buildWheelTemplateBaseContext(
2066
3305
  properties.start ?? properties.itemStart,
2067
3306
  context
2068
3307
  );
2069
- const step = resolveNumericValue(properties.step ?? properties.itemStep, context);
3308
+ const step = resolveNumericValue(
3309
+ properties.step ?? properties.itemStep,
3310
+ context
3311
+ );
2070
3312
 
2071
3313
  if (start !== null) context.start = start;
2072
3314
  if (step !== null) context.step = step;
@@ -2527,6 +3769,14 @@ function RadarChart({
2527
3769
  'color',
2528
3770
  '0xFF424242'
2529
3771
  );
3772
+ const resolvedAxisLabelFontSize = resolveRadarAxisLabelFontSize(
3773
+ axisLabelStyle.fontSize
3774
+ );
3775
+ const autoFitLabels = properties.autoFitLabels !== false;
3776
+ const resolvedLabelOffset = resolveRadarLabelOffset(
3777
+ properties.labelOffset,
3778
+ resolvedAxisLabelFontSize
3779
+ );
2530
3780
  const showLegend = properties.showLegend !== false;
2531
3781
  const legendPosition = (properties.legendPosition ?? 'bottom').toLowerCase();
2532
3782
  const legendSpacing = Number(properties.legendSpacing ?? 12);
@@ -2536,14 +3786,21 @@ function RadarChart({
2536
3786
  '0xFF212121'
2537
3787
  );
2538
3788
  const shape = (properties.shape ?? 'polygon').toLowerCase();
2539
-
2540
- const chartSize = Math.min(width, height);
2541
- const radius = Math.max(
2542
- chartSize / 2 -
2543
- Math.max(padding.left, padding.right, padding.top, padding.bottom),
2544
- 8
2545
- );
2546
- const center = { x: width / 2, y: height / 2 };
3789
+ const innerWidth = Math.max(1, width - padding.left - padding.right);
3790
+ const innerHeight = Math.max(1, height - padding.top - padding.bottom);
3791
+ const radius = calculateRadarChartRadius({
3792
+ width: innerWidth,
3793
+ height: innerHeight,
3794
+ padding: { left: 0, right: 0, top: 0, bottom: 0 },
3795
+ axisLabels: axes.map((axis) => axis.label),
3796
+ fontSize: resolvedAxisLabelFontSize,
3797
+ autoFitLabels,
3798
+ labelOffset: resolvedLabelOffset,
3799
+ });
3800
+ const center = {
3801
+ x: padding.left + innerWidth / 2,
3802
+ y: padding.top + innerHeight / 2,
3803
+ };
2547
3804
 
2548
3805
  const axisAngle = (Math.PI * 2) / axes.length;
2549
3806
 
@@ -2562,7 +3819,10 @@ function RadarChart({
2562
3819
  const points = dataset.values.map((value, index) => {
2563
3820
  const axis = normalizedAxes[index];
2564
3821
  const maxValue = axis?.maxValue ?? 0;
2565
- const ratio = (maxValue > 0 ? value / maxValue : 0) * lineProgress;
3822
+ const normalizedRatio = maxValue > 0 ? value / maxValue : 0;
3823
+ const ratio =
3824
+ Math.max(0, Math.min(1, normalizedRatio)) *
3825
+ Math.max(0, Math.min(1, lineProgress));
2566
3826
  const angle = index * axisAngle - Math.PI / 2;
2567
3827
  const x = center.x + radius * ratio * Math.cos(angle);
2568
3828
  const y = center.y + radius * ratio * Math.sin(angle);
@@ -2571,16 +3831,24 @@ function RadarChart({
2571
3831
  return { dataset, points };
2572
3832
  });
2573
3833
 
3834
+ const isVerticalLegend =
3835
+ legendPosition === 'left' || legendPosition === 'right';
2574
3836
  const legend = (
2575
- <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
3837
+ <View
3838
+ style={{
3839
+ flexDirection: isVerticalLegend ? 'column' : 'row',
3840
+ flexWrap: isVerticalLegend ? 'nowrap' : 'wrap',
3841
+ alignItems: isVerticalLegend ? 'flex-start' : 'center',
3842
+ }}
3843
+ >
2576
3844
  {datasets.map((dataset, index) => (
2577
3845
  <View
2578
3846
  key={`legend-${index}`}
2579
3847
  style={{
2580
3848
  flexDirection: 'row',
2581
3849
  alignItems: 'center',
2582
- marginRight: legendSpacing,
2583
- marginBottom: 6,
3850
+ marginRight: isVerticalLegend ? 0 : legendSpacing,
3851
+ marginBottom: isVerticalLegend ? legendSpacing : 6,
2584
3852
  }}
2585
3853
  >
2586
3854
  <View
@@ -2588,11 +3856,15 @@ function RadarChart({
2588
3856
  width: 12,
2589
3857
  height: 12,
2590
3858
  borderRadius: 6,
2591
- backgroundColor: dataset.borderColor,
3859
+ backgroundColor: dataset.fillColor ?? dataset.borderColor,
3860
+ borderWidth: 1,
3861
+ borderColor: dataset.borderColor,
2592
3862
  marginRight: 6,
2593
3863
  }}
2594
3864
  />
2595
- <Text style={legendStyle}>{dataset.label}</Text>
3865
+ <Text numberOfLines={1} style={legendStyle}>
3866
+ {dataset.label}
3867
+ </Text>
2596
3868
  </View>
2597
3869
  ))}
2598
3870
  </View>
@@ -2601,12 +3873,10 @@ function RadarChart({
2601
3873
  const chart = (
2602
3874
  <Animated.View
2603
3875
  style={{
2604
- width: normalizedWidth ?? width,
3876
+ width: isVerticalLegend ? width : normalizedWidth ?? '100%',
3877
+ maxWidth: '100%',
3878
+ flexShrink: 1,
2605
3879
  height,
2606
- paddingLeft: padding.left,
2607
- paddingRight: padding.right,
2608
- paddingTop: padding.top,
2609
- paddingBottom: padding.bottom,
2610
3880
  opacity: reveal,
2611
3881
  }}
2612
3882
  onLayout={(event) => {
@@ -2657,14 +3927,16 @@ function RadarChart({
2657
3927
  })}
2658
3928
  {axes.map((axis, index) => {
2659
3929
  const angle = index * axisAngle - Math.PI / 2;
2660
- const labelX = center.x + (radius + 12) * Math.cos(angle);
2661
- const labelY = center.y + (radius + 12) * Math.sin(angle);
3930
+ const labelX =
3931
+ center.x + (radius + resolvedLabelOffset) * Math.cos(angle);
3932
+ const labelY =
3933
+ center.y + (radius + resolvedLabelOffset) * Math.sin(angle);
2662
3934
  return (
2663
3935
  <SvgText
2664
3936
  key={`label-${index}`}
2665
3937
  x={labelX}
2666
3938
  y={labelY}
2667
- fontSize={axisLabelStyle.fontSize ?? 12}
3939
+ fontSize={resolvedAxisLabelFontSize}
2668
3940
  fontWeight={axisLabelStyle.fontWeight}
2669
3941
  fill={axisLabelStyle.color}
2670
3942
  textAnchor="middle"
@@ -2673,21 +3945,20 @@ function RadarChart({
2673
3945
  </SvgText>
2674
3946
  );
2675
3947
  })}
2676
- {datasetPolygons.map((entry, index) => (
2677
- <Polygon
2678
- key={`dataset-${index}`}
2679
- points={entry.points.join(' ')}
2680
- stroke={entry.dataset.borderColor}
2681
- strokeWidth={entry.dataset.borderWidth}
2682
- fill={entry.dataset.fillColor ?? 'transparent'}
2683
- strokeDasharray={
2684
- entry.dataset.borderStyle === 'dotted' ||
2685
- entry.dataset.borderStyle === 'dashed'
2686
- ? entry.dataset.dashArray ?? '6 4'
2687
- : undefined
2688
- }
2689
- />
2690
- ))}
3948
+ {datasetPolygons.map((entry, index) => {
3949
+ const strokePattern = resolveRadarStrokePattern(entry.dataset);
3950
+ return (
3951
+ <Polygon
3952
+ key={`dataset-${index}`}
3953
+ points={entry.points.join(' ')}
3954
+ stroke={entry.dataset.borderColor}
3955
+ strokeWidth={entry.dataset.borderWidth}
3956
+ fill={entry.dataset.fillColor ?? 'transparent'}
3957
+ strokeDasharray={strokePattern.strokeDasharray}
3958
+ strokeLinecap={strokePattern.strokeLinecap}
3959
+ />
3960
+ );
3961
+ })}
2691
3962
  {datasetPolygons.map((entry, datasetIndex) =>
2692
3963
  entry.dataset.showPoints
2693
3964
  ? entry.points.map((point, pointIndex) => {
@@ -2709,22 +3980,15 @@ function RadarChart({
2709
3980
  </Animated.View>
2710
3981
  );
2711
3982
 
2712
- const composed = (
2713
- <View style={{ backgroundColor }}>
2714
- {chart}
2715
- {showLegend && legendPosition === 'bottom' ? (
2716
- <View style={{ marginTop: 12 }}>{legend}</View>
2717
- ) : null}
2718
- </View>
2719
- );
2720
-
2721
- if (!showLegend || legendPosition === 'bottom') return composed;
3983
+ if (!showLegend) {
3984
+ return <View style={{ backgroundColor }}>{chart}</View>;
3985
+ }
2722
3986
 
2723
3987
  if (legendPosition === 'top') {
2724
3988
  return (
2725
- <View>
3989
+ <View style={{ backgroundColor, alignItems: 'center' }}>
2726
3990
  {legend}
2727
- <View style={{ height: 12 }} />
3991
+ <View style={{ height: legendSpacing }} />
2728
3992
  {chart}
2729
3993
  </View>
2730
3994
  );
@@ -2732,9 +3996,11 @@ function RadarChart({
2732
3996
 
2733
3997
  if (legendPosition === 'left') {
2734
3998
  return (
2735
- <View style={{ flexDirection: 'row' }}>
3999
+ <View
4000
+ style={{ backgroundColor, flexDirection: 'row', alignItems: 'center' }}
4001
+ >
2736
4002
  {legend}
2737
- <View style={{ width: 12 }} />
4003
+ <View style={{ width: legendSpacing }} />
2738
4004
  {chart}
2739
4005
  </View>
2740
4006
  );
@@ -2742,32 +4008,262 @@ function RadarChart({
2742
4008
 
2743
4009
  if (legendPosition === 'right') {
2744
4010
  return (
2745
- <View style={{ flexDirection: 'row' }}>
4011
+ <View
4012
+ style={{ backgroundColor, flexDirection: 'row', alignItems: 'center' }}
4013
+ >
2746
4014
  {chart}
2747
- <View style={{ width: 12 }} />
4015
+ <View style={{ width: legendSpacing }} />
2748
4016
  {legend}
2749
4017
  </View>
2750
4018
  );
2751
4019
  }
2752
4020
 
2753
- return composed;
4021
+ return (
4022
+ <View style={{ backgroundColor, alignItems: 'center' }}>
4023
+ {chart}
4024
+ <View style={{ height: legendSpacing }} />
4025
+ {legend}
4026
+ </View>
4027
+ );
2754
4028
  }
2755
4029
 
2756
4030
  export type RadarAxis = { label: string; maxValue: number };
4031
+ export type RadarBorderStyle = 'solid' | 'dotted' | 'dashed';
2757
4032
 
2758
4033
  export type RadarDataset = {
2759
4034
  label: string;
2760
4035
  values: number[];
2761
4036
  borderColor: string;
2762
4037
  borderWidth: number;
2763
- borderStyle: string;
4038
+ borderStyle: RadarBorderStyle;
2764
4039
  dashArray?: number[];
4040
+ dashLength?: number;
2765
4041
  showPoints: boolean;
2766
4042
  pointRadius: number;
2767
4043
  pointColor: string;
2768
4044
  fillColor?: string;
2769
4045
  };
2770
4046
 
4047
+ type RadarStrokePattern = {
4048
+ strokeDasharray?: string;
4049
+ strokeLinecap?: 'butt' | 'round';
4050
+ };
4051
+
4052
+ const RADAR_DASH_DEFAULT_LENGTH = 8;
4053
+ const RADAR_DASH_GAP_FACTOR = 0.6;
4054
+ const RADAR_DOT_DASH_LENGTH = 0.001;
4055
+ const RADAR_DOT_GAP_FACTOR = 2.4;
4056
+ const RADAR_DOT_MIN_GAP = 3;
4057
+ const radarDashConflictWarnings = new Set<string>();
4058
+
4059
+ function isDevelopmentEnvironment(): boolean {
4060
+ return process.env.NODE_ENV !== 'production';
4061
+ }
4062
+
4063
+ function warnRadarDashConflictOnce(label: string) {
4064
+ if (!isDevelopmentEnvironment()) return;
4065
+ if (radarDashConflictWarnings.has(label)) return;
4066
+ radarDashConflictWarnings.add(label);
4067
+ // Keep API backward-compatible but deterministic when both flags are passed.
4068
+ console.warn(
4069
+ `[RadarChart] Dataset "${label}" received both dotted=true and dashed=true. dashed takes precedence.`
4070
+ );
4071
+ }
4072
+
4073
+ function resolveRadarBorderStyle(
4074
+ dataset: Record<string, any>,
4075
+ fallbackLabel: string
4076
+ ): RadarBorderStyle {
4077
+ const dotted = dataset.dotted === true;
4078
+ const dashed = dataset.dashed === true;
4079
+
4080
+ if (dotted && dashed) {
4081
+ warnRadarDashConflictOnce(fallbackLabel);
4082
+ return 'dashed';
4083
+ }
4084
+ if (dashed) return 'dashed';
4085
+ if (dotted) return 'dotted';
4086
+
4087
+ const borderStyle = (dataset.borderStyle ?? 'solid').toString().toLowerCase();
4088
+ if (borderStyle === 'dotted' || borderStyle === 'dashed') {
4089
+ return borderStyle;
4090
+ }
4091
+ return 'solid';
4092
+ }
4093
+
4094
+ function resolveRadarStrokePattern(dataset: RadarDataset): RadarStrokePattern {
4095
+ if (dataset.borderStyle === 'dotted') {
4096
+ // Tiny dash + round caps renders circular dots more reliably than short dashes.
4097
+ const gap = Math.max(
4098
+ RADAR_DOT_MIN_GAP,
4099
+ dataset.borderWidth * RADAR_DOT_GAP_FACTOR
4100
+ );
4101
+ return {
4102
+ strokeDasharray: `${RADAR_DOT_DASH_LENGTH} ${gap}`,
4103
+ strokeLinecap: 'round',
4104
+ };
4105
+ }
4106
+
4107
+ if (dataset.borderStyle === 'dashed') {
4108
+ if (Array.isArray(dataset.dashArray) && dataset.dashArray.length > 0) {
4109
+ const dash = Math.max(
4110
+ 1,
4111
+ dataset.dashArray[0] ?? RADAR_DASH_DEFAULT_LENGTH
4112
+ );
4113
+ const gap = Math.max(
4114
+ 1,
4115
+ dataset.dashArray[1] ?? dash * RADAR_DASH_GAP_FACTOR
4116
+ );
4117
+ return {
4118
+ strokeDasharray: `${dash} ${gap}`,
4119
+ strokeLinecap: 'butt',
4120
+ };
4121
+ }
4122
+
4123
+ const dashLength =
4124
+ typeof dataset.dashLength === 'number' &&
4125
+ Number.isFinite(dataset.dashLength) &&
4126
+ dataset.dashLength > 0
4127
+ ? dataset.dashLength
4128
+ : RADAR_DASH_DEFAULT_LENGTH;
4129
+ const gap = Math.max(2, dashLength * RADAR_DASH_GAP_FACTOR);
4130
+ return {
4131
+ strokeDasharray: `${dashLength} ${gap}`,
4132
+ strokeLinecap: 'butt',
4133
+ };
4134
+ }
4135
+
4136
+ return {};
4137
+ }
4138
+
4139
+ export const RADAR_LABEL_ESTIMATION = {
4140
+ minRadius: 10,
4141
+ safeEdge: 4,
4142
+ minLabelWidthFactor: 1.2,
4143
+ avgCharWidthFactor: 0.58,
4144
+ lineHeightFactor: 1.2,
4145
+ defaultFontSize: 14,
4146
+ minLabelOffset: 20,
4147
+ labelOffsetFactor: 1.5,
4148
+ } as const;
4149
+
4150
+ export function resolveRadarAxisLabelFontSize(value: unknown): number {
4151
+ const parsed = Number(value);
4152
+ if (!Number.isFinite(parsed) || parsed <= 0) {
4153
+ return RADAR_LABEL_ESTIMATION.defaultFontSize;
4154
+ }
4155
+ return parsed;
4156
+ }
4157
+
4158
+ export function resolveRadarLabelOffset(
4159
+ value: unknown,
4160
+ fontSize: number
4161
+ ): number {
4162
+ const parsed = Number(value);
4163
+ if (Number.isFinite(parsed) && parsed > 0) {
4164
+ return parsed;
4165
+ }
4166
+ return Math.max(
4167
+ RADAR_LABEL_ESTIMATION.minLabelOffset,
4168
+ fontSize * RADAR_LABEL_ESTIMATION.labelOffsetFactor
4169
+ );
4170
+ }
4171
+
4172
+ export function estimateRadarLabelSize(
4173
+ label: string,
4174
+ fontSize: number
4175
+ ): {
4176
+ width: number;
4177
+ height: number;
4178
+ } {
4179
+ const safeLabel = `${label ?? ''}`;
4180
+ const width = Math.max(
4181
+ safeLabel.length * fontSize * RADAR_LABEL_ESTIMATION.avgCharWidthFactor,
4182
+ fontSize * RADAR_LABEL_ESTIMATION.minLabelWidthFactor
4183
+ );
4184
+ const height = fontSize * RADAR_LABEL_ESTIMATION.lineHeightFactor;
4185
+ return { width, height };
4186
+ }
4187
+
4188
+ export function calculateRadarChartRadius({
4189
+ width,
4190
+ height,
4191
+ padding,
4192
+ axisLabels,
4193
+ fontSize,
4194
+ autoFitLabels = true,
4195
+ labelOffset,
4196
+ }: {
4197
+ width: number;
4198
+ height: number;
4199
+ padding: { left: number; right: number; top: number; bottom: number };
4200
+ axisLabels: string[];
4201
+ fontSize: number;
4202
+ autoFitLabels?: boolean;
4203
+ labelOffset: number;
4204
+ }): number {
4205
+ const center = { x: width / 2, y: height / 2 };
4206
+ const baseRadius = Math.max(
4207
+ Math.min(width, height) / 2 -
4208
+ Math.max(padding.left, padding.right, padding.top, padding.bottom),
4209
+ RADAR_LABEL_ESTIMATION.minRadius
4210
+ );
4211
+ if (!autoFitLabels || axisLabels.length === 0) {
4212
+ return baseRadius;
4213
+ }
4214
+
4215
+ const angleStep = (Math.PI * 2) / axisLabels.length;
4216
+ let maxAllowedRadius = baseRadius;
4217
+
4218
+ for (let index = 0; index < axisLabels.length; index += 1) {
4219
+ const { width: labelWidth, height: labelHeight } = estimateRadarLabelSize(
4220
+ axisLabels[index] ?? '',
4221
+ fontSize
4222
+ );
4223
+ const halfWidth = labelWidth / 2;
4224
+ const halfHeight = labelHeight / 2;
4225
+ const angle = index * angleStep - Math.PI / 2;
4226
+ const dx = Math.cos(angle);
4227
+ const dy = Math.sin(angle);
4228
+
4229
+ // Solve per-axis line constraints so estimated label bounds stay inside.
4230
+ if (dx > 0.0001) {
4231
+ maxAllowedRadius = Math.min(
4232
+ maxAllowedRadius,
4233
+ (width - RADAR_LABEL_ESTIMATION.safeEdge - halfWidth - center.x) / dx -
4234
+ labelOffset
4235
+ );
4236
+ } else if (dx < -0.0001) {
4237
+ maxAllowedRadius = Math.min(
4238
+ maxAllowedRadius,
4239
+ (RADAR_LABEL_ESTIMATION.safeEdge + halfWidth - center.x) / dx -
4240
+ labelOffset
4241
+ );
4242
+ }
4243
+
4244
+ if (dy > 0.0001) {
4245
+ maxAllowedRadius = Math.min(
4246
+ maxAllowedRadius,
4247
+ (height - RADAR_LABEL_ESTIMATION.safeEdge - halfHeight - center.y) /
4248
+ dy -
4249
+ labelOffset
4250
+ );
4251
+ } else if (dy < -0.0001) {
4252
+ maxAllowedRadius = Math.min(
4253
+ maxAllowedRadius,
4254
+ (RADAR_LABEL_ESTIMATION.safeEdge + halfHeight - center.y) / dy -
4255
+ labelOffset
4256
+ );
4257
+ }
4258
+ }
4259
+
4260
+ const boundedRadius = Math.min(maxAllowedRadius, baseRadius);
4261
+ if (!Number.isFinite(boundedRadius)) {
4262
+ return baseRadius;
4263
+ }
4264
+ return Math.max(boundedRadius, RADAR_LABEL_ESTIMATION.minRadius);
4265
+ }
4266
+
2771
4267
  export function parseRadarAxes(rawAxes: any): RadarAxis[] {
2772
4268
  if (!Array.isArray(rawAxes)) return [];
2773
4269
  const axes: RadarAxis[] = [];
@@ -2776,7 +4272,9 @@ export function parseRadarAxes(rawAxes: any): RadarAxis[] {
2776
4272
  const label = axis.label ?? '';
2777
4273
  if (!label) return;
2778
4274
  const maxValue = resolveNumericValue(axis.maxValue, {}) ?? 100;
2779
- axes.push({ label, maxValue: maxValue > 0 ? maxValue : 100 });
4275
+ const normalizedMaxValue =
4276
+ Number.isFinite(maxValue) && maxValue > 0 ? maxValue : 100;
4277
+ axes.push({ label, maxValue: normalizedMaxValue });
2780
4278
  });
2781
4279
  return axes;
2782
4280
  }
@@ -2788,14 +4286,16 @@ export function parseRadarDatasets(
2788
4286
  ): RadarDataset[] {
2789
4287
  if (!Array.isArray(raw) || axisLength === 0) return [];
2790
4288
  const datasets: RadarDataset[] = [];
2791
- raw.forEach((dataset) => {
4289
+ raw.forEach((dataset, datasetIndex) => {
2792
4290
  if (!dataset || typeof dataset !== 'object') return;
2793
4291
  const valuesRaw = Array.isArray(dataset.data) ? dataset.data : [];
2794
4292
  const values: number[] = [];
2795
4293
  for (let i = 0; i < axisLength; i += 1) {
2796
4294
  const rawValue = i < valuesRaw.length ? valuesRaw[i] : null;
2797
- const resolved = resolveNumericValue(rawValue, formData) ?? 0;
2798
- values.push(resolved);
4295
+ const resolved = resolveNumericValue(rawValue, formData);
4296
+ values.push(
4297
+ typeof resolved === 'number' && Number.isFinite(resolved) ? resolved : 0
4298
+ );
2799
4299
  }
2800
4300
 
2801
4301
  const label = dataset.label ?? `Dataset ${datasets.length + 1}`;
@@ -2806,18 +4306,30 @@ export function parseRadarDatasets(
2806
4306
  const pointColor = parseColor(
2807
4307
  dataset.pointColor ?? dataset.borderColor ?? '0xFF2196F3'
2808
4308
  );
2809
- const borderWidth = Number(dataset.borderWidth ?? 2);
2810
- const pointRadius = Number(dataset.pointRadius ?? 4);
2811
- const borderStyle = (dataset.borderStyle ?? 'solid')
2812
- .toString()
2813
- .toLowerCase();
2814
- const normalizedStyle =
2815
- borderStyle === 'dotted' || borderStyle === 'dashed'
2816
- ? borderStyle
2817
- : 'solid';
4309
+ const rawBorderWidth = Number(dataset.borderWidth ?? 2);
4310
+ const borderWidth =
4311
+ Number.isFinite(rawBorderWidth) && rawBorderWidth > 0
4312
+ ? rawBorderWidth
4313
+ : 2;
4314
+ const rawPointRadius = Number(dataset.pointRadius ?? 4);
4315
+ const pointRadius =
4316
+ Number.isFinite(rawPointRadius) && rawPointRadius >= 0
4317
+ ? rawPointRadius
4318
+ : 4;
4319
+ const normalizedStyle = resolveRadarBorderStyle(
4320
+ dataset,
4321
+ label ?? `Dataset ${datasetIndex + 1}`
4322
+ );
2818
4323
  const dashArray = Array.isArray(dataset.dashArray)
2819
- ? dataset.dashArray.map((val: any) => Number(val))
4324
+ ? dataset.dashArray
4325
+ .map((val: any) => Number(val))
4326
+ .filter((val: number) => Number.isFinite(val) && val > 0)
2820
4327
  : undefined;
4328
+ const rawDashLength = Number(dataset.dashLength);
4329
+ const dashLength =
4330
+ Number.isFinite(rawDashLength) && rawDashLength > 0
4331
+ ? rawDashLength
4332
+ : undefined;
2821
4333
  const showPoints = dataset.showPoints !== false;
2822
4334
 
2823
4335
  datasets.push({
@@ -2827,6 +4339,7 @@ export function parseRadarDatasets(
2827
4339
  borderWidth,
2828
4340
  borderStyle: normalizedStyle,
2829
4341
  dashArray,
4342
+ dashLength,
2830
4343
  showPoints,
2831
4344
  pointRadius,
2832
4345
  pointColor,
@@ -2844,7 +4357,9 @@ export function normalizeAxisMaximums(
2844
4357
  let maxValue = axis.maxValue;
2845
4358
  datasets.forEach((dataset) => {
2846
4359
  const value = dataset.values[index];
2847
- if (value !== undefined && value > maxValue) maxValue = value;
4360
+ if (value !== undefined && Number.isFinite(value) && value > maxValue) {
4361
+ maxValue = value;
4362
+ }
2848
4363
  });
2849
4364
  return { ...axis, maxValue };
2850
4365
  });
@@ -2854,12 +4369,15 @@ function resolveRadarWidth(
2854
4369
  parsedWidth: number | string | undefined,
2855
4370
  measuredWidth: number
2856
4371
  ): number {
4372
+ if (measuredWidth > 0) {
4373
+ if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
4374
+ return Math.max(1, Math.min(parsedWidth, measuredWidth));
4375
+ }
4376
+ return Math.max(1, measuredWidth);
4377
+ }
2857
4378
  if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
2858
4379
  return parsedWidth;
2859
4380
  }
2860
- if (measuredWidth > 0) {
2861
- return measuredWidth;
2862
- }
2863
4381
  return 300;
2864
4382
  }
2865
4383