flowboard-react 0.6.1 → 0.6.3

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.
Files changed (97) hide show
  1. package/README.md +168 -52
  2. package/app.plugin.js +341 -0
  3. package/bin/setup.js +427 -0
  4. package/lib/module/Flowboard.js +1 -2
  5. package/lib/module/Flowboard.js.map +1 -1
  6. package/lib/module/FlowboardProvider.js +1 -1
  7. package/lib/module/FlowboardProvider.js.map +1 -1
  8. package/lib/module/components/FlowboardFlow.js +31 -9
  9. package/lib/module/components/FlowboardFlow.js.map +1 -1
  10. package/lib/module/components/FlowboardRenderer.js +419 -85
  11. package/lib/module/components/FlowboardRenderer.js.map +1 -1
  12. package/lib/module/core/clientContext.js +10 -29
  13. package/lib/module/core/clientContext.js.map +1 -1
  14. package/lib/module/core/fontAwesome.js +2 -9
  15. package/lib/module/core/fontAwesome.js.map +1 -1
  16. package/lib/module/core/onboardingRepository.js +13 -13
  17. package/lib/module/core/onboardingRepository.js.map +1 -1
  18. package/lib/module/native/asyncStorage.js +55 -0
  19. package/lib/module/native/asyncStorage.js.map +1 -0
  20. package/lib/module/native/deviceInfo.js +46 -0
  21. package/lib/module/native/deviceInfo.js.map +1 -0
  22. package/lib/module/native/inAppReview.js +17 -0
  23. package/lib/module/native/inAppReview.js.map +1 -0
  24. package/lib/module/native/linearGradient.js +29 -0
  25. package/lib/module/native/linearGradient.js.map +1 -0
  26. package/lib/module/native/lottie.js +20 -0
  27. package/lib/module/native/lottie.js.map +1 -0
  28. package/lib/module/native/maskedView.js +26 -0
  29. package/lib/module/native/maskedView.js.map +1 -0
  30. package/lib/module/native/pagerView.js +79 -0
  31. package/lib/module/native/pagerView.js.map +1 -0
  32. package/lib/module/native/permissions.js +30 -0
  33. package/lib/module/native/permissions.js.map +1 -0
  34. package/lib/module/native/runtime.js +81 -0
  35. package/lib/module/native/runtime.js.map +1 -0
  36. package/lib/module/native/safeAreaContext.js +43 -0
  37. package/lib/module/native/safeAreaContext.js.map +1 -0
  38. package/lib/module/native/svg.js +83 -0
  39. package/lib/module/native/svg.js.map +1 -0
  40. package/lib/module/native/vectorIcons.js +41 -0
  41. package/lib/module/native/vectorIcons.js.map +1 -0
  42. package/lib/module/utils/flowboardUtils.js +4 -1
  43. package/lib/module/utils/flowboardUtils.js.map +1 -1
  44. package/lib/typescript/src/Flowboard.d.ts.map +1 -1
  45. package/lib/typescript/src/components/FlowboardFlow.d.ts.map +1 -1
  46. package/lib/typescript/src/components/FlowboardRenderer.d.ts +33 -0
  47. package/lib/typescript/src/components/FlowboardRenderer.d.ts.map +1 -1
  48. package/lib/typescript/src/core/clientContext.d.ts.map +1 -1
  49. package/lib/typescript/src/core/fontAwesome.d.ts +1 -1
  50. package/lib/typescript/src/core/fontAwesome.d.ts.map +1 -1
  51. package/lib/typescript/src/core/onboardingRepository.d.ts.map +1 -1
  52. package/lib/typescript/src/native/asyncStorage.d.ts +16 -0
  53. package/lib/typescript/src/native/asyncStorage.d.ts.map +1 -0
  54. package/lib/typescript/src/native/deviceInfo.d.ts +11 -0
  55. package/lib/typescript/src/native/deviceInfo.d.ts.map +1 -0
  56. package/lib/typescript/src/native/inAppReview.d.ts +4 -0
  57. package/lib/typescript/src/native/inAppReview.d.ts.map +1 -0
  58. package/lib/typescript/src/native/linearGradient.d.ts +3 -0
  59. package/lib/typescript/src/native/linearGradient.d.ts.map +1 -0
  60. package/lib/typescript/src/native/lottie.d.ts +3 -0
  61. package/lib/typescript/src/native/lottie.d.ts.map +1 -0
  62. package/lib/typescript/src/native/maskedView.d.ts +3 -0
  63. package/lib/typescript/src/native/maskedView.d.ts.map +1 -0
  64. package/lib/typescript/src/native/pagerView.d.ts +14 -0
  65. package/lib/typescript/src/native/pagerView.d.ts.map +1 -0
  66. package/lib/typescript/src/native/permissions.d.ts +9 -0
  67. package/lib/typescript/src/native/permissions.d.ts.map +1 -0
  68. package/lib/typescript/src/native/runtime.d.ts +13 -0
  69. package/lib/typescript/src/native/runtime.d.ts.map +1 -0
  70. package/lib/typescript/src/native/safeAreaContext.d.ts +15 -0
  71. package/lib/typescript/src/native/safeAreaContext.d.ts.map +1 -0
  72. package/lib/typescript/src/native/svg.d.ts +8 -0
  73. package/lib/typescript/src/native/svg.d.ts.map +1 -0
  74. package/lib/typescript/src/native/vectorIcons.d.ts +4 -0
  75. package/lib/typescript/src/native/vectorIcons.d.ts.map +1 -0
  76. package/lib/typescript/src/utils/flowboardUtils.d.ts.map +1 -1
  77. package/package.json +24 -19
  78. package/src/Flowboard.ts +1 -2
  79. package/src/FlowboardProvider.tsx +1 -1
  80. package/src/components/FlowboardFlow.tsx +47 -9
  81. package/src/components/FlowboardRenderer.tsx +689 -113
  82. package/src/core/clientContext.ts +10 -32
  83. package/src/core/fontAwesome.ts +2 -9
  84. package/src/core/onboardingRepository.ts +18 -13
  85. package/src/native/asyncStorage.ts +99 -0
  86. package/src/native/deviceInfo.ts +88 -0
  87. package/src/native/inAppReview.ts +34 -0
  88. package/src/native/linearGradient.tsx +24 -0
  89. package/src/native/lottie.tsx +17 -0
  90. package/src/native/maskedView.tsx +19 -0
  91. package/src/native/pagerView.tsx +95 -0
  92. package/src/native/permissions.ts +59 -0
  93. package/src/native/runtime.ts +110 -0
  94. package/src/native/safeAreaContext.tsx +44 -0
  95. package/src/native/svg.tsx +82 -0
  96. package/src/native/vectorIcons.tsx +50 -0
  97. package/src/utils/flowboardUtils.ts +9 -1
@@ -14,21 +14,16 @@ import {
14
14
  type TextStyle,
15
15
  type ViewStyle,
16
16
  } from 'react-native';
17
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
18
- import MaskedView from '@react-native-masked-view/masked-view';
19
- import LinearGradient from 'react-native-linear-gradient';
20
- import LottieView from 'lottie-react-native';
21
17
  import MaskInput from 'react-native-mask-input';
22
- import Svg, {
23
- Circle,
24
- G,
25
- Line,
26
- Polygon,
27
- Text as SvgText,
28
- } from 'react-native-svg';
18
+ import LinearGradient from '../native/linearGradient';
19
+ import LottieView from '../native/lottie';
20
+ import MaskedView from '../native/maskedView';
29
21
  import PagerView, {
22
+ type PagerViewHandle,
30
23
  type PagerViewOnPageSelectedEvent,
31
- } from 'react-native-pager-view';
24
+ } from '../native/pagerView';
25
+ import { useSafeAreaInsets } from '../native/safeAreaContext';
26
+ import Svg, { Circle, G, Line, Polygon, SvgText } from '../native/svg';
32
27
  import {
33
28
  insetsToStyle,
34
29
  insetsToMarginStyle,
@@ -563,6 +558,7 @@ function isFillDimensionValue(value: unknown) {
563
558
  const normalized = value.trim().toLowerCase();
564
559
  return (
565
560
  normalized === 'infinity' ||
561
+ normalized === 'infinite' ||
566
562
  normalized === 'double.infinity' ||
567
563
  normalized === '+infinity' ||
568
564
  normalized === '100%'
@@ -575,33 +571,63 @@ function isFixedDimensionValue(value: unknown) {
575
571
  return Number.isFinite(parsed);
576
572
  }
577
573
 
574
+ function normalizeStackSizeMode(
575
+ value: unknown
576
+ ): 'fill' | 'fit' | 'fixed' | undefined {
577
+ if (typeof value !== 'string') return undefined;
578
+ const normalized = value.trim().toLowerCase();
579
+ if (normalized === 'fit' || normalized === 'fixed') return normalized;
580
+ if (
581
+ normalized === 'fill' ||
582
+ normalized === 'infinity' ||
583
+ normalized === 'infinite' ||
584
+ normalized === 'double.infinity' ||
585
+ normalized === '+infinity'
586
+ ) {
587
+ return 'fill';
588
+ }
589
+ return undefined;
590
+ }
591
+
592
+ function wantsFillWidth(props: Record<string, any>) {
593
+ return (
594
+ normalizeStackSizeMode(props.size?.width) === 'fill' ||
595
+ isFillDimensionValue(props.width)
596
+ );
597
+ }
598
+
599
+ function wantsFillHeight(props: Record<string, any>) {
600
+ return (
601
+ normalizeStackSizeMode(props.size?.height) === 'fill' ||
602
+ isFillDimensionValue(props.height)
603
+ );
604
+ }
605
+
578
606
  function normalizeStackSize(
579
607
  props: Record<string, any>,
580
608
  axis: 'vertical' | 'horizontal' | 'overlay'
581
609
  ) {
582
- const widthMode = String(props.size?.width ?? '').toLowerCase();
583
- const heightMode = String(props.size?.height ?? '').toLowerCase();
584
-
585
- const width =
586
- widthMode === 'fill' || widthMode === 'fit' || widthMode === 'fixed'
587
- ? widthMode
588
- : props.fit === 'expand' || props.width === 'infinity'
589
- ? 'fill'
590
- : isFixedDimensionValue(props.width)
591
- ? 'fixed'
592
- : axis === 'horizontal'
593
- ? 'fit'
594
- : 'fill';
595
- const height =
596
- heightMode === 'fill' || heightMode === 'fit' || heightMode === 'fixed'
597
- ? heightMode
598
- : props.fit === 'expand' || props.height === 'infinity'
599
- ? 'fill'
600
- : isFixedDimensionValue(props.height)
601
- ? 'fixed'
602
- : axis === 'vertical'
603
- ? 'fit'
604
- : 'fill';
610
+ const widthMode = normalizeStackSizeMode(props.size?.width);
611
+ const heightMode = normalizeStackSizeMode(props.size?.height);
612
+
613
+ const width = widthMode
614
+ ? widthMode
615
+ : props.fit === 'expand' || isFillDimensionValue(props.width)
616
+ ? 'fill'
617
+ : isFixedDimensionValue(props.width)
618
+ ? 'fixed'
619
+ : axis === 'horizontal'
620
+ ? 'fit'
621
+ : 'fill';
622
+ const height = heightMode
623
+ ? heightMode
624
+ : props.fit === 'expand' || isFillDimensionValue(props.height)
625
+ ? 'fill'
626
+ : isFixedDimensionValue(props.height)
627
+ ? 'fixed'
628
+ : axis === 'vertical'
629
+ ? 'fit'
630
+ : 'fill';
605
631
 
606
632
  return { width, height };
607
633
  }
@@ -687,8 +713,6 @@ function mergeStackProps(
687
713
  const fallbackAxisSource =
688
714
  axisSource === childProps ? parentProps : childProps;
689
715
 
690
- const parentPadding = spacingToInsets(parentProps.layout?.padding);
691
- const childPadding = spacingToInsets(childProps.layout?.padding);
692
716
  const parentMargin = spacingToInsets(parentProps.layout?.margin);
693
717
  const childMargin = spacingToInsets(childProps.layout?.margin);
694
718
 
@@ -705,12 +729,6 @@ function mergeStackProps(
705
729
  height: childProps.size?.height ?? parentProps.size?.height ?? 'fit',
706
730
  },
707
731
  layout: {
708
- padding: insetsToSpacing({
709
- top: parentPadding.top + childPadding.top,
710
- right: parentPadding.right + childPadding.right,
711
- bottom: parentPadding.bottom + childPadding.bottom,
712
- left: parentPadding.left + childPadding.left,
713
- }),
714
732
  margin: insetsToSpacing({
715
733
  top: parentMargin.top + childMargin.top,
716
734
  right: parentMargin.right + childMargin.right,
@@ -783,9 +801,6 @@ function normalizeStackLikeNode(
783
801
  childSpacing: Number(props.childSpacing ?? props.gap ?? props.spacing ?? 0),
784
802
  size,
785
803
  layout: {
786
- padding: normalizeStackSpacingConfig(
787
- props.layout?.padding ?? props.padding
788
- ),
789
804
  margin: normalizeStackSpacingConfig(props.layout?.margin ?? props.margin),
790
805
  },
791
806
  appearance: {
@@ -881,11 +896,12 @@ function resolveStackDimension(
881
896
  ) {
882
897
  const mode = props.size?.[key];
883
898
  const legacy = normalizeDimension(parseLayoutDimension(props[key]));
899
+ const fixedPx = parseLayoutDimension(props.size?.[`${key}Px`], true);
884
900
  const isBounded =
885
901
  key === 'width' ? axisBounds.widthBounded : axisBounds.heightBounded;
886
902
  if (mode === 'fill') return isBounded ? '100%' : legacy;
887
903
  if (mode === 'fit') return legacy;
888
- if (mode === 'fixed') return legacy;
904
+ if (mode === 'fixed') return legacy ?? fixedPx;
889
905
  if (legacy !== undefined) return legacy;
890
906
  if (key === 'width' && axis !== 'horizontal' && axisBounds.widthBounded)
891
907
  return '100%';
@@ -1485,19 +1501,38 @@ function buildWidget(
1485
1501
  break;
1486
1502
  }
1487
1503
  case 'slider': {
1504
+ const direction =
1505
+ props.direction === 'vertical' ? 'vertical' : 'horizontal';
1506
+ const interaction = props.interaction ?? {};
1507
+ const pageControl = props.pageControl ?? {};
1508
+ const sliderChildren = (childrenJson ?? []).filter(Boolean);
1488
1509
  node = (
1489
1510
  <SliderWidget
1490
1511
  id={json._internalId}
1491
- linkedTo={props.linkedTo}
1492
- direction={props.direction === 'vertical' ? 'vertical' : 'horizontal'}
1493
- autoPlay={props.autoPlay === true}
1494
- autoPlayDelay={Number(props.autoPlayDelay ?? 3000)}
1495
- loop={props.loop === true}
1496
- height={parseLayoutDimension(props.height)}
1497
- physics={props.physics}
1512
+ sliderProps={props}
1513
+ slidePropsList={sliderChildren.map(
1514
+ (child) => child?.properties ?? {}
1515
+ )}
1516
+ direction={direction}
1517
+ pageAlignment={
1518
+ props.pageAlignment === 'start' || props.pageAlignment === 'end'
1519
+ ? props.pageAlignment
1520
+ : 'center'
1521
+ }
1522
+ pageSpacing={Number(props.pageSpacing ?? 16)}
1523
+ pagePeek={Number(props.pagePeek ?? 16)}
1524
+ loop={interaction.loop === true}
1525
+ autoAdvance={interaction.autoAdvance === true}
1526
+ autoAdvanceIntervalMs={Number(
1527
+ interaction.autoAdvanceIntervalMs ?? 4000
1528
+ )}
1529
+ pageControlEnabled={pageControl.enabled !== false}
1530
+ pageControlPosition={
1531
+ pageControl.position === 'top' ? 'top' : 'bottom'
1532
+ }
1498
1533
  >
1499
- {(childrenJson ?? []).map((child, index) => (
1500
- <View key={`slider-${index}`} style={{ flex: 1 }}>
1534
+ {sliderChildren.map((child, index) => (
1535
+ <View key={`slider-${index}`}>
1501
1536
  {buildWidget(child, { ...params })}
1502
1537
  </View>
1503
1538
  ))}
@@ -1505,6 +1540,19 @@ function buildWidget(
1505
1540
  );
1506
1541
  break;
1507
1542
  }
1543
+ case 'slide': {
1544
+ node = buildWidget(
1545
+ {
1546
+ ...json,
1547
+ type: 'stack',
1548
+ properties: {
1549
+ ...(props || {}),
1550
+ },
1551
+ },
1552
+ { ...params }
1553
+ );
1554
+ break;
1555
+ }
1508
1556
  case 'pageview_indicator': {
1509
1557
  node = (
1510
1558
  <PageViewIndicator
@@ -1558,9 +1606,6 @@ function buildWidget(
1558
1606
  const applySpacing = !STACK_SPACE_DISTRIBUTIONS.has(
1559
1607
  String(props.distribution ?? 'start')
1560
1608
  );
1561
- const paddingInsets = stackSpacingToInsets(
1562
- props.layout?.padding ?? props.padding
1563
- );
1564
1609
  const width = resolveStackDimension(props, axis, 'width', pageAxisBounds);
1565
1610
  const height = resolveStackDimension(
1566
1611
  props,
@@ -1626,12 +1671,22 @@ function buildWidget(
1626
1671
  overflow: borderRadius > 0 ? 'hidden' : undefined,
1627
1672
  borderWidth: borderColor ? borderWidth : undefined,
1628
1673
  borderColor: borderColor ?? undefined,
1629
- ...insetsToStyle(paddingInsets),
1630
1674
  ...shadowStyle,
1631
1675
  };
1676
+ const stackContentSizeStyle = {
1677
+ width: width !== undefined ? '100%' : undefined,
1678
+ height: height !== undefined ? '100%' : undefined,
1679
+ };
1632
1680
 
1633
1681
  const content = isOverlay ? (
1634
- <View style={{ position: 'relative', minHeight: 0, minWidth: 0 }}>
1682
+ <View
1683
+ style={{
1684
+ position: 'relative',
1685
+ minHeight: 0,
1686
+ minWidth: 0,
1687
+ ...stackContentSizeStyle,
1688
+ }}
1689
+ >
1635
1690
  {(childrenJson ?? []).map((child, index) => {
1636
1691
  const alignment = parseOverlayGridAlignment(
1637
1692
  props.overlayAlignment ?? props.alignment
@@ -1667,6 +1722,7 @@ function buildWidget(
1667
1722
  alignItems: parseCrossAlignment(props.alignment),
1668
1723
  minHeight: 0,
1669
1724
  minWidth: 0,
1725
+ ...stackContentSizeStyle,
1670
1726
  }}
1671
1727
  >
1672
1728
  {(childrenJson ?? []).map((child, index) => (
@@ -1775,6 +1831,27 @@ function buildWidget(
1775
1831
  );
1776
1832
  }
1777
1833
 
1834
+ const shouldStretchCrossAxis =
1835
+ (params.parentFlexAxis === 'vertical' &&
1836
+ (type === 'selection_list' ||
1837
+ type === 'wheel_picker' ||
1838
+ wantsFillWidth(props))) ||
1839
+ (params.parentFlexAxis === 'horizontal' && wantsFillHeight(props));
1840
+
1841
+ if (shouldStretchCrossAxis) {
1842
+ node = (
1843
+ <View
1844
+ style={{
1845
+ alignSelf: 'stretch',
1846
+ minWidth: 0,
1847
+ minHeight: 0,
1848
+ }}
1849
+ >
1850
+ {node}
1851
+ </View>
1852
+ );
1853
+ }
1854
+
1778
1855
  if (params.allowFlexExpansion) {
1779
1856
  let shouldExpand = false;
1780
1857
  const parentAxis = params.parentFlexAxis ?? 'vertical';
@@ -2188,6 +2265,7 @@ function parseLayoutDimension(
2188
2265
 
2189
2266
  if (
2190
2267
  lowered === 'double.infinity' ||
2268
+ lowered === 'infinite' ||
2191
2269
  lowered === 'infinity' ||
2192
2270
  lowered === '+infinity'
2193
2271
  ) {
@@ -2626,6 +2704,16 @@ function SelectionList({
2626
2704
 
2627
2705
  const layout = properties.layout ?? 'column';
2628
2706
  const spacing = Number(properties.spacing ?? 8);
2707
+ const shouldOptionFillWidth = (option: Record<string, any>) => {
2708
+ if (layout === 'grid') return true;
2709
+ const child =
2710
+ option.child && typeof option.child === 'object'
2711
+ ? normalizeStackLikeNode(option.child)
2712
+ : option.child;
2713
+ const childProps =
2714
+ child && typeof child === 'object' ? child.properties ?? {} : {};
2715
+ return wantsFillWidth(childProps);
2716
+ };
2629
2717
 
2630
2718
  const renderItem = (option: Record<string, any>, index: number) => {
2631
2719
  const value = String(option.value ?? '');
@@ -2638,13 +2726,15 @@ function SelectionList({
2638
2726
  const borderColor = parseColor(styleProps.borderColor);
2639
2727
  const borderRadius = Number(styleProps.borderRadius ?? 8);
2640
2728
  const borderWidth = Number(styleProps.borderWidth ?? 1);
2729
+ const fillWidth = shouldOptionFillWidth(option);
2641
2730
 
2642
2731
  return (
2643
2732
  <Pressable
2644
2733
  key={`option-${index}`}
2645
2734
  onPress={() => toggleSelection(value)}
2646
2735
  style={{
2647
- width: '100%',
2736
+ width: fillWidth ? '100%' : undefined,
2737
+ alignSelf: fillWidth ? 'stretch' : 'flex-start',
2648
2738
  backgroundColor,
2649
2739
  borderColor,
2650
2740
  borderWidth,
@@ -2699,7 +2789,11 @@ function SelectionList({
2699
2789
  {options.map((option, index) => (
2700
2790
  <View
2701
2791
  key={`column-option-${index}`}
2702
- style={{ width: '100%', marginBottom: spacing }}
2792
+ style={{
2793
+ width: shouldOptionFillWidth(option) ? '100%' : undefined,
2794
+ alignSelf: shouldOptionFillWidth(option) ? 'stretch' : 'flex-start',
2795
+ marginBottom: spacing,
2796
+ }}
2703
2797
  >
2704
2798
  {renderItem(option, index)}
2705
2799
  </View>
@@ -3529,40 +3623,358 @@ function FakeProgressBar({
3529
3623
 
3530
3624
  const AnimatedCircle = Animated.createAnimatedComponent(Circle);
3531
3625
 
3626
+ type SliderSizeMode = 'fill' | 'fit' | 'fixed';
3627
+
3628
+ export function resolvePeekInsets(
3629
+ pageAlignment: 'start' | 'center' | 'end',
3630
+ pagePeek: number
3631
+ ): { leading: number; trailing: number } {
3632
+ const leading =
3633
+ pageAlignment === 'start'
3634
+ ? 0
3635
+ : pageAlignment === 'end'
3636
+ ? pagePeek * 2
3637
+ : pagePeek;
3638
+ const trailing =
3639
+ pageAlignment === 'end'
3640
+ ? 0
3641
+ : pageAlignment === 'start'
3642
+ ? pagePeek * 2
3643
+ : pagePeek;
3644
+ return { leading, trailing };
3645
+ }
3646
+
3647
+ function resolveSliderSizeMode(
3648
+ sliderProps: Record<string, any>,
3649
+ key: 'width' | 'height',
3650
+ fallback: SliderSizeMode
3651
+ ): SliderSizeMode {
3652
+ const raw = String(sliderProps?.size?.[key] ?? '').toLowerCase();
3653
+ if (raw === 'fill' || raw === 'fit' || raw === 'fixed') return raw;
3654
+ return sliderProps?.[key] !== undefined ? 'fixed' : fallback;
3655
+ }
3656
+
3657
+ function resolveSlideSizeMode(
3658
+ slideProps: Record<string, any>,
3659
+ key: 'width' | 'height'
3660
+ ): SliderSizeMode {
3661
+ const raw = String(slideProps?.size?.[key] ?? '').toLowerCase();
3662
+ if (raw === 'fill' || raw === 'fit' || raw === 'fixed') return raw;
3663
+ return key === 'width' ? 'fill' : 'fit';
3664
+ }
3665
+
3666
+ function toFinite(value: unknown): number | null {
3667
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
3668
+ if (typeof value === 'string') {
3669
+ const parsed = Number(value);
3670
+ if (Number.isFinite(parsed)) return parsed;
3671
+ }
3672
+ return null;
3673
+ }
3674
+
3675
+ export function resolveSliderContainerSize(args: {
3676
+ direction: 'horizontal' | 'vertical';
3677
+ widthMode: SliderSizeMode;
3678
+ heightMode: SliderSizeMode;
3679
+ availableWidth: number;
3680
+ availableHeight: number;
3681
+ activeSlideWidth: number;
3682
+ activeSlideHeight: number;
3683
+ fixedWidth: number | null;
3684
+ fixedHeight: number | null;
3685
+ leadingPeek: number;
3686
+ trailingPeek: number;
3687
+ showDots: boolean;
3688
+ }): {
3689
+ outerWidth: number | string;
3690
+ outerHeight: number | string;
3691
+ viewportWidth: number;
3692
+ viewportHeight: number;
3693
+ } {
3694
+ const dotsInset = args.showDots ? 8 + 6 + 8 : 0;
3695
+ const fitPrimary =
3696
+ args.direction === 'horizontal'
3697
+ ? args.activeSlideWidth
3698
+ : args.activeSlideHeight;
3699
+ const fitViewportPrimary = Math.max(
3700
+ 1,
3701
+ fitPrimary + args.leadingPeek + args.trailingPeek
3702
+ );
3703
+
3704
+ const viewportWidth =
3705
+ args.direction === 'horizontal'
3706
+ ? fitViewportPrimary
3707
+ : Math.max(1, args.activeSlideWidth);
3708
+ const viewportHeight =
3709
+ args.direction === 'vertical'
3710
+ ? fitViewportPrimary
3711
+ : Math.max(1, args.activeSlideHeight);
3712
+
3713
+ const fitOuterWidth = Math.min(
3714
+ Math.max(1, viewportWidth),
3715
+ Math.max(1, args.availableWidth || viewportWidth)
3716
+ );
3717
+ const fitOuterHeight = Math.max(1, viewportHeight + dotsInset);
3718
+
3719
+ const outerWidth =
3720
+ args.widthMode === 'fill'
3721
+ ? '100%'
3722
+ : args.widthMode === 'fixed'
3723
+ ? Math.max(1, args.fixedWidth ?? fitOuterWidth)
3724
+ : fitOuterWidth;
3725
+ const outerHeight =
3726
+ args.heightMode === 'fill'
3727
+ ? '100%'
3728
+ : args.heightMode === 'fixed'
3729
+ ? Math.max(1, args.fixedHeight ?? fitOuterHeight)
3730
+ : fitOuterHeight;
3731
+
3732
+ return {
3733
+ outerWidth,
3734
+ outerHeight,
3735
+ viewportWidth:
3736
+ args.widthMode === 'fit'
3737
+ ? fitOuterWidth
3738
+ : Math.max(1, args.availableWidth),
3739
+ viewportHeight:
3740
+ args.heightMode === 'fit'
3741
+ ? Math.max(1, fitOuterHeight - dotsInset)
3742
+ : Math.max(1, args.availableHeight || fitOuterHeight - dotsInset),
3743
+ };
3744
+ }
3745
+
3746
+ export function resolveSlidePageSize(args: {
3747
+ slideProps: Record<string, any>;
3748
+ direction: 'horizontal' | 'vertical';
3749
+ pagePrimary: number;
3750
+ crossSize: number | string;
3751
+ }): { width: number | string; height: number | string } {
3752
+ const widthMode = resolveSlideSizeMode(args.slideProps, 'width');
3753
+ const heightMode = resolveSlideSizeMode(args.slideProps, 'height');
3754
+ const fixedWidth = toFinite(
3755
+ parseLayoutDimension(
3756
+ args.slideProps.width ?? args.slideProps.size?.widthPx,
3757
+ true
3758
+ )
3759
+ );
3760
+ const fixedHeight = toFinite(
3761
+ parseLayoutDimension(
3762
+ args.slideProps.height ?? args.slideProps.size?.heightPx,
3763
+ true
3764
+ )
3765
+ );
3766
+ const width =
3767
+ widthMode === 'fixed'
3768
+ ? Math.max(1, fixedWidth ?? args.pagePrimary)
3769
+ : widthMode === 'fit'
3770
+ ? undefined
3771
+ : args.direction === 'horizontal'
3772
+ ? args.pagePrimary
3773
+ : args.crossSize;
3774
+ const height =
3775
+ heightMode === 'fixed'
3776
+ ? Math.max(1, fixedHeight ?? args.pagePrimary)
3777
+ : heightMode === 'fit'
3778
+ ? undefined
3779
+ : args.direction === 'vertical'
3780
+ ? args.pagePrimary
3781
+ : args.crossSize;
3782
+ return {
3783
+ width: width ?? 'auto',
3784
+ height: height ?? 'auto',
3785
+ };
3786
+ }
3787
+
3788
+ function resolveSliderBackgroundColor(
3789
+ sliderProps: Record<string, any>
3790
+ ): string | undefined {
3791
+ const extractColorCandidate = (input: unknown): unknown => {
3792
+ if (input === null || input === undefined) return undefined;
3793
+ if (typeof input === 'string' || typeof input === 'number') return input;
3794
+
3795
+ if (Array.isArray(input)) {
3796
+ for (const value of input) {
3797
+ const nested = extractColorCandidate(value);
3798
+ if (nested !== undefined) return nested;
3799
+ }
3800
+ return undefined;
3801
+ }
3802
+
3803
+ if (typeof input !== 'object') return undefined;
3804
+
3805
+ const source = input as Record<string, any>;
3806
+ if (
3807
+ Number.isFinite(Number(source.r)) &&
3808
+ Number.isFinite(Number(source.g)) &&
3809
+ Number.isFinite(Number(source.b))
3810
+ ) {
3811
+ const alphaRaw = Number.isFinite(Number(source.a)) ? Number(source.a) : 1;
3812
+ const alpha = Math.max(0, Math.min(1, alphaRaw));
3813
+ return `rgba(${Number(source.r)},${Number(source.g)},${Number(
3814
+ source.b
3815
+ )},${alpha})`;
3816
+ }
3817
+
3818
+ // If a gradient is provided for slider fill, use the first stop as fallback.
3819
+ if (
3820
+ String(source.type ?? '').toLowerCase() === 'gradient' &&
3821
+ Array.isArray(source.colors)
3822
+ ) {
3823
+ const first = extractColorCandidate(source.colors);
3824
+ if (first !== undefined) return first;
3825
+ }
3826
+
3827
+ const candidateKeys = [
3828
+ 'value',
3829
+ 'color',
3830
+ 'backgroundColor',
3831
+ 'hex',
3832
+ 'argb',
3833
+ 'rgba',
3834
+ 'raw',
3835
+ ];
3836
+ for (const key of candidateKeys) {
3837
+ const nested = extractColorCandidate(source[key]);
3838
+ if (nested !== undefined) return nested;
3839
+ }
3840
+
3841
+ return undefined;
3842
+ };
3843
+
3844
+ const parseSliderFillColor = (color: unknown): string => {
3845
+ const candidate = extractColorCandidate(color);
3846
+ if (candidate === undefined) return 'transparent';
3847
+
3848
+ if (typeof candidate === 'string') {
3849
+ const trimmed = candidate.trim();
3850
+ if (/^#[\da-fA-F]{8}$/.test(trimmed)) {
3851
+ // Dashboard stores alpha-enabled web hex as #RRGGBBAA.
3852
+ const rrggbbaa = trimmed.slice(1);
3853
+ const aarrggbb = `${rrggbbaa.slice(6, 8)}${rrggbbaa.slice(0, 6)}`;
3854
+ return parseColor(`0x${aarrggbb}`);
3855
+ }
3856
+ return parseColor(trimmed);
3857
+ }
3858
+
3859
+ if (typeof candidate === 'number') {
3860
+ return parseColor(candidate);
3861
+ }
3862
+
3863
+ if (typeof color === 'string') {
3864
+ const trimmed = color.trim();
3865
+ if (/^#[\da-fA-F]{8}$/.test(trimmed)) {
3866
+ // Dashboard stores alpha-enabled web hex as #RRGGBBAA.
3867
+ const rrggbbaa = trimmed.slice(1);
3868
+ const aarrggbb = `${rrggbbaa.slice(6, 8)}${rrggbbaa.slice(0, 6)}`;
3869
+ return parseColor(`0x${aarrggbb}`);
3870
+ }
3871
+ }
3872
+ return parseColor(candidate as string | number | null | undefined);
3873
+ };
3874
+
3875
+ const appearanceFill = sliderProps?.appearance?.fill;
3876
+ if (
3877
+ typeof appearanceFill === 'string' ||
3878
+ typeof appearanceFill === 'number'
3879
+ ) {
3880
+ return parseSliderFillColor(appearanceFill);
3881
+ }
3882
+ if (appearanceFill && typeof appearanceFill === 'object') {
3883
+ const type = String(appearanceFill.type ?? '').toLowerCase();
3884
+ if (type === 'color' || type === 'solid' || type === '') {
3885
+ const color = appearanceFill.value ?? appearanceFill.color;
3886
+ if (color !== undefined) return parseSliderFillColor(color);
3887
+ }
3888
+ }
3889
+
3890
+ const fill = sliderProps?.fill;
3891
+ if (typeof fill === 'string' || typeof fill === 'number') {
3892
+ return parseSliderFillColor(fill);
3893
+ }
3894
+ if (fill && typeof fill === 'object') {
3895
+ const type = String(fill.type ?? '').toLowerCase();
3896
+ if (type === 'color' || type === 'solid' || type === '') {
3897
+ const color = fill.value ?? fill.color;
3898
+ if (color !== undefined) return parseSliderFillColor(color);
3899
+ }
3900
+ }
3901
+
3902
+ const background = sliderProps?.background;
3903
+ if (background && typeof background === 'object') {
3904
+ const type = String(background.type ?? '').toLowerCase();
3905
+ if (type === 'color') {
3906
+ const color = background.value ?? background.color;
3907
+ if (color !== undefined) return parseSliderFillColor(color);
3908
+ }
3909
+ }
3910
+
3911
+ if (sliderProps?.backgroundColor !== undefined) {
3912
+ return parseSliderFillColor(sliderProps.backgroundColor);
3913
+ }
3914
+
3915
+ return undefined;
3916
+ }
3917
+
3532
3918
  function SliderWidget({
3533
3919
  id,
3534
- linkedTo,
3920
+ sliderProps,
3921
+ slidePropsList,
3535
3922
  direction,
3536
- autoPlay,
3537
- autoPlayDelay,
3923
+ pageAlignment,
3924
+ pageSpacing,
3925
+ pagePeek,
3538
3926
  loop,
3539
- height,
3540
- physics,
3927
+ autoAdvance,
3928
+ autoAdvanceIntervalMs,
3929
+ pageControlEnabled,
3930
+ pageControlPosition,
3541
3931
  children,
3542
3932
  }: {
3543
3933
  id?: string;
3544
- linkedTo?: string;
3934
+ sliderProps: Record<string, any>;
3935
+ slidePropsList: Record<string, any>[];
3545
3936
  direction: 'horizontal' | 'vertical';
3546
- autoPlay: boolean;
3547
- autoPlayDelay: number;
3937
+ pageAlignment: 'start' | 'center' | 'end';
3938
+ pageSpacing: number;
3939
+ pagePeek: number;
3548
3940
  loop: boolean;
3549
- height?: number | string;
3550
- physics?: string;
3941
+ autoAdvance: boolean;
3942
+ autoAdvanceIntervalMs: number;
3943
+ pageControlEnabled: boolean;
3944
+ pageControlPosition: 'top' | 'bottom';
3551
3945
  children: React.ReactNode[];
3552
3946
  }) {
3553
3947
  const registry = useSliderRegistry();
3554
- const pagerRef = React.useRef<PagerView>(null);
3948
+ const pagerRef = React.useRef<PagerViewHandle | null>(null);
3555
3949
  const pageLength = children.length;
3556
3950
  const pageCount = pageLength;
3557
3951
  const [currentPage, setCurrentPage] = React.useState(0);
3558
3952
  const timerRef = React.useRef<NodeJS.Timeout | null>(null);
3953
+ const [isInteracting, setIsInteracting] = React.useState(false);
3954
+ const idleTimerRef = React.useRef<NodeJS.Timeout | null>(null);
3955
+ const [availableSize, setAvailableSize] = React.useState({
3956
+ width: 1,
3957
+ height: 1,
3958
+ });
3959
+ const [activeSlideSize, setActiveSlideSize] = React.useState({
3960
+ width: 1,
3961
+ height: 1,
3962
+ });
3963
+
3964
+ const markInteracting = React.useCallback(() => {
3965
+ setIsInteracting(true);
3966
+ if (idleTimerRef.current) {
3967
+ clearTimeout(idleTimerRef.current);
3968
+ }
3969
+ idleTimerRef.current = setTimeout(() => setIsInteracting(false), 1400);
3970
+ }, []);
3559
3971
 
3560
3972
  React.useEffect(() => {
3561
3973
  setCurrentPage((prev) => clampSliderPage(prev, pageCount));
3562
3974
  }, [pageCount]);
3563
3975
 
3564
3976
  React.useEffect(() => {
3565
- if (autoPlay && !linkedTo && pageLength > 0) {
3977
+ if (autoAdvance && !isInteracting && pageLength > 1) {
3566
3978
  timerRef.current = setInterval(() => {
3567
3979
  setCurrentPage((prev) => {
3568
3980
  const safePrev = clampSliderPage(prev, pageCount);
@@ -3570,12 +3982,19 @@ function SliderWidget({
3570
3982
  if (loop) return 0;
3571
3983
  return safePrev;
3572
3984
  });
3573
- }, autoPlayDelay);
3985
+ }, autoAdvanceIntervalMs);
3574
3986
  }
3575
3987
  return () => {
3576
3988
  if (timerRef.current) clearInterval(timerRef.current);
3577
3989
  };
3578
- }, [autoPlay, autoPlayDelay, linkedTo, loop, pageCount, pageLength]);
3990
+ }, [
3991
+ autoAdvance,
3992
+ autoAdvanceIntervalMs,
3993
+ isInteracting,
3994
+ loop,
3995
+ pageCount,
3996
+ pageLength,
3997
+ ]);
3579
3998
 
3580
3999
  React.useEffect(() => {
3581
4000
  if (pagerRef.current && pageCount > 0) {
@@ -3589,42 +4008,205 @@ function SliderWidget({
3589
4008
  }
3590
4009
  }, [registry, id, currentPage, children.length]);
3591
4010
 
3592
- React.useEffect(() => {
3593
- if (!linkedTo || !registry) return;
3594
- return registry.getNotifier(linkedTo, (value) => {
3595
- const normalized = normalizeLoopIndex(value, pageLength);
3596
- const targetPage = clampSliderPage(normalized, pageCount);
3597
- setCurrentPage(targetPage);
3598
- if (pagerRef.current) {
3599
- pagerRef.current.setPage(targetPage);
3600
- }
3601
- });
3602
- }, [linkedTo, pageCount, pageLength, registry]);
3603
-
3604
4011
  if (pageLength === 0) return null;
3605
4012
 
4013
+ // Slider layout parity rules copied from Web Preview:
4014
+ // 1) leadingPeek = start?0 : end?pagePeek*2 : pagePeek
4015
+ // 2) trailingPeek = end?0 : start?pagePeek*2 : pagePeek
4016
+ // 3) pagePrimary = viewportPrimary - leadingPeek - trailingPeek (clamped >= 1)
4017
+ // 4) track offset = activeIndex * (pagePrimary + pageSpacing)
4018
+ // 5) fit parent mode follows active slide size but is clamped to container width
4019
+ // 6) viewport always clips track content (no root horizontal overflow)
4020
+ const { leading, trailing } = resolvePeekInsets(pageAlignment, pagePeek);
4021
+ const showDots = pageControlEnabled && pageLength > 1;
4022
+ const widthMode = resolveSliderSizeMode(sliderProps, 'width', 'fill');
4023
+ const heightMode = resolveSliderSizeMode(sliderProps, 'height', 'fit');
4024
+ const fixedWidth = toFinite(parseLayoutDimension(sliderProps.width, true));
4025
+ const fixedHeight = toFinite(parseLayoutDimension(sliderProps.height, true));
4026
+ const sliderBackgroundColor = resolveSliderBackgroundColor(sliderProps);
4027
+ const resolved = resolveSliderContainerSize({
4028
+ direction,
4029
+ widthMode,
4030
+ heightMode,
4031
+ availableWidth: availableSize.width,
4032
+ availableHeight: availableSize.height,
4033
+ activeSlideWidth: activeSlideSize.width,
4034
+ activeSlideHeight: activeSlideSize.height,
4035
+ fixedWidth,
4036
+ fixedHeight,
4037
+ leadingPeek: leading,
4038
+ trailingPeek: trailing,
4039
+ showDots,
4040
+ });
4041
+ const viewportPrimary =
4042
+ direction === 'horizontal'
4043
+ ? resolved.viewportWidth
4044
+ : resolved.viewportHeight;
4045
+ const pagePrimary = Math.max(1, viewportPrimary - leading - trailing);
4046
+
3606
4047
  return (
3607
- <PagerView
3608
- ref={pagerRef}
3609
- style={height === undefined ? { flex: 1 } : { height }}
3610
- orientation={direction}
3611
- scrollEnabled={physics !== 'never'}
3612
- initialPage={clampSliderPage(currentPage, pageCount)}
3613
- onPageSelected={(event: PagerViewOnPageSelectedEvent) => {
3614
- const page = event.nativeEvent.position;
3615
- const safePage = clampSliderPage(page, pageCount);
3616
- setCurrentPage(safePage);
3617
- if (registry && id) {
3618
- registry.update(id, safePage % pageLength, pageLength);
3619
- }
4048
+ <View
4049
+ onTouchStart={markInteracting}
4050
+ onTouchEnd={markInteracting}
4051
+ onTouchCancel={markInteracting}
4052
+ onPointerDown={markInteracting as any}
4053
+ onPointerUp={markInteracting as any}
4054
+ onLayout={(event) => {
4055
+ const nextWidth = Math.max(
4056
+ 1,
4057
+ Math.round(event.nativeEvent.layout.width)
4058
+ );
4059
+ const nextHeight = Math.max(
4060
+ 1,
4061
+ Math.round(event.nativeEvent.layout.height)
4062
+ );
4063
+ setAvailableSize((prev) =>
4064
+ prev.width === nextWidth && prev.height === nextHeight
4065
+ ? prev
4066
+ : { width: nextWidth, height: nextHeight }
4067
+ );
4068
+ }}
4069
+ style={{
4070
+ width: resolved.outerWidth,
4071
+ height: resolved.outerHeight,
4072
+ minWidth: 1,
4073
+ minHeight: 1,
4074
+ backgroundColor: sliderBackgroundColor,
4075
+ borderRadius: 0,
4076
+ overflow: 'visible',
3620
4077
  }}
3621
4078
  >
3622
- {Array.from({ length: pageCount }).map((_, index) => (
3623
- <View key={`slider-page-${index}`} style={{ flex: 1 }}>
3624
- {children[index % pageLength]}
4079
+ {showDots && pageControlPosition === 'top' ? (
4080
+ <View
4081
+ style={{
4082
+ marginBottom: 8,
4083
+ flexDirection: 'row',
4084
+ justifyContent: 'center',
4085
+ alignItems: 'center',
4086
+ }}
4087
+ >
4088
+ {Array.from({ length: pageLength }).map((_, index) => {
4089
+ const isActive = index === currentPage;
4090
+ return (
4091
+ <View
4092
+ key={`slider-dot-top-${index}`}
4093
+ style={{
4094
+ width: 6,
4095
+ height: 6,
4096
+ borderRadius: 3,
4097
+ marginHorizontal: 3,
4098
+ backgroundColor: isActive
4099
+ ? parseColor('0xFF111827')
4100
+ : parseColor('0xFFD1D5DB'),
4101
+ }}
4102
+ />
4103
+ );
4104
+ })}
3625
4105
  </View>
3626
- ))}
3627
- </PagerView>
4106
+ ) : null}
4107
+
4108
+ <View
4109
+ style={{
4110
+ overflow: 'hidden',
4111
+ ...(direction === 'horizontal'
4112
+ ? {
4113
+ width: resolved.viewportWidth,
4114
+ height: resolved.viewportHeight,
4115
+ paddingLeft: leading,
4116
+ paddingRight: trailing,
4117
+ }
4118
+ : {
4119
+ width: resolved.viewportWidth,
4120
+ height: resolved.viewportHeight,
4121
+ paddingTop: leading,
4122
+ paddingBottom: trailing,
4123
+ }),
4124
+ }}
4125
+ >
4126
+ <PagerView
4127
+ ref={pagerRef}
4128
+ style={{ width: '100%', height: '100%', minHeight: 1 }}
4129
+ orientation={direction}
4130
+ pageMargin={Math.max(0, pageSpacing)}
4131
+ initialPage={clampSliderPage(currentPage, pageCount)}
4132
+ onPageSelected={(event: PagerViewOnPageSelectedEvent) => {
4133
+ const page = event.nativeEvent.position;
4134
+ const safePage = clampSliderPage(page, pageCount);
4135
+ setCurrentPage(safePage);
4136
+ markInteracting();
4137
+ if (registry && id) {
4138
+ registry.update(id, safePage % pageLength, pageLength);
4139
+ }
4140
+ }}
4141
+ >
4142
+ {Array.from({ length: pageCount }).map((_, index) => (
4143
+ <View
4144
+ key={`slider-page-${index}`}
4145
+ onLayout={
4146
+ index === currentPage
4147
+ ? (event) => {
4148
+ const nextWidth = Math.max(
4149
+ 1,
4150
+ Math.round(event.nativeEvent.layout.width)
4151
+ );
4152
+ const nextHeight = Math.max(
4153
+ 1,
4154
+ Math.round(event.nativeEvent.layout.height)
4155
+ );
4156
+ setActiveSlideSize((prev) =>
4157
+ prev.width === nextWidth && prev.height === nextHeight
4158
+ ? prev
4159
+ : { width: nextWidth, height: nextHeight }
4160
+ );
4161
+ }
4162
+ : undefined
4163
+ }
4164
+ style={{
4165
+ alignSelf: 'flex-start',
4166
+ overflow: 'hidden',
4167
+ ...(resolveSlidePageSize({
4168
+ slideProps: slidePropsList[index] ?? {},
4169
+ direction,
4170
+ pagePrimary,
4171
+ crossSize: '100%',
4172
+ }) as ViewStyle),
4173
+ }}
4174
+ >
4175
+ {children[index % pageLength]}
4176
+ </View>
4177
+ ))}
4178
+ </PagerView>
4179
+ </View>
4180
+
4181
+ {showDots && pageControlPosition === 'bottom' ? (
4182
+ <View
4183
+ style={{
4184
+ marginTop: 8,
4185
+ flexDirection: 'row',
4186
+ justifyContent: 'center',
4187
+ alignItems: 'center',
4188
+ }}
4189
+ >
4190
+ {Array.from({ length: pageLength }).map((_, index) => {
4191
+ const isActive = index === currentPage;
4192
+ return (
4193
+ <View
4194
+ key={`slider-dot-bottom-${index}`}
4195
+ style={{
4196
+ width: 6,
4197
+ height: 6,
4198
+ borderRadius: 3,
4199
+ marginHorizontal: 3,
4200
+ backgroundColor: isActive
4201
+ ? parseColor('0xFF111827')
4202
+ : parseColor('0xFFD1D5DB'),
4203
+ }}
4204
+ />
4205
+ );
4206
+ })}
4207
+ </View>
4208
+ ) : null}
4209
+ </View>
3628
4210
  );
3629
4211
  }
3630
4212
 
@@ -3636,12 +4218,6 @@ function clampSliderPage(page: number, pageCount: number): number {
3636
4218
  return Math.floor(page);
3637
4219
  }
3638
4220
 
3639
- function normalizeLoopIndex(index: number, size: number): number {
3640
- if (size <= 0 || !Number.isFinite(index)) return 0;
3641
- const normalized = Math.floor(index) % size;
3642
- return normalized < 0 ? normalized + size : normalized;
3643
- }
3644
-
3645
4221
  function PageViewIndicator({
3646
4222
  linkedTo,
3647
4223
  activeColor,