flowboard-react 0.4.3 → 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.
- 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 +1092 -148
- 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 +1609 -197
- package/src/types/flowboard.ts +1 -0
- package/src/utils/flowboardUtils.ts +4 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Animated,
|
|
4
|
+
Platform,
|
|
4
5
|
Pressable,
|
|
5
6
|
ScrollView,
|
|
6
7
|
StyleSheet,
|
|
@@ -10,8 +11,9 @@ import {
|
|
|
10
11
|
Image,
|
|
11
12
|
Vibration,
|
|
12
13
|
type TextStyle,
|
|
14
|
+
type ViewStyle,
|
|
13
15
|
} from 'react-native';
|
|
14
|
-
import {
|
|
16
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
15
17
|
import MaskedView from '@react-native-masked-view/masked-view';
|
|
16
18
|
import LinearGradient from 'react-native-linear-gradient';
|
|
17
19
|
import LottieView from 'lottie-react-native';
|
|
@@ -53,7 +55,7 @@ const styles = StyleSheet.create({
|
|
|
53
55
|
whiteBg: {
|
|
54
56
|
backgroundColor: '#ffffff',
|
|
55
57
|
},
|
|
56
|
-
|
|
58
|
+
contentLayer: { flex: 1 },
|
|
57
59
|
progressWrapper: { width: '100%' },
|
|
58
60
|
});
|
|
59
61
|
|
|
@@ -67,6 +69,32 @@ type FlowboardRendererProps = {
|
|
|
67
69
|
totalScreens?: number;
|
|
68
70
|
};
|
|
69
71
|
|
|
72
|
+
type PageScrollAxis = 'vertical' | 'none';
|
|
73
|
+
|
|
74
|
+
type AxisBounds = {
|
|
75
|
+
widthBounded: boolean;
|
|
76
|
+
heightBounded: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function resolvePageScrollAxis(
|
|
80
|
+
screenData: Record<string, any>
|
|
81
|
+
): PageScrollAxis {
|
|
82
|
+
const raw = String(
|
|
83
|
+
screenData.pageScroll ??
|
|
84
|
+
screenData.scrollDirection ??
|
|
85
|
+
(screenData.scrollable === true ? 'vertical' : 'none')
|
|
86
|
+
).toLowerCase();
|
|
87
|
+
if (raw === 'none') return 'none';
|
|
88
|
+
return 'vertical';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getAxisBoundsForPageScroll(pageScroll: PageScrollAxis): AxisBounds {
|
|
92
|
+
if (pageScroll === 'vertical') {
|
|
93
|
+
return { widthBounded: true, heightBounded: false };
|
|
94
|
+
}
|
|
95
|
+
return { widthBounded: true, heightBounded: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
71
99
|
const {
|
|
72
100
|
screenData,
|
|
@@ -90,8 +118,12 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
90
118
|
const progressThickness = Number(screenData.progressThickness ?? 4);
|
|
91
119
|
const progressRadius = Number(screenData.progressRadius ?? 0);
|
|
92
120
|
const progressStyle = screenData.progressStyle ?? 'linear';
|
|
93
|
-
const
|
|
121
|
+
const pageScroll = resolvePageScrollAxis(screenData);
|
|
122
|
+
const pageAxisBounds = getAxisBoundsForPageScroll(pageScroll);
|
|
123
|
+
const scrollable = pageScroll !== 'none';
|
|
94
124
|
const safeArea = screenData.safeArea !== false;
|
|
125
|
+
const safeAreaInsets = useSafeAreaInsets();
|
|
126
|
+
const rootAxis: 'vertical' = 'vertical';
|
|
95
127
|
|
|
96
128
|
const padding = parseInsets(screenData.padding);
|
|
97
129
|
const contentPaddingStyle = insetsToStyle(padding);
|
|
@@ -102,6 +134,14 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
102
134
|
};
|
|
103
135
|
const rootCrossAxisAlignment =
|
|
104
136
|
screenData.crossAxisAlignment ?? screenData.crossAxis;
|
|
137
|
+
const safeAreaPaddingStyle = safeArea
|
|
138
|
+
? {
|
|
139
|
+
paddingTop: safeAreaInsets.top,
|
|
140
|
+
paddingRight: safeAreaInsets.right,
|
|
141
|
+
paddingBottom: safeAreaInsets.bottom,
|
|
142
|
+
paddingLeft: safeAreaInsets.left,
|
|
143
|
+
}
|
|
144
|
+
: null;
|
|
105
145
|
|
|
106
146
|
const content = (
|
|
107
147
|
<View style={{ flex: 1 }}>
|
|
@@ -120,6 +160,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
120
160
|
|
|
121
161
|
{scrollable ? (
|
|
122
162
|
<ScrollView
|
|
163
|
+
horizontal={false}
|
|
123
164
|
contentContainerStyle={{
|
|
124
165
|
...contentPaddingStyle,
|
|
125
166
|
paddingTop: showProgress ? 0 : padding.top,
|
|
@@ -128,8 +169,11 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
128
169
|
<View
|
|
129
170
|
style={{
|
|
130
171
|
flexGrow: 1,
|
|
172
|
+
flexDirection: 'column',
|
|
131
173
|
justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
|
|
132
174
|
alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
|
|
175
|
+
minHeight: 0,
|
|
176
|
+
minWidth: 0,
|
|
133
177
|
}}
|
|
134
178
|
>
|
|
135
179
|
{childrenData.map((child, index) =>
|
|
@@ -138,6 +182,9 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
138
182
|
formData,
|
|
139
183
|
onInputChange,
|
|
140
184
|
allowFlexExpansion: true,
|
|
185
|
+
parentFlexAxis: rootAxis,
|
|
186
|
+
parentMainAxisBounded: pageAxisBounds.heightBounded,
|
|
187
|
+
pageAxisBounds,
|
|
141
188
|
screenData,
|
|
142
189
|
enableFontAwesomeIcons,
|
|
143
190
|
key: `child-${index}`,
|
|
@@ -161,6 +208,9 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
161
208
|
formData,
|
|
162
209
|
onInputChange,
|
|
163
210
|
allowFlexExpansion: true,
|
|
211
|
+
parentFlexAxis: rootAxis,
|
|
212
|
+
parentMainAxisBounded: true,
|
|
213
|
+
pageAxisBounds,
|
|
164
214
|
screenData,
|
|
165
215
|
enableFontAwesomeIcons,
|
|
166
216
|
key: `child-${index}`,
|
|
@@ -174,17 +224,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
174
224
|
return (
|
|
175
225
|
<View style={styles.root}>
|
|
176
226
|
{renderBackground(backgroundData, bgColorCode)}
|
|
177
|
-
{
|
|
178
|
-
<SafeAreaView
|
|
179
|
-
style={styles.safeArea}
|
|
180
|
-
mode="padding"
|
|
181
|
-
edges={['top', 'right', 'bottom', 'left']}
|
|
182
|
-
>
|
|
183
|
-
{content}
|
|
184
|
-
</SafeAreaView>
|
|
185
|
-
) : (
|
|
186
|
-
content
|
|
187
|
-
)}
|
|
227
|
+
<View style={[styles.contentLayer, safeAreaPaddingStyle]}>{content}</View>
|
|
188
228
|
</View>
|
|
189
229
|
);
|
|
190
230
|
}
|
|
@@ -357,6 +397,484 @@ function renderProgressBar(
|
|
|
357
397
|
);
|
|
358
398
|
}
|
|
359
399
|
|
|
400
|
+
const STACK_SPACE_DISTRIBUTIONS = new Set([
|
|
401
|
+
'spaceBetween',
|
|
402
|
+
'spaceAround',
|
|
403
|
+
'spaceEvenly',
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
function normalizeStackSpacingConfig(value: any): Record<string, number> {
|
|
407
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
408
|
+
return { x: value, y: value };
|
|
409
|
+
}
|
|
410
|
+
if (!value || typeof value !== 'object') {
|
|
411
|
+
return { x: 0, y: 0 };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const x = Number(value.x ?? value.horizontal);
|
|
415
|
+
const y = Number(value.y ?? value.vertical);
|
|
416
|
+
const top = Number(value.top);
|
|
417
|
+
const right = Number(value.right);
|
|
418
|
+
const bottom = Number(value.bottom);
|
|
419
|
+
const left = Number(value.left);
|
|
420
|
+
|
|
421
|
+
const normalized: Record<string, number> = {};
|
|
422
|
+
if (!Number.isNaN(x)) normalized.x = x;
|
|
423
|
+
if (!Number.isNaN(y)) normalized.y = y;
|
|
424
|
+
if (!Number.isNaN(top)) normalized.top = top;
|
|
425
|
+
if (!Number.isNaN(right)) normalized.right = right;
|
|
426
|
+
if (!Number.isNaN(bottom)) normalized.bottom = bottom;
|
|
427
|
+
if (!Number.isNaN(left)) normalized.left = left;
|
|
428
|
+
|
|
429
|
+
if (Object.keys(normalized).length === 0) {
|
|
430
|
+
return { x: 0, y: 0 };
|
|
431
|
+
}
|
|
432
|
+
return normalized;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function stackSpacingToInsets(value: any) {
|
|
436
|
+
const normalized = normalizeStackSpacingConfig(value);
|
|
437
|
+
return parseInsets({
|
|
438
|
+
horizontal: normalized.x,
|
|
439
|
+
vertical: normalized.y,
|
|
440
|
+
top: normalized.top,
|
|
441
|
+
right: normalized.right,
|
|
442
|
+
bottom: normalized.bottom,
|
|
443
|
+
left: normalized.left,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function isLegacyOverlayAlignment(value: unknown): boolean {
|
|
448
|
+
return [
|
|
449
|
+
'topLeft',
|
|
450
|
+
'topCenter',
|
|
451
|
+
'topRight',
|
|
452
|
+
'centerLeft',
|
|
453
|
+
'center',
|
|
454
|
+
'centerRight',
|
|
455
|
+
'bottomLeft',
|
|
456
|
+
'bottomCenter',
|
|
457
|
+
'bottomRight',
|
|
458
|
+
].includes(String(value ?? ''));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function normalizeStackAxis(type: string, props: Record<string, any>) {
|
|
462
|
+
const explicit = String(props.axis ?? '').toLowerCase();
|
|
463
|
+
if (
|
|
464
|
+
explicit === 'vertical' ||
|
|
465
|
+
explicit === 'horizontal' ||
|
|
466
|
+
explicit === 'overlay'
|
|
467
|
+
) {
|
|
468
|
+
return explicit as 'vertical' | 'horizontal' | 'overlay';
|
|
469
|
+
}
|
|
470
|
+
if (type === 'container') return 'overlay';
|
|
471
|
+
if (type === 'row') return 'horizontal';
|
|
472
|
+
if (type === 'column') return 'vertical';
|
|
473
|
+
if (type === 'layout') {
|
|
474
|
+
return props.direction === 'horizontal' ? 'horizontal' : 'vertical';
|
|
475
|
+
}
|
|
476
|
+
if (
|
|
477
|
+
type === 'stack' &&
|
|
478
|
+
(props.fit !== undefined || isLegacyOverlayAlignment(props.alignment))
|
|
479
|
+
) {
|
|
480
|
+
return 'overlay';
|
|
481
|
+
}
|
|
482
|
+
return 'vertical';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function normalizeStackDistribution(value: unknown) {
|
|
486
|
+
const normalized = String(value ?? '').trim();
|
|
487
|
+
if (
|
|
488
|
+
normalized === 'center' ||
|
|
489
|
+
normalized === 'end' ||
|
|
490
|
+
normalized === 'spaceBetween' ||
|
|
491
|
+
normalized === 'spaceAround' ||
|
|
492
|
+
normalized === 'spaceEvenly'
|
|
493
|
+
) {
|
|
494
|
+
return normalized;
|
|
495
|
+
}
|
|
496
|
+
return 'start';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function normalizeStackCrossAlignment(value: unknown) {
|
|
500
|
+
const raw = String(value ?? '').toLowerCase();
|
|
501
|
+
if (raw === 'center' || raw.includes('center')) return 'center';
|
|
502
|
+
if (raw === 'end' || raw.endsWith('right') || raw.startsWith('bottom'))
|
|
503
|
+
return 'end';
|
|
504
|
+
return 'start';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function normalizeOverlayAlignment(value: unknown) {
|
|
508
|
+
const raw = String(value ?? '').trim();
|
|
509
|
+
switch (raw) {
|
|
510
|
+
case 'topStart':
|
|
511
|
+
case 'top':
|
|
512
|
+
case 'topEnd':
|
|
513
|
+
case 'start':
|
|
514
|
+
case 'center':
|
|
515
|
+
case 'end':
|
|
516
|
+
case 'bottomStart':
|
|
517
|
+
case 'bottom':
|
|
518
|
+
case 'bottomEnd':
|
|
519
|
+
return raw;
|
|
520
|
+
case 'topLeft':
|
|
521
|
+
return 'topStart';
|
|
522
|
+
case 'topCenter':
|
|
523
|
+
return 'top';
|
|
524
|
+
case 'topRight':
|
|
525
|
+
return 'topEnd';
|
|
526
|
+
case 'centerLeft':
|
|
527
|
+
return 'start';
|
|
528
|
+
case 'centerRight':
|
|
529
|
+
return 'end';
|
|
530
|
+
case 'bottomLeft':
|
|
531
|
+
return 'bottomStart';
|
|
532
|
+
case 'bottomCenter':
|
|
533
|
+
return 'bottom';
|
|
534
|
+
case 'bottomRight':
|
|
535
|
+
return 'bottomEnd';
|
|
536
|
+
default:
|
|
537
|
+
return 'center';
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isFillDimensionValue(value: unknown) {
|
|
542
|
+
if (typeof value === 'number') return value === Number.POSITIVE_INFINITY;
|
|
543
|
+
if (typeof value !== 'string') return false;
|
|
544
|
+
const normalized = value.trim().toLowerCase();
|
|
545
|
+
return (
|
|
546
|
+
normalized === 'infinity' ||
|
|
547
|
+
normalized === 'double.infinity' ||
|
|
548
|
+
normalized === '+infinity' ||
|
|
549
|
+
normalized === '100%'
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isFixedDimensionValue(value: unknown) {
|
|
554
|
+
if (isFillDimensionValue(value)) return false;
|
|
555
|
+
const parsed = Number(value);
|
|
556
|
+
return Number.isFinite(parsed);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function normalizeStackSize(
|
|
560
|
+
props: Record<string, any>,
|
|
561
|
+
axis: 'vertical' | 'horizontal' | 'overlay'
|
|
562
|
+
) {
|
|
563
|
+
const widthMode = String(props.size?.width ?? '').toLowerCase();
|
|
564
|
+
const heightMode = String(props.size?.height ?? '').toLowerCase();
|
|
565
|
+
|
|
566
|
+
const width =
|
|
567
|
+
widthMode === 'fill' || widthMode === 'fit' || widthMode === 'fixed'
|
|
568
|
+
? widthMode
|
|
569
|
+
: props.fit === 'expand' || props.width === 'infinity'
|
|
570
|
+
? 'fill'
|
|
571
|
+
: isFixedDimensionValue(props.width)
|
|
572
|
+
? 'fixed'
|
|
573
|
+
: axis === 'horizontal'
|
|
574
|
+
? 'fit'
|
|
575
|
+
: 'fill';
|
|
576
|
+
const height =
|
|
577
|
+
heightMode === 'fill' || heightMode === 'fit' || heightMode === 'fixed'
|
|
578
|
+
? heightMode
|
|
579
|
+
: props.fit === 'expand' || props.height === 'infinity'
|
|
580
|
+
? 'fill'
|
|
581
|
+
: isFixedDimensionValue(props.height)
|
|
582
|
+
? 'fixed'
|
|
583
|
+
: axis === 'vertical'
|
|
584
|
+
? 'fit'
|
|
585
|
+
: 'fill';
|
|
586
|
+
|
|
587
|
+
return { width, height };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function normalizeStackFill(props: Record<string, any>) {
|
|
591
|
+
if (props.fill && typeof props.fill === 'object') {
|
|
592
|
+
const next = { ...props.fill };
|
|
593
|
+
if (!next.type && next.color) next.type = 'solid';
|
|
594
|
+
return next;
|
|
595
|
+
}
|
|
596
|
+
if (props.background && typeof props.background === 'object') {
|
|
597
|
+
if (props.background.type === 'color') {
|
|
598
|
+
return { type: 'solid', color: props.background.color };
|
|
599
|
+
}
|
|
600
|
+
return { ...props.background };
|
|
601
|
+
}
|
|
602
|
+
if (props.backgroundColor) {
|
|
603
|
+
return { type: 'solid', color: props.backgroundColor };
|
|
604
|
+
}
|
|
605
|
+
return undefined;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function normalizeStackBorder(props: Record<string, any>) {
|
|
609
|
+
if (props.border && typeof props.border === 'object') {
|
|
610
|
+
return { ...props.border };
|
|
611
|
+
}
|
|
612
|
+
if (props.borderColor || props.borderWidth !== undefined) {
|
|
613
|
+
return {
|
|
614
|
+
width: Number(props.borderWidth ?? 1),
|
|
615
|
+
color: props.borderColor,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function normalizeStackShadow(props: Record<string, any>) {
|
|
622
|
+
if (props.shadow && typeof props.shadow === 'object') {
|
|
623
|
+
return { ...props.shadow };
|
|
624
|
+
}
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function spacingToInsets(value: any) {
|
|
629
|
+
const normalized = normalizeStackSpacingConfig(value);
|
|
630
|
+
const x = normalized.x ?? 0;
|
|
631
|
+
const y = normalized.y ?? 0;
|
|
632
|
+
return {
|
|
633
|
+
top: normalized.top ?? y,
|
|
634
|
+
right: normalized.right ?? x,
|
|
635
|
+
bottom: normalized.bottom ?? y,
|
|
636
|
+
left: normalized.left ?? x,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function insetsToSpacing(insets: {
|
|
641
|
+
top: number;
|
|
642
|
+
right: number;
|
|
643
|
+
bottom: number;
|
|
644
|
+
left: number;
|
|
645
|
+
}) {
|
|
646
|
+
if (insets.top === insets.bottom && insets.left === insets.right) {
|
|
647
|
+
return { x: insets.left, y: insets.top };
|
|
648
|
+
}
|
|
649
|
+
return insets;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function mergeStackProps(
|
|
653
|
+
parentProps: Record<string, any>,
|
|
654
|
+
childProps: Record<string, any>
|
|
655
|
+
) {
|
|
656
|
+
const parentAxis = String(parentProps.axis ?? 'vertical');
|
|
657
|
+
const childAxis = String(childProps.axis ?? 'vertical');
|
|
658
|
+
|
|
659
|
+
let axis = childAxis;
|
|
660
|
+
if (parentAxis !== childAxis) {
|
|
661
|
+
if (parentAxis === 'overlay' && childAxis !== 'overlay') axis = childAxis;
|
|
662
|
+
else if (childAxis === 'overlay' && parentAxis !== 'overlay')
|
|
663
|
+
axis = parentAxis;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const axisSource =
|
|
667
|
+
axis === parentAxis && axis !== childAxis ? parentProps : childProps;
|
|
668
|
+
const fallbackAxisSource =
|
|
669
|
+
axisSource === childProps ? parentProps : childProps;
|
|
670
|
+
|
|
671
|
+
const parentPadding = spacingToInsets(parentProps.layout?.padding);
|
|
672
|
+
const childPadding = spacingToInsets(childProps.layout?.padding);
|
|
673
|
+
const parentMargin = spacingToInsets(parentProps.layout?.margin);
|
|
674
|
+
const childMargin = spacingToInsets(childProps.layout?.margin);
|
|
675
|
+
|
|
676
|
+
const mergedProps: Record<string, any> = {
|
|
677
|
+
axis,
|
|
678
|
+
alignment: axisSource.alignment ?? fallbackAxisSource.alignment ?? 'start',
|
|
679
|
+
distribution:
|
|
680
|
+
axisSource.distribution ?? fallbackAxisSource.distribution ?? 'start',
|
|
681
|
+
childSpacing:
|
|
682
|
+
Number(axisSource.childSpacing ?? fallbackAxisSource.childSpacing ?? 0) ||
|
|
683
|
+
0,
|
|
684
|
+
size: {
|
|
685
|
+
width: childProps.size?.width ?? parentProps.size?.width ?? 'fill',
|
|
686
|
+
height: childProps.size?.height ?? parentProps.size?.height ?? 'fit',
|
|
687
|
+
},
|
|
688
|
+
layout: {
|
|
689
|
+
padding: insetsToSpacing({
|
|
690
|
+
top: parentPadding.top + childPadding.top,
|
|
691
|
+
right: parentPadding.right + childPadding.right,
|
|
692
|
+
bottom: parentPadding.bottom + childPadding.bottom,
|
|
693
|
+
left: parentPadding.left + childPadding.left,
|
|
694
|
+
}),
|
|
695
|
+
margin: insetsToSpacing({
|
|
696
|
+
top: parentMargin.top + childMargin.top,
|
|
697
|
+
right: parentMargin.right + childMargin.right,
|
|
698
|
+
bottom: parentMargin.bottom + childMargin.bottom,
|
|
699
|
+
left: parentMargin.left + childMargin.left,
|
|
700
|
+
}),
|
|
701
|
+
},
|
|
702
|
+
appearance: {
|
|
703
|
+
shape: 'rectangle',
|
|
704
|
+
cornerRadius: Number(
|
|
705
|
+
childProps.appearance?.cornerRadius ??
|
|
706
|
+
parentProps.appearance?.cornerRadius ??
|
|
707
|
+
0
|
|
708
|
+
),
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
if (axis === 'overlay') {
|
|
713
|
+
mergedProps.overlayAlignment =
|
|
714
|
+
childProps.overlayAlignment ?? parentProps.overlayAlignment ?? 'center';
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (parentProps.fill || childProps.fill) {
|
|
718
|
+
mergedProps.fill = childProps.fill ?? parentProps.fill;
|
|
719
|
+
}
|
|
720
|
+
if (parentProps.border || childProps.border) {
|
|
721
|
+
mergedProps.border = childProps.border ?? parentProps.border;
|
|
722
|
+
}
|
|
723
|
+
if (parentProps.shadow || childProps.shadow) {
|
|
724
|
+
mergedProps.shadow = childProps.shadow ?? parentProps.shadow;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (childProps.width !== undefined || parentProps.width !== undefined) {
|
|
728
|
+
mergedProps.width = childProps.width ?? parentProps.width;
|
|
729
|
+
}
|
|
730
|
+
if (childProps.height !== undefined || parentProps.height !== undefined) {
|
|
731
|
+
mergedProps.height = childProps.height ?? parentProps.height;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return mergedProps;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function normalizeStackLikeNode(
|
|
738
|
+
json: Record<string, any>
|
|
739
|
+
): Record<string, any> {
|
|
740
|
+
if (!json || typeof json !== 'object') return json;
|
|
741
|
+
const type = String(json.type ?? '');
|
|
742
|
+
if (
|
|
743
|
+
!['stack', 'container', 'layout', 'row', 'column', 'grid'].includes(type)
|
|
744
|
+
) {
|
|
745
|
+
return json;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const props = { ...(json.properties ?? {}) } as Record<string, any>;
|
|
749
|
+
const axis = normalizeStackAxis(type, props);
|
|
750
|
+
const size = normalizeStackSize(props, axis);
|
|
751
|
+
const children = Array.isArray(json.children) ? [...json.children] : [];
|
|
752
|
+
if (json.child) {
|
|
753
|
+
children.push(json.child);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const normalizedProps: Record<string, any> = {
|
|
757
|
+
axis,
|
|
758
|
+
alignment: normalizeStackCrossAlignment(
|
|
759
|
+
props.alignment ?? props.crossAxisAlignment
|
|
760
|
+
),
|
|
761
|
+
distribution: normalizeStackDistribution(
|
|
762
|
+
props.distribution ?? props.mainAxisAlignment
|
|
763
|
+
),
|
|
764
|
+
childSpacing: Number(props.childSpacing ?? props.gap ?? props.spacing ?? 0),
|
|
765
|
+
size,
|
|
766
|
+
layout: {
|
|
767
|
+
padding: normalizeStackSpacingConfig(
|
|
768
|
+
props.layout?.padding ?? props.padding
|
|
769
|
+
),
|
|
770
|
+
margin: normalizeStackSpacingConfig(props.layout?.margin ?? props.margin),
|
|
771
|
+
},
|
|
772
|
+
appearance: {
|
|
773
|
+
shape: 'rectangle',
|
|
774
|
+
cornerRadius: Number(
|
|
775
|
+
props.appearance?.cornerRadius ?? props.borderRadius ?? 0
|
|
776
|
+
),
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
if (axis === 'overlay') {
|
|
781
|
+
normalizedProps.overlayAlignment = normalizeOverlayAlignment(
|
|
782
|
+
props.overlayAlignment ?? props.alignment
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const fill = normalizeStackFill(props);
|
|
787
|
+
if (fill) normalizedProps.fill = fill;
|
|
788
|
+
|
|
789
|
+
const border = normalizeStackBorder(props);
|
|
790
|
+
if (border) normalizedProps.border = border;
|
|
791
|
+
|
|
792
|
+
const shadow = normalizeStackShadow(props);
|
|
793
|
+
if (shadow) normalizedProps.shadow = shadow;
|
|
794
|
+
|
|
795
|
+
if (props.width !== undefined) normalizedProps.width = props.width;
|
|
796
|
+
if (props.height !== undefined) normalizedProps.height = props.height;
|
|
797
|
+
|
|
798
|
+
if ((type === 'container' || type === 'layout') && children.length === 1) {
|
|
799
|
+
const childCandidate = children[0];
|
|
800
|
+
if (childCandidate && typeof childCandidate === 'object') {
|
|
801
|
+
const normalizedChild: Record<string, any> = normalizeStackLikeNode(
|
|
802
|
+
childCandidate as Record<string, any>
|
|
803
|
+
);
|
|
804
|
+
if (normalizedChild?.type === 'stack') {
|
|
805
|
+
const childProps = {
|
|
806
|
+
...(normalizedChild.properties ?? {}),
|
|
807
|
+
} as Record<string, any>;
|
|
808
|
+
const mergedProps = mergeStackProps(normalizedProps, childProps);
|
|
809
|
+
return {
|
|
810
|
+
...json,
|
|
811
|
+
type: 'stack',
|
|
812
|
+
properties: mergedProps,
|
|
813
|
+
children: Array.isArray(normalizedChild.children)
|
|
814
|
+
? normalizedChild.children
|
|
815
|
+
: [],
|
|
816
|
+
child: undefined,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
...json,
|
|
824
|
+
type: 'stack',
|
|
825
|
+
properties: normalizedProps,
|
|
826
|
+
children,
|
|
827
|
+
child: undefined,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function parseOverlayGridAlignment(
|
|
832
|
+
value?: string
|
|
833
|
+
): Pick<ViewStyle, 'justifyContent' | 'alignItems'> {
|
|
834
|
+
switch (normalizeOverlayAlignment(value)) {
|
|
835
|
+
case 'topStart':
|
|
836
|
+
return { justifyContent: 'flex-start', alignItems: 'flex-start' };
|
|
837
|
+
case 'top':
|
|
838
|
+
return { justifyContent: 'flex-start', alignItems: 'center' };
|
|
839
|
+
case 'topEnd':
|
|
840
|
+
return { justifyContent: 'flex-start', alignItems: 'flex-end' };
|
|
841
|
+
case 'start':
|
|
842
|
+
return { justifyContent: 'center', alignItems: 'flex-start' };
|
|
843
|
+
case 'end':
|
|
844
|
+
return { justifyContent: 'center', alignItems: 'flex-end' };
|
|
845
|
+
case 'bottomStart':
|
|
846
|
+
return { justifyContent: 'flex-end', alignItems: 'flex-start' };
|
|
847
|
+
case 'bottom':
|
|
848
|
+
return { justifyContent: 'flex-end', alignItems: 'center' };
|
|
849
|
+
case 'bottomEnd':
|
|
850
|
+
return { justifyContent: 'flex-end', alignItems: 'flex-end' };
|
|
851
|
+
case 'center':
|
|
852
|
+
default:
|
|
853
|
+
return { justifyContent: 'center', alignItems: 'center' };
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function resolveStackDimension(
|
|
858
|
+
props: Record<string, any>,
|
|
859
|
+
axis: 'vertical' | 'horizontal' | 'overlay',
|
|
860
|
+
key: 'width' | 'height',
|
|
861
|
+
axisBounds: AxisBounds
|
|
862
|
+
) {
|
|
863
|
+
const mode = props.size?.[key];
|
|
864
|
+
const legacy = normalizeDimension(parseLayoutDimension(props[key]));
|
|
865
|
+
const isBounded =
|
|
866
|
+
key === 'width' ? axisBounds.widthBounded : axisBounds.heightBounded;
|
|
867
|
+
if (mode === 'fill') return isBounded ? '100%' : legacy;
|
|
868
|
+
if (mode === 'fit') return legacy;
|
|
869
|
+
if (mode === 'fixed') return legacy;
|
|
870
|
+
if (legacy !== undefined) return legacy;
|
|
871
|
+
if (key === 'width' && axis !== 'horizontal' && axisBounds.widthBounded)
|
|
872
|
+
return '100%';
|
|
873
|
+
if (key === 'height' && axis === 'horizontal' && axisBounds.heightBounded)
|
|
874
|
+
return '100%';
|
|
875
|
+
return undefined;
|
|
876
|
+
}
|
|
877
|
+
|
|
360
878
|
function buildWidget(
|
|
361
879
|
json: Record<string, any>,
|
|
362
880
|
params: {
|
|
@@ -364,19 +882,32 @@ function buildWidget(
|
|
|
364
882
|
formData: Record<string, any>;
|
|
365
883
|
onInputChange?: (id: string, value: any) => void;
|
|
366
884
|
allowFlexExpansion?: boolean;
|
|
885
|
+
parentFlexAxis?: 'vertical' | 'horizontal';
|
|
886
|
+
parentMainAxisBounded?: boolean;
|
|
887
|
+
pageAxisBounds?: AxisBounds;
|
|
367
888
|
screenData: Record<string, any>;
|
|
368
889
|
enableFontAwesomeIcons: boolean;
|
|
369
890
|
key?: string;
|
|
370
891
|
}
|
|
371
892
|
): React.ReactNode {
|
|
372
893
|
if (!json || typeof json !== 'object') return null;
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
894
|
+
const normalizedJson = normalizeStackLikeNode(json);
|
|
895
|
+
const type = normalizedJson.type;
|
|
896
|
+
const id = normalizedJson.id;
|
|
897
|
+
const props = { ...(normalizedJson.properties ?? {}) } as Record<string, any>;
|
|
898
|
+
const pageAxisBounds =
|
|
899
|
+
params.pageAxisBounds ??
|
|
900
|
+
getAxisBoundsForPageScroll(resolvePageScrollAxis(params.screenData));
|
|
901
|
+
const childrenJson: Record<string, any>[] | undefined = Array.isArray(
|
|
902
|
+
normalizedJson.children
|
|
903
|
+
)
|
|
904
|
+
? (normalizedJson.children as Record<string, any>[])
|
|
905
|
+
: undefined;
|
|
906
|
+
const childJson = normalizedJson.child as Record<string, any> | undefined;
|
|
907
|
+
const action = normalizedJson.action as string | undefined;
|
|
908
|
+
const actionPayload = action
|
|
909
|
+
? buildActionPayload(props, normalizedJson)
|
|
910
|
+
: undefined;
|
|
380
911
|
|
|
381
912
|
let node: React.ReactNode = null;
|
|
382
913
|
|
|
@@ -565,62 +1096,121 @@ function buildWidget(
|
|
|
565
1096
|
const height =
|
|
566
1097
|
normalizeDimension(parseLayoutDimension(props.height)) ?? 50;
|
|
567
1098
|
const label = props.label ?? 'Button';
|
|
1099
|
+
const normalizedStroke = normalizeButtonStroke(props.stroke);
|
|
1100
|
+
const normalizedEffects = normalizeButtonEffects(
|
|
1101
|
+
props.effects,
|
|
1102
|
+
props.shadow
|
|
1103
|
+
);
|
|
568
1104
|
const textStyle = getTextStyle(
|
|
569
1105
|
{ ...props, height: null },
|
|
570
1106
|
'textColor',
|
|
571
1107
|
'0xFFFFFFFF'
|
|
572
1108
|
);
|
|
1109
|
+
const backgroundColor = parseColor(props.color ?? '0xFF2196F3');
|
|
1110
|
+
const shadowBounds = getButtonShadowLayerBounds(
|
|
1111
|
+
normalizedStroke,
|
|
1112
|
+
borderRadius
|
|
1113
|
+
);
|
|
1114
|
+
const webShadowStyle = buildButtonWebShadowStyle(normalizedEffects);
|
|
1115
|
+
const firstNativeEffect =
|
|
1116
|
+
normalizedEffects.length > 0 ? normalizedEffects[0] : null;
|
|
1117
|
+
const nativePrimaryShadowStyle =
|
|
1118
|
+
Platform.OS === 'web' || !firstNativeEffect
|
|
1119
|
+
? null
|
|
1120
|
+
: buildButtonNativeShadowStyle(firstNativeEffect);
|
|
1121
|
+
const centeredStrokeStyle =
|
|
1122
|
+
normalizedStroke && normalizedStroke.position === 'center'
|
|
1123
|
+
? {
|
|
1124
|
+
borderWidth: normalizedStroke.width,
|
|
1125
|
+
borderColor: normalizedStroke.color,
|
|
1126
|
+
}
|
|
1127
|
+
: null;
|
|
1128
|
+
const fillLayerStyle: any = {
|
|
1129
|
+
position: 'absolute',
|
|
1130
|
+
top: 0,
|
|
1131
|
+
right: 0,
|
|
1132
|
+
bottom: 0,
|
|
1133
|
+
left: 0,
|
|
1134
|
+
borderRadius,
|
|
1135
|
+
overflow: 'hidden',
|
|
1136
|
+
...(centeredStrokeStyle ?? {}),
|
|
1137
|
+
};
|
|
1138
|
+
const nativeShadowEffectsInPaintOrder = normalizedEffects
|
|
1139
|
+
.slice()
|
|
1140
|
+
.reverse();
|
|
573
1141
|
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
1142
|
+
const pressableStyle: any = {
|
|
1143
|
+
width,
|
|
1144
|
+
height,
|
|
1145
|
+
borderRadius,
|
|
1146
|
+
justifyContent: 'center',
|
|
1147
|
+
alignItems: 'center',
|
|
1148
|
+
overflow: 'visible',
|
|
1149
|
+
...(nativePrimaryShadowStyle ?? {}),
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
node = (
|
|
1153
|
+
<Pressable
|
|
1154
|
+
collapsable={false}
|
|
1155
|
+
onPress={
|
|
1156
|
+
action
|
|
1157
|
+
? () => params.onAction(action, actionPayload ?? props)
|
|
1158
|
+
: undefined
|
|
1159
|
+
}
|
|
1160
|
+
style={pressableStyle}
|
|
577
1161
|
>
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1162
|
+
{Platform.OS === 'web' ? (
|
|
1163
|
+
webShadowStyle ? (
|
|
1164
|
+
<View
|
|
1165
|
+
pointerEvents="none"
|
|
1166
|
+
style={{
|
|
1167
|
+
position: 'absolute',
|
|
1168
|
+
...shadowBounds,
|
|
1169
|
+
...webShadowStyle,
|
|
1170
|
+
}}
|
|
1171
|
+
/>
|
|
1172
|
+
) : null
|
|
1173
|
+
) : (
|
|
1174
|
+
nativeShadowEffectsInPaintOrder.map((effect, index) => (
|
|
1175
|
+
<View
|
|
1176
|
+
key={`shadow-${index}`}
|
|
1177
|
+
collapsable={false}
|
|
1178
|
+
pointerEvents="none"
|
|
1179
|
+
style={{
|
|
1180
|
+
position: 'absolute',
|
|
1181
|
+
...shadowBounds,
|
|
1182
|
+
...buildButtonNativeShadowStyle(effect),
|
|
1183
|
+
}}
|
|
1184
|
+
/>
|
|
1185
|
+
))
|
|
1186
|
+
)}
|
|
581
1187
|
|
|
582
|
-
|
|
583
|
-
node = (
|
|
584
|
-
<Pressable
|
|
585
|
-
onPress={
|
|
586
|
-
action
|
|
587
|
-
? () => params.onAction(action, actionPayload ?? props)
|
|
588
|
-
: undefined
|
|
589
|
-
}
|
|
590
|
-
style={{ width, height }}
|
|
591
|
-
>
|
|
1188
|
+
{gradient ? (
|
|
592
1189
|
<LinearGradient
|
|
593
1190
|
colors={gradient.colors}
|
|
594
1191
|
start={gradient.start}
|
|
595
1192
|
end={gradient.end}
|
|
596
1193
|
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
|
-
}}
|
|
1194
|
+
style={fillLayerStyle}
|
|
1195
|
+
/>
|
|
1196
|
+
) : (
|
|
1197
|
+
<View
|
|
1198
|
+
style={{
|
|
1199
|
+
...fillLayerStyle,
|
|
1200
|
+
backgroundColor,
|
|
1201
|
+
}}
|
|
1202
|
+
/>
|
|
1203
|
+
)}
|
|
1204
|
+
|
|
1205
|
+
{renderButtonStrokeOverlay(normalizedStroke, borderRadius)}
|
|
1206
|
+
|
|
1207
|
+
<View
|
|
1208
|
+
style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
|
|
619
1209
|
>
|
|
620
|
-
{
|
|
621
|
-
</
|
|
622
|
-
|
|
623
|
-
|
|
1210
|
+
<Text style={textStyle}>{label}</Text>
|
|
1211
|
+
</View>
|
|
1212
|
+
</Pressable>
|
|
1213
|
+
);
|
|
624
1214
|
break;
|
|
625
1215
|
}
|
|
626
1216
|
case 'text_input': {
|
|
@@ -936,25 +1526,170 @@ function buildWidget(
|
|
|
936
1526
|
break;
|
|
937
1527
|
}
|
|
938
1528
|
case 'stack': {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
1529
|
+
const axis =
|
|
1530
|
+
props.axis === 'horizontal' || props.axis === 'overlay'
|
|
1531
|
+
? props.axis
|
|
1532
|
+
: 'vertical';
|
|
1533
|
+
const isOverlay = axis === 'overlay';
|
|
1534
|
+
const spacing = Number(props.childSpacing ?? 0);
|
|
1535
|
+
const applySpacing = !STACK_SPACE_DISTRIBUTIONS.has(
|
|
1536
|
+
String(props.distribution ?? 'start')
|
|
1537
|
+
);
|
|
1538
|
+
const paddingInsets = stackSpacingToInsets(
|
|
1539
|
+
props.layout?.padding ?? props.padding
|
|
1540
|
+
);
|
|
1541
|
+
const width = resolveStackDimension(props, axis, 'width', pageAxisBounds);
|
|
1542
|
+
const height = resolveStackDimension(
|
|
1543
|
+
props,
|
|
1544
|
+
axis,
|
|
1545
|
+
'height',
|
|
1546
|
+
pageAxisBounds
|
|
1547
|
+
);
|
|
1548
|
+
const mainAxisBounded =
|
|
1549
|
+
axis === 'horizontal'
|
|
1550
|
+
? pageAxisBounds.widthBounded
|
|
1551
|
+
: pageAxisBounds.heightBounded;
|
|
1552
|
+
const borderRadius = Number(
|
|
1553
|
+
props.appearance?.cornerRadius ?? props.borderRadius ?? 0
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
const borderWidth = Number(props.border?.width ?? 0);
|
|
1557
|
+
const borderColor =
|
|
1558
|
+
borderWidth > 0 ? parseColor(props.border?.color ?? '#E5E7EB') : null;
|
|
1559
|
+
const shadowColor = props.shadow?.color
|
|
1560
|
+
? parseColor(props.shadow.color)
|
|
1561
|
+
: null;
|
|
1562
|
+
const shadowStyle: Record<string, any> =
|
|
1563
|
+
shadowColor && props.shadow
|
|
1564
|
+
? Platform.OS === 'web'
|
|
1565
|
+
? {
|
|
1566
|
+
boxShadow: `${Number(props.shadow.x ?? 0)}px ${Number(
|
|
1567
|
+
props.shadow.y ?? 8
|
|
1568
|
+
)}px ${Number(props.shadow.blur ?? 24)}px ${shadowColor}`,
|
|
1569
|
+
}
|
|
1570
|
+
: {
|
|
1571
|
+
shadowColor,
|
|
1572
|
+
shadowOffset: {
|
|
1573
|
+
width: Number(props.shadow.x ?? 0),
|
|
1574
|
+
height: Number(props.shadow.y ?? 8),
|
|
1575
|
+
},
|
|
1576
|
+
shadowOpacity: 0.35,
|
|
1577
|
+
shadowRadius: Math.max(0, Number(props.shadow.blur ?? 24) / 2),
|
|
1578
|
+
elevation: Math.max(
|
|
1579
|
+
1,
|
|
1580
|
+
Math.round(
|
|
1581
|
+
(Number(props.shadow.blur ?? 24) +
|
|
1582
|
+
Math.abs(Number(props.shadow.y ?? 8))) /
|
|
1583
|
+
3
|
|
1584
|
+
)
|
|
1585
|
+
),
|
|
1586
|
+
}
|
|
1587
|
+
: {};
|
|
1588
|
+
|
|
1589
|
+
const fill = props.fill;
|
|
1590
|
+
const stackGradient =
|
|
1591
|
+
fill?.type === 'gradient'
|
|
1592
|
+
? parseGradient({ ...fill, type: 'gradient' })
|
|
1593
|
+
: undefined;
|
|
1594
|
+
const stackColor =
|
|
1595
|
+
fill?.type === 'solid' || (!fill?.type && fill?.color)
|
|
1596
|
+
? parseColor(fill?.color ?? '#FFFFFFFF')
|
|
1597
|
+
: parseColor(props.backgroundColor ?? '#00000000');
|
|
1598
|
+
|
|
1599
|
+
const baseStackStyle: any = {
|
|
1600
|
+
width,
|
|
1601
|
+
height,
|
|
1602
|
+
borderRadius: borderRadius > 0 ? borderRadius : undefined,
|
|
1603
|
+
overflow: borderRadius > 0 ? 'hidden' : undefined,
|
|
1604
|
+
borderWidth: borderColor ? borderWidth : undefined,
|
|
1605
|
+
borderColor: borderColor ?? undefined,
|
|
1606
|
+
...insetsToStyle(paddingInsets),
|
|
1607
|
+
...shadowStyle,
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
const content = isOverlay ? (
|
|
1611
|
+
<View style={{ position: 'relative', minHeight: 0, minWidth: 0 }}>
|
|
1612
|
+
{(childrenJson ?? []).map((child, index) => {
|
|
1613
|
+
const alignment = parseOverlayGridAlignment(
|
|
1614
|
+
props.overlayAlignment ?? props.alignment
|
|
1615
|
+
);
|
|
1616
|
+
return (
|
|
1617
|
+
<View
|
|
1618
|
+
key={`stack-overlay-${index}`}
|
|
1619
|
+
style={{
|
|
1620
|
+
position: 'absolute',
|
|
1621
|
+
top: 0,
|
|
1622
|
+
right: 0,
|
|
1623
|
+
bottom: 0,
|
|
1624
|
+
left: 0,
|
|
1625
|
+
justifyContent: alignment.justifyContent,
|
|
1626
|
+
alignItems: alignment.alignItems,
|
|
1627
|
+
}}
|
|
1628
|
+
>
|
|
1629
|
+
{buildWidget(child, {
|
|
1630
|
+
...params,
|
|
1631
|
+
allowFlexExpansion: true,
|
|
1632
|
+
pageAxisBounds,
|
|
1633
|
+
})}
|
|
1634
|
+
</View>
|
|
1635
|
+
);
|
|
1636
|
+
})}
|
|
1637
|
+
{(childrenJson ?? []).length === 0 ? null : null}
|
|
1638
|
+
</View>
|
|
1639
|
+
) : (
|
|
942
1640
|
<View
|
|
943
1641
|
style={{
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1642
|
+
flexDirection: axis === 'horizontal' ? 'row' : 'column',
|
|
1643
|
+
justifyContent: parseFlexAlignment(props.distribution),
|
|
1644
|
+
alignItems: parseCrossAlignment(props.alignment),
|
|
1645
|
+
minHeight: 0,
|
|
1646
|
+
minWidth: 0,
|
|
949
1647
|
}}
|
|
950
1648
|
>
|
|
951
1649
|
{(childrenJson ?? []).map((child, index) => (
|
|
952
1650
|
<React.Fragment key={`stack-${index}`}>
|
|
953
|
-
{buildWidget(child, {
|
|
1651
|
+
{buildWidget(child, {
|
|
1652
|
+
...params,
|
|
1653
|
+
allowFlexExpansion: true,
|
|
1654
|
+
parentFlexAxis:
|
|
1655
|
+
axis === 'horizontal' ? 'horizontal' : 'vertical',
|
|
1656
|
+
parentMainAxisBounded: mainAxisBounded,
|
|
1657
|
+
pageAxisBounds,
|
|
1658
|
+
})}
|
|
1659
|
+
{applySpacing &&
|
|
1660
|
+
spacing > 0 &&
|
|
1661
|
+
index < (childrenJson?.length ?? 0) - 1 ? (
|
|
1662
|
+
<View
|
|
1663
|
+
style={{
|
|
1664
|
+
width: axis === 'horizontal' ? spacing : undefined,
|
|
1665
|
+
height: axis === 'vertical' ? spacing : undefined,
|
|
1666
|
+
}}
|
|
1667
|
+
/>
|
|
1668
|
+
) : null}
|
|
954
1669
|
</React.Fragment>
|
|
955
1670
|
))}
|
|
956
1671
|
</View>
|
|
957
1672
|
);
|
|
1673
|
+
|
|
1674
|
+
if (stackGradient) {
|
|
1675
|
+
node = (
|
|
1676
|
+
<LinearGradient
|
|
1677
|
+
colors={stackGradient.colors}
|
|
1678
|
+
start={stackGradient.start}
|
|
1679
|
+
end={stackGradient.end}
|
|
1680
|
+
locations={stackGradient.stops}
|
|
1681
|
+
style={baseStackStyle}
|
|
1682
|
+
>
|
|
1683
|
+
{content}
|
|
1684
|
+
</LinearGradient>
|
|
1685
|
+
);
|
|
1686
|
+
} else {
|
|
1687
|
+
node = (
|
|
1688
|
+
<View style={{ ...baseStackStyle, backgroundColor: stackColor }}>
|
|
1689
|
+
{content}
|
|
1690
|
+
</View>
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
958
1693
|
break;
|
|
959
1694
|
}
|
|
960
1695
|
case 'positioned': {
|
|
@@ -987,29 +1722,64 @@ function buildWidget(
|
|
|
987
1722
|
|
|
988
1723
|
if (!node) return null;
|
|
989
1724
|
|
|
990
|
-
const marginVal =
|
|
1725
|
+
const marginVal =
|
|
1726
|
+
type === 'stack' ? props.layout?.margin ?? props.margin : props.margin;
|
|
991
1727
|
if (marginVal !== undefined && marginVal !== null) {
|
|
992
|
-
const marginInsets =
|
|
1728
|
+
const marginInsets =
|
|
1729
|
+
type === 'stack'
|
|
1730
|
+
? stackSpacingToInsets(marginVal)
|
|
1731
|
+
: parseInsets(marginVal);
|
|
993
1732
|
node = <View style={insetsToMarginStyle(marginInsets)}>{node}</View>;
|
|
994
|
-
} else if (
|
|
1733
|
+
} else if (
|
|
1734
|
+
type !== 'container' &&
|
|
1735
|
+
type !== 'padding' &&
|
|
1736
|
+
type !== 'stack' &&
|
|
1737
|
+
props.padding
|
|
1738
|
+
) {
|
|
995
1739
|
const paddingInsets = parseInsets(props.padding);
|
|
996
1740
|
node = <View style={insetsToStyle(paddingInsets)}>{node}</View>;
|
|
997
1741
|
}
|
|
998
1742
|
|
|
999
|
-
if (params.allowFlexExpansion
|
|
1743
|
+
if (params.allowFlexExpansion) {
|
|
1000
1744
|
let shouldExpand = false;
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
if (
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1745
|
+
const parentAxis = params.parentFlexAxis ?? 'vertical';
|
|
1746
|
+
const parentMainAxisBounded = params.parentMainAxisBounded ?? true;
|
|
1747
|
+
|
|
1748
|
+
if (parentMainAxisBounded) {
|
|
1749
|
+
if (type === 'stack') {
|
|
1750
|
+
const legacyExpand = props.fit === 'expand';
|
|
1751
|
+
const widthMode = String(props.size?.width ?? '').toLowerCase();
|
|
1752
|
+
const heightMode = String(props.size?.height ?? '').toLowerCase();
|
|
1753
|
+
if (
|
|
1754
|
+
parentAxis === 'vertical' &&
|
|
1755
|
+
(legacyExpand || heightMode === 'fill')
|
|
1756
|
+
) {
|
|
1757
|
+
shouldExpand = true;
|
|
1758
|
+
}
|
|
1759
|
+
if (
|
|
1760
|
+
parentAxis === 'horizontal' &&
|
|
1761
|
+
(legacyExpand || widthMode === 'fill')
|
|
1762
|
+
) {
|
|
1763
|
+
shouldExpand = true;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (parentAxis === 'vertical' && props.height !== undefined) {
|
|
1768
|
+
const heightVal = parseLayoutDimension(props.height);
|
|
1769
|
+
if (heightVal === Number.POSITIVE_INFINITY) {
|
|
1770
|
+
shouldExpand = true;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (parentAxis === 'horizontal' && props.width !== undefined) {
|
|
1774
|
+
const widthVal = parseLayoutDimension(props.width);
|
|
1775
|
+
if (widthVal === Number.POSITIVE_INFINITY) {
|
|
1776
|
+
shouldExpand = true;
|
|
1777
|
+
}
|
|
1008
1778
|
}
|
|
1009
1779
|
}
|
|
1010
1780
|
|
|
1011
1781
|
if (shouldExpand) {
|
|
1012
|
-
node = <View style={{ flex: 1 }}>{node}</View>;
|
|
1782
|
+
node = <View style={{ flex: 1, minHeight: 0, minWidth: 0 }}>{node}</View>;
|
|
1013
1783
|
}
|
|
1014
1784
|
}
|
|
1015
1785
|
|
|
@@ -1088,6 +1858,227 @@ function parseGradient(value: any):
|
|
|
1088
1858
|
return { colors, start, end, stops };
|
|
1089
1859
|
}
|
|
1090
1860
|
|
|
1861
|
+
type ButtonStrokePosition = 'inside' | 'center' | 'outside';
|
|
1862
|
+
|
|
1863
|
+
interface ButtonStroke {
|
|
1864
|
+
enabled?: boolean;
|
|
1865
|
+
color?: string;
|
|
1866
|
+
opacity?: number;
|
|
1867
|
+
width?: number;
|
|
1868
|
+
position?: ButtonStrokePosition;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
interface ButtonDropShadowEffect {
|
|
1872
|
+
type?: 'dropShadow' | string;
|
|
1873
|
+
enabled?: boolean;
|
|
1874
|
+
x?: number;
|
|
1875
|
+
y?: number;
|
|
1876
|
+
blur?: number;
|
|
1877
|
+
spread?: number;
|
|
1878
|
+
color?: string;
|
|
1879
|
+
opacity?: number;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
interface LegacyButtonShadow {
|
|
1883
|
+
enabled?: boolean;
|
|
1884
|
+
x?: number;
|
|
1885
|
+
y?: number;
|
|
1886
|
+
blur?: number;
|
|
1887
|
+
color?: string;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
type NormalizedButtonStroke = {
|
|
1891
|
+
color: string;
|
|
1892
|
+
width: number;
|
|
1893
|
+
position: ButtonStrokePosition;
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
type NormalizedButtonDropShadow = {
|
|
1897
|
+
x: number;
|
|
1898
|
+
y: number;
|
|
1899
|
+
blur: number;
|
|
1900
|
+
spread: number;
|
|
1901
|
+
color: string;
|
|
1902
|
+
opacity: number;
|
|
1903
|
+
};
|
|
1904
|
+
|
|
1905
|
+
function normalizeButtonStroke(
|
|
1906
|
+
input: ButtonStroke | undefined
|
|
1907
|
+
): NormalizedButtonStroke | null {
|
|
1908
|
+
if (!input || typeof input !== 'object') return null;
|
|
1909
|
+
const legacyStrokeWithoutEnabled = input.enabled === undefined;
|
|
1910
|
+
const enabled = legacyStrokeWithoutEnabled ? true : input.enabled === true;
|
|
1911
|
+
if (!enabled) return null;
|
|
1912
|
+
|
|
1913
|
+
const width = Number(input.width ?? 1);
|
|
1914
|
+
if (!Number.isFinite(width) || width <= 0) return null;
|
|
1915
|
+
|
|
1916
|
+
const position =
|
|
1917
|
+
input.position === 'inside' ||
|
|
1918
|
+
input.position === 'center' ||
|
|
1919
|
+
input.position === 'outside'
|
|
1920
|
+
? input.position
|
|
1921
|
+
: 'center';
|
|
1922
|
+
const opacity = clamp01(Number(input.opacity ?? 1));
|
|
1923
|
+
const color = normalizeColorWithOpacity(input.color, opacity);
|
|
1924
|
+
if (color === 'transparent') return null;
|
|
1925
|
+
|
|
1926
|
+
return { color, width, position };
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function normalizeButtonEffects(
|
|
1930
|
+
effectsInput: ButtonDropShadowEffect[] | undefined,
|
|
1931
|
+
legacyShadowInput: LegacyButtonShadow | undefined
|
|
1932
|
+
): NormalizedButtonDropShadow[] {
|
|
1933
|
+
if (Array.isArray(effectsInput)) {
|
|
1934
|
+
return effectsInput
|
|
1935
|
+
.map(normalizeDropShadowEffect)
|
|
1936
|
+
.filter(
|
|
1937
|
+
(effect): effect is NormalizedButtonDropShadow => effect !== null
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if (!legacyShadowInput || typeof legacyShadowInput !== 'object') {
|
|
1942
|
+
return [];
|
|
1943
|
+
}
|
|
1944
|
+
if (legacyShadowInput.enabled !== true) return [];
|
|
1945
|
+
|
|
1946
|
+
const migrated = normalizeDropShadowEffect({
|
|
1947
|
+
type: 'dropShadow',
|
|
1948
|
+
enabled: true,
|
|
1949
|
+
x: legacyShadowInput.x,
|
|
1950
|
+
y: legacyShadowInput.y,
|
|
1951
|
+
blur: legacyShadowInput.blur,
|
|
1952
|
+
spread: 0,
|
|
1953
|
+
color: legacyShadowInput.color,
|
|
1954
|
+
opacity: 1,
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
return migrated ? [migrated] : [];
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
function normalizeDropShadowEffect(
|
|
1961
|
+
effect: ButtonDropShadowEffect
|
|
1962
|
+
): NormalizedButtonDropShadow | null {
|
|
1963
|
+
if (!effect || typeof effect !== 'object') return null;
|
|
1964
|
+
if ((effect.type ?? 'dropShadow') !== 'dropShadow') return null;
|
|
1965
|
+
if (effect.enabled === false) return null;
|
|
1966
|
+
|
|
1967
|
+
const opacity = clamp01(Number(effect.opacity ?? 1));
|
|
1968
|
+
const color = normalizeColorWithOpacity(effect.color, opacity);
|
|
1969
|
+
if (color === 'transparent') return null;
|
|
1970
|
+
|
|
1971
|
+
return {
|
|
1972
|
+
x: Number(effect.x ?? 0),
|
|
1973
|
+
y: Number(effect.y ?? 0),
|
|
1974
|
+
blur: Math.max(0, Number(effect.blur ?? 0)),
|
|
1975
|
+
spread: Number(effect.spread ?? 0),
|
|
1976
|
+
color,
|
|
1977
|
+
opacity,
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function normalizeColorWithOpacity(
|
|
1982
|
+
input: string | undefined,
|
|
1983
|
+
opacity: number
|
|
1984
|
+
): string {
|
|
1985
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
1986
|
+
const normalizedOpacity = clamp01(opacity);
|
|
1987
|
+
if (raw.startsWith('rgba(') || raw.startsWith('rgb(')) {
|
|
1988
|
+
return withOpacity(raw, normalizedOpacity);
|
|
1989
|
+
}
|
|
1990
|
+
return withOpacity(parseColor(input ?? '0xFF000000'), normalizedOpacity);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function buildButtonWebShadowStyle(
|
|
1994
|
+
effects: NormalizedButtonDropShadow[]
|
|
1995
|
+
): Record<string, any> | null {
|
|
1996
|
+
if (effects.length === 0) return null;
|
|
1997
|
+
const layers = effects.map(
|
|
1998
|
+
(effect) =>
|
|
1999
|
+
`${effect.x}px ${effect.y}px ${effect.blur}px ${effect.spread}px ${effect.color}`
|
|
2000
|
+
);
|
|
2001
|
+
return { boxShadow: layers.join(', ') };
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function buildButtonNativeShadowStyle(
|
|
2005
|
+
effect: NormalizedButtonDropShadow
|
|
2006
|
+
): Record<string, any> {
|
|
2007
|
+
const rgba = parseRgbaColor(effect.color);
|
|
2008
|
+
return {
|
|
2009
|
+
// Android elevation requires a drawable host shape; keep it effectively invisible.
|
|
2010
|
+
backgroundColor: 'rgba(255,255,255,0.02)',
|
|
2011
|
+
shadowColor: rgba ? `rgba(${rgba.r},${rgba.g},${rgba.b},1)` : effect.color,
|
|
2012
|
+
shadowOffset: { width: effect.x, height: effect.y },
|
|
2013
|
+
shadowOpacity: rgba ? rgba.a : effect.opacity,
|
|
2014
|
+
shadowRadius: effect.blur / 2,
|
|
2015
|
+
elevation: Math.max(1, Math.round((effect.blur + Math.abs(effect.y)) / 3)),
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
function getButtonShadowLayerBounds(
|
|
2020
|
+
stroke: NormalizedButtonStroke | null,
|
|
2021
|
+
borderRadius: number
|
|
2022
|
+
): Record<string, number> {
|
|
2023
|
+
const expandBy =
|
|
2024
|
+
!stroke || stroke.position === 'inside'
|
|
2025
|
+
? 0
|
|
2026
|
+
: stroke.position === 'center'
|
|
2027
|
+
? stroke.width / 2
|
|
2028
|
+
: stroke.width;
|
|
2029
|
+
|
|
2030
|
+
return {
|
|
2031
|
+
top: -expandBy,
|
|
2032
|
+
right: -expandBy,
|
|
2033
|
+
bottom: -expandBy,
|
|
2034
|
+
left: -expandBy,
|
|
2035
|
+
borderRadius: borderRadius + expandBy,
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function renderButtonStrokeOverlay(
|
|
2040
|
+
stroke: NormalizedButtonStroke | null,
|
|
2041
|
+
borderRadius: number
|
|
2042
|
+
) {
|
|
2043
|
+
if (!stroke || stroke.position === 'center') {
|
|
2044
|
+
return null;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
if (stroke.position === 'inside') {
|
|
2048
|
+
return (
|
|
2049
|
+
<View
|
|
2050
|
+
pointerEvents="none"
|
|
2051
|
+
style={{
|
|
2052
|
+
position: 'absolute',
|
|
2053
|
+
top: stroke.width / 2,
|
|
2054
|
+
left: stroke.width / 2,
|
|
2055
|
+
right: stroke.width / 2,
|
|
2056
|
+
bottom: stroke.width / 2,
|
|
2057
|
+
borderRadius: Math.max(0, borderRadius - stroke.width / 2),
|
|
2058
|
+
borderWidth: stroke.width,
|
|
2059
|
+
borderColor: stroke.color,
|
|
2060
|
+
}}
|
|
2061
|
+
/>
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
return (
|
|
2066
|
+
<View
|
|
2067
|
+
pointerEvents="none"
|
|
2068
|
+
style={{
|
|
2069
|
+
position: 'absolute',
|
|
2070
|
+
top: -stroke.width,
|
|
2071
|
+
left: -stroke.width,
|
|
2072
|
+
right: -stroke.width,
|
|
2073
|
+
bottom: -stroke.width,
|
|
2074
|
+
borderRadius: borderRadius + stroke.width,
|
|
2075
|
+
borderWidth: stroke.width,
|
|
2076
|
+
borderColor: stroke.color,
|
|
2077
|
+
}}
|
|
2078
|
+
/>
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1091
2082
|
function alignmentToGradient(value: string) {
|
|
1092
2083
|
switch (value) {
|
|
1093
2084
|
case 'topCenter':
|
|
@@ -1113,13 +2104,29 @@ function alignmentToGradient(value: string) {
|
|
|
1113
2104
|
}
|
|
1114
2105
|
|
|
1115
2106
|
function withOpacity(color: string, opacity: number): string {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
2107
|
+
const rgba = parseRgbaColor(color);
|
|
2108
|
+
if (!rgba) return color;
|
|
2109
|
+
return `rgba(${rgba.r},${rgba.g},${rgba.b},${clamp01(rgba.a * opacity)})`;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
function parseRgbaColor(
|
|
2113
|
+
color: string
|
|
2114
|
+
): { r: number; g: number; b: number; a: number } | null {
|
|
2115
|
+
const match = color.match(
|
|
2116
|
+
/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i
|
|
2117
|
+
);
|
|
2118
|
+
if (!match) return null;
|
|
2119
|
+
return {
|
|
2120
|
+
r: Math.round(Number(match[1])),
|
|
2121
|
+
g: Math.round(Number(match[2])),
|
|
2122
|
+
b: Math.round(Number(match[3])),
|
|
2123
|
+
a: match[4] === undefined ? 1 : clamp01(Number(match[4])),
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function clamp01(value: number): number {
|
|
2128
|
+
if (!Number.isFinite(value)) return 1;
|
|
2129
|
+
return Math.max(0, Math.min(1, value));
|
|
1123
2130
|
}
|
|
1124
2131
|
|
|
1125
2132
|
function normalizeDimension(
|
|
@@ -1278,6 +2285,81 @@ function GradientText({
|
|
|
1278
2285
|
);
|
|
1279
2286
|
}
|
|
1280
2287
|
|
|
2288
|
+
type TextInputStrokeStyle = 'solid' | 'dashed' | 'dotted';
|
|
2289
|
+
|
|
2290
|
+
type NormalizedTextInputStroke = {
|
|
2291
|
+
width: number;
|
|
2292
|
+
color: string;
|
|
2293
|
+
radius: number;
|
|
2294
|
+
style: TextInputStrokeStyle;
|
|
2295
|
+
};
|
|
2296
|
+
|
|
2297
|
+
function resolveTextInputBackgroundColor(properties: Record<string, any>) {
|
|
2298
|
+
const canonical = properties.backgroundColor;
|
|
2299
|
+
if (typeof canonical === 'string' && canonical.trim().length > 0) {
|
|
2300
|
+
return parseColor(canonical);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const legacyBackground = properties.background;
|
|
2304
|
+
if (
|
|
2305
|
+
legacyBackground &&
|
|
2306
|
+
typeof legacyBackground === 'object' &&
|
|
2307
|
+
!Array.isArray(legacyBackground)
|
|
2308
|
+
) {
|
|
2309
|
+
const type = String(legacyBackground.type ?? '').toLowerCase();
|
|
2310
|
+
if (type === 'color' && typeof legacyBackground.color === 'string') {
|
|
2311
|
+
return parseColor(legacyBackground.color);
|
|
2312
|
+
}
|
|
2313
|
+
if (type === 'gradient' && Array.isArray(legacyBackground.colors)) {
|
|
2314
|
+
const firstGradientColor = legacyBackground.colors.find(
|
|
2315
|
+
(value: unknown) => typeof value === 'string' && value.trim().length > 0
|
|
2316
|
+
);
|
|
2317
|
+
if (typeof firstGradientColor === 'string') {
|
|
2318
|
+
return parseColor(firstGradientColor);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
} else if (
|
|
2322
|
+
typeof legacyBackground === 'string' &&
|
|
2323
|
+
legacyBackground.trim().length > 0
|
|
2324
|
+
) {
|
|
2325
|
+
return parseColor(legacyBackground);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
return parseColor('0xFFF0F0F0');
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function normalizeTextInputStroke(
|
|
2332
|
+
properties: Record<string, any>
|
|
2333
|
+
): NormalizedTextInputStroke {
|
|
2334
|
+
const parsedWidth = Number(properties.strokeWidth ?? 0);
|
|
2335
|
+
const width =
|
|
2336
|
+
Number.isFinite(parsedWidth) && parsedWidth > 0 ? parsedWidth : 0;
|
|
2337
|
+
const enabled =
|
|
2338
|
+
typeof properties.strokeEnabled === 'boolean'
|
|
2339
|
+
? properties.strokeEnabled
|
|
2340
|
+
: width > 0;
|
|
2341
|
+
const normalizedWidth = enabled ? width : 0;
|
|
2342
|
+
const color =
|
|
2343
|
+
normalizedWidth > 0
|
|
2344
|
+
? parseColor(properties.strokeColor ?? '#000000')
|
|
2345
|
+
: 'transparent';
|
|
2346
|
+
const parsedRadius = Number(
|
|
2347
|
+
properties.strokeRadius ?? properties.borderRadius ?? 8
|
|
2348
|
+
);
|
|
2349
|
+
const radius = Number.isFinite(parsedRadius) ? Math.max(0, parsedRadius) : 8;
|
|
2350
|
+
const style: TextInputStrokeStyle =
|
|
2351
|
+
properties.strokeStyle === 'dashed' || properties.strokeStyle === 'dotted'
|
|
2352
|
+
? properties.strokeStyle
|
|
2353
|
+
: 'solid';
|
|
2354
|
+
|
|
2355
|
+
return {
|
|
2356
|
+
width: normalizedWidth,
|
|
2357
|
+
color,
|
|
2358
|
+
radius,
|
|
2359
|
+
style,
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
|
|
1281
2363
|
function DynamicInput({
|
|
1282
2364
|
initialValue,
|
|
1283
2365
|
properties,
|
|
@@ -1315,10 +2397,12 @@ function DynamicInput({
|
|
|
1315
2397
|
);
|
|
1316
2398
|
const inputStyle = getTextStyle({ ...properties, height: null }, 'textColor');
|
|
1317
2399
|
|
|
1318
|
-
const backgroundColor =
|
|
1319
|
-
|
|
1320
|
-
);
|
|
1321
|
-
const
|
|
2400
|
+
const backgroundColor = resolveTextInputBackgroundColor(properties);
|
|
2401
|
+
const stroke = normalizeTextInputStroke(properties);
|
|
2402
|
+
const parsedPadding = Number(properties.padding ?? 12);
|
|
2403
|
+
const padding = Number.isFinite(parsedPadding)
|
|
2404
|
+
? Math.max(0, parsedPadding)
|
|
2405
|
+
: 12;
|
|
1322
2406
|
|
|
1323
2407
|
return (
|
|
1324
2408
|
<View>
|
|
@@ -1326,9 +2410,11 @@ function DynamicInput({
|
|
|
1326
2410
|
<View
|
|
1327
2411
|
style={{
|
|
1328
2412
|
backgroundColor,
|
|
1329
|
-
borderRadius,
|
|
1330
|
-
|
|
1331
|
-
|
|
2413
|
+
borderRadius: stroke.radius,
|
|
2414
|
+
borderStyle: stroke.width > 0 ? stroke.style : undefined,
|
|
2415
|
+
borderWidth: stroke.width,
|
|
2416
|
+
borderColor: stroke.color,
|
|
2417
|
+
padding,
|
|
1332
2418
|
}}
|
|
1333
2419
|
>
|
|
1334
2420
|
{mask ? (
|
|
@@ -1498,7 +2584,7 @@ function SelectionList({
|
|
|
1498
2584
|
|
|
1499
2585
|
if (layout === 'row') {
|
|
1500
2586
|
return (
|
|
1501
|
-
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
2587
|
+
<View style={{ width: '100%', flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
1502
2588
|
{options.map((option, index) => (
|
|
1503
2589
|
<View
|
|
1504
2590
|
key={`row-option-${index}`}
|
|
@@ -1515,7 +2601,7 @@ function SelectionList({
|
|
|
1515
2601
|
const crossAxisCount = Number(properties.gridCrossAxisCount ?? 2);
|
|
1516
2602
|
const aspectRatio = Number(properties.gridAspectRatio ?? 1);
|
|
1517
2603
|
return (
|
|
1518
|
-
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
2604
|
+
<View style={{ width: '100%', flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
1519
2605
|
{options.map((option, index) => (
|
|
1520
2606
|
<View
|
|
1521
2607
|
key={`grid-option-${index}`}
|
|
@@ -1534,9 +2620,12 @@ function SelectionList({
|
|
|
1534
2620
|
}
|
|
1535
2621
|
|
|
1536
2622
|
return (
|
|
1537
|
-
<View>
|
|
2623
|
+
<View style={{ width: '100%' }}>
|
|
1538
2624
|
{options.map((option, index) => (
|
|
1539
|
-
<View
|
|
2625
|
+
<View
|
|
2626
|
+
key={`column-option-${index}`}
|
|
2627
|
+
style={{ width: '100%', marginBottom: spacing }}
|
|
2628
|
+
>
|
|
1540
2629
|
{renderItem(option, index)}
|
|
1541
2630
|
</View>
|
|
1542
2631
|
))}
|
|
@@ -1653,7 +2742,11 @@ function WheelPicker({
|
|
|
1653
2742
|
const parsed = Number(raw);
|
|
1654
2743
|
if (!Number.isFinite(parsed)) return 1;
|
|
1655
2744
|
return Math.max(1, Math.floor(parsed));
|
|
1656
|
-
}, [
|
|
2745
|
+
}, [
|
|
2746
|
+
properties.startIndex,
|
|
2747
|
+
properties.startItemIndex,
|
|
2748
|
+
properties.start_index,
|
|
2749
|
+
]);
|
|
1657
2750
|
|
|
1658
2751
|
const clampWheelIndex = React.useCallback(
|
|
1659
2752
|
(index: number) =>
|
|
@@ -1697,7 +2790,14 @@ function WheelPicker({
|
|
|
1697
2790
|
}
|
|
1698
2791
|
}
|
|
1699
2792
|
},
|
|
1700
|
-
[
|
|
2793
|
+
[
|
|
2794
|
+
clampWheelIndex,
|
|
2795
|
+
onAction,
|
|
2796
|
+
onChanged,
|
|
2797
|
+
properties.autoGoNext,
|
|
2798
|
+
resolvedOptions,
|
|
2799
|
+
triggerLightHaptic,
|
|
2800
|
+
]
|
|
1701
2801
|
);
|
|
1702
2802
|
|
|
1703
2803
|
React.useEffect(() => {
|
|
@@ -1861,10 +2961,19 @@ function WheelPicker({
|
|
|
1861
2961
|
const overlayBackgroundColor = parseColor(
|
|
1862
2962
|
properties.wheelCenterBackgroundColor ?? '#24FFFFFF'
|
|
1863
2963
|
);
|
|
2964
|
+
const wheelEdgeFadeOpacity = Math.max(
|
|
2965
|
+
0,
|
|
2966
|
+
Math.min(1, Number(properties.wheelEdgeFadeOpacity ?? 0))
|
|
2967
|
+
);
|
|
2968
|
+
const wheelEdgeFadeColor = parseColor(
|
|
2969
|
+
properties.wheelEdgeFadeColor ?? '#FFFFFF'
|
|
2970
|
+
);
|
|
1864
2971
|
|
|
1865
2972
|
const handleWheelMomentumEnd = (event: any) => {
|
|
1866
2973
|
const offsetY = Number(event?.nativeEvent?.contentOffset?.y ?? 0);
|
|
1867
|
-
const settledIndex = clampWheelIndex(
|
|
2974
|
+
const settledIndex = clampWheelIndex(
|
|
2975
|
+
Math.round(offsetY / wheelRowStride)
|
|
2976
|
+
);
|
|
1868
2977
|
const targetOffset = settledIndex * wheelRowStride;
|
|
1869
2978
|
if (Math.abs(targetOffset - offsetY) > 0.5) {
|
|
1870
2979
|
wheelScrollRef.current?.scrollTo({
|
|
@@ -1937,7 +3046,12 @@ function WheelPicker({
|
|
|
1937
3046
|
],
|
|
1938
3047
|
}}
|
|
1939
3048
|
>
|
|
1940
|
-
<View
|
|
3049
|
+
<View
|
|
3050
|
+
style={{
|
|
3051
|
+
minHeight: wheelItemHeight,
|
|
3052
|
+
justifyContent: 'center',
|
|
3053
|
+
}}
|
|
3054
|
+
>
|
|
1941
3055
|
{renderItem(option, index)}
|
|
1942
3056
|
</View>
|
|
1943
3057
|
</Animated.View>
|
|
@@ -1959,22 +3073,38 @@ function WheelPicker({
|
|
|
1959
3073
|
backgroundColor: overlayBackgroundColor,
|
|
1960
3074
|
}}
|
|
1961
3075
|
/>
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
3076
|
+
{wheelEdgeFadeOpacity > 0 && centerPadding > 0 ? (
|
|
3077
|
+
<LinearGradient
|
|
3078
|
+
pointerEvents="none"
|
|
3079
|
+
colors={[
|
|
3080
|
+
withOpacity(wheelEdgeFadeColor, wheelEdgeFadeOpacity),
|
|
3081
|
+
withOpacity(wheelEdgeFadeColor, 0),
|
|
3082
|
+
]}
|
|
3083
|
+
style={{
|
|
3084
|
+
position: 'absolute',
|
|
3085
|
+
top: 0,
|
|
3086
|
+
left: 0,
|
|
3087
|
+
right: 0,
|
|
3088
|
+
height: centerPadding,
|
|
3089
|
+
}}
|
|
3090
|
+
/>
|
|
3091
|
+
) : null}
|
|
3092
|
+
{wheelEdgeFadeOpacity > 0 && centerPadding > 0 ? (
|
|
3093
|
+
<LinearGradient
|
|
3094
|
+
pointerEvents="none"
|
|
3095
|
+
colors={[
|
|
3096
|
+
withOpacity(wheelEdgeFadeColor, 0),
|
|
3097
|
+
withOpacity(wheelEdgeFadeColor, wheelEdgeFadeOpacity),
|
|
3098
|
+
]}
|
|
3099
|
+
style={{
|
|
3100
|
+
position: 'absolute',
|
|
3101
|
+
bottom: 0,
|
|
3102
|
+
left: 0,
|
|
3103
|
+
right: 0,
|
|
3104
|
+
height: centerPadding,
|
|
3105
|
+
}}
|
|
3106
|
+
/>
|
|
3107
|
+
) : null}
|
|
1978
3108
|
</View>
|
|
1979
3109
|
);
|
|
1980
3110
|
}
|
|
@@ -1982,7 +3112,10 @@ function WheelPicker({
|
|
|
1982
3112
|
return (
|
|
1983
3113
|
<View>
|
|
1984
3114
|
{resolvedOptions.map((option, index) => (
|
|
1985
|
-
<View
|
|
3115
|
+
<View
|
|
3116
|
+
key={`wheel-list-option-${index}`}
|
|
3117
|
+
style={{ marginBottom: spacing }}
|
|
3118
|
+
>
|
|
1986
3119
|
{renderItem(option, index)}
|
|
1987
3120
|
</View>
|
|
1988
3121
|
))}
|
|
@@ -2066,7 +3199,10 @@ function buildWheelTemplateBaseContext(
|
|
|
2066
3199
|
properties.start ?? properties.itemStart,
|
|
2067
3200
|
context
|
|
2068
3201
|
);
|
|
2069
|
-
const step = resolveNumericValue(
|
|
3202
|
+
const step = resolveNumericValue(
|
|
3203
|
+
properties.step ?? properties.itemStep,
|
|
3204
|
+
context
|
|
3205
|
+
);
|
|
2070
3206
|
|
|
2071
3207
|
if (start !== null) context.start = start;
|
|
2072
3208
|
if (step !== null) context.step = step;
|
|
@@ -2527,6 +3663,14 @@ function RadarChart({
|
|
|
2527
3663
|
'color',
|
|
2528
3664
|
'0xFF424242'
|
|
2529
3665
|
);
|
|
3666
|
+
const resolvedAxisLabelFontSize = resolveRadarAxisLabelFontSize(
|
|
3667
|
+
axisLabelStyle.fontSize
|
|
3668
|
+
);
|
|
3669
|
+
const autoFitLabels = properties.autoFitLabels !== false;
|
|
3670
|
+
const resolvedLabelOffset = resolveRadarLabelOffset(
|
|
3671
|
+
properties.labelOffset,
|
|
3672
|
+
resolvedAxisLabelFontSize
|
|
3673
|
+
);
|
|
2530
3674
|
const showLegend = properties.showLegend !== false;
|
|
2531
3675
|
const legendPosition = (properties.legendPosition ?? 'bottom').toLowerCase();
|
|
2532
3676
|
const legendSpacing = Number(properties.legendSpacing ?? 12);
|
|
@@ -2536,14 +3680,21 @@ function RadarChart({
|
|
|
2536
3680
|
'0xFF212121'
|
|
2537
3681
|
);
|
|
2538
3682
|
const shape = (properties.shape ?? 'polygon').toLowerCase();
|
|
2539
|
-
|
|
2540
|
-
const
|
|
2541
|
-
const radius =
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
3683
|
+
const innerWidth = Math.max(1, width - padding.left - padding.right);
|
|
3684
|
+
const innerHeight = Math.max(1, height - padding.top - padding.bottom);
|
|
3685
|
+
const radius = calculateRadarChartRadius({
|
|
3686
|
+
width: innerWidth,
|
|
3687
|
+
height: innerHeight,
|
|
3688
|
+
padding: { left: 0, right: 0, top: 0, bottom: 0 },
|
|
3689
|
+
axisLabels: axes.map((axis) => axis.label),
|
|
3690
|
+
fontSize: resolvedAxisLabelFontSize,
|
|
3691
|
+
autoFitLabels,
|
|
3692
|
+
labelOffset: resolvedLabelOffset,
|
|
3693
|
+
});
|
|
3694
|
+
const center = {
|
|
3695
|
+
x: padding.left + innerWidth / 2,
|
|
3696
|
+
y: padding.top + innerHeight / 2,
|
|
3697
|
+
};
|
|
2547
3698
|
|
|
2548
3699
|
const axisAngle = (Math.PI * 2) / axes.length;
|
|
2549
3700
|
|
|
@@ -2562,7 +3713,10 @@ function RadarChart({
|
|
|
2562
3713
|
const points = dataset.values.map((value, index) => {
|
|
2563
3714
|
const axis = normalizedAxes[index];
|
|
2564
3715
|
const maxValue = axis?.maxValue ?? 0;
|
|
2565
|
-
const
|
|
3716
|
+
const normalizedRatio = maxValue > 0 ? value / maxValue : 0;
|
|
3717
|
+
const ratio =
|
|
3718
|
+
Math.max(0, Math.min(1, normalizedRatio)) *
|
|
3719
|
+
Math.max(0, Math.min(1, lineProgress));
|
|
2566
3720
|
const angle = index * axisAngle - Math.PI / 2;
|
|
2567
3721
|
const x = center.x + radius * ratio * Math.cos(angle);
|
|
2568
3722
|
const y = center.y + radius * ratio * Math.sin(angle);
|
|
@@ -2571,16 +3725,24 @@ function RadarChart({
|
|
|
2571
3725
|
return { dataset, points };
|
|
2572
3726
|
});
|
|
2573
3727
|
|
|
3728
|
+
const isVerticalLegend =
|
|
3729
|
+
legendPosition === 'left' || legendPosition === 'right';
|
|
2574
3730
|
const legend = (
|
|
2575
|
-
<View
|
|
3731
|
+
<View
|
|
3732
|
+
style={{
|
|
3733
|
+
flexDirection: isVerticalLegend ? 'column' : 'row',
|
|
3734
|
+
flexWrap: isVerticalLegend ? 'nowrap' : 'wrap',
|
|
3735
|
+
alignItems: isVerticalLegend ? 'flex-start' : 'center',
|
|
3736
|
+
}}
|
|
3737
|
+
>
|
|
2576
3738
|
{datasets.map((dataset, index) => (
|
|
2577
3739
|
<View
|
|
2578
3740
|
key={`legend-${index}`}
|
|
2579
3741
|
style={{
|
|
2580
3742
|
flexDirection: 'row',
|
|
2581
3743
|
alignItems: 'center',
|
|
2582
|
-
marginRight: legendSpacing,
|
|
2583
|
-
marginBottom: 6,
|
|
3744
|
+
marginRight: isVerticalLegend ? 0 : legendSpacing,
|
|
3745
|
+
marginBottom: isVerticalLegend ? legendSpacing : 6,
|
|
2584
3746
|
}}
|
|
2585
3747
|
>
|
|
2586
3748
|
<View
|
|
@@ -2588,11 +3750,15 @@ function RadarChart({
|
|
|
2588
3750
|
width: 12,
|
|
2589
3751
|
height: 12,
|
|
2590
3752
|
borderRadius: 6,
|
|
2591
|
-
backgroundColor: dataset.borderColor,
|
|
3753
|
+
backgroundColor: dataset.fillColor ?? dataset.borderColor,
|
|
3754
|
+
borderWidth: 1,
|
|
3755
|
+
borderColor: dataset.borderColor,
|
|
2592
3756
|
marginRight: 6,
|
|
2593
3757
|
}}
|
|
2594
3758
|
/>
|
|
2595
|
-
<Text style={legendStyle}>
|
|
3759
|
+
<Text numberOfLines={1} style={legendStyle}>
|
|
3760
|
+
{dataset.label}
|
|
3761
|
+
</Text>
|
|
2596
3762
|
</View>
|
|
2597
3763
|
))}
|
|
2598
3764
|
</View>
|
|
@@ -2601,12 +3767,10 @@ function RadarChart({
|
|
|
2601
3767
|
const chart = (
|
|
2602
3768
|
<Animated.View
|
|
2603
3769
|
style={{
|
|
2604
|
-
width: normalizedWidth ??
|
|
3770
|
+
width: isVerticalLegend ? width : normalizedWidth ?? '100%',
|
|
3771
|
+
maxWidth: '100%',
|
|
3772
|
+
flexShrink: 1,
|
|
2605
3773
|
height,
|
|
2606
|
-
paddingLeft: padding.left,
|
|
2607
|
-
paddingRight: padding.right,
|
|
2608
|
-
paddingTop: padding.top,
|
|
2609
|
-
paddingBottom: padding.bottom,
|
|
2610
3774
|
opacity: reveal,
|
|
2611
3775
|
}}
|
|
2612
3776
|
onLayout={(event) => {
|
|
@@ -2657,14 +3821,16 @@ function RadarChart({
|
|
|
2657
3821
|
})}
|
|
2658
3822
|
{axes.map((axis, index) => {
|
|
2659
3823
|
const angle = index * axisAngle - Math.PI / 2;
|
|
2660
|
-
const labelX =
|
|
2661
|
-
|
|
3824
|
+
const labelX =
|
|
3825
|
+
center.x + (radius + resolvedLabelOffset) * Math.cos(angle);
|
|
3826
|
+
const labelY =
|
|
3827
|
+
center.y + (radius + resolvedLabelOffset) * Math.sin(angle);
|
|
2662
3828
|
return (
|
|
2663
3829
|
<SvgText
|
|
2664
3830
|
key={`label-${index}`}
|
|
2665
3831
|
x={labelX}
|
|
2666
3832
|
y={labelY}
|
|
2667
|
-
fontSize={
|
|
3833
|
+
fontSize={resolvedAxisLabelFontSize}
|
|
2668
3834
|
fontWeight={axisLabelStyle.fontWeight}
|
|
2669
3835
|
fill={axisLabelStyle.color}
|
|
2670
3836
|
textAnchor="middle"
|
|
@@ -2673,21 +3839,20 @@ function RadarChart({
|
|
|
2673
3839
|
</SvgText>
|
|
2674
3840
|
);
|
|
2675
3841
|
})}
|
|
2676
|
-
{datasetPolygons.map((entry, index) =>
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
entry.dataset.
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
))}
|
|
3842
|
+
{datasetPolygons.map((entry, index) => {
|
|
3843
|
+
const strokePattern = resolveRadarStrokePattern(entry.dataset);
|
|
3844
|
+
return (
|
|
3845
|
+
<Polygon
|
|
3846
|
+
key={`dataset-${index}`}
|
|
3847
|
+
points={entry.points.join(' ')}
|
|
3848
|
+
stroke={entry.dataset.borderColor}
|
|
3849
|
+
strokeWidth={entry.dataset.borderWidth}
|
|
3850
|
+
fill={entry.dataset.fillColor ?? 'transparent'}
|
|
3851
|
+
strokeDasharray={strokePattern.strokeDasharray}
|
|
3852
|
+
strokeLinecap={strokePattern.strokeLinecap}
|
|
3853
|
+
/>
|
|
3854
|
+
);
|
|
3855
|
+
})}
|
|
2691
3856
|
{datasetPolygons.map((entry, datasetIndex) =>
|
|
2692
3857
|
entry.dataset.showPoints
|
|
2693
3858
|
? entry.points.map((point, pointIndex) => {
|
|
@@ -2709,22 +3874,15 @@ function RadarChart({
|
|
|
2709
3874
|
</Animated.View>
|
|
2710
3875
|
);
|
|
2711
3876
|
|
|
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;
|
|
3877
|
+
if (!showLegend) {
|
|
3878
|
+
return <View style={{ backgroundColor }}>{chart}</View>;
|
|
3879
|
+
}
|
|
2722
3880
|
|
|
2723
3881
|
if (legendPosition === 'top') {
|
|
2724
3882
|
return (
|
|
2725
|
-
<View>
|
|
3883
|
+
<View style={{ backgroundColor, alignItems: 'center' }}>
|
|
2726
3884
|
{legend}
|
|
2727
|
-
<View style={{ height:
|
|
3885
|
+
<View style={{ height: legendSpacing }} />
|
|
2728
3886
|
{chart}
|
|
2729
3887
|
</View>
|
|
2730
3888
|
);
|
|
@@ -2732,9 +3890,11 @@ function RadarChart({
|
|
|
2732
3890
|
|
|
2733
3891
|
if (legendPosition === 'left') {
|
|
2734
3892
|
return (
|
|
2735
|
-
<View
|
|
3893
|
+
<View
|
|
3894
|
+
style={{ backgroundColor, flexDirection: 'row', alignItems: 'center' }}
|
|
3895
|
+
>
|
|
2736
3896
|
{legend}
|
|
2737
|
-
<View style={{ width:
|
|
3897
|
+
<View style={{ width: legendSpacing }} />
|
|
2738
3898
|
{chart}
|
|
2739
3899
|
</View>
|
|
2740
3900
|
);
|
|
@@ -2742,32 +3902,262 @@ function RadarChart({
|
|
|
2742
3902
|
|
|
2743
3903
|
if (legendPosition === 'right') {
|
|
2744
3904
|
return (
|
|
2745
|
-
<View
|
|
3905
|
+
<View
|
|
3906
|
+
style={{ backgroundColor, flexDirection: 'row', alignItems: 'center' }}
|
|
3907
|
+
>
|
|
2746
3908
|
{chart}
|
|
2747
|
-
<View style={{ width:
|
|
3909
|
+
<View style={{ width: legendSpacing }} />
|
|
2748
3910
|
{legend}
|
|
2749
3911
|
</View>
|
|
2750
3912
|
);
|
|
2751
3913
|
}
|
|
2752
3914
|
|
|
2753
|
-
return
|
|
3915
|
+
return (
|
|
3916
|
+
<View style={{ backgroundColor, alignItems: 'center' }}>
|
|
3917
|
+
{chart}
|
|
3918
|
+
<View style={{ height: legendSpacing }} />
|
|
3919
|
+
{legend}
|
|
3920
|
+
</View>
|
|
3921
|
+
);
|
|
2754
3922
|
}
|
|
2755
3923
|
|
|
2756
3924
|
export type RadarAxis = { label: string; maxValue: number };
|
|
3925
|
+
export type RadarBorderStyle = 'solid' | 'dotted' | 'dashed';
|
|
2757
3926
|
|
|
2758
3927
|
export type RadarDataset = {
|
|
2759
3928
|
label: string;
|
|
2760
3929
|
values: number[];
|
|
2761
3930
|
borderColor: string;
|
|
2762
3931
|
borderWidth: number;
|
|
2763
|
-
borderStyle:
|
|
3932
|
+
borderStyle: RadarBorderStyle;
|
|
2764
3933
|
dashArray?: number[];
|
|
3934
|
+
dashLength?: number;
|
|
2765
3935
|
showPoints: boolean;
|
|
2766
3936
|
pointRadius: number;
|
|
2767
3937
|
pointColor: string;
|
|
2768
3938
|
fillColor?: string;
|
|
2769
3939
|
};
|
|
2770
3940
|
|
|
3941
|
+
type RadarStrokePattern = {
|
|
3942
|
+
strokeDasharray?: string;
|
|
3943
|
+
strokeLinecap?: 'butt' | 'round';
|
|
3944
|
+
};
|
|
3945
|
+
|
|
3946
|
+
const RADAR_DASH_DEFAULT_LENGTH = 8;
|
|
3947
|
+
const RADAR_DASH_GAP_FACTOR = 0.6;
|
|
3948
|
+
const RADAR_DOT_DASH_LENGTH = 0.001;
|
|
3949
|
+
const RADAR_DOT_GAP_FACTOR = 2.4;
|
|
3950
|
+
const RADAR_DOT_MIN_GAP = 3;
|
|
3951
|
+
const radarDashConflictWarnings = new Set<string>();
|
|
3952
|
+
|
|
3953
|
+
function isDevelopmentEnvironment(): boolean {
|
|
3954
|
+
return process.env.NODE_ENV !== 'production';
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
function warnRadarDashConflictOnce(label: string) {
|
|
3958
|
+
if (!isDevelopmentEnvironment()) return;
|
|
3959
|
+
if (radarDashConflictWarnings.has(label)) return;
|
|
3960
|
+
radarDashConflictWarnings.add(label);
|
|
3961
|
+
// Keep API backward-compatible but deterministic when both flags are passed.
|
|
3962
|
+
console.warn(
|
|
3963
|
+
`[RadarChart] Dataset "${label}" received both dotted=true and dashed=true. dashed takes precedence.`
|
|
3964
|
+
);
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
function resolveRadarBorderStyle(
|
|
3968
|
+
dataset: Record<string, any>,
|
|
3969
|
+
fallbackLabel: string
|
|
3970
|
+
): RadarBorderStyle {
|
|
3971
|
+
const dotted = dataset.dotted === true;
|
|
3972
|
+
const dashed = dataset.dashed === true;
|
|
3973
|
+
|
|
3974
|
+
if (dotted && dashed) {
|
|
3975
|
+
warnRadarDashConflictOnce(fallbackLabel);
|
|
3976
|
+
return 'dashed';
|
|
3977
|
+
}
|
|
3978
|
+
if (dashed) return 'dashed';
|
|
3979
|
+
if (dotted) return 'dotted';
|
|
3980
|
+
|
|
3981
|
+
const borderStyle = (dataset.borderStyle ?? 'solid').toString().toLowerCase();
|
|
3982
|
+
if (borderStyle === 'dotted' || borderStyle === 'dashed') {
|
|
3983
|
+
return borderStyle;
|
|
3984
|
+
}
|
|
3985
|
+
return 'solid';
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
function resolveRadarStrokePattern(dataset: RadarDataset): RadarStrokePattern {
|
|
3989
|
+
if (dataset.borderStyle === 'dotted') {
|
|
3990
|
+
// Tiny dash + round caps renders circular dots more reliably than short dashes.
|
|
3991
|
+
const gap = Math.max(
|
|
3992
|
+
RADAR_DOT_MIN_GAP,
|
|
3993
|
+
dataset.borderWidth * RADAR_DOT_GAP_FACTOR
|
|
3994
|
+
);
|
|
3995
|
+
return {
|
|
3996
|
+
strokeDasharray: `${RADAR_DOT_DASH_LENGTH} ${gap}`,
|
|
3997
|
+
strokeLinecap: 'round',
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
if (dataset.borderStyle === 'dashed') {
|
|
4002
|
+
if (Array.isArray(dataset.dashArray) && dataset.dashArray.length > 0) {
|
|
4003
|
+
const dash = Math.max(
|
|
4004
|
+
1,
|
|
4005
|
+
dataset.dashArray[0] ?? RADAR_DASH_DEFAULT_LENGTH
|
|
4006
|
+
);
|
|
4007
|
+
const gap = Math.max(
|
|
4008
|
+
1,
|
|
4009
|
+
dataset.dashArray[1] ?? dash * RADAR_DASH_GAP_FACTOR
|
|
4010
|
+
);
|
|
4011
|
+
return {
|
|
4012
|
+
strokeDasharray: `${dash} ${gap}`,
|
|
4013
|
+
strokeLinecap: 'butt',
|
|
4014
|
+
};
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
const dashLength =
|
|
4018
|
+
typeof dataset.dashLength === 'number' &&
|
|
4019
|
+
Number.isFinite(dataset.dashLength) &&
|
|
4020
|
+
dataset.dashLength > 0
|
|
4021
|
+
? dataset.dashLength
|
|
4022
|
+
: RADAR_DASH_DEFAULT_LENGTH;
|
|
4023
|
+
const gap = Math.max(2, dashLength * RADAR_DASH_GAP_FACTOR);
|
|
4024
|
+
return {
|
|
4025
|
+
strokeDasharray: `${dashLength} ${gap}`,
|
|
4026
|
+
strokeLinecap: 'butt',
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
return {};
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
export const RADAR_LABEL_ESTIMATION = {
|
|
4034
|
+
minRadius: 10,
|
|
4035
|
+
safeEdge: 4,
|
|
4036
|
+
minLabelWidthFactor: 1.2,
|
|
4037
|
+
avgCharWidthFactor: 0.58,
|
|
4038
|
+
lineHeightFactor: 1.2,
|
|
4039
|
+
defaultFontSize: 14,
|
|
4040
|
+
minLabelOffset: 20,
|
|
4041
|
+
labelOffsetFactor: 1.5,
|
|
4042
|
+
} as const;
|
|
4043
|
+
|
|
4044
|
+
export function resolveRadarAxisLabelFontSize(value: unknown): number {
|
|
4045
|
+
const parsed = Number(value);
|
|
4046
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
4047
|
+
return RADAR_LABEL_ESTIMATION.defaultFontSize;
|
|
4048
|
+
}
|
|
4049
|
+
return parsed;
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
export function resolveRadarLabelOffset(
|
|
4053
|
+
value: unknown,
|
|
4054
|
+
fontSize: number
|
|
4055
|
+
): number {
|
|
4056
|
+
const parsed = Number(value);
|
|
4057
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
4058
|
+
return parsed;
|
|
4059
|
+
}
|
|
4060
|
+
return Math.max(
|
|
4061
|
+
RADAR_LABEL_ESTIMATION.minLabelOffset,
|
|
4062
|
+
fontSize * RADAR_LABEL_ESTIMATION.labelOffsetFactor
|
|
4063
|
+
);
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
export function estimateRadarLabelSize(
|
|
4067
|
+
label: string,
|
|
4068
|
+
fontSize: number
|
|
4069
|
+
): {
|
|
4070
|
+
width: number;
|
|
4071
|
+
height: number;
|
|
4072
|
+
} {
|
|
4073
|
+
const safeLabel = `${label ?? ''}`;
|
|
4074
|
+
const width = Math.max(
|
|
4075
|
+
safeLabel.length * fontSize * RADAR_LABEL_ESTIMATION.avgCharWidthFactor,
|
|
4076
|
+
fontSize * RADAR_LABEL_ESTIMATION.minLabelWidthFactor
|
|
4077
|
+
);
|
|
4078
|
+
const height = fontSize * RADAR_LABEL_ESTIMATION.lineHeightFactor;
|
|
4079
|
+
return { width, height };
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
export function calculateRadarChartRadius({
|
|
4083
|
+
width,
|
|
4084
|
+
height,
|
|
4085
|
+
padding,
|
|
4086
|
+
axisLabels,
|
|
4087
|
+
fontSize,
|
|
4088
|
+
autoFitLabels = true,
|
|
4089
|
+
labelOffset,
|
|
4090
|
+
}: {
|
|
4091
|
+
width: number;
|
|
4092
|
+
height: number;
|
|
4093
|
+
padding: { left: number; right: number; top: number; bottom: number };
|
|
4094
|
+
axisLabels: string[];
|
|
4095
|
+
fontSize: number;
|
|
4096
|
+
autoFitLabels?: boolean;
|
|
4097
|
+
labelOffset: number;
|
|
4098
|
+
}): number {
|
|
4099
|
+
const center = { x: width / 2, y: height / 2 };
|
|
4100
|
+
const baseRadius = Math.max(
|
|
4101
|
+
Math.min(width, height) / 2 -
|
|
4102
|
+
Math.max(padding.left, padding.right, padding.top, padding.bottom),
|
|
4103
|
+
RADAR_LABEL_ESTIMATION.minRadius
|
|
4104
|
+
);
|
|
4105
|
+
if (!autoFitLabels || axisLabels.length === 0) {
|
|
4106
|
+
return baseRadius;
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
const angleStep = (Math.PI * 2) / axisLabels.length;
|
|
4110
|
+
let maxAllowedRadius = baseRadius;
|
|
4111
|
+
|
|
4112
|
+
for (let index = 0; index < axisLabels.length; index += 1) {
|
|
4113
|
+
const { width: labelWidth, height: labelHeight } = estimateRadarLabelSize(
|
|
4114
|
+
axisLabels[index] ?? '',
|
|
4115
|
+
fontSize
|
|
4116
|
+
);
|
|
4117
|
+
const halfWidth = labelWidth / 2;
|
|
4118
|
+
const halfHeight = labelHeight / 2;
|
|
4119
|
+
const angle = index * angleStep - Math.PI / 2;
|
|
4120
|
+
const dx = Math.cos(angle);
|
|
4121
|
+
const dy = Math.sin(angle);
|
|
4122
|
+
|
|
4123
|
+
// Solve per-axis line constraints so estimated label bounds stay inside.
|
|
4124
|
+
if (dx > 0.0001) {
|
|
4125
|
+
maxAllowedRadius = Math.min(
|
|
4126
|
+
maxAllowedRadius,
|
|
4127
|
+
(width - RADAR_LABEL_ESTIMATION.safeEdge - halfWidth - center.x) / dx -
|
|
4128
|
+
labelOffset
|
|
4129
|
+
);
|
|
4130
|
+
} else if (dx < -0.0001) {
|
|
4131
|
+
maxAllowedRadius = Math.min(
|
|
4132
|
+
maxAllowedRadius,
|
|
4133
|
+
(RADAR_LABEL_ESTIMATION.safeEdge + halfWidth - center.x) / dx -
|
|
4134
|
+
labelOffset
|
|
4135
|
+
);
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
if (dy > 0.0001) {
|
|
4139
|
+
maxAllowedRadius = Math.min(
|
|
4140
|
+
maxAllowedRadius,
|
|
4141
|
+
(height - RADAR_LABEL_ESTIMATION.safeEdge - halfHeight - center.y) /
|
|
4142
|
+
dy -
|
|
4143
|
+
labelOffset
|
|
4144
|
+
);
|
|
4145
|
+
} else if (dy < -0.0001) {
|
|
4146
|
+
maxAllowedRadius = Math.min(
|
|
4147
|
+
maxAllowedRadius,
|
|
4148
|
+
(RADAR_LABEL_ESTIMATION.safeEdge + halfHeight - center.y) / dy -
|
|
4149
|
+
labelOffset
|
|
4150
|
+
);
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
const boundedRadius = Math.min(maxAllowedRadius, baseRadius);
|
|
4155
|
+
if (!Number.isFinite(boundedRadius)) {
|
|
4156
|
+
return baseRadius;
|
|
4157
|
+
}
|
|
4158
|
+
return Math.max(boundedRadius, RADAR_LABEL_ESTIMATION.minRadius);
|
|
4159
|
+
}
|
|
4160
|
+
|
|
2771
4161
|
export function parseRadarAxes(rawAxes: any): RadarAxis[] {
|
|
2772
4162
|
if (!Array.isArray(rawAxes)) return [];
|
|
2773
4163
|
const axes: RadarAxis[] = [];
|
|
@@ -2776,7 +4166,9 @@ export function parseRadarAxes(rawAxes: any): RadarAxis[] {
|
|
|
2776
4166
|
const label = axis.label ?? '';
|
|
2777
4167
|
if (!label) return;
|
|
2778
4168
|
const maxValue = resolveNumericValue(axis.maxValue, {}) ?? 100;
|
|
2779
|
-
|
|
4169
|
+
const normalizedMaxValue =
|
|
4170
|
+
Number.isFinite(maxValue) && maxValue > 0 ? maxValue : 100;
|
|
4171
|
+
axes.push({ label, maxValue: normalizedMaxValue });
|
|
2780
4172
|
});
|
|
2781
4173
|
return axes;
|
|
2782
4174
|
}
|
|
@@ -2788,14 +4180,16 @@ export function parseRadarDatasets(
|
|
|
2788
4180
|
): RadarDataset[] {
|
|
2789
4181
|
if (!Array.isArray(raw) || axisLength === 0) return [];
|
|
2790
4182
|
const datasets: RadarDataset[] = [];
|
|
2791
|
-
raw.forEach((dataset) => {
|
|
4183
|
+
raw.forEach((dataset, datasetIndex) => {
|
|
2792
4184
|
if (!dataset || typeof dataset !== 'object') return;
|
|
2793
4185
|
const valuesRaw = Array.isArray(dataset.data) ? dataset.data : [];
|
|
2794
4186
|
const values: number[] = [];
|
|
2795
4187
|
for (let i = 0; i < axisLength; i += 1) {
|
|
2796
4188
|
const rawValue = i < valuesRaw.length ? valuesRaw[i] : null;
|
|
2797
|
-
const resolved = resolveNumericValue(rawValue, formData)
|
|
2798
|
-
values.push(
|
|
4189
|
+
const resolved = resolveNumericValue(rawValue, formData);
|
|
4190
|
+
values.push(
|
|
4191
|
+
typeof resolved === 'number' && Number.isFinite(resolved) ? resolved : 0
|
|
4192
|
+
);
|
|
2799
4193
|
}
|
|
2800
4194
|
|
|
2801
4195
|
const label = dataset.label ?? `Dataset ${datasets.length + 1}`;
|
|
@@ -2806,18 +4200,30 @@ export function parseRadarDatasets(
|
|
|
2806
4200
|
const pointColor = parseColor(
|
|
2807
4201
|
dataset.pointColor ?? dataset.borderColor ?? '0xFF2196F3'
|
|
2808
4202
|
);
|
|
2809
|
-
const
|
|
2810
|
-
const
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
const
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
4203
|
+
const rawBorderWidth = Number(dataset.borderWidth ?? 2);
|
|
4204
|
+
const borderWidth =
|
|
4205
|
+
Number.isFinite(rawBorderWidth) && rawBorderWidth > 0
|
|
4206
|
+
? rawBorderWidth
|
|
4207
|
+
: 2;
|
|
4208
|
+
const rawPointRadius = Number(dataset.pointRadius ?? 4);
|
|
4209
|
+
const pointRadius =
|
|
4210
|
+
Number.isFinite(rawPointRadius) && rawPointRadius >= 0
|
|
4211
|
+
? rawPointRadius
|
|
4212
|
+
: 4;
|
|
4213
|
+
const normalizedStyle = resolveRadarBorderStyle(
|
|
4214
|
+
dataset,
|
|
4215
|
+
label ?? `Dataset ${datasetIndex + 1}`
|
|
4216
|
+
);
|
|
2818
4217
|
const dashArray = Array.isArray(dataset.dashArray)
|
|
2819
|
-
? dataset.dashArray
|
|
4218
|
+
? dataset.dashArray
|
|
4219
|
+
.map((val: any) => Number(val))
|
|
4220
|
+
.filter((val: number) => Number.isFinite(val) && val > 0)
|
|
2820
4221
|
: undefined;
|
|
4222
|
+
const rawDashLength = Number(dataset.dashLength);
|
|
4223
|
+
const dashLength =
|
|
4224
|
+
Number.isFinite(rawDashLength) && rawDashLength > 0
|
|
4225
|
+
? rawDashLength
|
|
4226
|
+
: undefined;
|
|
2821
4227
|
const showPoints = dataset.showPoints !== false;
|
|
2822
4228
|
|
|
2823
4229
|
datasets.push({
|
|
@@ -2827,6 +4233,7 @@ export function parseRadarDatasets(
|
|
|
2827
4233
|
borderWidth,
|
|
2828
4234
|
borderStyle: normalizedStyle,
|
|
2829
4235
|
dashArray,
|
|
4236
|
+
dashLength,
|
|
2830
4237
|
showPoints,
|
|
2831
4238
|
pointRadius,
|
|
2832
4239
|
pointColor,
|
|
@@ -2844,7 +4251,9 @@ export function normalizeAxisMaximums(
|
|
|
2844
4251
|
let maxValue = axis.maxValue;
|
|
2845
4252
|
datasets.forEach((dataset) => {
|
|
2846
4253
|
const value = dataset.values[index];
|
|
2847
|
-
if (value !== undefined && value > maxValue)
|
|
4254
|
+
if (value !== undefined && Number.isFinite(value) && value > maxValue) {
|
|
4255
|
+
maxValue = value;
|
|
4256
|
+
}
|
|
2848
4257
|
});
|
|
2849
4258
|
return { ...axis, maxValue };
|
|
2850
4259
|
});
|
|
@@ -2854,12 +4263,15 @@ function resolveRadarWidth(
|
|
|
2854
4263
|
parsedWidth: number | string | undefined,
|
|
2855
4264
|
measuredWidth: number
|
|
2856
4265
|
): number {
|
|
4266
|
+
if (measuredWidth > 0) {
|
|
4267
|
+
if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
|
|
4268
|
+
return Math.max(1, Math.min(parsedWidth, measuredWidth));
|
|
4269
|
+
}
|
|
4270
|
+
return Math.max(1, measuredWidth);
|
|
4271
|
+
}
|
|
2857
4272
|
if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
|
|
2858
4273
|
return parsedWidth;
|
|
2859
4274
|
}
|
|
2860
|
-
if (measuredWidth > 0) {
|
|
2861
|
-
return measuredWidth;
|
|
2862
|
-
}
|
|
2863
4275
|
return 300;
|
|
2864
4276
|
}
|
|
2865
4277
|
|