flowboard-react 0.6.0 → 0.6.2

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