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