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,8 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useMemo } from 'react';
4
- import { Animated, Pressable, ScrollView, StyleSheet, Text, TextInput, View, Image, Vibration } from 'react-native';
5
- import { SafeAreaView } from 'react-native-safe-area-context';
4
+ import { Animated, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, Image, Vibration } from 'react-native';
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
6
  import MaskedView from '@react-native-masked-view/masked-view';
7
7
  import LinearGradient from 'react-native-linear-gradient';
8
8
  import LottieView from 'lottie-react-native';
@@ -24,13 +24,30 @@ const styles = StyleSheet.create({
24
24
  whiteBg: {
25
25
  backgroundColor: '#ffffff'
26
26
  },
27
- safeArea: {
27
+ contentLayer: {
28
28
  flex: 1
29
29
  },
30
30
  progressWrapper: {
31
31
  width: '100%'
32
32
  }
33
33
  });
34
+ function resolvePageScrollAxis(screenData) {
35
+ const raw = String(screenData.pageScroll ?? screenData.scrollDirection ?? (screenData.scrollable === true ? 'vertical' : 'none')).toLowerCase();
36
+ if (raw === 'none') return 'none';
37
+ return 'vertical';
38
+ }
39
+ function getAxisBoundsForPageScroll(pageScroll) {
40
+ if (pageScroll === 'vertical') {
41
+ return {
42
+ widthBounded: true,
43
+ heightBounded: false
44
+ };
45
+ }
46
+ return {
47
+ widthBounded: true,
48
+ heightBounded: true
49
+ };
50
+ }
34
51
  export default function FlowboardRenderer(props) {
35
52
  const {
36
53
  screenData,
@@ -49,8 +66,12 @@ export default function FlowboardRenderer(props) {
49
66
  const progressThickness = Number(screenData.progressThickness ?? 4);
50
67
  const progressRadius = Number(screenData.progressRadius ?? 0);
51
68
  const progressStyle = screenData.progressStyle ?? 'linear';
52
- const scrollable = screenData.scrollable === true;
69
+ const pageScroll = resolvePageScrollAxis(screenData);
70
+ const pageAxisBounds = getAxisBoundsForPageScroll(pageScroll);
71
+ const scrollable = pageScroll !== 'none';
53
72
  const safeArea = screenData.safeArea !== false;
73
+ const safeAreaInsets = useSafeAreaInsets();
74
+ const rootAxis = 'vertical';
54
75
  const padding = parseInsets(screenData.padding);
55
76
  const contentPaddingStyle = insetsToStyle(padding);
56
77
  const progressPaddingStyle = {
@@ -59,6 +80,12 @@ export default function FlowboardRenderer(props) {
59
80
  paddingLeft: padding.left
60
81
  };
61
82
  const rootCrossAxisAlignment = screenData.crossAxisAlignment ?? screenData.crossAxis;
83
+ const safeAreaPaddingStyle = safeArea ? {
84
+ paddingTop: safeAreaInsets.top,
85
+ paddingRight: safeAreaInsets.right,
86
+ paddingBottom: safeAreaInsets.bottom,
87
+ paddingLeft: safeAreaInsets.left
88
+ } : null;
62
89
  const content = /*#__PURE__*/_jsxs(View, {
63
90
  style: {
64
91
  flex: 1
@@ -69,6 +96,9 @@ export default function FlowboardRenderer(props) {
69
96
  }],
70
97
  children: renderProgressBar(currentIndex, totalScreens, progressColor, progressThickness, progressRadius, progressStyle)
71
98
  }), scrollable ? /*#__PURE__*/_jsx(ScrollView, {
99
+ horizontal: false,
100
+ keyboardShouldPersistTaps: "handled",
101
+ keyboardDismissMode: Platform.OS === 'ios' ? 'interactive' : 'on-drag',
72
102
  contentContainerStyle: {
73
103
  ...contentPaddingStyle,
74
104
  paddingTop: showProgress ? 0 : padding.top
@@ -76,14 +106,20 @@ export default function FlowboardRenderer(props) {
76
106
  children: /*#__PURE__*/_jsx(View, {
77
107
  style: {
78
108
  flexGrow: 1,
109
+ flexDirection: 'column',
79
110
  justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
80
- alignItems: parseRootCrossAlignment(rootCrossAxisAlignment)
111
+ alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
112
+ minHeight: 0,
113
+ minWidth: 0
81
114
  },
82
115
  children: childrenData.map((child, index) => buildWidget(child, {
83
116
  onAction,
84
117
  formData,
85
118
  onInputChange,
86
119
  allowFlexExpansion: true,
120
+ parentFlexAxis: rootAxis,
121
+ parentMainAxisBounded: pageAxisBounds.heightBounded,
122
+ pageAxisBounds,
87
123
  screenData,
88
124
  enableFontAwesomeIcons,
89
125
  key: `child-${index}`
@@ -102,6 +138,9 @@ export default function FlowboardRenderer(props) {
102
138
  formData,
103
139
  onInputChange,
104
140
  allowFlexExpansion: true,
141
+ parentFlexAxis: rootAxis,
142
+ parentMainAxisBounded: true,
143
+ pageAxisBounds,
105
144
  screenData,
106
145
  enableFontAwesomeIcons,
107
146
  key: `child-${index}`
@@ -110,12 +149,15 @@ export default function FlowboardRenderer(props) {
110
149
  });
111
150
  return /*#__PURE__*/_jsxs(View, {
112
151
  style: styles.root,
113
- children: [renderBackground(backgroundData, bgColorCode), safeArea ? /*#__PURE__*/_jsx(SafeAreaView, {
114
- style: styles.safeArea,
115
- mode: "padding",
116
- edges: ['top', 'right', 'bottom', 'left'],
117
- children: content
118
- }) : content]
152
+ children: [renderBackground(backgroundData, bgColorCode), /*#__PURE__*/_jsx(KeyboardAvoidingView, {
153
+ style: styles.contentLayer,
154
+ behavior: Platform.OS === 'ios' ? 'padding' : Platform.OS === 'android' ? 'height' : undefined,
155
+ keyboardVerticalOffset: 0,
156
+ children: /*#__PURE__*/_jsx(View, {
157
+ style: [styles.contentLayer, safeAreaPaddingStyle],
158
+ children: content
159
+ })
160
+ })]
119
161
  });
120
162
  }
121
163
  function renderBackground(bgData, legacyColorCode) {
@@ -272,17 +314,411 @@ function renderProgressBar(currentIndex, totalScreens, color, thickness, radius,
272
314
  })
273
315
  });
274
316
  }
317
+ const STACK_SPACE_DISTRIBUTIONS = new Set(['spaceBetween', 'spaceAround', 'spaceEvenly']);
318
+ function normalizeStackSpacingConfig(value) {
319
+ if (typeof value === 'number' && Number.isFinite(value)) {
320
+ return {
321
+ x: value,
322
+ y: value
323
+ };
324
+ }
325
+ if (!value || typeof value !== 'object') {
326
+ return {
327
+ x: 0,
328
+ y: 0
329
+ };
330
+ }
331
+ const x = Number(value.x ?? value.horizontal);
332
+ const y = Number(value.y ?? value.vertical);
333
+ const top = Number(value.top);
334
+ const right = Number(value.right);
335
+ const bottom = Number(value.bottom);
336
+ const left = Number(value.left);
337
+ const normalized = {};
338
+ if (!Number.isNaN(x)) normalized.x = x;
339
+ if (!Number.isNaN(y)) normalized.y = y;
340
+ if (!Number.isNaN(top)) normalized.top = top;
341
+ if (!Number.isNaN(right)) normalized.right = right;
342
+ if (!Number.isNaN(bottom)) normalized.bottom = bottom;
343
+ if (!Number.isNaN(left)) normalized.left = left;
344
+ if (Object.keys(normalized).length === 0) {
345
+ return {
346
+ x: 0,
347
+ y: 0
348
+ };
349
+ }
350
+ return normalized;
351
+ }
352
+ function stackSpacingToInsets(value) {
353
+ const normalized = normalizeStackSpacingConfig(value);
354
+ return parseInsets({
355
+ horizontal: normalized.x,
356
+ vertical: normalized.y,
357
+ top: normalized.top,
358
+ right: normalized.right,
359
+ bottom: normalized.bottom,
360
+ left: normalized.left
361
+ });
362
+ }
363
+ function isLegacyOverlayAlignment(value) {
364
+ return ['topLeft', 'topCenter', 'topRight', 'centerLeft', 'center', 'centerRight', 'bottomLeft', 'bottomCenter', 'bottomRight'].includes(String(value ?? ''));
365
+ }
366
+ function normalizeStackAxis(type, props) {
367
+ const explicit = String(props.axis ?? '').toLowerCase();
368
+ if (explicit === 'vertical' || explicit === 'horizontal' || explicit === 'overlay') {
369
+ return explicit;
370
+ }
371
+ if (type === 'container') return 'overlay';
372
+ if (type === 'row') return 'horizontal';
373
+ if (type === 'column') return 'vertical';
374
+ if (type === 'layout') {
375
+ return props.direction === 'horizontal' ? 'horizontal' : 'vertical';
376
+ }
377
+ if (type === 'stack' && (props.fit !== undefined || isLegacyOverlayAlignment(props.alignment))) {
378
+ return 'overlay';
379
+ }
380
+ return 'vertical';
381
+ }
382
+ function normalizeStackDistribution(value) {
383
+ const normalized = String(value ?? '').trim();
384
+ if (normalized === 'center' || normalized === 'end' || normalized === 'spaceBetween' || normalized === 'spaceAround' || normalized === 'spaceEvenly') {
385
+ return normalized;
386
+ }
387
+ return 'start';
388
+ }
389
+ function normalizeStackCrossAlignment(value) {
390
+ const raw = String(value ?? '').toLowerCase();
391
+ if (raw === 'center' || raw.includes('center')) return 'center';
392
+ if (raw === 'end' || raw.endsWith('right') || raw.startsWith('bottom')) return 'end';
393
+ return 'start';
394
+ }
395
+ function normalizeOverlayAlignment(value) {
396
+ const raw = String(value ?? '').trim();
397
+ switch (raw) {
398
+ case 'topStart':
399
+ case 'top':
400
+ case 'topEnd':
401
+ case 'start':
402
+ case 'center':
403
+ case 'end':
404
+ case 'bottomStart':
405
+ case 'bottom':
406
+ case 'bottomEnd':
407
+ return raw;
408
+ case 'topLeft':
409
+ return 'topStart';
410
+ case 'topCenter':
411
+ return 'top';
412
+ case 'topRight':
413
+ return 'topEnd';
414
+ case 'centerLeft':
415
+ return 'start';
416
+ case 'centerRight':
417
+ return 'end';
418
+ case 'bottomLeft':
419
+ return 'bottomStart';
420
+ case 'bottomCenter':
421
+ return 'bottom';
422
+ case 'bottomRight':
423
+ return 'bottomEnd';
424
+ default:
425
+ return 'center';
426
+ }
427
+ }
428
+ function isFillDimensionValue(value) {
429
+ if (typeof value === 'number') return value === Number.POSITIVE_INFINITY;
430
+ if (typeof value !== 'string') return false;
431
+ const normalized = value.trim().toLowerCase();
432
+ return normalized === 'infinity' || normalized === 'double.infinity' || normalized === '+infinity' || normalized === '100%';
433
+ }
434
+ function isFixedDimensionValue(value) {
435
+ if (isFillDimensionValue(value)) return false;
436
+ const parsed = Number(value);
437
+ return Number.isFinite(parsed);
438
+ }
439
+ function normalizeStackSize(props, axis) {
440
+ const widthMode = String(props.size?.width ?? '').toLowerCase();
441
+ const heightMode = String(props.size?.height ?? '').toLowerCase();
442
+ const width = widthMode === 'fill' || widthMode === 'fit' || widthMode === 'fixed' ? widthMode : props.fit === 'expand' || props.width === 'infinity' ? 'fill' : isFixedDimensionValue(props.width) ? 'fixed' : axis === 'horizontal' ? 'fit' : 'fill';
443
+ const height = heightMode === 'fill' || heightMode === 'fit' || heightMode === 'fixed' ? heightMode : props.fit === 'expand' || props.height === 'infinity' ? 'fill' : isFixedDimensionValue(props.height) ? 'fixed' : axis === 'vertical' ? 'fit' : 'fill';
444
+ return {
445
+ width,
446
+ height
447
+ };
448
+ }
449
+ function normalizeStackFill(props) {
450
+ if (props.fill && typeof props.fill === 'object') {
451
+ const next = {
452
+ ...props.fill
453
+ };
454
+ if (!next.type && next.color) next.type = 'solid';
455
+ return next;
456
+ }
457
+ if (props.background && typeof props.background === 'object') {
458
+ if (props.background.type === 'color') {
459
+ return {
460
+ type: 'solid',
461
+ color: props.background.color
462
+ };
463
+ }
464
+ return {
465
+ ...props.background
466
+ };
467
+ }
468
+ if (props.backgroundColor) {
469
+ return {
470
+ type: 'solid',
471
+ color: props.backgroundColor
472
+ };
473
+ }
474
+ return undefined;
475
+ }
476
+ function normalizeStackBorder(props) {
477
+ if (props.border && typeof props.border === 'object') {
478
+ return {
479
+ ...props.border
480
+ };
481
+ }
482
+ if (props.borderColor || props.borderWidth !== undefined) {
483
+ return {
484
+ width: Number(props.borderWidth ?? 1),
485
+ color: props.borderColor
486
+ };
487
+ }
488
+ return undefined;
489
+ }
490
+ function normalizeStackShadow(props) {
491
+ if (props.shadow && typeof props.shadow === 'object') {
492
+ return {
493
+ ...props.shadow
494
+ };
495
+ }
496
+ return undefined;
497
+ }
498
+ function spacingToInsets(value) {
499
+ const normalized = normalizeStackSpacingConfig(value);
500
+ const x = normalized.x ?? 0;
501
+ const y = normalized.y ?? 0;
502
+ return {
503
+ top: normalized.top ?? y,
504
+ right: normalized.right ?? x,
505
+ bottom: normalized.bottom ?? y,
506
+ left: normalized.left ?? x
507
+ };
508
+ }
509
+ function insetsToSpacing(insets) {
510
+ if (insets.top === insets.bottom && insets.left === insets.right) {
511
+ return {
512
+ x: insets.left,
513
+ y: insets.top
514
+ };
515
+ }
516
+ return insets;
517
+ }
518
+ function mergeStackProps(parentProps, childProps) {
519
+ const parentAxis = String(parentProps.axis ?? 'vertical');
520
+ const childAxis = String(childProps.axis ?? 'vertical');
521
+ let axis = childAxis;
522
+ if (parentAxis !== childAxis) {
523
+ if (parentAxis === 'overlay' && childAxis !== 'overlay') axis = childAxis;else if (childAxis === 'overlay' && parentAxis !== 'overlay') axis = parentAxis;
524
+ }
525
+ const axisSource = axis === parentAxis && axis !== childAxis ? parentProps : childProps;
526
+ const fallbackAxisSource = axisSource === childProps ? parentProps : childProps;
527
+ const parentPadding = spacingToInsets(parentProps.layout?.padding);
528
+ const childPadding = spacingToInsets(childProps.layout?.padding);
529
+ const parentMargin = spacingToInsets(parentProps.layout?.margin);
530
+ const childMargin = spacingToInsets(childProps.layout?.margin);
531
+ const mergedProps = {
532
+ axis,
533
+ alignment: axisSource.alignment ?? fallbackAxisSource.alignment ?? 'start',
534
+ distribution: axisSource.distribution ?? fallbackAxisSource.distribution ?? 'start',
535
+ childSpacing: Number(axisSource.childSpacing ?? fallbackAxisSource.childSpacing ?? 0) || 0,
536
+ size: {
537
+ width: childProps.size?.width ?? parentProps.size?.width ?? 'fill',
538
+ height: childProps.size?.height ?? parentProps.size?.height ?? 'fit'
539
+ },
540
+ layout: {
541
+ padding: insetsToSpacing({
542
+ top: parentPadding.top + childPadding.top,
543
+ right: parentPadding.right + childPadding.right,
544
+ bottom: parentPadding.bottom + childPadding.bottom,
545
+ left: parentPadding.left + childPadding.left
546
+ }),
547
+ margin: insetsToSpacing({
548
+ top: parentMargin.top + childMargin.top,
549
+ right: parentMargin.right + childMargin.right,
550
+ bottom: parentMargin.bottom + childMargin.bottom,
551
+ left: parentMargin.left + childMargin.left
552
+ })
553
+ },
554
+ appearance: {
555
+ shape: 'rectangle',
556
+ cornerRadius: Number(childProps.appearance?.cornerRadius ?? parentProps.appearance?.cornerRadius ?? 0)
557
+ }
558
+ };
559
+ if (axis === 'overlay') {
560
+ mergedProps.overlayAlignment = childProps.overlayAlignment ?? parentProps.overlayAlignment ?? 'center';
561
+ }
562
+ if (parentProps.fill || childProps.fill) {
563
+ mergedProps.fill = childProps.fill ?? parentProps.fill;
564
+ }
565
+ if (parentProps.border || childProps.border) {
566
+ mergedProps.border = childProps.border ?? parentProps.border;
567
+ }
568
+ if (parentProps.shadow || childProps.shadow) {
569
+ mergedProps.shadow = childProps.shadow ?? parentProps.shadow;
570
+ }
571
+ if (childProps.width !== undefined || parentProps.width !== undefined) {
572
+ mergedProps.width = childProps.width ?? parentProps.width;
573
+ }
574
+ if (childProps.height !== undefined || parentProps.height !== undefined) {
575
+ mergedProps.height = childProps.height ?? parentProps.height;
576
+ }
577
+ return mergedProps;
578
+ }
579
+ function normalizeStackLikeNode(json) {
580
+ if (!json || typeof json !== 'object') return json;
581
+ const type = String(json.type ?? '');
582
+ if (!['stack', 'container', 'layout', 'row', 'column', 'grid'].includes(type)) {
583
+ return json;
584
+ }
585
+ const props = {
586
+ ...(json.properties ?? {})
587
+ };
588
+ const axis = normalizeStackAxis(type, props);
589
+ const size = normalizeStackSize(props, axis);
590
+ const children = Array.isArray(json.children) ? [...json.children] : [];
591
+ if (json.child) {
592
+ children.push(json.child);
593
+ }
594
+ const normalizedProps = {
595
+ axis,
596
+ alignment: normalizeStackCrossAlignment(props.alignment ?? props.crossAxisAlignment),
597
+ distribution: normalizeStackDistribution(props.distribution ?? props.mainAxisAlignment),
598
+ childSpacing: Number(props.childSpacing ?? props.gap ?? props.spacing ?? 0),
599
+ size,
600
+ layout: {
601
+ padding: normalizeStackSpacingConfig(props.layout?.padding ?? props.padding),
602
+ margin: normalizeStackSpacingConfig(props.layout?.margin ?? props.margin)
603
+ },
604
+ appearance: {
605
+ shape: 'rectangle',
606
+ cornerRadius: Number(props.appearance?.cornerRadius ?? props.borderRadius ?? 0)
607
+ }
608
+ };
609
+ if (axis === 'overlay') {
610
+ normalizedProps.overlayAlignment = normalizeOverlayAlignment(props.overlayAlignment ?? props.alignment);
611
+ }
612
+ const fill = normalizeStackFill(props);
613
+ if (fill) normalizedProps.fill = fill;
614
+ const border = normalizeStackBorder(props);
615
+ if (border) normalizedProps.border = border;
616
+ const shadow = normalizeStackShadow(props);
617
+ if (shadow) normalizedProps.shadow = shadow;
618
+ if (props.width !== undefined) normalizedProps.width = props.width;
619
+ if (props.height !== undefined) normalizedProps.height = props.height;
620
+ if ((type === 'container' || type === 'layout') && children.length === 1) {
621
+ const childCandidate = children[0];
622
+ if (childCandidate && typeof childCandidate === 'object') {
623
+ const normalizedChild = normalizeStackLikeNode(childCandidate);
624
+ if (normalizedChild?.type === 'stack') {
625
+ const childProps = {
626
+ ...(normalizedChild.properties ?? {})
627
+ };
628
+ const mergedProps = mergeStackProps(normalizedProps, childProps);
629
+ return {
630
+ ...json,
631
+ type: 'stack',
632
+ properties: mergedProps,
633
+ children: Array.isArray(normalizedChild.children) ? normalizedChild.children : [],
634
+ child: undefined
635
+ };
636
+ }
637
+ }
638
+ }
639
+ return {
640
+ ...json,
641
+ type: 'stack',
642
+ properties: normalizedProps,
643
+ children,
644
+ child: undefined
645
+ };
646
+ }
647
+ function parseOverlayGridAlignment(value) {
648
+ switch (normalizeOverlayAlignment(value)) {
649
+ case 'topStart':
650
+ return {
651
+ justifyContent: 'flex-start',
652
+ alignItems: 'flex-start'
653
+ };
654
+ case 'top':
655
+ return {
656
+ justifyContent: 'flex-start',
657
+ alignItems: 'center'
658
+ };
659
+ case 'topEnd':
660
+ return {
661
+ justifyContent: 'flex-start',
662
+ alignItems: 'flex-end'
663
+ };
664
+ case 'start':
665
+ return {
666
+ justifyContent: 'center',
667
+ alignItems: 'flex-start'
668
+ };
669
+ case 'end':
670
+ return {
671
+ justifyContent: 'center',
672
+ alignItems: 'flex-end'
673
+ };
674
+ case 'bottomStart':
675
+ return {
676
+ justifyContent: 'flex-end',
677
+ alignItems: 'flex-start'
678
+ };
679
+ case 'bottom':
680
+ return {
681
+ justifyContent: 'flex-end',
682
+ alignItems: 'center'
683
+ };
684
+ case 'bottomEnd':
685
+ return {
686
+ justifyContent: 'flex-end',
687
+ alignItems: 'flex-end'
688
+ };
689
+ case 'center':
690
+ default:
691
+ return {
692
+ justifyContent: 'center',
693
+ alignItems: 'center'
694
+ };
695
+ }
696
+ }
697
+ function resolveStackDimension(props, axis, key, axisBounds) {
698
+ const mode = props.size?.[key];
699
+ const legacy = normalizeDimension(parseLayoutDimension(props[key]));
700
+ const isBounded = key === 'width' ? axisBounds.widthBounded : axisBounds.heightBounded;
701
+ if (mode === 'fill') return isBounded ? '100%' : legacy;
702
+ if (mode === 'fit') return legacy;
703
+ if (mode === 'fixed') return legacy;
704
+ if (legacy !== undefined) return legacy;
705
+ if (key === 'width' && axis !== 'horizontal' && axisBounds.widthBounded) return '100%';
706
+ if (key === 'height' && axis === 'horizontal' && axisBounds.heightBounded) return '100%';
707
+ return undefined;
708
+ }
275
709
  function buildWidget(json, params) {
276
710
  if (!json || typeof json !== 'object') return null;
277
- const type = json.type;
278
- const id = json.id;
711
+ const normalizedJson = normalizeStackLikeNode(json);
712
+ const type = normalizedJson.type;
713
+ const id = normalizedJson.id;
279
714
  const props = {
280
- ...(json.properties ?? {})
715
+ ...(normalizedJson.properties ?? {})
281
716
  };
282
- const childrenJson = Array.isArray(json.children) ? json.children : undefined;
283
- const childJson = json.child;
284
- const action = json.action;
285
- const actionPayload = action ? buildActionPayload(props, json) : undefined;
717
+ const pageAxisBounds = params.pageAxisBounds ?? getAxisBoundsForPageScroll(resolvePageScrollAxis(params.screenData));
718
+ const childrenJson = Array.isArray(normalizedJson.children) ? normalizedJson.children : undefined;
719
+ const childJson = normalizedJson.child;
720
+ const action = normalizedJson.action;
721
+ const actionPayload = action ? buildActionPayload(props, normalizedJson) : undefined;
286
722
  let node = null;
287
723
  switch (type) {
288
724
  case 'column':
@@ -457,55 +893,83 @@ function buildWidget(json, params) {
457
893
  const width = normalizeDimension(parseLayoutDimension(props.width)) ?? '100%';
458
894
  const height = normalizeDimension(parseLayoutDimension(props.height)) ?? 50;
459
895
  const label = props.label ?? 'Button';
896
+ const normalizedStroke = normalizeButtonStroke(props.stroke);
897
+ const normalizedEffects = normalizeButtonEffects(props.effects, props.shadow);
460
898
  const textStyle = getTextStyle({
461
899
  ...props,
462
900
  height: null
463
901
  }, 'textColor', '0xFFFFFFFF');
464
- const content = /*#__PURE__*/_jsx(View, {
465
- style: {
466
- flex: 1,
467
- justifyContent: 'center',
468
- alignItems: 'center'
469
- },
470
- children: /*#__PURE__*/_jsx(Text, {
471
- style: textStyle,
472
- children: label
473
- })
474
- });
475
- if (gradient) {
476
- node = /*#__PURE__*/_jsx(Pressable, {
477
- onPress: action ? () => params.onAction(action, actionPayload ?? props) : undefined,
902
+ const backgroundColor = parseColor(props.color ?? '0xFF2196F3');
903
+ const shadowBounds = getButtonShadowLayerBounds(normalizedStroke, borderRadius);
904
+ const webShadowStyle = buildButtonWebShadowStyle(normalizedEffects);
905
+ const firstNativeEffect = normalizedEffects.length > 0 ? normalizedEffects[0] : null;
906
+ const nativePrimaryShadowStyle = Platform.OS === 'web' || !firstNativeEffect ? null : buildButtonNativeShadowStyle(firstNativeEffect);
907
+ const centeredStrokeStyle = normalizedStroke && normalizedStroke.position === 'center' ? {
908
+ borderWidth: normalizedStroke.width,
909
+ borderColor: normalizedStroke.color
910
+ } : null;
911
+ const fillLayerStyle = {
912
+ position: 'absolute',
913
+ top: 0,
914
+ right: 0,
915
+ bottom: 0,
916
+ left: 0,
917
+ borderRadius,
918
+ overflow: 'hidden',
919
+ ...(centeredStrokeStyle ?? {})
920
+ };
921
+ const nativeShadowEffectsInPaintOrder = normalizedEffects.slice().reverse();
922
+ const pressableStyle = {
923
+ width,
924
+ height,
925
+ borderRadius,
926
+ justifyContent: 'center',
927
+ alignItems: 'center',
928
+ overflow: 'visible',
929
+ ...(nativePrimaryShadowStyle ?? {})
930
+ };
931
+ node = /*#__PURE__*/_jsxs(Pressable, {
932
+ collapsable: false,
933
+ onPress: action ? () => params.onAction(action, actionPayload ?? props) : undefined,
934
+ style: pressableStyle,
935
+ children: [Platform.OS === 'web' ? webShadowStyle ? /*#__PURE__*/_jsx(View, {
936
+ pointerEvents: "none",
478
937
  style: {
479
- width,
480
- height
481
- },
482
- children: /*#__PURE__*/_jsx(LinearGradient, {
483
- colors: gradient.colors,
484
- start: gradient.start,
485
- end: gradient.end,
486
- locations: gradient.stops,
487
- style: {
488
- flex: 1,
489
- borderRadius,
490
- overflow: 'hidden'
491
- },
492
- children: content
493
- })
494
- });
495
- } else {
496
- node = /*#__PURE__*/_jsx(Pressable, {
497
- onPress: action ? () => params.onAction(action, actionPayload ?? props) : undefined,
938
+ position: 'absolute',
939
+ ...shadowBounds,
940
+ ...webShadowStyle
941
+ }
942
+ }) : null : nativeShadowEffectsInPaintOrder.map((effect, index) => /*#__PURE__*/_jsx(View, {
943
+ collapsable: false,
944
+ pointerEvents: "none",
498
945
  style: {
499
- width,
500
- height,
501
- backgroundColor: parseColor(props.color ?? '0xFF2196F3'),
502
- borderRadius,
946
+ position: 'absolute',
947
+ ...shadowBounds,
948
+ ...buildButtonNativeShadowStyle(effect)
949
+ }
950
+ }, `shadow-${index}`)), gradient ? /*#__PURE__*/_jsx(LinearGradient, {
951
+ colors: gradient.colors,
952
+ start: gradient.start,
953
+ end: gradient.end,
954
+ locations: gradient.stops,
955
+ style: fillLayerStyle
956
+ }) : /*#__PURE__*/_jsx(View, {
957
+ style: {
958
+ ...fillLayerStyle,
959
+ backgroundColor
960
+ }
961
+ }), renderButtonStrokeOverlay(normalizedStroke, borderRadius), /*#__PURE__*/_jsx(View, {
962
+ style: {
963
+ flex: 1,
503
964
  justifyContent: 'center',
504
965
  alignItems: 'center'
505
966
  },
506
- children: content
507
- });
508
- }
967
+ children: /*#__PURE__*/_jsx(Text, {
968
+ style: textStyle,
969
+ children: label
970
+ })
971
+ })]
972
+ });
509
973
  break;
510
974
  }
511
975
  case 'text_input':
@@ -661,16 +1125,16 @@ function buildWidget(json, params) {
661
1125
  const source = props.source;
662
1126
  const width = normalizeDimension(parseLayoutDimension(props.width, true));
663
1127
  const height = normalizeDimension(parseLayoutDimension(props.height, true));
1128
+ const style = resolveLottieStyle(width, height);
1129
+ const resizeMode = parseResizeMode(props.fit);
664
1130
  const loop = props.loop === true;
665
1131
  if (source === 'asset') {
666
1132
  node = /*#__PURE__*/_jsx(LottieView, {
667
1133
  source: {
668
1134
  uri: props.path ?? ''
669
1135
  },
670
- style: {
671
- width,
672
- height
673
- },
1136
+ style: style,
1137
+ resizeMode: resizeMode,
674
1138
  autoPlay: true,
675
1139
  loop: loop
676
1140
  });
@@ -680,10 +1144,8 @@ function buildWidget(json, params) {
680
1144
  source: {
681
1145
  uri: props.url
682
1146
  },
683
- style: {
684
- width,
685
- height
686
- },
1147
+ style: style,
1148
+ resizeMode: resizeMode,
687
1149
  autoPlay: true,
688
1150
  loop: loop
689
1151
  });
@@ -819,22 +1281,112 @@ function buildWidget(json, params) {
819
1281
  }
820
1282
  case 'stack':
821
1283
  {
822
- const alignment = parseAlignment(props.alignment ?? 'topLeft');
823
- const fit = props.fit === 'expand' ? 'expand' : 'loose';
824
- node = /*#__PURE__*/_jsx(View, {
1284
+ const axis = props.axis === 'horizontal' || props.axis === 'overlay' ? props.axis : 'vertical';
1285
+ const isOverlay = axis === 'overlay';
1286
+ const spacing = Number(props.childSpacing ?? 0);
1287
+ const applySpacing = !STACK_SPACE_DISTRIBUTIONS.has(String(props.distribution ?? 'start'));
1288
+ const paddingInsets = stackSpacingToInsets(props.layout?.padding ?? props.padding);
1289
+ const width = resolveStackDimension(props, axis, 'width', pageAxisBounds);
1290
+ const height = resolveStackDimension(props, axis, 'height', pageAxisBounds);
1291
+ const mainAxisBounded = axis === 'horizontal' ? pageAxisBounds.widthBounded : pageAxisBounds.heightBounded;
1292
+ const borderRadius = Number(props.appearance?.cornerRadius ?? props.borderRadius ?? 0);
1293
+ const borderWidth = Number(props.border?.width ?? 0);
1294
+ const borderColor = borderWidth > 0 ? parseColor(props.border?.color ?? '#E5E7EB') : null;
1295
+ const shadowColor = props.shadow?.color ? parseColor(props.shadow.color) : null;
1296
+ const shadowStyle = shadowColor && props.shadow ? Platform.OS === 'web' ? {
1297
+ boxShadow: `${Number(props.shadow.x ?? 0)}px ${Number(props.shadow.y ?? 8)}px ${Number(props.shadow.blur ?? 24)}px ${shadowColor}`
1298
+ } : {
1299
+ shadowColor,
1300
+ shadowOffset: {
1301
+ width: Number(props.shadow.x ?? 0),
1302
+ height: Number(props.shadow.y ?? 8)
1303
+ },
1304
+ shadowOpacity: 0.35,
1305
+ shadowRadius: Math.max(0, Number(props.shadow.blur ?? 24) / 2),
1306
+ elevation: Math.max(1, Math.round((Number(props.shadow.blur ?? 24) + Math.abs(Number(props.shadow.y ?? 8))) / 3))
1307
+ } : {};
1308
+ const fill = props.fill;
1309
+ const stackGradient = fill?.type === 'gradient' ? parseGradient({
1310
+ ...fill,
1311
+ type: 'gradient'
1312
+ }) : undefined;
1313
+ const stackColor = fill?.type === 'solid' || !fill?.type && fill?.color ? parseColor(fill?.color ?? '#FFFFFFFF') : parseColor(props.backgroundColor ?? '#00000000');
1314
+ const baseStackStyle = {
1315
+ width,
1316
+ height,
1317
+ borderRadius: borderRadius > 0 ? borderRadius : undefined,
1318
+ overflow: borderRadius > 0 ? 'hidden' : undefined,
1319
+ borderWidth: borderColor ? borderWidth : undefined,
1320
+ borderColor: borderColor ?? undefined,
1321
+ ...insetsToStyle(paddingInsets),
1322
+ ...shadowStyle
1323
+ };
1324
+ const content = isOverlay ? /*#__PURE__*/_jsxs(View, {
825
1325
  style: {
826
1326
  position: 'relative',
827
- width: fit === 'expand' ? '100%' : undefined,
828
- height: fit === 'expand' ? '100%' : undefined,
829
- justifyContent: alignment.justifyContent,
830
- alignItems: alignment.alignItems
1327
+ minHeight: 0,
1328
+ minWidth: 0
831
1329
  },
832
- children: (childrenJson ?? []).map((child, index) => /*#__PURE__*/_jsx(React.Fragment, {
833
- children: buildWidget(child, {
834
- ...params
835
- })
1330
+ children: [(childrenJson ?? []).map((child, index) => {
1331
+ const alignment = parseOverlayGridAlignment(props.overlayAlignment ?? props.alignment);
1332
+ return /*#__PURE__*/_jsx(View, {
1333
+ style: {
1334
+ position: 'absolute',
1335
+ top: 0,
1336
+ right: 0,
1337
+ bottom: 0,
1338
+ left: 0,
1339
+ justifyContent: alignment.justifyContent,
1340
+ alignItems: alignment.alignItems
1341
+ },
1342
+ children: buildWidget(child, {
1343
+ ...params,
1344
+ allowFlexExpansion: true,
1345
+ pageAxisBounds
1346
+ })
1347
+ }, `stack-overlay-${index}`);
1348
+ }), (childrenJson ?? []).length === 0 ? null : null]
1349
+ }) : /*#__PURE__*/_jsx(View, {
1350
+ style: {
1351
+ flexDirection: axis === 'horizontal' ? 'row' : 'column',
1352
+ justifyContent: parseFlexAlignment(props.distribution),
1353
+ alignItems: parseCrossAlignment(props.alignment),
1354
+ minHeight: 0,
1355
+ minWidth: 0
1356
+ },
1357
+ children: (childrenJson ?? []).map((child, index) => /*#__PURE__*/_jsxs(React.Fragment, {
1358
+ children: [buildWidget(child, {
1359
+ ...params,
1360
+ allowFlexExpansion: true,
1361
+ parentFlexAxis: axis === 'horizontal' ? 'horizontal' : 'vertical',
1362
+ parentMainAxisBounded: mainAxisBounded,
1363
+ pageAxisBounds
1364
+ }), applySpacing && spacing > 0 && index < (childrenJson?.length ?? 0) - 1 ? /*#__PURE__*/_jsx(View, {
1365
+ style: {
1366
+ width: axis === 'horizontal' ? spacing : undefined,
1367
+ height: axis === 'vertical' ? spacing : undefined
1368
+ }
1369
+ }) : null]
836
1370
  }, `stack-${index}`))
837
1371
  });
1372
+ if (stackGradient) {
1373
+ node = /*#__PURE__*/_jsx(LinearGradient, {
1374
+ colors: stackGradient.colors,
1375
+ start: stackGradient.start,
1376
+ end: stackGradient.end,
1377
+ locations: stackGradient.stops,
1378
+ style: baseStackStyle,
1379
+ children: content
1380
+ });
1381
+ } else {
1382
+ node = /*#__PURE__*/_jsx(View, {
1383
+ style: {
1384
+ ...baseStackStyle,
1385
+ backgroundColor: stackColor
1386
+ },
1387
+ children: content
1388
+ });
1389
+ }
838
1390
  break;
839
1391
  }
840
1392
  case 'positioned':
@@ -865,35 +1417,66 @@ function buildWidget(json, params) {
865
1417
  node = null;
866
1418
  }
867
1419
  if (!node) return null;
868
- const marginVal = props.margin;
1420
+ const marginVal = type === 'stack' ? props.layout?.margin ?? props.margin : props.margin;
869
1421
  if (marginVal !== undefined && marginVal !== null) {
870
- const marginInsets = parseInsets(marginVal);
1422
+ const marginInsets = type === 'stack' ? stackSpacingToInsets(marginVal) : parseInsets(marginVal);
871
1423
  node = /*#__PURE__*/_jsx(View, {
872
1424
  style: insetsToMarginStyle(marginInsets),
873
1425
  children: node
874
1426
  });
875
- } else if (type !== 'container' && type !== 'padding' && props.padding) {
1427
+ } else if (type !== 'container' && type !== 'padding' && type !== 'stack' && props.padding) {
876
1428
  const paddingInsets = parseInsets(props.padding);
877
1429
  node = /*#__PURE__*/_jsx(View, {
878
1430
  style: insetsToStyle(paddingInsets),
879
1431
  children: node
880
1432
  });
881
1433
  }
882
- if (params.allowFlexExpansion && params.screenData.scrollable !== true) {
1434
+ const shouldStretchTextInput = type === 'text_input' && props.width === undefined && props.size?.width === undefined;
1435
+ if (shouldStretchTextInput) {
1436
+ node = /*#__PURE__*/_jsx(View, {
1437
+ style: {
1438
+ width: '100%',
1439
+ alignSelf: 'stretch',
1440
+ minWidth: 0
1441
+ },
1442
+ children: node
1443
+ });
1444
+ }
1445
+ if (params.allowFlexExpansion) {
883
1446
  let shouldExpand = false;
884
- if (type === 'stack' && props.fit === 'expand') {
885
- shouldExpand = true;
886
- }
887
- if (props.height !== undefined) {
888
- const heightVal = parseLayoutDimension(props.height);
889
- if (heightVal === Number.POSITIVE_INFINITY) {
890
- shouldExpand = true;
1447
+ const parentAxis = params.parentFlexAxis ?? 'vertical';
1448
+ const parentMainAxisBounded = params.parentMainAxisBounded ?? true;
1449
+ if (parentMainAxisBounded) {
1450
+ if (type === 'stack') {
1451
+ const legacyExpand = props.fit === 'expand';
1452
+ const widthMode = String(props.size?.width ?? '').toLowerCase();
1453
+ const heightMode = String(props.size?.height ?? '').toLowerCase();
1454
+ if (parentAxis === 'vertical' && (legacyExpand || heightMode === 'fill')) {
1455
+ shouldExpand = true;
1456
+ }
1457
+ if (parentAxis === 'horizontal' && (legacyExpand || widthMode === 'fill')) {
1458
+ shouldExpand = true;
1459
+ }
1460
+ }
1461
+ if (parentAxis === 'vertical' && props.height !== undefined) {
1462
+ const heightVal = parseLayoutDimension(props.height);
1463
+ if (heightVal === Number.POSITIVE_INFINITY) {
1464
+ shouldExpand = true;
1465
+ }
1466
+ }
1467
+ if (parentAxis === 'horizontal' && props.width !== undefined) {
1468
+ const widthVal = parseLayoutDimension(props.width);
1469
+ if (widthVal === Number.POSITIVE_INFINITY) {
1470
+ shouldExpand = true;
1471
+ }
891
1472
  }
892
1473
  }
893
1474
  if (shouldExpand) {
894
1475
  node = /*#__PURE__*/_jsx(View, {
895
1476
  style: {
896
- flex: 1
1477
+ flex: 1,
1478
+ minHeight: 0,
1479
+ minWidth: 0
897
1480
  },
898
1481
  children: node
899
1482
  });
@@ -958,6 +1541,132 @@ function parseGradient(value) {
958
1541
  stops
959
1542
  };
960
1543
  }
1544
+ function normalizeButtonStroke(input) {
1545
+ if (!input || typeof input !== 'object') return null;
1546
+ const legacyStrokeWithoutEnabled = input.enabled === undefined;
1547
+ const enabled = legacyStrokeWithoutEnabled ? true : input.enabled === true;
1548
+ if (!enabled) return null;
1549
+ const width = Number(input.width ?? 1);
1550
+ if (!Number.isFinite(width) || width <= 0) return null;
1551
+ const position = input.position === 'inside' || input.position === 'center' || input.position === 'outside' ? input.position : 'center';
1552
+ const opacity = clamp01(Number(input.opacity ?? 1));
1553
+ const color = normalizeColorWithOpacity(input.color, opacity);
1554
+ if (color === 'transparent') return null;
1555
+ return {
1556
+ color,
1557
+ width,
1558
+ position
1559
+ };
1560
+ }
1561
+ function normalizeButtonEffects(effectsInput, legacyShadowInput) {
1562
+ if (Array.isArray(effectsInput)) {
1563
+ return effectsInput.map(normalizeDropShadowEffect).filter(effect => effect !== null);
1564
+ }
1565
+ if (!legacyShadowInput || typeof legacyShadowInput !== 'object') {
1566
+ return [];
1567
+ }
1568
+ if (legacyShadowInput.enabled !== true) return [];
1569
+ const migrated = normalizeDropShadowEffect({
1570
+ type: 'dropShadow',
1571
+ enabled: true,
1572
+ x: legacyShadowInput.x,
1573
+ y: legacyShadowInput.y,
1574
+ blur: legacyShadowInput.blur,
1575
+ spread: 0,
1576
+ color: legacyShadowInput.color,
1577
+ opacity: 1
1578
+ });
1579
+ return migrated ? [migrated] : [];
1580
+ }
1581
+ function normalizeDropShadowEffect(effect) {
1582
+ if (!effect || typeof effect !== 'object') return null;
1583
+ if ((effect.type ?? 'dropShadow') !== 'dropShadow') return null;
1584
+ if (effect.enabled === false) return null;
1585
+ const opacity = clamp01(Number(effect.opacity ?? 1));
1586
+ const color = normalizeColorWithOpacity(effect.color, opacity);
1587
+ if (color === 'transparent') return null;
1588
+ return {
1589
+ x: Number(effect.x ?? 0),
1590
+ y: Number(effect.y ?? 0),
1591
+ blur: Math.max(0, Number(effect.blur ?? 0)),
1592
+ spread: Number(effect.spread ?? 0),
1593
+ color,
1594
+ opacity
1595
+ };
1596
+ }
1597
+ function normalizeColorWithOpacity(input, opacity) {
1598
+ const raw = typeof input === 'string' ? input.trim() : '';
1599
+ const normalizedOpacity = clamp01(opacity);
1600
+ if (raw.startsWith('rgba(') || raw.startsWith('rgb(')) {
1601
+ return withOpacity(raw, normalizedOpacity);
1602
+ }
1603
+ return withOpacity(parseColor(input ?? '0xFF000000'), normalizedOpacity);
1604
+ }
1605
+ function buildButtonWebShadowStyle(effects) {
1606
+ if (effects.length === 0) return null;
1607
+ const layers = effects.map(effect => `${effect.x}px ${effect.y}px ${effect.blur}px ${effect.spread}px ${effect.color}`);
1608
+ return {
1609
+ boxShadow: layers.join(', ')
1610
+ };
1611
+ }
1612
+ function buildButtonNativeShadowStyle(effect) {
1613
+ const rgba = parseRgbaColor(effect.color);
1614
+ return {
1615
+ // Android elevation requires a drawable host shape; keep it effectively invisible.
1616
+ backgroundColor: 'rgba(255,255,255,0.02)',
1617
+ shadowColor: rgba ? `rgba(${rgba.r},${rgba.g},${rgba.b},1)` : effect.color,
1618
+ shadowOffset: {
1619
+ width: effect.x,
1620
+ height: effect.y
1621
+ },
1622
+ shadowOpacity: rgba ? rgba.a : effect.opacity,
1623
+ shadowRadius: effect.blur / 2,
1624
+ elevation: Math.max(1, Math.round((effect.blur + Math.abs(effect.y)) / 3))
1625
+ };
1626
+ }
1627
+ function getButtonShadowLayerBounds(stroke, borderRadius) {
1628
+ const expandBy = !stroke || stroke.position === 'inside' ? 0 : stroke.position === 'center' ? stroke.width / 2 : stroke.width;
1629
+ return {
1630
+ top: -expandBy,
1631
+ right: -expandBy,
1632
+ bottom: -expandBy,
1633
+ left: -expandBy,
1634
+ borderRadius: borderRadius + expandBy
1635
+ };
1636
+ }
1637
+ function renderButtonStrokeOverlay(stroke, borderRadius) {
1638
+ if (!stroke || stroke.position === 'center') {
1639
+ return null;
1640
+ }
1641
+ if (stroke.position === 'inside') {
1642
+ return /*#__PURE__*/_jsx(View, {
1643
+ pointerEvents: "none",
1644
+ style: {
1645
+ position: 'absolute',
1646
+ top: stroke.width / 2,
1647
+ left: stroke.width / 2,
1648
+ right: stroke.width / 2,
1649
+ bottom: stroke.width / 2,
1650
+ borderRadius: Math.max(0, borderRadius - stroke.width / 2),
1651
+ borderWidth: stroke.width,
1652
+ borderColor: stroke.color
1653
+ }
1654
+ });
1655
+ }
1656
+ return /*#__PURE__*/_jsx(View, {
1657
+ pointerEvents: "none",
1658
+ style: {
1659
+ position: 'absolute',
1660
+ top: -stroke.width,
1661
+ left: -stroke.width,
1662
+ right: -stroke.width,
1663
+ bottom: -stroke.width,
1664
+ borderRadius: borderRadius + stroke.width,
1665
+ borderWidth: stroke.width,
1666
+ borderColor: stroke.color
1667
+ }
1668
+ });
1669
+ }
961
1670
  function alignmentToGradient(value) {
962
1671
  switch (value) {
963
1672
  case 'topCenter':
@@ -1009,10 +1718,23 @@ function alignmentToGradient(value) {
1009
1718
  }
1010
1719
  }
1011
1720
  function withOpacity(color, opacity) {
1012
- if (color.startsWith('rgba')) {
1013
- return color.replace(/rgba\((\d+),(\d+),(\d+),([\d.]+)\)/, (_m, r, g, b) => `rgba(${r},${g},${b},${opacity})`);
1014
- }
1015
- return color;
1721
+ const rgba = parseRgbaColor(color);
1722
+ if (!rgba) return color;
1723
+ return `rgba(${rgba.r},${rgba.g},${rgba.b},${clamp01(rgba.a * opacity)})`;
1724
+ }
1725
+ function parseRgbaColor(color) {
1726
+ const match = color.match(/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i);
1727
+ if (!match) return null;
1728
+ return {
1729
+ r: Math.round(Number(match[1])),
1730
+ g: Math.round(Number(match[2])),
1731
+ b: Math.round(Number(match[3])),
1732
+ a: match[4] === undefined ? 1 : clamp01(Number(match[4]))
1733
+ };
1734
+ }
1735
+ function clamp01(value) {
1736
+ if (!Number.isFinite(value)) return 1;
1737
+ return Math.max(0, Math.min(1, value));
1016
1738
  }
1017
1739
  function normalizeDimension(value) {
1018
1740
  if (value === Number.POSITIVE_INFINITY) {
@@ -1114,6 +1836,35 @@ function SduiImage({
1114
1836
  onError: () => setHasError(true)
1115
1837
  });
1116
1838
  }
1839
+ function resolveLottieStyle(width, height) {
1840
+ const hasExplicitWidth = width !== undefined;
1841
+ const hasExplicitHeight = height !== undefined;
1842
+ const style = {};
1843
+ if (hasExplicitWidth) {
1844
+ style.width = width;
1845
+ } else {
1846
+ style.width = 'auto';
1847
+ }
1848
+ if (hasExplicitHeight) {
1849
+ style.height = height;
1850
+ } else {
1851
+ style.height = 'auto';
1852
+ }
1853
+ if (width === '100%') {
1854
+ style.width = '100%';
1855
+ style.alignSelf = 'stretch';
1856
+ style.minWidth = 0;
1857
+ }
1858
+ if (hasExplicitWidth && !hasExplicitHeight) {
1859
+ delete style.height;
1860
+ style.aspectRatio = 1;
1861
+ }
1862
+ if (!hasExplicitWidth && hasExplicitHeight) {
1863
+ delete style.width;
1864
+ style.aspectRatio = 1;
1865
+ }
1866
+ return style;
1867
+ }
1117
1868
  function GradientText({
1118
1869
  text,
1119
1870
  gradient,
@@ -1140,6 +1891,44 @@ function GradientText({
1140
1891
  })
1141
1892
  });
1142
1893
  }
1894
+ function resolveTextInputBackgroundColor(properties) {
1895
+ const canonical = properties.backgroundColor;
1896
+ if (typeof canonical === 'string' && canonical.trim().length > 0) {
1897
+ return parseColor(canonical);
1898
+ }
1899
+ const legacyBackground = properties.background;
1900
+ if (legacyBackground && typeof legacyBackground === 'object' && !Array.isArray(legacyBackground)) {
1901
+ const type = String(legacyBackground.type ?? '').toLowerCase();
1902
+ if (type === 'color' && typeof legacyBackground.color === 'string') {
1903
+ return parseColor(legacyBackground.color);
1904
+ }
1905
+ if (type === 'gradient' && Array.isArray(legacyBackground.colors)) {
1906
+ const firstGradientColor = legacyBackground.colors.find(value => typeof value === 'string' && value.trim().length > 0);
1907
+ if (typeof firstGradientColor === 'string') {
1908
+ return parseColor(firstGradientColor);
1909
+ }
1910
+ }
1911
+ } else if (typeof legacyBackground === 'string' && legacyBackground.trim().length > 0) {
1912
+ return parseColor(legacyBackground);
1913
+ }
1914
+ return parseColor('0xFFF0F0F0');
1915
+ }
1916
+ function normalizeTextInputStroke(properties) {
1917
+ const parsedWidth = Number(properties.strokeWidth ?? 0);
1918
+ const width = Number.isFinite(parsedWidth) && parsedWidth > 0 ? parsedWidth : 0;
1919
+ const enabled = typeof properties.strokeEnabled === 'boolean' ? properties.strokeEnabled : width > 0;
1920
+ const normalizedWidth = enabled ? width : 0;
1921
+ const color = normalizedWidth > 0 ? parseColor(properties.strokeColor ?? '#000000') : 'transparent';
1922
+ const parsedRadius = Number(properties.strokeRadius ?? properties.borderRadius ?? 8);
1923
+ const radius = Number.isFinite(parsedRadius) ? Math.max(0, parsedRadius) : 8;
1924
+ const style = properties.strokeStyle === 'dashed' || properties.strokeStyle === 'dotted' ? properties.strokeStyle : 'solid';
1925
+ return {
1926
+ width: normalizedWidth,
1927
+ color,
1928
+ radius,
1929
+ style
1930
+ };
1931
+ }
1143
1932
  function DynamicInput({
1144
1933
  initialValue,
1145
1934
  properties,
@@ -1163,18 +1952,26 @@ function DynamicInput({
1163
1952
  ...properties,
1164
1953
  height: null
1165
1954
  }, 'textColor');
1166
- const backgroundColor = parseColor(properties.backgroundColor ?? '0xFFF0F0F0');
1167
- const borderRadius = Number(properties.borderRadius ?? 8);
1955
+ const backgroundColor = resolveTextInputBackgroundColor(properties);
1956
+ const stroke = normalizeTextInputStroke(properties);
1957
+ const parsedPadding = Number(properties.padding ?? 12);
1958
+ const padding = Number.isFinite(parsedPadding) ? Math.max(0, parsedPadding) : 12;
1168
1959
  return /*#__PURE__*/_jsxs(View, {
1960
+ style: {
1961
+ width: '100%'
1962
+ },
1169
1963
  children: [label ? /*#__PURE__*/_jsx(Text, {
1170
1964
  style: labelStyle,
1171
1965
  children: label
1172
1966
  }) : null, /*#__PURE__*/_jsx(View, {
1173
1967
  style: {
1968
+ width: '100%',
1174
1969
  backgroundColor,
1175
- borderRadius,
1176
- paddingHorizontal: 16,
1177
- paddingVertical: 10
1970
+ borderRadius: stroke.radius,
1971
+ borderStyle: stroke.width > 0 ? stroke.style : undefined,
1972
+ borderWidth: stroke.width,
1973
+ borderColor: stroke.color,
1974
+ padding
1178
1975
  },
1179
1976
  children: mask ? /*#__PURE__*/_jsx(MaskInput, {
1180
1977
  value: value,
@@ -1313,6 +2110,7 @@ function SelectionList({
1313
2110
  if (layout === 'row') {
1314
2111
  return /*#__PURE__*/_jsx(View, {
1315
2112
  style: {
2113
+ width: '100%',
1316
2114
  flexDirection: 'row',
1317
2115
  flexWrap: 'wrap'
1318
2116
  },
@@ -1330,6 +2128,7 @@ function SelectionList({
1330
2128
  const aspectRatio = Number(properties.gridAspectRatio ?? 1);
1331
2129
  return /*#__PURE__*/_jsx(View, {
1332
2130
  style: {
2131
+ width: '100%',
1333
2132
  flexDirection: 'row',
1334
2133
  flexWrap: 'wrap'
1335
2134
  },
@@ -1345,8 +2144,12 @@ function SelectionList({
1345
2144
  });
1346
2145
  }
1347
2146
  return /*#__PURE__*/_jsx(View, {
2147
+ style: {
2148
+ width: '100%'
2149
+ },
1348
2150
  children: options.map((option, index) => /*#__PURE__*/_jsx(View, {
1349
2151
  style: {
2152
+ width: '100%',
1350
2153
  marginBottom: spacing
1351
2154
  },
1352
2155
  children: renderItem(option, index)
@@ -1529,6 +2332,8 @@ function WheelPicker({
1529
2332
  if (layout === 'row') {
1530
2333
  return /*#__PURE__*/_jsx(View, {
1531
2334
  style: {
2335
+ width: '100%',
2336
+ alignSelf: 'stretch',
1532
2337
  flexDirection: 'row',
1533
2338
  flexWrap: 'wrap'
1534
2339
  },
@@ -1546,6 +2351,8 @@ function WheelPicker({
1546
2351
  const aspectRatio = Number(properties.gridAspectRatio ?? 1);
1547
2352
  return /*#__PURE__*/_jsx(View, {
1548
2353
  style: {
2354
+ width: '100%',
2355
+ alignSelf: 'stretch',
1549
2356
  flexDirection: 'row',
1550
2357
  flexWrap: 'wrap'
1551
2358
  },
@@ -1566,6 +2373,8 @@ function WheelPicker({
1566
2373
  const baseUnselectedStyle = properties.unselectedStyle ?? properties.selectedStyle ?? {};
1567
2374
  const overlayBorderColor = parseColor(properties.wheelCenterBorderColor ?? properties.wheelSelectedBorderColor ?? baseUnselectedStyle.borderColor ?? '#D6D6DC');
1568
2375
  const overlayBackgroundColor = parseColor(properties.wheelCenterBackgroundColor ?? '#24FFFFFF');
2376
+ const wheelEdgeFadeOpacity = Math.max(0, Math.min(1, Number(properties.wheelEdgeFadeOpacity ?? 0)));
2377
+ const wheelEdgeFadeColor = parseColor(properties.wheelEdgeFadeColor ?? '#FFFFFF');
1569
2378
  const handleWheelMomentumEnd = event => {
1570
2379
  const offsetY = Number(event?.nativeEvent?.contentOffset?.y ?? 0);
1571
2380
  const settledIndex = clampWheelIndex(Math.round(offsetY / wheelRowStride));
@@ -1580,11 +2389,17 @@ function WheelPicker({
1580
2389
  };
1581
2390
  return /*#__PURE__*/_jsxs(View, {
1582
2391
  style: {
2392
+ width: '100%',
2393
+ alignSelf: 'stretch',
1583
2394
  height: wheelContainerHeight,
1584
- overflow: 'hidden'
2395
+ overflow: 'hidden',
2396
+ justifyContent: 'center'
1585
2397
  },
1586
2398
  children: [/*#__PURE__*/_jsx(Animated.ScrollView, {
1587
2399
  ref: wheelScrollRef,
2400
+ style: {
2401
+ width: '100%'
2402
+ },
1588
2403
  showsVerticalScrollIndicator: false,
1589
2404
  bounces: false,
1590
2405
  decelerationRate: "fast",
@@ -1593,7 +2408,9 @@ function WheelPicker({
1593
2408
  disableIntervalMomentum: false,
1594
2409
  onMomentumScrollEnd: handleWheelMomentumEnd,
1595
2410
  contentContainerStyle: {
1596
- paddingVertical: centerPadding
2411
+ width: '100%',
2412
+ paddingVertical: centerPadding,
2413
+ alignItems: 'center'
1597
2414
  },
1598
2415
  onScroll: Animated.event([{
1599
2416
  nativeEvent: {
@@ -1629,8 +2446,10 @@ function WheelPicker({
1629
2446
  });
1630
2447
  return /*#__PURE__*/_jsx(Animated.View, {
1631
2448
  style: {
2449
+ width: '100%',
1632
2450
  height: wheelRowStride,
1633
2451
  justifyContent: 'center',
2452
+ alignItems: 'center',
1634
2453
  opacity,
1635
2454
  transform: [{
1636
2455
  perspective: 1200
@@ -1644,8 +2463,10 @@ function WheelPicker({
1644
2463
  },
1645
2464
  children: /*#__PURE__*/_jsx(View, {
1646
2465
  style: {
2466
+ width: '100%',
1647
2467
  minHeight: wheelItemHeight,
1648
- justifyContent: 'center'
2468
+ justifyContent: 'center',
2469
+ alignItems: 'center'
1649
2470
  },
1650
2471
  children: renderItem(option, index)
1651
2472
  })
@@ -1664,9 +2485,9 @@ function WheelPicker({
1664
2485
  borderColor: overlayBorderColor,
1665
2486
  backgroundColor: overlayBackgroundColor
1666
2487
  }
1667
- }), /*#__PURE__*/_jsx(LinearGradient, {
2488
+ }), wheelEdgeFadeOpacity > 0 && centerPadding > 0 ? /*#__PURE__*/_jsx(LinearGradient, {
1668
2489
  pointerEvents: "none",
1669
- colors: ['rgba(255,255,255,0.92)', 'rgba(255,255,255,0)'],
2490
+ colors: [withOpacity(wheelEdgeFadeColor, wheelEdgeFadeOpacity), withOpacity(wheelEdgeFadeColor, 0)],
1670
2491
  style: {
1671
2492
  position: 'absolute',
1672
2493
  top: 0,
@@ -1674,9 +2495,9 @@ function WheelPicker({
1674
2495
  right: 0,
1675
2496
  height: centerPadding
1676
2497
  }
1677
- }), /*#__PURE__*/_jsx(LinearGradient, {
2498
+ }) : null, wheelEdgeFadeOpacity > 0 && centerPadding > 0 ? /*#__PURE__*/_jsx(LinearGradient, {
1678
2499
  pointerEvents: "none",
1679
- colors: ['rgba(255,255,255,0)', 'rgba(255,255,255,0.92)'],
2500
+ colors: [withOpacity(wheelEdgeFadeColor, 0), withOpacity(wheelEdgeFadeColor, wheelEdgeFadeOpacity)],
1680
2501
  style: {
1681
2502
  position: 'absolute',
1682
2503
  bottom: 0,
@@ -1684,12 +2505,17 @@ function WheelPicker({
1684
2505
  right: 0,
1685
2506
  height: centerPadding
1686
2507
  }
1687
- })]
2508
+ }) : null]
1688
2509
  });
1689
2510
  }
1690
2511
  return /*#__PURE__*/_jsx(View, {
2512
+ style: {
2513
+ width: '100%',
2514
+ alignSelf: 'stretch'
2515
+ },
1691
2516
  children: resolvedOptions.map((option, index) => /*#__PURE__*/_jsx(View, {
1692
2517
  style: {
2518
+ width: '100%',
1693
2519
  marginBottom: spacing
1694
2520
  },
1695
2521
  children: renderItem(option, index)
@@ -2113,16 +2939,33 @@ function RadarChart({
2113
2939
  const axisColor = parseColor(properties.axisColor ?? '0xFF9E9E9E');
2114
2940
  const axisStrokeWidth = Number(properties.axisStrokeWidth ?? 1);
2115
2941
  const axisLabelStyle = getTextStyle(properties.axisLabelStyle ?? {}, 'color', '0xFF424242');
2942
+ const resolvedAxisLabelFontSize = resolveRadarAxisLabelFontSize(axisLabelStyle.fontSize);
2943
+ const autoFitLabels = properties.autoFitLabels !== false;
2944
+ const resolvedLabelOffset = resolveRadarLabelOffset(properties.labelOffset, resolvedAxisLabelFontSize);
2116
2945
  const showLegend = properties.showLegend !== false;
2117
2946
  const legendPosition = (properties.legendPosition ?? 'bottom').toLowerCase();
2118
2947
  const legendSpacing = Number(properties.legendSpacing ?? 12);
2119
2948
  const legendStyle = getTextStyle(properties.legendStyle ?? {}, 'color', '0xFF212121');
2120
2949
  const shape = (properties.shape ?? 'polygon').toLowerCase();
2121
- const chartSize = Math.min(width, height);
2122
- const radius = Math.max(chartSize / 2 - Math.max(padding.left, padding.right, padding.top, padding.bottom), 8);
2950
+ const innerWidth = Math.max(1, width - padding.left - padding.right);
2951
+ const innerHeight = Math.max(1, height - padding.top - padding.bottom);
2952
+ const radius = calculateRadarChartRadius({
2953
+ width: innerWidth,
2954
+ height: innerHeight,
2955
+ padding: {
2956
+ left: 0,
2957
+ right: 0,
2958
+ top: 0,
2959
+ bottom: 0
2960
+ },
2961
+ axisLabels: axes.map(axis => axis.label),
2962
+ fontSize: resolvedAxisLabelFontSize,
2963
+ autoFitLabels,
2964
+ labelOffset: resolvedLabelOffset
2965
+ });
2123
2966
  const center = {
2124
- x: width / 2,
2125
- y: height / 2
2967
+ x: padding.left + innerWidth / 2,
2968
+ y: padding.top + innerHeight / 2
2126
2969
  };
2127
2970
  const axisAngle = Math.PI * 2 / axes.length;
2128
2971
  const gridPolygons = Array.from({
@@ -2143,7 +2986,8 @@ function RadarChart({
2143
2986
  const points = dataset.values.map((value, index) => {
2144
2987
  const axis = normalizedAxes[index];
2145
2988
  const maxValue = axis?.maxValue ?? 0;
2146
- const ratio = (maxValue > 0 ? value / maxValue : 0) * lineProgress;
2989
+ const normalizedRatio = maxValue > 0 ? value / maxValue : 0;
2990
+ const ratio = Math.max(0, Math.min(1, normalizedRatio)) * Math.max(0, Math.min(1, lineProgress));
2147
2991
  const angle = index * axisAngle - Math.PI / 2;
2148
2992
  const x = center.x + radius * ratio * Math.cos(angle);
2149
2993
  const y = center.y + radius * ratio * Math.sin(angle);
@@ -2154,27 +2998,32 @@ function RadarChart({
2154
2998
  points
2155
2999
  };
2156
3000
  });
3001
+ const isVerticalLegend = legendPosition === 'left' || legendPosition === 'right';
2157
3002
  const legend = /*#__PURE__*/_jsx(View, {
2158
3003
  style: {
2159
- flexDirection: 'row',
2160
- flexWrap: 'wrap'
3004
+ flexDirection: isVerticalLegend ? 'column' : 'row',
3005
+ flexWrap: isVerticalLegend ? 'nowrap' : 'wrap',
3006
+ alignItems: isVerticalLegend ? 'flex-start' : 'center'
2161
3007
  },
2162
3008
  children: datasets.map((dataset, index) => /*#__PURE__*/_jsxs(View, {
2163
3009
  style: {
2164
3010
  flexDirection: 'row',
2165
3011
  alignItems: 'center',
2166
- marginRight: legendSpacing,
2167
- marginBottom: 6
3012
+ marginRight: isVerticalLegend ? 0 : legendSpacing,
3013
+ marginBottom: isVerticalLegend ? legendSpacing : 6
2168
3014
  },
2169
3015
  children: [/*#__PURE__*/_jsx(View, {
2170
3016
  style: {
2171
3017
  width: 12,
2172
3018
  height: 12,
2173
3019
  borderRadius: 6,
2174
- backgroundColor: dataset.borderColor,
3020
+ backgroundColor: dataset.fillColor ?? dataset.borderColor,
3021
+ borderWidth: 1,
3022
+ borderColor: dataset.borderColor,
2175
3023
  marginRight: 6
2176
3024
  }
2177
3025
  }), /*#__PURE__*/_jsx(Text, {
3026
+ numberOfLines: 1,
2178
3027
  style: legendStyle,
2179
3028
  children: dataset.label
2180
3029
  })]
@@ -2182,12 +3031,10 @@ function RadarChart({
2182
3031
  });
2183
3032
  const chart = /*#__PURE__*/_jsx(Animated.View, {
2184
3033
  style: {
2185
- width: normalizedWidth ?? width,
3034
+ width: isVerticalLegend ? width : normalizedWidth ?? '100%',
3035
+ maxWidth: '100%',
3036
+ flexShrink: 1,
2186
3037
  height,
2187
- paddingLeft: padding.left,
2188
- paddingRight: padding.right,
2189
- paddingTop: padding.top,
2190
- paddingBottom: padding.bottom,
2191
3038
  opacity: reveal
2192
3039
  },
2193
3040
  onLayout: event => {
@@ -2226,24 +3073,28 @@ function RadarChart({
2226
3073
  }, `axis-${index}`);
2227
3074
  }), axes.map((axis, index) => {
2228
3075
  const angle = index * axisAngle - Math.PI / 2;
2229
- const labelX = center.x + (radius + 12) * Math.cos(angle);
2230
- const labelY = center.y + (radius + 12) * Math.sin(angle);
3076
+ const labelX = center.x + (radius + resolvedLabelOffset) * Math.cos(angle);
3077
+ const labelY = center.y + (radius + resolvedLabelOffset) * Math.sin(angle);
2231
3078
  return /*#__PURE__*/_jsx(SvgText, {
2232
3079
  x: labelX,
2233
3080
  y: labelY,
2234
- fontSize: axisLabelStyle.fontSize ?? 12,
3081
+ fontSize: resolvedAxisLabelFontSize,
2235
3082
  fontWeight: axisLabelStyle.fontWeight,
2236
3083
  fill: axisLabelStyle.color,
2237
3084
  textAnchor: "middle",
2238
3085
  children: axis.label
2239
3086
  }, `label-${index}`);
2240
- }), datasetPolygons.map((entry, index) => /*#__PURE__*/_jsx(Polygon, {
2241
- points: entry.points.join(' '),
2242
- stroke: entry.dataset.borderColor,
2243
- strokeWidth: entry.dataset.borderWidth,
2244
- fill: entry.dataset.fillColor ?? 'transparent',
2245
- strokeDasharray: entry.dataset.borderStyle === 'dotted' || entry.dataset.borderStyle === 'dashed' ? entry.dataset.dashArray ?? '6 4' : undefined
2246
- }, `dataset-${index}`)), datasetPolygons.map((entry, datasetIndex) => entry.dataset.showPoints ? entry.points.map((point, pointIndex) => {
3087
+ }), datasetPolygons.map((entry, index) => {
3088
+ const strokePattern = resolveRadarStrokePattern(entry.dataset);
3089
+ return /*#__PURE__*/_jsx(Polygon, {
3090
+ points: entry.points.join(' '),
3091
+ stroke: entry.dataset.borderColor,
3092
+ strokeWidth: entry.dataset.borderWidth,
3093
+ fill: entry.dataset.fillColor ?? 'transparent',
3094
+ strokeDasharray: strokePattern.strokeDasharray,
3095
+ strokeLinecap: strokePattern.strokeLinecap
3096
+ }, `dataset-${index}`);
3097
+ }), datasetPolygons.map((entry, datasetIndex) => entry.dataset.showPoints ? entry.points.map((point, pointIndex) => {
2247
3098
  const [x, y] = point.split(',').map(val => Number(val));
2248
3099
  return /*#__PURE__*/_jsx(Circle, {
2249
3100
  cx: x,
@@ -2255,23 +3106,23 @@ function RadarChart({
2255
3106
  })
2256
3107
  })
2257
3108
  });
2258
- const composed = /*#__PURE__*/_jsxs(View, {
2259
- style: {
2260
- backgroundColor
2261
- },
2262
- children: [chart, showLegend && legendPosition === 'bottom' ? /*#__PURE__*/_jsx(View, {
3109
+ if (!showLegend) {
3110
+ return /*#__PURE__*/_jsx(View, {
2263
3111
  style: {
2264
- marginTop: 12
3112
+ backgroundColor
2265
3113
  },
2266
- children: legend
2267
- }) : null]
2268
- });
2269
- if (!showLegend || legendPosition === 'bottom') return composed;
3114
+ children: chart
3115
+ });
3116
+ }
2270
3117
  if (legendPosition === 'top') {
2271
3118
  return /*#__PURE__*/_jsxs(View, {
3119
+ style: {
3120
+ backgroundColor,
3121
+ alignItems: 'center'
3122
+ },
2272
3123
  children: [legend, /*#__PURE__*/_jsx(View, {
2273
3124
  style: {
2274
- height: 12
3125
+ height: legendSpacing
2275
3126
  }
2276
3127
  }), chart]
2277
3128
  });
@@ -2279,11 +3130,13 @@ function RadarChart({
2279
3130
  if (legendPosition === 'left') {
2280
3131
  return /*#__PURE__*/_jsxs(View, {
2281
3132
  style: {
2282
- flexDirection: 'row'
3133
+ backgroundColor,
3134
+ flexDirection: 'row',
3135
+ alignItems: 'center'
2283
3136
  },
2284
3137
  children: [legend, /*#__PURE__*/_jsx(View, {
2285
3138
  style: {
2286
- width: 12
3139
+ width: legendSpacing
2287
3140
  }
2288
3141
  }), chart]
2289
3142
  });
@@ -2291,16 +3144,167 @@ function RadarChart({
2291
3144
  if (legendPosition === 'right') {
2292
3145
  return /*#__PURE__*/_jsxs(View, {
2293
3146
  style: {
2294
- flexDirection: 'row'
3147
+ backgroundColor,
3148
+ flexDirection: 'row',
3149
+ alignItems: 'center'
2295
3150
  },
2296
3151
  children: [chart, /*#__PURE__*/_jsx(View, {
2297
3152
  style: {
2298
- width: 12
3153
+ width: legendSpacing
2299
3154
  }
2300
3155
  }), legend]
2301
3156
  });
2302
3157
  }
2303
- return composed;
3158
+ return /*#__PURE__*/_jsxs(View, {
3159
+ style: {
3160
+ backgroundColor,
3161
+ alignItems: 'center'
3162
+ },
3163
+ children: [chart, /*#__PURE__*/_jsx(View, {
3164
+ style: {
3165
+ height: legendSpacing
3166
+ }
3167
+ }), legend]
3168
+ });
3169
+ }
3170
+ const RADAR_DASH_DEFAULT_LENGTH = 8;
3171
+ const RADAR_DASH_GAP_FACTOR = 0.6;
3172
+ const RADAR_DOT_DASH_LENGTH = 0.001;
3173
+ const RADAR_DOT_GAP_FACTOR = 2.4;
3174
+ const RADAR_DOT_MIN_GAP = 3;
3175
+ const radarDashConflictWarnings = new Set();
3176
+ function isDevelopmentEnvironment() {
3177
+ return process.env.NODE_ENV !== 'production';
3178
+ }
3179
+ function warnRadarDashConflictOnce(label) {
3180
+ if (!isDevelopmentEnvironment()) return;
3181
+ if (radarDashConflictWarnings.has(label)) return;
3182
+ radarDashConflictWarnings.add(label);
3183
+ // Keep API backward-compatible but deterministic when both flags are passed.
3184
+ console.warn(`[RadarChart] Dataset "${label}" received both dotted=true and dashed=true. dashed takes precedence.`);
3185
+ }
3186
+ function resolveRadarBorderStyle(dataset, fallbackLabel) {
3187
+ const dotted = dataset.dotted === true;
3188
+ const dashed = dataset.dashed === true;
3189
+ if (dotted && dashed) {
3190
+ warnRadarDashConflictOnce(fallbackLabel);
3191
+ return 'dashed';
3192
+ }
3193
+ if (dashed) return 'dashed';
3194
+ if (dotted) return 'dotted';
3195
+ const borderStyle = (dataset.borderStyle ?? 'solid').toString().toLowerCase();
3196
+ if (borderStyle === 'dotted' || borderStyle === 'dashed') {
3197
+ return borderStyle;
3198
+ }
3199
+ return 'solid';
3200
+ }
3201
+ function resolveRadarStrokePattern(dataset) {
3202
+ if (dataset.borderStyle === 'dotted') {
3203
+ // Tiny dash + round caps renders circular dots more reliably than short dashes.
3204
+ const gap = Math.max(RADAR_DOT_MIN_GAP, dataset.borderWidth * RADAR_DOT_GAP_FACTOR);
3205
+ return {
3206
+ strokeDasharray: `${RADAR_DOT_DASH_LENGTH} ${gap}`,
3207
+ strokeLinecap: 'round'
3208
+ };
3209
+ }
3210
+ if (dataset.borderStyle === 'dashed') {
3211
+ if (Array.isArray(dataset.dashArray) && dataset.dashArray.length > 0) {
3212
+ const dash = Math.max(1, dataset.dashArray[0] ?? RADAR_DASH_DEFAULT_LENGTH);
3213
+ const gap = Math.max(1, dataset.dashArray[1] ?? dash * RADAR_DASH_GAP_FACTOR);
3214
+ return {
3215
+ strokeDasharray: `${dash} ${gap}`,
3216
+ strokeLinecap: 'butt'
3217
+ };
3218
+ }
3219
+ const dashLength = typeof dataset.dashLength === 'number' && Number.isFinite(dataset.dashLength) && dataset.dashLength > 0 ? dataset.dashLength : RADAR_DASH_DEFAULT_LENGTH;
3220
+ const gap = Math.max(2, dashLength * RADAR_DASH_GAP_FACTOR);
3221
+ return {
3222
+ strokeDasharray: `${dashLength} ${gap}`,
3223
+ strokeLinecap: 'butt'
3224
+ };
3225
+ }
3226
+ return {};
3227
+ }
3228
+ export const RADAR_LABEL_ESTIMATION = {
3229
+ minRadius: 10,
3230
+ safeEdge: 4,
3231
+ minLabelWidthFactor: 1.2,
3232
+ avgCharWidthFactor: 0.58,
3233
+ lineHeightFactor: 1.2,
3234
+ defaultFontSize: 14,
3235
+ minLabelOffset: 20,
3236
+ labelOffsetFactor: 1.5
3237
+ };
3238
+ export function resolveRadarAxisLabelFontSize(value) {
3239
+ const parsed = Number(value);
3240
+ if (!Number.isFinite(parsed) || parsed <= 0) {
3241
+ return RADAR_LABEL_ESTIMATION.defaultFontSize;
3242
+ }
3243
+ return parsed;
3244
+ }
3245
+ export function resolveRadarLabelOffset(value, fontSize) {
3246
+ const parsed = Number(value);
3247
+ if (Number.isFinite(parsed) && parsed > 0) {
3248
+ return parsed;
3249
+ }
3250
+ return Math.max(RADAR_LABEL_ESTIMATION.minLabelOffset, fontSize * RADAR_LABEL_ESTIMATION.labelOffsetFactor);
3251
+ }
3252
+ export function estimateRadarLabelSize(label, fontSize) {
3253
+ const safeLabel = `${label ?? ''}`;
3254
+ const width = Math.max(safeLabel.length * fontSize * RADAR_LABEL_ESTIMATION.avgCharWidthFactor, fontSize * RADAR_LABEL_ESTIMATION.minLabelWidthFactor);
3255
+ const height = fontSize * RADAR_LABEL_ESTIMATION.lineHeightFactor;
3256
+ return {
3257
+ width,
3258
+ height
3259
+ };
3260
+ }
3261
+ export function calculateRadarChartRadius({
3262
+ width,
3263
+ height,
3264
+ padding,
3265
+ axisLabels,
3266
+ fontSize,
3267
+ autoFitLabels = true,
3268
+ labelOffset
3269
+ }) {
3270
+ const center = {
3271
+ x: width / 2,
3272
+ y: height / 2
3273
+ };
3274
+ const baseRadius = Math.max(Math.min(width, height) / 2 - Math.max(padding.left, padding.right, padding.top, padding.bottom), RADAR_LABEL_ESTIMATION.minRadius);
3275
+ if (!autoFitLabels || axisLabels.length === 0) {
3276
+ return baseRadius;
3277
+ }
3278
+ const angleStep = Math.PI * 2 / axisLabels.length;
3279
+ let maxAllowedRadius = baseRadius;
3280
+ for (let index = 0; index < axisLabels.length; index += 1) {
3281
+ const {
3282
+ width: labelWidth,
3283
+ height: labelHeight
3284
+ } = estimateRadarLabelSize(axisLabels[index] ?? '', fontSize);
3285
+ const halfWidth = labelWidth / 2;
3286
+ const halfHeight = labelHeight / 2;
3287
+ const angle = index * angleStep - Math.PI / 2;
3288
+ const dx = Math.cos(angle);
3289
+ const dy = Math.sin(angle);
3290
+
3291
+ // Solve per-axis line constraints so estimated label bounds stay inside.
3292
+ if (dx > 0.0001) {
3293
+ maxAllowedRadius = Math.min(maxAllowedRadius, (width - RADAR_LABEL_ESTIMATION.safeEdge - halfWidth - center.x) / dx - labelOffset);
3294
+ } else if (dx < -0.0001) {
3295
+ maxAllowedRadius = Math.min(maxAllowedRadius, (RADAR_LABEL_ESTIMATION.safeEdge + halfWidth - center.x) / dx - labelOffset);
3296
+ }
3297
+ if (dy > 0.0001) {
3298
+ maxAllowedRadius = Math.min(maxAllowedRadius, (height - RADAR_LABEL_ESTIMATION.safeEdge - halfHeight - center.y) / dy - labelOffset);
3299
+ } else if (dy < -0.0001) {
3300
+ maxAllowedRadius = Math.min(maxAllowedRadius, (RADAR_LABEL_ESTIMATION.safeEdge + halfHeight - center.y) / dy - labelOffset);
3301
+ }
3302
+ }
3303
+ const boundedRadius = Math.min(maxAllowedRadius, baseRadius);
3304
+ if (!Number.isFinite(boundedRadius)) {
3305
+ return baseRadius;
3306
+ }
3307
+ return Math.max(boundedRadius, RADAR_LABEL_ESTIMATION.minRadius);
2304
3308
  }
2305
3309
  export function parseRadarAxes(rawAxes) {
2306
3310
  if (!Array.isArray(rawAxes)) return [];
@@ -2310,9 +3314,10 @@ export function parseRadarAxes(rawAxes) {
2310
3314
  const label = axis.label ?? '';
2311
3315
  if (!label) return;
2312
3316
  const maxValue = resolveNumericValue(axis.maxValue, {}) ?? 100;
3317
+ const normalizedMaxValue = Number.isFinite(maxValue) && maxValue > 0 ? maxValue : 100;
2313
3318
  axes.push({
2314
3319
  label,
2315
- maxValue: maxValue > 0 ? maxValue : 100
3320
+ maxValue: normalizedMaxValue
2316
3321
  });
2317
3322
  });
2318
3323
  return axes;
@@ -2320,24 +3325,27 @@ export function parseRadarAxes(rawAxes) {
2320
3325
  export function parseRadarDatasets(raw, axisLength, formData) {
2321
3326
  if (!Array.isArray(raw) || axisLength === 0) return [];
2322
3327
  const datasets = [];
2323
- raw.forEach(dataset => {
3328
+ raw.forEach((dataset, datasetIndex) => {
2324
3329
  if (!dataset || typeof dataset !== 'object') return;
2325
3330
  const valuesRaw = Array.isArray(dataset.data) ? dataset.data : [];
2326
3331
  const values = [];
2327
3332
  for (let i = 0; i < axisLength; i += 1) {
2328
3333
  const rawValue = i < valuesRaw.length ? valuesRaw[i] : null;
2329
- const resolved = resolveNumericValue(rawValue, formData) ?? 0;
2330
- values.push(resolved);
3334
+ const resolved = resolveNumericValue(rawValue, formData);
3335
+ values.push(typeof resolved === 'number' && Number.isFinite(resolved) ? resolved : 0);
2331
3336
  }
2332
3337
  const label = dataset.label ?? `Dataset ${datasets.length + 1}`;
2333
3338
  const borderColor = parseColor(dataset.borderColor ?? '0xFF2196F3');
2334
3339
  const fillColor = dataset.fillColor ? parseColor(dataset.fillColor) : undefined;
2335
3340
  const pointColor = parseColor(dataset.pointColor ?? dataset.borderColor ?? '0xFF2196F3');
2336
- const borderWidth = Number(dataset.borderWidth ?? 2);
2337
- const pointRadius = Number(dataset.pointRadius ?? 4);
2338
- const borderStyle = (dataset.borderStyle ?? 'solid').toString().toLowerCase();
2339
- const normalizedStyle = borderStyle === 'dotted' || borderStyle === 'dashed' ? borderStyle : 'solid';
2340
- const dashArray = Array.isArray(dataset.dashArray) ? dataset.dashArray.map(val => Number(val)) : undefined;
3341
+ const rawBorderWidth = Number(dataset.borderWidth ?? 2);
3342
+ const borderWidth = Number.isFinite(rawBorderWidth) && rawBorderWidth > 0 ? rawBorderWidth : 2;
3343
+ const rawPointRadius = Number(dataset.pointRadius ?? 4);
3344
+ const pointRadius = Number.isFinite(rawPointRadius) && rawPointRadius >= 0 ? rawPointRadius : 4;
3345
+ const normalizedStyle = resolveRadarBorderStyle(dataset, label ?? `Dataset ${datasetIndex + 1}`);
3346
+ const dashArray = Array.isArray(dataset.dashArray) ? dataset.dashArray.map(val => Number(val)).filter(val => Number.isFinite(val) && val > 0) : undefined;
3347
+ const rawDashLength = Number(dataset.dashLength);
3348
+ const dashLength = Number.isFinite(rawDashLength) && rawDashLength > 0 ? rawDashLength : undefined;
2341
3349
  const showPoints = dataset.showPoints !== false;
2342
3350
  datasets.push({
2343
3351
  label,
@@ -2346,6 +3354,7 @@ export function parseRadarDatasets(raw, axisLength, formData) {
2346
3354
  borderWidth,
2347
3355
  borderStyle: normalizedStyle,
2348
3356
  dashArray,
3357
+ dashLength,
2349
3358
  showPoints,
2350
3359
  pointRadius,
2351
3360
  pointColor,
@@ -2359,7 +3368,9 @@ export function normalizeAxisMaximums(axes, datasets) {
2359
3368
  let maxValue = axis.maxValue;
2360
3369
  datasets.forEach(dataset => {
2361
3370
  const value = dataset.values[index];
2362
- if (value !== undefined && value > maxValue) maxValue = value;
3371
+ if (value !== undefined && Number.isFinite(value) && value > maxValue) {
3372
+ maxValue = value;
3373
+ }
2363
3374
  });
2364
3375
  return {
2365
3376
  ...axis,
@@ -2368,12 +3379,15 @@ export function normalizeAxisMaximums(axes, datasets) {
2368
3379
  });
2369
3380
  }
2370
3381
  function resolveRadarWidth(parsedWidth, measuredWidth) {
3382
+ if (measuredWidth > 0) {
3383
+ if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
3384
+ return Math.max(1, Math.min(parsedWidth, measuredWidth));
3385
+ }
3386
+ return Math.max(1, measuredWidth);
3387
+ }
2371
3388
  if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
2372
3389
  return parsedWidth;
2373
3390
  }
2374
- if (measuredWidth > 0) {
2375
- return measuredWidth;
2376
- }
2377
3391
  return 300;
2378
3392
  }
2379
3393
  function radarPlaceholder(message) {