flowboard-react 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 } from 'react-native';
5
- import { SafeAreaView } from 'react-native-safe-area-context';
4
+ import { Animated, 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,7 @@ 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,
72
100
  contentContainerStyle: {
73
101
  ...contentPaddingStyle,
74
102
  paddingTop: showProgress ? 0 : padding.top
@@ -76,14 +104,20 @@ export default function FlowboardRenderer(props) {
76
104
  children: /*#__PURE__*/_jsx(View, {
77
105
  style: {
78
106
  flexGrow: 1,
107
+ flexDirection: 'column',
79
108
  justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
80
- alignItems: parseRootCrossAlignment(rootCrossAxisAlignment)
109
+ alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
110
+ minHeight: 0,
111
+ minWidth: 0
81
112
  },
82
113
  children: childrenData.map((child, index) => buildWidget(child, {
83
114
  onAction,
84
115
  formData,
85
116
  onInputChange,
86
117
  allowFlexExpansion: true,
118
+ parentFlexAxis: rootAxis,
119
+ parentMainAxisBounded: pageAxisBounds.heightBounded,
120
+ pageAxisBounds,
87
121
  screenData,
88
122
  enableFontAwesomeIcons,
89
123
  key: `child-${index}`
@@ -102,6 +136,9 @@ export default function FlowboardRenderer(props) {
102
136
  formData,
103
137
  onInputChange,
104
138
  allowFlexExpansion: true,
139
+ parentFlexAxis: rootAxis,
140
+ parentMainAxisBounded: true,
141
+ pageAxisBounds,
105
142
  screenData,
106
143
  enableFontAwesomeIcons,
107
144
  key: `child-${index}`
@@ -110,12 +147,10 @@ export default function FlowboardRenderer(props) {
110
147
  });
111
148
  return /*#__PURE__*/_jsxs(View, {
112
149
  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'],
150
+ children: [renderBackground(backgroundData, bgColorCode), /*#__PURE__*/_jsx(View, {
151
+ style: [styles.contentLayer, safeAreaPaddingStyle],
117
152
  children: content
118
- }) : content]
153
+ })]
119
154
  });
120
155
  }
121
156
  function renderBackground(bgData, legacyColorCode) {
@@ -272,17 +307,411 @@ function renderProgressBar(currentIndex, totalScreens, color, thickness, radius,
272
307
  })
273
308
  });
274
309
  }
310
+ const STACK_SPACE_DISTRIBUTIONS = new Set(['spaceBetween', 'spaceAround', 'spaceEvenly']);
311
+ function normalizeStackSpacingConfig(value) {
312
+ if (typeof value === 'number' && Number.isFinite(value)) {
313
+ return {
314
+ x: value,
315
+ y: value
316
+ };
317
+ }
318
+ if (!value || typeof value !== 'object') {
319
+ return {
320
+ x: 0,
321
+ y: 0
322
+ };
323
+ }
324
+ const x = Number(value.x ?? value.horizontal);
325
+ const y = Number(value.y ?? value.vertical);
326
+ const top = Number(value.top);
327
+ const right = Number(value.right);
328
+ const bottom = Number(value.bottom);
329
+ const left = Number(value.left);
330
+ const normalized = {};
331
+ if (!Number.isNaN(x)) normalized.x = x;
332
+ if (!Number.isNaN(y)) normalized.y = y;
333
+ if (!Number.isNaN(top)) normalized.top = top;
334
+ if (!Number.isNaN(right)) normalized.right = right;
335
+ if (!Number.isNaN(bottom)) normalized.bottom = bottom;
336
+ if (!Number.isNaN(left)) normalized.left = left;
337
+ if (Object.keys(normalized).length === 0) {
338
+ return {
339
+ x: 0,
340
+ y: 0
341
+ };
342
+ }
343
+ return normalized;
344
+ }
345
+ function stackSpacingToInsets(value) {
346
+ const normalized = normalizeStackSpacingConfig(value);
347
+ return parseInsets({
348
+ horizontal: normalized.x,
349
+ vertical: normalized.y,
350
+ top: normalized.top,
351
+ right: normalized.right,
352
+ bottom: normalized.bottom,
353
+ left: normalized.left
354
+ });
355
+ }
356
+ function isLegacyOverlayAlignment(value) {
357
+ return ['topLeft', 'topCenter', 'topRight', 'centerLeft', 'center', 'centerRight', 'bottomLeft', 'bottomCenter', 'bottomRight'].includes(String(value ?? ''));
358
+ }
359
+ function normalizeStackAxis(type, props) {
360
+ const explicit = String(props.axis ?? '').toLowerCase();
361
+ if (explicit === 'vertical' || explicit === 'horizontal' || explicit === 'overlay') {
362
+ return explicit;
363
+ }
364
+ if (type === 'container') return 'overlay';
365
+ if (type === 'row') return 'horizontal';
366
+ if (type === 'column') return 'vertical';
367
+ if (type === 'layout') {
368
+ return props.direction === 'horizontal' ? 'horizontal' : 'vertical';
369
+ }
370
+ if (type === 'stack' && (props.fit !== undefined || isLegacyOverlayAlignment(props.alignment))) {
371
+ return 'overlay';
372
+ }
373
+ return 'vertical';
374
+ }
375
+ function normalizeStackDistribution(value) {
376
+ const normalized = String(value ?? '').trim();
377
+ if (normalized === 'center' || normalized === 'end' || normalized === 'spaceBetween' || normalized === 'spaceAround' || normalized === 'spaceEvenly') {
378
+ return normalized;
379
+ }
380
+ return 'start';
381
+ }
382
+ function normalizeStackCrossAlignment(value) {
383
+ const raw = String(value ?? '').toLowerCase();
384
+ if (raw === 'center' || raw.includes('center')) return 'center';
385
+ if (raw === 'end' || raw.endsWith('right') || raw.startsWith('bottom')) return 'end';
386
+ return 'start';
387
+ }
388
+ function normalizeOverlayAlignment(value) {
389
+ const raw = String(value ?? '').trim();
390
+ switch (raw) {
391
+ case 'topStart':
392
+ case 'top':
393
+ case 'topEnd':
394
+ case 'start':
395
+ case 'center':
396
+ case 'end':
397
+ case 'bottomStart':
398
+ case 'bottom':
399
+ case 'bottomEnd':
400
+ return raw;
401
+ case 'topLeft':
402
+ return 'topStart';
403
+ case 'topCenter':
404
+ return 'top';
405
+ case 'topRight':
406
+ return 'topEnd';
407
+ case 'centerLeft':
408
+ return 'start';
409
+ case 'centerRight':
410
+ return 'end';
411
+ case 'bottomLeft':
412
+ return 'bottomStart';
413
+ case 'bottomCenter':
414
+ return 'bottom';
415
+ case 'bottomRight':
416
+ return 'bottomEnd';
417
+ default:
418
+ return 'center';
419
+ }
420
+ }
421
+ function isFillDimensionValue(value) {
422
+ if (typeof value === 'number') return value === Number.POSITIVE_INFINITY;
423
+ if (typeof value !== 'string') return false;
424
+ const normalized = value.trim().toLowerCase();
425
+ return normalized === 'infinity' || normalized === 'double.infinity' || normalized === '+infinity' || normalized === '100%';
426
+ }
427
+ function isFixedDimensionValue(value) {
428
+ if (isFillDimensionValue(value)) return false;
429
+ const parsed = Number(value);
430
+ return Number.isFinite(parsed);
431
+ }
432
+ function normalizeStackSize(props, axis) {
433
+ const widthMode = String(props.size?.width ?? '').toLowerCase();
434
+ const heightMode = String(props.size?.height ?? '').toLowerCase();
435
+ const width = widthMode === 'fill' || widthMode === 'fit' || widthMode === 'fixed' ? widthMode : props.fit === 'expand' || props.width === 'infinity' ? 'fill' : isFixedDimensionValue(props.width) ? 'fixed' : axis === 'horizontal' ? 'fit' : 'fill';
436
+ const height = heightMode === 'fill' || heightMode === 'fit' || heightMode === 'fixed' ? heightMode : props.fit === 'expand' || props.height === 'infinity' ? 'fill' : isFixedDimensionValue(props.height) ? 'fixed' : axis === 'vertical' ? 'fit' : 'fill';
437
+ return {
438
+ width,
439
+ height
440
+ };
441
+ }
442
+ function normalizeStackFill(props) {
443
+ if (props.fill && typeof props.fill === 'object') {
444
+ const next = {
445
+ ...props.fill
446
+ };
447
+ if (!next.type && next.color) next.type = 'solid';
448
+ return next;
449
+ }
450
+ if (props.background && typeof props.background === 'object') {
451
+ if (props.background.type === 'color') {
452
+ return {
453
+ type: 'solid',
454
+ color: props.background.color
455
+ };
456
+ }
457
+ return {
458
+ ...props.background
459
+ };
460
+ }
461
+ if (props.backgroundColor) {
462
+ return {
463
+ type: 'solid',
464
+ color: props.backgroundColor
465
+ };
466
+ }
467
+ return undefined;
468
+ }
469
+ function normalizeStackBorder(props) {
470
+ if (props.border && typeof props.border === 'object') {
471
+ return {
472
+ ...props.border
473
+ };
474
+ }
475
+ if (props.borderColor || props.borderWidth !== undefined) {
476
+ return {
477
+ width: Number(props.borderWidth ?? 1),
478
+ color: props.borderColor
479
+ };
480
+ }
481
+ return undefined;
482
+ }
483
+ function normalizeStackShadow(props) {
484
+ if (props.shadow && typeof props.shadow === 'object') {
485
+ return {
486
+ ...props.shadow
487
+ };
488
+ }
489
+ return undefined;
490
+ }
491
+ function spacingToInsets(value) {
492
+ const normalized = normalizeStackSpacingConfig(value);
493
+ const x = normalized.x ?? 0;
494
+ const y = normalized.y ?? 0;
495
+ return {
496
+ top: normalized.top ?? y,
497
+ right: normalized.right ?? x,
498
+ bottom: normalized.bottom ?? y,
499
+ left: normalized.left ?? x
500
+ };
501
+ }
502
+ function insetsToSpacing(insets) {
503
+ if (insets.top === insets.bottom && insets.left === insets.right) {
504
+ return {
505
+ x: insets.left,
506
+ y: insets.top
507
+ };
508
+ }
509
+ return insets;
510
+ }
511
+ function mergeStackProps(parentProps, childProps) {
512
+ const parentAxis = String(parentProps.axis ?? 'vertical');
513
+ const childAxis = String(childProps.axis ?? 'vertical');
514
+ let axis = childAxis;
515
+ if (parentAxis !== childAxis) {
516
+ if (parentAxis === 'overlay' && childAxis !== 'overlay') axis = childAxis;else if (childAxis === 'overlay' && parentAxis !== 'overlay') axis = parentAxis;
517
+ }
518
+ const axisSource = axis === parentAxis && axis !== childAxis ? parentProps : childProps;
519
+ const fallbackAxisSource = axisSource === childProps ? parentProps : childProps;
520
+ const parentPadding = spacingToInsets(parentProps.layout?.padding);
521
+ const childPadding = spacingToInsets(childProps.layout?.padding);
522
+ const parentMargin = spacingToInsets(parentProps.layout?.margin);
523
+ const childMargin = spacingToInsets(childProps.layout?.margin);
524
+ const mergedProps = {
525
+ axis,
526
+ alignment: axisSource.alignment ?? fallbackAxisSource.alignment ?? 'start',
527
+ distribution: axisSource.distribution ?? fallbackAxisSource.distribution ?? 'start',
528
+ childSpacing: Number(axisSource.childSpacing ?? fallbackAxisSource.childSpacing ?? 0) || 0,
529
+ size: {
530
+ width: childProps.size?.width ?? parentProps.size?.width ?? 'fill',
531
+ height: childProps.size?.height ?? parentProps.size?.height ?? 'fit'
532
+ },
533
+ layout: {
534
+ padding: insetsToSpacing({
535
+ top: parentPadding.top + childPadding.top,
536
+ right: parentPadding.right + childPadding.right,
537
+ bottom: parentPadding.bottom + childPadding.bottom,
538
+ left: parentPadding.left + childPadding.left
539
+ }),
540
+ margin: insetsToSpacing({
541
+ top: parentMargin.top + childMargin.top,
542
+ right: parentMargin.right + childMargin.right,
543
+ bottom: parentMargin.bottom + childMargin.bottom,
544
+ left: parentMargin.left + childMargin.left
545
+ })
546
+ },
547
+ appearance: {
548
+ shape: 'rectangle',
549
+ cornerRadius: Number(childProps.appearance?.cornerRadius ?? parentProps.appearance?.cornerRadius ?? 0)
550
+ }
551
+ };
552
+ if (axis === 'overlay') {
553
+ mergedProps.overlayAlignment = childProps.overlayAlignment ?? parentProps.overlayAlignment ?? 'center';
554
+ }
555
+ if (parentProps.fill || childProps.fill) {
556
+ mergedProps.fill = childProps.fill ?? parentProps.fill;
557
+ }
558
+ if (parentProps.border || childProps.border) {
559
+ mergedProps.border = childProps.border ?? parentProps.border;
560
+ }
561
+ if (parentProps.shadow || childProps.shadow) {
562
+ mergedProps.shadow = childProps.shadow ?? parentProps.shadow;
563
+ }
564
+ if (childProps.width !== undefined || parentProps.width !== undefined) {
565
+ mergedProps.width = childProps.width ?? parentProps.width;
566
+ }
567
+ if (childProps.height !== undefined || parentProps.height !== undefined) {
568
+ mergedProps.height = childProps.height ?? parentProps.height;
569
+ }
570
+ return mergedProps;
571
+ }
572
+ function normalizeStackLikeNode(json) {
573
+ if (!json || typeof json !== 'object') return json;
574
+ const type = String(json.type ?? '');
575
+ if (!['stack', 'container', 'layout', 'row', 'column', 'grid'].includes(type)) {
576
+ return json;
577
+ }
578
+ const props = {
579
+ ...(json.properties ?? {})
580
+ };
581
+ const axis = normalizeStackAxis(type, props);
582
+ const size = normalizeStackSize(props, axis);
583
+ const children = Array.isArray(json.children) ? [...json.children] : [];
584
+ if (json.child) {
585
+ children.push(json.child);
586
+ }
587
+ const normalizedProps = {
588
+ axis,
589
+ alignment: normalizeStackCrossAlignment(props.alignment ?? props.crossAxisAlignment),
590
+ distribution: normalizeStackDistribution(props.distribution ?? props.mainAxisAlignment),
591
+ childSpacing: Number(props.childSpacing ?? props.gap ?? props.spacing ?? 0),
592
+ size,
593
+ layout: {
594
+ padding: normalizeStackSpacingConfig(props.layout?.padding ?? props.padding),
595
+ margin: normalizeStackSpacingConfig(props.layout?.margin ?? props.margin)
596
+ },
597
+ appearance: {
598
+ shape: 'rectangle',
599
+ cornerRadius: Number(props.appearance?.cornerRadius ?? props.borderRadius ?? 0)
600
+ }
601
+ };
602
+ if (axis === 'overlay') {
603
+ normalizedProps.overlayAlignment = normalizeOverlayAlignment(props.overlayAlignment ?? props.alignment);
604
+ }
605
+ const fill = normalizeStackFill(props);
606
+ if (fill) normalizedProps.fill = fill;
607
+ const border = normalizeStackBorder(props);
608
+ if (border) normalizedProps.border = border;
609
+ const shadow = normalizeStackShadow(props);
610
+ if (shadow) normalizedProps.shadow = shadow;
611
+ if (props.width !== undefined) normalizedProps.width = props.width;
612
+ if (props.height !== undefined) normalizedProps.height = props.height;
613
+ if ((type === 'container' || type === 'layout') && children.length === 1) {
614
+ const childCandidate = children[0];
615
+ if (childCandidate && typeof childCandidate === 'object') {
616
+ const normalizedChild = normalizeStackLikeNode(childCandidate);
617
+ if (normalizedChild?.type === 'stack') {
618
+ const childProps = {
619
+ ...(normalizedChild.properties ?? {})
620
+ };
621
+ const mergedProps = mergeStackProps(normalizedProps, childProps);
622
+ return {
623
+ ...json,
624
+ type: 'stack',
625
+ properties: mergedProps,
626
+ children: Array.isArray(normalizedChild.children) ? normalizedChild.children : [],
627
+ child: undefined
628
+ };
629
+ }
630
+ }
631
+ }
632
+ return {
633
+ ...json,
634
+ type: 'stack',
635
+ properties: normalizedProps,
636
+ children,
637
+ child: undefined
638
+ };
639
+ }
640
+ function parseOverlayGridAlignment(value) {
641
+ switch (normalizeOverlayAlignment(value)) {
642
+ case 'topStart':
643
+ return {
644
+ justifyContent: 'flex-start',
645
+ alignItems: 'flex-start'
646
+ };
647
+ case 'top':
648
+ return {
649
+ justifyContent: 'flex-start',
650
+ alignItems: 'center'
651
+ };
652
+ case 'topEnd':
653
+ return {
654
+ justifyContent: 'flex-start',
655
+ alignItems: 'flex-end'
656
+ };
657
+ case 'start':
658
+ return {
659
+ justifyContent: 'center',
660
+ alignItems: 'flex-start'
661
+ };
662
+ case 'end':
663
+ return {
664
+ justifyContent: 'center',
665
+ alignItems: 'flex-end'
666
+ };
667
+ case 'bottomStart':
668
+ return {
669
+ justifyContent: 'flex-end',
670
+ alignItems: 'flex-start'
671
+ };
672
+ case 'bottom':
673
+ return {
674
+ justifyContent: 'flex-end',
675
+ alignItems: 'center'
676
+ };
677
+ case 'bottomEnd':
678
+ return {
679
+ justifyContent: 'flex-end',
680
+ alignItems: 'flex-end'
681
+ };
682
+ case 'center':
683
+ default:
684
+ return {
685
+ justifyContent: 'center',
686
+ alignItems: 'center'
687
+ };
688
+ }
689
+ }
690
+ function resolveStackDimension(props, axis, key, axisBounds) {
691
+ const mode = props.size?.[key];
692
+ const legacy = normalizeDimension(parseLayoutDimension(props[key]));
693
+ const isBounded = key === 'width' ? axisBounds.widthBounded : axisBounds.heightBounded;
694
+ if (mode === 'fill') return isBounded ? '100%' : legacy;
695
+ if (mode === 'fit') return legacy;
696
+ if (mode === 'fixed') return legacy;
697
+ if (legacy !== undefined) return legacy;
698
+ if (key === 'width' && axis !== 'horizontal' && axisBounds.widthBounded) return '100%';
699
+ if (key === 'height' && axis === 'horizontal' && axisBounds.heightBounded) return '100%';
700
+ return undefined;
701
+ }
275
702
  function buildWidget(json, params) {
276
703
  if (!json || typeof json !== 'object') return null;
277
- const type = json.type;
278
- const id = json.id;
704
+ const normalizedJson = normalizeStackLikeNode(json);
705
+ const type = normalizedJson.type;
706
+ const id = normalizedJson.id;
279
707
  const props = {
280
- ...(json.properties ?? {})
708
+ ...(normalizedJson.properties ?? {})
281
709
  };
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;
710
+ const pageAxisBounds = params.pageAxisBounds ?? getAxisBoundsForPageScroll(resolvePageScrollAxis(params.screenData));
711
+ const childrenJson = Array.isArray(normalizedJson.children) ? normalizedJson.children : undefined;
712
+ const childJson = normalizedJson.child;
713
+ const action = normalizedJson.action;
714
+ const actionPayload = action ? buildActionPayload(props, normalizedJson) : undefined;
286
715
  let node = null;
287
716
  switch (type) {
288
717
  case 'column':
@@ -457,55 +886,83 @@ function buildWidget(json, params) {
457
886
  const width = normalizeDimension(parseLayoutDimension(props.width)) ?? '100%';
458
887
  const height = normalizeDimension(parseLayoutDimension(props.height)) ?? 50;
459
888
  const label = props.label ?? 'Button';
889
+ const normalizedStroke = normalizeButtonStroke(props.stroke);
890
+ const normalizedEffects = normalizeButtonEffects(props.effects, props.shadow);
460
891
  const textStyle = getTextStyle({
461
892
  ...props,
462
893
  height: null
463
894
  }, '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,
895
+ const backgroundColor = parseColor(props.color ?? '0xFF2196F3');
896
+ const shadowBounds = getButtonShadowLayerBounds(normalizedStroke, borderRadius);
897
+ const webShadowStyle = buildButtonWebShadowStyle(normalizedEffects);
898
+ const firstNativeEffect = normalizedEffects.length > 0 ? normalizedEffects[0] : null;
899
+ const nativePrimaryShadowStyle = Platform.OS === 'web' || !firstNativeEffect ? null : buildButtonNativeShadowStyle(firstNativeEffect);
900
+ const centeredStrokeStyle = normalizedStroke && normalizedStroke.position === 'center' ? {
901
+ borderWidth: normalizedStroke.width,
902
+ borderColor: normalizedStroke.color
903
+ } : null;
904
+ const fillLayerStyle = {
905
+ position: 'absolute',
906
+ top: 0,
907
+ right: 0,
908
+ bottom: 0,
909
+ left: 0,
910
+ borderRadius,
911
+ overflow: 'hidden',
912
+ ...(centeredStrokeStyle ?? {})
913
+ };
914
+ const nativeShadowEffectsInPaintOrder = normalizedEffects.slice().reverse();
915
+ const pressableStyle = {
916
+ width,
917
+ height,
918
+ borderRadius,
919
+ justifyContent: 'center',
920
+ alignItems: 'center',
921
+ overflow: 'visible',
922
+ ...(nativePrimaryShadowStyle ?? {})
923
+ };
924
+ node = /*#__PURE__*/_jsxs(Pressable, {
925
+ collapsable: false,
926
+ onPress: action ? () => params.onAction(action, actionPayload ?? props) : undefined,
927
+ style: pressableStyle,
928
+ children: [Platform.OS === 'web' ? webShadowStyle ? /*#__PURE__*/_jsx(View, {
929
+ pointerEvents: "none",
478
930
  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,
931
+ position: 'absolute',
932
+ ...shadowBounds,
933
+ ...webShadowStyle
934
+ }
935
+ }) : null : nativeShadowEffectsInPaintOrder.map((effect, index) => /*#__PURE__*/_jsx(View, {
936
+ collapsable: false,
937
+ pointerEvents: "none",
498
938
  style: {
499
- width,
500
- height,
501
- backgroundColor: parseColor(props.color ?? '0xFF2196F3'),
502
- borderRadius,
939
+ position: 'absolute',
940
+ ...shadowBounds,
941
+ ...buildButtonNativeShadowStyle(effect)
942
+ }
943
+ }, `shadow-${index}`)), gradient ? /*#__PURE__*/_jsx(LinearGradient, {
944
+ colors: gradient.colors,
945
+ start: gradient.start,
946
+ end: gradient.end,
947
+ locations: gradient.stops,
948
+ style: fillLayerStyle
949
+ }) : /*#__PURE__*/_jsx(View, {
950
+ style: {
951
+ ...fillLayerStyle,
952
+ backgroundColor
953
+ }
954
+ }), renderButtonStrokeOverlay(normalizedStroke, borderRadius), /*#__PURE__*/_jsx(View, {
955
+ style: {
956
+ flex: 1,
503
957
  justifyContent: 'center',
504
958
  alignItems: 'center'
505
959
  },
506
- children: content
507
- });
508
- }
960
+ children: /*#__PURE__*/_jsx(Text, {
961
+ style: textStyle,
962
+ children: label
963
+ })
964
+ })]
965
+ });
509
966
  break;
510
967
  }
511
968
  case 'text_input':
@@ -819,22 +1276,112 @@ function buildWidget(json, params) {
819
1276
  }
820
1277
  case 'stack':
821
1278
  {
822
- const alignment = parseAlignment(props.alignment ?? 'topLeft');
823
- const fit = props.fit === 'expand' ? 'expand' : 'loose';
824
- node = /*#__PURE__*/_jsx(View, {
1279
+ const axis = props.axis === 'horizontal' || props.axis === 'overlay' ? props.axis : 'vertical';
1280
+ const isOverlay = axis === 'overlay';
1281
+ const spacing = Number(props.childSpacing ?? 0);
1282
+ const applySpacing = !STACK_SPACE_DISTRIBUTIONS.has(String(props.distribution ?? 'start'));
1283
+ const paddingInsets = stackSpacingToInsets(props.layout?.padding ?? props.padding);
1284
+ const width = resolveStackDimension(props, axis, 'width', pageAxisBounds);
1285
+ const height = resolveStackDimension(props, axis, 'height', pageAxisBounds);
1286
+ const mainAxisBounded = axis === 'horizontal' ? pageAxisBounds.widthBounded : pageAxisBounds.heightBounded;
1287
+ const borderRadius = Number(props.appearance?.cornerRadius ?? props.borderRadius ?? 0);
1288
+ const borderWidth = Number(props.border?.width ?? 0);
1289
+ const borderColor = borderWidth > 0 ? parseColor(props.border?.color ?? '#E5E7EB') : null;
1290
+ const shadowColor = props.shadow?.color ? parseColor(props.shadow.color) : null;
1291
+ const shadowStyle = shadowColor && props.shadow ? Platform.OS === 'web' ? {
1292
+ boxShadow: `${Number(props.shadow.x ?? 0)}px ${Number(props.shadow.y ?? 8)}px ${Number(props.shadow.blur ?? 24)}px ${shadowColor}`
1293
+ } : {
1294
+ shadowColor,
1295
+ shadowOffset: {
1296
+ width: Number(props.shadow.x ?? 0),
1297
+ height: Number(props.shadow.y ?? 8)
1298
+ },
1299
+ shadowOpacity: 0.35,
1300
+ shadowRadius: Math.max(0, Number(props.shadow.blur ?? 24) / 2),
1301
+ elevation: Math.max(1, Math.round((Number(props.shadow.blur ?? 24) + Math.abs(Number(props.shadow.y ?? 8))) / 3))
1302
+ } : {};
1303
+ const fill = props.fill;
1304
+ const stackGradient = fill?.type === 'gradient' ? parseGradient({
1305
+ ...fill,
1306
+ type: 'gradient'
1307
+ }) : undefined;
1308
+ const stackColor = fill?.type === 'solid' || !fill?.type && fill?.color ? parseColor(fill?.color ?? '#FFFFFFFF') : parseColor(props.backgroundColor ?? '#00000000');
1309
+ const baseStackStyle = {
1310
+ width,
1311
+ height,
1312
+ borderRadius: borderRadius > 0 ? borderRadius : undefined,
1313
+ overflow: borderRadius > 0 ? 'hidden' : undefined,
1314
+ borderWidth: borderColor ? borderWidth : undefined,
1315
+ borderColor: borderColor ?? undefined,
1316
+ ...insetsToStyle(paddingInsets),
1317
+ ...shadowStyle
1318
+ };
1319
+ const content = isOverlay ? /*#__PURE__*/_jsxs(View, {
825
1320
  style: {
826
1321
  position: 'relative',
827
- width: fit === 'expand' ? '100%' : undefined,
828
- height: fit === 'expand' ? '100%' : undefined,
829
- justifyContent: alignment.justifyContent,
830
- alignItems: alignment.alignItems
1322
+ minHeight: 0,
1323
+ minWidth: 0
831
1324
  },
832
- children: (childrenJson ?? []).map((child, index) => /*#__PURE__*/_jsx(React.Fragment, {
833
- children: buildWidget(child, {
834
- ...params
835
- })
1325
+ children: [(childrenJson ?? []).map((child, index) => {
1326
+ const alignment = parseOverlayGridAlignment(props.overlayAlignment ?? props.alignment);
1327
+ return /*#__PURE__*/_jsx(View, {
1328
+ style: {
1329
+ position: 'absolute',
1330
+ top: 0,
1331
+ right: 0,
1332
+ bottom: 0,
1333
+ left: 0,
1334
+ justifyContent: alignment.justifyContent,
1335
+ alignItems: alignment.alignItems
1336
+ },
1337
+ children: buildWidget(child, {
1338
+ ...params,
1339
+ allowFlexExpansion: true,
1340
+ pageAxisBounds
1341
+ })
1342
+ }, `stack-overlay-${index}`);
1343
+ }), (childrenJson ?? []).length === 0 ? null : null]
1344
+ }) : /*#__PURE__*/_jsx(View, {
1345
+ style: {
1346
+ flexDirection: axis === 'horizontal' ? 'row' : 'column',
1347
+ justifyContent: parseFlexAlignment(props.distribution),
1348
+ alignItems: parseCrossAlignment(props.alignment),
1349
+ minHeight: 0,
1350
+ minWidth: 0
1351
+ },
1352
+ children: (childrenJson ?? []).map((child, index) => /*#__PURE__*/_jsxs(React.Fragment, {
1353
+ children: [buildWidget(child, {
1354
+ ...params,
1355
+ allowFlexExpansion: true,
1356
+ parentFlexAxis: axis === 'horizontal' ? 'horizontal' : 'vertical',
1357
+ parentMainAxisBounded: mainAxisBounded,
1358
+ pageAxisBounds
1359
+ }), applySpacing && spacing > 0 && index < (childrenJson?.length ?? 0) - 1 ? /*#__PURE__*/_jsx(View, {
1360
+ style: {
1361
+ width: axis === 'horizontal' ? spacing : undefined,
1362
+ height: axis === 'vertical' ? spacing : undefined
1363
+ }
1364
+ }) : null]
836
1365
  }, `stack-${index}`))
837
1366
  });
1367
+ if (stackGradient) {
1368
+ node = /*#__PURE__*/_jsx(LinearGradient, {
1369
+ colors: stackGradient.colors,
1370
+ start: stackGradient.start,
1371
+ end: stackGradient.end,
1372
+ locations: stackGradient.stops,
1373
+ style: baseStackStyle,
1374
+ children: content
1375
+ });
1376
+ } else {
1377
+ node = /*#__PURE__*/_jsx(View, {
1378
+ style: {
1379
+ ...baseStackStyle,
1380
+ backgroundColor: stackColor
1381
+ },
1382
+ children: content
1383
+ });
1384
+ }
838
1385
  break;
839
1386
  }
840
1387
  case 'positioned':
@@ -865,35 +1412,55 @@ function buildWidget(json, params) {
865
1412
  node = null;
866
1413
  }
867
1414
  if (!node) return null;
868
- const marginVal = props.margin;
1415
+ const marginVal = type === 'stack' ? props.layout?.margin ?? props.margin : props.margin;
869
1416
  if (marginVal !== undefined && marginVal !== null) {
870
- const marginInsets = parseInsets(marginVal);
1417
+ const marginInsets = type === 'stack' ? stackSpacingToInsets(marginVal) : parseInsets(marginVal);
871
1418
  node = /*#__PURE__*/_jsx(View, {
872
1419
  style: insetsToMarginStyle(marginInsets),
873
1420
  children: node
874
1421
  });
875
- } else if (type !== 'container' && type !== 'padding' && props.padding) {
1422
+ } else if (type !== 'container' && type !== 'padding' && type !== 'stack' && props.padding) {
876
1423
  const paddingInsets = parseInsets(props.padding);
877
1424
  node = /*#__PURE__*/_jsx(View, {
878
1425
  style: insetsToStyle(paddingInsets),
879
1426
  children: node
880
1427
  });
881
1428
  }
882
- if (params.allowFlexExpansion && params.screenData.scrollable !== true) {
1429
+ if (params.allowFlexExpansion) {
883
1430
  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;
1431
+ const parentAxis = params.parentFlexAxis ?? 'vertical';
1432
+ const parentMainAxisBounded = params.parentMainAxisBounded ?? true;
1433
+ if (parentMainAxisBounded) {
1434
+ if (type === 'stack') {
1435
+ const legacyExpand = props.fit === 'expand';
1436
+ const widthMode = String(props.size?.width ?? '').toLowerCase();
1437
+ const heightMode = String(props.size?.height ?? '').toLowerCase();
1438
+ if (parentAxis === 'vertical' && (legacyExpand || heightMode === 'fill')) {
1439
+ shouldExpand = true;
1440
+ }
1441
+ if (parentAxis === 'horizontal' && (legacyExpand || widthMode === 'fill')) {
1442
+ shouldExpand = true;
1443
+ }
1444
+ }
1445
+ if (parentAxis === 'vertical' && props.height !== undefined) {
1446
+ const heightVal = parseLayoutDimension(props.height);
1447
+ if (heightVal === Number.POSITIVE_INFINITY) {
1448
+ shouldExpand = true;
1449
+ }
1450
+ }
1451
+ if (parentAxis === 'horizontal' && props.width !== undefined) {
1452
+ const widthVal = parseLayoutDimension(props.width);
1453
+ if (widthVal === Number.POSITIVE_INFINITY) {
1454
+ shouldExpand = true;
1455
+ }
891
1456
  }
892
1457
  }
893
1458
  if (shouldExpand) {
894
1459
  node = /*#__PURE__*/_jsx(View, {
895
1460
  style: {
896
- flex: 1
1461
+ flex: 1,
1462
+ minHeight: 0,
1463
+ minWidth: 0
897
1464
  },
898
1465
  children: node
899
1466
  });
@@ -958,6 +1525,132 @@ function parseGradient(value) {
958
1525
  stops
959
1526
  };
960
1527
  }
1528
+ function normalizeButtonStroke(input) {
1529
+ if (!input || typeof input !== 'object') return null;
1530
+ const legacyStrokeWithoutEnabled = input.enabled === undefined;
1531
+ const enabled = legacyStrokeWithoutEnabled ? true : input.enabled === true;
1532
+ if (!enabled) return null;
1533
+ const width = Number(input.width ?? 1);
1534
+ if (!Number.isFinite(width) || width <= 0) return null;
1535
+ const position = input.position === 'inside' || input.position === 'center' || input.position === 'outside' ? input.position : 'center';
1536
+ const opacity = clamp01(Number(input.opacity ?? 1));
1537
+ const color = normalizeColorWithOpacity(input.color, opacity);
1538
+ if (color === 'transparent') return null;
1539
+ return {
1540
+ color,
1541
+ width,
1542
+ position
1543
+ };
1544
+ }
1545
+ function normalizeButtonEffects(effectsInput, legacyShadowInput) {
1546
+ if (Array.isArray(effectsInput)) {
1547
+ return effectsInput.map(normalizeDropShadowEffect).filter(effect => effect !== null);
1548
+ }
1549
+ if (!legacyShadowInput || typeof legacyShadowInput !== 'object') {
1550
+ return [];
1551
+ }
1552
+ if (legacyShadowInput.enabled !== true) return [];
1553
+ const migrated = normalizeDropShadowEffect({
1554
+ type: 'dropShadow',
1555
+ enabled: true,
1556
+ x: legacyShadowInput.x,
1557
+ y: legacyShadowInput.y,
1558
+ blur: legacyShadowInput.blur,
1559
+ spread: 0,
1560
+ color: legacyShadowInput.color,
1561
+ opacity: 1
1562
+ });
1563
+ return migrated ? [migrated] : [];
1564
+ }
1565
+ function normalizeDropShadowEffect(effect) {
1566
+ if (!effect || typeof effect !== 'object') return null;
1567
+ if ((effect.type ?? 'dropShadow') !== 'dropShadow') return null;
1568
+ if (effect.enabled === false) return null;
1569
+ const opacity = clamp01(Number(effect.opacity ?? 1));
1570
+ const color = normalizeColorWithOpacity(effect.color, opacity);
1571
+ if (color === 'transparent') return null;
1572
+ return {
1573
+ x: Number(effect.x ?? 0),
1574
+ y: Number(effect.y ?? 0),
1575
+ blur: Math.max(0, Number(effect.blur ?? 0)),
1576
+ spread: Number(effect.spread ?? 0),
1577
+ color,
1578
+ opacity
1579
+ };
1580
+ }
1581
+ function normalizeColorWithOpacity(input, opacity) {
1582
+ const raw = typeof input === 'string' ? input.trim() : '';
1583
+ const normalizedOpacity = clamp01(opacity);
1584
+ if (raw.startsWith('rgba(') || raw.startsWith('rgb(')) {
1585
+ return withOpacity(raw, normalizedOpacity);
1586
+ }
1587
+ return withOpacity(parseColor(input ?? '0xFF000000'), normalizedOpacity);
1588
+ }
1589
+ function buildButtonWebShadowStyle(effects) {
1590
+ if (effects.length === 0) return null;
1591
+ const layers = effects.map(effect => `${effect.x}px ${effect.y}px ${effect.blur}px ${effect.spread}px ${effect.color}`);
1592
+ return {
1593
+ boxShadow: layers.join(', ')
1594
+ };
1595
+ }
1596
+ function buildButtonNativeShadowStyle(effect) {
1597
+ const rgba = parseRgbaColor(effect.color);
1598
+ return {
1599
+ // Android elevation requires a drawable host shape; keep it effectively invisible.
1600
+ backgroundColor: 'rgba(255,255,255,0.02)',
1601
+ shadowColor: rgba ? `rgba(${rgba.r},${rgba.g},${rgba.b},1)` : effect.color,
1602
+ shadowOffset: {
1603
+ width: effect.x,
1604
+ height: effect.y
1605
+ },
1606
+ shadowOpacity: rgba ? rgba.a : effect.opacity,
1607
+ shadowRadius: effect.blur / 2,
1608
+ elevation: Math.max(1, Math.round((effect.blur + Math.abs(effect.y)) / 3))
1609
+ };
1610
+ }
1611
+ function getButtonShadowLayerBounds(stroke, borderRadius) {
1612
+ const expandBy = !stroke || stroke.position === 'inside' ? 0 : stroke.position === 'center' ? stroke.width / 2 : stroke.width;
1613
+ return {
1614
+ top: -expandBy,
1615
+ right: -expandBy,
1616
+ bottom: -expandBy,
1617
+ left: -expandBy,
1618
+ borderRadius: borderRadius + expandBy
1619
+ };
1620
+ }
1621
+ function renderButtonStrokeOverlay(stroke, borderRadius) {
1622
+ if (!stroke || stroke.position === 'center') {
1623
+ return null;
1624
+ }
1625
+ if (stroke.position === 'inside') {
1626
+ return /*#__PURE__*/_jsx(View, {
1627
+ pointerEvents: "none",
1628
+ style: {
1629
+ position: 'absolute',
1630
+ top: stroke.width / 2,
1631
+ left: stroke.width / 2,
1632
+ right: stroke.width / 2,
1633
+ bottom: stroke.width / 2,
1634
+ borderRadius: Math.max(0, borderRadius - stroke.width / 2),
1635
+ borderWidth: stroke.width,
1636
+ borderColor: stroke.color
1637
+ }
1638
+ });
1639
+ }
1640
+ return /*#__PURE__*/_jsx(View, {
1641
+ pointerEvents: "none",
1642
+ style: {
1643
+ position: 'absolute',
1644
+ top: -stroke.width,
1645
+ left: -stroke.width,
1646
+ right: -stroke.width,
1647
+ bottom: -stroke.width,
1648
+ borderRadius: borderRadius + stroke.width,
1649
+ borderWidth: stroke.width,
1650
+ borderColor: stroke.color
1651
+ }
1652
+ });
1653
+ }
961
1654
  function alignmentToGradient(value) {
962
1655
  switch (value) {
963
1656
  case 'topCenter':
@@ -1009,10 +1702,23 @@ function alignmentToGradient(value) {
1009
1702
  }
1010
1703
  }
1011
1704
  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;
1705
+ const rgba = parseRgbaColor(color);
1706
+ if (!rgba) return color;
1707
+ return `rgba(${rgba.r},${rgba.g},${rgba.b},${clamp01(rgba.a * opacity)})`;
1708
+ }
1709
+ function parseRgbaColor(color) {
1710
+ const match = color.match(/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i);
1711
+ if (!match) return null;
1712
+ return {
1713
+ r: Math.round(Number(match[1])),
1714
+ g: Math.round(Number(match[2])),
1715
+ b: Math.round(Number(match[3])),
1716
+ a: match[4] === undefined ? 1 : clamp01(Number(match[4]))
1717
+ };
1718
+ }
1719
+ function clamp01(value) {
1720
+ if (!Number.isFinite(value)) return 1;
1721
+ return Math.max(0, Math.min(1, value));
1016
1722
  }
1017
1723
  function normalizeDimension(value) {
1018
1724
  if (value === Number.POSITIVE_INFINITY) {
@@ -1140,6 +1846,44 @@ function GradientText({
1140
1846
  })
1141
1847
  });
1142
1848
  }
1849
+ function resolveTextInputBackgroundColor(properties) {
1850
+ const canonical = properties.backgroundColor;
1851
+ if (typeof canonical === 'string' && canonical.trim().length > 0) {
1852
+ return parseColor(canonical);
1853
+ }
1854
+ const legacyBackground = properties.background;
1855
+ if (legacyBackground && typeof legacyBackground === 'object' && !Array.isArray(legacyBackground)) {
1856
+ const type = String(legacyBackground.type ?? '').toLowerCase();
1857
+ if (type === 'color' && typeof legacyBackground.color === 'string') {
1858
+ return parseColor(legacyBackground.color);
1859
+ }
1860
+ if (type === 'gradient' && Array.isArray(legacyBackground.colors)) {
1861
+ const firstGradientColor = legacyBackground.colors.find(value => typeof value === 'string' && value.trim().length > 0);
1862
+ if (typeof firstGradientColor === 'string') {
1863
+ return parseColor(firstGradientColor);
1864
+ }
1865
+ }
1866
+ } else if (typeof legacyBackground === 'string' && legacyBackground.trim().length > 0) {
1867
+ return parseColor(legacyBackground);
1868
+ }
1869
+ return parseColor('0xFFF0F0F0');
1870
+ }
1871
+ function normalizeTextInputStroke(properties) {
1872
+ const parsedWidth = Number(properties.strokeWidth ?? 0);
1873
+ const width = Number.isFinite(parsedWidth) && parsedWidth > 0 ? parsedWidth : 0;
1874
+ const enabled = typeof properties.strokeEnabled === 'boolean' ? properties.strokeEnabled : width > 0;
1875
+ const normalizedWidth = enabled ? width : 0;
1876
+ const color = normalizedWidth > 0 ? parseColor(properties.strokeColor ?? '#000000') : 'transparent';
1877
+ const parsedRadius = Number(properties.strokeRadius ?? properties.borderRadius ?? 8);
1878
+ const radius = Number.isFinite(parsedRadius) ? Math.max(0, parsedRadius) : 8;
1879
+ const style = properties.strokeStyle === 'dashed' || properties.strokeStyle === 'dotted' ? properties.strokeStyle : 'solid';
1880
+ return {
1881
+ width: normalizedWidth,
1882
+ color,
1883
+ radius,
1884
+ style
1885
+ };
1886
+ }
1143
1887
  function DynamicInput({
1144
1888
  initialValue,
1145
1889
  properties,
@@ -1163,8 +1907,10 @@ function DynamicInput({
1163
1907
  ...properties,
1164
1908
  height: null
1165
1909
  }, 'textColor');
1166
- const backgroundColor = parseColor(properties.backgroundColor ?? '0xFFF0F0F0');
1167
- const borderRadius = Number(properties.borderRadius ?? 8);
1910
+ const backgroundColor = resolveTextInputBackgroundColor(properties);
1911
+ const stroke = normalizeTextInputStroke(properties);
1912
+ const parsedPadding = Number(properties.padding ?? 12);
1913
+ const padding = Number.isFinite(parsedPadding) ? Math.max(0, parsedPadding) : 12;
1168
1914
  return /*#__PURE__*/_jsxs(View, {
1169
1915
  children: [label ? /*#__PURE__*/_jsx(Text, {
1170
1916
  style: labelStyle,
@@ -1172,9 +1918,11 @@ function DynamicInput({
1172
1918
  }) : null, /*#__PURE__*/_jsx(View, {
1173
1919
  style: {
1174
1920
  backgroundColor,
1175
- borderRadius,
1176
- paddingHorizontal: 16,
1177
- paddingVertical: 10
1921
+ borderRadius: stroke.radius,
1922
+ borderStyle: stroke.width > 0 ? stroke.style : undefined,
1923
+ borderWidth: stroke.width,
1924
+ borderColor: stroke.color,
1925
+ padding
1178
1926
  },
1179
1927
  children: mask ? /*#__PURE__*/_jsx(MaskInput, {
1180
1928
  value: value,
@@ -1313,6 +2061,7 @@ function SelectionList({
1313
2061
  if (layout === 'row') {
1314
2062
  return /*#__PURE__*/_jsx(View, {
1315
2063
  style: {
2064
+ width: '100%',
1316
2065
  flexDirection: 'row',
1317
2066
  flexWrap: 'wrap'
1318
2067
  },
@@ -1330,6 +2079,7 @@ function SelectionList({
1330
2079
  const aspectRatio = Number(properties.gridAspectRatio ?? 1);
1331
2080
  return /*#__PURE__*/_jsx(View, {
1332
2081
  style: {
2082
+ width: '100%',
1333
2083
  flexDirection: 'row',
1334
2084
  flexWrap: 'wrap'
1335
2085
  },
@@ -1345,8 +2095,12 @@ function SelectionList({
1345
2095
  });
1346
2096
  }
1347
2097
  return /*#__PURE__*/_jsx(View, {
2098
+ style: {
2099
+ width: '100%'
2100
+ },
1348
2101
  children: options.map((option, index) => /*#__PURE__*/_jsx(View, {
1349
2102
  style: {
2103
+ width: '100%',
1350
2104
  marginBottom: spacing
1351
2105
  },
1352
2106
  children: renderItem(option, index)
@@ -1363,6 +2117,9 @@ function WheelPicker({
1363
2117
  onAction,
1364
2118
  buildWidget: _buildWidget
1365
2119
  }) {
2120
+ const triggerLightHaptic = React.useCallback(() => {
2121
+ Vibration.vibrate(8);
2122
+ }, []);
1366
2123
  const multiSelect = properties.multiSelect === true;
1367
2124
  const [selectedValues, setSelectedValues] = React.useState(() => {
1368
2125
  if (initialValue === null || initialValue === undefined) return [];
@@ -1417,6 +2174,18 @@ function WheelPicker({
1417
2174
  const visibleItems = requestedVisible % 2 === 0 ? requestedVisible + 1 : requestedVisible;
1418
2175
  const wheelRowStride = wheelItemHeight + Math.max(0, spacing);
1419
2176
  const wheelContainerHeight = wheelItemHeight * visibleItems;
2177
+ const hasProvidedInitialValue = React.useMemo(() => {
2178
+ if (initialValue === null || initialValue === undefined) return false;
2179
+ if (Array.isArray(initialValue)) return initialValue.length > 0;
2180
+ if (typeof initialValue === 'string') return initialValue.trim().length > 0;
2181
+ return true;
2182
+ }, [initialValue]);
2183
+ const resolveStartItemIndex = React.useCallback(() => {
2184
+ const raw = properties.startItemIndex ?? properties.start_index ?? properties.startIndex ?? 1;
2185
+ const parsed = Number(raw);
2186
+ if (!Number.isFinite(parsed)) return 1;
2187
+ return Math.max(1, Math.floor(parsed));
2188
+ }, [properties.startIndex, properties.startItemIndex, properties.start_index]);
1420
2189
  const clampWheelIndex = React.useCallback(index => Math.max(0, Math.min(index, Math.max(0, resolvedOptions.length - 1))), [resolvedOptions.length]);
1421
2190
  const getWheelSelectedIndex = React.useCallback(() => {
1422
2191
  if (resolvedOptions.length === 0) return 0;
@@ -1439,6 +2208,7 @@ function WheelPicker({
1439
2208
  });
1440
2209
  if (lastCommittedWheelValue.current !== value) {
1441
2210
  lastCommittedWheelValue.current = value;
2211
+ triggerLightHaptic();
1442
2212
  onChanged(value);
1443
2213
  if (properties.autoGoNext === true) {
1444
2214
  requestAnimationFrame(() => onAction('next', {
@@ -1446,7 +2216,7 @@ function WheelPicker({
1446
2216
  }));
1447
2217
  }
1448
2218
  }
1449
- }, [clampWheelIndex, onAction, onChanged, properties.autoGoNext, resolvedOptions]);
2219
+ }, [clampWheelIndex, onAction, onChanged, properties.autoGoNext, resolvedOptions, triggerLightHaptic]);
1450
2220
  React.useEffect(() => {
1451
2221
  if (layout !== 'wheel' || resolvedOptions.length === 0) return;
1452
2222
  const selectedIndex = getWheelSelectedIndex();
@@ -1459,6 +2229,18 @@ function WheelPicker({
1459
2229
  wheelScrollY.setValue(offsetY);
1460
2230
  });
1461
2231
  }, [getWheelSelectedIndex, layout, resolvedOptions.length, wheelRowStride, wheelScrollY]);
2232
+ React.useEffect(() => {
2233
+ if (layout !== 'wheel') return;
2234
+ if (hasProvidedInitialValue) return;
2235
+ if (selectedValues.length > 0) return;
2236
+ if (resolvedOptions.length === 0) return;
2237
+ const oneBasedIndex = resolveStartItemIndex();
2238
+ const targetIndex = Math.max(0, Math.min(resolvedOptions.length - 1, oneBasedIndex - 1));
2239
+ const value = String(resolvedOptions[targetIndex]?.value ?? '');
2240
+ if (!value) return;
2241
+ setSelectedValues([value]);
2242
+ onChanged(value);
2243
+ }, [hasProvidedInitialValue, layout, onChanged, resolveStartItemIndex, resolvedOptions, selectedValues.length]);
1462
2244
  const renderItem = (option, index) => {
1463
2245
  const value = String(option.value ?? '');
1464
2246
  const label = String(option.text ?? value);
@@ -1538,6 +2320,8 @@ function WheelPicker({
1538
2320
  const baseUnselectedStyle = properties.unselectedStyle ?? properties.selectedStyle ?? {};
1539
2321
  const overlayBorderColor = parseColor(properties.wheelCenterBorderColor ?? properties.wheelSelectedBorderColor ?? baseUnselectedStyle.borderColor ?? '#D6D6DC');
1540
2322
  const overlayBackgroundColor = parseColor(properties.wheelCenterBackgroundColor ?? '#24FFFFFF');
2323
+ const wheelEdgeFadeOpacity = Math.max(0, Math.min(1, Number(properties.wheelEdgeFadeOpacity ?? 0)));
2324
+ const wheelEdgeFadeColor = parseColor(properties.wheelEdgeFadeColor ?? '#FFFFFF');
1541
2325
  const handleWheelMomentumEnd = event => {
1542
2326
  const offsetY = Number(event?.nativeEvent?.contentOffset?.y ?? 0);
1543
2327
  const settledIndex = clampWheelIndex(Math.round(offsetY / wheelRowStride));
@@ -1636,9 +2420,9 @@ function WheelPicker({
1636
2420
  borderColor: overlayBorderColor,
1637
2421
  backgroundColor: overlayBackgroundColor
1638
2422
  }
1639
- }), /*#__PURE__*/_jsx(LinearGradient, {
2423
+ }), wheelEdgeFadeOpacity > 0 && centerPadding > 0 ? /*#__PURE__*/_jsx(LinearGradient, {
1640
2424
  pointerEvents: "none",
1641
- colors: ['rgba(255,255,255,0.92)', 'rgba(255,255,255,0)'],
2425
+ colors: [withOpacity(wheelEdgeFadeColor, wheelEdgeFadeOpacity), withOpacity(wheelEdgeFadeColor, 0)],
1642
2426
  style: {
1643
2427
  position: 'absolute',
1644
2428
  top: 0,
@@ -1646,9 +2430,9 @@ function WheelPicker({
1646
2430
  right: 0,
1647
2431
  height: centerPadding
1648
2432
  }
1649
- }), /*#__PURE__*/_jsx(LinearGradient, {
2433
+ }) : null, wheelEdgeFadeOpacity > 0 && centerPadding > 0 ? /*#__PURE__*/_jsx(LinearGradient, {
1650
2434
  pointerEvents: "none",
1651
- colors: ['rgba(255,255,255,0)', 'rgba(255,255,255,0.92)'],
2435
+ colors: [withOpacity(wheelEdgeFadeColor, 0), withOpacity(wheelEdgeFadeColor, wheelEdgeFadeOpacity)],
1652
2436
  style: {
1653
2437
  position: 'absolute',
1654
2438
  bottom: 0,
@@ -1656,7 +2440,7 @@ function WheelPicker({
1656
2440
  right: 0,
1657
2441
  height: centerPadding
1658
2442
  }
1659
- })]
2443
+ }) : null]
1660
2444
  });
1661
2445
  }
1662
2446
  return /*#__PURE__*/_jsx(View, {
@@ -2085,16 +2869,33 @@ function RadarChart({
2085
2869
  const axisColor = parseColor(properties.axisColor ?? '0xFF9E9E9E');
2086
2870
  const axisStrokeWidth = Number(properties.axisStrokeWidth ?? 1);
2087
2871
  const axisLabelStyle = getTextStyle(properties.axisLabelStyle ?? {}, 'color', '0xFF424242');
2872
+ const resolvedAxisLabelFontSize = resolveRadarAxisLabelFontSize(axisLabelStyle.fontSize);
2873
+ const autoFitLabels = properties.autoFitLabels !== false;
2874
+ const resolvedLabelOffset = resolveRadarLabelOffset(properties.labelOffset, resolvedAxisLabelFontSize);
2088
2875
  const showLegend = properties.showLegend !== false;
2089
2876
  const legendPosition = (properties.legendPosition ?? 'bottom').toLowerCase();
2090
2877
  const legendSpacing = Number(properties.legendSpacing ?? 12);
2091
2878
  const legendStyle = getTextStyle(properties.legendStyle ?? {}, 'color', '0xFF212121');
2092
2879
  const shape = (properties.shape ?? 'polygon').toLowerCase();
2093
- const chartSize = Math.min(width, height);
2094
- const radius = Math.max(chartSize / 2 - Math.max(padding.left, padding.right, padding.top, padding.bottom), 8);
2880
+ const innerWidth = Math.max(1, width - padding.left - padding.right);
2881
+ const innerHeight = Math.max(1, height - padding.top - padding.bottom);
2882
+ const radius = calculateRadarChartRadius({
2883
+ width: innerWidth,
2884
+ height: innerHeight,
2885
+ padding: {
2886
+ left: 0,
2887
+ right: 0,
2888
+ top: 0,
2889
+ bottom: 0
2890
+ },
2891
+ axisLabels: axes.map(axis => axis.label),
2892
+ fontSize: resolvedAxisLabelFontSize,
2893
+ autoFitLabels,
2894
+ labelOffset: resolvedLabelOffset
2895
+ });
2095
2896
  const center = {
2096
- x: width / 2,
2097
- y: height / 2
2897
+ x: padding.left + innerWidth / 2,
2898
+ y: padding.top + innerHeight / 2
2098
2899
  };
2099
2900
  const axisAngle = Math.PI * 2 / axes.length;
2100
2901
  const gridPolygons = Array.from({
@@ -2115,7 +2916,8 @@ function RadarChart({
2115
2916
  const points = dataset.values.map((value, index) => {
2116
2917
  const axis = normalizedAxes[index];
2117
2918
  const maxValue = axis?.maxValue ?? 0;
2118
- const ratio = (maxValue > 0 ? value / maxValue : 0) * lineProgress;
2919
+ const normalizedRatio = maxValue > 0 ? value / maxValue : 0;
2920
+ const ratio = Math.max(0, Math.min(1, normalizedRatio)) * Math.max(0, Math.min(1, lineProgress));
2119
2921
  const angle = index * axisAngle - Math.PI / 2;
2120
2922
  const x = center.x + radius * ratio * Math.cos(angle);
2121
2923
  const y = center.y + radius * ratio * Math.sin(angle);
@@ -2126,27 +2928,32 @@ function RadarChart({
2126
2928
  points
2127
2929
  };
2128
2930
  });
2931
+ const isVerticalLegend = legendPosition === 'left' || legendPosition === 'right';
2129
2932
  const legend = /*#__PURE__*/_jsx(View, {
2130
2933
  style: {
2131
- flexDirection: 'row',
2132
- flexWrap: 'wrap'
2934
+ flexDirection: isVerticalLegend ? 'column' : 'row',
2935
+ flexWrap: isVerticalLegend ? 'nowrap' : 'wrap',
2936
+ alignItems: isVerticalLegend ? 'flex-start' : 'center'
2133
2937
  },
2134
2938
  children: datasets.map((dataset, index) => /*#__PURE__*/_jsxs(View, {
2135
2939
  style: {
2136
2940
  flexDirection: 'row',
2137
2941
  alignItems: 'center',
2138
- marginRight: legendSpacing,
2139
- marginBottom: 6
2942
+ marginRight: isVerticalLegend ? 0 : legendSpacing,
2943
+ marginBottom: isVerticalLegend ? legendSpacing : 6
2140
2944
  },
2141
2945
  children: [/*#__PURE__*/_jsx(View, {
2142
2946
  style: {
2143
2947
  width: 12,
2144
2948
  height: 12,
2145
2949
  borderRadius: 6,
2146
- backgroundColor: dataset.borderColor,
2950
+ backgroundColor: dataset.fillColor ?? dataset.borderColor,
2951
+ borderWidth: 1,
2952
+ borderColor: dataset.borderColor,
2147
2953
  marginRight: 6
2148
2954
  }
2149
2955
  }), /*#__PURE__*/_jsx(Text, {
2956
+ numberOfLines: 1,
2150
2957
  style: legendStyle,
2151
2958
  children: dataset.label
2152
2959
  })]
@@ -2154,12 +2961,10 @@ function RadarChart({
2154
2961
  });
2155
2962
  const chart = /*#__PURE__*/_jsx(Animated.View, {
2156
2963
  style: {
2157
- width: normalizedWidth ?? width,
2964
+ width: isVerticalLegend ? width : normalizedWidth ?? '100%',
2965
+ maxWidth: '100%',
2966
+ flexShrink: 1,
2158
2967
  height,
2159
- paddingLeft: padding.left,
2160
- paddingRight: padding.right,
2161
- paddingTop: padding.top,
2162
- paddingBottom: padding.bottom,
2163
2968
  opacity: reveal
2164
2969
  },
2165
2970
  onLayout: event => {
@@ -2198,24 +3003,28 @@ function RadarChart({
2198
3003
  }, `axis-${index}`);
2199
3004
  }), axes.map((axis, index) => {
2200
3005
  const angle = index * axisAngle - Math.PI / 2;
2201
- const labelX = center.x + (radius + 12) * Math.cos(angle);
2202
- const labelY = center.y + (radius + 12) * Math.sin(angle);
3006
+ const labelX = center.x + (radius + resolvedLabelOffset) * Math.cos(angle);
3007
+ const labelY = center.y + (radius + resolvedLabelOffset) * Math.sin(angle);
2203
3008
  return /*#__PURE__*/_jsx(SvgText, {
2204
3009
  x: labelX,
2205
3010
  y: labelY,
2206
- fontSize: axisLabelStyle.fontSize ?? 12,
3011
+ fontSize: resolvedAxisLabelFontSize,
2207
3012
  fontWeight: axisLabelStyle.fontWeight,
2208
3013
  fill: axisLabelStyle.color,
2209
3014
  textAnchor: "middle",
2210
3015
  children: axis.label
2211
3016
  }, `label-${index}`);
2212
- }), datasetPolygons.map((entry, index) => /*#__PURE__*/_jsx(Polygon, {
2213
- points: entry.points.join(' '),
2214
- stroke: entry.dataset.borderColor,
2215
- strokeWidth: entry.dataset.borderWidth,
2216
- fill: entry.dataset.fillColor ?? 'transparent',
2217
- strokeDasharray: entry.dataset.borderStyle === 'dotted' || entry.dataset.borderStyle === 'dashed' ? entry.dataset.dashArray ?? '6 4' : undefined
2218
- }, `dataset-${index}`)), datasetPolygons.map((entry, datasetIndex) => entry.dataset.showPoints ? entry.points.map((point, pointIndex) => {
3017
+ }), datasetPolygons.map((entry, index) => {
3018
+ const strokePattern = resolveRadarStrokePattern(entry.dataset);
3019
+ return /*#__PURE__*/_jsx(Polygon, {
3020
+ points: entry.points.join(' '),
3021
+ stroke: entry.dataset.borderColor,
3022
+ strokeWidth: entry.dataset.borderWidth,
3023
+ fill: entry.dataset.fillColor ?? 'transparent',
3024
+ strokeDasharray: strokePattern.strokeDasharray,
3025
+ strokeLinecap: strokePattern.strokeLinecap
3026
+ }, `dataset-${index}`);
3027
+ }), datasetPolygons.map((entry, datasetIndex) => entry.dataset.showPoints ? entry.points.map((point, pointIndex) => {
2219
3028
  const [x, y] = point.split(',').map(val => Number(val));
2220
3029
  return /*#__PURE__*/_jsx(Circle, {
2221
3030
  cx: x,
@@ -2227,23 +3036,23 @@ function RadarChart({
2227
3036
  })
2228
3037
  })
2229
3038
  });
2230
- const composed = /*#__PURE__*/_jsxs(View, {
2231
- style: {
2232
- backgroundColor
2233
- },
2234
- children: [chart, showLegend && legendPosition === 'bottom' ? /*#__PURE__*/_jsx(View, {
3039
+ if (!showLegend) {
3040
+ return /*#__PURE__*/_jsx(View, {
2235
3041
  style: {
2236
- marginTop: 12
3042
+ backgroundColor
2237
3043
  },
2238
- children: legend
2239
- }) : null]
2240
- });
2241
- if (!showLegend || legendPosition === 'bottom') return composed;
3044
+ children: chart
3045
+ });
3046
+ }
2242
3047
  if (legendPosition === 'top') {
2243
3048
  return /*#__PURE__*/_jsxs(View, {
3049
+ style: {
3050
+ backgroundColor,
3051
+ alignItems: 'center'
3052
+ },
2244
3053
  children: [legend, /*#__PURE__*/_jsx(View, {
2245
3054
  style: {
2246
- height: 12
3055
+ height: legendSpacing
2247
3056
  }
2248
3057
  }), chart]
2249
3058
  });
@@ -2251,11 +3060,13 @@ function RadarChart({
2251
3060
  if (legendPosition === 'left') {
2252
3061
  return /*#__PURE__*/_jsxs(View, {
2253
3062
  style: {
2254
- flexDirection: 'row'
3063
+ backgroundColor,
3064
+ flexDirection: 'row',
3065
+ alignItems: 'center'
2255
3066
  },
2256
3067
  children: [legend, /*#__PURE__*/_jsx(View, {
2257
3068
  style: {
2258
- width: 12
3069
+ width: legendSpacing
2259
3070
  }
2260
3071
  }), chart]
2261
3072
  });
@@ -2263,16 +3074,167 @@ function RadarChart({
2263
3074
  if (legendPosition === 'right') {
2264
3075
  return /*#__PURE__*/_jsxs(View, {
2265
3076
  style: {
2266
- flexDirection: 'row'
3077
+ backgroundColor,
3078
+ flexDirection: 'row',
3079
+ alignItems: 'center'
2267
3080
  },
2268
3081
  children: [chart, /*#__PURE__*/_jsx(View, {
2269
3082
  style: {
2270
- width: 12
3083
+ width: legendSpacing
2271
3084
  }
2272
3085
  }), legend]
2273
3086
  });
2274
3087
  }
2275
- return composed;
3088
+ return /*#__PURE__*/_jsxs(View, {
3089
+ style: {
3090
+ backgroundColor,
3091
+ alignItems: 'center'
3092
+ },
3093
+ children: [chart, /*#__PURE__*/_jsx(View, {
3094
+ style: {
3095
+ height: legendSpacing
3096
+ }
3097
+ }), legend]
3098
+ });
3099
+ }
3100
+ const RADAR_DASH_DEFAULT_LENGTH = 8;
3101
+ const RADAR_DASH_GAP_FACTOR = 0.6;
3102
+ const RADAR_DOT_DASH_LENGTH = 0.001;
3103
+ const RADAR_DOT_GAP_FACTOR = 2.4;
3104
+ const RADAR_DOT_MIN_GAP = 3;
3105
+ const radarDashConflictWarnings = new Set();
3106
+ function isDevelopmentEnvironment() {
3107
+ return process.env.NODE_ENV !== 'production';
3108
+ }
3109
+ function warnRadarDashConflictOnce(label) {
3110
+ if (!isDevelopmentEnvironment()) return;
3111
+ if (radarDashConflictWarnings.has(label)) return;
3112
+ radarDashConflictWarnings.add(label);
3113
+ // Keep API backward-compatible but deterministic when both flags are passed.
3114
+ console.warn(`[RadarChart] Dataset "${label}" received both dotted=true and dashed=true. dashed takes precedence.`);
3115
+ }
3116
+ function resolveRadarBorderStyle(dataset, fallbackLabel) {
3117
+ const dotted = dataset.dotted === true;
3118
+ const dashed = dataset.dashed === true;
3119
+ if (dotted && dashed) {
3120
+ warnRadarDashConflictOnce(fallbackLabel);
3121
+ return 'dashed';
3122
+ }
3123
+ if (dashed) return 'dashed';
3124
+ if (dotted) return 'dotted';
3125
+ const borderStyle = (dataset.borderStyle ?? 'solid').toString().toLowerCase();
3126
+ if (borderStyle === 'dotted' || borderStyle === 'dashed') {
3127
+ return borderStyle;
3128
+ }
3129
+ return 'solid';
3130
+ }
3131
+ function resolveRadarStrokePattern(dataset) {
3132
+ if (dataset.borderStyle === 'dotted') {
3133
+ // Tiny dash + round caps renders circular dots more reliably than short dashes.
3134
+ const gap = Math.max(RADAR_DOT_MIN_GAP, dataset.borderWidth * RADAR_DOT_GAP_FACTOR);
3135
+ return {
3136
+ strokeDasharray: `${RADAR_DOT_DASH_LENGTH} ${gap}`,
3137
+ strokeLinecap: 'round'
3138
+ };
3139
+ }
3140
+ if (dataset.borderStyle === 'dashed') {
3141
+ if (Array.isArray(dataset.dashArray) && dataset.dashArray.length > 0) {
3142
+ const dash = Math.max(1, dataset.dashArray[0] ?? RADAR_DASH_DEFAULT_LENGTH);
3143
+ const gap = Math.max(1, dataset.dashArray[1] ?? dash * RADAR_DASH_GAP_FACTOR);
3144
+ return {
3145
+ strokeDasharray: `${dash} ${gap}`,
3146
+ strokeLinecap: 'butt'
3147
+ };
3148
+ }
3149
+ const dashLength = typeof dataset.dashLength === 'number' && Number.isFinite(dataset.dashLength) && dataset.dashLength > 0 ? dataset.dashLength : RADAR_DASH_DEFAULT_LENGTH;
3150
+ const gap = Math.max(2, dashLength * RADAR_DASH_GAP_FACTOR);
3151
+ return {
3152
+ strokeDasharray: `${dashLength} ${gap}`,
3153
+ strokeLinecap: 'butt'
3154
+ };
3155
+ }
3156
+ return {};
3157
+ }
3158
+ export const RADAR_LABEL_ESTIMATION = {
3159
+ minRadius: 10,
3160
+ safeEdge: 4,
3161
+ minLabelWidthFactor: 1.2,
3162
+ avgCharWidthFactor: 0.58,
3163
+ lineHeightFactor: 1.2,
3164
+ defaultFontSize: 14,
3165
+ minLabelOffset: 20,
3166
+ labelOffsetFactor: 1.5
3167
+ };
3168
+ export function resolveRadarAxisLabelFontSize(value) {
3169
+ const parsed = Number(value);
3170
+ if (!Number.isFinite(parsed) || parsed <= 0) {
3171
+ return RADAR_LABEL_ESTIMATION.defaultFontSize;
3172
+ }
3173
+ return parsed;
3174
+ }
3175
+ export function resolveRadarLabelOffset(value, fontSize) {
3176
+ const parsed = Number(value);
3177
+ if (Number.isFinite(parsed) && parsed > 0) {
3178
+ return parsed;
3179
+ }
3180
+ return Math.max(RADAR_LABEL_ESTIMATION.minLabelOffset, fontSize * RADAR_LABEL_ESTIMATION.labelOffsetFactor);
3181
+ }
3182
+ export function estimateRadarLabelSize(label, fontSize) {
3183
+ const safeLabel = `${label ?? ''}`;
3184
+ const width = Math.max(safeLabel.length * fontSize * RADAR_LABEL_ESTIMATION.avgCharWidthFactor, fontSize * RADAR_LABEL_ESTIMATION.minLabelWidthFactor);
3185
+ const height = fontSize * RADAR_LABEL_ESTIMATION.lineHeightFactor;
3186
+ return {
3187
+ width,
3188
+ height
3189
+ };
3190
+ }
3191
+ export function calculateRadarChartRadius({
3192
+ width,
3193
+ height,
3194
+ padding,
3195
+ axisLabels,
3196
+ fontSize,
3197
+ autoFitLabels = true,
3198
+ labelOffset
3199
+ }) {
3200
+ const center = {
3201
+ x: width / 2,
3202
+ y: height / 2
3203
+ };
3204
+ const baseRadius = Math.max(Math.min(width, height) / 2 - Math.max(padding.left, padding.right, padding.top, padding.bottom), RADAR_LABEL_ESTIMATION.minRadius);
3205
+ if (!autoFitLabels || axisLabels.length === 0) {
3206
+ return baseRadius;
3207
+ }
3208
+ const angleStep = Math.PI * 2 / axisLabels.length;
3209
+ let maxAllowedRadius = baseRadius;
3210
+ for (let index = 0; index < axisLabels.length; index += 1) {
3211
+ const {
3212
+ width: labelWidth,
3213
+ height: labelHeight
3214
+ } = estimateRadarLabelSize(axisLabels[index] ?? '', fontSize);
3215
+ const halfWidth = labelWidth / 2;
3216
+ const halfHeight = labelHeight / 2;
3217
+ const angle = index * angleStep - Math.PI / 2;
3218
+ const dx = Math.cos(angle);
3219
+ const dy = Math.sin(angle);
3220
+
3221
+ // Solve per-axis line constraints so estimated label bounds stay inside.
3222
+ if (dx > 0.0001) {
3223
+ maxAllowedRadius = Math.min(maxAllowedRadius, (width - RADAR_LABEL_ESTIMATION.safeEdge - halfWidth - center.x) / dx - labelOffset);
3224
+ } else if (dx < -0.0001) {
3225
+ maxAllowedRadius = Math.min(maxAllowedRadius, (RADAR_LABEL_ESTIMATION.safeEdge + halfWidth - center.x) / dx - labelOffset);
3226
+ }
3227
+ if (dy > 0.0001) {
3228
+ maxAllowedRadius = Math.min(maxAllowedRadius, (height - RADAR_LABEL_ESTIMATION.safeEdge - halfHeight - center.y) / dy - labelOffset);
3229
+ } else if (dy < -0.0001) {
3230
+ maxAllowedRadius = Math.min(maxAllowedRadius, (RADAR_LABEL_ESTIMATION.safeEdge + halfHeight - center.y) / dy - labelOffset);
3231
+ }
3232
+ }
3233
+ const boundedRadius = Math.min(maxAllowedRadius, baseRadius);
3234
+ if (!Number.isFinite(boundedRadius)) {
3235
+ return baseRadius;
3236
+ }
3237
+ return Math.max(boundedRadius, RADAR_LABEL_ESTIMATION.minRadius);
2276
3238
  }
2277
3239
  export function parseRadarAxes(rawAxes) {
2278
3240
  if (!Array.isArray(rawAxes)) return [];
@@ -2282,9 +3244,10 @@ export function parseRadarAxes(rawAxes) {
2282
3244
  const label = axis.label ?? '';
2283
3245
  if (!label) return;
2284
3246
  const maxValue = resolveNumericValue(axis.maxValue, {}) ?? 100;
3247
+ const normalizedMaxValue = Number.isFinite(maxValue) && maxValue > 0 ? maxValue : 100;
2285
3248
  axes.push({
2286
3249
  label,
2287
- maxValue: maxValue > 0 ? maxValue : 100
3250
+ maxValue: normalizedMaxValue
2288
3251
  });
2289
3252
  });
2290
3253
  return axes;
@@ -2292,24 +3255,27 @@ export function parseRadarAxes(rawAxes) {
2292
3255
  export function parseRadarDatasets(raw, axisLength, formData) {
2293
3256
  if (!Array.isArray(raw) || axisLength === 0) return [];
2294
3257
  const datasets = [];
2295
- raw.forEach(dataset => {
3258
+ raw.forEach((dataset, datasetIndex) => {
2296
3259
  if (!dataset || typeof dataset !== 'object') return;
2297
3260
  const valuesRaw = Array.isArray(dataset.data) ? dataset.data : [];
2298
3261
  const values = [];
2299
3262
  for (let i = 0; i < axisLength; i += 1) {
2300
3263
  const rawValue = i < valuesRaw.length ? valuesRaw[i] : null;
2301
- const resolved = resolveNumericValue(rawValue, formData) ?? 0;
2302
- values.push(resolved);
3264
+ const resolved = resolveNumericValue(rawValue, formData);
3265
+ values.push(typeof resolved === 'number' && Number.isFinite(resolved) ? resolved : 0);
2303
3266
  }
2304
3267
  const label = dataset.label ?? `Dataset ${datasets.length + 1}`;
2305
3268
  const borderColor = parseColor(dataset.borderColor ?? '0xFF2196F3');
2306
3269
  const fillColor = dataset.fillColor ? parseColor(dataset.fillColor) : undefined;
2307
3270
  const pointColor = parseColor(dataset.pointColor ?? dataset.borderColor ?? '0xFF2196F3');
2308
- const borderWidth = Number(dataset.borderWidth ?? 2);
2309
- const pointRadius = Number(dataset.pointRadius ?? 4);
2310
- const borderStyle = (dataset.borderStyle ?? 'solid').toString().toLowerCase();
2311
- const normalizedStyle = borderStyle === 'dotted' || borderStyle === 'dashed' ? borderStyle : 'solid';
2312
- const dashArray = Array.isArray(dataset.dashArray) ? dataset.dashArray.map(val => Number(val)) : undefined;
3271
+ const rawBorderWidth = Number(dataset.borderWidth ?? 2);
3272
+ const borderWidth = Number.isFinite(rawBorderWidth) && rawBorderWidth > 0 ? rawBorderWidth : 2;
3273
+ const rawPointRadius = Number(dataset.pointRadius ?? 4);
3274
+ const pointRadius = Number.isFinite(rawPointRadius) && rawPointRadius >= 0 ? rawPointRadius : 4;
3275
+ const normalizedStyle = resolveRadarBorderStyle(dataset, label ?? `Dataset ${datasetIndex + 1}`);
3276
+ const dashArray = Array.isArray(dataset.dashArray) ? dataset.dashArray.map(val => Number(val)).filter(val => Number.isFinite(val) && val > 0) : undefined;
3277
+ const rawDashLength = Number(dataset.dashLength);
3278
+ const dashLength = Number.isFinite(rawDashLength) && rawDashLength > 0 ? rawDashLength : undefined;
2313
3279
  const showPoints = dataset.showPoints !== false;
2314
3280
  datasets.push({
2315
3281
  label,
@@ -2318,6 +3284,7 @@ export function parseRadarDatasets(raw, axisLength, formData) {
2318
3284
  borderWidth,
2319
3285
  borderStyle: normalizedStyle,
2320
3286
  dashArray,
3287
+ dashLength,
2321
3288
  showPoints,
2322
3289
  pointRadius,
2323
3290
  pointColor,
@@ -2331,7 +3298,9 @@ export function normalizeAxisMaximums(axes, datasets) {
2331
3298
  let maxValue = axis.maxValue;
2332
3299
  datasets.forEach(dataset => {
2333
3300
  const value = dataset.values[index];
2334
- if (value !== undefined && value > maxValue) maxValue = value;
3301
+ if (value !== undefined && Number.isFinite(value) && value > maxValue) {
3302
+ maxValue = value;
3303
+ }
2335
3304
  });
2336
3305
  return {
2337
3306
  ...axis,
@@ -2340,12 +3309,15 @@ export function normalizeAxisMaximums(axes, datasets) {
2340
3309
  });
2341
3310
  }
2342
3311
  function resolveRadarWidth(parsedWidth, measuredWidth) {
3312
+ if (measuredWidth > 0) {
3313
+ if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
3314
+ return Math.max(1, Math.min(parsedWidth, measuredWidth));
3315
+ }
3316
+ return Math.max(1, measuredWidth);
3317
+ }
2343
3318
  if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
2344
3319
  return parsedWidth;
2345
3320
  }
2346
- if (measuredWidth > 0) {
2347
- return measuredWidth;
2348
- }
2349
3321
  return 300;
2350
3322
  }
2351
3323
  function radarPlaceholder(message) {