flowboard-react 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1121 -149
- 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 +1656 -196
- 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,
|
|
@@ -8,9 +9,11 @@ import {
|
|
|
8
9
|
TextInput,
|
|
9
10
|
View,
|
|
10
11
|
Image,
|
|
12
|
+
Vibration,
|
|
11
13
|
type TextStyle,
|
|
14
|
+
type ViewStyle,
|
|
12
15
|
} from 'react-native';
|
|
13
|
-
import {
|
|
16
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
14
17
|
import MaskedView from '@react-native-masked-view/masked-view';
|
|
15
18
|
import LinearGradient from 'react-native-linear-gradient';
|
|
16
19
|
import LottieView from 'lottie-react-native';
|
|
@@ -52,7 +55,7 @@ const styles = StyleSheet.create({
|
|
|
52
55
|
whiteBg: {
|
|
53
56
|
backgroundColor: '#ffffff',
|
|
54
57
|
},
|
|
55
|
-
|
|
58
|
+
contentLayer: { flex: 1 },
|
|
56
59
|
progressWrapper: { width: '100%' },
|
|
57
60
|
});
|
|
58
61
|
|
|
@@ -66,6 +69,32 @@ type FlowboardRendererProps = {
|
|
|
66
69
|
totalScreens?: number;
|
|
67
70
|
};
|
|
68
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
|
+
|
|
69
98
|
export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
70
99
|
const {
|
|
71
100
|
screenData,
|
|
@@ -89,8 +118,12 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
89
118
|
const progressThickness = Number(screenData.progressThickness ?? 4);
|
|
90
119
|
const progressRadius = Number(screenData.progressRadius ?? 0);
|
|
91
120
|
const progressStyle = screenData.progressStyle ?? 'linear';
|
|
92
|
-
const
|
|
121
|
+
const pageScroll = resolvePageScrollAxis(screenData);
|
|
122
|
+
const pageAxisBounds = getAxisBoundsForPageScroll(pageScroll);
|
|
123
|
+
const scrollable = pageScroll !== 'none';
|
|
93
124
|
const safeArea = screenData.safeArea !== false;
|
|
125
|
+
const safeAreaInsets = useSafeAreaInsets();
|
|
126
|
+
const rootAxis: 'vertical' = 'vertical';
|
|
94
127
|
|
|
95
128
|
const padding = parseInsets(screenData.padding);
|
|
96
129
|
const contentPaddingStyle = insetsToStyle(padding);
|
|
@@ -101,6 +134,14 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
101
134
|
};
|
|
102
135
|
const rootCrossAxisAlignment =
|
|
103
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;
|
|
104
145
|
|
|
105
146
|
const content = (
|
|
106
147
|
<View style={{ flex: 1 }}>
|
|
@@ -119,6 +160,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
119
160
|
|
|
120
161
|
{scrollable ? (
|
|
121
162
|
<ScrollView
|
|
163
|
+
horizontal={false}
|
|
122
164
|
contentContainerStyle={{
|
|
123
165
|
...contentPaddingStyle,
|
|
124
166
|
paddingTop: showProgress ? 0 : padding.top,
|
|
@@ -127,8 +169,11 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
127
169
|
<View
|
|
128
170
|
style={{
|
|
129
171
|
flexGrow: 1,
|
|
172
|
+
flexDirection: 'column',
|
|
130
173
|
justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
|
|
131
174
|
alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
|
|
175
|
+
minHeight: 0,
|
|
176
|
+
minWidth: 0,
|
|
132
177
|
}}
|
|
133
178
|
>
|
|
134
179
|
{childrenData.map((child, index) =>
|
|
@@ -137,6 +182,9 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
137
182
|
formData,
|
|
138
183
|
onInputChange,
|
|
139
184
|
allowFlexExpansion: true,
|
|
185
|
+
parentFlexAxis: rootAxis,
|
|
186
|
+
parentMainAxisBounded: pageAxisBounds.heightBounded,
|
|
187
|
+
pageAxisBounds,
|
|
140
188
|
screenData,
|
|
141
189
|
enableFontAwesomeIcons,
|
|
142
190
|
key: `child-${index}`,
|
|
@@ -160,6 +208,9 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
160
208
|
formData,
|
|
161
209
|
onInputChange,
|
|
162
210
|
allowFlexExpansion: true,
|
|
211
|
+
parentFlexAxis: rootAxis,
|
|
212
|
+
parentMainAxisBounded: true,
|
|
213
|
+
pageAxisBounds,
|
|
163
214
|
screenData,
|
|
164
215
|
enableFontAwesomeIcons,
|
|
165
216
|
key: `child-${index}`,
|
|
@@ -173,17 +224,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
173
224
|
return (
|
|
174
225
|
<View style={styles.root}>
|
|
175
226
|
{renderBackground(backgroundData, bgColorCode)}
|
|
176
|
-
{
|
|
177
|
-
<SafeAreaView
|
|
178
|
-
style={styles.safeArea}
|
|
179
|
-
mode="padding"
|
|
180
|
-
edges={['top', 'right', 'bottom', 'left']}
|
|
181
|
-
>
|
|
182
|
-
{content}
|
|
183
|
-
</SafeAreaView>
|
|
184
|
-
) : (
|
|
185
|
-
content
|
|
186
|
-
)}
|
|
227
|
+
<View style={[styles.contentLayer, safeAreaPaddingStyle]}>{content}</View>
|
|
187
228
|
</View>
|
|
188
229
|
);
|
|
189
230
|
}
|
|
@@ -356,6 +397,484 @@ function renderProgressBar(
|
|
|
356
397
|
);
|
|
357
398
|
}
|
|
358
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
|
+
|
|
359
878
|
function buildWidget(
|
|
360
879
|
json: Record<string, any>,
|
|
361
880
|
params: {
|
|
@@ -363,19 +882,32 @@ function buildWidget(
|
|
|
363
882
|
formData: Record<string, any>;
|
|
364
883
|
onInputChange?: (id: string, value: any) => void;
|
|
365
884
|
allowFlexExpansion?: boolean;
|
|
885
|
+
parentFlexAxis?: 'vertical' | 'horizontal';
|
|
886
|
+
parentMainAxisBounded?: boolean;
|
|
887
|
+
pageAxisBounds?: AxisBounds;
|
|
366
888
|
screenData: Record<string, any>;
|
|
367
889
|
enableFontAwesomeIcons: boolean;
|
|
368
890
|
key?: string;
|
|
369
891
|
}
|
|
370
892
|
): React.ReactNode {
|
|
371
893
|
if (!json || typeof json !== 'object') return null;
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
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;
|
|
379
911
|
|
|
380
912
|
let node: React.ReactNode = null;
|
|
381
913
|
|
|
@@ -564,62 +1096,121 @@ function buildWidget(
|
|
|
564
1096
|
const height =
|
|
565
1097
|
normalizeDimension(parseLayoutDimension(props.height)) ?? 50;
|
|
566
1098
|
const label = props.label ?? 'Button';
|
|
1099
|
+
const normalizedStroke = normalizeButtonStroke(props.stroke);
|
|
1100
|
+
const normalizedEffects = normalizeButtonEffects(
|
|
1101
|
+
props.effects,
|
|
1102
|
+
props.shadow
|
|
1103
|
+
);
|
|
567
1104
|
const textStyle = getTextStyle(
|
|
568
1105
|
{ ...props, height: null },
|
|
569
1106
|
'textColor',
|
|
570
1107
|
'0xFFFFFFFF'
|
|
571
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();
|
|
572
1141
|
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
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}
|
|
576
1161
|
>
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
)}
|
|
580
1187
|
|
|
581
|
-
|
|
582
|
-
node = (
|
|
583
|
-
<Pressable
|
|
584
|
-
onPress={
|
|
585
|
-
action
|
|
586
|
-
? () => params.onAction(action, actionPayload ?? props)
|
|
587
|
-
: undefined
|
|
588
|
-
}
|
|
589
|
-
style={{ width, height }}
|
|
590
|
-
>
|
|
1188
|
+
{gradient ? (
|
|
591
1189
|
<LinearGradient
|
|
592
1190
|
colors={gradient.colors}
|
|
593
1191
|
start={gradient.start}
|
|
594
1192
|
end={gradient.end}
|
|
595
1193
|
locations={gradient.stops}
|
|
596
|
-
style={
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
style={{
|
|
611
|
-
width,
|
|
612
|
-
height,
|
|
613
|
-
backgroundColor: parseColor(props.color ?? '0xFF2196F3'),
|
|
614
|
-
borderRadius,
|
|
615
|
-
justifyContent: 'center',
|
|
616
|
-
alignItems: 'center',
|
|
617
|
-
}}
|
|
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' }}
|
|
618
1209
|
>
|
|
619
|
-
{
|
|
620
|
-
</
|
|
621
|
-
|
|
622
|
-
|
|
1210
|
+
<Text style={textStyle}>{label}</Text>
|
|
1211
|
+
</View>
|
|
1212
|
+
</Pressable>
|
|
1213
|
+
);
|
|
623
1214
|
break;
|
|
624
1215
|
}
|
|
625
1216
|
case 'text_input': {
|
|
@@ -935,25 +1526,170 @@ function buildWidget(
|
|
|
935
1526
|
break;
|
|
936
1527
|
}
|
|
937
1528
|
case 'stack': {
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
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
|
+
) : (
|
|
941
1640
|
<View
|
|
942
1641
|
style={{
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1642
|
+
flexDirection: axis === 'horizontal' ? 'row' : 'column',
|
|
1643
|
+
justifyContent: parseFlexAlignment(props.distribution),
|
|
1644
|
+
alignItems: parseCrossAlignment(props.alignment),
|
|
1645
|
+
minHeight: 0,
|
|
1646
|
+
minWidth: 0,
|
|
948
1647
|
}}
|
|
949
1648
|
>
|
|
950
1649
|
{(childrenJson ?? []).map((child, index) => (
|
|
951
1650
|
<React.Fragment key={`stack-${index}`}>
|
|
952
|
-
{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}
|
|
953
1669
|
</React.Fragment>
|
|
954
1670
|
))}
|
|
955
1671
|
</View>
|
|
956
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
|
+
}
|
|
957
1693
|
break;
|
|
958
1694
|
}
|
|
959
1695
|
case 'positioned': {
|
|
@@ -986,29 +1722,64 @@ function buildWidget(
|
|
|
986
1722
|
|
|
987
1723
|
if (!node) return null;
|
|
988
1724
|
|
|
989
|
-
const marginVal =
|
|
1725
|
+
const marginVal =
|
|
1726
|
+
type === 'stack' ? props.layout?.margin ?? props.margin : props.margin;
|
|
990
1727
|
if (marginVal !== undefined && marginVal !== null) {
|
|
991
|
-
const marginInsets =
|
|
1728
|
+
const marginInsets =
|
|
1729
|
+
type === 'stack'
|
|
1730
|
+
? stackSpacingToInsets(marginVal)
|
|
1731
|
+
: parseInsets(marginVal);
|
|
992
1732
|
node = <View style={insetsToMarginStyle(marginInsets)}>{node}</View>;
|
|
993
|
-
} else if (
|
|
1733
|
+
} else if (
|
|
1734
|
+
type !== 'container' &&
|
|
1735
|
+
type !== 'padding' &&
|
|
1736
|
+
type !== 'stack' &&
|
|
1737
|
+
props.padding
|
|
1738
|
+
) {
|
|
994
1739
|
const paddingInsets = parseInsets(props.padding);
|
|
995
1740
|
node = <View style={insetsToStyle(paddingInsets)}>{node}</View>;
|
|
996
1741
|
}
|
|
997
1742
|
|
|
998
|
-
if (params.allowFlexExpansion
|
|
1743
|
+
if (params.allowFlexExpansion) {
|
|
999
1744
|
let shouldExpand = false;
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
+
}
|
|
1007
1778
|
}
|
|
1008
1779
|
}
|
|
1009
1780
|
|
|
1010
1781
|
if (shouldExpand) {
|
|
1011
|
-
node = <View style={{ flex: 1 }}>{node}</View>;
|
|
1782
|
+
node = <View style={{ flex: 1, minHeight: 0, minWidth: 0 }}>{node}</View>;
|
|
1012
1783
|
}
|
|
1013
1784
|
}
|
|
1014
1785
|
|
|
@@ -1087,6 +1858,227 @@ function parseGradient(value: any):
|
|
|
1087
1858
|
return { colors, start, end, stops };
|
|
1088
1859
|
}
|
|
1089
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
|
+
|
|
1090
2082
|
function alignmentToGradient(value: string) {
|
|
1091
2083
|
switch (value) {
|
|
1092
2084
|
case 'topCenter':
|
|
@@ -1112,13 +2104,29 @@ function alignmentToGradient(value: string) {
|
|
|
1112
2104
|
}
|
|
1113
2105
|
|
|
1114
2106
|
function withOpacity(color: string, opacity: number): string {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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));
|
|
1122
2130
|
}
|
|
1123
2131
|
|
|
1124
2132
|
function normalizeDimension(
|
|
@@ -1277,6 +2285,81 @@ function GradientText({
|
|
|
1277
2285
|
);
|
|
1278
2286
|
}
|
|
1279
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
|
+
|
|
1280
2363
|
function DynamicInput({
|
|
1281
2364
|
initialValue,
|
|
1282
2365
|
properties,
|
|
@@ -1314,10 +2397,12 @@ function DynamicInput({
|
|
|
1314
2397
|
);
|
|
1315
2398
|
const inputStyle = getTextStyle({ ...properties, height: null }, 'textColor');
|
|
1316
2399
|
|
|
1317
|
-
const backgroundColor =
|
|
1318
|
-
|
|
1319
|
-
);
|
|
1320
|
-
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;
|
|
1321
2406
|
|
|
1322
2407
|
return (
|
|
1323
2408
|
<View>
|
|
@@ -1325,9 +2410,11 @@ function DynamicInput({
|
|
|
1325
2410
|
<View
|
|
1326
2411
|
style={{
|
|
1327
2412
|
backgroundColor,
|
|
1328
|
-
borderRadius,
|
|
1329
|
-
|
|
1330
|
-
|
|
2413
|
+
borderRadius: stroke.radius,
|
|
2414
|
+
borderStyle: stroke.width > 0 ? stroke.style : undefined,
|
|
2415
|
+
borderWidth: stroke.width,
|
|
2416
|
+
borderColor: stroke.color,
|
|
2417
|
+
padding,
|
|
1331
2418
|
}}
|
|
1332
2419
|
>
|
|
1333
2420
|
{mask ? (
|
|
@@ -1497,7 +2584,7 @@ function SelectionList({
|
|
|
1497
2584
|
|
|
1498
2585
|
if (layout === 'row') {
|
|
1499
2586
|
return (
|
|
1500
|
-
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
2587
|
+
<View style={{ width: '100%', flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
1501
2588
|
{options.map((option, index) => (
|
|
1502
2589
|
<View
|
|
1503
2590
|
key={`row-option-${index}`}
|
|
@@ -1514,7 +2601,7 @@ function SelectionList({
|
|
|
1514
2601
|
const crossAxisCount = Number(properties.gridCrossAxisCount ?? 2);
|
|
1515
2602
|
const aspectRatio = Number(properties.gridAspectRatio ?? 1);
|
|
1516
2603
|
return (
|
|
1517
|
-
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
2604
|
+
<View style={{ width: '100%', flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
1518
2605
|
{options.map((option, index) => (
|
|
1519
2606
|
<View
|
|
1520
2607
|
key={`grid-option-${index}`}
|
|
@@ -1533,9 +2620,12 @@ function SelectionList({
|
|
|
1533
2620
|
}
|
|
1534
2621
|
|
|
1535
2622
|
return (
|
|
1536
|
-
<View>
|
|
2623
|
+
<View style={{ width: '100%' }}>
|
|
1537
2624
|
{options.map((option, index) => (
|
|
1538
|
-
<View
|
|
2625
|
+
<View
|
|
2626
|
+
key={`column-option-${index}`}
|
|
2627
|
+
style={{ width: '100%', marginBottom: spacing }}
|
|
2628
|
+
>
|
|
1539
2629
|
{renderItem(option, index)}
|
|
1540
2630
|
</View>
|
|
1541
2631
|
))}
|
|
@@ -1562,6 +2652,10 @@ function WheelPicker({
|
|
|
1562
2652
|
onAction: (action: string, data?: Record<string, any>) => void;
|
|
1563
2653
|
buildWidget: (widgetJson: Record<string, any>) => React.ReactNode;
|
|
1564
2654
|
}) {
|
|
2655
|
+
const triggerLightHaptic = React.useCallback(() => {
|
|
2656
|
+
Vibration.vibrate(8);
|
|
2657
|
+
}, []);
|
|
2658
|
+
|
|
1565
2659
|
const multiSelect = properties.multiSelect === true;
|
|
1566
2660
|
const [selectedValues, setSelectedValues] = React.useState<string[]>(() => {
|
|
1567
2661
|
if (initialValue === null || initialValue === undefined) return [];
|
|
@@ -1632,6 +2726,27 @@ function WheelPicker({
|
|
|
1632
2726
|
requestedVisible % 2 === 0 ? requestedVisible + 1 : requestedVisible;
|
|
1633
2727
|
const wheelRowStride = wheelItemHeight + Math.max(0, spacing);
|
|
1634
2728
|
const wheelContainerHeight = wheelItemHeight * visibleItems;
|
|
2729
|
+
const hasProvidedInitialValue = React.useMemo(() => {
|
|
2730
|
+
if (initialValue === null || initialValue === undefined) return false;
|
|
2731
|
+
if (Array.isArray(initialValue)) return initialValue.length > 0;
|
|
2732
|
+
if (typeof initialValue === 'string') return initialValue.trim().length > 0;
|
|
2733
|
+
return true;
|
|
2734
|
+
}, [initialValue]);
|
|
2735
|
+
|
|
2736
|
+
const resolveStartItemIndex = React.useCallback(() => {
|
|
2737
|
+
const raw =
|
|
2738
|
+
properties.startItemIndex ??
|
|
2739
|
+
properties.start_index ??
|
|
2740
|
+
properties.startIndex ??
|
|
2741
|
+
1;
|
|
2742
|
+
const parsed = Number(raw);
|
|
2743
|
+
if (!Number.isFinite(parsed)) return 1;
|
|
2744
|
+
return Math.max(1, Math.floor(parsed));
|
|
2745
|
+
}, [
|
|
2746
|
+
properties.startIndex,
|
|
2747
|
+
properties.startItemIndex,
|
|
2748
|
+
properties.start_index,
|
|
2749
|
+
]);
|
|
1635
2750
|
|
|
1636
2751
|
const clampWheelIndex = React.useCallback(
|
|
1637
2752
|
(index: number) =>
|
|
@@ -1666,6 +2781,7 @@ function WheelPicker({
|
|
|
1666
2781
|
|
|
1667
2782
|
if (lastCommittedWheelValue.current !== value) {
|
|
1668
2783
|
lastCommittedWheelValue.current = value;
|
|
2784
|
+
triggerLightHaptic();
|
|
1669
2785
|
onChanged(value);
|
|
1670
2786
|
if (properties.autoGoNext === true) {
|
|
1671
2787
|
requestAnimationFrame(() =>
|
|
@@ -1674,7 +2790,14 @@ function WheelPicker({
|
|
|
1674
2790
|
}
|
|
1675
2791
|
}
|
|
1676
2792
|
},
|
|
1677
|
-
[
|
|
2793
|
+
[
|
|
2794
|
+
clampWheelIndex,
|
|
2795
|
+
onAction,
|
|
2796
|
+
onChanged,
|
|
2797
|
+
properties.autoGoNext,
|
|
2798
|
+
resolvedOptions,
|
|
2799
|
+
triggerLightHaptic,
|
|
2800
|
+
]
|
|
1678
2801
|
);
|
|
1679
2802
|
|
|
1680
2803
|
React.useEffect(() => {
|
|
@@ -1696,6 +2819,31 @@ function WheelPicker({
|
|
|
1696
2819
|
wheelScrollY,
|
|
1697
2820
|
]);
|
|
1698
2821
|
|
|
2822
|
+
React.useEffect(() => {
|
|
2823
|
+
if (layout !== 'wheel') return;
|
|
2824
|
+
if (hasProvidedInitialValue) return;
|
|
2825
|
+
if (selectedValues.length > 0) return;
|
|
2826
|
+
if (resolvedOptions.length === 0) return;
|
|
2827
|
+
|
|
2828
|
+
const oneBasedIndex = resolveStartItemIndex();
|
|
2829
|
+
const targetIndex = Math.max(
|
|
2830
|
+
0,
|
|
2831
|
+
Math.min(resolvedOptions.length - 1, oneBasedIndex - 1)
|
|
2832
|
+
);
|
|
2833
|
+
const value = String(resolvedOptions[targetIndex]?.value ?? '');
|
|
2834
|
+
if (!value) return;
|
|
2835
|
+
|
|
2836
|
+
setSelectedValues([value]);
|
|
2837
|
+
onChanged(value);
|
|
2838
|
+
}, [
|
|
2839
|
+
hasProvidedInitialValue,
|
|
2840
|
+
layout,
|
|
2841
|
+
onChanged,
|
|
2842
|
+
resolveStartItemIndex,
|
|
2843
|
+
resolvedOptions,
|
|
2844
|
+
selectedValues.length,
|
|
2845
|
+
]);
|
|
2846
|
+
|
|
1699
2847
|
const renderItem = (option: Record<string, any>, index: number) => {
|
|
1700
2848
|
const value = String(option.value ?? '');
|
|
1701
2849
|
const label = String(option.text ?? value);
|
|
@@ -1813,10 +2961,19 @@ function WheelPicker({
|
|
|
1813
2961
|
const overlayBackgroundColor = parseColor(
|
|
1814
2962
|
properties.wheelCenterBackgroundColor ?? '#24FFFFFF'
|
|
1815
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
|
+
);
|
|
1816
2971
|
|
|
1817
2972
|
const handleWheelMomentumEnd = (event: any) => {
|
|
1818
2973
|
const offsetY = Number(event?.nativeEvent?.contentOffset?.y ?? 0);
|
|
1819
|
-
const settledIndex = clampWheelIndex(
|
|
2974
|
+
const settledIndex = clampWheelIndex(
|
|
2975
|
+
Math.round(offsetY / wheelRowStride)
|
|
2976
|
+
);
|
|
1820
2977
|
const targetOffset = settledIndex * wheelRowStride;
|
|
1821
2978
|
if (Math.abs(targetOffset - offsetY) > 0.5) {
|
|
1822
2979
|
wheelScrollRef.current?.scrollTo({
|
|
@@ -1889,7 +3046,12 @@ function WheelPicker({
|
|
|
1889
3046
|
],
|
|
1890
3047
|
}}
|
|
1891
3048
|
>
|
|
1892
|
-
<View
|
|
3049
|
+
<View
|
|
3050
|
+
style={{
|
|
3051
|
+
minHeight: wheelItemHeight,
|
|
3052
|
+
justifyContent: 'center',
|
|
3053
|
+
}}
|
|
3054
|
+
>
|
|
1893
3055
|
{renderItem(option, index)}
|
|
1894
3056
|
</View>
|
|
1895
3057
|
</Animated.View>
|
|
@@ -1911,22 +3073,38 @@ function WheelPicker({
|
|
|
1911
3073
|
backgroundColor: overlayBackgroundColor,
|
|
1912
3074
|
}}
|
|
1913
3075
|
/>
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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}
|
|
1930
3108
|
</View>
|
|
1931
3109
|
);
|
|
1932
3110
|
}
|
|
@@ -1934,7 +3112,10 @@ function WheelPicker({
|
|
|
1934
3112
|
return (
|
|
1935
3113
|
<View>
|
|
1936
3114
|
{resolvedOptions.map((option, index) => (
|
|
1937
|
-
<View
|
|
3115
|
+
<View
|
|
3116
|
+
key={`wheel-list-option-${index}`}
|
|
3117
|
+
style={{ marginBottom: spacing }}
|
|
3118
|
+
>
|
|
1938
3119
|
{renderItem(option, index)}
|
|
1939
3120
|
</View>
|
|
1940
3121
|
))}
|
|
@@ -2018,7 +3199,10 @@ function buildWheelTemplateBaseContext(
|
|
|
2018
3199
|
properties.start ?? properties.itemStart,
|
|
2019
3200
|
context
|
|
2020
3201
|
);
|
|
2021
|
-
const step = resolveNumericValue(
|
|
3202
|
+
const step = resolveNumericValue(
|
|
3203
|
+
properties.step ?? properties.itemStep,
|
|
3204
|
+
context
|
|
3205
|
+
);
|
|
2022
3206
|
|
|
2023
3207
|
if (start !== null) context.start = start;
|
|
2024
3208
|
if (step !== null) context.step = step;
|
|
@@ -2479,6 +3663,14 @@ function RadarChart({
|
|
|
2479
3663
|
'color',
|
|
2480
3664
|
'0xFF424242'
|
|
2481
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
|
+
);
|
|
2482
3674
|
const showLegend = properties.showLegend !== false;
|
|
2483
3675
|
const legendPosition = (properties.legendPosition ?? 'bottom').toLowerCase();
|
|
2484
3676
|
const legendSpacing = Number(properties.legendSpacing ?? 12);
|
|
@@ -2488,14 +3680,21 @@ function RadarChart({
|
|
|
2488
3680
|
'0xFF212121'
|
|
2489
3681
|
);
|
|
2490
3682
|
const shape = (properties.shape ?? 'polygon').toLowerCase();
|
|
2491
|
-
|
|
2492
|
-
const
|
|
2493
|
-
const radius =
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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
|
+
};
|
|
2499
3698
|
|
|
2500
3699
|
const axisAngle = (Math.PI * 2) / axes.length;
|
|
2501
3700
|
|
|
@@ -2514,7 +3713,10 @@ function RadarChart({
|
|
|
2514
3713
|
const points = dataset.values.map((value, index) => {
|
|
2515
3714
|
const axis = normalizedAxes[index];
|
|
2516
3715
|
const maxValue = axis?.maxValue ?? 0;
|
|
2517
|
-
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));
|
|
2518
3720
|
const angle = index * axisAngle - Math.PI / 2;
|
|
2519
3721
|
const x = center.x + radius * ratio * Math.cos(angle);
|
|
2520
3722
|
const y = center.y + radius * ratio * Math.sin(angle);
|
|
@@ -2523,16 +3725,24 @@ function RadarChart({
|
|
|
2523
3725
|
return { dataset, points };
|
|
2524
3726
|
});
|
|
2525
3727
|
|
|
3728
|
+
const isVerticalLegend =
|
|
3729
|
+
legendPosition === 'left' || legendPosition === 'right';
|
|
2526
3730
|
const legend = (
|
|
2527
|
-
<View
|
|
3731
|
+
<View
|
|
3732
|
+
style={{
|
|
3733
|
+
flexDirection: isVerticalLegend ? 'column' : 'row',
|
|
3734
|
+
flexWrap: isVerticalLegend ? 'nowrap' : 'wrap',
|
|
3735
|
+
alignItems: isVerticalLegend ? 'flex-start' : 'center',
|
|
3736
|
+
}}
|
|
3737
|
+
>
|
|
2528
3738
|
{datasets.map((dataset, index) => (
|
|
2529
3739
|
<View
|
|
2530
3740
|
key={`legend-${index}`}
|
|
2531
3741
|
style={{
|
|
2532
3742
|
flexDirection: 'row',
|
|
2533
3743
|
alignItems: 'center',
|
|
2534
|
-
marginRight: legendSpacing,
|
|
2535
|
-
marginBottom: 6,
|
|
3744
|
+
marginRight: isVerticalLegend ? 0 : legendSpacing,
|
|
3745
|
+
marginBottom: isVerticalLegend ? legendSpacing : 6,
|
|
2536
3746
|
}}
|
|
2537
3747
|
>
|
|
2538
3748
|
<View
|
|
@@ -2540,11 +3750,15 @@ function RadarChart({
|
|
|
2540
3750
|
width: 12,
|
|
2541
3751
|
height: 12,
|
|
2542
3752
|
borderRadius: 6,
|
|
2543
|
-
backgroundColor: dataset.borderColor,
|
|
3753
|
+
backgroundColor: dataset.fillColor ?? dataset.borderColor,
|
|
3754
|
+
borderWidth: 1,
|
|
3755
|
+
borderColor: dataset.borderColor,
|
|
2544
3756
|
marginRight: 6,
|
|
2545
3757
|
}}
|
|
2546
3758
|
/>
|
|
2547
|
-
<Text style={legendStyle}>
|
|
3759
|
+
<Text numberOfLines={1} style={legendStyle}>
|
|
3760
|
+
{dataset.label}
|
|
3761
|
+
</Text>
|
|
2548
3762
|
</View>
|
|
2549
3763
|
))}
|
|
2550
3764
|
</View>
|
|
@@ -2553,12 +3767,10 @@ function RadarChart({
|
|
|
2553
3767
|
const chart = (
|
|
2554
3768
|
<Animated.View
|
|
2555
3769
|
style={{
|
|
2556
|
-
width: normalizedWidth ??
|
|
3770
|
+
width: isVerticalLegend ? width : normalizedWidth ?? '100%',
|
|
3771
|
+
maxWidth: '100%',
|
|
3772
|
+
flexShrink: 1,
|
|
2557
3773
|
height,
|
|
2558
|
-
paddingLeft: padding.left,
|
|
2559
|
-
paddingRight: padding.right,
|
|
2560
|
-
paddingTop: padding.top,
|
|
2561
|
-
paddingBottom: padding.bottom,
|
|
2562
3774
|
opacity: reveal,
|
|
2563
3775
|
}}
|
|
2564
3776
|
onLayout={(event) => {
|
|
@@ -2609,14 +3821,16 @@ function RadarChart({
|
|
|
2609
3821
|
})}
|
|
2610
3822
|
{axes.map((axis, index) => {
|
|
2611
3823
|
const angle = index * axisAngle - Math.PI / 2;
|
|
2612
|
-
const labelX =
|
|
2613
|
-
|
|
3824
|
+
const labelX =
|
|
3825
|
+
center.x + (radius + resolvedLabelOffset) * Math.cos(angle);
|
|
3826
|
+
const labelY =
|
|
3827
|
+
center.y + (radius + resolvedLabelOffset) * Math.sin(angle);
|
|
2614
3828
|
return (
|
|
2615
3829
|
<SvgText
|
|
2616
3830
|
key={`label-${index}`}
|
|
2617
3831
|
x={labelX}
|
|
2618
3832
|
y={labelY}
|
|
2619
|
-
fontSize={
|
|
3833
|
+
fontSize={resolvedAxisLabelFontSize}
|
|
2620
3834
|
fontWeight={axisLabelStyle.fontWeight}
|
|
2621
3835
|
fill={axisLabelStyle.color}
|
|
2622
3836
|
textAnchor="middle"
|
|
@@ -2625,21 +3839,20 @@ function RadarChart({
|
|
|
2625
3839
|
</SvgText>
|
|
2626
3840
|
);
|
|
2627
3841
|
})}
|
|
2628
|
-
{datasetPolygons.map((entry, index) =>
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
entry.dataset.
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
))}
|
|
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
|
+
})}
|
|
2643
3856
|
{datasetPolygons.map((entry, datasetIndex) =>
|
|
2644
3857
|
entry.dataset.showPoints
|
|
2645
3858
|
? entry.points.map((point, pointIndex) => {
|
|
@@ -2661,22 +3874,15 @@ function RadarChart({
|
|
|
2661
3874
|
</Animated.View>
|
|
2662
3875
|
);
|
|
2663
3876
|
|
|
2664
|
-
|
|
2665
|
-
<View style={{ backgroundColor }}>
|
|
2666
|
-
|
|
2667
|
-
{showLegend && legendPosition === 'bottom' ? (
|
|
2668
|
-
<View style={{ marginTop: 12 }}>{legend}</View>
|
|
2669
|
-
) : null}
|
|
2670
|
-
</View>
|
|
2671
|
-
);
|
|
2672
|
-
|
|
2673
|
-
if (!showLegend || legendPosition === 'bottom') return composed;
|
|
3877
|
+
if (!showLegend) {
|
|
3878
|
+
return <View style={{ backgroundColor }}>{chart}</View>;
|
|
3879
|
+
}
|
|
2674
3880
|
|
|
2675
3881
|
if (legendPosition === 'top') {
|
|
2676
3882
|
return (
|
|
2677
|
-
<View>
|
|
3883
|
+
<View style={{ backgroundColor, alignItems: 'center' }}>
|
|
2678
3884
|
{legend}
|
|
2679
|
-
<View style={{ height:
|
|
3885
|
+
<View style={{ height: legendSpacing }} />
|
|
2680
3886
|
{chart}
|
|
2681
3887
|
</View>
|
|
2682
3888
|
);
|
|
@@ -2684,9 +3890,11 @@ function RadarChart({
|
|
|
2684
3890
|
|
|
2685
3891
|
if (legendPosition === 'left') {
|
|
2686
3892
|
return (
|
|
2687
|
-
<View
|
|
3893
|
+
<View
|
|
3894
|
+
style={{ backgroundColor, flexDirection: 'row', alignItems: 'center' }}
|
|
3895
|
+
>
|
|
2688
3896
|
{legend}
|
|
2689
|
-
<View style={{ width:
|
|
3897
|
+
<View style={{ width: legendSpacing }} />
|
|
2690
3898
|
{chart}
|
|
2691
3899
|
</View>
|
|
2692
3900
|
);
|
|
@@ -2694,32 +3902,262 @@ function RadarChart({
|
|
|
2694
3902
|
|
|
2695
3903
|
if (legendPosition === 'right') {
|
|
2696
3904
|
return (
|
|
2697
|
-
<View
|
|
3905
|
+
<View
|
|
3906
|
+
style={{ backgroundColor, flexDirection: 'row', alignItems: 'center' }}
|
|
3907
|
+
>
|
|
2698
3908
|
{chart}
|
|
2699
|
-
<View style={{ width:
|
|
3909
|
+
<View style={{ width: legendSpacing }} />
|
|
2700
3910
|
{legend}
|
|
2701
3911
|
</View>
|
|
2702
3912
|
);
|
|
2703
3913
|
}
|
|
2704
3914
|
|
|
2705
|
-
return
|
|
3915
|
+
return (
|
|
3916
|
+
<View style={{ backgroundColor, alignItems: 'center' }}>
|
|
3917
|
+
{chart}
|
|
3918
|
+
<View style={{ height: legendSpacing }} />
|
|
3919
|
+
{legend}
|
|
3920
|
+
</View>
|
|
3921
|
+
);
|
|
2706
3922
|
}
|
|
2707
3923
|
|
|
2708
3924
|
export type RadarAxis = { label: string; maxValue: number };
|
|
3925
|
+
export type RadarBorderStyle = 'solid' | 'dotted' | 'dashed';
|
|
2709
3926
|
|
|
2710
3927
|
export type RadarDataset = {
|
|
2711
3928
|
label: string;
|
|
2712
3929
|
values: number[];
|
|
2713
3930
|
borderColor: string;
|
|
2714
3931
|
borderWidth: number;
|
|
2715
|
-
borderStyle:
|
|
3932
|
+
borderStyle: RadarBorderStyle;
|
|
2716
3933
|
dashArray?: number[];
|
|
3934
|
+
dashLength?: number;
|
|
2717
3935
|
showPoints: boolean;
|
|
2718
3936
|
pointRadius: number;
|
|
2719
3937
|
pointColor: string;
|
|
2720
3938
|
fillColor?: string;
|
|
2721
3939
|
};
|
|
2722
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
|
+
|
|
2723
4161
|
export function parseRadarAxes(rawAxes: any): RadarAxis[] {
|
|
2724
4162
|
if (!Array.isArray(rawAxes)) return [];
|
|
2725
4163
|
const axes: RadarAxis[] = [];
|
|
@@ -2728,7 +4166,9 @@ export function parseRadarAxes(rawAxes: any): RadarAxis[] {
|
|
|
2728
4166
|
const label = axis.label ?? '';
|
|
2729
4167
|
if (!label) return;
|
|
2730
4168
|
const maxValue = resolveNumericValue(axis.maxValue, {}) ?? 100;
|
|
2731
|
-
|
|
4169
|
+
const normalizedMaxValue =
|
|
4170
|
+
Number.isFinite(maxValue) && maxValue > 0 ? maxValue : 100;
|
|
4171
|
+
axes.push({ label, maxValue: normalizedMaxValue });
|
|
2732
4172
|
});
|
|
2733
4173
|
return axes;
|
|
2734
4174
|
}
|
|
@@ -2740,14 +4180,16 @@ export function parseRadarDatasets(
|
|
|
2740
4180
|
): RadarDataset[] {
|
|
2741
4181
|
if (!Array.isArray(raw) || axisLength === 0) return [];
|
|
2742
4182
|
const datasets: RadarDataset[] = [];
|
|
2743
|
-
raw.forEach((dataset) => {
|
|
4183
|
+
raw.forEach((dataset, datasetIndex) => {
|
|
2744
4184
|
if (!dataset || typeof dataset !== 'object') return;
|
|
2745
4185
|
const valuesRaw = Array.isArray(dataset.data) ? dataset.data : [];
|
|
2746
4186
|
const values: number[] = [];
|
|
2747
4187
|
for (let i = 0; i < axisLength; i += 1) {
|
|
2748
4188
|
const rawValue = i < valuesRaw.length ? valuesRaw[i] : null;
|
|
2749
|
-
const resolved = resolveNumericValue(rawValue, formData)
|
|
2750
|
-
values.push(
|
|
4189
|
+
const resolved = resolveNumericValue(rawValue, formData);
|
|
4190
|
+
values.push(
|
|
4191
|
+
typeof resolved === 'number' && Number.isFinite(resolved) ? resolved : 0
|
|
4192
|
+
);
|
|
2751
4193
|
}
|
|
2752
4194
|
|
|
2753
4195
|
const label = dataset.label ?? `Dataset ${datasets.length + 1}`;
|
|
@@ -2758,18 +4200,30 @@ export function parseRadarDatasets(
|
|
|
2758
4200
|
const pointColor = parseColor(
|
|
2759
4201
|
dataset.pointColor ?? dataset.borderColor ?? '0xFF2196F3'
|
|
2760
4202
|
);
|
|
2761
|
-
const
|
|
2762
|
-
const
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
const
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
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
|
+
);
|
|
2770
4217
|
const dashArray = Array.isArray(dataset.dashArray)
|
|
2771
|
-
? dataset.dashArray
|
|
4218
|
+
? dataset.dashArray
|
|
4219
|
+
.map((val: any) => Number(val))
|
|
4220
|
+
.filter((val: number) => Number.isFinite(val) && val > 0)
|
|
2772
4221
|
: undefined;
|
|
4222
|
+
const rawDashLength = Number(dataset.dashLength);
|
|
4223
|
+
const dashLength =
|
|
4224
|
+
Number.isFinite(rawDashLength) && rawDashLength > 0
|
|
4225
|
+
? rawDashLength
|
|
4226
|
+
: undefined;
|
|
2773
4227
|
const showPoints = dataset.showPoints !== false;
|
|
2774
4228
|
|
|
2775
4229
|
datasets.push({
|
|
@@ -2779,6 +4233,7 @@ export function parseRadarDatasets(
|
|
|
2779
4233
|
borderWidth,
|
|
2780
4234
|
borderStyle: normalizedStyle,
|
|
2781
4235
|
dashArray,
|
|
4236
|
+
dashLength,
|
|
2782
4237
|
showPoints,
|
|
2783
4238
|
pointRadius,
|
|
2784
4239
|
pointColor,
|
|
@@ -2796,7 +4251,9 @@ export function normalizeAxisMaximums(
|
|
|
2796
4251
|
let maxValue = axis.maxValue;
|
|
2797
4252
|
datasets.forEach((dataset) => {
|
|
2798
4253
|
const value = dataset.values[index];
|
|
2799
|
-
if (value !== undefined && value > maxValue)
|
|
4254
|
+
if (value !== undefined && Number.isFinite(value) && value > maxValue) {
|
|
4255
|
+
maxValue = value;
|
|
4256
|
+
}
|
|
2800
4257
|
});
|
|
2801
4258
|
return { ...axis, maxValue };
|
|
2802
4259
|
});
|
|
@@ -2806,12 +4263,15 @@ function resolveRadarWidth(
|
|
|
2806
4263
|
parsedWidth: number | string | undefined,
|
|
2807
4264
|
measuredWidth: number
|
|
2808
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
|
+
}
|
|
2809
4272
|
if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
|
|
2810
4273
|
return parsedWidth;
|
|
2811
4274
|
}
|
|
2812
|
-
if (measuredWidth > 0) {
|
|
2813
|
-
return measuredWidth;
|
|
2814
|
-
}
|
|
2815
4275
|
return 300;
|
|
2816
4276
|
}
|
|
2817
4277
|
|